* No need to use is_null here
[lhc/web/wiklou.git] / languages / Language.php
1 <?php
2 /**
3 * @defgroup Language Language
4 *
5 * @file
6 * @ingroup Language
7 */
8
9 if( !defined( 'MEDIAWIKI' ) ) {
10 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
11 exit( 1 );
12 }
13
14 # Read language names
15 global $wgLanguageNames;
16 require_once( dirname(__FILE__) . '/Names.php' ) ;
17
18 global $wgInputEncoding, $wgOutputEncoding;
19
20 /**
21 * These are always UTF-8, they exist only for backwards compatibility
22 */
23 $wgInputEncoding = "UTF-8";
24 $wgOutputEncoding = "UTF-8";
25
26 if( function_exists( 'mb_strtoupper' ) ) {
27 mb_internal_encoding('UTF-8');
28 }
29
30 /**
31 * a fake language converter
32 *
33 * @ingroup Language
34 */
35 class FakeConverter {
36 var $mLang;
37 function FakeConverter($langobj) {$this->mLang = $langobj;}
38 function convert($t, $i) {return $t;}
39 function parserConvert($t, $p) {return $t;}
40 function getVariants() { return array( $this->mLang->getCode() ); }
41 function getPreferredVariant() {return $this->mLang->getCode(); }
42 function findVariantLink(&$l, &$n, $forTemplate = false) {}
43 function getExtraHashOptions() {return '';}
44 function getParsedTitle() {return '';}
45 function markNoConversion($text, $noParse=false) {return $text;}
46 function convertCategoryKey( $key ) {return $key; }
47 function convertLinkToAllVariants($text){ return array( $this->mLang->getCode() => $text); }
48 function armourMath($text){ return $text; }
49 }
50
51 /**
52 * Internationalisation code
53 * @ingroup Language
54 */
55 class Language {
56 var $mConverter, $mVariants, $mCode, $mLoaded = false;
57 var $mMagicExtensions = array(), $mMagicHookDone = false;
58
59 static public $mLocalisationKeys = array( 'fallback', 'namespaceNames',
60 'skinNames', 'mathNames',
61 'bookstoreList', 'magicWords', 'messages', 'rtl', 'digitTransformTable',
62 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
63 'defaultUserOptionOverrides', 'linkTrail', 'namespaceAliases',
64 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
65 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
66 'imageFiles'
67 );
68
69 static public $mMergeableMapKeys = array( 'messages', 'namespaceNames', 'mathNames',
70 'dateFormats', 'defaultUserOptionOverrides', 'magicWords', 'imageFiles' );
71
72 static public $mMergeableListKeys = array( 'extraUserToggles' );
73
74 static public $mMergeableAliasListKeys = array( 'specialPageAliases' );
75
76 static public $mLocalisationCache = array();
77 static public $mLangObjCache = array();
78
79 static public $mWeekdayMsgs = array(
80 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
81 'friday', 'saturday'
82 );
83
84 static public $mWeekdayAbbrevMsgs = array(
85 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
86 );
87
88 static public $mMonthMsgs = array(
89 'january', 'february', 'march', 'april', 'may_long', 'june',
90 'july', 'august', 'september', 'october', 'november',
91 'december'
92 );
93 static public $mMonthGenMsgs = array(
94 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
95 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
96 'december-gen'
97 );
98 static public $mMonthAbbrevMsgs = array(
99 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
100 'sep', 'oct', 'nov', 'dec'
101 );
102
103 static public $mIranianCalendarMonthMsgs = array(
104 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
105 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
106 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
107 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
108 );
109
110 static public $mHebrewCalendarMonthMsgs = array(
111 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
112 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
113 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
114 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
115 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
116 );
117
118 static public $mHebrewCalendarMonthGenMsgs = array(
119 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
120 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
121 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
122 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
123 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
124 );
125
126 static public $mHijriCalendarMonthMsgs = array(
127 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
128 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
129 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
130 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
131 );
132
133 /**
134 * Get a cached language object for a given language code
135 */
136 static function factory( $code ) {
137 if ( !isset( self::$mLangObjCache[$code] ) ) {
138 if( count( self::$mLangObjCache ) > 10 ) {
139 // Don't keep a billion objects around, that's stupid.
140 self::$mLangObjCache = array();
141 }
142 self::$mLangObjCache[$code] = self::newFromCode( $code );
143 }
144 return self::$mLangObjCache[$code];
145 }
146
147 /**
148 * Create a language object for a given language code
149 */
150 protected static function newFromCode( $code ) {
151 global $IP;
152 static $recursionLevel = 0;
153 if ( $code == 'en' ) {
154 $class = 'Language';
155 } else {
156 $class = 'Language' . str_replace( '-', '_', ucfirst( $code ) );
157 // Preload base classes to work around APC/PHP5 bug
158 if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
159 include_once("$IP/languages/classes/$class.deps.php");
160 }
161 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
162 include_once("$IP/languages/classes/$class.php");
163 }
164 }
165
166 if ( $recursionLevel > 5 ) {
167 throw new MWException( "Language fallback loop detected when creating class $class\n" );
168 }
169
170 if( ! class_exists( $class ) ) {
171 $fallback = Language::getFallbackFor( $code );
172 ++$recursionLevel;
173 $lang = Language::newFromCode( $fallback );
174 --$recursionLevel;
175 $lang->setCode( $code );
176 } else {
177 $lang = new $class;
178 }
179 return $lang;
180 }
181
182 function __construct() {
183 $this->mConverter = new FakeConverter($this);
184 // Set the code to the name of the descendant
185 if ( get_class( $this ) == 'Language' ) {
186 $this->mCode = 'en';
187 } else {
188 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
189 }
190 }
191
192 /**
193 * Reduce memory usage
194 */
195 function __destruct() {
196 foreach ( $this as $name => $value ) {
197 unset( $this->$name );
198 }
199 }
200
201 /**
202 * Hook which will be called if this is the content language.
203 * Descendants can use this to register hook functions or modify globals
204 */
205 function initContLang() {}
206
207 /**
208 * @deprecated Use User::getDefaultOptions()
209 * @return array
210 */
211 function getDefaultUserOptions() {
212 wfDeprecated( __METHOD__ );
213 return User::getDefaultOptions();
214 }
215
216 function getFallbackLanguageCode() {
217 return self::getFallbackFor( $this->mCode );
218 }
219
220 /**
221 * Exports $wgBookstoreListEn
222 * @return array
223 */
224 function getBookstoreList() {
225 $this->load();
226 return $this->bookstoreList;
227 }
228
229 /**
230 * @return array
231 */
232 function getNamespaces() {
233 $this->load();
234 return $this->namespaceNames;
235 }
236
237 /**
238 * A convenience function that returns the same thing as
239 * getNamespaces() except with the array values changed to ' '
240 * where it found '_', useful for producing output to be displayed
241 * e.g. in <select> forms.
242 *
243 * @return array
244 */
245 function getFormattedNamespaces() {
246 $ns = $this->getNamespaces();
247 foreach($ns as $k => $v) {
248 $ns[$k] = strtr($v, '_', ' ');
249 }
250 return $ns;
251 }
252
253 /**
254 * Get a namespace value by key
255 * <code>
256 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
257 * echo $mw_ns; // prints 'MediaWiki'
258 * </code>
259 *
260 * @param $index Int: the array key of the namespace to return
261 * @return mixed, string if the namespace value exists, otherwise false
262 */
263 function getNsText( $index ) {
264 $ns = $this->getNamespaces();
265 return isset( $ns[$index] ) ? $ns[$index] : false;
266 }
267
268 /**
269 * A convenience function that returns the same thing as
270 * getNsText() except with '_' changed to ' ', useful for
271 * producing output.
272 *
273 * @return array
274 */
275 function getFormattedNsText( $index ) {
276 $ns = $this->getNsText( $index );
277 return strtr($ns, '_', ' ');
278 }
279
280 /**
281 * Get a namespace key by value, case insensitive.
282 * Only matches namespace names for the current language, not the
283 * canonical ones defined in Namespace.php.
284 *
285 * @param $text String
286 * @return mixed An integer if $text is a valid value otherwise false
287 */
288 function getLocalNsIndex( $text ) {
289 $this->load();
290 $lctext = $this->lc($text);
291 return isset( $this->mNamespaceIds[$lctext] ) ? $this->mNamespaceIds[$lctext] : false;
292 }
293
294 /**
295 * Get a namespace key by value, case insensitive. Canonical namespace
296 * names override custom ones defined for the current language.
297 *
298 * @param $text String
299 * @return mixed An integer if $text is a valid value otherwise false
300 */
301 function getNsIndex( $text ) {
302 $this->load();
303 $lctext = $this->lc($text);
304 if( ( $ns = MWNamespace::getCanonicalIndex( $lctext ) ) !== null ) return $ns;
305 return isset( $this->mNamespaceIds[$lctext] ) ? $this->mNamespaceIds[$lctext] : false;
306 }
307
308 /**
309 * short names for language variants used for language conversion links.
310 *
311 * @param $code String
312 * @return string
313 */
314 function getVariantname( $code ) {
315 return $this->getMessageFromDB( "variantname-$code" );
316 }
317
318 function specialPage( $name ) {
319 $aliases = $this->getSpecialPageAliases();
320 if ( isset( $aliases[$name][0] ) ) {
321 $name = $aliases[$name][0];
322 }
323 return $this->getNsText(NS_SPECIAL) . ':' . $name;
324 }
325
326 function getQuickbarSettings() {
327 return array(
328 $this->getMessage( 'qbsettings-none' ),
329 $this->getMessage( 'qbsettings-fixedleft' ),
330 $this->getMessage( 'qbsettings-fixedright' ),
331 $this->getMessage( 'qbsettings-floatingleft' ),
332 $this->getMessage( 'qbsettings-floatingright' )
333 );
334 }
335
336 function getSkinNames() {
337 $this->load();
338 return $this->skinNames;
339 }
340
341 function getMathNames() {
342 $this->load();
343 return $this->mathNames;
344 }
345
346 function getDatePreferences() {
347 $this->load();
348 return $this->datePreferences;
349 }
350
351 function getDateFormats() {
352 $this->load();
353 return $this->dateFormats;
354 }
355
356 function getDefaultDateFormat() {
357 $this->load();
358 return $this->defaultDateFormat;
359 }
360
361 function getDatePreferenceMigrationMap() {
362 $this->load();
363 return $this->datePreferenceMigrationMap;
364 }
365
366 function getImageFile( $image ) {
367 $this->load();
368 return $this->imageFiles[$image];
369 }
370
371 function getDefaultUserOptionOverrides() {
372 $this->load();
373 # XXX - apparently some languageas get empty arrays, didn't get to it yet -- midom
374 if (is_array($this->defaultUserOptionOverrides)) {
375 return $this->defaultUserOptionOverrides;
376 } else {
377 return array();
378 }
379 }
380
381 function getExtraUserToggles() {
382 $this->load();
383 return $this->extraUserToggles;
384 }
385
386 function getUserToggle( $tog ) {
387 return $this->getMessageFromDB( "tog-$tog" );
388 }
389
390 /**
391 * Get language names, indexed by code.
392 * If $customisedOnly is true, only returns codes with a messages file
393 */
394 public static function getLanguageNames( $customisedOnly = false ) {
395 global $wgLanguageNames, $wgExtraLanguageNames;
396 $allNames = $wgExtraLanguageNames + $wgLanguageNames;
397 if ( !$customisedOnly ) {
398 return $allNames;
399 }
400
401 global $IP;
402 $names = array();
403 $dir = opendir( "$IP/languages/messages" );
404 while( false !== ( $file = readdir( $dir ) ) ) {
405 $m = array();
406 if( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $file, $m ) ) {
407 $code = str_replace( '_', '-', strtolower( $m[1] ) );
408 if ( isset( $allNames[$code] ) ) {
409 $names[$code] = $allNames[$code];
410 }
411 }
412 }
413 closedir( $dir );
414 return $names;
415 }
416
417 /**
418 * Get a message from the MediaWiki namespace.
419 *
420 * @param $msg String: message name
421 * @return string
422 */
423 function getMessageFromDB( $msg ) {
424 return wfMsgExt( $msg, array( 'parsemag', 'language' => $this ) );
425 }
426
427 function getLanguageName( $code ) {
428 $names = self::getLanguageNames();
429 if ( !array_key_exists( $code, $names ) ) {
430 return '';
431 }
432 return $names[$code];
433 }
434
435 function getMonthName( $key ) {
436 return $this->getMessageFromDB( self::$mMonthMsgs[$key-1] );
437 }
438
439 function getMonthNameGen( $key ) {
440 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key-1] );
441 }
442
443 function getMonthAbbreviation( $key ) {
444 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key-1] );
445 }
446
447 function getWeekdayName( $key ) {
448 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key-1] );
449 }
450
451 function getWeekdayAbbreviation( $key ) {
452 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key-1] );
453 }
454
455 function getIranianCalendarMonthName( $key ) {
456 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key-1] );
457 }
458
459 function getHebrewCalendarMonthName( $key ) {
460 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key-1] );
461 }
462
463 function getHebrewCalendarMonthNameGen( $key ) {
464 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key-1] );
465 }
466
467 function getHijriCalendarMonthName( $key ) {
468 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key-1] );
469 }
470
471 /**
472 * Used by date() and time() to adjust the time output.
473 *
474 * @param $ts Int the time in date('YmdHis') format
475 * @param $tz Mixed: adjust the time by this amount (default false, mean we
476 * get user timecorrection setting)
477 * @return int
478 */
479 function userAdjust( $ts, $tz = false ) {
480 global $wgUser, $wgLocalTZoffset;
481
482 if ( $tz === false ) {
483 $tz = $wgUser->getOption( 'timecorrection' );
484 }
485
486 $data = explode( '|', $tz, 3 );
487
488 if ( $data[0] == 'ZoneInfo' ) {
489 if ( function_exists( 'timezone_open' ) && @timezone_open( $data[2] ) !== false ) {
490 $date = date_create( $ts, timezone_open( 'UTC' ) );
491 date_timezone_set( $date, timezone_open( $data[2] ) );
492 $date = date_format( $date, 'YmdHis' );
493 return $date;
494 }
495 # Unrecognized timezone, default to 'Offset' with the stored offset.
496 $data[0] = 'Offset';
497 }
498
499 $minDiff = 0;
500 if ( $data[0] == 'System' || $tz == '' ) {
501 # Global offset in minutes.
502 if( isset($wgLocalTZoffset) ) $minDiff = $wgLocalTZoffset;
503 } else if ( $data[0] == 'Offset' ) {
504 $minDiff = intval( $data[1] );
505 } else {
506 $data = explode( ':', $tz );
507 if( count( $data ) == 2 ) {
508 $data[0] = intval( $data[0] );
509 $data[1] = intval( $data[1] );
510 $minDiff = abs( $data[0] ) * 60 + $data[1];
511 if ( $data[0] < 0 ) $minDiff = -$minDiff;
512 } else {
513 $minDiff = intval( $data[0] ) * 60;
514 }
515 }
516
517 # No difference ? Return time unchanged
518 if ( 0 == $minDiff ) return $ts;
519
520 wfSuppressWarnings(); // E_STRICT system time bitching
521 # Generate an adjusted date; take advantage of the fact that mktime
522 # will normalize out-of-range values so we don't have to split $minDiff
523 # into hours and minutes.
524 $t = mktime( (
525 (int)substr( $ts, 8, 2) ), # Hours
526 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
527 (int)substr( $ts, 12, 2 ), # Seconds
528 (int)substr( $ts, 4, 2 ), # Month
529 (int)substr( $ts, 6, 2 ), # Day
530 (int)substr( $ts, 0, 4 ) ); #Year
531
532 $date = date( 'YmdHis', $t );
533 wfRestoreWarnings();
534
535 return $date;
536 }
537
538 /**
539 * This is a workalike of PHP's date() function, but with better
540 * internationalisation, a reduced set of format characters, and a better
541 * escaping format.
542 *
543 * Supported format characters are dDjlNwzWFmMntLYyaAgGhHiscrU. See the
544 * PHP manual for definitions. There are a number of extensions, which
545 * start with "x":
546 *
547 * xn Do not translate digits of the next numeric format character
548 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
549 * xr Use roman numerals for the next numeric format character
550 * xh Use hebrew numerals for the next numeric format character
551 * xx Literal x
552 * xg Genitive month name
553 *
554 * xij j (day number) in Iranian calendar
555 * xiF F (month name) in Iranian calendar
556 * xin n (month number) in Iranian calendar
557 * xiY Y (full year) in Iranian calendar
558 *
559 * xjj j (day number) in Hebrew calendar
560 * xjF F (month name) in Hebrew calendar
561 * xjt t (days in month) in Hebrew calendar
562 * xjx xg (genitive month name) in Hebrew calendar
563 * xjn n (month number) in Hebrew calendar
564 * xjY Y (full year) in Hebrew calendar
565 *
566 * xmj j (day number) in Hijri calendar
567 * xmF F (month name) in Hijri calendar
568 * xmn n (month number) in Hijri calendar
569 * xmY Y (full year) in Hijri calendar
570 *
571 * xkY Y (full year) in Thai solar calendar. Months and days are
572 * identical to the Gregorian calendar
573 *
574 * Characters enclosed in double quotes will be considered literal (with
575 * the quotes themselves removed). Unmatched quotes will be considered
576 * literal quotes. Example:
577 *
578 * "The month is" F => The month is January
579 * i's" => 20'11"
580 *
581 * Backslash escaping is also supported.
582 *
583 * Input timestamp is assumed to be pre-normalized to the desired local
584 * time zone, if any.
585 *
586 * @param $format String
587 * @param $ts String: 14-character timestamp
588 * YYYYMMDDHHMMSS
589 * 01234567890123
590 */
591 function sprintfDate( $format, $ts ) {
592 $s = '';
593 $raw = false;
594 $roman = false;
595 $hebrewNum = false;
596 $unix = false;
597 $rawToggle = false;
598 $iranian = false;
599 $hebrew = false;
600 $hijri = false;
601 $thai = false;
602 for ( $p = 0; $p < strlen( $format ); $p++ ) {
603 $num = false;
604 $code = $format[$p];
605 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
606 $code .= $format[++$p];
607 }
608
609 if ( ( $code === 'xi' || $code == 'xj' || $code == 'xk' || $code == 'xm' ) && $p < strlen( $format ) - 1 ) {
610 $code .= $format[++$p];
611 }
612
613 switch ( $code ) {
614 case 'xx':
615 $s .= 'x';
616 break;
617 case 'xn':
618 $raw = true;
619 break;
620 case 'xN':
621 $rawToggle = !$rawToggle;
622 break;
623 case 'xr':
624 $roman = true;
625 break;
626 case 'xh':
627 $hebrewNum = true;
628 break;
629 case 'xg':
630 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
631 break;
632 case 'xjx':
633 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
634 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
635 break;
636 case 'd':
637 $num = substr( $ts, 6, 2 );
638 break;
639 case 'D':
640 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
641 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) + 1 );
642 break;
643 case 'j':
644 $num = intval( substr( $ts, 6, 2 ) );
645 break;
646 case 'xij':
647 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
648 $num = $iranian[2];
649 break;
650 case 'xmj':
651 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
652 $num = $hijri[2];
653 break;
654 case 'xjj':
655 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
656 $num = $hebrew[2];
657 break;
658 case 'l':
659 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
660 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) + 1 );
661 break;
662 case 'N':
663 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
664 $w = gmdate( 'w', $unix );
665 $num = $w ? $w : 7;
666 break;
667 case 'w':
668 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
669 $num = gmdate( 'w', $unix );
670 break;
671 case 'z':
672 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
673 $num = gmdate( 'z', $unix );
674 break;
675 case 'W':
676 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
677 $num = gmdate( 'W', $unix );
678 break;
679 case 'F':
680 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
681 break;
682 case 'xiF':
683 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
684 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
685 break;
686 case 'xmF':
687 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
688 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
689 break;
690 case 'xjF':
691 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
692 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
693 break;
694 case 'm':
695 $num = substr( $ts, 4, 2 );
696 break;
697 case 'M':
698 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
699 break;
700 case 'n':
701 $num = intval( substr( $ts, 4, 2 ) );
702 break;
703 case 'xin':
704 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
705 $num = $iranian[1];
706 break;
707 case 'xmn':
708 if ( !$hijri ) $hijri = self::tsToHijri ( $ts );
709 $num = $hijri[1];
710 break;
711 case 'xjn':
712 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
713 $num = $hebrew[1];
714 break;
715 case 't':
716 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
717 $num = gmdate( 't', $unix );
718 break;
719 case 'xjt':
720 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
721 $num = $hebrew[3];
722 break;
723 case 'L':
724 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
725 $num = gmdate( 'L', $unix );
726 break;
727 case 'Y':
728 $num = substr( $ts, 0, 4 );
729 break;
730 case 'xiY':
731 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
732 $num = $iranian[0];
733 break;
734 case 'xmY':
735 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
736 $num = $hijri[0];
737 break;
738 case 'xjY':
739 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
740 $num = $hebrew[0];
741 break;
742 case 'xkY':
743 if ( !$thai ) $thai = self::tsToThai( $ts );
744 $num = $thai[0];
745 break;
746 case 'y':
747 $num = substr( $ts, 2, 2 );
748 break;
749 case 'a':
750 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
751 break;
752 case 'A':
753 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
754 break;
755 case 'g':
756 $h = substr( $ts, 8, 2 );
757 $num = $h % 12 ? $h % 12 : 12;
758 break;
759 case 'G':
760 $num = intval( substr( $ts, 8, 2 ) );
761 break;
762 case 'h':
763 $h = substr( $ts, 8, 2 );
764 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
765 break;
766 case 'H':
767 $num = substr( $ts, 8, 2 );
768 break;
769 case 'i':
770 $num = substr( $ts, 10, 2 );
771 break;
772 case 's':
773 $num = substr( $ts, 12, 2 );
774 break;
775 case 'c':
776 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
777 $s .= gmdate( 'c', $unix );
778 break;
779 case 'r':
780 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
781 $s .= gmdate( 'r', $unix );
782 break;
783 case 'U':
784 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
785 $num = $unix;
786 break;
787 case '\\':
788 # Backslash escaping
789 if ( $p < strlen( $format ) - 1 ) {
790 $s .= $format[++$p];
791 } else {
792 $s .= '\\';
793 }
794 break;
795 case '"':
796 # Quoted literal
797 if ( $p < strlen( $format ) - 1 ) {
798 $endQuote = strpos( $format, '"', $p + 1 );
799 if ( $endQuote === false ) {
800 # No terminating quote, assume literal "
801 $s .= '"';
802 } else {
803 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
804 $p = $endQuote;
805 }
806 } else {
807 # Quote at end of string, assume literal "
808 $s .= '"';
809 }
810 break;
811 default:
812 $s .= $format[$p];
813 }
814 if ( $num !== false ) {
815 if ( $rawToggle || $raw ) {
816 $s .= $num;
817 $raw = false;
818 } elseif ( $roman ) {
819 $s .= self::romanNumeral( $num );
820 $roman = false;
821 } elseif( $hebrewNum ) {
822 $s .= self::hebrewNumeral( $num );
823 $hebrewNum = false;
824 } else {
825 $s .= $this->formatNum( $num, true );
826 }
827 $num = false;
828 }
829 }
830 return $s;
831 }
832
833 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
834 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
835 /**
836 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
837 * Gregorian dates to Iranian dates. Originally written in C, it
838 * is released under the terms of GNU Lesser General Public
839 * License. Conversion to PHP was performed by Niklas Laxström.
840 *
841 * Link: http://www.farsiweb.info/jalali/jalali.c
842 */
843 private static function tsToIranian( $ts ) {
844 $gy = substr( $ts, 0, 4 ) -1600;
845 $gm = substr( $ts, 4, 2 ) -1;
846 $gd = substr( $ts, 6, 2 ) -1;
847
848 # Days passed from the beginning (including leap years)
849 $gDayNo = 365*$gy
850 + floor(($gy+3) / 4)
851 - floor(($gy+99) / 100)
852 + floor(($gy+399) / 400);
853
854
855 // Add days of the past months of this year
856 for( $i = 0; $i < $gm; $i++ ) {
857 $gDayNo += self::$GREG_DAYS[$i];
858 }
859
860 // Leap years
861 if ( $gm > 1 && (($gy%4===0 && $gy%100!==0 || ($gy%400==0)))) {
862 $gDayNo++;
863 }
864
865 // Days passed in current month
866 $gDayNo += $gd;
867
868 $jDayNo = $gDayNo - 79;
869
870 $jNp = floor($jDayNo / 12053);
871 $jDayNo %= 12053;
872
873 $jy = 979 + 33*$jNp + 4*floor($jDayNo/1461);
874 $jDayNo %= 1461;
875
876 if ( $jDayNo >= 366 ) {
877 $jy += floor(($jDayNo-1)/365);
878 $jDayNo = floor(($jDayNo-1)%365);
879 }
880
881 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
882 $jDayNo -= self::$IRANIAN_DAYS[$i];
883 }
884
885 $jm= $i+1;
886 $jd= $jDayNo+1;
887
888 return array($jy, $jm, $jd);
889 }
890 /**
891 * Converting Gregorian dates to Hijri dates.
892 *
893 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
894 *
895 * @link http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
896 */
897 private static function tsToHijri ( $ts ) {
898 $year = substr( $ts, 0, 4 );
899 $month = substr( $ts, 4, 2 );
900 $day = substr( $ts, 6, 2 );
901
902 $zyr = $year;
903 $zd=$day;
904 $zm=$month;
905 $zy=$zyr;
906
907
908
909 if (($zy>1582)||(($zy==1582)&&($zm>10))||(($zy==1582)&&($zm==10)&&($zd>14)))
910 {
911
912
913 $zjd=(int)((1461*($zy + 4800 + (int)( ($zm-14) /12) ))/4) + (int)((367*($zm-2-12*((int)(($zm-14)/12))))/12)-(int)((3*(int)(( ($zy+4900+(int)(($zm-14)/12))/100)))/4)+$zd-32075;
914 }
915 else
916 {
917 $zjd = 367*$zy-(int)((7*($zy+5001+(int)(($zm-9)/7)))/4)+(int)((275*$zm)/9)+$zd+1729777;
918 }
919
920 $zl=$zjd-1948440+10632;
921 $zn=(int)(($zl-1)/10631);
922 $zl=$zl-10631*$zn+354;
923 $zj=((int)((10985-$zl)/5316))*((int)((50*$zl)/17719))+((int)($zl/5670))*((int)((43*$zl)/15238));
924 $zl=$zl-((int)((30-$zj)/15))*((int)((17719*$zj)/50))-((int)($zj/16))*((int)((15238*$zj)/43))+29;
925 $zm=(int)((24*$zl)/709);
926 $zd=$zl-(int)((709*$zm)/24);
927 $zy=30*$zn+$zj-30;
928
929 return array ($zy, $zm, $zd);
930 }
931
932 /**
933 * Converting Gregorian dates to Hebrew dates.
934 *
935 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
936 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
937 * to translate the relevant functions into PHP and release them under
938 * GNU GPL.
939 *
940 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
941 * and Adar II is 14. In a non-leap year, Adar is 6.
942 */
943 private static function tsToHebrew( $ts ) {
944 # Parse date
945 $year = substr( $ts, 0, 4 );
946 $month = substr( $ts, 4, 2 );
947 $day = substr( $ts, 6, 2 );
948
949 # Calculate Hebrew year
950 $hebrewYear = $year + 3760;
951
952 # Month number when September = 1, August = 12
953 $month += 4;
954 if( $month > 12 ) {
955 # Next year
956 $month -= 12;
957 $year++;
958 $hebrewYear++;
959 }
960
961 # Calculate day of year from 1 September
962 $dayOfYear = $day;
963 for( $i = 1; $i < $month; $i++ ) {
964 if( $i == 6 ) {
965 # February
966 $dayOfYear += 28;
967 # Check if the year is leap
968 if( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
969 $dayOfYear++;
970 }
971 } elseif( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
972 $dayOfYear += 30;
973 } else {
974 $dayOfYear += 31;
975 }
976 }
977
978 # Calculate the start of the Hebrew year
979 $start = self::hebrewYearStart( $hebrewYear );
980
981 # Calculate next year's start
982 if( $dayOfYear <= $start ) {
983 # Day is before the start of the year - it is the previous year
984 # Next year's start
985 $nextStart = $start;
986 # Previous year
987 $year--;
988 $hebrewYear--;
989 # Add days since previous year's 1 September
990 $dayOfYear += 365;
991 if( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
992 # Leap year
993 $dayOfYear++;
994 }
995 # Start of the new (previous) year
996 $start = self::hebrewYearStart( $hebrewYear );
997 } else {
998 # Next year's start
999 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1000 }
1001
1002 # Calculate Hebrew day of year
1003 $hebrewDayOfYear = $dayOfYear - $start;
1004
1005 # Difference between year's days
1006 $diff = $nextStart - $start;
1007 # Add 12 (or 13 for leap years) days to ignore the difference between
1008 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1009 # difference is only about the year type
1010 if( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1011 $diff += 13;
1012 } else {
1013 $diff += 12;
1014 }
1015
1016 # Check the year pattern, and is leap year
1017 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1018 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1019 # and non-leap years
1020 $yearPattern = $diff % 30;
1021 # Check if leap year
1022 $isLeap = $diff >= 30;
1023
1024 # Calculate day in the month from number of day in the Hebrew year
1025 # Don't check Adar - if the day is not in Adar, we will stop before;
1026 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1027 $hebrewDay = $hebrewDayOfYear;
1028 $hebrewMonth = 1;
1029 $days = 0;
1030 while( $hebrewMonth <= 12 ) {
1031 # Calculate days in this month
1032 if( $isLeap && $hebrewMonth == 6 ) {
1033 # Adar in a leap year
1034 if( $isLeap ) {
1035 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1036 $days = 30;
1037 if( $hebrewDay <= $days ) {
1038 # Day in Adar I
1039 $hebrewMonth = 13;
1040 } else {
1041 # Subtract the days of Adar I
1042 $hebrewDay -= $days;
1043 # Try Adar II
1044 $days = 29;
1045 if( $hebrewDay <= $days ) {
1046 # Day in Adar II
1047 $hebrewMonth = 14;
1048 }
1049 }
1050 }
1051 } elseif( $hebrewMonth == 2 && $yearPattern == 2 ) {
1052 # Cheshvan in a complete year (otherwise as the rule below)
1053 $days = 30;
1054 } elseif( $hebrewMonth == 3 && $yearPattern == 0 ) {
1055 # Kislev in an incomplete year (otherwise as the rule below)
1056 $days = 29;
1057 } else {
1058 # Odd months have 30 days, even have 29
1059 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1060 }
1061 if( $hebrewDay <= $days ) {
1062 # In the current month
1063 break;
1064 } else {
1065 # Subtract the days of the current month
1066 $hebrewDay -= $days;
1067 # Try in the next month
1068 $hebrewMonth++;
1069 }
1070 }
1071
1072 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1073 }
1074
1075 /**
1076 * This calculates the Hebrew year start, as days since 1 September.
1077 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1078 * Used for Hebrew date.
1079 */
1080 private static function hebrewYearStart( $year ) {
1081 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1082 $b = intval( ( $year - 1 ) % 4 );
1083 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1084 if( $m < 0 ) {
1085 $m--;
1086 }
1087 $Mar = intval( $m );
1088 if( $m < 0 ) {
1089 $m++;
1090 }
1091 $m -= $Mar;
1092
1093 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7);
1094 if( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1095 $Mar++;
1096 } else if( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1097 $Mar += 2;
1098 } else if( $c == 2 || $c == 4 || $c == 6 ) {
1099 $Mar++;
1100 }
1101
1102 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1103 return $Mar;
1104 }
1105
1106 /**
1107 * Algorithm to convert Gregorian dates to Thai solar dates.
1108 *
1109 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1110 *
1111 * @param $ts String: 14-character timestamp
1112 * @return array converted year, month, day
1113 */
1114 private static function tsToThai( $ts ) {
1115 $gy = substr( $ts, 0, 4 );
1116 $gm = substr( $ts, 4, 2 );
1117 $gd = substr( $ts, 6, 2 );
1118
1119 # Add 543 years to the Gregorian calendar
1120 # Months and days are identical
1121 $gy_thai = $gy + 543;
1122
1123 return array( $gy_thai, $gm, $gd );
1124 }
1125
1126
1127 /**
1128 * Roman number formatting up to 3000
1129 */
1130 static function romanNumeral( $num ) {
1131 static $table = array(
1132 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1133 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1134 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1135 array( '', 'M', 'MM', 'MMM' )
1136 );
1137
1138 $num = intval( $num );
1139 if ( $num > 3000 || $num <= 0 ) {
1140 return $num;
1141 }
1142
1143 $s = '';
1144 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1145 if ( $num >= $pow10 ) {
1146 $s .= $table[$i][floor($num / $pow10)];
1147 }
1148 $num = $num % $pow10;
1149 }
1150 return $s;
1151 }
1152
1153 /**
1154 * Hebrew Gematria number formatting up to 9999
1155 */
1156 static function hebrewNumeral( $num ) {
1157 static $table = array(
1158 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1159 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1160 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1161 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1162 );
1163
1164 $num = intval( $num );
1165 if ( $num > 9999 || $num <= 0 ) {
1166 return $num;
1167 }
1168
1169 $s = '';
1170 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1171 if ( $num >= $pow10 ) {
1172 if ( $num == 15 || $num == 16 ) {
1173 $s .= $table[0][9] . $table[0][$num - 9];
1174 $num = 0;
1175 } else {
1176 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1177 if( $pow10 == 1000 ) {
1178 $s .= "'";
1179 }
1180 }
1181 }
1182 $num = $num % $pow10;
1183 }
1184 if( strlen( $s ) == 2 ) {
1185 $str = $s . "'";
1186 } else {
1187 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1188 $str .= substr( $s, strlen( $s ) - 2, 2 );
1189 }
1190 $start = substr( $str, 0, strlen( $str ) - 2 );
1191 $end = substr( $str, strlen( $str ) - 2 );
1192 switch( $end ) {
1193 case 'כ':
1194 $str = $start . 'ך';
1195 break;
1196 case 'מ':
1197 $str = $start . 'ם';
1198 break;
1199 case 'נ':
1200 $str = $start . 'ן';
1201 break;
1202 case 'פ':
1203 $str = $start . 'ף';
1204 break;
1205 case 'צ':
1206 $str = $start . 'ץ';
1207 break;
1208 }
1209 return $str;
1210 }
1211
1212 /**
1213 * This is meant to be used by time(), date(), and timeanddate() to get
1214 * the date preference they're supposed to use, it should be used in
1215 * all children.
1216 *
1217 *<code>
1218 * function timeanddate([...], $format = true) {
1219 * $datePreference = $this->dateFormat($format);
1220 * [...]
1221 * }
1222 *</code>
1223 *
1224 * @param $usePrefs Mixed: if true, the user's preference is used
1225 * if false, the site/language default is used
1226 * if int/string, assumed to be a format.
1227 * @return string
1228 */
1229 function dateFormat( $usePrefs = true ) {
1230 global $wgUser;
1231
1232 if( is_bool( $usePrefs ) ) {
1233 if( $usePrefs ) {
1234 $datePreference = $wgUser->getDatePreference();
1235 } else {
1236 $options = User::getDefaultOptions();
1237 $datePreference = (string)$options['date'];
1238 }
1239 } else {
1240 $datePreference = (string)$usePrefs;
1241 }
1242
1243 // return int
1244 if( $datePreference == '' ) {
1245 return 'default';
1246 }
1247
1248 return $datePreference;
1249 }
1250
1251 /**
1252 * @param $ts Mixed: the time format which needs to be turned into a
1253 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1254 * @param $adj Bool: whether to adjust the time output according to the
1255 * user configured offset ($timecorrection)
1256 * @param $format Mixed: true to use user's date format preference
1257 * @param $timecorrection String: the time offset as returned by
1258 * validateTimeZone() in Special:Preferences
1259 * @return string
1260 */
1261 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1262 $this->load();
1263 if ( $adj ) {
1264 $ts = $this->userAdjust( $ts, $timecorrection );
1265 }
1266
1267 $pref = $this->dateFormat( $format );
1268 if( $pref == 'default' || !isset( $this->dateFormats["$pref date"] ) ) {
1269 $pref = $this->defaultDateFormat;
1270 }
1271 return $this->sprintfDate( $this->dateFormats["$pref date"], $ts );
1272 }
1273
1274 /**
1275 * @param $ts Mixed: the time format which needs to be turned into a
1276 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1277 * @param $adj Bool: whether to adjust the time output according to the
1278 * user configured offset ($timecorrection)
1279 * @param $format Mixed: true to use user's date format preference
1280 * @param $timecorrection String: the time offset as returned by
1281 * validateTimeZone() in Special:Preferences
1282 * @return string
1283 */
1284 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1285 $this->load();
1286 if ( $adj ) {
1287 $ts = $this->userAdjust( $ts, $timecorrection );
1288 }
1289
1290 $pref = $this->dateFormat( $format );
1291 if( $pref == 'default' || !isset( $this->dateFormats["$pref time"] ) ) {
1292 $pref = $this->defaultDateFormat;
1293 }
1294 return $this->sprintfDate( $this->dateFormats["$pref time"], $ts );
1295 }
1296
1297 /**
1298 * @param $ts Mixed: the time format which needs to be turned into a
1299 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1300 * @param $adj Bool: whether to adjust the time output according to the
1301 * user configured offset ($timecorrection)
1302 * @param $format Mixed: what format to return, if it's false output the
1303 * default one (default true)
1304 * @param $timecorrection String: the time offset as returned by
1305 * validateTimeZone() in Special:Preferences
1306 * @return string
1307 */
1308 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false) {
1309 $this->load();
1310
1311 $ts = wfTimestamp( TS_MW, $ts );
1312
1313 if ( $adj ) {
1314 $ts = $this->userAdjust( $ts, $timecorrection );
1315 }
1316
1317 $pref = $this->dateFormat( $format );
1318 if( $pref == 'default' || !isset( $this->dateFormats["$pref both"] ) ) {
1319 $pref = $this->defaultDateFormat;
1320 }
1321
1322 return $this->sprintfDate( $this->dateFormats["$pref both"], $ts );
1323 }
1324
1325 function getMessage( $key ) {
1326 $this->load();
1327 return isset( $this->messages[$key] ) ? $this->messages[$key] : null;
1328 }
1329
1330 function getAllMessages() {
1331 $this->load();
1332 return $this->messages;
1333 }
1334
1335 function iconv( $in, $out, $string ) {
1336 # For most languages, this is a wrapper for iconv
1337 return iconv( $in, $out . '//IGNORE', $string );
1338 }
1339
1340 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
1341 function ucwordbreaksCallbackAscii($matches){
1342 return $this->ucfirst($matches[1]);
1343 }
1344
1345 function ucwordbreaksCallbackMB($matches){
1346 return mb_strtoupper($matches[0]);
1347 }
1348
1349 function ucCallback($matches){
1350 list( $wikiUpperChars ) = self::getCaseMaps();
1351 return strtr( $matches[1], $wikiUpperChars );
1352 }
1353
1354 function lcCallback($matches){
1355 list( , $wikiLowerChars ) = self::getCaseMaps();
1356 return strtr( $matches[1], $wikiLowerChars );
1357 }
1358
1359 function ucwordsCallbackMB($matches){
1360 return mb_strtoupper($matches[0]);
1361 }
1362
1363 function ucwordsCallbackWiki($matches){
1364 list( $wikiUpperChars ) = self::getCaseMaps();
1365 return strtr( $matches[0], $wikiUpperChars );
1366 }
1367
1368 function ucfirst( $str ) {
1369 if ( empty($str) ) return $str;
1370 if ( ord($str[0]) < 128 ) return ucfirst($str);
1371 else return self::uc($str,true); // fall back to more complex logic in case of multibyte strings
1372 }
1373
1374 function uc( $str, $first = false ) {
1375 if ( function_exists( 'mb_strtoupper' ) ) {
1376 if ( $first ) {
1377 if ( self::isMultibyte( $str ) ) {
1378 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1379 } else {
1380 return ucfirst( $str );
1381 }
1382 } else {
1383 return self::isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
1384 }
1385 } else {
1386 if ( self::isMultibyte( $str ) ) {
1387 list( $wikiUpperChars ) = $this->getCaseMaps();
1388 $x = $first ? '^' : '';
1389 return preg_replace_callback(
1390 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1391 array($this,"ucCallback"),
1392 $str
1393 );
1394 } else {
1395 return $first ? ucfirst( $str ) : strtoupper( $str );
1396 }
1397 }
1398 }
1399
1400 function lcfirst( $str ) {
1401 if ( empty($str) ) return $str;
1402 if ( is_string( $str ) && ord($str[0]) < 128 ) {
1403 // editing string in place = cool
1404 $str[0]=strtolower($str[0]);
1405 return $str;
1406 }
1407 else return self::lc( $str, true );
1408 }
1409
1410 function lc( $str, $first = false ) {
1411 if ( function_exists( 'mb_strtolower' ) )
1412 if ( $first )
1413 if ( self::isMultibyte( $str ) )
1414 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1415 else
1416 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
1417 else
1418 return self::isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
1419 else
1420 if ( self::isMultibyte( $str ) ) {
1421 list( , $wikiLowerChars ) = self::getCaseMaps();
1422 $x = $first ? '^' : '';
1423 return preg_replace_callback(
1424 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1425 array($this,"lcCallback"),
1426 $str
1427 );
1428 } else
1429 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
1430 }
1431
1432 function isMultibyte( $str ) {
1433 return (bool)preg_match( '/[\x80-\xff]/', $str );
1434 }
1435
1436 function ucwords($str) {
1437 if ( self::isMultibyte( $str ) ) {
1438 $str = self::lc($str);
1439
1440 // regexp to find first letter in each word (i.e. after each space)
1441 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
1442
1443 // function to use to capitalize a single char
1444 if ( function_exists( 'mb_strtoupper' ) )
1445 return preg_replace_callback(
1446 $replaceRegexp,
1447 array($this,"ucwordsCallbackMB"),
1448 $str
1449 );
1450 else
1451 return preg_replace_callback(
1452 $replaceRegexp,
1453 array($this,"ucwordsCallbackWiki"),
1454 $str
1455 );
1456 }
1457 else
1458 return ucwords( strtolower( $str ) );
1459 }
1460
1461 # capitalize words at word breaks
1462 function ucwordbreaks($str){
1463 if (self::isMultibyte( $str ) ) {
1464 $str = self::lc($str);
1465
1466 // since \b doesn't work for UTF-8, we explicitely define word break chars
1467 $breaks= "[ \-\(\)\}\{\.,\?!]";
1468
1469 // find first letter after word break
1470 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
1471
1472 if ( function_exists( 'mb_strtoupper' ) )
1473 return preg_replace_callback(
1474 $replaceRegexp,
1475 array($this,"ucwordbreaksCallbackMB"),
1476 $str
1477 );
1478 else
1479 return preg_replace_callback(
1480 $replaceRegexp,
1481 array($this,"ucwordsCallbackWiki"),
1482 $str
1483 );
1484 }
1485 else
1486 return preg_replace_callback(
1487 '/\b([\w\x80-\xff]+)\b/',
1488 array($this,"ucwordbreaksCallbackAscii"),
1489 $str );
1490 }
1491
1492 /**
1493 * Return a case-folded representation of $s
1494 *
1495 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
1496 * and $s2 are the same except for the case of their characters. It is not
1497 * necessary for the value returned to make sense when displayed.
1498 *
1499 * Do *not* perform any other normalisation in this function. If a caller
1500 * uses this function when it should be using a more general normalisation
1501 * function, then fix the caller.
1502 */
1503 function caseFold( $s ) {
1504 return $this->uc( $s );
1505 }
1506
1507 function checkTitleEncoding( $s ) {
1508 if( is_array( $s ) ) {
1509 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
1510 }
1511 # Check for non-UTF-8 URLs
1512 $ishigh = preg_match( '/[\x80-\xff]/', $s);
1513 if(!$ishigh) return $s;
1514
1515 $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1516 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
1517 if( $isutf8 ) return $s;
1518
1519 return $this->iconv( $this->fallback8bitEncoding(), "utf-8", $s );
1520 }
1521
1522 function fallback8bitEncoding() {
1523 $this->load();
1524 return $this->fallback8bitEncoding;
1525 }
1526
1527 /**
1528 * Some languages have special punctuation to strip out
1529 * or characters which need to be converted for MySQL's
1530 * indexing to grok it correctly. Make such changes here.
1531 *
1532 * @param $string String
1533 * @return String
1534 */
1535 function stripForSearch( $string ) {
1536 global $wgDBtype;
1537 if ( $wgDBtype != 'mysql' ) {
1538 return $string;
1539 }
1540
1541
1542 wfProfileIn( __METHOD__ );
1543
1544 // MySQL fulltext index doesn't grok utf-8, so we
1545 // need to fold cases and convert to hex
1546 $out = preg_replace_callback(
1547 "/([\\xc0-\\xff][\\x80-\\xbf]*)/",
1548 array( $this, 'stripForSearchCallback' ),
1549 $this->lc( $string ) );
1550
1551 // And to add insult to injury, the default indexing
1552 // ignores short words... Pad them so we can pass them
1553 // through without reconfiguring the server...
1554 $minLength = $this->minSearchLength();
1555 if( $minLength > 1 ) {
1556 $n = $minLength-1;
1557 $out = preg_replace(
1558 "/\b(\w{1,$n})\b/",
1559 "$1U800",
1560 $out );
1561 }
1562
1563 // Periods within things like hostnames and IP addresses
1564 // are also important -- we want a search for "example.com"
1565 // or "192.168.1.1" to work sanely.
1566 //
1567 // MySQL's search seems to ignore them, so you'd match on
1568 // "example.wikipedia.com" and "192.168.83.1" as well.
1569 $out = preg_replace(
1570 "/(\w)\.(\w|\*)/u",
1571 "$1U82e$2",
1572 $out );
1573
1574 wfProfileOut( __METHOD__ );
1575 return $out;
1576 }
1577
1578 /**
1579 * Armor a case-folded UTF-8 string to get through MySQL's
1580 * fulltext search without being mucked up by funny charset
1581 * settings or anything else of the sort.
1582 */
1583 protected function stripForSearchCallback( $matches ) {
1584 return 'U8' . bin2hex( $matches[1] );
1585 }
1586
1587 /**
1588 * Check MySQL server's ft_min_word_len setting so we know
1589 * if we need to pad short words...
1590 */
1591 protected function minSearchLength() {
1592 if( !isset( $this->minSearchLength ) ) {
1593 $sql = "show global variables like 'ft\\_min\\_word\\_len'";
1594 $dbr = wfGetDB( DB_SLAVE );
1595 $result = $dbr->query( $sql );
1596 $row = $result->fetchObject();
1597 $result->free();
1598
1599 if( $row && $row->Variable_name == 'ft_min_word_len' ) {
1600 $this->minSearchLength = intval( $row->Value );
1601 } else {
1602 $this->minSearchLength = 0;
1603 }
1604 }
1605 return $this->minSearchLength;
1606 }
1607
1608 function convertForSearchResult( $termsArray ) {
1609 # some languages, e.g. Chinese, need to do a conversion
1610 # in order for search results to be displayed correctly
1611 return $termsArray;
1612 }
1613
1614 /**
1615 * Get the first character of a string.
1616 *
1617 * @param $s string
1618 * @return string
1619 */
1620 function firstChar( $s ) {
1621 $matches = array();
1622 preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1623 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/', $s, $matches);
1624
1625 if ( isset( $matches[1] ) ) {
1626 if ( strlen( $matches[1] ) != 3 ) {
1627 return $matches[1];
1628 }
1629
1630 // Break down Hangul syllables to grab the first jamo
1631 $code = utf8ToCodepoint( $matches[1] );
1632 if ( $code < 0xac00 || 0xd7a4 <= $code) {
1633 return $matches[1];
1634 } elseif ( $code < 0xb098 ) {
1635 return "\xe3\x84\xb1";
1636 } elseif ( $code < 0xb2e4 ) {
1637 return "\xe3\x84\xb4";
1638 } elseif ( $code < 0xb77c ) {
1639 return "\xe3\x84\xb7";
1640 } elseif ( $code < 0xb9c8 ) {
1641 return "\xe3\x84\xb9";
1642 } elseif ( $code < 0xbc14 ) {
1643 return "\xe3\x85\x81";
1644 } elseif ( $code < 0xc0ac ) {
1645 return "\xe3\x85\x82";
1646 } elseif ( $code < 0xc544 ) {
1647 return "\xe3\x85\x85";
1648 } elseif ( $code < 0xc790 ) {
1649 return "\xe3\x85\x87";
1650 } elseif ( $code < 0xcc28 ) {
1651 return "\xe3\x85\x88";
1652 } elseif ( $code < 0xce74 ) {
1653 return "\xe3\x85\x8a";
1654 } elseif ( $code < 0xd0c0 ) {
1655 return "\xe3\x85\x8b";
1656 } elseif ( $code < 0xd30c ) {
1657 return "\xe3\x85\x8c";
1658 } elseif ( $code < 0xd558 ) {
1659 return "\xe3\x85\x8d";
1660 } else {
1661 return "\xe3\x85\x8e";
1662 }
1663 } else {
1664 return "";
1665 }
1666 }
1667
1668 function initEncoding() {
1669 # Some languages may have an alternate char encoding option
1670 # (Esperanto X-coding, Japanese furigana conversion, etc)
1671 # If this language is used as the primary content language,
1672 # an override to the defaults can be set here on startup.
1673 }
1674
1675 function recodeForEdit( $s ) {
1676 # For some languages we'll want to explicitly specify
1677 # which characters make it into the edit box raw
1678 # or are converted in some way or another.
1679 # Note that if wgOutputEncoding is different from
1680 # wgInputEncoding, this text will be further converted
1681 # to wgOutputEncoding.
1682 global $wgEditEncoding;
1683 if( $wgEditEncoding == '' or
1684 $wgEditEncoding == 'UTF-8' ) {
1685 return $s;
1686 } else {
1687 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
1688 }
1689 }
1690
1691 function recodeInput( $s ) {
1692 # Take the previous into account.
1693 global $wgEditEncoding;
1694 if($wgEditEncoding != "") {
1695 $enc = $wgEditEncoding;
1696 } else {
1697 $enc = 'UTF-8';
1698 }
1699 if( $enc == 'UTF-8' ) {
1700 return $s;
1701 } else {
1702 return $this->iconv( $enc, 'UTF-8', $s );
1703 }
1704 }
1705
1706 /**
1707 * For right-to-left language support
1708 *
1709 * @return bool
1710 */
1711 function isRTL() {
1712 $this->load();
1713 return $this->rtl;
1714 }
1715
1716 /**
1717 * A hidden direction mark (LRM or RLM), depending on the language direction
1718 *
1719 * @return string
1720 */
1721 function getDirMark() {
1722 return $this->isRTL() ? "\xE2\x80\x8F" : "\xE2\x80\x8E";
1723 }
1724
1725 /**
1726 * An arrow, depending on the language direction
1727 *
1728 * @return string
1729 */
1730 function getArrow() {
1731 return $this->isRTL() ? '←' : '→';
1732 }
1733
1734 /**
1735 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
1736 *
1737 * @return bool
1738 */
1739 function linkPrefixExtension() {
1740 $this->load();
1741 return $this->linkPrefixExtension;
1742 }
1743
1744 function &getMagicWords() {
1745 $this->load();
1746 return $this->magicWords;
1747 }
1748
1749 # Fill a MagicWord object with data from here
1750 function getMagic( &$mw ) {
1751 if ( !$this->mMagicHookDone ) {
1752 $this->mMagicHookDone = true;
1753 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
1754 }
1755 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
1756 $rawEntry = $this->mMagicExtensions[$mw->mId];
1757 } else {
1758 $magicWords =& $this->getMagicWords();
1759 if ( isset( $magicWords[$mw->mId] ) ) {
1760 $rawEntry = $magicWords[$mw->mId];
1761 } else {
1762 # Fall back to English if local list is incomplete
1763 $magicWords =& Language::getMagicWords();
1764 if ( !isset($magicWords[$mw->mId]) ) {
1765 throw new MWException("Magic word '{$mw->mId}' not found" );
1766 }
1767 $rawEntry = $magicWords[$mw->mId];
1768 }
1769 }
1770
1771 if( !is_array( $rawEntry ) ) {
1772 error_log( "\"$rawEntry\" is not a valid magic thingie for \"$mw->mId\"" );
1773 } else {
1774 $mw->mCaseSensitive = $rawEntry[0];
1775 $mw->mSynonyms = array_slice( $rawEntry, 1 );
1776 }
1777 }
1778
1779 /**
1780 * Add magic words to the extension array
1781 */
1782 function addMagicWordsByLang( $newWords ) {
1783 $code = $this->getCode();
1784 $fallbackChain = array();
1785 while ( $code && !in_array( $code, $fallbackChain ) ) {
1786 $fallbackChain[] = $code;
1787 $code = self::getFallbackFor( $code );
1788 }
1789 if ( !in_array( 'en', $fallbackChain ) ) {
1790 $fallbackChain[] = 'en';
1791 }
1792 $fallbackChain = array_reverse( $fallbackChain );
1793 foreach ( $fallbackChain as $code ) {
1794 if ( isset( $newWords[$code] ) ) {
1795 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
1796 }
1797 }
1798 }
1799
1800 /**
1801 * Get special page names, as an associative array
1802 * case folded alias => real name
1803 */
1804 function getSpecialPageAliases() {
1805 $this->load();
1806
1807 // Cache aliases because it may be slow to load them
1808 if ( !isset( $this->mExtendedSpecialPageAliases ) ) {
1809
1810 // Initialise array
1811 $this->mExtendedSpecialPageAliases = $this->specialPageAliases;
1812
1813 global $wgExtensionAliasesFiles;
1814 foreach ( $wgExtensionAliasesFiles as $file ) {
1815
1816 // Fail fast
1817 if ( !file_exists($file) )
1818 throw new MWException( "Aliases file does not exist: $file" );
1819
1820 $aliases = array();
1821 require($file);
1822
1823 // Check the availability of aliases
1824 if ( !isset($aliases['en']) )
1825 throw new MWException( "Malformed aliases file: $file" );
1826
1827 // Merge all aliases in fallback chain
1828 $code = $this->getCode();
1829 do {
1830 if ( !isset($aliases[$code]) ) continue;
1831
1832 $aliases[$code] = $this->fixSpecialPageAliases( $aliases[$code] );
1833 /* Merge the aliases, THIS will break if there is special page name
1834 * which looks like a numerical key, thanks to PHP...
1835 * See the array_merge_recursive manual entry */
1836 $this->mExtendedSpecialPageAliases = array_merge_recursive(
1837 $this->mExtendedSpecialPageAliases, $aliases[$code] );
1838
1839 } while ( $code = self::getFallbackFor( $code ) );
1840 }
1841
1842 wfRunHooks( 'LanguageGetSpecialPageAliases',
1843 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
1844 }
1845
1846 return $this->mExtendedSpecialPageAliases;
1847 }
1848
1849 /**
1850 * Function to fix special page aliases. Will convert the first letter to
1851 * upper case and spaces to underscores. Can be given a full aliases array,
1852 * in which case it will recursively fix all aliases.
1853 */
1854 public function fixSpecialPageAliases( $mixed ) {
1855 // Work recursively until in string level
1856 if ( is_array($mixed) ) {
1857 $callback = array( $this, 'fixSpecialPageAliases' );
1858 return array_map( $callback, $mixed );
1859 }
1860 return str_replace( ' ', '_', $this->ucfirst( $mixed ) );
1861 }
1862
1863 /**
1864 * Italic is unsuitable for some languages
1865 *
1866 * @param $text String: the text to be emphasized.
1867 * @return string
1868 */
1869 function emphasize( $text ) {
1870 return "<em>$text</em>";
1871 }
1872
1873 /**
1874 * Normally we output all numbers in plain en_US style, that is
1875 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
1876 * point twohundredthirtyfive. However this is not sutable for all
1877 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
1878 * Icelandic just want to use commas instead of dots, and dots instead
1879 * of commas like "293.291,235".
1880 *
1881 * An example of this function being called:
1882 * <code>
1883 * wfMsg( 'message', $wgLang->formatNum( $num ) )
1884 * </code>
1885 *
1886 * See LanguageGu.php for the Gujarati implementation and
1887 * $separatorTransformTable on MessageIs.php for
1888 * the , => . and . => , implementation.
1889 *
1890 * @todo check if it's viable to use localeconv() for the decimal
1891 * separator thing.
1892 * @param $number Mixed: the string to be formatted, should be an integer
1893 * or a floating point number.
1894 * @param $nocommafy Bool: set to true for special numbers like dates
1895 * @return string
1896 */
1897 function formatNum( $number, $nocommafy = false ) {
1898 global $wgTranslateNumerals;
1899 if (!$nocommafy) {
1900 $number = $this->commafy($number);
1901 $s = $this->separatorTransformTable();
1902 if ($s) { $number = strtr($number, $s); }
1903 }
1904
1905 if ($wgTranslateNumerals) {
1906 $s = $this->digitTransformTable();
1907 if ($s) { $number = strtr($number, $s); }
1908 }
1909
1910 return $number;
1911 }
1912
1913 function parseFormattedNumber( $number ) {
1914 $s = $this->digitTransformTable();
1915 if ($s) { $number = strtr($number, array_flip($s)); }
1916
1917 $s = $this->separatorTransformTable();
1918 if ($s) { $number = strtr($number, array_flip($s)); }
1919
1920 $number = strtr( $number, array (',' => '') );
1921 return $number;
1922 }
1923
1924 /**
1925 * Adds commas to a given number
1926 *
1927 * @param $_ mixed
1928 * @return string
1929 */
1930 function commafy($_) {
1931 return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_)));
1932 }
1933
1934 function digitTransformTable() {
1935 $this->load();
1936 return $this->digitTransformTable;
1937 }
1938
1939 function separatorTransformTable() {
1940 $this->load();
1941 return $this->separatorTransformTable;
1942 }
1943
1944
1945 /**
1946 * For the credit list in includes/Credits.php (action=credits)
1947 *
1948 * @param $l Array
1949 * @return string
1950 */
1951 function listToText( $l ) {
1952 $s = '';
1953 $m = count($l) - 1;
1954 for ($i = $m; $i >= 0; $i--) {
1955 if ($i == $m) {
1956 $s = $l[$i];
1957 } else if ($i == $m - 1) {
1958 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
1959 } else {
1960 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
1961 }
1962 }
1963 return $s;
1964 }
1965
1966 /**
1967 * Take a list of strings and build a locale-friendly comma-separated
1968 * list, using the local comma-separator message.
1969 * @param $list array of strings to put in a comma list
1970 * @return string
1971 */
1972 function commaList( $list, $forContent = false ) {
1973 return implode(
1974 $list,
1975 wfMsgExt( 'comma-separator', array( 'escapenoentities', 'language' => $this ) ) );
1976 }
1977
1978 /**
1979 * Same as commaList, but separate it with the pipe instead.
1980 * @param $list array of strings to put in a pipe list
1981 * @return string
1982 */
1983 function pipeList( $list ) {
1984 return implode(
1985 $list,
1986 wfMsgExt( 'pipe-separator', array( 'escapenoentities', 'language' => $this ) ) );
1987 }
1988
1989 /**
1990 * Truncate a string to a specified length in bytes, appending an optional
1991 * string (e.g. for ellipses)
1992 *
1993 * The database offers limited byte lengths for some columns in the database;
1994 * multi-byte character sets mean we need to ensure that only whole characters
1995 * are included, otherwise broken characters can be passed to the user
1996 *
1997 * If $length is negative, the string will be truncated from the beginning
1998 *
1999 * @param $string String to truncate
2000 * @param $length Int: maximum length (excluding ellipses)
2001 * @param $ellipsis String to append to the truncated text
2002 * @return string
2003 */
2004 function truncate( $string, $length, $ellipsis = "" ) {
2005 if( $length == 0 ) {
2006 return $ellipsis;
2007 }
2008 if ( strlen( $string ) <= abs( $length ) ) {
2009 return $string;
2010 }
2011 if( $length > 0 ) {
2012 $string = substr( $string, 0, $length );
2013 $char = ord( $string[strlen( $string ) - 1] );
2014 $m = array();
2015 if ($char >= 0xc0) {
2016 # We got the first byte only of a multibyte char; remove it.
2017 $string = substr( $string, 0, -1 );
2018 } elseif( $char >= 0x80 &&
2019 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
2020 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) ) {
2021 # We chopped in the middle of a character; remove it
2022 $string = $m[1];
2023 }
2024 return $string . $ellipsis;
2025 } else {
2026 $string = substr( $string, $length );
2027 $char = ord( $string[0] );
2028 if( $char >= 0x80 && $char < 0xc0 ) {
2029 # We chopped in the middle of a character; remove the whole thing
2030 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
2031 }
2032 return $ellipsis . $string;
2033 }
2034 }
2035
2036 /**
2037 * Grammatical transformations, needed for inflected languages
2038 * Invoked by putting {{grammar:case|word}} in a message
2039 *
2040 * @param $word string
2041 * @param $case string
2042 * @return string
2043 */
2044 function convertGrammar( $word, $case ) {
2045 global $wgGrammarForms;
2046 if ( isset($wgGrammarForms[$this->getCode()][$case][$word]) ) {
2047 return $wgGrammarForms[$this->getCode()][$case][$word];
2048 }
2049 return $word;
2050 }
2051
2052 /**
2053 * Plural form transformations, needed for some languages.
2054 * For example, there are 3 form of plural in Russian and Polish,
2055 * depending on "count mod 10". See [[w:Plural]]
2056 * For English it is pretty simple.
2057 *
2058 * Invoked by putting {{plural:count|wordform1|wordform2}}
2059 * or {{plural:count|wordform1|wordform2|wordform3}}
2060 *
2061 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
2062 *
2063 * @param $count Integer: non-localized number
2064 * @param $forms Array: different plural forms
2065 * @return string Correct form of plural for $count in this language
2066 */
2067 function convertPlural( $count, $forms ) {
2068 if ( !count($forms) ) { return ''; }
2069 $forms = $this->preConvertPlural( $forms, 2 );
2070
2071 return ( $count == 1 ) ? $forms[0] : $forms[1];
2072 }
2073
2074 /**
2075 * Checks that convertPlural was given an array and pads it to requested
2076 * amound of forms by copying the last one.
2077 *
2078 * @param $count Integer: How many forms should there be at least
2079 * @param $forms Array of forms given to convertPlural
2080 * @return array Padded array of forms or an exception if not an array
2081 */
2082 protected function preConvertPlural( /* Array */ $forms, $count ) {
2083 while ( count($forms) < $count ) {
2084 $forms[] = $forms[count($forms)-1];
2085 }
2086 return $forms;
2087 }
2088
2089 /**
2090 * For translaing of expiry times
2091 * @param $str String: the validated block time in English
2092 * @return Somehow translated block time
2093 * @see LanguageFi.php for example implementation
2094 */
2095 function translateBlockExpiry( $str ) {
2096
2097 $scBlockExpiryOptions = $this->getMessageFromDB( 'ipboptions' );
2098
2099 if ( $scBlockExpiryOptions == '-') {
2100 return $str;
2101 }
2102
2103 foreach (explode(',', $scBlockExpiryOptions) as $option) {
2104 if ( strpos($option, ":") === false )
2105 continue;
2106 list($show, $value) = explode(":", $option);
2107 if ( strcmp ( $str, $value) == 0 ) {
2108 return htmlspecialchars( trim( $show ) );
2109 }
2110 }
2111
2112 return $str;
2113 }
2114
2115 /**
2116 * languages like Chinese need to be segmented in order for the diff
2117 * to be of any use
2118 *
2119 * @param $text String
2120 * @return String
2121 */
2122 function segmentForDiff( $text ) {
2123 return $text;
2124 }
2125
2126 /**
2127 * and unsegment to show the result
2128 *
2129 * @param $text String
2130 * @return String
2131 */
2132 function unsegmentForDiff( $text ) {
2133 return $text;
2134 }
2135
2136 # convert text to different variants of a language.
2137 function convert( $text, $isTitle = false) {
2138 return $this->mConverter->convert($text, $isTitle);
2139 }
2140
2141 # Convert text from within Parser
2142 function parserConvert( $text, &$parser ) {
2143 return $this->mConverter->parserConvert( $text, $parser );
2144 }
2145
2146 # Check if this is a language with variants
2147 function hasVariants(){
2148 return sizeof($this->getVariants())>1;
2149 }
2150
2151 # Put custom tags (e.g. -{ }-) around math to prevent conversion
2152 function armourMath($text){
2153 return $this->mConverter->armourMath($text);
2154 }
2155
2156
2157 /**
2158 * Perform output conversion on a string, and encode for safe HTML output.
2159 * @param $text String
2160 * @param $isTitle Bool -- wtf?
2161 * @return string
2162 * @todo this should get integrated somewhere sane
2163 */
2164 function convertHtml( $text, $isTitle = false ) {
2165 return htmlspecialchars( $this->convert( $text, $isTitle ) );
2166 }
2167
2168 function convertCategoryKey( $key ) {
2169 return $this->mConverter->convertCategoryKey( $key );
2170 }
2171
2172 /**
2173 * get the list of variants supported by this langauge
2174 * see sample implementation in LanguageZh.php
2175 *
2176 * @return array an array of language codes
2177 */
2178 function getVariants() {
2179 return $this->mConverter->getVariants();
2180 }
2181
2182
2183 function getPreferredVariant( $fromUser = true ) {
2184 return $this->mConverter->getPreferredVariant( $fromUser );
2185 }
2186
2187 /**
2188 * if a language supports multiple variants, it is
2189 * possible that non-existing link in one variant
2190 * actually exists in another variant. this function
2191 * tries to find it. See e.g. LanguageZh.php
2192 *
2193 * @param $link String: the name of the link
2194 * @param $nt Mixed: the title object of the link
2195 * @return null the input parameters may be modified upon return
2196 */
2197 function findVariantLink( &$link, &$nt, $forTemplate = false ) {
2198 $this->mConverter->findVariantLink($link, $nt, $forTemplate );
2199 }
2200
2201 /**
2202 * If a language supports multiple variants, converts text
2203 * into an array of all possible variants of the text:
2204 * 'variant' => text in that variant
2205 */
2206
2207 function convertLinkToAllVariants($text){
2208 return $this->mConverter->convertLinkToAllVariants($text);
2209 }
2210
2211
2212 /**
2213 * returns language specific options used by User::getPageRenderHash()
2214 * for example, the preferred language variant
2215 *
2216 * @return string
2217 */
2218 function getExtraHashOptions() {
2219 return $this->mConverter->getExtraHashOptions();
2220 }
2221
2222 /**
2223 * for languages that support multiple variants, the title of an
2224 * article may be displayed differently in different variants. this
2225 * function returns the apporiate title defined in the body of the article.
2226 *
2227 * @return string
2228 */
2229 function getParsedTitle() {
2230 return $this->mConverter->getParsedTitle();
2231 }
2232
2233 /**
2234 * Enclose a string with the "no conversion" tag. This is used by
2235 * various functions in the Parser
2236 *
2237 * @param $text String: text to be tagged for no conversion
2238 * @param $noParse
2239 * @return string the tagged text
2240 */
2241 function markNoConversion( $text, $noParse=false ) {
2242 return $this->mConverter->markNoConversion( $text, $noParse );
2243 }
2244
2245 /**
2246 * A regular expression to match legal word-trailing characters
2247 * which should be merged onto a link of the form [[foo]]bar.
2248 *
2249 * @return string
2250 */
2251 function linkTrail() {
2252 $this->load();
2253 return $this->linkTrail;
2254 }
2255
2256 function getLangObj() {
2257 return $this;
2258 }
2259
2260 /**
2261 * Get the RFC 3066 code for this language object
2262 */
2263 function getCode() {
2264 return $this->mCode;
2265 }
2266
2267 function setCode( $code ) {
2268 $this->mCode = $code;
2269 }
2270
2271 static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
2272 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
2273 }
2274
2275 static function getMessagesFileName( $code ) {
2276 global $IP;
2277 return self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
2278 }
2279
2280 static function getClassFileName( $code ) {
2281 global $IP;
2282 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
2283 }
2284
2285 static function getLocalisationArray( $code, $disableCache = false ) {
2286 self::loadLocalisation( $code, $disableCache );
2287 return self::$mLocalisationCache[$code];
2288 }
2289
2290 /**
2291 * Load localisation data for a given code into the static cache
2292 *
2293 * @return array Dependencies, map of filenames to mtimes
2294 */
2295 static function loadLocalisation( $code, $disableCache = false ) {
2296 static $recursionGuard = array();
2297 global $wgMemc, $wgEnableSerializedMessages, $wgCheckSerialized;
2298
2299 if ( !$code ) {
2300 throw new MWException( "Invalid language code requested" );
2301 }
2302
2303 if ( !$disableCache ) {
2304 # Try the per-process cache
2305 if ( isset( self::$mLocalisationCache[$code] ) ) {
2306 return self::$mLocalisationCache[$code]['deps'];
2307 }
2308
2309 wfProfileIn( __METHOD__ );
2310
2311 # Try the serialized directory
2312 if( $wgEnableSerializedMessages ) {
2313 $cache = wfGetPrecompiledData( self::getFileName( "Messages", $code, '.ser' ) );
2314 if ( $cache ) {
2315 if ( $wgCheckSerialized && self::isLocalisationOutOfDate( $cache ) ) {
2316 $cache = false;
2317 wfDebug( "Language::loadLocalisation(): precompiled data file for $code is out of date\n" );
2318 } else {
2319 self::$mLocalisationCache[$code] = $cache;
2320 wfDebug( "Language::loadLocalisation(): got localisation for $code from precompiled data file\n" );
2321 wfProfileOut( __METHOD__ );
2322 return self::$mLocalisationCache[$code]['deps'];
2323 }
2324 }
2325 }
2326
2327 # Try the global cache
2328 $memcKey = wfMemcKey('localisation', $code );
2329 $fbMemcKey = wfMemcKey('fallback', $cache['fallback'] );
2330 $cache = $wgMemc->get( $memcKey );
2331 if ( $cache ) {
2332 if ( self::isLocalisationOutOfDate( $cache ) ) {
2333 $wgMemc->delete( $memcKey );
2334 $wgMemc->delete( $fbMemcKey );
2335 $cache = false;
2336 wfDebug( "Language::loadLocalisation(): localisation cache for $code had expired\n" );
2337 } else {
2338 self::$mLocalisationCache[$code] = $cache;
2339 wfDebug( "Language::loadLocalisation(): got localisation for $code from cache\n" );
2340 wfProfileOut( __METHOD__ );
2341 return $cache['deps'];
2342 }
2343 }
2344 } else {
2345 wfProfileIn( __METHOD__ );
2346 }
2347
2348 # Default fallback, may be overridden when the messages file is included
2349 if ( $code != 'en' ) {
2350 $fallback = 'en';
2351 } else {
2352 $fallback = false;
2353 }
2354
2355 # Load the primary localisation from the source file
2356 $filename = self::getMessagesFileName( $code );
2357 if ( !file_exists( $filename ) ) {
2358 wfDebug( "Language::loadLocalisation(): no localisation file for $code, using implicit fallback to en\n" );
2359 $cache = compact( self::$mLocalisationKeys ); // Set correct fallback
2360 $deps = array();
2361 } else {
2362 $deps = array( $filename => filemtime( $filename ) );
2363 require( $filename );
2364 $cache = compact( self::$mLocalisationKeys );
2365 wfDebug( "Language::loadLocalisation(): got localisation for $code from source\n" );
2366 }
2367
2368 if ( !empty( $fallback ) ) {
2369 # Load the fallback localisation, with a circular reference guard
2370 if ( isset( $recursionGuard[$code] ) ) {
2371 throw new MWException( "Error: Circular fallback reference in language code $code" );
2372 }
2373 $recursionGuard[$code] = true;
2374 $newDeps = self::loadLocalisation( $fallback, $disableCache );
2375 unset( $recursionGuard[$code] );
2376
2377 $secondary = self::$mLocalisationCache[$fallback];
2378 $deps = array_merge( $deps, $newDeps );
2379
2380 # Merge the fallback localisation with the current localisation
2381 foreach ( self::$mLocalisationKeys as $key ) {
2382 if ( isset( $cache[$key] ) ) {
2383 if ( isset( $secondary[$key] ) ) {
2384 if ( in_array( $key, self::$mMergeableMapKeys ) ) {
2385 $cache[$key] = $cache[$key] + $secondary[$key];
2386 } elseif ( in_array( $key, self::$mMergeableListKeys ) ) {
2387 $cache[$key] = array_merge( $secondary[$key], $cache[$key] );
2388 } elseif ( in_array( $key, self::$mMergeableAliasListKeys ) ) {
2389 $cache[$key] = array_merge_recursive( $cache[$key], $secondary[$key] );
2390 }
2391 }
2392 } else {
2393 $cache[$key] = $secondary[$key];
2394 }
2395 }
2396
2397 # Merge bookstore lists if requested
2398 if ( !empty( $cache['bookstoreList']['inherit'] ) ) {
2399 $cache['bookstoreList'] = array_merge( $cache['bookstoreList'], $secondary['bookstoreList'] );
2400 }
2401 if ( isset( $cache['bookstoreList']['inherit'] ) ) {
2402 unset( $cache['bookstoreList']['inherit'] );
2403 }
2404 }
2405
2406 # Add dependencies to the cache entry
2407 $cache['deps'] = $deps;
2408
2409 # Replace spaces with underscores in namespace names
2410 $cache['namespaceNames'] = str_replace( ' ', '_', $cache['namespaceNames'] );
2411
2412 # And do the same for specialpage aliases. $page is an array.
2413 foreach ( $cache['specialPageAliases'] as &$page ) {
2414 $page = str_replace( ' ', '_', $page );
2415 }
2416 # Decouple the reference to prevent accidental damage
2417 unset($page);
2418
2419 # Save to both caches
2420 self::$mLocalisationCache[$code] = $cache;
2421 if ( !$disableCache ) {
2422 $wgMemc->set( $memcKey, $cache );
2423 $wgMemc->set( $fbMemcKey, (string) $cache['fallback'] );
2424 }
2425
2426 wfProfileOut( __METHOD__ );
2427 return $deps;
2428 }
2429
2430 /**
2431 * Test if a given localisation cache is out of date with respect to the
2432 * source Messages files. This is done automatically for the global cache
2433 * in $wgMemc, but is only done on certain occasions for the serialized
2434 * data file.
2435 *
2436 * @param $cache mixed Either a language code or a cache array
2437 */
2438 static function isLocalisationOutOfDate( $cache ) {
2439 if ( !is_array( $cache ) ) {
2440 self::loadLocalisation( $cache );
2441 $cache = self::$mLocalisationCache[$cache];
2442 }
2443 $expired = false;
2444 foreach ( $cache['deps'] as $file => $mtime ) {
2445 if ( !file_exists( $file ) || filemtime( $file ) > $mtime ) {
2446 $expired = true;
2447 break;
2448 }
2449 }
2450 return $expired;
2451 }
2452
2453 /**
2454 * Get the fallback for a given language
2455 */
2456 static function getFallbackFor( $code ) {
2457 // Shortcut
2458 if ( $code === 'en' ) return false;
2459
2460 // Local cache
2461 static $cache = array();
2462 // Quick return
2463 if ( isset($cache[$code]) ) return $cache[$code];
2464
2465 // Try memcache
2466 global $wgMemc;
2467 $memcKey = wfMemcKey( 'fallback', $code );
2468 $fbcode = $wgMemc->get( $memcKey );
2469
2470 if ( is_string($fbcode) ) {
2471 // False is stored as a string to detect failures in memcache properly
2472 if ( $fbcode === '' ) $fbcode = false;
2473
2474 // Update local cache and return
2475 $cache[$code] = $fbcode;
2476 return $fbcode;
2477 }
2478
2479 // Nothing in caches, load and and update both caches
2480 self::loadLocalisation( $code );
2481 $fbcode = self::$mLocalisationCache[$code]['fallback'];
2482
2483 $cache[$code] = $fbcode;
2484 $wgMemc->set( $memcKey, (string) $fbcode );
2485
2486 return $fbcode;
2487 }
2488
2489 /**
2490 * Get all messages for a given language
2491 */
2492 static function getMessagesFor( $code ) {
2493 self::loadLocalisation( $code );
2494 return self::$mLocalisationCache[$code]['messages'];
2495 }
2496
2497 /**
2498 * Get a message for a given language
2499 */
2500 static function getMessageFor( $key, $code ) {
2501 self::loadLocalisation( $code );
2502 return isset( self::$mLocalisationCache[$code]['messages'][$key] ) ? self::$mLocalisationCache[$code]['messages'][$key] : null;
2503 }
2504
2505 /**
2506 * Load localisation data for this object
2507 */
2508 function load() {
2509 if ( !$this->mLoaded ) {
2510 self::loadLocalisation( $this->getCode() );
2511 $cache =& self::$mLocalisationCache[$this->getCode()];
2512 foreach ( self::$mLocalisationKeys as $key ) {
2513 $this->$key = $cache[$key];
2514 }
2515 $this->mLoaded = true;
2516
2517 $this->fixUpSettings();
2518 }
2519 }
2520
2521 /**
2522 * Do any necessary post-cache-load settings adjustment
2523 */
2524 function fixUpSettings() {
2525 global $wgExtraNamespaces, $wgMetaNamespace, $wgMetaNamespaceTalk,
2526 $wgNamespaceAliases, $wgAmericanDates;
2527 wfProfileIn( __METHOD__ );
2528 if ( $wgExtraNamespaces ) {
2529 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames;
2530 }
2531
2532 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
2533 if ( $wgMetaNamespaceTalk ) {
2534 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
2535 } else {
2536 $talk = $this->namespaceNames[NS_PROJECT_TALK];
2537 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
2538
2539 # Allow grammar transformations
2540 # Allowing full message-style parsing would make simple requests
2541 # such as action=raw much more expensive than they need to be.
2542 # This will hopefully cover most cases.
2543 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
2544 array( &$this, 'replaceGrammarInNamespace' ), $talk );
2545 $talk = str_replace( ' ', '_', $talk );
2546 $this->namespaceNames[NS_PROJECT_TALK] = $talk;
2547 }
2548
2549 # The above mixing may leave namespaces out of canonical order.
2550 # Re-order by namespace ID number...
2551 ksort( $this->namespaceNames );
2552
2553 # Put namespace names and aliases into a hashtable.
2554 # If this is too slow, then we should arrange it so that it is done
2555 # before caching. The catch is that at pre-cache time, the above
2556 # class-specific fixup hasn't been done.
2557 $this->mNamespaceIds = array();
2558 foreach ( $this->namespaceNames as $index => $name ) {
2559 $this->mNamespaceIds[$this->lc($name)] = $index;
2560 }
2561 if ( $this->namespaceAliases ) {
2562 foreach ( $this->namespaceAliases as $name => $index ) {
2563 $this->mNamespaceIds[$this->lc($name)] = $index;
2564 }
2565 }
2566 if ( $wgNamespaceAliases ) {
2567 foreach ( $wgNamespaceAliases as $name => $index ) {
2568 $this->mNamespaceIds[$this->lc($name)] = $index;
2569 }
2570 }
2571
2572 if ( $this->defaultDateFormat == 'dmy or mdy' ) {
2573 $this->defaultDateFormat = $wgAmericanDates ? 'mdy' : 'dmy';
2574 }
2575 wfProfileOut( __METHOD__ );
2576 }
2577
2578 function replaceGrammarInNamespace( $m ) {
2579 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
2580 }
2581
2582 static function getCaseMaps() {
2583 static $wikiUpperChars, $wikiLowerChars;
2584 if ( isset( $wikiUpperChars ) ) {
2585 return array( $wikiUpperChars, $wikiLowerChars );
2586 }
2587
2588 wfProfileIn( __METHOD__ );
2589 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
2590 if ( $arr === false ) {
2591 throw new MWException(
2592 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
2593 }
2594 extract( $arr );
2595 wfProfileOut( __METHOD__ );
2596 return array( $wikiUpperChars, $wikiLowerChars );
2597 }
2598
2599 function formatTimePeriod( $seconds ) {
2600 if ( $seconds < 10 ) {
2601 return $this->formatNum( sprintf( "%.1f", $seconds ) ) . wfMsg( 'seconds-abbrev' );
2602 } elseif ( $seconds < 60 ) {
2603 return $this->formatNum( round( $seconds ) ) . wfMsg( 'seconds-abbrev' );
2604 } elseif ( $seconds < 3600 ) {
2605 return $this->formatNum( floor( $seconds / 60 ) ) . wfMsg( 'minutes-abbrev' ) .
2606 $this->formatNum( round( fmod( $seconds, 60 ) ) ) . wfMsg( 'seconds-abbrev' );
2607 } else {
2608 $hours = floor( $seconds / 3600 );
2609 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
2610 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
2611 return $this->formatNum( $hours ) . wfMsg( 'hours-abbrev' ) .
2612 $this->formatNum( $minutes ) . wfMsg( 'minutes-abbrev' ) .
2613 $this->formatNum( $secondsPart ) . wfMsg( 'seconds-abbrev' );
2614 }
2615 }
2616
2617 function formatBitrate( $bps ) {
2618 $units = array( 'bps', 'kbps', 'Mbps', 'Gbps' );
2619 if ( $bps <= 0 ) {
2620 return $this->formatNum( $bps ) . $units[0];
2621 }
2622 $unitIndex = floor( log10( $bps ) / 3 );
2623 $mantissa = $bps / pow( 1000, $unitIndex );
2624 if ( $mantissa < 10 ) {
2625 $mantissa = round( $mantissa, 1 );
2626 } else {
2627 $mantissa = round( $mantissa );
2628 }
2629 return $this->formatNum( $mantissa ) . $units[$unitIndex];
2630 }
2631
2632 /**
2633 * Format a size in bytes for output, using an appropriate
2634 * unit (B, KB, MB or GB) according to the magnitude in question
2635 *
2636 * @param $size Size to format
2637 * @return string Plain text (not HTML)
2638 */
2639 function formatSize( $size ) {
2640 // For small sizes no decimal places necessary
2641 $round = 0;
2642 if( $size > 1024 ) {
2643 $size = $size / 1024;
2644 if( $size > 1024 ) {
2645 $size = $size / 1024;
2646 // For MB and bigger two decimal places are smarter
2647 $round = 2;
2648 if( $size > 1024 ) {
2649 $size = $size / 1024;
2650 $msg = 'size-gigabytes';
2651 } else {
2652 $msg = 'size-megabytes';
2653 }
2654 } else {
2655 $msg = 'size-kilobytes';
2656 }
2657 } else {
2658 $msg = 'size-bytes';
2659 }
2660 $size = round( $size, $round );
2661 $text = $this->getMessageFromDB( $msg );
2662 return str_replace( '$1', $this->formatNum( $size ), $text );
2663 }
2664 }