MediaWiki  REL1_22
Language.php
Go to the documentation of this file.
00001 <?php
00028 if ( !defined( 'MEDIAWIKI' ) ) {
00029     echo "This file is part of MediaWiki, it is not a valid entry point.\n";
00030     exit( 1 );
00031 }
00032 
00033 if ( function_exists( 'mb_strtoupper' ) ) {
00034     mb_internal_encoding( 'UTF-8' );
00035 }
00036 
00042 class FakeConverter {
00046     public $mLang;
00047     function __construct( $langobj ) { $this->mLang = $langobj; }
00048     function autoConvert( $text, $variant = false ) { return $text; }
00049     function autoConvertToAllVariants( $text ) { return array( $this->mLang->getCode() => $text ); }
00050     function convert( $t ) { return $t; }
00051     function convertTo( $text, $variant ) { return $text; }
00052     function convertTitle( $t ) { return $t->getPrefixedText(); }
00053     function convertNamespace( $ns ) { return $this->mLang->getFormattedNsText( $ns ); }
00054     function getVariants() { return array( $this->mLang->getCode() ); }
00055     function getVariantFallbacks( $variant ) { return $this->mLang->getCode(); }
00056     function getPreferredVariant() { return $this->mLang->getCode(); }
00057     function getDefaultVariant() { return $this->mLang->getCode(); }
00058     function getURLVariant() { return ''; }
00059     function getConvRuleTitle() { return false; }
00060     function findVariantLink( &$l, &$n, $ignoreOtherCond = false ) { }
00061     function getExtraHashOptions() { return ''; }
00062     function getParsedTitle() { return ''; }
00063     function markNoConversion( $text, $noParse = false ) { return $text; }
00064     function convertCategoryKey( $key ) { return $key; }
00065     function convertLinkToAllVariants( $text ) { return $this->autoConvertToAllVariants( $text ); }
00067     function armourMath( $text ) { return $text; }
00068     function validateVariant( $variant = null ) { return $variant === $this->mLang->getCode() ? $variant : null; }
00069     function translate( $text, $variant ) { return $text; }
00070 }
00071 
00076 class Language {
00077 
00081     public $mConverter;
00082 
00083     public $mVariants, $mCode, $mLoaded = false;
00084     public $mMagicExtensions = array(), $mMagicHookDone = false;
00085     private $mHtmlCode = null, $mParentLanguage = false;
00086 
00087     public $dateFormatStrings = array();
00088     public $mExtendedSpecialPageAliases;
00089 
00090     protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
00091 
00095     public $transformData = array();
00096 
00100     static public $dataCache;
00101 
00102     static public $mLangObjCache = array();
00103 
00104     static public $mWeekdayMsgs = array(
00105         'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
00106         'friday', 'saturday'
00107     );
00108 
00109     static public $mWeekdayAbbrevMsgs = array(
00110         'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
00111     );
00112 
00113     static public $mMonthMsgs = array(
00114         'january', 'february', 'march', 'april', 'may_long', 'june',
00115         'july', 'august', 'september', 'october', 'november',
00116         'december'
00117     );
00118     static public $mMonthGenMsgs = array(
00119         'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
00120         'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
00121         'december-gen'
00122     );
00123     static public $mMonthAbbrevMsgs = array(
00124         'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
00125         'sep', 'oct', 'nov', 'dec'
00126     );
00127 
00128     static public $mIranianCalendarMonthMsgs = array(
00129         'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
00130         'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
00131         'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
00132         'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
00133     );
00134 
00135     static public $mHebrewCalendarMonthMsgs = array(
00136         'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
00137         'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
00138         'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
00139         'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
00140         'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
00141     );
00142 
00143     static public $mHebrewCalendarMonthGenMsgs = array(
00144         'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
00145         'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
00146         'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
00147         'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
00148         'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
00149     );
00150 
00151     static public $mHijriCalendarMonthMsgs = array(
00152         'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
00153         'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
00154         'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
00155         'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
00156     );
00157 
00162     static public $durationIntervals = array(
00163         'millennia' => 31556952000,
00164         'centuries' => 3155695200,
00165         'decades' => 315569520,
00166         'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
00167         'weeks' => 604800,
00168         'days' => 86400,
00169         'hours' => 3600,
00170         'minutes' => 60,
00171         'seconds' => 1,
00172     );
00173 
00180     static private $fallbackLanguageCache = array();
00181 
00187     static function factory( $code ) {
00188         global $wgDummyLanguageCodes, $wgLangObjCacheSize;
00189 
00190         if ( isset( $wgDummyLanguageCodes[$code] ) ) {
00191             $code = $wgDummyLanguageCodes[$code];
00192         }
00193 
00194         // get the language object to process
00195         $langObj = isset( self::$mLangObjCache[$code] )
00196             ? self::$mLangObjCache[$code]
00197             : self::newFromCode( $code );
00198 
00199         // merge the language object in to get it up front in the cache
00200         self::$mLangObjCache = array_merge( array( $code => $langObj ), self::$mLangObjCache );
00201         // get rid of the oldest ones in case we have an overflow
00202         self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
00203 
00204         return $langObj;
00205     }
00206 
00213     protected static function newFromCode( $code ) {
00214         // Protect against path traversal below
00215         if ( !Language::isValidCode( $code )
00216             || strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
00217         {
00218             throw new MWException( "Invalid language code \"$code\"" );
00219         }
00220 
00221         if ( !Language::isValidBuiltInCode( $code ) ) {
00222             // It's not possible to customise this code with class files, so
00223             // just return a Language object. This is to support uselang= hacks.
00224             $lang = new Language;
00225             $lang->setCode( $code );
00226             return $lang;
00227         }
00228 
00229         // Check if there is a language class for the code
00230         $class = self::classFromCode( $code );
00231         self::preloadLanguageClass( $class );
00232         if ( class_exists( $class ) ) {
00233             $lang = new $class;
00234             return $lang;
00235         }
00236 
00237         // Keep trying the fallback list until we find an existing class
00238         $fallbacks = Language::getFallbacksFor( $code );
00239         foreach ( $fallbacks as $fallbackCode ) {
00240             if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
00241                 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
00242             }
00243 
00244             $class = self::classFromCode( $fallbackCode );
00245             self::preloadLanguageClass( $class );
00246             if ( class_exists( $class ) ) {
00247                 $lang = Language::newFromCode( $fallbackCode );
00248                 $lang->setCode( $code );
00249                 return $lang;
00250             }
00251         }
00252 
00253         throw new MWException( "Invalid fallback sequence for language '$code'" );
00254     }
00255 
00264     public static function isSupportedLanguage( $code ) {
00265         return $code === strtolower( $code ) && is_readable( self::getMessagesFileName( $code ) );
00266     }
00267 
00283     public static function isWellFormedLanguageTag( $code, $lenient = false ) {
00284         $alpha = '[a-z]';
00285         $digit = '[0-9]';
00286         $alphanum = '[a-z0-9]';
00287         $x = 'x'; # private use singleton
00288         $singleton = '[a-wy-z]'; # other singleton
00289         $s = $lenient ? '[-_]' : '-';
00290 
00291         $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
00292         $script = "$alpha{4}"; # ISO 15924
00293         $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
00294         $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
00295         $extension = "$singleton(?:$s$alphanum{2,8})+";
00296         $privateUse = "$x(?:$s$alphanum{1,8})+";
00297 
00298         # Define certain grandfathered codes, since otherwise the regex is pretty useless.
00299         # Since these are limited, this is safe even later changes to the registry --
00300         # the only oddity is that it might change the type of the tag, and thus
00301         # the results from the capturing groups.
00302         # http://www.iana.org/assignments/language-subtag-registry
00303 
00304         $grandfathered = "en{$s}GB{$s}oed"
00305             . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
00306             . "|no{$s}(?:bok|nyn)"
00307             . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
00308             . "|zh{$s}min{$s}nan";
00309 
00310         $variantList = "$variant(?:$s$variant)*";
00311         $extensionList = "$extension(?:$s$extension)*";
00312 
00313         $langtag = "(?:($language)"
00314             . "(?:$s$script)?"
00315             . "(?:$s$region)?"
00316             . "(?:$s$variantList)?"
00317             . "(?:$s$extensionList)?"
00318             . "(?:$s$privateUse)?)";
00319 
00320         # The final breakdown, with capturing groups for each of these components
00321         # The variants, extensions, grandfathered, and private-use may have interior '-'
00322 
00323         $root = "^(?:$langtag|$privateUse|$grandfathered)$";
00324 
00325         return (bool)preg_match( "/$root/", strtolower( $code ) );
00326     }
00327 
00337     public static function isValidCode( $code ) {
00338         static $cache = array();
00339         if ( isset( $cache[$code] ) ) {
00340             return $cache[$code];
00341         }
00342         // People think language codes are html safe, so enforce it.
00343         // Ideally we should only allow a-zA-Z0-9-
00344         // but, .+ and other chars are often used for {{int:}} hacks
00345         // see bugs 37564, 37587, 36938
00346         $cache[$code] =
00347             strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
00348             && !preg_match( Title::getTitleInvalidRegex(), $code );
00349 
00350         return $cache[$code];
00351     }
00352 
00363     public static function isValidBuiltInCode( $code ) {
00364 
00365         if ( !is_string( $code ) ) {
00366             if ( is_object( $code ) ) {
00367                 $addmsg = " of class " . get_class( $code );
00368             } else {
00369                 $addmsg = '';
00370             }
00371             $type = gettype( $code );
00372             throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
00373         }
00374 
00375         return (bool)preg_match( '/^[a-z0-9-]{2,}$/i', $code );
00376     }
00377 
00386     public static function isKnownLanguageTag( $tag ) {
00387         static $coreLanguageNames;
00388 
00389         // Quick escape for invalid input to avoid exceptions down the line
00390         // when code tries to process tags which are not valid at all.
00391         if ( !self::isValidBuiltInCode( $tag ) ) {
00392             return false;
00393         }
00394 
00395         if ( $coreLanguageNames === null ) {
00396             global $IP;
00397             include "$IP/languages/Names.php";
00398         }
00399 
00400         if ( isset( $coreLanguageNames[$tag] )
00401             || self::fetchLanguageName( $tag, $tag ) !== ''
00402         ) {
00403             return true;
00404         }
00405 
00406         return false;
00407     }
00408 
00413     public static function classFromCode( $code ) {
00414         if ( $code == 'en' ) {
00415             return 'Language';
00416         } else {
00417             return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
00418         }
00419     }
00420 
00426     public static function preloadLanguageClass( $class ) {
00427         global $IP;
00428 
00429         if ( $class === 'Language' ) {
00430             return;
00431         }
00432 
00433         if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
00434             include_once "$IP/languages/classes/$class.php";
00435         }
00436     }
00437 
00443     public static function getLocalisationCache() {
00444         if ( is_null( self::$dataCache ) ) {
00445             global $wgLocalisationCacheConf;
00446             $class = $wgLocalisationCacheConf['class'];
00447             self::$dataCache = new $class( $wgLocalisationCacheConf );
00448         }
00449         return self::$dataCache;
00450     }
00451 
00452     function __construct() {
00453         $this->mConverter = new FakeConverter( $this );
00454         // Set the code to the name of the descendant
00455         if ( get_class( $this ) == 'Language' ) {
00456             $this->mCode = 'en';
00457         } else {
00458             $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
00459         }
00460         self::getLocalisationCache();
00461     }
00462 
00466     function __destruct() {
00467         foreach ( $this as $name => $value ) {
00468             unset( $this->$name );
00469         }
00470     }
00471 
00476     function initContLang() { }
00477 
00483     function getFallbackLanguageCode() {
00484         wfDeprecated( __METHOD__, '1.19' );
00485         return self::getFallbackFor( $this->mCode );
00486     }
00487 
00492     function getFallbackLanguages() {
00493         return self::getFallbacksFor( $this->mCode );
00494     }
00495 
00500     function getBookstoreList() {
00501         return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
00502     }
00503 
00510     public function getNamespaces() {
00511         if ( is_null( $this->namespaceNames ) ) {
00512             global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
00513 
00514             $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
00515             $validNamespaces = MWNamespace::getCanonicalNamespaces();
00516 
00517             $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
00518 
00519             $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
00520             if ( $wgMetaNamespaceTalk ) {
00521                 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
00522             } else {
00523                 $talk = $this->namespaceNames[NS_PROJECT_TALK];
00524                 $this->namespaceNames[NS_PROJECT_TALK] =
00525                     $this->fixVariableInNamespace( $talk );
00526             }
00527 
00528             # Sometimes a language will be localised but not actually exist on this wiki.
00529             foreach ( $this->namespaceNames as $key => $text ) {
00530                 if ( !isset( $validNamespaces[$key] ) ) {
00531                     unset( $this->namespaceNames[$key] );
00532                 }
00533             }
00534 
00535             # The above mixing may leave namespaces out of canonical order.
00536             # Re-order by namespace ID number...
00537             ksort( $this->namespaceNames );
00538 
00539             wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames ) );
00540         }
00541         return $this->namespaceNames;
00542     }
00543 
00548     public function setNamespaces( array $namespaces ) {
00549         $this->namespaceNames = $namespaces;
00550         $this->mNamespaceIds = null;
00551     }
00552 
00556     public function resetNamespaces() {
00557         $this->namespaceNames = null;
00558         $this->mNamespaceIds = null;
00559         $this->namespaceAliases = null;
00560     }
00561 
00570     function getFormattedNamespaces() {
00571         $ns = $this->getNamespaces();
00572         foreach ( $ns as $k => $v ) {
00573             $ns[$k] = strtr( $v, '_', ' ' );
00574         }
00575         return $ns;
00576     }
00577 
00588     function getNsText( $index ) {
00589         $ns = $this->getNamespaces();
00590         return isset( $ns[$index] ) ? $ns[$index] : false;
00591     }
00592 
00606     function getFormattedNsText( $index ) {
00607         $ns = $this->getNsText( $index );
00608         return strtr( $ns, '_', ' ' );
00609     }
00610 
00618     function getGenderNsText( $index, $gender ) {
00619         global $wgExtraGenderNamespaces;
00620 
00621         $ns = $wgExtraGenderNamespaces + self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
00622         return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
00623     }
00624 
00631     function needsGenderDistinction() {
00632         global $wgExtraGenderNamespaces, $wgExtraNamespaces;
00633         if ( count( $wgExtraGenderNamespaces ) > 0 ) {
00634             // $wgExtraGenderNamespaces overrides everything
00635             return true;
00636         } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
00638             // $wgExtraNamespaces overrides any gender aliases specified in i18n files
00639             return false;
00640         } else {
00641             // Check what is in i18n files
00642             $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
00643             return count( $aliases ) > 0;
00644         }
00645     }
00646 
00655     function getLocalNsIndex( $text ) {
00656         $lctext = $this->lc( $text );
00657         $ids = $this->getNamespaceIds();
00658         return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
00659     }
00660 
00664     function getNamespaceAliases() {
00665         if ( is_null( $this->namespaceAliases ) ) {
00666             $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
00667             if ( !$aliases ) {
00668                 $aliases = array();
00669             } else {
00670                 foreach ( $aliases as $name => $index ) {
00671                     if ( $index === NS_PROJECT_TALK ) {
00672                         unset( $aliases[$name] );
00673                         $name = $this->fixVariableInNamespace( $name );
00674                         $aliases[$name] = $index;
00675                     }
00676                 }
00677             }
00678 
00679             global $wgExtraGenderNamespaces;
00680             $genders = $wgExtraGenderNamespaces + (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
00681             foreach ( $genders as $index => $forms ) {
00682                 foreach ( $forms as $alias ) {
00683                     $aliases[$alias] = $index;
00684                 }
00685             }
00686 
00687             # Also add converted namespace names as aliases, to avoid confusion.
00688             $convertedNames = array();
00689             foreach ( $this->getVariants() as $variant ) {
00690                 if ( $variant === $this->mCode ) {
00691                     continue;
00692                 }
00693                 foreach ( $this->getNamespaces() as $ns => $_ ) {
00694                     $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
00695                 }
00696             }
00697 
00698             $this->namespaceAliases = $aliases + $convertedNames;
00699         }
00700         return $this->namespaceAliases;
00701     }
00702 
00706     function getNamespaceIds() {
00707         if ( is_null( $this->mNamespaceIds ) ) {
00708             global $wgNamespaceAliases;
00709             # Put namespace names and aliases into a hashtable.
00710             # If this is too slow, then we should arrange it so that it is done
00711             # before caching. The catch is that at pre-cache time, the above
00712             # class-specific fixup hasn't been done.
00713             $this->mNamespaceIds = array();
00714             foreach ( $this->getNamespaces() as $index => $name ) {
00715                 $this->mNamespaceIds[$this->lc( $name )] = $index;
00716             }
00717             foreach ( $this->getNamespaceAliases() as $name => $index ) {
00718                 $this->mNamespaceIds[$this->lc( $name )] = $index;
00719             }
00720             if ( $wgNamespaceAliases ) {
00721                 foreach ( $wgNamespaceAliases as $name => $index ) {
00722                     $this->mNamespaceIds[$this->lc( $name )] = $index;
00723                 }
00724             }
00725         }
00726         return $this->mNamespaceIds;
00727     }
00728 
00736     function getNsIndex( $text ) {
00737         $lctext = $this->lc( $text );
00738         $ns = MWNamespace::getCanonicalIndex( $lctext );
00739         if ( $ns !== null ) {
00740             return $ns;
00741         }
00742         $ids = $this->getNamespaceIds();
00743         return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
00744     }
00745 
00753     function getVariantname( $code, $usemsg = true ) {
00754         $msg = "variantname-$code";
00755         if ( $usemsg && wfMessage( $msg )->exists() ) {
00756             return $this->getMessageFromDB( $msg );
00757         }
00758         $name = self::fetchLanguageName( $code );
00759         if ( $name ) {
00760             return $name; # if it's defined as a language name, show that
00761         } else {
00762             # otherwise, output the language code
00763             return $code;
00764         }
00765     }
00766 
00771     function specialPage( $name ) {
00772         $aliases = $this->getSpecialPageAliases();
00773         if ( isset( $aliases[$name][0] ) ) {
00774             $name = $aliases[$name][0];
00775         }
00776         return $this->getNsText( NS_SPECIAL ) . ':' . $name;
00777     }
00778 
00782     function getDatePreferences() {
00783         return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
00784     }
00785 
00789     function getDateFormats() {
00790         return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
00791     }
00792 
00796     function getDefaultDateFormat() {
00797         $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
00798         if ( $df === 'dmy or mdy' ) {
00799             global $wgAmericanDates;
00800             return $wgAmericanDates ? 'mdy' : 'dmy';
00801         } else {
00802             return $df;
00803         }
00804     }
00805 
00809     function getDatePreferenceMigrationMap() {
00810         return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
00811     }
00812 
00817     function getImageFile( $image ) {
00818         return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
00819     }
00820 
00824     function getExtraUserToggles() {
00825         return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
00826     }
00827 
00832     function getUserToggle( $tog ) {
00833         return $this->getMessageFromDB( "tog-$tog" );
00834     }
00835 
00846     public static function getLanguageNames( $customisedOnly = false ) {
00847         return self::fetchLanguageNames( null, $customisedOnly ? 'mwfile' : 'mw' );
00848     }
00849 
00859     public static function getTranslatedLanguageNames( $code ) {
00860         return self::fetchLanguageNames( $code, 'all' );
00861     }
00862 
00874     public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
00875         global $wgExtraLanguageNames;
00876         static $coreLanguageNames;
00877 
00878         if ( $coreLanguageNames === null ) {
00879             global $IP;
00880             include "$IP/languages/Names.php";
00881         }
00882 
00883         $names = array();
00884 
00885         if ( $inLanguage ) {
00886             # TODO: also include when $inLanguage is null, when this code is more efficient
00887             wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
00888         }
00889 
00890         $mwNames = $wgExtraLanguageNames + $coreLanguageNames;
00891         foreach ( $mwNames as $mwCode => $mwName ) {
00892             # - Prefer own MediaWiki native name when not using the hook
00893             # - For other names just add if not added through the hook
00894             if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
00895                 $names[$mwCode] = $mwName;
00896             }
00897         }
00898 
00899         if ( $include === 'all' ) {
00900             return $names;
00901         }
00902 
00903         $returnMw = array();
00904         $coreCodes = array_keys( $mwNames );
00905         foreach ( $coreCodes as $coreCode ) {
00906             $returnMw[$coreCode] = $names[$coreCode];
00907         }
00908 
00909         if ( $include === 'mwfile' ) {
00910             $namesMwFile = array();
00911             # We do this using a foreach over the codes instead of a directory
00912             # loop so that messages files in extensions will work correctly.
00913             foreach ( $returnMw as $code => $value ) {
00914                 if ( is_readable( self::getMessagesFileName( $code ) ) ) {
00915                     $namesMwFile[$code] = $names[$code];
00916                 }
00917             }
00918             return $namesMwFile;
00919         }
00920         # 'mw' option; default if it's not one of the other two options (all/mwfile)
00921         return $returnMw;
00922     }
00923 
00931     public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
00932         $array = self::fetchLanguageNames( $inLanguage, $include );
00933         return !array_key_exists( $code, $array ) ? '' : $array[$code];
00934     }
00935 
00942     function getMessageFromDB( $msg ) {
00943         return wfMessage( $msg )->inLanguage( $this )->text();
00944     }
00945 
00953     function getLanguageName( $code ) {
00954         return self::fetchLanguageName( $code );
00955     }
00956 
00961     function getMonthName( $key ) {
00962         return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
00963     }
00964 
00968     function getMonthNamesArray() {
00969         $monthNames = array( '' );
00970         for ( $i = 1; $i < 13; $i++ ) {
00971             $monthNames[] = $this->getMonthName( $i );
00972         }
00973         return $monthNames;
00974     }
00975 
00980     function getMonthNameGen( $key ) {
00981         return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
00982     }
00983 
00988     function getMonthAbbreviation( $key ) {
00989         return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
00990     }
00991 
00995     function getMonthAbbreviationsArray() {
00996         $monthNames = array( '' );
00997         for ( $i = 1; $i < 13; $i++ ) {
00998             $monthNames[] = $this->getMonthAbbreviation( $i );
00999         }
01000         return $monthNames;
01001     }
01002 
01007     function getWeekdayName( $key ) {
01008         return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
01009     }
01010 
01015     function getWeekdayAbbreviation( $key ) {
01016         return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
01017     }
01018 
01023     function getIranianCalendarMonthName( $key ) {
01024         return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
01025     }
01026 
01031     function getHebrewCalendarMonthName( $key ) {
01032         return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
01033     }
01034 
01039     function getHebrewCalendarMonthNameGen( $key ) {
01040         return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
01041     }
01042 
01047     function getHijriCalendarMonthName( $key ) {
01048         return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
01049     }
01050 
01116     function sprintfDate( $format, $ts, DateTimeZone $zone = null ) {
01117         $s = '';
01118         $raw = false;
01119         $roman = false;
01120         $hebrewNum = false;
01121         $dateTimeObj = false;
01122         $rawToggle = false;
01123         $iranian = false;
01124         $hebrew = false;
01125         $hijri = false;
01126         $thai = false;
01127         $minguo = false;
01128         $tenno = false;
01129 
01130         if ( strlen( $ts ) !== 14 ) {
01131             throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
01132         }
01133 
01134         if ( !ctype_digit( $ts ) ) {
01135             throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
01136         }
01137 
01138         for ( $p = 0; $p < strlen( $format ); $p++ ) {
01139             $num = false;
01140             $code = $format[$p];
01141             if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
01142                 $code .= $format[++$p];
01143             }
01144 
01145             if ( ( $code === 'xi' || $code == 'xj' || $code == 'xk' || $code == 'xm' || $code == 'xo' || $code == 'xt' ) && $p < strlen( $format ) - 1 ) {
01146                 $code .= $format[++$p];
01147             }
01148 
01149             switch ( $code ) {
01150                 case 'xx':
01151                     $s .= 'x';
01152                     break;
01153                 case 'xn':
01154                     $raw = true;
01155                     break;
01156                 case 'xN':
01157                     $rawToggle = !$rawToggle;
01158                     break;
01159                 case 'xr':
01160                     $roman = true;
01161                     break;
01162                 case 'xh':
01163                     $hebrewNum = true;
01164                     break;
01165                 case 'xg':
01166                     $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
01167                     break;
01168                 case 'xjx':
01169                     if ( !$hebrew ) {
01170                         $hebrew = self::tsToHebrew( $ts );
01171                     }
01172                     $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
01173                     break;
01174                 case 'd':
01175                     $num = substr( $ts, 6, 2 );
01176                     break;
01177                 case 'D':
01178                     if ( !$dateTimeObj ) {
01179                         $dateTimeObj = DateTime::createFromFormat(
01180                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01181                         );
01182                     }
01183                     $s .= $this->getWeekdayAbbreviation( $dateTimeObj->format( 'w' ) + 1 );
01184                     break;
01185                 case 'j':
01186                     $num = intval( substr( $ts, 6, 2 ) );
01187                     break;
01188                 case 'xij':
01189                     if ( !$iranian ) {
01190                         $iranian = self::tsToIranian( $ts );
01191                     }
01192                     $num = $iranian[2];
01193                     break;
01194                 case 'xmj':
01195                     if ( !$hijri ) {
01196                         $hijri = self::tsToHijri( $ts );
01197                     }
01198                     $num = $hijri[2];
01199                     break;
01200                 case 'xjj':
01201                     if ( !$hebrew ) {
01202                         $hebrew = self::tsToHebrew( $ts );
01203                     }
01204                     $num = $hebrew[2];
01205                     break;
01206                 case 'l':
01207                     if ( !$dateTimeObj ) {
01208                         $dateTimeObj = DateTime::createFromFormat(
01209                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01210                         );
01211                     }
01212                     $s .= $this->getWeekdayName( $dateTimeObj->format( 'w' ) + 1 );
01213                     break;
01214                 case 'F':
01215                     $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
01216                     break;
01217                 case 'xiF':
01218                     if ( !$iranian ) {
01219                         $iranian = self::tsToIranian( $ts );
01220                     }
01221                     $s .= $this->getIranianCalendarMonthName( $iranian[1] );
01222                     break;
01223                 case 'xmF':
01224                     if ( !$hijri ) {
01225                         $hijri = self::tsToHijri( $ts );
01226                     }
01227                     $s .= $this->getHijriCalendarMonthName( $hijri[1] );
01228                     break;
01229                 case 'xjF':
01230                     if ( !$hebrew ) {
01231                         $hebrew = self::tsToHebrew( $ts );
01232                     }
01233                     $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
01234                     break;
01235                 case 'm':
01236                     $num = substr( $ts, 4, 2 );
01237                     break;
01238                 case 'M':
01239                     $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
01240                     break;
01241                 case 'n':
01242                     $num = intval( substr( $ts, 4, 2 ) );
01243                     break;
01244                 case 'xin':
01245                     if ( !$iranian ) {
01246                         $iranian = self::tsToIranian( $ts );
01247                     }
01248                     $num = $iranian[1];
01249                     break;
01250                 case 'xmn':
01251                     if ( !$hijri ) {
01252                         $hijri = self::tsToHijri ( $ts );
01253                     }
01254                     $num = $hijri[1];
01255                     break;
01256                 case 'xjn':
01257                     if ( !$hebrew ) {
01258                         $hebrew = self::tsToHebrew( $ts );
01259                     }
01260                     $num = $hebrew[1];
01261                     break;
01262                 case 'xjt':
01263                     if ( !$hebrew ) {
01264                         $hebrew = self::tsToHebrew( $ts );
01265                     }
01266                     $num = $hebrew[3];
01267                     break;
01268                 case 'Y':
01269                     $num = substr( $ts, 0, 4 );
01270                     break;
01271                 case 'xiY':
01272                     if ( !$iranian ) {
01273                         $iranian = self::tsToIranian( $ts );
01274                     }
01275                     $num = $iranian[0];
01276                     break;
01277                 case 'xmY':
01278                     if ( !$hijri ) {
01279                         $hijri = self::tsToHijri( $ts );
01280                     }
01281                     $num = $hijri[0];
01282                     break;
01283                 case 'xjY':
01284                     if ( !$hebrew ) {
01285                         $hebrew = self::tsToHebrew( $ts );
01286                     }
01287                     $num = $hebrew[0];
01288                     break;
01289                 case 'xkY':
01290                     if ( !$thai ) {
01291                         $thai = self::tsToYear( $ts, 'thai' );
01292                     }
01293                     $num = $thai[0];
01294                     break;
01295                 case 'xoY':
01296                     if ( !$minguo ) {
01297                         $minguo = self::tsToYear( $ts, 'minguo' );
01298                     }
01299                     $num = $minguo[0];
01300                     break;
01301                 case 'xtY':
01302                     if ( !$tenno ) {
01303                         $tenno = self::tsToYear( $ts, 'tenno' );
01304                     }
01305                     $num = $tenno[0];
01306                     break;
01307                 case 'y':
01308                     $num = substr( $ts, 2, 2 );
01309                     break;
01310                 case 'xiy':
01311                     if ( !$iranian ) {
01312                         $iranian = self::tsToIranian( $ts );
01313                     }
01314                     $num = substr( $iranian[0], -2 );
01315                     break;
01316                 case 'a':
01317                     $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
01318                     break;
01319                 case 'A':
01320                     $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
01321                     break;
01322                 case 'g':
01323                     $h = substr( $ts, 8, 2 );
01324                     $num = $h % 12 ? $h % 12 : 12;
01325                     break;
01326                 case 'G':
01327                     $num = intval( substr( $ts, 8, 2 ) );
01328                     break;
01329                 case 'h':
01330                     $h = substr( $ts, 8, 2 );
01331                     $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
01332                     break;
01333                 case 'H':
01334                     $num = substr( $ts, 8, 2 );
01335                     break;
01336                 case 'i':
01337                     $num = substr( $ts, 10, 2 );
01338                     break;
01339                 case 's':
01340                     $num = substr( $ts, 12, 2 );
01341                     break;
01342                 case 'c':
01343                 case 'r':
01344                 case 'e':
01345                 case 'O':
01346                 case 'P':
01347                 case 'T':
01348                     // Pass through string from $dateTimeObj->format()
01349                     if ( !$dateTimeObj ) {
01350                         $dateTimeObj = DateTime::createFromFormat(
01351                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01352                         );
01353                     }
01354                     $s .= $dateTimeObj->format( $code );
01355                     break;
01356                 case 'w':
01357                 case 'N':
01358                 case 'z':
01359                 case 'W':
01360                 case 't':
01361                 case 'L':
01362                 case 'o':
01363                 case 'U':
01364                 case 'I':
01365                 case 'Z':
01366                     // Pass through number from $dateTimeObj->format()
01367                     if ( !$dateTimeObj ) {
01368                         $dateTimeObj = DateTime::createFromFormat(
01369                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01370                         );
01371                     }
01372                     $num = $dateTimeObj->format( $code );
01373                     break;
01374                 case '\\':
01375                     # Backslash escaping
01376                     if ( $p < strlen( $format ) - 1 ) {
01377                         $s .= $format[++$p];
01378                     } else {
01379                         $s .= '\\';
01380                     }
01381                     break;
01382                 case '"':
01383                     # Quoted literal
01384                     if ( $p < strlen( $format ) - 1 ) {
01385                         $endQuote = strpos( $format, '"', $p + 1 );
01386                         if ( $endQuote === false ) {
01387                             # No terminating quote, assume literal "
01388                             $s .= '"';
01389                         } else {
01390                             $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
01391                             $p = $endQuote;
01392                         }
01393                     } else {
01394                         # Quote at end of string, assume literal "
01395                         $s .= '"';
01396                     }
01397                     break;
01398                 default:
01399                     $s .= $format[$p];
01400             }
01401             if ( $num !== false ) {
01402                 if ( $rawToggle || $raw ) {
01403                     $s .= $num;
01404                     $raw = false;
01405                 } elseif ( $roman ) {
01406                     $s .= Language::romanNumeral( $num );
01407                     $roman = false;
01408                 } elseif ( $hebrewNum ) {
01409                     $s .= self::hebrewNumeral( $num );
01410                     $hebrewNum = false;
01411                 } else {
01412                     $s .= $this->formatNum( $num, true );
01413                 }
01414             }
01415         }
01416         return $s;
01417     }
01418 
01419     private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
01420     private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
01421 
01434     private static function tsToIranian( $ts ) {
01435         $gy = substr( $ts, 0, 4 ) -1600;
01436         $gm = substr( $ts, 4, 2 ) -1;
01437         $gd = substr( $ts, 6, 2 ) -1;
01438 
01439         # Days passed from the beginning (including leap years)
01440         $gDayNo = 365 * $gy
01441             + floor( ( $gy + 3 ) / 4 )
01442             - floor( ( $gy + 99 ) / 100 )
01443             + floor( ( $gy + 399 ) / 400 );
01444 
01445         // Add days of the past months of this year
01446         for ( $i = 0; $i < $gm; $i++ ) {
01447             $gDayNo += self::$GREG_DAYS[$i];
01448         }
01449 
01450         // Leap years
01451         if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
01452             $gDayNo++;
01453         }
01454 
01455         // Days passed in current month
01456         $gDayNo += (int)$gd;
01457 
01458         $jDayNo = $gDayNo - 79;
01459 
01460         $jNp = floor( $jDayNo / 12053 );
01461         $jDayNo %= 12053;
01462 
01463         $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
01464         $jDayNo %= 1461;
01465 
01466         if ( $jDayNo >= 366 ) {
01467             $jy += floor( ( $jDayNo - 1 ) / 365 );
01468             $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
01469         }
01470 
01471         for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
01472             $jDayNo -= self::$IRANIAN_DAYS[$i];
01473         }
01474 
01475         $jm = $i + 1;
01476         $jd = $jDayNo + 1;
01477 
01478         return array( $jy, $jm, $jd );
01479     }
01480 
01492     private static function tsToHijri( $ts ) {
01493         $year = substr( $ts, 0, 4 );
01494         $month = substr( $ts, 4, 2 );
01495         $day = substr( $ts, 6, 2 );
01496 
01497         $zyr = $year;
01498         $zd = $day;
01499         $zm = $month;
01500         $zy = $zyr;
01501 
01502         if (
01503             ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
01504             ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
01505         )
01506         {
01507             $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
01508                     (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
01509                     (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
01510                     $zd - 32075;
01511         } else {
01512             $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
01513                                 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
01514         }
01515 
01516         $zl = $zjd -1948440 + 10632;
01517         $zn = (int)( ( $zl - 1 ) / 10631 );
01518         $zl = $zl - 10631 * $zn + 354;
01519         $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) + ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
01520         $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
01521         $zm = (int)( ( 24 * $zl ) / 709 );
01522         $zd = $zl - (int)( ( 709 * $zm ) / 24 );
01523         $zy = 30 * $zn + $zj - 30;
01524 
01525         return array( $zy, $zm, $zd );
01526     }
01527 
01543     private static function tsToHebrew( $ts ) {
01544         # Parse date
01545         $year = substr( $ts, 0, 4 );
01546         $month = substr( $ts, 4, 2 );
01547         $day = substr( $ts, 6, 2 );
01548 
01549         # Calculate Hebrew year
01550         $hebrewYear = $year + 3760;
01551 
01552         # Month number when September = 1, August = 12
01553         $month += 4;
01554         if ( $month > 12 ) {
01555             # Next year
01556             $month -= 12;
01557             $year++;
01558             $hebrewYear++;
01559         }
01560 
01561         # Calculate day of year from 1 September
01562         $dayOfYear = $day;
01563         for ( $i = 1; $i < $month; $i++ ) {
01564             if ( $i == 6 ) {
01565                 # February
01566                 $dayOfYear += 28;
01567                 # Check if the year is leap
01568                 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
01569                     $dayOfYear++;
01570                 }
01571             } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
01572                 $dayOfYear += 30;
01573             } else {
01574                 $dayOfYear += 31;
01575             }
01576         }
01577 
01578         # Calculate the start of the Hebrew year
01579         $start = self::hebrewYearStart( $hebrewYear );
01580 
01581         # Calculate next year's start
01582         if ( $dayOfYear <= $start ) {
01583             # Day is before the start of the year - it is the previous year
01584             # Next year's start
01585             $nextStart = $start;
01586             # Previous year
01587             $year--;
01588             $hebrewYear--;
01589             # Add days since previous year's 1 September
01590             $dayOfYear += 365;
01591             if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
01592                 # Leap year
01593                 $dayOfYear++;
01594             }
01595             # Start of the new (previous) year
01596             $start = self::hebrewYearStart( $hebrewYear );
01597         } else {
01598             # Next year's start
01599             $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
01600         }
01601 
01602         # Calculate Hebrew day of year
01603         $hebrewDayOfYear = $dayOfYear - $start;
01604 
01605         # Difference between year's days
01606         $diff = $nextStart - $start;
01607         # Add 12 (or 13 for leap years) days to ignore the difference between
01608         # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
01609         # difference is only about the year type
01610         if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
01611             $diff += 13;
01612         } else {
01613             $diff += 12;
01614         }
01615 
01616         # Check the year pattern, and is leap year
01617         # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
01618         # This is mod 30, to work on both leap years (which add 30 days of Adar I)
01619         # and non-leap years
01620         $yearPattern = $diff % 30;
01621         # Check if leap year
01622         $isLeap = $diff >= 30;
01623 
01624         # Calculate day in the month from number of day in the Hebrew year
01625         # Don't check Adar - if the day is not in Adar, we will stop before;
01626         # if it is in Adar, we will use it to check if it is Adar I or Adar II
01627         $hebrewDay = $hebrewDayOfYear;
01628         $hebrewMonth = 1;
01629         $days = 0;
01630         while ( $hebrewMonth <= 12 ) {
01631             # Calculate days in this month
01632             if ( $isLeap && $hebrewMonth == 6 ) {
01633                 # Adar in a leap year
01634                 if ( $isLeap ) {
01635                     # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
01636                     $days = 30;
01637                     if ( $hebrewDay <= $days ) {
01638                         # Day in Adar I
01639                         $hebrewMonth = 13;
01640                     } else {
01641                         # Subtract the days of Adar I
01642                         $hebrewDay -= $days;
01643                         # Try Adar II
01644                         $days = 29;
01645                         if ( $hebrewDay <= $days ) {
01646                             # Day in Adar II
01647                             $hebrewMonth = 14;
01648                         }
01649                     }
01650                 }
01651             } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
01652                 # Cheshvan in a complete year (otherwise as the rule below)
01653                 $days = 30;
01654             } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
01655                 # Kislev in an incomplete year (otherwise as the rule below)
01656                 $days = 29;
01657             } else {
01658                 # Odd months have 30 days, even have 29
01659                 $days = 30 - ( $hebrewMonth - 1 ) % 2;
01660             }
01661             if ( $hebrewDay <= $days ) {
01662                 # In the current month
01663                 break;
01664             } else {
01665                 # Subtract the days of the current month
01666                 $hebrewDay -= $days;
01667                 # Try in the next month
01668                 $hebrewMonth++;
01669             }
01670         }
01671 
01672         return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
01673     }
01674 
01684     private static function hebrewYearStart( $year ) {
01685         $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
01686         $b = intval( ( $year - 1 ) % 4 );
01687         $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
01688         if ( $m < 0 ) {
01689             $m--;
01690         }
01691         $Mar = intval( $m );
01692         if ( $m < 0 ) {
01693             $m++;
01694         }
01695         $m -= $Mar;
01696 
01697         $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
01698         if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
01699             $Mar++;
01700         } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
01701             $Mar += 2;
01702         } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
01703             $Mar++;
01704         }
01705 
01706         $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
01707         return $Mar;
01708     }
01709 
01722     private static function tsToYear( $ts, $cName ) {
01723         $gy = substr( $ts, 0, 4 );
01724         $gm = substr( $ts, 4, 2 );
01725         $gd = substr( $ts, 6, 2 );
01726 
01727         if ( !strcmp( $cName, 'thai' ) ) {
01728             # Thai solar dates
01729             # Add 543 years to the Gregorian calendar
01730             # Months and days are identical
01731             $gy_offset = $gy + 543;
01732         } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
01733             # Minguo dates
01734             # Deduct 1911 years from the Gregorian calendar
01735             # Months and days are identical
01736             $gy_offset = $gy - 1911;
01737         } elseif ( !strcmp( $cName, 'tenno' ) ) {
01738             # Nengō dates up to Meiji period
01739             # Deduct years from the Gregorian calendar
01740             # depending on the nengo periods
01741             # Months and days are identical
01742             if ( ( $gy < 1912 ) || ( ( $gy == 1912 ) && ( $gm < 7 ) ) || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
01743                 # Meiji period
01744                 $gy_gannen = $gy - 1868 + 1;
01745                 $gy_offset = $gy_gannen;
01746                 if ( $gy_gannen == 1 ) {
01747                     $gy_offset = '元';
01748                 }
01749                 $gy_offset = '明治' . $gy_offset;
01750             } elseif (
01751                 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
01752                 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
01753                 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
01754                 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
01755                 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
01756             )
01757             {
01758                 # Taishō period
01759                 $gy_gannen = $gy - 1912 + 1;
01760                 $gy_offset = $gy_gannen;
01761                 if ( $gy_gannen == 1 ) {
01762                     $gy_offset = '元';
01763                 }
01764                 $gy_offset = '大正' . $gy_offset;
01765             } elseif (
01766                 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
01767                 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
01768                 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
01769             )
01770             {
01771                 # Shōwa period
01772                 $gy_gannen = $gy - 1926 + 1;
01773                 $gy_offset = $gy_gannen;
01774                 if ( $gy_gannen == 1 ) {
01775                     $gy_offset = '元';
01776                 }
01777                 $gy_offset = '昭和' . $gy_offset;
01778             } else {
01779                 # Heisei period
01780                 $gy_gannen = $gy - 1989 + 1;
01781                 $gy_offset = $gy_gannen;
01782                 if ( $gy_gannen == 1 ) {
01783                     $gy_offset = '元';
01784                 }
01785                 $gy_offset = '平成' . $gy_offset;
01786             }
01787         } else {
01788             $gy_offset = $gy;
01789         }
01790 
01791         return array( $gy_offset, $gm, $gd );
01792     }
01793 
01801     static function romanNumeral( $num ) {
01802         static $table = array(
01803             array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
01804             array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
01805             array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
01806             array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM', 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
01807         );
01808 
01809         $num = intval( $num );
01810         if ( $num > 10000 || $num <= 0 ) {
01811             return $num;
01812         }
01813 
01814         $s = '';
01815         for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
01816             if ( $num >= $pow10 ) {
01817                 $s .= $table[$i][(int)floor( $num / $pow10 )];
01818             }
01819             $num = $num % $pow10;
01820         }
01821         return $s;
01822     }
01823 
01831     static function hebrewNumeral( $num ) {
01832         static $table = array(
01833             array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
01834             array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
01835             array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
01836             array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
01837         );
01838 
01839         $num = intval( $num );
01840         if ( $num > 9999 || $num <= 0 ) {
01841             return $num;
01842         }
01843 
01844         $s = '';
01845         for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
01846             if ( $num >= $pow10 ) {
01847                 if ( $num == 15 || $num == 16 ) {
01848                     $s .= $table[0][9] . $table[0][$num - 9];
01849                     $num = 0;
01850                 } else {
01851                     $s .= $table[$i][intval( ( $num / $pow10 ) )];
01852                     if ( $pow10 == 1000 ) {
01853                         $s .= "'";
01854                     }
01855                 }
01856             }
01857             $num = $num % $pow10;
01858         }
01859         if ( strlen( $s ) == 2 ) {
01860             $str = $s . "'";
01861         } else {
01862             $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
01863             $str .= substr( $s, strlen( $s ) - 2, 2 );
01864         }
01865         $start = substr( $str, 0, strlen( $str ) - 2 );
01866         $end = substr( $str, strlen( $str ) - 2 );
01867         switch ( $end ) {
01868             case 'כ':
01869                 $str = $start . 'ך';
01870                 break;
01871             case 'מ':
01872                 $str = $start . 'ם';
01873                 break;
01874             case 'נ':
01875                 $str = $start . 'ן';
01876                 break;
01877             case 'פ':
01878                 $str = $start . 'ף';
01879                 break;
01880             case 'צ':
01881                 $str = $start . 'ץ';
01882                 break;
01883         }
01884         return $str;
01885     }
01886 
01895     function userAdjust( $ts, $tz = false ) {
01896         global $wgUser, $wgLocalTZoffset;
01897 
01898         if ( $tz === false ) {
01899             $tz = $wgUser->getOption( 'timecorrection' );
01900         }
01901 
01902         $data = explode( '|', $tz, 3 );
01903 
01904         if ( $data[0] == 'ZoneInfo' ) {
01905             wfSuppressWarnings();
01906             $userTZ = timezone_open( $data[2] );
01907             wfRestoreWarnings();
01908             if ( $userTZ !== false ) {
01909                 $date = date_create( $ts, timezone_open( 'UTC' ) );
01910                 date_timezone_set( $date, $userTZ );
01911                 $date = date_format( $date, 'YmdHis' );
01912                 return $date;
01913             }
01914             # Unrecognized timezone, default to 'Offset' with the stored offset.
01915             $data[0] = 'Offset';
01916         }
01917 
01918         $minDiff = 0;
01919         if ( $data[0] == 'System' || $tz == '' ) {
01920             #  Global offset in minutes.
01921             if ( isset( $wgLocalTZoffset ) ) {
01922                 $minDiff = $wgLocalTZoffset;
01923             }
01924         } elseif ( $data[0] == 'Offset' ) {
01925             $minDiff = intval( $data[1] );
01926         } else {
01927             $data = explode( ':', $tz );
01928             if ( count( $data ) == 2 ) {
01929                 $data[0] = intval( $data[0] );
01930                 $data[1] = intval( $data[1] );
01931                 $minDiff = abs( $data[0] ) * 60 + $data[1];
01932                 if ( $data[0] < 0 ) {
01933                     $minDiff = -$minDiff;
01934                 }
01935             } else {
01936                 $minDiff = intval( $data[0] ) * 60;
01937             }
01938         }
01939 
01940         # No difference ? Return time unchanged
01941         if ( 0 == $minDiff ) {
01942             return $ts;
01943         }
01944 
01945         wfSuppressWarnings(); // E_STRICT system time bitching
01946         # Generate an adjusted date; take advantage of the fact that mktime
01947         # will normalize out-of-range values so we don't have to split $minDiff
01948         # into hours and minutes.
01949         $t = mktime( (
01950             (int)substr( $ts, 8, 2 ) ), # Hours
01951             (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
01952             (int)substr( $ts, 12, 2 ), # Seconds
01953             (int)substr( $ts, 4, 2 ), # Month
01954             (int)substr( $ts, 6, 2 ), # Day
01955             (int)substr( $ts, 0, 4 ) ); # Year
01956 
01957         $date = date( 'YmdHis', $t );
01958         wfRestoreWarnings();
01959 
01960         return $date;
01961     }
01962 
01980     function dateFormat( $usePrefs = true ) {
01981         global $wgUser;
01982 
01983         if ( is_bool( $usePrefs ) ) {
01984             if ( $usePrefs ) {
01985                 $datePreference = $wgUser->getDatePreference();
01986             } else {
01987                 $datePreference = (string)User::getDefaultOption( 'date' );
01988             }
01989         } else {
01990             $datePreference = (string)$usePrefs;
01991         }
01992 
01993         // return int
01994         if ( $datePreference == '' ) {
01995             return 'default';
01996         }
01997 
01998         return $datePreference;
01999     }
02000 
02010     function getDateFormatString( $type, $pref ) {
02011         if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
02012             if ( $pref == 'default' ) {
02013                 $pref = $this->getDefaultDateFormat();
02014                 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
02015             } else {
02016                 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
02017 
02018                 if ( $type === 'pretty' && $df === null ) {
02019                     $df = $this->getDateFormatString( 'date', $pref );
02020                 }
02021 
02022                 if ( $df === null ) {
02023                     $pref = $this->getDefaultDateFormat();
02024                     $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
02025                 }
02026             }
02027             $this->dateFormatStrings[$type][$pref] = $df;
02028         }
02029         return $this->dateFormatStrings[$type][$pref];
02030     }
02031 
02042     function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
02043         $ts = wfTimestamp( TS_MW, $ts );
02044         if ( $adj ) {
02045             $ts = $this->userAdjust( $ts, $timecorrection );
02046         }
02047         $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
02048         return $this->sprintfDate( $df, $ts );
02049     }
02050 
02061     function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
02062         $ts = wfTimestamp( TS_MW, $ts );
02063         if ( $adj ) {
02064             $ts = $this->userAdjust( $ts, $timecorrection );
02065         }
02066         $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
02067         return $this->sprintfDate( $df, $ts );
02068     }
02069 
02081     function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
02082         $ts = wfTimestamp( TS_MW, $ts );
02083         if ( $adj ) {
02084             $ts = $this->userAdjust( $ts, $timecorrection );
02085         }
02086         $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
02087         return $this->sprintfDate( $df, $ts );
02088     }
02089 
02100     public function formatDuration( $seconds, array $chosenIntervals = array() ) {
02101         $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
02102 
02103         $segments = array();
02104 
02105         foreach ( $intervals as $intervalName => $intervalValue ) {
02106             // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
02107             // duration-years, duration-decades, duration-centuries, duration-millennia
02108             $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
02109             $segments[] = $message->inLanguage( $this )->escaped();
02110         }
02111 
02112         return $this->listToText( $segments );
02113     }
02114 
02126     public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
02127         if ( empty( $chosenIntervals ) ) {
02128             $chosenIntervals = array( 'millennia', 'centuries', 'decades', 'years', 'days', 'hours', 'minutes', 'seconds' );
02129         }
02130 
02131         $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
02132         $sortedNames = array_keys( $intervals );
02133         $smallestInterval = array_pop( $sortedNames );
02134 
02135         $segments = array();
02136 
02137         foreach ( $intervals as $name => $length ) {
02138             $value = floor( $seconds / $length );
02139 
02140             if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
02141                 $seconds -= $value * $length;
02142                 $segments[$name] = $value;
02143             }
02144         }
02145 
02146         return $segments;
02147     }
02148 
02168     private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
02169         $ts = wfTimestamp( TS_MW, $ts );
02170         $options += array( 'timecorrection' => true, 'format' => true );
02171         if ( $options['timecorrection'] !== false ) {
02172             if ( $options['timecorrection'] === true ) {
02173                 $offset = $user->getOption( 'timecorrection' );
02174             } else {
02175                 $offset = $options['timecorrection'];
02176             }
02177             $ts = $this->userAdjust( $ts, $offset );
02178         }
02179         if ( $options['format'] === true ) {
02180             $format = $user->getDatePreference();
02181         } else {
02182             $format = $options['format'];
02183         }
02184         $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
02185         return $this->sprintfDate( $df, $ts );
02186     }
02187 
02207     public function userDate( $ts, User $user, array $options = array() ) {
02208         return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
02209     }
02210 
02230     public function userTime( $ts, User $user, array $options = array() ) {
02231         return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
02232     }
02233 
02253     public function userTimeAndDate( $ts, User $user, array $options = array() ) {
02254         return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
02255     }
02256 
02272     public function getHumanTimestamp( MWTimestamp $ts, MWTimestamp $relativeTo, User $user ) {
02273         $diff = $ts->diff( $relativeTo );
02274         $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) - (int)$relativeTo->timestamp->format( 'w' ) );
02275         $days = $diff->days ?: (int)$diffDay;
02276         if ( $diff->invert || $days > 5 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' ) ) {
02277             // Timestamps are in different years: use full timestamp
02278             // Also do full timestamp for future dates
02282             $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
02283             $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
02284         } elseif ( $days > 5 ) {
02285             // Timestamps are in same year,  but more than 5 days ago: show day and month only.
02286             $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
02287             $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
02288         } elseif ( $days > 1 ) {
02289             // Timestamp within the past week: show the day of the week and time
02290             $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
02291             $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
02292             // Messages:
02293             // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
02294             $ts = wfMessage( "$weekday-at" )
02295                 ->inLanguage( $this )
02296                 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
02297                 ->text();
02298         } elseif ( $days == 1 ) {
02299             // Timestamp was yesterday: say 'yesterday' and the time.
02300             $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
02301             $ts = wfMessage( 'yesterday-at' )
02302                 ->inLanguage( $this )
02303                 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
02304                 ->text();
02305         } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
02306             // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
02307             $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
02308             $ts = wfMessage( 'today-at' )
02309                 ->inLanguage( $this )
02310                 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
02311                 ->text();
02312 
02313         // From here on in, the timestamp was soon enough ago so that we can simply say
02314         // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
02315         } elseif ( $diff->h == 1 ) {
02316             // Less than 90 minutes, but more than an hour ago.
02317             $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
02318         } elseif ( $diff->i >= 1 ) {
02319             // A few minutes ago.
02320             $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
02321         } elseif ( $diff->s >= 30 ) {
02322             // Less than a minute, but more than 30 sec ago.
02323             $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
02324         } else {
02325             // Less than 30 seconds ago.
02326             $ts = wfMessage( 'just-now' )->text();
02327         }
02328 
02329         return $ts;
02330     }
02331 
02336     function getMessage( $key ) {
02337         return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
02338     }
02339 
02343     function getAllMessages() {
02344         return self::$dataCache->getItem( $this->mCode, 'messages' );
02345     }
02346 
02353     function iconv( $in, $out, $string ) {
02354         # This is a wrapper for iconv in all languages except esperanto,
02355         # which does some nasty x-conversions beforehand
02356 
02357         # Even with //IGNORE iconv can whine about illegal characters in
02358         # *input* string. We just ignore those too.
02359         # REF: http://bugs.php.net/bug.php?id=37166
02360         # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
02361         wfSuppressWarnings();
02362         $text = iconv( $in, $out . '//IGNORE', $string );
02363         wfRestoreWarnings();
02364         return $text;
02365     }
02366 
02367     // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
02368 
02373     function ucwordbreaksCallbackAscii( $matches ) {
02374         return $this->ucfirst( $matches[1] );
02375     }
02376 
02381     function ucwordbreaksCallbackMB( $matches ) {
02382         return mb_strtoupper( $matches[0] );
02383     }
02384 
02389     function ucCallback( $matches ) {
02390         list( $wikiUpperChars ) = self::getCaseMaps();
02391         return strtr( $matches[1], $wikiUpperChars );
02392     }
02393 
02398     function lcCallback( $matches ) {
02399         list( , $wikiLowerChars ) = self::getCaseMaps();
02400         return strtr( $matches[1], $wikiLowerChars );
02401     }
02402 
02407     function ucwordsCallbackMB( $matches ) {
02408         return mb_strtoupper( $matches[0] );
02409     }
02410 
02415     function ucwordsCallbackWiki( $matches ) {
02416         list( $wikiUpperChars ) = self::getCaseMaps();
02417         return strtr( $matches[0], $wikiUpperChars );
02418     }
02419 
02427     function ucfirst( $str ) {
02428         $o = ord( $str );
02429         if ( $o < 96 ) { // if already uppercase...
02430             return $str;
02431         } elseif ( $o < 128 ) {
02432             return ucfirst( $str ); // use PHP's ucfirst()
02433         } else {
02434             // fall back to more complex logic in case of multibyte strings
02435             return $this->uc( $str, true );
02436         }
02437     }
02438 
02447     function uc( $str, $first = false ) {
02448         if ( function_exists( 'mb_strtoupper' ) ) {
02449             if ( $first ) {
02450                 if ( $this->isMultibyte( $str ) ) {
02451                     return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
02452                 } else {
02453                     return ucfirst( $str );
02454                 }
02455             } else {
02456                 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
02457             }
02458         } else {
02459             if ( $this->isMultibyte( $str ) ) {
02460                 $x = $first ? '^' : '';
02461                 return preg_replace_callback(
02462                     "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
02463                     array( $this, 'ucCallback' ),
02464                     $str
02465                 );
02466             } else {
02467                 return $first ? ucfirst( $str ) : strtoupper( $str );
02468             }
02469         }
02470     }
02471 
02476     function lcfirst( $str ) {
02477         $o = ord( $str );
02478         if ( !$o ) {
02479             return strval( $str );
02480         } elseif ( $o >= 128 ) {
02481             return $this->lc( $str, true );
02482         } elseif ( $o > 96 ) {
02483             return $str;
02484         } else {
02485             $str[0] = strtolower( $str[0] );
02486             return $str;
02487         }
02488     }
02489 
02495     function lc( $str, $first = false ) {
02496         if ( function_exists( 'mb_strtolower' ) ) {
02497             if ( $first ) {
02498                 if ( $this->isMultibyte( $str ) ) {
02499                     return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
02500                 } else {
02501                     return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
02502                 }
02503             } else {
02504                 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
02505             }
02506         } else {
02507             if ( $this->isMultibyte( $str ) ) {
02508                 $x = $first ? '^' : '';
02509                 return preg_replace_callback(
02510                     "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
02511                     array( $this, 'lcCallback' ),
02512                     $str
02513                 );
02514             } else {
02515                 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
02516             }
02517         }
02518     }
02519 
02524     function isMultibyte( $str ) {
02525         return (bool)preg_match( '/[\x80-\xff]/', $str );
02526     }
02527 
02532     function ucwords( $str ) {
02533         if ( $this->isMultibyte( $str ) ) {
02534             $str = $this->lc( $str );
02535 
02536             // regexp to find first letter in each word (i.e. after each space)
02537             $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
02538 
02539             // function to use to capitalize a single char
02540             if ( function_exists( 'mb_strtoupper' ) ) {
02541                 return preg_replace_callback(
02542                     $replaceRegexp,
02543                     array( $this, 'ucwordsCallbackMB' ),
02544                     $str
02545                 );
02546             } else {
02547                 return preg_replace_callback(
02548                     $replaceRegexp,
02549                     array( $this, 'ucwordsCallbackWiki' ),
02550                     $str
02551                 );
02552             }
02553         } else {
02554             return ucwords( strtolower( $str ) );
02555         }
02556     }
02557 
02564     function ucwordbreaks( $str ) {
02565         if ( $this->isMultibyte( $str ) ) {
02566             $str = $this->lc( $str );
02567 
02568             // since \b doesn't work for UTF-8, we explicitely define word break chars
02569             $breaks = "[ \-\(\)\}\{\.,\?!]";
02570 
02571             // find first letter after word break
02572             $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
02573 
02574             if ( function_exists( 'mb_strtoupper' ) ) {
02575                 return preg_replace_callback(
02576                     $replaceRegexp,
02577                     array( $this, 'ucwordbreaksCallbackMB' ),
02578                     $str
02579                 );
02580             } else {
02581                 return preg_replace_callback(
02582                     $replaceRegexp,
02583                     array( $this, 'ucwordsCallbackWiki' ),
02584                     $str
02585                 );
02586             }
02587         } else {
02588             return preg_replace_callback(
02589                 '/\b([\w\x80-\xff]+)\b/',
02590                 array( $this, 'ucwordbreaksCallbackAscii' ),
02591                 $str
02592             );
02593         }
02594     }
02595 
02611     function caseFold( $s ) {
02612         return $this->uc( $s );
02613     }
02614 
02619     function checkTitleEncoding( $s ) {
02620         if ( is_array( $s ) ) {
02621             throw new MWException( 'Given array to checkTitleEncoding.' );
02622         }
02623         if ( StringUtils::isUtf8( $s ) ) {
02624             return $s;
02625         }
02626 
02627         return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
02628     }
02629 
02633     function fallback8bitEncoding() {
02634         return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
02635     }
02636 
02645     function hasWordBreaks() {
02646         return true;
02647     }
02648 
02656     function segmentByWord( $string ) {
02657         return $string;
02658     }
02659 
02667     function normalizeForSearch( $string ) {
02668         return self::convertDoubleWidth( $string );
02669     }
02670 
02679     protected static function convertDoubleWidth( $string ) {
02680         static $full = null;
02681         static $half = null;
02682 
02683         if ( $full === null ) {
02684             $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
02685             $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
02686             $full = str_split( $fullWidth, 3 );
02687             $half = str_split( $halfWidth );
02688         }
02689 
02690         $string = str_replace( $full, $half, $string );
02691         return $string;
02692     }
02693 
02699     protected static function insertSpace( $string, $pattern ) {
02700         $string = preg_replace( $pattern, " $1 ", $string );
02701         $string = preg_replace( '/ +/', ' ', $string );
02702         return $string;
02703     }
02704 
02709     function convertForSearchResult( $termsArray ) {
02710         # some languages, e.g. Chinese, need to do a conversion
02711         # in order for search results to be displayed correctly
02712         return $termsArray;
02713     }
02714 
02721     function firstChar( $s ) {
02722         $matches = array();
02723         preg_match(
02724             '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
02725                 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
02726             $s,
02727             $matches
02728         );
02729 
02730         if ( isset( $matches[1] ) ) {
02731             if ( strlen( $matches[1] ) != 3 ) {
02732                 return $matches[1];
02733             }
02734 
02735             // Break down Hangul syllables to grab the first jamo
02736             $code = utf8ToCodepoint( $matches[1] );
02737             if ( $code < 0xac00 || 0xd7a4 <= $code ) {
02738                 return $matches[1];
02739             } elseif ( $code < 0xb098 ) {
02740                 return "\xe3\x84\xb1";
02741             } elseif ( $code < 0xb2e4 ) {
02742                 return "\xe3\x84\xb4";
02743             } elseif ( $code < 0xb77c ) {
02744                 return "\xe3\x84\xb7";
02745             } elseif ( $code < 0xb9c8 ) {
02746                 return "\xe3\x84\xb9";
02747             } elseif ( $code < 0xbc14 ) {
02748                 return "\xe3\x85\x81";
02749             } elseif ( $code < 0xc0ac ) {
02750                 return "\xe3\x85\x82";
02751             } elseif ( $code < 0xc544 ) {
02752                 return "\xe3\x85\x85";
02753             } elseif ( $code < 0xc790 ) {
02754                 return "\xe3\x85\x87";
02755             } elseif ( $code < 0xcc28 ) {
02756                 return "\xe3\x85\x88";
02757             } elseif ( $code < 0xce74 ) {
02758                 return "\xe3\x85\x8a";
02759             } elseif ( $code < 0xd0c0 ) {
02760                 return "\xe3\x85\x8b";
02761             } elseif ( $code < 0xd30c ) {
02762                 return "\xe3\x85\x8c";
02763             } elseif ( $code < 0xd558 ) {
02764                 return "\xe3\x85\x8d";
02765             } else {
02766                 return "\xe3\x85\x8e";
02767             }
02768         } else {
02769             return '';
02770         }
02771     }
02772 
02773     function initEncoding() {
02774         # Some languages may have an alternate char encoding option
02775         # (Esperanto X-coding, Japanese furigana conversion, etc)
02776         # If this language is used as the primary content language,
02777         # an override to the defaults can be set here on startup.
02778     }
02779 
02784     function recodeForEdit( $s ) {
02785         # For some languages we'll want to explicitly specify
02786         # which characters make it into the edit box raw
02787         # or are converted in some way or another.
02788         global $wgEditEncoding;
02789         if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
02790             return $s;
02791         } else {
02792             return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
02793         }
02794     }
02795 
02800     function recodeInput( $s ) {
02801         # Take the previous into account.
02802         global $wgEditEncoding;
02803         if ( $wgEditEncoding != '' ) {
02804             $enc = $wgEditEncoding;
02805         } else {
02806             $enc = 'UTF-8';
02807         }
02808         if ( $enc == 'UTF-8' ) {
02809             return $s;
02810         } else {
02811             return $this->iconv( $enc, 'UTF-8', $s );
02812         }
02813     }
02814 
02826     function normalize( $s ) {
02827         global $wgAllUnicodeFixes;
02828         $s = UtfNormal::cleanUp( $s );
02829         if ( $wgAllUnicodeFixes ) {
02830             $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
02831             $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
02832         }
02833 
02834         return $s;
02835     }
02836 
02851     function transformUsingPairFile( $file, $string ) {
02852         if ( !isset( $this->transformData[$file] ) ) {
02853             $data = wfGetPrecompiledData( $file );
02854             if ( $data === false ) {
02855                 throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
02856             }
02857             $this->transformData[$file] = new ReplacementArray( $data );
02858         }
02859         return $this->transformData[$file]->replace( $string );
02860     }
02861 
02867     function isRTL() {
02868         return self::$dataCache->getItem( $this->mCode, 'rtl' );
02869     }
02870 
02875     function getDir() {
02876         return $this->isRTL() ? 'rtl' : 'ltr';
02877     }
02878 
02887     function alignStart() {
02888         return $this->isRTL() ? 'right' : 'left';
02889     }
02890 
02899     function alignEnd() {
02900         return $this->isRTL() ? 'left' : 'right';
02901     }
02902 
02914     function getDirMarkEntity( $opposite = false ) {
02915         if ( $opposite ) {
02916             return $this->isRTL() ? '&lrm;' : '&rlm;';
02917         }
02918         return $this->isRTL() ? '&rlm;' : '&lrm;';
02919     }
02920 
02931     function getDirMark( $opposite = false ) {
02932         $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
02933         $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
02934         if ( $opposite ) {
02935             return $this->isRTL() ? $lrm : $rlm;
02936         }
02937         return $this->isRTL() ? $rlm : $lrm;
02938     }
02939 
02943     function capitalizeAllNouns() {
02944         return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
02945     }
02946 
02953     function getArrow( $direction = 'forwards' ) {
02954         switch ( $direction ) {
02955         case 'forwards':
02956             return $this->isRTL() ? '←' : '→';
02957         case 'backwards':
02958             return $this->isRTL() ? '→' : '←';
02959         case 'left':
02960             return '←';
02961         case 'right':
02962             return '→';
02963         case 'up':
02964             return '↑';
02965         case 'down':
02966             return '↓';
02967         }
02968     }
02969 
02975     function linkPrefixExtension() {
02976         return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
02977     }
02978 
02983     function getMagicWords() {
02984         return self::$dataCache->getItem( $this->mCode, 'magicWords' );
02985     }
02986 
02990     protected function doMagicHook() {
02991         if ( $this->mMagicHookDone ) {
02992             return;
02993         }
02994         $this->mMagicHookDone = true;
02995         wfProfileIn( 'LanguageGetMagic' );
02996         wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
02997         wfProfileOut( 'LanguageGetMagic' );
02998     }
02999 
03005     function getMagic( $mw ) {
03006         // Saves a function call
03007         if ( ! $this->mMagicHookDone ) {
03008             $this->doMagicHook();
03009         }
03010 
03011         if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
03012             $rawEntry = $this->mMagicExtensions[$mw->mId];
03013         } else {
03014             $rawEntry = self::$dataCache->getSubitem(
03015                 $this->mCode, 'magicWords', $mw->mId );
03016         }
03017 
03018         if ( !is_array( $rawEntry ) ) {
03019             error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
03020         } else {
03021             $mw->mCaseSensitive = $rawEntry[0];
03022             $mw->mSynonyms = array_slice( $rawEntry, 1 );
03023         }
03024     }
03025 
03031     function addMagicWordsByLang( $newWords ) {
03032         $fallbackChain = $this->getFallbackLanguages();
03033         $fallbackChain = array_reverse( $fallbackChain );
03034         foreach ( $fallbackChain as $code ) {
03035             if ( isset( $newWords[$code] ) ) {
03036                 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
03037             }
03038         }
03039     }
03040 
03045     function getSpecialPageAliases() {
03046         // Cache aliases because it may be slow to load them
03047         if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
03048             // Initialise array
03049             $this->mExtendedSpecialPageAliases =
03050                 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
03051             wfRunHooks( 'LanguageGetSpecialPageAliases',
03052                 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
03053         }
03054 
03055         return $this->mExtendedSpecialPageAliases;
03056     }
03057 
03064     function emphasize( $text ) {
03065         return "<em>$text</em>";
03066     }
03067 
03092     public function formatNum( $number, $nocommafy = false ) {
03093         global $wgTranslateNumerals;
03094         if ( !$nocommafy ) {
03095             $number = $this->commafy( $number );
03096             $s = $this->separatorTransformTable();
03097             if ( $s ) {
03098                 $number = strtr( $number, $s );
03099             }
03100         }
03101 
03102         if ( $wgTranslateNumerals ) {
03103             $s = $this->digitTransformTable();
03104             if ( $s ) {
03105                 $number = strtr( $number, $s );
03106             }
03107         }
03108 
03109         return $number;
03110     }
03111 
03120     public function formatNumNoSeparators( $number ) {
03121         return $this->formatNum( $number, true );
03122     }
03123 
03128     function parseFormattedNumber( $number ) {
03129         $s = $this->digitTransformTable();
03130         if ( $s ) {
03131             $number = strtr( $number, array_flip( $s ) );
03132         }
03133 
03134         $s = $this->separatorTransformTable();
03135         if ( $s ) {
03136             $number = strtr( $number, array_flip( $s ) );
03137         }
03138 
03139         $number = strtr( $number, array( ',' => '' ) );
03140         return $number;
03141     }
03142 
03149     function commafy( $number ) {
03150         $digitGroupingPattern = $this->digitGroupingPattern();
03151         if ( $number === null ) {
03152             return '';
03153         }
03154 
03155         if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
03156             // default grouping is at thousands,  use the same for ###,###,### pattern too.
03157             return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
03158         } else {
03159             // Ref: http://cldr.unicode.org/translation/number-patterns
03160             $sign = "";
03161             if ( intval( $number ) < 0 ) {
03162                 // For negative numbers apply the algorithm like positive number and add sign.
03163                 $sign = "-";
03164                 $number = substr( $number, 1 );
03165             }
03166             $integerPart = array();
03167             $decimalPart = array();
03168             $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
03169             preg_match( "/\d+/", $number, $integerPart );
03170             preg_match( "/\.\d*/", $number, $decimalPart );
03171             $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
03172             if ( $groupedNumber === $number ) {
03173                 // the string does not have any number part. Eg: .12345
03174                 return $sign . $groupedNumber;
03175             }
03176             $start = $end = strlen( $integerPart[0] );
03177             while ( $start > 0 ) {
03178                 $match = $matches[0][$numMatches - 1];
03179                 $matchLen = strlen( $match );
03180                 $start = $end - $matchLen;
03181                 if ( $start < 0 ) {
03182                     $start = 0;
03183                 }
03184                 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
03185                 $end = $start;
03186                 if ( $numMatches > 1 ) {
03187                     // use the last pattern for the rest of the number
03188                     $numMatches--;
03189                 }
03190                 if ( $start > 0 ) {
03191                     $groupedNumber = "," . $groupedNumber;
03192                 }
03193             }
03194             return $sign . $groupedNumber;
03195         }
03196     }
03197 
03201     function digitGroupingPattern() {
03202         return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
03203     }
03204 
03208     function digitTransformTable() {
03209         return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
03210     }
03211 
03215     function separatorTransformTable() {
03216         return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
03217     }
03218 
03228     function listToText( array $l ) {
03229         $m = count( $l ) - 1;
03230         if ( $m < 0 ) {
03231             return '';
03232         }
03233         if ( $m > 0 ) {
03234             $and = $this->getMessageFromDB( 'and' );
03235             $space = $this->getMessageFromDB( 'word-separator' );
03236             if ( $m > 1 ) {
03237                 $comma = $this->getMessageFromDB( 'comma-separator' );
03238             }
03239         }
03240         $s = $l[$m];
03241         for ( $i = $m - 1; $i >= 0; $i-- ) {
03242             if ( $i == $m - 1 ) {
03243                 $s = $l[$i] . $and . $space . $s;
03244             } else {
03245                 $s = $l[$i] . $comma . $s;
03246             }
03247         }
03248         return $s;
03249     }
03250 
03257     function commaList( array $list ) {
03258         return implode(
03259             wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
03260             $list
03261         );
03262     }
03263 
03270     function semicolonList( array $list ) {
03271         return implode(
03272             wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
03273             $list
03274         );
03275     }
03276 
03282     function pipeList( array $list ) {
03283         return implode(
03284             wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
03285             $list
03286         );
03287     }
03288 
03306     function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
03307         # Use the localized ellipsis character
03308         if ( $ellipsis == '...' ) {
03309             $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
03310         }
03311         # Check if there is no need to truncate
03312         if ( $length == 0 ) {
03313             return $ellipsis; // convention
03314         } elseif ( strlen( $string ) <= abs( $length ) ) {
03315             return $string; // no need to truncate
03316         }
03317         $stringOriginal = $string;
03318         # If ellipsis length is >= $length then we can't apply $adjustLength
03319         if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
03320             $string = $ellipsis; // this can be slightly unexpected
03321         # Otherwise, truncate and add ellipsis...
03322         } else {
03323             $eLength = $adjustLength ? strlen( $ellipsis ) : 0;
03324             if ( $length > 0 ) {
03325                 $length -= $eLength;
03326                 $string = substr( $string, 0, $length ); // xyz...
03327                 $string = $this->removeBadCharLast( $string );
03328                 $string = $string . $ellipsis;
03329             } else {
03330                 $length += $eLength;
03331                 $string = substr( $string, $length ); // ...xyz
03332                 $string = $this->removeBadCharFirst( $string );
03333                 $string = $ellipsis . $string;
03334             }
03335         }
03336         # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
03337         # This check is *not* redundant if $adjustLength, due to the single case where
03338         # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
03339         if ( strlen( $string ) < strlen( $stringOriginal ) ) {
03340             return $string;
03341         } else {
03342             return $stringOriginal;
03343         }
03344     }
03345 
03353     protected function removeBadCharLast( $string ) {
03354         if ( $string != '' ) {
03355             $char = ord( $string[strlen( $string ) - 1] );
03356             $m = array();
03357             if ( $char >= 0xc0 ) {
03358                 # We got the first byte only of a multibyte char; remove it.
03359                 $string = substr( $string, 0, -1 );
03360             } elseif ( $char >= 0x80 &&
03361                 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
03362                     '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m )
03363             ) {
03364                 # We chopped in the middle of a character; remove it
03365                 $string = $m[1];
03366             }
03367         }
03368         return $string;
03369     }
03370 
03378     protected function removeBadCharFirst( $string ) {
03379         if ( $string != '' ) {
03380             $char = ord( $string[0] );
03381             if ( $char >= 0x80 && $char < 0xc0 ) {
03382                 # We chopped in the middle of a character; remove the whole thing
03383                 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
03384             }
03385         }
03386         return $string;
03387     }
03388 
03404     function truncateHtml( $text, $length, $ellipsis = '...' ) {
03405         # Use the localized ellipsis character
03406         if ( $ellipsis == '...' ) {
03407             $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
03408         }
03409         # Check if there is clearly no need to truncate
03410         if ( $length <= 0 ) {
03411             return $ellipsis; // no text shown, nothing to format (convention)
03412         } elseif ( strlen( $text ) <= $length ) {
03413             return $text; // string short enough even *with* HTML (short-circuit)
03414         }
03415 
03416         $dispLen = 0; // innerHTML legth so far
03417         $testingEllipsis = false; // checking if ellipses will make string longer/equal?
03418         $tagType = 0; // 0-open, 1-close
03419         $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
03420         $entityState = 0; // 0-not entity, 1-entity
03421         $tag = $ret = ''; // accumulated tag name, accumulated result string
03422         $openTags = array(); // open tag stack
03423         $maybeState = null; // possible truncation state
03424 
03425         $textLen = strlen( $text );
03426         $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
03427         for ( $pos = 0; true; ++$pos ) {
03428             # Consider truncation once the display length has reached the maximim.
03429             # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
03430             # Check that we're not in the middle of a bracket/entity...
03431             if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
03432                 if ( !$testingEllipsis ) {
03433                     $testingEllipsis = true;
03434                     # Save where we are; we will truncate here unless there turn out to
03435                     # be so few remaining characters that truncation is not necessary.
03436                     if ( !$maybeState ) { // already saved? ($neLength = 0 case)
03437                         $maybeState = array( $ret, $openTags ); // save state
03438                     }
03439                 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
03440                     # String in fact does need truncation, the truncation point was OK.
03441                     list( $ret, $openTags ) = $maybeState; // reload state
03442                     $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
03443                     $ret .= $ellipsis; // add ellipsis
03444                     break;
03445                 }
03446             }
03447             if ( $pos >= $textLen ) {
03448                 break; // extra iteration just for above checks
03449             }
03450 
03451             # Read the next char...
03452             $ch = $text[$pos];
03453             $lastCh = $pos ? $text[$pos - 1] : '';
03454             $ret .= $ch; // add to result string
03455             if ( $ch == '<' ) {
03456                 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
03457                 $entityState = 0; // for bad HTML
03458                 $bracketState = 1; // tag started (checking for backslash)
03459             } elseif ( $ch == '>' ) {
03460                 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
03461                 $entityState = 0; // for bad HTML
03462                 $bracketState = 0; // out of brackets
03463             } elseif ( $bracketState == 1 ) {
03464                 if ( $ch == '/' ) {
03465                     $tagType = 1; // close tag (e.g. "</span>")
03466                 } else {
03467                     $tagType = 0; // open tag (e.g. "<span>")
03468                     $tag .= $ch;
03469                 }
03470                 $bracketState = 2; // building tag name
03471             } elseif ( $bracketState == 2 ) {
03472                 if ( $ch != ' ' ) {
03473                     $tag .= $ch;
03474                 } else {
03475                     // Name found (e.g. "<a href=..."), add on tag attributes...
03476                     $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
03477                 }
03478             } elseif ( $bracketState == 0 ) {
03479                 if ( $entityState ) {
03480                     if ( $ch == ';' ) {
03481                         $entityState = 0;
03482                         $dispLen++; // entity is one displayed char
03483                     }
03484                 } else {
03485                     if ( $neLength == 0 && !$maybeState ) {
03486                         // Save state without $ch. We want to *hit* the first
03487                         // display char (to get tags) but not *use* it if truncating.
03488                         $maybeState = array( substr( $ret, 0, -1 ), $openTags );
03489                     }
03490                     if ( $ch == '&' ) {
03491                         $entityState = 1; // entity found, (e.g. "&#160;")
03492                     } else {
03493                         $dispLen++; // this char is displayed
03494                         // Add the next $max display text chars after this in one swoop...
03495                         $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
03496                         $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
03497                         $dispLen += $skipped;
03498                         $pos += $skipped;
03499                     }
03500                 }
03501             }
03502         }
03503         // Close the last tag if left unclosed by bad HTML
03504         $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
03505         while ( count( $openTags ) > 0 ) {
03506             $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
03507         }
03508         return $ret;
03509     }
03510 
03522     private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
03523         if ( $len === null ) {
03524             $len = -1; // -1 means "no limit" for strcspn
03525         } elseif ( $len < 0 ) {
03526             $len = 0; // sanity
03527         }
03528         $skipCount = 0;
03529         if ( $start < strlen( $text ) ) {
03530             $skipCount = strcspn( $text, $search, $start, $len );
03531             $ret .= substr( $text, $start, $skipCount );
03532         }
03533         return $skipCount;
03534     }
03535 
03545     private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
03546         $tag = ltrim( $tag );
03547         if ( $tag != '' ) {
03548             if ( $tagType == 0 && $lastCh != '/' ) {
03549                 $openTags[] = $tag; // tag opened (didn't close itself)
03550             } elseif ( $tagType == 1 ) {
03551                 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
03552                     array_pop( $openTags ); // tag closed
03553                 }
03554             }
03555             $tag = '';
03556         }
03557     }
03558 
03567     function convertGrammar( $word, $case ) {
03568         global $wgGrammarForms;
03569         if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
03570             return $wgGrammarForms[$this->getCode()][$case][$word];
03571         }
03572         return $word;
03573     }
03579     function getGrammarForms() {
03580         global $wgGrammarForms;
03581         if ( isset( $wgGrammarForms[$this->getCode()] ) && is_array( $wgGrammarForms[$this->getCode()] ) ) {
03582             return $wgGrammarForms[$this->getCode()];
03583         }
03584         return array();
03585     }
03605     function gender( $gender, $forms ) {
03606         if ( !count( $forms ) ) {
03607             return '';
03608         }
03609         $forms = $this->preConvertPlural( $forms, 2 );
03610         if ( $gender === 'male' ) {
03611             return $forms[0];
03612         }
03613         if ( $gender === 'female' ) {
03614             return $forms[1];
03615         }
03616         return isset( $forms[2] ) ? $forms[2] : $forms[0];
03617     }
03618 
03634     function convertPlural( $count, $forms ) {
03635         // Handle explicit n=pluralform cases
03636         $forms = $this->handleExplicitPluralForms( $count, $forms );
03637         if ( is_string( $forms ) ) {
03638             return $forms;
03639         }
03640         if ( !count( $forms ) ) {
03641             return '';
03642         }
03643 
03644         $pluralForm = $this->getPluralRuleIndexNumber( $count );
03645         $pluralForm = min( $pluralForm, count( $forms ) - 1 );
03646         return $forms[$pluralForm];
03647     }
03648 
03664     protected function handleExplicitPluralForms( $count, array $forms ) {
03665         foreach ( $forms as $index => $form ) {
03666             if ( preg_match( '/\d+=/i', $form ) ) {
03667                 $pos = strpos( $form, '=' );
03668                 if ( substr( $form, 0, $pos ) === (string) $count ) {
03669                     return substr( $form, $pos + 1 );
03670                 }
03671                 unset( $forms[$index] );
03672             }
03673         }
03674         return array_values( $forms );
03675     }
03676 
03685     protected function preConvertPlural( /* Array */ $forms, $count ) {
03686         while ( count( $forms ) < $count ) {
03687             $forms[] = $forms[count( $forms ) - 1];
03688         }
03689         return $forms;
03690     }
03691 
03703     function translateBlockExpiry( $str ) {
03704         $duration = SpecialBlock::getSuggestedDurations( $this );
03705         foreach ( $duration as $show => $value ) {
03706             if ( strcmp( $str, $value ) == 0 ) {
03707                 return htmlspecialchars( trim( $show ) );
03708             }
03709         }
03710 
03711         // Since usually only infinite or indefinite is only on list, so try
03712         // equivalents if still here.
03713         $indefs = array( 'infinite', 'infinity', 'indefinite' );
03714         if ( in_array( $str, $indefs ) ) {
03715             foreach ( $indefs as $val ) {
03716                 $show = array_search( $val, $duration, true );
03717                 if ( $show !== false ) {
03718                     return htmlspecialchars( trim( $show ) );
03719                 }
03720             }
03721         }
03722 
03723         // If all else fails, return a standard duration or timestamp description.
03724         $time = strtotime( $str, 0 );
03725         if ( $time === false ) { // Unknown format. Return it as-is in case.
03726             return $str;
03727         } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
03728             // $time is relative to 0 so it's a duration length.
03729             return $this->formatDuration( $time );
03730         } else { // It's an absolute timestamp.
03731             if ( $time === 0 ) {
03732                 // wfTimestamp() handles 0 as current time instead of epoch.
03733                 return $this->timeanddate( '19700101000000' );
03734             } else {
03735                 return $this->timeanddate( $time );
03736             }
03737         }
03738     }
03739 
03747     public function segmentForDiff( $text ) {
03748         return $text;
03749     }
03750 
03757     public function unsegmentForDiff( $text ) {
03758         return $text;
03759     }
03760 
03767     public function getConverter() {
03768         return $this->mConverter;
03769     }
03770 
03777     public function autoConvertToAllVariants( $text ) {
03778         return $this->mConverter->autoConvertToAllVariants( $text );
03779     }
03780 
03787     public function convert( $text ) {
03788         return $this->mConverter->convert( $text );
03789     }
03790 
03797     public function convertTitle( $title ) {
03798         return $this->mConverter->convertTitle( $title );
03799     }
03800 
03807     public function convertNamespace( $ns ) {
03808         return $this->mConverter->convertNamespace( $ns );
03809     }
03810 
03816     public function hasVariants() {
03817         return count( $this->getVariants() ) > 1;
03818     }
03819 
03827     public function hasVariant( $variant ) {
03828         return (bool)$this->mConverter->validateVariant( $variant );
03829     }
03830 
03838     public function armourMath( $text ) {
03839         return $this->mConverter->armourMath( $text );
03840     }
03841 
03849     public function convertHtml( $text, $isTitle = false ) {
03850         return htmlspecialchars( $this->convert( $text, $isTitle ) );
03851     }
03852 
03857     public function convertCategoryKey( $key ) {
03858         return $this->mConverter->convertCategoryKey( $key );
03859     }
03860 
03867     public function getVariants() {
03868         return $this->mConverter->getVariants();
03869     }
03870 
03874     public function getPreferredVariant() {
03875         return $this->mConverter->getPreferredVariant();
03876     }
03877 
03881     public function getDefaultVariant() {
03882         return $this->mConverter->getDefaultVariant();
03883     }
03884 
03888     public function getURLVariant() {
03889         return $this->mConverter->getURLVariant();
03890     }
03891 
03904     public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
03905         $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
03906     }
03907 
03919     public function convertLinkToAllVariants( $text ) {
03920         return $this->mConverter->convertLinkToAllVariants( $text );
03921     }
03922 
03929     function getExtraHashOptions() {
03930         return $this->mConverter->getExtraHashOptions();
03931     }
03932 
03940     public function getParsedTitle() {
03941         return $this->mConverter->getParsedTitle();
03942     }
03943 
03956     public function markNoConversion( $text, $noParse = false ) {
03957         // Excluding protocal-relative URLs may avoid many false positives.
03958         if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
03959             return $this->mConverter->markNoConversion( $text );
03960         } else {
03961             return $text;
03962         }
03963     }
03964 
03971     public function linkTrail() {
03972         return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
03973     }
03974 
03978     function getLangObj() {
03979         return $this;
03980     }
03981 
03989     public function getParentLanguage() {
03990         if ( $this->mParentLanguage !== false ) {
03991             return $this->mParentLanguage;
03992         }
03993 
03994         $pieces = explode( '-', $this->getCode() );
03995         $code = $pieces[0];
03996         if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
03997             $this->mParentLanguage = null;
03998             return null;
03999         }
04000         $lang = Language::factory( $code );
04001         if ( !$lang->hasVariant( $this->getCode() ) ) {
04002             $this->mParentLanguage = null;
04003             return null;
04004         }
04005 
04006         $this->mParentLanguage = $lang;
04007         return $lang;
04008     }
04009 
04018     public function getCode() {
04019         return $this->mCode;
04020     }
04021 
04032     public function getHtmlCode() {
04033         if ( is_null( $this->mHtmlCode ) ) {
04034             $this->mHtmlCode = wfBCP47( $this->getCode() );
04035         }
04036         return $this->mHtmlCode;
04037     }
04038 
04042     public function setCode( $code ) {
04043         $this->mCode = $code;
04044         // Ensure we don't leave incorrect cached data lying around
04045         $this->mHtmlCode = null;
04046         $this->mParentLanguage = false;
04047     }
04048 
04057     public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
04058         // Protect against path traversal
04059         if ( !Language::isValidCode( $code )
04060             || strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
04061         {
04062             throw new MWException( "Invalid language code \"$code\"" );
04063         }
04064 
04065         return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
04066     }
04067 
04075     public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
04076         $m = null;
04077         preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
04078             preg_quote( $suffix, '/' ) . '/', $filename, $m );
04079         if ( !count( $m ) ) {
04080             return false;
04081         }
04082         return str_replace( '_', '-', strtolower( $m[1] ) );
04083     }
04084 
04089     public static function getMessagesFileName( $code ) {
04090         global $IP;
04091         $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
04092         wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
04093         return $file;
04094     }
04095 
04100     public static function getClassFileName( $code ) {
04101         global $IP;
04102         return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
04103     }
04104 
04112     public static function getFallbackFor( $code ) {
04113         if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
04114             return false;
04115         } else {
04116             $fallbacks = self::getFallbacksFor( $code );
04117             $first = array_shift( $fallbacks );
04118             return $first;
04119         }
04120     }
04121 
04129     public static function getFallbacksFor( $code ) {
04130         if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
04131             return array();
04132         } else {
04133             $v = self::getLocalisationCache()->getItem( $code, 'fallback' );
04134             $v = array_map( 'trim', explode( ',', $v ) );
04135             if ( $v[count( $v ) - 1] !== 'en' ) {
04136                 $v[] = 'en';
04137             }
04138             return $v;
04139         }
04140     }
04141 
04150     public static function getFallbacksIncludingSiteLanguage( $code ) {
04151         global $wgLanguageCode;
04152 
04153         // Usually, we will only store a tiny number of fallback chains, so we
04154         // keep them in static memory.
04155         $cacheKey = "{$code}-{$wgLanguageCode}";
04156 
04157         if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
04158             $fallbacks = self::getFallbacksFor( $code );
04159 
04160             // Append the site's fallback chain, including the site language itself
04161             $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
04162             array_unshift( $siteFallbacks, $wgLanguageCode );
04163 
04164             // Eliminate any languages already included in the chain
04165             $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
04166 
04167             self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
04168         }
04169         return self::$fallbackLanguageCache[$cacheKey];
04170     }
04171 
04181     public static function getMessagesFor( $code ) {
04182         return self::getLocalisationCache()->getItem( $code, 'messages' );
04183     }
04184 
04193     public static function getMessageFor( $key, $code ) {
04194         return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
04195     }
04196 
04205     public static function getMessageKeysFor( $code ) {
04206         return self::getLocalisationCache()->getSubItemList( $code, 'messages' );
04207     }
04208 
04213     function fixVariableInNamespace( $talk ) {
04214         if ( strpos( $talk, '$1' ) === false ) {
04215             return $talk;
04216         }
04217 
04218         global $wgMetaNamespace;
04219         $talk = str_replace( '$1', $wgMetaNamespace, $talk );
04220 
04221         # Allow grammar transformations
04222         # Allowing full message-style parsing would make simple requests
04223         # such as action=raw much more expensive than they need to be.
04224         # This will hopefully cover most cases.
04225         $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
04226             array( &$this, 'replaceGrammarInNamespace' ), $talk );
04227         return str_replace( ' ', '_', $talk );
04228     }
04229 
04234     function replaceGrammarInNamespace( $m ) {
04235         return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
04236     }
04237 
04242     static function getCaseMaps() {
04243         static $wikiUpperChars, $wikiLowerChars;
04244         if ( isset( $wikiUpperChars ) ) {
04245             return array( $wikiUpperChars, $wikiLowerChars );
04246         }
04247 
04248         wfProfileIn( __METHOD__ );
04249         $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
04250         if ( $arr === false ) {
04251             throw new MWException(
04252                 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
04253         }
04254         $wikiUpperChars = $arr['wikiUpperChars'];
04255         $wikiLowerChars = $arr['wikiLowerChars'];
04256         wfProfileOut( __METHOD__ );
04257         return array( $wikiUpperChars, $wikiLowerChars );
04258     }
04259 
04271     public function formatExpiry( $expiry, $format = true ) {
04272         static $infinity;
04273         if ( $infinity === null ) {
04274             $infinity = wfGetDB( DB_SLAVE )->getInfinity();
04275         }
04276 
04277         if ( $expiry == '' || $expiry == $infinity ) {
04278             return $format === true
04279                 ? $this->getMessageFromDB( 'infiniteblock' )
04280                 : $infinity;
04281         } else {
04282             return $format === true
04283                 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
04284                 : wfTimestamp( $format, $expiry );
04285         }
04286     }
04287 
04298     function formatTimePeriod( $seconds, $format = array() ) {
04299         if ( !is_array( $format ) ) {
04300             $format = array( 'avoid' => $format ); // For backwards compatibility
04301         }
04302         if ( !isset( $format['avoid'] ) ) {
04303             $format['avoid'] = false;
04304         }
04305         if ( !isset( $format['noabbrevs' ] ) ) {
04306             $format['noabbrevs'] = false;
04307         }
04308         $secondsMsg = wfMessage(
04309             $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
04310         $minutesMsg = wfMessage(
04311             $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
04312         $hoursMsg = wfMessage(
04313             $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
04314         $daysMsg = wfMessage(
04315             $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
04316 
04317         if ( round( $seconds * 10 ) < 100 ) {
04318             $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
04319             $s = $secondsMsg->params( $s )->text();
04320         } elseif ( round( $seconds ) < 60 ) {
04321             $s = $this->formatNum( round( $seconds ) );
04322             $s = $secondsMsg->params( $s )->text();
04323         } elseif ( round( $seconds ) < 3600 ) {
04324             $minutes = floor( $seconds / 60 );
04325             $secondsPart = round( fmod( $seconds, 60 ) );
04326             if ( $secondsPart == 60 ) {
04327                 $secondsPart = 0;
04328                 $minutes++;
04329             }
04330             $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
04331             $s .= ' ';
04332             $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
04333         } elseif ( round( $seconds ) <= 2 * 86400 ) {
04334             $hours = floor( $seconds / 3600 );
04335             $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
04336             $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
04337             if ( $secondsPart == 60 ) {
04338                 $secondsPart = 0;
04339                 $minutes++;
04340             }
04341             if ( $minutes == 60 ) {
04342                 $minutes = 0;
04343                 $hours++;
04344             }
04345             $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
04346             $s .= ' ';
04347             $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
04348             if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
04349                 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
04350             }
04351         } else {
04352             $days = floor( $seconds / 86400 );
04353             if ( $format['avoid'] === 'avoidminutes' ) {
04354                 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
04355                 if ( $hours == 24 ) {
04356                     $hours = 0;
04357                     $days++;
04358                 }
04359                 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
04360                 $s .= ' ';
04361                 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
04362             } elseif ( $format['avoid'] === 'avoidseconds' ) {
04363                 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
04364                 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
04365                 if ( $minutes == 60 ) {
04366                     $minutes = 0;
04367                     $hours++;
04368                 }
04369                 if ( $hours == 24 ) {
04370                     $hours = 0;
04371                     $days++;
04372                 }
04373                 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
04374                 $s .= ' ';
04375                 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
04376                 $s .= ' ';
04377                 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
04378             } else {
04379                 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
04380                 $s .= ' ';
04381                 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
04382             }
04383         }
04384         return $s;
04385     }
04386 
04397     function formatBitrate( $bps ) {
04398         return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
04399     }
04400 
04407     function formatComputingNumbers( $size, $boundary, $messageKey ) {
04408         if ( $size <= 0 ) {
04409             return str_replace( '$1', $this->formatNum( $size ),
04410                 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
04411             );
04412         }
04413         $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
04414         $index = 0;
04415 
04416         $maxIndex = count( $sizes ) - 1;
04417         while ( $size >= $boundary && $index < $maxIndex ) {
04418             $index++;
04419             $size /= $boundary;
04420         }
04421 
04422         // For small sizes no decimal places necessary
04423         $round = 0;
04424         if ( $index > 1 ) {
04425             // For MB and bigger two decimal places are smarter
04426             $round = 2;
04427         }
04428         $msg = str_replace( '$1', $sizes[$index], $messageKey );
04429 
04430         $size = round( $size, $round );
04431         $text = $this->getMessageFromDB( $msg );
04432         return str_replace( '$1', $this->formatNum( $size ), $text );
04433     }
04434 
04445     function formatSize( $size ) {
04446         return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
04447     }
04448 
04458     function specialList( $page, $details, $oppositedm = true ) {
04459         $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) .
04460             $this->getDirMark();
04461         $details = $details ? $dirmark . $this->getMessageFromDB( 'word-separator' ) .
04462             wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : '';
04463         return $page . $details;
04464     }
04465 
04476     public function viewPrevNext( Title $title, $offset, $limit, array $query = array(), $atend = false ) {
04477         // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
04478 
04479         # Make 'previous' link
04480         $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
04481         if ( $offset > 0 ) {
04482             $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
04483                 $query, $prev, 'prevn-title', 'mw-prevlink' );
04484         } else {
04485             $plink = htmlspecialchars( $prev );
04486         }
04487 
04488         # Make 'next' link
04489         $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
04490         if ( $atend ) {
04491             $nlink = htmlspecialchars( $next );
04492         } else {
04493             $nlink = $this->numLink( $title, $offset + $limit, $limit,
04494                 $query, $next, 'nextn-title', 'mw-nextlink' );
04495         }
04496 
04497         # Make links to set number of items per page
04498         $numLinks = array();
04499         foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
04500             $numLinks[] = $this->numLink( $title, $offset, $num,
04501                 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
04502         }
04503 
04504         return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
04505             )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
04506     }
04507 
04520     private function numLink( Title $title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) {
04521         $query = array( 'limit' => $limit, 'offset' => $offset ) + $query;
04522         $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
04523         return Html::element( 'a', array( 'href' => $title->getLocalURL( $query ),
04524             'title' => $tooltip, 'class' => $class ), $link );
04525     }
04526 
04532     public function getConvRuleTitle() {
04533         return $this->mConverter->getConvRuleTitle();
04534     }
04535 
04541     public function getCompiledPluralRules() {
04542         $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
04543         $fallbacks = Language::getFallbacksFor( $this->mCode );
04544         if ( !$pluralRules ) {
04545             foreach ( $fallbacks as $fallbackCode ) {
04546                 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
04547                 if ( $pluralRules ) {
04548                     break;
04549                 }
04550             }
04551         }
04552         return $pluralRules;
04553     }
04554 
04560     public function getPluralRules() {
04561         $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
04562         $fallbacks = Language::getFallbacksFor( $this->mCode );
04563         if ( !$pluralRules ) {
04564             foreach ( $fallbacks as $fallbackCode ) {
04565                 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
04566                 if ( $pluralRules ) {
04567                     break;
04568                 }
04569             }
04570         }
04571         return $pluralRules;
04572     }
04573 
04579     public function getPluralRuleTypes() {
04580         $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
04581         $fallbacks = Language::getFallbacksFor( $this->mCode );
04582         if ( !$pluralRuleTypes ) {
04583             foreach ( $fallbacks as $fallbackCode ) {
04584                 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
04585                 if ( $pluralRuleTypes ) {
04586                     break;
04587                 }
04588             }
04589         }
04590         return $pluralRuleTypes;
04591     }
04592 
04597     public function getPluralRuleIndexNumber( $number ) {
04598         $pluralRules = $this->getCompiledPluralRules();
04599         $form = CLDRPluralRuleEvaluator::evaluateCompiled( $number, $pluralRules );
04600         return $form;
04601     }
04602 
04610     public function getPluralRuleType( $number ) {
04611         $index = $this->getPluralRuleIndexNumber( $number );
04612         $pluralRuleTypes = $this->getPluralRuleTypes();
04613         if ( isset( $pluralRuleTypes[$index] ) ) {
04614             return $pluralRuleTypes[$index];
04615         } else {
04616             return 'other';
04617         }
04618     }
04619 }