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