MediaWiki  REL1_23
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; }
00066     function armourMath( $text ) { return $text; }
00067     function validateVariant( $variant = null ) { return $variant === $this->mLang->getCode() ? $variant : null; }
00068     function translate( $text, $variant ) { return $text; }
00069 }
00070 
00075 class Language {
00076 
00080     public $mConverter;
00081 
00082     public $mVariants, $mCode, $mLoaded = false;
00083     public $mMagicExtensions = array(), $mMagicHookDone = false;
00084     private $mHtmlCode = null, $mParentLanguage = false;
00085 
00086     public $dateFormatStrings = array();
00087     public $mExtendedSpecialPageAliases;
00088 
00089     protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
00090 
00094     public $transformData = array();
00095 
00099     static public $dataCache;
00100 
00101     static public $mLangObjCache = array();
00102 
00103     static public $mWeekdayMsgs = array(
00104         'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
00105         'friday', 'saturday'
00106     );
00107 
00108     static public $mWeekdayAbbrevMsgs = array(
00109         'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
00110     );
00111 
00112     static public $mMonthMsgs = array(
00113         'january', 'february', 'march', 'april', 'may_long', 'june',
00114         'july', 'august', 'september', 'october', 'november',
00115         'december'
00116     );
00117     static public $mMonthGenMsgs = array(
00118         'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
00119         'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
00120         'december-gen'
00121     );
00122     static public $mMonthAbbrevMsgs = array(
00123         'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
00124         'sep', 'oct', 'nov', 'dec'
00125     );
00126 
00127     static public $mIranianCalendarMonthMsgs = array(
00128         'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
00129         'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
00130         'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
00131         'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
00132     );
00133 
00134     static public $mHebrewCalendarMonthMsgs = array(
00135         'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
00136         'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
00137         'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
00138         'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
00139         'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
00140     );
00141 
00142     static public $mHebrewCalendarMonthGenMsgs = array(
00143         'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
00144         'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
00145         'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
00146         'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
00147         'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
00148     );
00149 
00150     static public $mHijriCalendarMonthMsgs = array(
00151         'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
00152         'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
00153         'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
00154         'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
00155     );
00156 
00161     static public $durationIntervals = array(
00162         'millennia' => 31556952000,
00163         'centuries' => 3155695200,
00164         'decades' => 315569520,
00165         'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
00166         'weeks' => 604800,
00167         'days' => 86400,
00168         'hours' => 3600,
00169         'minutes' => 60,
00170         'seconds' => 1,
00171     );
00172 
00179     static private $fallbackLanguageCache = array();
00180 
00186     static function factory( $code ) {
00187         global $wgDummyLanguageCodes, $wgLangObjCacheSize;
00188 
00189         if ( isset( $wgDummyLanguageCodes[$code] ) ) {
00190             $code = $wgDummyLanguageCodes[$code];
00191         }
00192 
00193         // get the language object to process
00194         $langObj = isset( self::$mLangObjCache[$code] )
00195             ? self::$mLangObjCache[$code]
00196             : self::newFromCode( $code );
00197 
00198         // merge the language object in to get it up front in the cache
00199         self::$mLangObjCache = array_merge( array( $code => $langObj ), self::$mLangObjCache );
00200         // get rid of the oldest ones in case we have an overflow
00201         self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
00202 
00203         return $langObj;
00204     }
00205 
00212     protected static function newFromCode( $code ) {
00213         // Protect against path traversal below
00214         if ( !Language::isValidCode( $code )
00215             || strcspn( $code, ":/\\\000" ) !== strlen( $code )
00216         ) {
00217             throw new MWException( "Invalid language code \"$code\"" );
00218         }
00219 
00220         if ( !Language::isValidBuiltInCode( $code ) ) {
00221             // It's not possible to customise this code with class files, so
00222             // just return a Language object. This is to support uselang= hacks.
00223             $lang = new Language;
00224             $lang->setCode( $code );
00225             return $lang;
00226         }
00227 
00228         // Check if there is a language class for the code
00229         $class = self::classFromCode( $code );
00230         self::preloadLanguageClass( $class );
00231         if ( class_exists( $class ) ) {
00232             $lang = new $class;
00233             return $lang;
00234         }
00235 
00236         // Keep trying the fallback list until we find an existing class
00237         $fallbacks = Language::getFallbacksFor( $code );
00238         foreach ( $fallbacks as $fallbackCode ) {
00239             if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
00240                 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
00241             }
00242 
00243             $class = self::classFromCode( $fallbackCode );
00244             self::preloadLanguageClass( $class );
00245             if ( class_exists( $class ) ) {
00246                 $lang = Language::newFromCode( $fallbackCode );
00247                 $lang->setCode( $code );
00248                 return $lang;
00249             }
00250         }
00251 
00252         throw new MWException( "Invalid fallback sequence for language '$code'" );
00253     }
00254 
00263     public static function isSupportedLanguage( $code ) {
00264         return self::isValidBuiltInCode( $code )
00265             && ( is_readable( self::getMessagesFileName( $code ) )
00266                 || is_readable( self::getJsonMessagesFileName( $code ) )
00267         );
00268     }
00269 
00285     public static function isWellFormedLanguageTag( $code, $lenient = false ) {
00286         $alpha = '[a-z]';
00287         $digit = '[0-9]';
00288         $alphanum = '[a-z0-9]';
00289         $x = 'x'; # private use singleton
00290         $singleton = '[a-wy-z]'; # other singleton
00291         $s = $lenient ? '[-_]' : '-';
00292 
00293         $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
00294         $script = "$alpha{4}"; # ISO 15924
00295         $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
00296         $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
00297         $extension = "$singleton(?:$s$alphanum{2,8})+";
00298         $privateUse = "$x(?:$s$alphanum{1,8})+";
00299 
00300         # Define certain grandfathered codes, since otherwise the regex is pretty useless.
00301         # Since these are limited, this is safe even later changes to the registry --
00302         # the only oddity is that it might change the type of the tag, and thus
00303         # the results from the capturing groups.
00304         # http://www.iana.org/assignments/language-subtag-registry
00305 
00306         $grandfathered = "en{$s}GB{$s}oed"
00307             . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
00308             . "|no{$s}(?:bok|nyn)"
00309             . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
00310             . "|zh{$s}min{$s}nan";
00311 
00312         $variantList = "$variant(?:$s$variant)*";
00313         $extensionList = "$extension(?:$s$extension)*";
00314 
00315         $langtag = "(?:($language)"
00316             . "(?:$s$script)?"
00317             . "(?:$s$region)?"
00318             . "(?:$s$variantList)?"
00319             . "(?:$s$extensionList)?"
00320             . "(?:$s$privateUse)?)";
00321 
00322         # The final breakdown, with capturing groups for each of these components
00323         # The variants, extensions, grandfathered, and private-use may have interior '-'
00324 
00325         $root = "^(?:$langtag|$privateUse|$grandfathered)$";
00326 
00327         return (bool)preg_match( "/$root/", strtolower( $code ) );
00328     }
00329 
00339     public static function isValidCode( $code ) {
00340         static $cache = array();
00341         if ( isset( $cache[$code] ) ) {
00342             return $cache[$code];
00343         }
00344         // People think language codes are html safe, so enforce it.
00345         // Ideally we should only allow a-zA-Z0-9-
00346         // but, .+ and other chars are often used for {{int:}} hacks
00347         // see bugs 37564, 37587, 36938
00348         $cache[$code] =
00349             strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
00350             && !preg_match( Title::getTitleInvalidRegex(), $code );
00351 
00352         return $cache[$code];
00353     }
00354 
00365     public static function isValidBuiltInCode( $code ) {
00366 
00367         if ( !is_string( $code ) ) {
00368             if ( is_object( $code ) ) {
00369                 $addmsg = " of class " . get_class( $code );
00370             } else {
00371                 $addmsg = '';
00372             }
00373             $type = gettype( $code );
00374             throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
00375         }
00376 
00377         return (bool)preg_match( '/^[a-z0-9-]{2,}$/i', $code );
00378     }
00379 
00388     public static function isKnownLanguageTag( $tag ) {
00389         static $coreLanguageNames;
00390 
00391         // Quick escape for invalid input to avoid exceptions down the line
00392         // when code tries to process tags which are not valid at all.
00393         if ( !self::isValidBuiltInCode( $tag ) ) {
00394             return false;
00395         }
00396 
00397         if ( $coreLanguageNames === null ) {
00398             global $IP;
00399             include "$IP/languages/Names.php";
00400         }
00401 
00402         if ( isset( $coreLanguageNames[$tag] )
00403             || self::fetchLanguageName( $tag, $tag ) !== ''
00404         ) {
00405             return true;
00406         }
00407 
00408         return false;
00409     }
00410 
00415     public static function classFromCode( $code ) {
00416         if ( $code == 'en' ) {
00417             return 'Language';
00418         } else {
00419             return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
00420         }
00421     }
00422 
00428     public static function preloadLanguageClass( $class ) {
00429         global $IP;
00430 
00431         if ( $class === 'Language' ) {
00432             return;
00433         }
00434 
00435         if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
00436             include_once "$IP/languages/classes/$class.php";
00437         }
00438     }
00439 
00445     public static function getLocalisationCache() {
00446         if ( is_null( self::$dataCache ) ) {
00447             global $wgLocalisationCacheConf;
00448             $class = $wgLocalisationCacheConf['class'];
00449             self::$dataCache = new $class( $wgLocalisationCacheConf );
00450         }
00451         return self::$dataCache;
00452     }
00453 
00454     function __construct() {
00455         $this->mConverter = new FakeConverter( $this );
00456         // Set the code to the name of the descendant
00457         if ( get_class( $this ) == 'Language' ) {
00458             $this->mCode = 'en';
00459         } else {
00460             $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
00461         }
00462         self::getLocalisationCache();
00463     }
00464 
00468     function __destruct() {
00469         foreach ( $this as $name => $value ) {
00470             unset( $this->$name );
00471         }
00472     }
00473 
00478     function initContLang() { }
00479 
00485     function getFallbackLanguageCode() {
00486         wfDeprecated( __METHOD__, '1.19' );
00487         return self::getFallbackFor( $this->mCode );
00488     }
00489 
00494     function getFallbackLanguages() {
00495         return self::getFallbacksFor( $this->mCode );
00496     }
00497 
00502     function getBookstoreList() {
00503         return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
00504     }
00505 
00512     public function getNamespaces() {
00513         if ( is_null( $this->namespaceNames ) ) {
00514             global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
00515 
00516             $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
00517             $validNamespaces = MWNamespace::getCanonicalNamespaces();
00518 
00519             $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
00520 
00521             $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
00522             if ( $wgMetaNamespaceTalk ) {
00523                 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
00524             } else {
00525                 $talk = $this->namespaceNames[NS_PROJECT_TALK];
00526                 $this->namespaceNames[NS_PROJECT_TALK] =
00527                     $this->fixVariableInNamespace( $talk );
00528             }
00529 
00530             # Sometimes a language will be localised but not actually exist on this wiki.
00531             foreach ( $this->namespaceNames as $key => $text ) {
00532                 if ( !isset( $validNamespaces[$key] ) ) {
00533                     unset( $this->namespaceNames[$key] );
00534                 }
00535             }
00536 
00537             # The above mixing may leave namespaces out of canonical order.
00538             # Re-order by namespace ID number...
00539             ksort( $this->namespaceNames );
00540 
00541             wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames ) );
00542         }
00543         return $this->namespaceNames;
00544     }
00545 
00550     public function setNamespaces( array $namespaces ) {
00551         $this->namespaceNames = $namespaces;
00552         $this->mNamespaceIds = null;
00553     }
00554 
00558     public function resetNamespaces() {
00559         $this->namespaceNames = null;
00560         $this->mNamespaceIds = null;
00561         $this->namespaceAliases = null;
00562     }
00563 
00572     function getFormattedNamespaces() {
00573         $ns = $this->getNamespaces();
00574         foreach ( $ns as $k => $v ) {
00575             $ns[$k] = strtr( $v, '_', ' ' );
00576         }
00577         return $ns;
00578     }
00579 
00590     function getNsText( $index ) {
00591         $ns = $this->getNamespaces();
00592         return isset( $ns[$index] ) ? $ns[$index] : false;
00593     }
00594 
00608     function getFormattedNsText( $index ) {
00609         $ns = $this->getNsText( $index );
00610         return strtr( $ns, '_', ' ' );
00611     }
00612 
00621     function getGenderNsText( $index, $gender ) {
00622         global $wgExtraGenderNamespaces;
00623 
00624         $ns = $wgExtraGenderNamespaces + self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
00625         return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
00626     }
00627 
00634     function needsGenderDistinction() {
00635         global $wgExtraGenderNamespaces, $wgExtraNamespaces;
00636         if ( count( $wgExtraGenderNamespaces ) > 0 ) {
00637             // $wgExtraGenderNamespaces overrides everything
00638             return true;
00639         } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
00641             // $wgExtraNamespaces overrides any gender aliases specified in i18n files
00642             return false;
00643         } else {
00644             // Check what is in i18n files
00645             $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
00646             return count( $aliases ) > 0;
00647         }
00648     }
00649 
00658     function getLocalNsIndex( $text ) {
00659         $lctext = $this->lc( $text );
00660         $ids = $this->getNamespaceIds();
00661         return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
00662     }
00663 
00667     function getNamespaceAliases() {
00668         if ( is_null( $this->namespaceAliases ) ) {
00669             $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
00670             if ( !$aliases ) {
00671                 $aliases = array();
00672             } else {
00673                 foreach ( $aliases as $name => $index ) {
00674                     if ( $index === NS_PROJECT_TALK ) {
00675                         unset( $aliases[$name] );
00676                         $name = $this->fixVariableInNamespace( $name );
00677                         $aliases[$name] = $index;
00678                     }
00679                 }
00680             }
00681 
00682             global $wgExtraGenderNamespaces;
00683             $genders = $wgExtraGenderNamespaces + (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
00684             foreach ( $genders as $index => $forms ) {
00685                 foreach ( $forms as $alias ) {
00686                     $aliases[$alias] = $index;
00687                 }
00688             }
00689 
00690             # Also add converted namespace names as aliases, to avoid confusion.
00691             $convertedNames = array();
00692             foreach ( $this->getVariants() as $variant ) {
00693                 if ( $variant === $this->mCode ) {
00694                     continue;
00695                 }
00696                 foreach ( $this->getNamespaces() as $ns => $_ ) {
00697                     $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
00698                 }
00699             }
00700 
00701             $this->namespaceAliases = $aliases + $convertedNames;
00702         }
00703         return $this->namespaceAliases;
00704     }
00705 
00709     function getNamespaceIds() {
00710         if ( is_null( $this->mNamespaceIds ) ) {
00711             global $wgNamespaceAliases;
00712             # Put namespace names and aliases into a hashtable.
00713             # If this is too slow, then we should arrange it so that it is done
00714             # before caching. The catch is that at pre-cache time, the above
00715             # class-specific fixup hasn't been done.
00716             $this->mNamespaceIds = array();
00717             foreach ( $this->getNamespaces() as $index => $name ) {
00718                 $this->mNamespaceIds[$this->lc( $name )] = $index;
00719             }
00720             foreach ( $this->getNamespaceAliases() as $name => $index ) {
00721                 $this->mNamespaceIds[$this->lc( $name )] = $index;
00722             }
00723             if ( $wgNamespaceAliases ) {
00724                 foreach ( $wgNamespaceAliases as $name => $index ) {
00725                     $this->mNamespaceIds[$this->lc( $name )] = $index;
00726                 }
00727             }
00728         }
00729         return $this->mNamespaceIds;
00730     }
00731 
00739     function getNsIndex( $text ) {
00740         $lctext = $this->lc( $text );
00741         $ns = MWNamespace::getCanonicalIndex( $lctext );
00742         if ( $ns !== null ) {
00743             return $ns;
00744         }
00745         $ids = $this->getNamespaceIds();
00746         return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
00747     }
00748 
00756     function getVariantname( $code, $usemsg = true ) {
00757         $msg = "variantname-$code";
00758         if ( $usemsg && wfMessage( $msg )->exists() ) {
00759             return $this->getMessageFromDB( $msg );
00760         }
00761         $name = self::fetchLanguageName( $code );
00762         if ( $name ) {
00763             return $name; # if it's defined as a language name, show that
00764         } else {
00765             # otherwise, output the language code
00766             return $code;
00767         }
00768     }
00769 
00774     function specialPage( $name ) {
00775         $aliases = $this->getSpecialPageAliases();
00776         if ( isset( $aliases[$name][0] ) ) {
00777             $name = $aliases[$name][0];
00778         }
00779         return $this->getNsText( NS_SPECIAL ) . ':' . $name;
00780     }
00781 
00785     function getDatePreferences() {
00786         return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
00787     }
00788 
00792     function getDateFormats() {
00793         return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
00794     }
00795 
00799     function getDefaultDateFormat() {
00800         $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
00801         if ( $df === 'dmy or mdy' ) {
00802             global $wgAmericanDates;
00803             return $wgAmericanDates ? 'mdy' : 'dmy';
00804         } else {
00805             return $df;
00806         }
00807     }
00808 
00812     function getDatePreferenceMigrationMap() {
00813         return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
00814     }
00815 
00820     function getImageFile( $image ) {
00821         return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
00822     }
00823 
00827     function getExtraUserToggles() {
00828         return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
00829     }
00830 
00835     function getUserToggle( $tog ) {
00836         return $this->getMessageFromDB( "tog-$tog" );
00837     }
00838 
00849     public static function getLanguageNames( $customisedOnly = false ) {
00850         return self::fetchLanguageNames( null, $customisedOnly ? 'mwfile' : 'mw' );
00851     }
00852 
00862     public static function getTranslatedLanguageNames( $code ) {
00863         return self::fetchLanguageNames( $code, 'all' );
00864     }
00865 
00877     public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
00878         global $wgExtraLanguageNames;
00879         static $coreLanguageNames;
00880 
00881         if ( $coreLanguageNames === null ) {
00882             global $IP;
00883             include "$IP/languages/Names.php";
00884         }
00885 
00886         // If passed an invalid language code to use, fallback to en
00887         if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
00888             $inLanguage = 'en';
00889         }
00890 
00891         $names = array();
00892 
00893         if ( $inLanguage ) {
00894             # TODO: also include when $inLanguage is null, when this code is more efficient
00895             wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
00896         }
00897 
00898         $mwNames = $wgExtraLanguageNames + $coreLanguageNames;
00899         foreach ( $mwNames as $mwCode => $mwName ) {
00900             # - Prefer own MediaWiki native name when not using the hook
00901             # - For other names just add if not added through the hook
00902             if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
00903                 $names[$mwCode] = $mwName;
00904             }
00905         }
00906 
00907         if ( $include === 'all' ) {
00908             return $names;
00909         }
00910 
00911         $returnMw = array();
00912         $coreCodes = array_keys( $mwNames );
00913         foreach ( $coreCodes as $coreCode ) {
00914             $returnMw[$coreCode] = $names[$coreCode];
00915         }
00916 
00917         if ( $include === 'mwfile' ) {
00918             $namesMwFile = array();
00919             # We do this using a foreach over the codes instead of a directory
00920             # loop so that messages files in extensions will work correctly.
00921             foreach ( $returnMw as $code => $value ) {
00922                 if ( is_readable( self::getMessagesFileName( $code ) )
00923                     || is_readable( self::getJsonMessagesFileName( $code ) )
00924                 ) {
00925                     $namesMwFile[$code] = $names[$code];
00926                 }
00927             }
00928 
00929             return $namesMwFile;
00930         }
00931 
00932         # 'mw' option; default if it's not one of the other two options (all/mwfile)
00933         return $returnMw;
00934     }
00935 
00943     public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
00944         $code = strtolower( $code );
00945         $array = self::fetchLanguageNames( $inLanguage, $include );
00946         return !array_key_exists( $code, $array ) ? '' : $array[$code];
00947     }
00948 
00955     function getMessageFromDB( $msg ) {
00956         return wfMessage( $msg )->inLanguage( $this )->text();
00957     }
00958 
00966     function getLanguageName( $code ) {
00967         return self::fetchLanguageName( $code );
00968     }
00969 
00974     function getMonthName( $key ) {
00975         return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
00976     }
00977 
00981     function getMonthNamesArray() {
00982         $monthNames = array( '' );
00983         for ( $i = 1; $i < 13; $i++ ) {
00984             $monthNames[] = $this->getMonthName( $i );
00985         }
00986         return $monthNames;
00987     }
00988 
00993     function getMonthNameGen( $key ) {
00994         return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
00995     }
00996 
01001     function getMonthAbbreviation( $key ) {
01002         return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
01003     }
01004 
01008     function getMonthAbbreviationsArray() {
01009         $monthNames = array( '' );
01010         for ( $i = 1; $i < 13; $i++ ) {
01011             $monthNames[] = $this->getMonthAbbreviation( $i );
01012         }
01013         return $monthNames;
01014     }
01015 
01020     function getWeekdayName( $key ) {
01021         return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
01022     }
01023 
01028     function getWeekdayAbbreviation( $key ) {
01029         return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
01030     }
01031 
01036     function getIranianCalendarMonthName( $key ) {
01037         return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
01038     }
01039 
01044     function getHebrewCalendarMonthName( $key ) {
01045         return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
01046     }
01047 
01052     function getHebrewCalendarMonthNameGen( $key ) {
01053         return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
01054     }
01055 
01060     function getHijriCalendarMonthName( $key ) {
01061         return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
01062     }
01063 
01129     function sprintfDate( $format, $ts, DateTimeZone $zone = null ) {
01130         $s = '';
01131         $raw = false;
01132         $roman = false;
01133         $hebrewNum = false;
01134         $dateTimeObj = false;
01135         $rawToggle = false;
01136         $iranian = false;
01137         $hebrew = false;
01138         $hijri = false;
01139         $thai = false;
01140         $minguo = false;
01141         $tenno = false;
01142 
01143         if ( strlen( $ts ) !== 14 ) {
01144             throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
01145         }
01146 
01147         if ( !ctype_digit( $ts ) ) {
01148             throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
01149         }
01150 
01151         for ( $p = 0; $p < strlen( $format ); $p++ ) {
01152             $num = false;
01153             $code = $format[$p];
01154             if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
01155                 $code .= $format[++$p];
01156             }
01157 
01158             if ( ( $code === 'xi' || $code == 'xj' || $code == 'xk' || $code == 'xm' || $code == 'xo' || $code == 'xt' ) && $p < strlen( $format ) - 1 ) {
01159                 $code .= $format[++$p];
01160             }
01161 
01162             switch ( $code ) {
01163                 case 'xx':
01164                     $s .= 'x';
01165                     break;
01166                 case 'xn':
01167                     $raw = true;
01168                     break;
01169                 case 'xN':
01170                     $rawToggle = !$rawToggle;
01171                     break;
01172                 case 'xr':
01173                     $roman = true;
01174                     break;
01175                 case 'xh':
01176                     $hebrewNum = true;
01177                     break;
01178                 case 'xg':
01179                     $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
01180                     break;
01181                 case 'xjx':
01182                     if ( !$hebrew ) {
01183                         $hebrew = self::tsToHebrew( $ts );
01184                     }
01185                     $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
01186                     break;
01187                 case 'd':
01188                     $num = substr( $ts, 6, 2 );
01189                     break;
01190                 case 'D':
01191                     if ( !$dateTimeObj ) {
01192                         $dateTimeObj = DateTime::createFromFormat(
01193                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01194                         );
01195                     }
01196                     $s .= $this->getWeekdayAbbreviation( $dateTimeObj->format( 'w' ) + 1 );
01197                     break;
01198                 case 'j':
01199                     $num = intval( substr( $ts, 6, 2 ) );
01200                     break;
01201                 case 'xij':
01202                     if ( !$iranian ) {
01203                         $iranian = self::tsToIranian( $ts );
01204                     }
01205                     $num = $iranian[2];
01206                     break;
01207                 case 'xmj':
01208                     if ( !$hijri ) {
01209                         $hijri = self::tsToHijri( $ts );
01210                     }
01211                     $num = $hijri[2];
01212                     break;
01213                 case 'xjj':
01214                     if ( !$hebrew ) {
01215                         $hebrew = self::tsToHebrew( $ts );
01216                     }
01217                     $num = $hebrew[2];
01218                     break;
01219                 case 'l':
01220                     if ( !$dateTimeObj ) {
01221                         $dateTimeObj = DateTime::createFromFormat(
01222                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01223                         );
01224                     }
01225                     $s .= $this->getWeekdayName( $dateTimeObj->format( 'w' ) + 1 );
01226                     break;
01227                 case 'F':
01228                     $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
01229                     break;
01230                 case 'xiF':
01231                     if ( !$iranian ) {
01232                         $iranian = self::tsToIranian( $ts );
01233                     }
01234                     $s .= $this->getIranianCalendarMonthName( $iranian[1] );
01235                     break;
01236                 case 'xmF':
01237                     if ( !$hijri ) {
01238                         $hijri = self::tsToHijri( $ts );
01239                     }
01240                     $s .= $this->getHijriCalendarMonthName( $hijri[1] );
01241                     break;
01242                 case 'xjF':
01243                     if ( !$hebrew ) {
01244                         $hebrew = self::tsToHebrew( $ts );
01245                     }
01246                     $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
01247                     break;
01248                 case 'm':
01249                     $num = substr( $ts, 4, 2 );
01250                     break;
01251                 case 'M':
01252                     $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
01253                     break;
01254                 case 'n':
01255                     $num = intval( substr( $ts, 4, 2 ) );
01256                     break;
01257                 case 'xin':
01258                     if ( !$iranian ) {
01259                         $iranian = self::tsToIranian( $ts );
01260                     }
01261                     $num = $iranian[1];
01262                     break;
01263                 case 'xmn':
01264                     if ( !$hijri ) {
01265                         $hijri = self::tsToHijri ( $ts );
01266                     }
01267                     $num = $hijri[1];
01268                     break;
01269                 case 'xjn':
01270                     if ( !$hebrew ) {
01271                         $hebrew = self::tsToHebrew( $ts );
01272                     }
01273                     $num = $hebrew[1];
01274                     break;
01275                 case 'xjt':
01276                     if ( !$hebrew ) {
01277                         $hebrew = self::tsToHebrew( $ts );
01278                     }
01279                     $num = $hebrew[3];
01280                     break;
01281                 case 'Y':
01282                     $num = substr( $ts, 0, 4 );
01283                     break;
01284                 case 'xiY':
01285                     if ( !$iranian ) {
01286                         $iranian = self::tsToIranian( $ts );
01287                     }
01288                     $num = $iranian[0];
01289                     break;
01290                 case 'xmY':
01291                     if ( !$hijri ) {
01292                         $hijri = self::tsToHijri( $ts );
01293                     }
01294                     $num = $hijri[0];
01295                     break;
01296                 case 'xjY':
01297                     if ( !$hebrew ) {
01298                         $hebrew = self::tsToHebrew( $ts );
01299                     }
01300                     $num = $hebrew[0];
01301                     break;
01302                 case 'xkY':
01303                     if ( !$thai ) {
01304                         $thai = self::tsToYear( $ts, 'thai' );
01305                     }
01306                     $num = $thai[0];
01307                     break;
01308                 case 'xoY':
01309                     if ( !$minguo ) {
01310                         $minguo = self::tsToYear( $ts, 'minguo' );
01311                     }
01312                     $num = $minguo[0];
01313                     break;
01314                 case 'xtY':
01315                     if ( !$tenno ) {
01316                         $tenno = self::tsToYear( $ts, 'tenno' );
01317                     }
01318                     $num = $tenno[0];
01319                     break;
01320                 case 'y':
01321                     $num = substr( $ts, 2, 2 );
01322                     break;
01323                 case 'xiy':
01324                     if ( !$iranian ) {
01325                         $iranian = self::tsToIranian( $ts );
01326                     }
01327                     $num = substr( $iranian[0], -2 );
01328                     break;
01329                 case 'a':
01330                     $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
01331                     break;
01332                 case 'A':
01333                     $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
01334                     break;
01335                 case 'g':
01336                     $h = substr( $ts, 8, 2 );
01337                     $num = $h % 12 ? $h % 12 : 12;
01338                     break;
01339                 case 'G':
01340                     $num = intval( substr( $ts, 8, 2 ) );
01341                     break;
01342                 case 'h':
01343                     $h = substr( $ts, 8, 2 );
01344                     $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
01345                     break;
01346                 case 'H':
01347                     $num = substr( $ts, 8, 2 );
01348                     break;
01349                 case 'i':
01350                     $num = substr( $ts, 10, 2 );
01351                     break;
01352                 case 's':
01353                     $num = substr( $ts, 12, 2 );
01354                     break;
01355                 case 'c':
01356                 case 'r':
01357                 case 'e':
01358                 case 'O':
01359                 case 'P':
01360                 case 'T':
01361                     // Pass through string from $dateTimeObj->format()
01362                     if ( !$dateTimeObj ) {
01363                         $dateTimeObj = DateTime::createFromFormat(
01364                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01365                         );
01366                     }
01367                     $s .= $dateTimeObj->format( $code );
01368                     break;
01369                 case 'w':
01370                 case 'N':
01371                 case 'z':
01372                 case 'W':
01373                 case 't':
01374                 case 'L':
01375                 case 'o':
01376                 case 'U':
01377                 case 'I':
01378                 case 'Z':
01379                     // Pass through number from $dateTimeObj->format()
01380                     if ( !$dateTimeObj ) {
01381                         $dateTimeObj = DateTime::createFromFormat(
01382                             'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
01383                         );
01384                     }
01385                     $num = $dateTimeObj->format( $code );
01386                     break;
01387                 case '\\':
01388                     # Backslash escaping
01389                     if ( $p < strlen( $format ) - 1 ) {
01390                         $s .= $format[++$p];
01391                     } else {
01392                         $s .= '\\';
01393                     }
01394                     break;
01395                 case '"':
01396                     # Quoted literal
01397                     if ( $p < strlen( $format ) - 1 ) {
01398                         $endQuote = strpos( $format, '"', $p + 1 );
01399                         if ( $endQuote === false ) {
01400                             # No terminating quote, assume literal "
01401                             $s .= '"';
01402                         } else {
01403                             $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
01404                             $p = $endQuote;
01405                         }
01406                     } else {
01407                         # Quote at end of string, assume literal "
01408                         $s .= '"';
01409                     }
01410                     break;
01411                 default:
01412                     $s .= $format[$p];
01413             }
01414             if ( $num !== false ) {
01415                 if ( $rawToggle || $raw ) {
01416                     $s .= $num;
01417                     $raw = false;
01418                 } elseif ( $roman ) {
01419                     $s .= Language::romanNumeral( $num );
01420                     $roman = false;
01421                 } elseif ( $hebrewNum ) {
01422                     $s .= self::hebrewNumeral( $num );
01423                     $hebrewNum = false;
01424                 } else {
01425                     $s .= $this->formatNum( $num, true );
01426                 }
01427             }
01428         }
01429         return $s;
01430     }
01431 
01432     private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
01433     private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
01434 
01447     private static function tsToIranian( $ts ) {
01448         $gy = substr( $ts, 0, 4 ) -1600;
01449         $gm = substr( $ts, 4, 2 ) -1;
01450         $gd = substr( $ts, 6, 2 ) -1;
01451 
01452         # Days passed from the beginning (including leap years)
01453         $gDayNo = 365 * $gy
01454             + floor( ( $gy + 3 ) / 4 )
01455             - floor( ( $gy + 99 ) / 100 )
01456             + floor( ( $gy + 399 ) / 400 );
01457 
01458         // Add days of the past months of this year
01459         for ( $i = 0; $i < $gm; $i++ ) {
01460             $gDayNo += self::$GREG_DAYS[$i];
01461         }
01462 
01463         // Leap years
01464         if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
01465             $gDayNo++;
01466         }
01467 
01468         // Days passed in current month
01469         $gDayNo += (int)$gd;
01470 
01471         $jDayNo = $gDayNo - 79;
01472 
01473         $jNp = floor( $jDayNo / 12053 );
01474         $jDayNo %= 12053;
01475 
01476         $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
01477         $jDayNo %= 1461;
01478 
01479         if ( $jDayNo >= 366 ) {
01480             $jy += floor( ( $jDayNo - 1 ) / 365 );
01481             $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
01482         }
01483 
01484         for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
01485             $jDayNo -= self::$IRANIAN_DAYS[$i];
01486         }
01487 
01488         $jm = $i + 1;
01489         $jd = $jDayNo + 1;
01490 
01491         return array( $jy, $jm, $jd );
01492     }
01493 
01505     private static function tsToHijri( $ts ) {
01506         $year = substr( $ts, 0, 4 );
01507         $month = substr( $ts, 4, 2 );
01508         $day = substr( $ts, 6, 2 );
01509 
01510         $zyr = $year;
01511         $zd = $day;
01512         $zm = $month;
01513         $zy = $zyr;
01514 
01515         if (
01516             ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
01517             ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
01518         ) {
01519             $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
01520                     (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
01521                     (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
01522                     $zd - 32075;
01523         } else {
01524             $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
01525                                 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
01526         }
01527 
01528         $zl = $zjd -1948440 + 10632;
01529         $zn = (int)( ( $zl - 1 ) / 10631 );
01530         $zl = $zl - 10631 * $zn + 354;
01531         $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) + ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
01532         $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
01533         $zm = (int)( ( 24 * $zl ) / 709 );
01534         $zd = $zl - (int)( ( 709 * $zm ) / 24 );
01535         $zy = 30 * $zn + $zj - 30;
01536 
01537         return array( $zy, $zm, $zd );
01538     }
01539 
01555     private static function tsToHebrew( $ts ) {
01556         # Parse date
01557         $year = substr( $ts, 0, 4 );
01558         $month = substr( $ts, 4, 2 );
01559         $day = substr( $ts, 6, 2 );
01560 
01561         # Calculate Hebrew year
01562         $hebrewYear = $year + 3760;
01563 
01564         # Month number when September = 1, August = 12
01565         $month += 4;
01566         if ( $month > 12 ) {
01567             # Next year
01568             $month -= 12;
01569             $year++;
01570             $hebrewYear++;
01571         }
01572 
01573         # Calculate day of year from 1 September
01574         $dayOfYear = $day;
01575         for ( $i = 1; $i < $month; $i++ ) {
01576             if ( $i == 6 ) {
01577                 # February
01578                 $dayOfYear += 28;
01579                 # Check if the year is leap
01580                 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
01581                     $dayOfYear++;
01582                 }
01583             } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
01584                 $dayOfYear += 30;
01585             } else {
01586                 $dayOfYear += 31;
01587             }
01588         }
01589 
01590         # Calculate the start of the Hebrew year
01591         $start = self::hebrewYearStart( $hebrewYear );
01592 
01593         # Calculate next year's start
01594         if ( $dayOfYear <= $start ) {
01595             # Day is before the start of the year - it is the previous year
01596             # Next year's start
01597             $nextStart = $start;
01598             # Previous year
01599             $year--;
01600             $hebrewYear--;
01601             # Add days since previous year's 1 September
01602             $dayOfYear += 365;
01603             if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
01604                 # Leap year
01605                 $dayOfYear++;
01606             }
01607             # Start of the new (previous) year
01608             $start = self::hebrewYearStart( $hebrewYear );
01609         } else {
01610             # Next year's start
01611             $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
01612         }
01613 
01614         # Calculate Hebrew day of year
01615         $hebrewDayOfYear = $dayOfYear - $start;
01616 
01617         # Difference between year's days
01618         $diff = $nextStart - $start;
01619         # Add 12 (or 13 for leap years) days to ignore the difference between
01620         # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
01621         # difference is only about the year type
01622         if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
01623             $diff += 13;
01624         } else {
01625             $diff += 12;
01626         }
01627 
01628         # Check the year pattern, and is leap year
01629         # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
01630         # This is mod 30, to work on both leap years (which add 30 days of Adar I)
01631         # and non-leap years
01632         $yearPattern = $diff % 30;
01633         # Check if leap year
01634         $isLeap = $diff >= 30;
01635 
01636         # Calculate day in the month from number of day in the Hebrew year
01637         # Don't check Adar - if the day is not in Adar, we will stop before;
01638         # if it is in Adar, we will use it to check if it is Adar I or Adar II
01639         $hebrewDay = $hebrewDayOfYear;
01640         $hebrewMonth = 1;
01641         $days = 0;
01642         while ( $hebrewMonth <= 12 ) {
01643             # Calculate days in this month
01644             if ( $isLeap && $hebrewMonth == 6 ) {
01645                 # Adar in a leap year
01646                 if ( $isLeap ) {
01647                     # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
01648                     $days = 30;
01649                     if ( $hebrewDay <= $days ) {
01650                         # Day in Adar I
01651                         $hebrewMonth = 13;
01652                     } else {
01653                         # Subtract the days of Adar I
01654                         $hebrewDay -= $days;
01655                         # Try Adar II
01656                         $days = 29;
01657                         if ( $hebrewDay <= $days ) {
01658                             # Day in Adar II
01659                             $hebrewMonth = 14;
01660                         }
01661                     }
01662                 }
01663             } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
01664                 # Cheshvan in a complete year (otherwise as the rule below)
01665                 $days = 30;
01666             } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
01667                 # Kislev in an incomplete year (otherwise as the rule below)
01668                 $days = 29;
01669             } else {
01670                 # Odd months have 30 days, even have 29
01671                 $days = 30 - ( $hebrewMonth - 1 ) % 2;
01672             }
01673             if ( $hebrewDay <= $days ) {
01674                 # In the current month
01675                 break;
01676             } else {
01677                 # Subtract the days of the current month
01678                 $hebrewDay -= $days;
01679                 # Try in the next month
01680                 $hebrewMonth++;
01681             }
01682         }
01683 
01684         return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
01685     }
01686 
01696     private static function hebrewYearStart( $year ) {
01697         $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
01698         $b = intval( ( $year - 1 ) % 4 );
01699         $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
01700         if ( $m < 0 ) {
01701             $m--;
01702         }
01703         $Mar = intval( $m );
01704         if ( $m < 0 ) {
01705             $m++;
01706         }
01707         $m -= $Mar;
01708 
01709         $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
01710         if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
01711             $Mar++;
01712         } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
01713             $Mar += 2;
01714         } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
01715             $Mar++;
01716         }
01717 
01718         $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
01719         return $Mar;
01720     }
01721 
01734     private static function tsToYear( $ts, $cName ) {
01735         $gy = substr( $ts, 0, 4 );
01736         $gm = substr( $ts, 4, 2 );
01737         $gd = substr( $ts, 6, 2 );
01738 
01739         if ( !strcmp( $cName, 'thai' ) ) {
01740             # Thai solar dates
01741             # Add 543 years to the Gregorian calendar
01742             # Months and days are identical
01743             $gy_offset = $gy + 543;
01744         } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
01745             # Minguo dates
01746             # Deduct 1911 years from the Gregorian calendar
01747             # Months and days are identical
01748             $gy_offset = $gy - 1911;
01749         } elseif ( !strcmp( $cName, 'tenno' ) ) {
01750             # Nengō dates up to Meiji period
01751             # Deduct years from the Gregorian calendar
01752             # depending on the nengo periods
01753             # Months and days are identical
01754             if ( ( $gy < 1912 ) || ( ( $gy == 1912 ) && ( $gm < 7 ) ) || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
01755                 # Meiji period
01756                 $gy_gannen = $gy - 1868 + 1;
01757                 $gy_offset = $gy_gannen;
01758                 if ( $gy_gannen == 1 ) {
01759                     $gy_offset = '元';
01760                 }
01761                 $gy_offset = '明治' . $gy_offset;
01762             } elseif (
01763                 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
01764                 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
01765                 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
01766                 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
01767                 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
01768             ) {
01769                 # Taishō period
01770                 $gy_gannen = $gy - 1912 + 1;
01771                 $gy_offset = $gy_gannen;
01772                 if ( $gy_gannen == 1 ) {
01773                     $gy_offset = '元';
01774                 }
01775                 $gy_offset = '大正' . $gy_offset;
01776             } elseif (
01777                 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
01778                 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
01779                 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
01780             ) {
01781                 # Shōwa period
01782                 $gy_gannen = $gy - 1926 + 1;
01783                 $gy_offset = $gy_gannen;
01784                 if ( $gy_gannen == 1 ) {
01785                     $gy_offset = '元';
01786                 }
01787                 $gy_offset = '昭和' . $gy_offset;
01788             } else {
01789                 # Heisei period
01790                 $gy_gannen = $gy - 1989 + 1;
01791                 $gy_offset = $gy_gannen;
01792                 if ( $gy_gannen == 1 ) {
01793                     $gy_offset = '元';
01794                 }
01795                 $gy_offset = '平成' . $gy_offset;
01796             }
01797         } else {
01798             $gy_offset = $gy;
01799         }
01800 
01801         return array( $gy_offset, $gm, $gd );
01802     }
01803 
01811     static function romanNumeral( $num ) {
01812         static $table = array(
01813             array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
01814             array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
01815             array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
01816             array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM', 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
01817         );
01818 
01819         $num = intval( $num );
01820         if ( $num > 10000 || $num <= 0 ) {
01821             return $num;
01822         }
01823 
01824         $s = '';
01825         for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
01826             if ( $num >= $pow10 ) {
01827                 $s .= $table[$i][(int)floor( $num / $pow10 )];
01828             }
01829             $num = $num % $pow10;
01830         }
01831         return $s;
01832     }
01833 
01841     static function hebrewNumeral( $num ) {
01842         static $table = array(
01843             array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
01844             array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
01845             array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
01846             array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
01847         );
01848 
01849         $num = intval( $num );
01850         if ( $num > 9999 || $num <= 0 ) {
01851             return $num;
01852         }
01853 
01854         $s = '';
01855         for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
01856             if ( $num >= $pow10 ) {
01857                 if ( $num == 15 || $num == 16 ) {
01858                     $s .= $table[0][9] . $table[0][$num - 9];
01859                     $num = 0;
01860                 } else {
01861                     $s .= $table[$i][intval( ( $num / $pow10 ) )];
01862                     if ( $pow10 == 1000 ) {
01863                         $s .= "'";
01864                     }
01865                 }
01866             }
01867             $num = $num % $pow10;
01868         }
01869         if ( strlen( $s ) == 2 ) {
01870             $str = $s . "'";
01871         } else {
01872             $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
01873             $str .= substr( $s, strlen( $s ) - 2, 2 );
01874         }
01875         $start = substr( $str, 0, strlen( $str ) - 2 );
01876         $end = substr( $str, strlen( $str ) - 2 );
01877         switch ( $end ) {
01878             case 'כ':
01879                 $str = $start . 'ך';
01880                 break;
01881             case 'מ':
01882                 $str = $start . 'ם';
01883                 break;
01884             case 'נ':
01885                 $str = $start . 'ן';
01886                 break;
01887             case 'פ':
01888                 $str = $start . 'ף';
01889                 break;
01890             case 'צ':
01891                 $str = $start . 'ץ';
01892                 break;
01893         }
01894         return $str;
01895     }
01896 
01905     function userAdjust( $ts, $tz = false ) {
01906         global $wgUser, $wgLocalTZoffset;
01907 
01908         if ( $tz === false ) {
01909             $tz = $wgUser->getOption( 'timecorrection' );
01910         }
01911 
01912         $data = explode( '|', $tz, 3 );
01913 
01914         if ( $data[0] == 'ZoneInfo' ) {
01915             wfSuppressWarnings();
01916             $userTZ = timezone_open( $data[2] );
01917             wfRestoreWarnings();
01918             if ( $userTZ !== false ) {
01919                 $date = date_create( $ts, timezone_open( 'UTC' ) );
01920                 date_timezone_set( $date, $userTZ );
01921                 $date = date_format( $date, 'YmdHis' );
01922                 return $date;
01923             }
01924             # Unrecognized timezone, default to 'Offset' with the stored offset.
01925             $data[0] = 'Offset';
01926         }
01927 
01928         $minDiff = 0;
01929         if ( $data[0] == 'System' || $tz == '' ) {
01930             #  Global offset in minutes.
01931             if ( isset( $wgLocalTZoffset ) ) {
01932                 $minDiff = $wgLocalTZoffset;
01933             }
01934         } elseif ( $data[0] == 'Offset' ) {
01935             $minDiff = intval( $data[1] );
01936         } else {
01937             $data = explode( ':', $tz );
01938             if ( count( $data ) == 2 ) {
01939                 $data[0] = intval( $data[0] );
01940                 $data[1] = intval( $data[1] );
01941                 $minDiff = abs( $data[0] ) * 60 + $data[1];
01942                 if ( $data[0] < 0 ) {
01943                     $minDiff = -$minDiff;
01944                 }
01945             } else {
01946                 $minDiff = intval( $data[0] ) * 60;
01947             }
01948         }
01949 
01950         # No difference ? Return time unchanged
01951         if ( 0 == $minDiff ) {
01952             return $ts;
01953         }
01954 
01955         wfSuppressWarnings(); // E_STRICT system time bitching
01956         # Generate an adjusted date; take advantage of the fact that mktime
01957         # will normalize out-of-range values so we don't have to split $minDiff
01958         # into hours and minutes.
01959         $t = mktime( (
01960             (int)substr( $ts, 8, 2 ) ), # Hours
01961             (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
01962             (int)substr( $ts, 12, 2 ), # Seconds
01963             (int)substr( $ts, 4, 2 ), # Month
01964             (int)substr( $ts, 6, 2 ), # Day
01965             (int)substr( $ts, 0, 4 ) ); # Year
01966 
01967         $date = date( 'YmdHis', $t );
01968         wfRestoreWarnings();
01969 
01970         return $date;
01971     }
01972 
01990     function dateFormat( $usePrefs = true ) {
01991         global $wgUser;
01992 
01993         if ( is_bool( $usePrefs ) ) {
01994             if ( $usePrefs ) {
01995                 $datePreference = $wgUser->getDatePreference();
01996             } else {
01997                 $datePreference = (string)User::getDefaultOption( 'date' );
01998             }
01999         } else {
02000             $datePreference = (string)$usePrefs;
02001         }
02002 
02003         // return int
02004         if ( $datePreference == '' ) {
02005             return 'default';
02006         }
02007 
02008         return $datePreference;
02009     }
02010 
02020     function getDateFormatString( $type, $pref ) {
02021         if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
02022             if ( $pref == 'default' ) {
02023                 $pref = $this->getDefaultDateFormat();
02024                 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
02025             } else {
02026                 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
02027 
02028                 if ( $type === 'pretty' && $df === null ) {
02029                     $df = $this->getDateFormatString( 'date', $pref );
02030                 }
02031 
02032                 if ( $df === null ) {
02033                     $pref = $this->getDefaultDateFormat();
02034                     $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
02035                 }
02036             }
02037             $this->dateFormatStrings[$type][$pref] = $df;
02038         }
02039         return $this->dateFormatStrings[$type][$pref];
02040     }
02041 
02052     function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
02053         $ts = wfTimestamp( TS_MW, $ts );
02054         if ( $adj ) {
02055             $ts = $this->userAdjust( $ts, $timecorrection );
02056         }
02057         $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
02058         return $this->sprintfDate( $df, $ts );
02059     }
02060 
02071     function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
02072         $ts = wfTimestamp( TS_MW, $ts );
02073         if ( $adj ) {
02074             $ts = $this->userAdjust( $ts, $timecorrection );
02075         }
02076         $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
02077         return $this->sprintfDate( $df, $ts );
02078     }
02079 
02091     function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
02092         $ts = wfTimestamp( TS_MW, $ts );
02093         if ( $adj ) {
02094             $ts = $this->userAdjust( $ts, $timecorrection );
02095         }
02096         $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
02097         return $this->sprintfDate( $df, $ts );
02098     }
02099 
02110     public function formatDuration( $seconds, array $chosenIntervals = array() ) {
02111         $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
02112 
02113         $segments = array();
02114 
02115         foreach ( $intervals as $intervalName => $intervalValue ) {
02116             // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
02117             // duration-years, duration-decades, duration-centuries, duration-millennia
02118             $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
02119             $segments[] = $message->inLanguage( $this )->escaped();
02120         }
02121 
02122         return $this->listToText( $segments );
02123     }
02124 
02136     public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
02137         if ( empty( $chosenIntervals ) ) {
02138             $chosenIntervals = array( 'millennia', 'centuries', 'decades', 'years', 'days', 'hours', 'minutes', 'seconds' );
02139         }
02140 
02141         $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
02142         $sortedNames = array_keys( $intervals );
02143         $smallestInterval = array_pop( $sortedNames );
02144 
02145         $segments = array();
02146 
02147         foreach ( $intervals as $name => $length ) {
02148             $value = floor( $seconds / $length );
02149 
02150             if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
02151                 $seconds -= $value * $length;
02152                 $segments[$name] = $value;
02153             }
02154         }
02155 
02156         return $segments;
02157     }
02158 
02178     private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
02179         $ts = wfTimestamp( TS_MW, $ts );
02180         $options += array( 'timecorrection' => true, 'format' => true );
02181         if ( $options['timecorrection'] !== false ) {
02182             if ( $options['timecorrection'] === true ) {
02183                 $offset = $user->getOption( 'timecorrection' );
02184             } else {
02185                 $offset = $options['timecorrection'];
02186             }
02187             $ts = $this->userAdjust( $ts, $offset );
02188         }
02189         if ( $options['format'] === true ) {
02190             $format = $user->getDatePreference();
02191         } else {
02192             $format = $options['format'];
02193         }
02194         $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
02195         return $this->sprintfDate( $df, $ts );
02196     }
02197 
02217     public function userDate( $ts, User $user, array $options = array() ) {
02218         return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
02219     }
02220 
02240     public function userTime( $ts, User $user, array $options = array() ) {
02241         return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
02242     }
02243 
02263     public function userTimeAndDate( $ts, User $user, array $options = array() ) {
02264         return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
02265     }
02266 
02282     public function getHumanTimestamp( MWTimestamp $ts, MWTimestamp $relativeTo, User $user ) {
02283         $diff = $ts->diff( $relativeTo );
02284         $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) - (int)$relativeTo->timestamp->format( 'w' ) );
02285         $days = $diff->days ?: (int)$diffDay;
02286         if ( $diff->invert || $days > 5 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' ) ) {
02287             // Timestamps are in different years: use full timestamp
02288             // Also do full timestamp for future dates
02292             $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
02293             $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
02294         } elseif ( $days > 5 ) {
02295             // Timestamps are in same year,  but more than 5 days ago: show day and month only.
02296             $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
02297             $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
02298         } elseif ( $days > 1 ) {
02299             // Timestamp within the past week: show the day of the week and time
02300             $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
02301             $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
02302             // Messages:
02303             // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
02304             $ts = wfMessage( "$weekday-at" )
02305                 ->inLanguage( $this )
02306                 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
02307                 ->text();
02308         } elseif ( $days == 1 ) {
02309             // Timestamp was yesterday: say 'yesterday' and the time.
02310             $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
02311             $ts = wfMessage( 'yesterday-at' )
02312                 ->inLanguage( $this )
02313                 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
02314                 ->text();
02315         } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
02316             // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
02317             $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
02318             $ts = wfMessage( 'today-at' )
02319                 ->inLanguage( $this )
02320                 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
02321                 ->text();
02322 
02323         // From here on in, the timestamp was soon enough ago so that we can simply say
02324         // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
02325         } elseif ( $diff->h == 1 ) {
02326             // Less than 90 minutes, but more than an hour ago.
02327             $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
02328         } elseif ( $diff->i >= 1 ) {
02329             // A few minutes ago.
02330             $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
02331         } elseif ( $diff->s >= 30 ) {
02332             // Less than a minute, but more than 30 sec ago.
02333             $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
02334         } else {
02335             // Less than 30 seconds ago.
02336             $ts = wfMessage( 'just-now' )->text();
02337         }
02338 
02339         return $ts;
02340     }
02341 
02346     function getMessage( $key ) {
02347         return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
02348     }
02349 
02353     function getAllMessages() {
02354         return self::$dataCache->getItem( $this->mCode, 'messages' );
02355     }
02356 
02363     function iconv( $in, $out, $string ) {
02364         # This is a wrapper for iconv in all languages except esperanto,
02365         # which does some nasty x-conversions beforehand
02366 
02367         # Even with //IGNORE iconv can whine about illegal characters in
02368         # *input* string. We just ignore those too.
02369         # REF: http://bugs.php.net/bug.php?id=37166
02370         # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
02371         wfSuppressWarnings();
02372         $text = iconv( $in, $out . '//IGNORE', $string );
02373         wfRestoreWarnings();
02374         return $text;
02375     }
02376 
02377     // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
02378 
02383     function ucwordbreaksCallbackAscii( $matches ) {
02384         return $this->ucfirst( $matches[1] );
02385     }
02386 
02391     function ucwordbreaksCallbackMB( $matches ) {
02392         return mb_strtoupper( $matches[0] );
02393     }
02394 
02399     function ucCallback( $matches ) {
02400         list( $wikiUpperChars ) = self::getCaseMaps();
02401         return strtr( $matches[1], $wikiUpperChars );
02402     }
02403 
02408     function lcCallback( $matches ) {
02409         list( , $wikiLowerChars ) = self::getCaseMaps();
02410         return strtr( $matches[1], $wikiLowerChars );
02411     }
02412 
02417     function ucwordsCallbackMB( $matches ) {
02418         return mb_strtoupper( $matches[0] );
02419     }
02420 
02425     function ucwordsCallbackWiki( $matches ) {
02426         list( $wikiUpperChars ) = self::getCaseMaps();
02427         return strtr( $matches[0], $wikiUpperChars );
02428     }
02429 
02437     function ucfirst( $str ) {
02438         $o = ord( $str );
02439         if ( $o < 96 ) { // if already uppercase...
02440             return $str;
02441         } elseif ( $o < 128 ) {
02442             return ucfirst( $str ); // use PHP's ucfirst()
02443         } else {
02444             // fall back to more complex logic in case of multibyte strings
02445             return $this->uc( $str, true );
02446         }
02447     }
02448 
02457     function uc( $str, $first = false ) {
02458         if ( function_exists( 'mb_strtoupper' ) ) {
02459             if ( $first ) {
02460                 if ( $this->isMultibyte( $str ) ) {
02461                     return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
02462                 } else {
02463                     return ucfirst( $str );
02464                 }
02465             } else {
02466                 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
02467             }
02468         } else {
02469             if ( $this->isMultibyte( $str ) ) {
02470                 $x = $first ? '^' : '';
02471                 return preg_replace_callback(
02472                     "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
02473                     array( $this, 'ucCallback' ),
02474                     $str
02475                 );
02476             } else {
02477                 return $first ? ucfirst( $str ) : strtoupper( $str );
02478             }
02479         }
02480     }
02481 
02486     function lcfirst( $str ) {
02487         $o = ord( $str );
02488         if ( !$o ) {
02489             return strval( $str );
02490         } elseif ( $o >= 128 ) {
02491             return $this->lc( $str, true );
02492         } elseif ( $o > 96 ) {
02493             return $str;
02494         } else {
02495             $str[0] = strtolower( $str[0] );
02496             return $str;
02497         }
02498     }
02499 
02505     function lc( $str, $first = false ) {
02506         if ( function_exists( 'mb_strtolower' ) ) {
02507             if ( $first ) {
02508                 if ( $this->isMultibyte( $str ) ) {
02509                     return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
02510                 } else {
02511                     return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
02512                 }
02513             } else {
02514                 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
02515             }
02516         } else {
02517             if ( $this->isMultibyte( $str ) ) {
02518                 $x = $first ? '^' : '';
02519                 return preg_replace_callback(
02520                     "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
02521                     array( $this, 'lcCallback' ),
02522                     $str
02523                 );
02524             } else {
02525                 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
02526             }
02527         }
02528     }
02529 
02534     function isMultibyte( $str ) {
02535         return (bool)preg_match( '/[\x80-\xff]/', $str );
02536     }
02537 
02542     function ucwords( $str ) {
02543         if ( $this->isMultibyte( $str ) ) {
02544             $str = $this->lc( $str );
02545 
02546             // regexp to find first letter in each word (i.e. after each space)
02547             $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
02548 
02549             // function to use to capitalize a single char
02550             if ( function_exists( 'mb_strtoupper' ) ) {
02551                 return preg_replace_callback(
02552                     $replaceRegexp,
02553                     array( $this, 'ucwordsCallbackMB' ),
02554                     $str
02555                 );
02556             } else {
02557                 return preg_replace_callback(
02558                     $replaceRegexp,
02559                     array( $this, 'ucwordsCallbackWiki' ),
02560                     $str
02561                 );
02562             }
02563         } else {
02564             return ucwords( strtolower( $str ) );
02565         }
02566     }
02567 
02574     function ucwordbreaks( $str ) {
02575         if ( $this->isMultibyte( $str ) ) {
02576             $str = $this->lc( $str );
02577 
02578             // since \b doesn't work for UTF-8, we explicitely define word break chars
02579             $breaks = "[ \-\(\)\}\{\.,\?!]";
02580 
02581             // find first letter after word break
02582             $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
02583 
02584             if ( function_exists( 'mb_strtoupper' ) ) {
02585                 return preg_replace_callback(
02586                     $replaceRegexp,
02587                     array( $this, 'ucwordbreaksCallbackMB' ),
02588                     $str
02589                 );
02590             } else {
02591                 return preg_replace_callback(
02592                     $replaceRegexp,
02593                     array( $this, 'ucwordsCallbackWiki' ),
02594                     $str
02595                 );
02596             }
02597         } else {
02598             return preg_replace_callback(
02599                 '/\b([\w\x80-\xff]+)\b/',
02600                 array( $this, 'ucwordbreaksCallbackAscii' ),
02601                 $str
02602             );
02603         }
02604     }
02605 
02621     function caseFold( $s ) {
02622         return $this->uc( $s );
02623     }
02624 
02629     function checkTitleEncoding( $s ) {
02630         if ( is_array( $s ) ) {
02631             throw new MWException( 'Given array to checkTitleEncoding.' );
02632         }
02633         if ( StringUtils::isUtf8( $s ) ) {
02634             return $s;
02635         }
02636 
02637         return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
02638     }
02639 
02643     function fallback8bitEncoding() {
02644         return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
02645     }
02646 
02655     function hasWordBreaks() {
02656         return true;
02657     }
02658 
02666     function segmentByWord( $string ) {
02667         return $string;
02668     }
02669 
02677     function normalizeForSearch( $string ) {
02678         return self::convertDoubleWidth( $string );
02679     }
02680 
02689     protected static function convertDoubleWidth( $string ) {
02690         static $full = null;
02691         static $half = null;
02692 
02693         if ( $full === null ) {
02694             $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
02695             $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
02696             $full = str_split( $fullWidth, 3 );
02697             $half = str_split( $halfWidth );
02698         }
02699 
02700         $string = str_replace( $full, $half, $string );
02701         return $string;
02702     }
02703 
02709     protected static function insertSpace( $string, $pattern ) {
02710         $string = preg_replace( $pattern, " $1 ", $string );
02711         $string = preg_replace( '/ +/', ' ', $string );
02712         return $string;
02713     }
02714 
02719     function convertForSearchResult( $termsArray ) {
02720         # some languages, e.g. Chinese, need to do a conversion
02721         # in order for search results to be displayed correctly
02722         return $termsArray;
02723     }
02724 
02731     function firstChar( $s ) {
02732         $matches = array();
02733         preg_match(
02734             '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
02735                 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
02736             $s,
02737             $matches
02738         );
02739 
02740         if ( isset( $matches[1] ) ) {
02741             if ( strlen( $matches[1] ) != 3 ) {
02742                 return $matches[1];
02743             }
02744 
02745             // Break down Hangul syllables to grab the first jamo
02746             $code = utf8ToCodepoint( $matches[1] );
02747             if ( $code < 0xac00 || 0xd7a4 <= $code ) {
02748                 return $matches[1];
02749             } elseif ( $code < 0xb098 ) {
02750                 return "\xe3\x84\xb1";
02751             } elseif ( $code < 0xb2e4 ) {
02752                 return "\xe3\x84\xb4";
02753             } elseif ( $code < 0xb77c ) {
02754                 return "\xe3\x84\xb7";
02755             } elseif ( $code < 0xb9c8 ) {
02756                 return "\xe3\x84\xb9";
02757             } elseif ( $code < 0xbc14 ) {
02758                 return "\xe3\x85\x81";
02759             } elseif ( $code < 0xc0ac ) {
02760                 return "\xe3\x85\x82";
02761             } elseif ( $code < 0xc544 ) {
02762                 return "\xe3\x85\x85";
02763             } elseif ( $code < 0xc790 ) {
02764                 return "\xe3\x85\x87";
02765             } elseif ( $code < 0xcc28 ) {
02766                 return "\xe3\x85\x88";
02767             } elseif ( $code < 0xce74 ) {
02768                 return "\xe3\x85\x8a";
02769             } elseif ( $code < 0xd0c0 ) {
02770                 return "\xe3\x85\x8b";
02771             } elseif ( $code < 0xd30c ) {
02772                 return "\xe3\x85\x8c";
02773             } elseif ( $code < 0xd558 ) {
02774                 return "\xe3\x85\x8d";
02775             } else {
02776                 return "\xe3\x85\x8e";
02777             }
02778         } else {
02779             return '';
02780         }
02781     }
02782 
02783     function initEncoding() {
02784         # Some languages may have an alternate char encoding option
02785         # (Esperanto X-coding, Japanese furigana conversion, etc)
02786         # If this language is used as the primary content language,
02787         # an override to the defaults can be set here on startup.
02788     }
02789 
02794     function recodeForEdit( $s ) {
02795         # For some languages we'll want to explicitly specify
02796         # which characters make it into the edit box raw
02797         # or are converted in some way or another.
02798         global $wgEditEncoding;
02799         if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
02800             return $s;
02801         } else {
02802             return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
02803         }
02804     }
02805 
02810     function recodeInput( $s ) {
02811         # Take the previous into account.
02812         global $wgEditEncoding;
02813         if ( $wgEditEncoding != '' ) {
02814             $enc = $wgEditEncoding;
02815         } else {
02816             $enc = 'UTF-8';
02817         }
02818         if ( $enc == 'UTF-8' ) {
02819             return $s;
02820         } else {
02821             return $this->iconv( $enc, 'UTF-8', $s );
02822         }
02823     }
02824 
02836     function normalize( $s ) {
02837         global $wgAllUnicodeFixes;
02838         $s = UtfNormal::cleanUp( $s );
02839         if ( $wgAllUnicodeFixes ) {
02840             $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
02841             $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
02842         }
02843 
02844         return $s;
02845     }
02846 
02861     function transformUsingPairFile( $file, $string ) {
02862         if ( !isset( $this->transformData[$file] ) ) {
02863             $data = wfGetPrecompiledData( $file );
02864             if ( $data === false ) {
02865                 throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
02866             }
02867             $this->transformData[$file] = new ReplacementArray( $data );
02868         }
02869         return $this->transformData[$file]->replace( $string );
02870     }
02871 
02877     function isRTL() {
02878         return self::$dataCache->getItem( $this->mCode, 'rtl' );
02879     }
02880 
02885     function getDir() {
02886         return $this->isRTL() ? 'rtl' : 'ltr';
02887     }
02888 
02897     function alignStart() {
02898         return $this->isRTL() ? 'right' : 'left';
02899     }
02900 
02909     function alignEnd() {
02910         return $this->isRTL() ? 'left' : 'right';
02911     }
02912 
02924     function getDirMarkEntity( $opposite = false ) {
02925         if ( $opposite ) {
02926             return $this->isRTL() ? '&lrm;' : '&rlm;';
02927         }
02928         return $this->isRTL() ? '&rlm;' : '&lrm;';
02929     }
02930 
02941     function getDirMark( $opposite = false ) {
02942         $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
02943         $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
02944         if ( $opposite ) {
02945             return $this->isRTL() ? $lrm : $rlm;
02946         }
02947         return $this->isRTL() ? $rlm : $lrm;
02948     }
02949 
02953     function capitalizeAllNouns() {
02954         return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
02955     }
02956 
02963     function getArrow( $direction = 'forwards' ) {
02964         switch ( $direction ) {
02965         case 'forwards':
02966             return $this->isRTL() ? '←' : '→';
02967         case 'backwards':
02968             return $this->isRTL() ? '→' : '←';
02969         case 'left':
02970             return '←';
02971         case 'right':
02972             return '→';
02973         case 'up':
02974             return '↑';
02975         case 'down':
02976             return '↓';
02977         }
02978     }
02979 
02985     function linkPrefixExtension() {
02986         return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
02987     }
02988 
02993     function getMagicWords() {
02994         return self::$dataCache->getItem( $this->mCode, 'magicWords' );
02995     }
02996 
03000     protected function doMagicHook() {
03001         if ( $this->mMagicHookDone ) {
03002             return;
03003         }
03004         $this->mMagicHookDone = true;
03005         wfProfileIn( 'LanguageGetMagic' );
03006         wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
03007         wfProfileOut( 'LanguageGetMagic' );
03008     }
03009 
03015     function getMagic( $mw ) {
03016         // Saves a function call
03017         if ( ! $this->mMagicHookDone ) {
03018             $this->doMagicHook();
03019         }
03020 
03021         if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
03022             $rawEntry = $this->mMagicExtensions[$mw->mId];
03023         } else {
03024             $rawEntry = self::$dataCache->getSubitem(
03025                 $this->mCode, 'magicWords', $mw->mId );
03026         }
03027 
03028         if ( !is_array( $rawEntry ) ) {
03029             error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
03030         } else {
03031             $mw->mCaseSensitive = $rawEntry[0];
03032             $mw->mSynonyms = array_slice( $rawEntry, 1 );
03033         }
03034     }
03035 
03041     function addMagicWordsByLang( $newWords ) {
03042         $fallbackChain = $this->getFallbackLanguages();
03043         $fallbackChain = array_reverse( $fallbackChain );
03044         foreach ( $fallbackChain as $code ) {
03045             if ( isset( $newWords[$code] ) ) {
03046                 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
03047             }
03048         }
03049     }
03050 
03055     function getSpecialPageAliases() {
03056         // Cache aliases because it may be slow to load them
03057         if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
03058             // Initialise array
03059             $this->mExtendedSpecialPageAliases =
03060                 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
03061             wfRunHooks( 'LanguageGetSpecialPageAliases',
03062                 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
03063         }
03064 
03065         return $this->mExtendedSpecialPageAliases;
03066     }
03067 
03074     function emphasize( $text ) {
03075         return "<em>$text</em>";
03076     }
03077 
03101     public function formatNum( $number, $nocommafy = false ) {
03102         global $wgTranslateNumerals;
03103         if ( !$nocommafy ) {
03104             $number = $this->commafy( $number );
03105             $s = $this->separatorTransformTable();
03106             if ( $s ) {
03107                 $number = strtr( $number, $s );
03108             }
03109         }
03110 
03111         if ( $wgTranslateNumerals ) {
03112             $s = $this->digitTransformTable();
03113             if ( $s ) {
03114                 $number = strtr( $number, $s );
03115             }
03116         }
03117 
03118         return $number;
03119     }
03120 
03129     public function formatNumNoSeparators( $number ) {
03130         return $this->formatNum( $number, true );
03131     }
03132 
03137     function parseFormattedNumber( $number ) {
03138         $s = $this->digitTransformTable();
03139         if ( $s ) {
03140             $number = strtr( $number, array_flip( $s ) );
03141         }
03142 
03143         $s = $this->separatorTransformTable();
03144         if ( $s ) {
03145             $number = strtr( $number, array_flip( $s ) );
03146         }
03147 
03148         $number = strtr( $number, array( ',' => '' ) );
03149         return $number;
03150     }
03151 
03158     function commafy( $number ) {
03159         $digitGroupingPattern = $this->digitGroupingPattern();
03160         if ( $number === null ) {
03161             return '';
03162         }
03163 
03164         if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
03165             // default grouping is at thousands,  use the same for ###,###,### pattern too.
03166             return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
03167         } else {
03168             // Ref: http://cldr.unicode.org/translation/number-patterns
03169             $sign = "";
03170             if ( intval( $number ) < 0 ) {
03171                 // For negative numbers apply the algorithm like positive number and add sign.
03172                 $sign = "-";
03173                 $number = substr( $number, 1 );
03174             }
03175             $integerPart = array();
03176             $decimalPart = array();
03177             $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
03178             preg_match( "/\d+/", $number, $integerPart );
03179             preg_match( "/\.\d*/", $number, $decimalPart );
03180             $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
03181             if ( $groupedNumber === $number ) {
03182                 // the string does not have any number part. Eg: .12345
03183                 return $sign . $groupedNumber;
03184             }
03185             $start = $end = strlen( $integerPart[0] );
03186             while ( $start > 0 ) {
03187                 $match = $matches[0][$numMatches - 1];
03188                 $matchLen = strlen( $match );
03189                 $start = $end - $matchLen;
03190                 if ( $start < 0 ) {
03191                     $start = 0;
03192                 }
03193                 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
03194                 $end = $start;
03195                 if ( $numMatches > 1 ) {
03196                     // use the last pattern for the rest of the number
03197                     $numMatches--;
03198                 }
03199                 if ( $start > 0 ) {
03200                     $groupedNumber = "," . $groupedNumber;
03201                 }
03202             }
03203             return $sign . $groupedNumber;
03204         }
03205     }
03206 
03210     function digitGroupingPattern() {
03211         return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
03212     }
03213 
03217     function digitTransformTable() {
03218         return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
03219     }
03220 
03224     function separatorTransformTable() {
03225         return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
03226     }
03227 
03237     function listToText( array $l ) {
03238         $m = count( $l ) - 1;
03239         if ( $m < 0 ) {
03240             return '';
03241         }
03242         if ( $m > 0 ) {
03243             $and = $this->getMessageFromDB( 'and' );
03244             $space = $this->getMessageFromDB( 'word-separator' );
03245             if ( $m > 1 ) {
03246                 $comma = $this->getMessageFromDB( 'comma-separator' );
03247             }
03248         }
03249         $s = $l[$m];
03250         for ( $i = $m - 1; $i >= 0; $i-- ) {
03251             if ( $i == $m - 1 ) {
03252                 $s = $l[$i] . $and . $space . $s;
03253             } else {
03254                 $s = $l[$i] . $comma . $s;
03255             }
03256         }
03257         return $s;
03258     }
03259 
03266     function commaList( array $list ) {
03267         return implode(
03268             wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
03269             $list
03270         );
03271     }
03272 
03279     function semicolonList( array $list ) {
03280         return implode(
03281             wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
03282             $list
03283         );
03284     }
03285 
03291     function pipeList( array $list ) {
03292         return implode(
03293             wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
03294             $list
03295         );
03296     }
03297 
03315     function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
03316         # Use the localized ellipsis character
03317         if ( $ellipsis == '...' ) {
03318             $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
03319         }
03320         # Check if there is no need to truncate
03321         if ( $length == 0 ) {
03322             return $ellipsis; // convention
03323         } elseif ( strlen( $string ) <= abs( $length ) ) {
03324             return $string; // no need to truncate
03325         }
03326         $stringOriginal = $string;
03327         # If ellipsis length is >= $length then we can't apply $adjustLength
03328         if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
03329             $string = $ellipsis; // this can be slightly unexpected
03330         # Otherwise, truncate and add ellipsis...
03331         } else {
03332             $eLength = $adjustLength ? strlen( $ellipsis ) : 0;
03333             if ( $length > 0 ) {
03334                 $length -= $eLength;
03335                 $string = substr( $string, 0, $length ); // xyz...
03336                 $string = $this->removeBadCharLast( $string );
03337                 $string = rtrim( $string );
03338                 $string = $string . $ellipsis;
03339             } else {
03340                 $length += $eLength;
03341                 $string = substr( $string, $length ); // ...xyz
03342                 $string = $this->removeBadCharFirst( $string );
03343                 $string = ltrim( $string );
03344                 $string = $ellipsis . $string;
03345             }
03346         }
03347         # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
03348         # This check is *not* redundant if $adjustLength, due to the single case where
03349         # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
03350         if ( strlen( $string ) < strlen( $stringOriginal ) ) {
03351             return $string;
03352         } else {
03353             return $stringOriginal;
03354         }
03355     }
03356 
03364     protected function removeBadCharLast( $string ) {
03365         if ( $string != '' ) {
03366             $char = ord( $string[strlen( $string ) - 1] );
03367             $m = array();
03368             if ( $char >= 0xc0 ) {
03369                 # We got the first byte only of a multibyte char; remove it.
03370                 $string = substr( $string, 0, -1 );
03371             } elseif ( $char >= 0x80 &&
03372                 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
03373                     '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m )
03374             ) {
03375                 # We chopped in the middle of a character; remove it
03376                 $string = $m[1];
03377             }
03378         }
03379         return $string;
03380     }
03381 
03389     protected function removeBadCharFirst( $string ) {
03390         if ( $string != '' ) {
03391             $char = ord( $string[0] );
03392             if ( $char >= 0x80 && $char < 0xc0 ) {
03393                 # We chopped in the middle of a character; remove the whole thing
03394                 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
03395             }
03396         }
03397         return $string;
03398     }
03399 
03415     function truncateHtml( $text, $length, $ellipsis = '...' ) {
03416         # Use the localized ellipsis character
03417         if ( $ellipsis == '...' ) {
03418             $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
03419         }
03420         # Check if there is clearly no need to truncate
03421         if ( $length <= 0 ) {
03422             return $ellipsis; // no text shown, nothing to format (convention)
03423         } elseif ( strlen( $text ) <= $length ) {
03424             return $text; // string short enough even *with* HTML (short-circuit)
03425         }
03426 
03427         $dispLen = 0; // innerHTML legth so far
03428         $testingEllipsis = false; // checking if ellipses will make string longer/equal?
03429         $tagType = 0; // 0-open, 1-close
03430         $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
03431         $entityState = 0; // 0-not entity, 1-entity
03432         $tag = $ret = ''; // accumulated tag name, accumulated result string
03433         $openTags = array(); // open tag stack
03434         $maybeState = null; // possible truncation state
03435 
03436         $textLen = strlen( $text );
03437         $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
03438         for ( $pos = 0; true; ++$pos ) {
03439             # Consider truncation once the display length has reached the maximim.
03440             # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
03441             # Check that we're not in the middle of a bracket/entity...
03442             if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
03443                 if ( !$testingEllipsis ) {
03444                     $testingEllipsis = true;
03445                     # Save where we are; we will truncate here unless there turn out to
03446                     # be so few remaining characters that truncation is not necessary.
03447                     if ( !$maybeState ) { // already saved? ($neLength = 0 case)
03448                         $maybeState = array( $ret, $openTags ); // save state
03449                     }
03450                 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
03451                     # String in fact does need truncation, the truncation point was OK.
03452                     list( $ret, $openTags ) = $maybeState; // reload state
03453                     $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
03454                     $ret .= $ellipsis; // add ellipsis
03455                     break;
03456                 }
03457             }
03458             if ( $pos >= $textLen ) {
03459                 break; // extra iteration just for above checks
03460             }
03461 
03462             # Read the next char...
03463             $ch = $text[$pos];
03464             $lastCh = $pos ? $text[$pos - 1] : '';
03465             $ret .= $ch; // add to result string
03466             if ( $ch == '<' ) {
03467                 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
03468                 $entityState = 0; // for bad HTML
03469                 $bracketState = 1; // tag started (checking for backslash)
03470             } elseif ( $ch == '>' ) {
03471                 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
03472                 $entityState = 0; // for bad HTML
03473                 $bracketState = 0; // out of brackets
03474             } elseif ( $bracketState == 1 ) {
03475                 if ( $ch == '/' ) {
03476                     $tagType = 1; // close tag (e.g. "</span>")
03477                 } else {
03478                     $tagType = 0; // open tag (e.g. "<span>")
03479                     $tag .= $ch;
03480                 }
03481                 $bracketState = 2; // building tag name
03482             } elseif ( $bracketState == 2 ) {
03483                 if ( $ch != ' ' ) {
03484                     $tag .= $ch;
03485                 } else {
03486                     // Name found (e.g. "<a href=..."), add on tag attributes...
03487                     $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
03488                 }
03489             } elseif ( $bracketState == 0 ) {
03490                 if ( $entityState ) {
03491                     if ( $ch == ';' ) {
03492                         $entityState = 0;
03493                         $dispLen++; // entity is one displayed char
03494                     }
03495                 } else {
03496                     if ( $neLength == 0 && !$maybeState ) {
03497                         // Save state without $ch. We want to *hit* the first
03498                         // display char (to get tags) but not *use* it if truncating.
03499                         $maybeState = array( substr( $ret, 0, -1 ), $openTags );
03500                     }
03501                     if ( $ch == '&' ) {
03502                         $entityState = 1; // entity found, (e.g. "&#160;")
03503                     } else {
03504                         $dispLen++; // this char is displayed
03505                         // Add the next $max display text chars after this in one swoop...
03506                         $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
03507                         $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
03508                         $dispLen += $skipped;
03509                         $pos += $skipped;
03510                     }
03511                 }
03512             }
03513         }
03514         // Close the last tag if left unclosed by bad HTML
03515         $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
03516         while ( count( $openTags ) > 0 ) {
03517             $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
03518         }
03519         return $ret;
03520     }
03521 
03533     private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
03534         if ( $len === null ) {
03535             $len = -1; // -1 means "no limit" for strcspn
03536         } elseif ( $len < 0 ) {
03537             $len = 0; // sanity
03538         }
03539         $skipCount = 0;
03540         if ( $start < strlen( $text ) ) {
03541             $skipCount = strcspn( $text, $search, $start, $len );
03542             $ret .= substr( $text, $start, $skipCount );
03543         }
03544         return $skipCount;
03545     }
03546 
03556     private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
03557         $tag = ltrim( $tag );
03558         if ( $tag != '' ) {
03559             if ( $tagType == 0 && $lastCh != '/' ) {
03560                 $openTags[] = $tag; // tag opened (didn't close itself)
03561             } elseif ( $tagType == 1 ) {
03562                 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
03563                     array_pop( $openTags ); // tag closed
03564                 }
03565             }
03566             $tag = '';
03567         }
03568     }
03569 
03578     function convertGrammar( $word, $case ) {
03579         global $wgGrammarForms;
03580         if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
03581             return $wgGrammarForms[$this->getCode()][$case][$word];
03582         }
03583         return $word;
03584     }
03590     function getGrammarForms() {
03591         global $wgGrammarForms;
03592         if ( isset( $wgGrammarForms[$this->getCode()] ) && is_array( $wgGrammarForms[$this->getCode()] ) ) {
03593             return $wgGrammarForms[$this->getCode()];
03594         }
03595         return array();
03596     }
03616     function gender( $gender, $forms ) {
03617         if ( !count( $forms ) ) {
03618             return '';
03619         }
03620         $forms = $this->preConvertPlural( $forms, 2 );
03621         if ( $gender === 'male' ) {
03622             return $forms[0];
03623         }
03624         if ( $gender === 'female' ) {
03625             return $forms[1];
03626         }
03627         return isset( $forms[2] ) ? $forms[2] : $forms[0];
03628     }
03629 
03645     function convertPlural( $count, $forms ) {
03646         // Handle explicit n=pluralform cases
03647         $forms = $this->handleExplicitPluralForms( $count, $forms );
03648         if ( is_string( $forms ) ) {
03649             return $forms;
03650         }
03651         if ( !count( $forms ) ) {
03652             return '';
03653         }
03654 
03655         $pluralForm = $this->getPluralRuleIndexNumber( $count );
03656         $pluralForm = min( $pluralForm, count( $forms ) - 1 );
03657         return $forms[$pluralForm];
03658     }
03659 
03675     protected function handleExplicitPluralForms( $count, array $forms ) {
03676         foreach ( $forms as $index => $form ) {
03677             if ( preg_match( '/\d+=/i', $form ) ) {
03678                 $pos = strpos( $form, '=' );
03679                 if ( substr( $form, 0, $pos ) === (string) $count ) {
03680                     return substr( $form, $pos + 1 );
03681                 }
03682                 unset( $forms[$index] );
03683             }
03684         }
03685         return array_values( $forms );
03686     }
03687 
03696     protected function preConvertPlural( /* Array */ $forms, $count ) {
03697         while ( count( $forms ) < $count ) {
03698             $forms[] = $forms[count( $forms ) - 1];
03699         }
03700         return $forms;
03701     }
03702 
03714     function translateBlockExpiry( $str ) {
03715         $duration = SpecialBlock::getSuggestedDurations( $this );
03716         foreach ( $duration as $show => $value ) {
03717             if ( strcmp( $str, $value ) == 0 ) {
03718                 return htmlspecialchars( trim( $show ) );
03719             }
03720         }
03721 
03722         // Since usually only infinite or indefinite is only on list, so try
03723         // equivalents if still here.
03724         $indefs = array( 'infinite', 'infinity', 'indefinite' );
03725         if ( in_array( $str, $indefs ) ) {
03726             foreach ( $indefs as $val ) {
03727                 $show = array_search( $val, $duration, true );
03728                 if ( $show !== false ) {
03729                     return htmlspecialchars( trim( $show ) );
03730                 }
03731             }
03732         }
03733 
03734         // If all else fails, return a standard duration or timestamp description.
03735         $time = strtotime( $str, 0 );
03736         if ( $time === false ) { // Unknown format. Return it as-is in case.
03737             return $str;
03738         } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
03739             // $time is relative to 0 so it's a duration length.
03740             return $this->formatDuration( $time );
03741         } else { // It's an absolute timestamp.
03742             if ( $time === 0 ) {
03743                 // wfTimestamp() handles 0 as current time instead of epoch.
03744                 return $this->timeanddate( '19700101000000' );
03745             } else {
03746                 return $this->timeanddate( $time );
03747             }
03748         }
03749     }
03750 
03758     public function segmentForDiff( $text ) {
03759         return $text;
03760     }
03761 
03768     public function unsegmentForDiff( $text ) {
03769         return $text;
03770     }
03771 
03778     public function getConverter() {
03779         return $this->mConverter;
03780     }
03781 
03788     public function autoConvertToAllVariants( $text ) {
03789         return $this->mConverter->autoConvertToAllVariants( $text );
03790     }
03791 
03798     public function convert( $text ) {
03799         return $this->mConverter->convert( $text );
03800     }
03801 
03808     public function convertTitle( $title ) {
03809         return $this->mConverter->convertTitle( $title );
03810     }
03811 
03818     public function convertNamespace( $ns ) {
03819         return $this->mConverter->convertNamespace( $ns );
03820     }
03821 
03827     public function hasVariants() {
03828         return count( $this->getVariants() ) > 1;
03829     }
03830 
03838     public function hasVariant( $variant ) {
03839         return (bool)$this->mConverter->validateVariant( $variant );
03840     }
03841 
03849     public function armourMath( $text ) {
03850         return $this->mConverter->armourMath( $text );
03851     }
03852 
03860     public function convertHtml( $text, $isTitle = false ) {
03861         return htmlspecialchars( $this->convert( $text, $isTitle ) );
03862     }
03863 
03868     public function convertCategoryKey( $key ) {
03869         return $this->mConverter->convertCategoryKey( $key );
03870     }
03871 
03878     public function getVariants() {
03879         return $this->mConverter->getVariants();
03880     }
03881 
03885     public function getPreferredVariant() {
03886         return $this->mConverter->getPreferredVariant();
03887     }
03888 
03892     public function getDefaultVariant() {
03893         return $this->mConverter->getDefaultVariant();
03894     }
03895 
03899     public function getURLVariant() {
03900         return $this->mConverter->getURLVariant();
03901     }
03902 
03915     public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
03916         $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
03917     }
03918 
03925     function getExtraHashOptions() {
03926         return $this->mConverter->getExtraHashOptions();
03927     }
03928 
03936     public function getParsedTitle() {
03937         return $this->mConverter->getParsedTitle();
03938     }
03939 
03952     public function markNoConversion( $text, $noParse = false ) {
03953         // Excluding protocal-relative URLs may avoid many false positives.
03954         if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
03955             return $this->mConverter->markNoConversion( $text );
03956         } else {
03957             return $text;
03958         }
03959     }
03960 
03967     public function linkTrail() {
03968         return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
03969     }
03970 
03977     public function linkPrefixCharset() {
03978         return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
03979     }
03980 
03984     function getLangObj() {
03985         return $this;
03986     }
03987 
03995     public function getParentLanguage() {
03996         if ( $this->mParentLanguage !== false ) {
03997             return $this->mParentLanguage;
03998         }
03999 
04000         $pieces = explode( '-', $this->getCode() );
04001         $code = $pieces[0];
04002         if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
04003             $this->mParentLanguage = null;
04004             return null;
04005         }
04006         $lang = Language::factory( $code );
04007         if ( !$lang->hasVariant( $this->getCode() ) ) {
04008             $this->mParentLanguage = null;
04009             return null;
04010         }
04011 
04012         $this->mParentLanguage = $lang;
04013         return $lang;
04014     }
04015 
04024     public function getCode() {
04025         return $this->mCode;
04026     }
04027 
04038     public function getHtmlCode() {
04039         if ( is_null( $this->mHtmlCode ) ) {
04040             $this->mHtmlCode = wfBCP47( $this->getCode() );
04041         }
04042         return $this->mHtmlCode;
04043     }
04044 
04048     public function setCode( $code ) {
04049         $this->mCode = $code;
04050         // Ensure we don't leave incorrect cached data lying around
04051         $this->mHtmlCode = null;
04052         $this->mParentLanguage = false;
04053     }
04054 
04063     public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
04064         if ( !self::isValidBuiltInCode( $code ) ) {
04065             throw new MWException( "Invalid language code \"$code\"" );
04066         }
04067 
04068         return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
04069     }
04070 
04078     public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
04079         $m = null;
04080         preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
04081             preg_quote( $suffix, '/' ) . '/', $filename, $m );
04082         if ( !count( $m ) ) {
04083             return false;
04084         }
04085         return str_replace( '_', '-', strtolower( $m[1] ) );
04086     }
04087 
04092     public static function getMessagesFileName( $code ) {
04093         global $IP;
04094         $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
04095         wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
04096         return $file;
04097     }
04098 
04104     public static function getJsonMessagesFileName( $code ) {
04105         global $IP;
04106 
04107         if ( !self::isValidBuiltInCode( $code ) ) {
04108             throw new MWException( "Invalid language code \"$code\"" );
04109         }
04110 
04111         return "$IP/languages/i18n/$code.json" ;
04112     }
04113 
04118     public static function getClassFileName( $code ) {
04119         global $IP;
04120         return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
04121     }
04122 
04130     public static function getFallbackFor( $code ) {
04131         if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
04132             return false;
04133         } else {
04134             $fallbacks = self::getFallbacksFor( $code );
04135             $first = array_shift( $fallbacks );
04136             return $first;
04137         }
04138     }
04139 
04147     public static function getFallbacksFor( $code ) {
04148         if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
04149             return array();
04150         } else {
04151             $v = self::getLocalisationCache()->getItem( $code, 'fallback' );
04152             $v = array_map( 'trim', explode( ',', $v ) );
04153             if ( $v[count( $v ) - 1] !== 'en' ) {
04154                 $v[] = 'en';
04155             }
04156             return $v;
04157         }
04158     }
04159 
04168     public static function getFallbacksIncludingSiteLanguage( $code ) {
04169         global $wgLanguageCode;
04170 
04171         // Usually, we will only store a tiny number of fallback chains, so we
04172         // keep them in static memory.
04173         $cacheKey = "{$code}-{$wgLanguageCode}";
04174 
04175         if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
04176             $fallbacks = self::getFallbacksFor( $code );
04177 
04178             // Append the site's fallback chain, including the site language itself
04179             $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
04180             array_unshift( $siteFallbacks, $wgLanguageCode );
04181 
04182             // Eliminate any languages already included in the chain
04183             $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
04184 
04185             self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
04186         }
04187         return self::$fallbackLanguageCache[$cacheKey];
04188     }
04189 
04199     public static function getMessagesFor( $code ) {
04200         return self::getLocalisationCache()->getItem( $code, 'messages' );
04201     }
04202 
04211     public static function getMessageFor( $key, $code ) {
04212         return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
04213     }
04214 
04223     public static function getMessageKeysFor( $code ) {
04224         return self::getLocalisationCache()->getSubItemList( $code, 'messages' );
04225     }
04226 
04231     function fixVariableInNamespace( $talk ) {
04232         if ( strpos( $talk, '$1' ) === false ) {
04233             return $talk;
04234         }
04235 
04236         global $wgMetaNamespace;
04237         $talk = str_replace( '$1', $wgMetaNamespace, $talk );
04238 
04239         # Allow grammar transformations
04240         # Allowing full message-style parsing would make simple requests
04241         # such as action=raw much more expensive than they need to be.
04242         # This will hopefully cover most cases.
04243         $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
04244             array( &$this, 'replaceGrammarInNamespace' ), $talk );
04245         return str_replace( ' ', '_', $talk );
04246     }
04247 
04252     function replaceGrammarInNamespace( $m ) {
04253         return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
04254     }
04255 
04260     static function getCaseMaps() {
04261         static $wikiUpperChars, $wikiLowerChars;
04262         if ( isset( $wikiUpperChars ) ) {
04263             return array( $wikiUpperChars, $wikiLowerChars );
04264         }
04265 
04266         wfProfileIn( __METHOD__ );
04267         $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
04268         if ( $arr === false ) {
04269             throw new MWException(
04270                 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
04271         }
04272         $wikiUpperChars = $arr['wikiUpperChars'];
04273         $wikiLowerChars = $arr['wikiLowerChars'];
04274         wfProfileOut( __METHOD__ );
04275         return array( $wikiUpperChars, $wikiLowerChars );
04276     }
04277 
04289     public function formatExpiry( $expiry, $format = true ) {
04290         static $infinity;
04291         if ( $infinity === null ) {
04292             $infinity = wfGetDB( DB_SLAVE )->getInfinity();
04293         }
04294 
04295         if ( $expiry == '' || $expiry == $infinity ) {
04296             return $format === true
04297                 ? $this->getMessageFromDB( 'infiniteblock' )
04298                 : $infinity;
04299         } else {
04300             return $format === true
04301                 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
04302                 : wfTimestamp( $format, $expiry );
04303         }
04304     }
04305 
04316     function formatTimePeriod( $seconds, $format = array() ) {
04317         if ( !is_array( $format ) ) {
04318             $format = array( 'avoid' => $format ); // For backwards compatibility
04319         }
04320         if ( !isset( $format['avoid'] ) ) {
04321             $format['avoid'] = false;
04322         }
04323         if ( !isset( $format['noabbrevs' ] ) ) {
04324             $format['noabbrevs'] = false;
04325         }
04326         $secondsMsg = wfMessage(
04327             $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
04328         $minutesMsg = wfMessage(
04329             $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
04330         $hoursMsg = wfMessage(
04331             $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
04332         $daysMsg = wfMessage(
04333             $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
04334 
04335         if ( round( $seconds * 10 ) < 100 ) {
04336             $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
04337             $s = $secondsMsg->params( $s )->text();
04338         } elseif ( round( $seconds ) < 60 ) {
04339             $s = $this->formatNum( round( $seconds ) );
04340             $s = $secondsMsg->params( $s )->text();
04341         } elseif ( round( $seconds ) < 3600 ) {
04342             $minutes = floor( $seconds / 60 );
04343             $secondsPart = round( fmod( $seconds, 60 ) );
04344             if ( $secondsPart == 60 ) {
04345                 $secondsPart = 0;
04346                 $minutes++;
04347             }
04348             $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
04349             $s .= ' ';
04350             $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
04351         } elseif ( round( $seconds ) <= 2 * 86400 ) {
04352             $hours = floor( $seconds / 3600 );
04353             $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
04354             $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
04355             if ( $secondsPart == 60 ) {
04356                 $secondsPart = 0;
04357                 $minutes++;
04358             }
04359             if ( $minutes == 60 ) {
04360                 $minutes = 0;
04361                 $hours++;
04362             }
04363             $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
04364             $s .= ' ';
04365             $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
04366             if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
04367                 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
04368             }
04369         } else {
04370             $days = floor( $seconds / 86400 );
04371             if ( $format['avoid'] === 'avoidminutes' ) {
04372                 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
04373                 if ( $hours == 24 ) {
04374                     $hours = 0;
04375                     $days++;
04376                 }
04377                 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
04378                 $s .= ' ';
04379                 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
04380             } elseif ( $format['avoid'] === 'avoidseconds' ) {
04381                 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
04382                 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
04383                 if ( $minutes == 60 ) {
04384                     $minutes = 0;
04385                     $hours++;
04386                 }
04387                 if ( $hours == 24 ) {
04388                     $hours = 0;
04389                     $days++;
04390                 }
04391                 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
04392                 $s .= ' ';
04393                 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
04394                 $s .= ' ';
04395                 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
04396             } else {
04397                 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
04398                 $s .= ' ';
04399                 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
04400             }
04401         }
04402         return $s;
04403     }
04404 
04415     function formatBitrate( $bps ) {
04416         return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
04417     }
04418 
04425     function formatComputingNumbers( $size, $boundary, $messageKey ) {
04426         if ( $size <= 0 ) {
04427             return str_replace( '$1', $this->formatNum( $size ),
04428                 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
04429             );
04430         }
04431         $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
04432         $index = 0;
04433 
04434         $maxIndex = count( $sizes ) - 1;
04435         while ( $size >= $boundary && $index < $maxIndex ) {
04436             $index++;
04437             $size /= $boundary;
04438         }
04439 
04440         // For small sizes no decimal places necessary
04441         $round = 0;
04442         if ( $index > 1 ) {
04443             // For MB and bigger two decimal places are smarter
04444             $round = 2;
04445         }
04446         $msg = str_replace( '$1', $sizes[$index], $messageKey );
04447 
04448         $size = round( $size, $round );
04449         $text = $this->getMessageFromDB( $msg );
04450         return str_replace( '$1', $this->formatNum( $size ), $text );
04451     }
04452 
04463     function formatSize( $size ) {
04464         return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
04465     }
04466 
04476     function specialList( $page, $details, $oppositedm = true ) {
04477         $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) .
04478             $this->getDirMark();
04479         $details = $details ? $dirmark . $this->getMessageFromDB( 'word-separator' ) .
04480             wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : '';
04481         return $page . $details;
04482     }
04483 
04494     public function viewPrevNext( Title $title, $offset, $limit, array $query = array(), $atend = false ) {
04495         // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
04496 
04497         # Make 'previous' link
04498         $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
04499         if ( $offset > 0 ) {
04500             $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
04501                 $query, $prev, 'prevn-title', 'mw-prevlink' );
04502         } else {
04503             $plink = htmlspecialchars( $prev );
04504         }
04505 
04506         # Make 'next' link
04507         $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
04508         if ( $atend ) {
04509             $nlink = htmlspecialchars( $next );
04510         } else {
04511             $nlink = $this->numLink( $title, $offset + $limit, $limit,
04512                 $query, $next, 'nextn-title', 'mw-nextlink' );
04513         }
04514 
04515         # Make links to set number of items per page
04516         $numLinks = array();
04517         foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
04518             $numLinks[] = $this->numLink( $title, $offset, $num,
04519                 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
04520         }
04521 
04522         return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
04523             )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
04524     }
04525 
04538     private function numLink( Title $title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) {
04539         $query = array( 'limit' => $limit, 'offset' => $offset ) + $query;
04540         $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
04541         return Html::element( 'a', array( 'href' => $title->getLocalURL( $query ),
04542             'title' => $tooltip, 'class' => $class ), $link );
04543     }
04544 
04550     public function getConvRuleTitle() {
04551         return $this->mConverter->getConvRuleTitle();
04552     }
04553 
04559     public function getCompiledPluralRules() {
04560         $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
04561         $fallbacks = Language::getFallbacksFor( $this->mCode );
04562         if ( !$pluralRules ) {
04563             foreach ( $fallbacks as $fallbackCode ) {
04564                 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
04565                 if ( $pluralRules ) {
04566                     break;
04567                 }
04568             }
04569         }
04570         return $pluralRules;
04571     }
04572 
04578     public function getPluralRules() {
04579         $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
04580         $fallbacks = Language::getFallbacksFor( $this->mCode );
04581         if ( !$pluralRules ) {
04582             foreach ( $fallbacks as $fallbackCode ) {
04583                 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
04584                 if ( $pluralRules ) {
04585                     break;
04586                 }
04587             }
04588         }
04589         return $pluralRules;
04590     }
04591 
04597     public function getPluralRuleTypes() {
04598         $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
04599         $fallbacks = Language::getFallbacksFor( $this->mCode );
04600         if ( !$pluralRuleTypes ) {
04601             foreach ( $fallbacks as $fallbackCode ) {
04602                 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
04603                 if ( $pluralRuleTypes ) {
04604                     break;
04605                 }
04606             }
04607         }
04608         return $pluralRuleTypes;
04609     }
04610 
04615     public function getPluralRuleIndexNumber( $number ) {
04616         $pluralRules = $this->getCompiledPluralRules();
04617         $form = CLDRPluralRuleEvaluator::evaluateCompiled( $number, $pluralRules );
04618         return $form;
04619     }
04620 
04628     public function getPluralRuleType( $number ) {
04629         $index = $this->getPluralRuleIndexNumber( $number );
04630         $pluralRuleTypes = $this->getPluralRuleTypes();
04631         if ( isset( $pluralRuleTypes[$index] ) ) {
04632             return $pluralRuleTypes[$index];
04633         } else {
04634             return 'other';
04635         }
04636     }
04637 }