3 * Internationalisation code.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
25 * @defgroup Language Language
28 if ( !defined( 'MEDIAWIKI' ) ) {
29 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
34 global $wgLanguageNames;
35 require_once( __DIR__
. '/Names.php' );
37 if ( function_exists( 'mb_strtoupper' ) ) {
38 mb_internal_encoding( 'UTF-8' );
42 * a fake language converter
52 function __construct( $langobj ) { $this->mLang
= $langobj; }
53 function autoConvertToAllVariants( $text ) { return array( $this->mLang
->getCode() => $text ); }
54 function convert( $t ) { return $t; }
55 function convertTo( $text, $variant ) { return $text; }
56 function convertTitle( $t ) { return $t->getPrefixedText(); }
57 function getVariants() { return array( $this->mLang
->getCode() ); }
58 function getPreferredVariant() { return $this->mLang
->getCode(); }
59 function getDefaultVariant() { return $this->mLang
->getCode(); }
60 function getURLVariant() { return ''; }
61 function getConvRuleTitle() { return false; }
62 function findVariantLink( &$l, &$n, $ignoreOtherCond = false ) { }
63 function getExtraHashOptions() { return ''; }
64 function getParsedTitle() { return ''; }
65 function markNoConversion( $text, $noParse = false ) { return $text; }
66 function convertCategoryKey( $key ) { return $key; }
67 function convertLinkToAllVariants( $text ) { return $this->autoConvertToAllVariants( $text ); }
68 function armourMath( $text ) { return $text; }
72 * Internationalisation code
78 * @var LanguageConverter
82 public $mVariants, $mCode, $mLoaded = false;
83 public $mMagicExtensions = array(), $mMagicHookDone = false;
84 private $mHtmlCode = null;
86 public $dateFormatStrings = array();
87 public $mExtendedSpecialPageAliases;
89 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
92 * ReplacementArray object caches
94 public $transformData = array();
97 * @var LocalisationCache
99 static public $dataCache;
101 static public $mLangObjCache = array();
103 static public $mWeekdayMsgs = array(
104 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
108 static public $mWeekdayAbbrevMsgs = array(
109 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
112 static public $mMonthMsgs = array(
113 'january', 'february', 'march', 'april', 'may_long', 'june',
114 'july', 'august', 'september', 'october', 'november',
117 static public $mMonthGenMsgs = array(
118 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
119 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
122 static public $mMonthAbbrevMsgs = array(
123 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
124 'sep', 'oct', 'nov', 'dec'
127 static public $mIranianCalendarMonthMsgs = array(
128 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
129 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
130 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
131 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
134 static public $mHebrewCalendarMonthMsgs = array(
135 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
136 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
137 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
138 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
139 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
142 static public $mHebrewCalendarMonthGenMsgs = array(
143 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
144 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
145 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
146 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
147 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
150 static public $mHijriCalendarMonthMsgs = array(
151 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
152 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
153 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
154 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
157 // For pretty timestamps
158 // Cutoff for specifying "weekday at XX:XX" format
159 protected $mWeekdayAtCutoff = 432000; // 5 days
165 static public $durationIntervals = array(
166 'millennia' => 31557600000,
167 'centuries' => 3155760000,
168 'decades' => 315576000,
169 'years' => 31557600, // 86400 * 365.25
178 * Get a cached language object for a given language code
179 * @param $code String
182 static function factory( $code ) {
183 if ( !isset( self
::$mLangObjCache[$code] ) ) {
184 if ( count( self
::$mLangObjCache ) > 10 ) {
185 // Don't keep a billion objects around, that's stupid.
186 self
::$mLangObjCache = array();
188 self
::$mLangObjCache[$code] = self
::newFromCode( $code );
190 return self
::$mLangObjCache[$code];
194 * Create a language object for a given language code
195 * @param $code String
196 * @throws MWException
199 protected static function newFromCode( $code ) {
200 // Protect against path traversal below
201 if ( !Language
::isValidCode( $code )
202 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
204 throw new MWException( "Invalid language code \"$code\"" );
207 if ( !Language
::isValidBuiltInCode( $code ) ) {
208 // It's not possible to customise this code with class files, so
209 // just return a Language object. This is to support uselang= hacks.
210 $lang = new Language
;
211 $lang->setCode( $code );
215 // Check if there is a language class for the code
216 $class = self
::classFromCode( $code );
217 self
::preloadLanguageClass( $class );
218 if ( MWInit
::classExists( $class ) ) {
223 // Keep trying the fallback list until we find an existing class
224 $fallbacks = Language
::getFallbacksFor( $code );
225 foreach ( $fallbacks as $fallbackCode ) {
226 if ( !Language
::isValidBuiltInCode( $fallbackCode ) ) {
227 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
230 $class = self
::classFromCode( $fallbackCode );
231 self
::preloadLanguageClass( $class );
232 if ( MWInit
::classExists( $class ) ) {
233 $lang = Language
::newFromCode( $fallbackCode );
234 $lang->setCode( $code );
239 throw new MWException( "Invalid fallback sequence for language '$code'" );
243 * Returns true if a language code string is of a valid form, whether or
244 * not it exists. This includes codes which are used solely for
245 * customisation via the MediaWiki namespace.
247 * @param $code string
251 public static function isValidCode( $code ) {
253 // People think language codes are html safe, so enforce it.
254 // Ideally we should only allow a-zA-Z0-9-
255 // but, .+ and other chars are often used for {{int:}} hacks
256 // see bugs 37564, 37587, 36938
257 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
258 && !preg_match( Title
::getTitleInvalidRegex(), $code );
262 * Returns true if a language code is of a valid form for the purposes of
263 * internal customisation of MediaWiki, via Messages*.php.
265 * @param $code string
267 * @throws MWException
271 public static function isValidBuiltInCode( $code ) {
273 if ( !is_string( $code ) ) {
274 $type = gettype( $code );
275 if ( $type === 'object' ) {
276 $addmsg = " of class " . get_class( $code );
280 throw new MWException( __METHOD__
. " must be passed a string, $type given$addmsg" );
283 return preg_match( '/^[a-z0-9-]+$/i', $code );
288 * @return String Name of the language class
290 public static function classFromCode( $code ) {
291 if ( $code == 'en' ) {
294 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
299 * Includes language class files
301 * @param $class string Name of the language class
303 public static function preloadLanguageClass( $class ) {
306 if ( $class === 'Language' ) {
310 if ( !defined( 'MW_COMPILED' ) ) {
311 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
312 include_once( "$IP/languages/classes/$class.php" );
318 * Get the LocalisationCache instance
320 * @return LocalisationCache
322 public static function getLocalisationCache() {
323 if ( is_null( self
::$dataCache ) ) {
324 global $wgLocalisationCacheConf;
325 $class = $wgLocalisationCacheConf['class'];
326 self
::$dataCache = new $class( $wgLocalisationCacheConf );
328 return self
::$dataCache;
331 function __construct() {
332 $this->mConverter
= new FakeConverter( $this );
333 // Set the code to the name of the descendant
334 if ( get_class( $this ) == 'Language' ) {
337 $this->mCode
= str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
339 self
::getLocalisationCache();
343 * Reduce memory usage
345 function __destruct() {
346 foreach ( $this as $name => $value ) {
347 unset( $this->$name );
352 * Hook which will be called if this is the content language.
353 * Descendants can use this to register hook functions or modify globals
355 function initContLang() { }
358 * Same as getFallbacksFor for current language.
360 * @deprecated in 1.19
362 function getFallbackLanguageCode() {
363 wfDeprecated( __METHOD__
);
364 return self
::getFallbackFor( $this->mCode
);
371 function getFallbackLanguages() {
372 return self
::getFallbacksFor( $this->mCode
);
376 * Exports $wgBookstoreListEn
379 function getBookstoreList() {
380 return self
::$dataCache->getItem( $this->mCode
, 'bookstoreList' );
386 public function getNamespaces() {
387 if ( is_null( $this->namespaceNames
) ) {
388 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
390 $this->namespaceNames
= self
::$dataCache->getItem( $this->mCode
, 'namespaceNames' );
391 $validNamespaces = MWNamespace
::getCanonicalNamespaces();
393 $this->namespaceNames
= $wgExtraNamespaces +
$this->namespaceNames +
$validNamespaces;
395 $this->namespaceNames
[NS_PROJECT
] = $wgMetaNamespace;
396 if ( $wgMetaNamespaceTalk ) {
397 $this->namespaceNames
[NS_PROJECT_TALK
] = $wgMetaNamespaceTalk;
399 $talk = $this->namespaceNames
[NS_PROJECT_TALK
];
400 $this->namespaceNames
[NS_PROJECT_TALK
] =
401 $this->fixVariableInNamespace( $talk );
404 # Sometimes a language will be localised but not actually exist on this wiki.
405 foreach ( $this->namespaceNames
as $key => $text ) {
406 if ( !isset( $validNamespaces[$key] ) ) {
407 unset( $this->namespaceNames
[$key] );
411 # The above mixing may leave namespaces out of canonical order.
412 # Re-order by namespace ID number...
413 ksort( $this->namespaceNames
);
415 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames
) );
417 return $this->namespaceNames
;
421 * Arbitrarily set all of the namespace names at once. Mainly used for testing
422 * @param $namespaces Array of namespaces (id => name)
424 public function setNamespaces( array $namespaces ) {
425 $this->namespaceNames
= $namespaces;
429 * A convenience function that returns the same thing as
430 * getNamespaces() except with the array values changed to ' '
431 * where it found '_', useful for producing output to be displayed
432 * e.g. in <select> forms.
436 function getFormattedNamespaces() {
437 $ns = $this->getNamespaces();
438 foreach ( $ns as $k => $v ) {
439 $ns[$k] = strtr( $v, '_', ' ' );
445 * Get a namespace value by key
447 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
448 * echo $mw_ns; // prints 'MediaWiki'
451 * @param $index Int: the array key of the namespace to return
452 * @return mixed, string if the namespace value exists, otherwise false
454 function getNsText( $index ) {
455 $ns = $this->getNamespaces();
456 return isset( $ns[$index] ) ?
$ns[$index] : false;
460 * A convenience function that returns the same thing as
461 * getNsText() except with '_' changed to ' ', useful for
464 * @param $index string
468 function getFormattedNsText( $index ) {
469 $ns = $this->getNsText( $index );
470 return strtr( $ns, '_', ' ' );
474 * Returns gender-dependent namespace alias if available.
475 * @param $index Int: namespace index
476 * @param $gender String: gender key (male, female... )
480 function getGenderNsText( $index, $gender ) {
481 global $wgExtraGenderNamespaces;
483 $ns = $wgExtraGenderNamespaces + self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
484 return isset( $ns[$index][$gender] ) ?
$ns[$index][$gender] : $this->getNsText( $index );
488 * Whether this language makes distinguishes genders for example in
493 function needsGenderDistinction() {
494 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
495 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
496 // $wgExtraGenderNamespaces overrides everything
498 } elseif ( isset( $wgExtraNamespaces[NS_USER
] ) && isset( $wgExtraNamespaces[NS_USER_TALK
] ) ) {
499 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
500 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
503 // Check what is in i18n files
504 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
505 return count( $aliases ) > 0;
510 * Get a namespace key by value, case insensitive.
511 * Only matches namespace names for the current language, not the
512 * canonical ones defined in Namespace.php.
514 * @param $text String
515 * @return mixed An integer if $text is a valid value otherwise false
517 function getLocalNsIndex( $text ) {
518 $lctext = $this->lc( $text );
519 $ids = $this->getNamespaceIds();
520 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
526 function getNamespaceAliases() {
527 if ( is_null( $this->namespaceAliases
) ) {
528 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceAliases' );
532 foreach ( $aliases as $name => $index ) {
533 if ( $index === NS_PROJECT_TALK
) {
534 unset( $aliases[$name] );
535 $name = $this->fixVariableInNamespace( $name );
536 $aliases[$name] = $index;
541 global $wgExtraGenderNamespaces;
542 $genders = $wgExtraGenderNamespaces +
(array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
543 foreach ( $genders as $index => $forms ) {
544 foreach ( $forms as $alias ) {
545 $aliases[$alias] = $index;
549 $this->namespaceAliases
= $aliases;
551 return $this->namespaceAliases
;
557 function getNamespaceIds() {
558 if ( is_null( $this->mNamespaceIds
) ) {
559 global $wgNamespaceAliases;
560 # Put namespace names and aliases into a hashtable.
561 # If this is too slow, then we should arrange it so that it is done
562 # before caching. The catch is that at pre-cache time, the above
563 # class-specific fixup hasn't been done.
564 $this->mNamespaceIds
= array();
565 foreach ( $this->getNamespaces() as $index => $name ) {
566 $this->mNamespaceIds
[$this->lc( $name )] = $index;
568 foreach ( $this->getNamespaceAliases() as $name => $index ) {
569 $this->mNamespaceIds
[$this->lc( $name )] = $index;
571 if ( $wgNamespaceAliases ) {
572 foreach ( $wgNamespaceAliases as $name => $index ) {
573 $this->mNamespaceIds
[$this->lc( $name )] = $index;
577 return $this->mNamespaceIds
;
581 * Get a namespace key by value, case insensitive. Canonical namespace
582 * names override custom ones defined for the current language.
584 * @param $text String
585 * @return mixed An integer if $text is a valid value otherwise false
587 function getNsIndex( $text ) {
588 $lctext = $this->lc( $text );
589 $ns = MWNamespace
::getCanonicalIndex( $lctext );
590 if ( $ns !== null ) {
593 $ids = $this->getNamespaceIds();
594 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
598 * short names for language variants used for language conversion links.
600 * @param $code String
601 * @param $usemsg bool Use the "variantname-xyz" message if it exists
604 function getVariantname( $code, $usemsg = true ) {
605 $msg = "variantname-$code";
606 if ( $usemsg && wfMessage( $msg )->exists() ) {
607 return $this->getMessageFromDB( $msg );
609 $name = self
::fetchLanguageName( $code );
611 return $name; # if it's defined as a language name, show that
613 # otherwise, output the language code
619 * @param $name string
622 function specialPage( $name ) {
623 $aliases = $this->getSpecialPageAliases();
624 if ( isset( $aliases[$name][0] ) ) {
625 $name = $aliases[$name][0];
627 return $this->getNsText( NS_SPECIAL
) . ':' . $name;
633 function getQuickbarSettings() {
635 $this->getMessage( 'qbsettings-none' ),
636 $this->getMessage( 'qbsettings-fixedleft' ),
637 $this->getMessage( 'qbsettings-fixedright' ),
638 $this->getMessage( 'qbsettings-floatingleft' ),
639 $this->getMessage( 'qbsettings-floatingright' ),
640 $this->getMessage( 'qbsettings-directionality' )
647 function getDatePreferences() {
648 return self
::$dataCache->getItem( $this->mCode
, 'datePreferences' );
654 function getDateFormats() {
655 return self
::$dataCache->getItem( $this->mCode
, 'dateFormats' );
659 * @return array|string
661 function getDefaultDateFormat() {
662 $df = self
::$dataCache->getItem( $this->mCode
, 'defaultDateFormat' );
663 if ( $df === 'dmy or mdy' ) {
664 global $wgAmericanDates;
665 return $wgAmericanDates ?
'mdy' : 'dmy';
674 function getDatePreferenceMigrationMap() {
675 return self
::$dataCache->getItem( $this->mCode
, 'datePreferenceMigrationMap' );
682 function getImageFile( $image ) {
683 return self
::$dataCache->getSubitem( $this->mCode
, 'imageFiles', $image );
689 function getExtraUserToggles() {
690 return (array)self
::$dataCache->getItem( $this->mCode
, 'extraUserToggles' );
697 function getUserToggle( $tog ) {
698 return $this->getMessageFromDB( "tog-$tog" );
702 * Get native language names, indexed by code.
703 * Only those defined in MediaWiki, no other data like CLDR.
704 * If $customisedOnly is true, only returns codes with a messages file
706 * @param $customisedOnly bool
709 * @deprecated in 1.20, use fetchLanguageNames()
711 public static function getLanguageNames( $customisedOnly = false ) {
712 return self
::fetchLanguageNames( null, $customisedOnly ?
'mwfile' : 'mw' );
716 * Get translated language names. This is done on best effort and
717 * by default this is exactly the same as Language::getLanguageNames.
718 * The CLDR extension provides translated names.
719 * @param $code String Language code.
720 * @return Array language code => language name
722 * @deprecated in 1.20, use fetchLanguageNames()
724 public static function getTranslatedLanguageNames( $code ) {
725 return self
::fetchLanguageNames( $code, 'all' );
729 * Get an array of language names, indexed by code.
730 * @param $inLanguage null|string: Code of language in which to return the names
731 * Use null for autonyms (native names)
732 * @param $include string:
733 * 'all' all available languages
734 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
735 * 'mwfile' only if the language is in 'mw' *and* has a message file
736 * @return array: language code => language name
739 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
740 global $wgExtraLanguageNames;
741 static $coreLanguageNames;
743 if ( $coreLanguageNames === null ) {
744 include( MWInit
::compiledPath( 'languages/Names.php' ) );
750 # TODO: also include when $inLanguage is null, when this code is more efficient
751 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
754 $mwNames = $wgExtraLanguageNames +
$coreLanguageNames;
755 foreach ( $mwNames as $mwCode => $mwName ) {
756 # - Prefer own MediaWiki native name when not using the hook
757 # - For other names just add if not added through the hook
758 if ( $mwCode === $inLanguage ||
!isset( $names[$mwCode] ) ) {
759 $names[$mwCode] = $mwName;
763 if ( $include === 'all' ) {
768 $coreCodes = array_keys( $mwNames );
769 foreach ( $coreCodes as $coreCode ) {
770 $returnMw[$coreCode] = $names[$coreCode];
773 if ( $include === 'mwfile' ) {
774 $namesMwFile = array();
775 # We do this using a foreach over the codes instead of a directory
776 # loop so that messages files in extensions will work correctly.
777 foreach ( $returnMw as $code => $value ) {
778 if ( is_readable( self
::getMessagesFileName( $code ) ) ) {
779 $namesMwFile[$code] = $names[$code];
784 # 'mw' option; default if it's not one of the other two options (all/mwfile)
789 * @param $code string: The code of the language for which to get the name
790 * @param $inLanguage null|string: Code of language in which to return the name (null for autonyms)
791 * @param $include string: 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
792 * @return string: Language name or empty
795 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
796 $array = self
::fetchLanguageNames( $inLanguage, $include );
797 return !array_key_exists( $code, $array ) ?
'' : $array[$code];
801 * Get a message from the MediaWiki namespace.
803 * @param $msg String: message name
806 function getMessageFromDB( $msg ) {
807 return wfMessage( $msg )->inLanguage( $this )->text();
811 * Get the native language name of $code.
812 * Only if defined in MediaWiki, no other data like CLDR.
813 * @param $code string
815 * @deprecated in 1.20, use fetchLanguageName()
817 function getLanguageName( $code ) {
818 return self
::fetchLanguageName( $code );
825 function getMonthName( $key ) {
826 return $this->getMessageFromDB( self
::$mMonthMsgs[$key - 1] );
832 function getMonthNamesArray() {
833 $monthNames = array( '' );
834 for ( $i = 1; $i < 13; $i++
) {
835 $monthNames[] = $this->getMonthName( $i );
844 function getMonthNameGen( $key ) {
845 return $this->getMessageFromDB( self
::$mMonthGenMsgs[$key - 1] );
852 function getMonthAbbreviation( $key ) {
853 return $this->getMessageFromDB( self
::$mMonthAbbrevMsgs[$key - 1] );
859 function getMonthAbbreviationsArray() {
860 $monthNames = array( '' );
861 for ( $i = 1; $i < 13; $i++
) {
862 $monthNames[] = $this->getMonthAbbreviation( $i );
871 function getWeekdayName( $key ) {
872 return $this->getMessageFromDB( self
::$mWeekdayMsgs[$key - 1] );
879 function getWeekdayAbbreviation( $key ) {
880 return $this->getMessageFromDB( self
::$mWeekdayAbbrevMsgs[$key - 1] );
887 function getIranianCalendarMonthName( $key ) {
888 return $this->getMessageFromDB( self
::$mIranianCalendarMonthMsgs[$key - 1] );
895 function getHebrewCalendarMonthName( $key ) {
896 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthMsgs[$key - 1] );
903 function getHebrewCalendarMonthNameGen( $key ) {
904 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthGenMsgs[$key - 1] );
911 function getHijriCalendarMonthName( $key ) {
912 return $this->getMessageFromDB( self
::$mHijriCalendarMonthMsgs[$key - 1] );
916 * This is a workalike of PHP's date() function, but with better
917 * internationalisation, a reduced set of format characters, and a better
920 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
921 * PHP manual for definitions. There are a number of extensions, which
924 * xn Do not translate digits of the next numeric format character
925 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
926 * xr Use roman numerals for the next numeric format character
927 * xh Use hebrew numerals for the next numeric format character
929 * xg Genitive month name
931 * xij j (day number) in Iranian calendar
932 * xiF F (month name) in Iranian calendar
933 * xin n (month number) in Iranian calendar
934 * xiy y (two digit year) in Iranian calendar
935 * xiY Y (full year) in Iranian calendar
937 * xjj j (day number) in Hebrew calendar
938 * xjF F (month name) in Hebrew calendar
939 * xjt t (days in month) in Hebrew calendar
940 * xjx xg (genitive month name) in Hebrew calendar
941 * xjn n (month number) in Hebrew calendar
942 * xjY Y (full year) in Hebrew calendar
944 * xmj j (day number) in Hijri calendar
945 * xmF F (month name) in Hijri calendar
946 * xmn n (month number) in Hijri calendar
947 * xmY Y (full year) in Hijri calendar
949 * xkY Y (full year) in Thai solar calendar. Months and days are
950 * identical to the Gregorian calendar
951 * xoY Y (full year) in Minguo calendar or Juche year.
952 * Months and days are identical to the
954 * xtY Y (full year) in Japanese nengo. Months and days are
955 * identical to the Gregorian calendar
957 * Characters enclosed in double quotes will be considered literal (with
958 * the quotes themselves removed). Unmatched quotes will be considered
959 * literal quotes. Example:
961 * "The month is" F => The month is January
964 * Backslash escaping is also supported.
966 * Input timestamp is assumed to be pre-normalized to the desired local
969 * @param $format String
970 * @param $ts String: 14-character timestamp
973 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
977 function sprintfDate( $format, $ts ) {
990 for ( $p = 0; $p < strlen( $format ); $p++
) {
993 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
994 $code .= $format[++
$p];
997 if ( ( $code === 'xi' ||
$code == 'xj' ||
$code == 'xk' ||
$code == 'xm' ||
$code == 'xo' ||
$code == 'xt' ) && $p < strlen( $format ) - 1 ) {
998 $code .= $format[++
$p];
1009 $rawToggle = !$rawToggle;
1018 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1021 if ( !$hebrew ) $hebrew = self
::tsToHebrew( $ts );
1022 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1025 $num = substr( $ts, 6, 2 );
1028 if ( !$unix ) $unix = wfTimestamp( TS_UNIX
, $ts );
1029 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) +
1 );
1032 $num = intval( substr( $ts, 6, 2 ) );
1036 $iranian = self
::tsToIranian( $ts );
1042 $hijri = self
::tsToHijri( $ts );
1048 $hebrew = self
::tsToHebrew( $ts );
1054 $unix = wfTimestamp( TS_UNIX
, $ts );
1056 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) +
1 );
1060 $unix = wfTimestamp( TS_UNIX
, $ts );
1062 $w = gmdate( 'w', $unix );
1067 $unix = wfTimestamp( TS_UNIX
, $ts );
1069 $num = gmdate( 'w', $unix );
1073 $unix = wfTimestamp( TS_UNIX
, $ts );
1075 $num = gmdate( 'z', $unix );
1079 $unix = wfTimestamp( TS_UNIX
, $ts );
1081 $num = gmdate( 'W', $unix );
1084 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1088 $iranian = self
::tsToIranian( $ts );
1090 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1094 $hijri = self
::tsToHijri( $ts );
1096 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1100 $hebrew = self
::tsToHebrew( $ts );
1102 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1105 $num = substr( $ts, 4, 2 );
1108 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1111 $num = intval( substr( $ts, 4, 2 ) );
1115 $iranian = self
::tsToIranian( $ts );
1121 $hijri = self
::tsToHijri ( $ts );
1127 $hebrew = self
::tsToHebrew( $ts );
1133 $unix = wfTimestamp( TS_UNIX
, $ts );
1135 $num = gmdate( 't', $unix );
1139 $hebrew = self
::tsToHebrew( $ts );
1145 $unix = wfTimestamp( TS_UNIX
, $ts );
1147 $num = gmdate( 'L', $unix );
1151 $unix = wfTimestamp( TS_UNIX
, $ts );
1153 $num = gmdate( 'o', $unix );
1156 $num = substr( $ts, 0, 4 );
1160 $iranian = self
::tsToIranian( $ts );
1166 $hijri = self
::tsToHijri( $ts );
1172 $hebrew = self
::tsToHebrew( $ts );
1178 $thai = self
::tsToYear( $ts, 'thai' );
1184 $minguo = self
::tsToYear( $ts, 'minguo' );
1190 $tenno = self
::tsToYear( $ts, 'tenno' );
1195 $num = substr( $ts, 2, 2 );
1199 $iranian = self
::tsToIranian( $ts );
1201 $num = substr( $iranian[0], -2 );
1204 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'am' : 'pm';
1207 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'AM' : 'PM';
1210 $h = substr( $ts, 8, 2 );
1211 $num = $h %
12 ?
$h %
12 : 12;
1214 $num = intval( substr( $ts, 8, 2 ) );
1217 $h = substr( $ts, 8, 2 );
1218 $num = sprintf( '%02d', $h %
12 ?
$h %
12 : 12 );
1221 $num = substr( $ts, 8, 2 );
1224 $num = substr( $ts, 10, 2 );
1227 $num = substr( $ts, 12, 2 );
1231 $unix = wfTimestamp( TS_UNIX
, $ts );
1233 $s .= gmdate( 'c', $unix );
1237 $unix = wfTimestamp( TS_UNIX
, $ts );
1239 $s .= gmdate( 'r', $unix );
1243 $unix = wfTimestamp( TS_UNIX
, $ts );
1248 # Backslash escaping
1249 if ( $p < strlen( $format ) - 1 ) {
1250 $s .= $format[++
$p];
1257 if ( $p < strlen( $format ) - 1 ) {
1258 $endQuote = strpos( $format, '"', $p +
1 );
1259 if ( $endQuote === false ) {
1260 # No terminating quote, assume literal "
1263 $s .= substr( $format, $p +
1, $endQuote - $p - 1 );
1267 # Quote at end of string, assume literal "
1274 if ( $num !== false ) {
1275 if ( $rawToggle ||
$raw ) {
1278 } elseif ( $roman ) {
1279 $s .= Language
::romanNumeral( $num );
1281 } elseif ( $hebrewNum ) {
1282 $s .= self
::hebrewNumeral( $num );
1285 $s .= $this->formatNum( $num, true );
1292 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1293 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1296 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1297 * Gregorian dates to Iranian dates. Originally written in C, it
1298 * is released under the terms of GNU Lesser General Public
1299 * License. Conversion to PHP was performed by Niklas Laxström.
1301 * Link: http://www.farsiweb.info/jalali/jalali.c
1307 private static function tsToIranian( $ts ) {
1308 $gy = substr( $ts, 0, 4 ) -1600;
1309 $gm = substr( $ts, 4, 2 ) -1;
1310 $gd = substr( $ts, 6, 2 ) -1;
1312 # Days passed from the beginning (including leap years)
1314 +
floor( ( $gy +
3 ) / 4 )
1315 - floor( ( $gy +
99 ) / 100 )
1316 +
floor( ( $gy +
399 ) / 400 );
1318 // Add days of the past months of this year
1319 for ( $i = 0; $i < $gm; $i++
) {
1320 $gDayNo +
= self
::$GREG_DAYS[$i];
1324 if ( $gm > 1 && ( ( $gy %
4 === 0 && $gy %
100 !== 0 ||
( $gy %
400 == 0 ) ) ) ) {
1328 // Days passed in current month
1329 $gDayNo +
= (int)$gd;
1331 $jDayNo = $gDayNo - 79;
1333 $jNp = floor( $jDayNo / 12053 );
1336 $jy = 979 +
33 * $jNp +
4 * floor( $jDayNo / 1461 );
1339 if ( $jDayNo >= 366 ) {
1340 $jy +
= floor( ( $jDayNo - 1 ) / 365 );
1341 $jDayNo = floor( ( $jDayNo - 1 ) %
365 );
1344 for ( $i = 0; $i < 11 && $jDayNo >= self
::$IRANIAN_DAYS[$i]; $i++
) {
1345 $jDayNo -= self
::$IRANIAN_DAYS[$i];
1351 return array( $jy, $jm, $jd );
1355 * Converting Gregorian dates to Hijri dates.
1357 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1359 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1365 private static function tsToHijri( $ts ) {
1366 $year = substr( $ts, 0, 4 );
1367 $month = substr( $ts, 4, 2 );
1368 $day = substr( $ts, 6, 2 );
1376 ( $zy > 1582 ) ||
( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1377 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1380 $zjd = (int)( ( 1461 * ( $zy +
4800 +
(int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1381 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1382 (int)( ( 3 * (int)( ( ( $zy +
4900 +
(int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1385 $zjd = 367 * $zy - (int)( ( 7 * ( $zy +
5001 +
(int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1386 (int)( ( 275 * $zm ) / 9 ) +
$zd +
1729777;
1389 $zl = $zjd -1948440 +
10632;
1390 $zn = (int)( ( $zl - 1 ) / 10631 );
1391 $zl = $zl - 10631 * $zn +
354;
1392 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1393 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) +
29;
1394 $zm = (int)( ( 24 * $zl ) / 709 );
1395 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1396 $zy = 30 * $zn +
$zj - 30;
1398 return array( $zy, $zm, $zd );
1402 * Converting Gregorian dates to Hebrew dates.
1404 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1405 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1406 * to translate the relevant functions into PHP and release them under
1409 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1410 * and Adar II is 14. In a non-leap year, Adar is 6.
1416 private static function tsToHebrew( $ts ) {
1418 $year = substr( $ts, 0, 4 );
1419 $month = substr( $ts, 4, 2 );
1420 $day = substr( $ts, 6, 2 );
1422 # Calculate Hebrew year
1423 $hebrewYear = $year +
3760;
1425 # Month number when September = 1, August = 12
1427 if ( $month > 12 ) {
1434 # Calculate day of year from 1 September
1436 for ( $i = 1; $i < $month; $i++
) {
1440 # Check if the year is leap
1441 if ( $year %
400 == 0 ||
( $year %
4 == 0 && $year %
100 > 0 ) ) {
1444 } elseif ( $i == 8 ||
$i == 10 ||
$i == 1 ||
$i == 3 ) {
1451 # Calculate the start of the Hebrew year
1452 $start = self
::hebrewYearStart( $hebrewYear );
1454 # Calculate next year's start
1455 if ( $dayOfYear <= $start ) {
1456 # Day is before the start of the year - it is the previous year
1458 $nextStart = $start;
1462 # Add days since previous year's 1 September
1464 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1468 # Start of the new (previous) year
1469 $start = self
::hebrewYearStart( $hebrewYear );
1472 $nextStart = self
::hebrewYearStart( $hebrewYear +
1 );
1475 # Calculate Hebrew day of year
1476 $hebrewDayOfYear = $dayOfYear - $start;
1478 # Difference between year's days
1479 $diff = $nextStart - $start;
1480 # Add 12 (or 13 for leap years) days to ignore the difference between
1481 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1482 # difference is only about the year type
1483 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1489 # Check the year pattern, and is leap year
1490 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1491 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1492 # and non-leap years
1493 $yearPattern = $diff %
30;
1494 # Check if leap year
1495 $isLeap = $diff >= 30;
1497 # Calculate day in the month from number of day in the Hebrew year
1498 # Don't check Adar - if the day is not in Adar, we will stop before;
1499 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1500 $hebrewDay = $hebrewDayOfYear;
1503 while ( $hebrewMonth <= 12 ) {
1504 # Calculate days in this month
1505 if ( $isLeap && $hebrewMonth == 6 ) {
1506 # Adar in a leap year
1508 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1510 if ( $hebrewDay <= $days ) {
1514 # Subtract the days of Adar I
1515 $hebrewDay -= $days;
1518 if ( $hebrewDay <= $days ) {
1524 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1525 # Cheshvan in a complete year (otherwise as the rule below)
1527 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1528 # Kislev in an incomplete year (otherwise as the rule below)
1531 # Odd months have 30 days, even have 29
1532 $days = 30 - ( $hebrewMonth - 1 ) %
2;
1534 if ( $hebrewDay <= $days ) {
1535 # In the current month
1538 # Subtract the days of the current month
1539 $hebrewDay -= $days;
1540 # Try in the next month
1545 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1549 * This calculates the Hebrew year start, as days since 1 September.
1550 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1551 * Used for Hebrew date.
1557 private static function hebrewYearStart( $year ) {
1558 $a = intval( ( 12 * ( $year - 1 ) +
17 ) %
19 );
1559 $b = intval( ( $year - 1 ) %
4 );
1560 $m = 32.044093161144 +
1.5542417966212 * $a +
$b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1564 $Mar = intval( $m );
1570 $c = intval( ( $Mar +
3 * ( $year - 1 ) +
5 * $b +
5 ) %
7 );
1571 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1573 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1575 } elseif ( $c == 2 ||
$c == 4 ||
$c == 6 ) {
1579 $Mar +
= intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1584 * Algorithm to convert Gregorian dates to Thai solar dates,
1585 * Minguo dates or Minguo dates.
1587 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1588 * http://en.wikipedia.org/wiki/Minguo_calendar
1589 * http://en.wikipedia.org/wiki/Japanese_era_name
1591 * @param $ts String: 14-character timestamp
1592 * @param $cName String: calender name
1593 * @return Array: converted year, month, day
1595 private static function tsToYear( $ts, $cName ) {
1596 $gy = substr( $ts, 0, 4 );
1597 $gm = substr( $ts, 4, 2 );
1598 $gd = substr( $ts, 6, 2 );
1600 if ( !strcmp( $cName, 'thai' ) ) {
1602 # Add 543 years to the Gregorian calendar
1603 # Months and days are identical
1604 $gy_offset = $gy +
543;
1605 } elseif ( ( !strcmp( $cName, 'minguo' ) ) ||
!strcmp( $cName, 'juche' ) ) {
1607 # Deduct 1911 years from the Gregorian calendar
1608 # Months and days are identical
1609 $gy_offset = $gy - 1911;
1610 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1611 # Nengō dates up to Meiji period
1612 # Deduct years from the Gregorian calendar
1613 # depending on the nengo periods
1614 # Months and days are identical
1615 if ( ( $gy < 1912 ) ||
( ( $gy == 1912 ) && ( $gm < 7 ) ) ||
( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
1617 $gy_gannen = $gy - 1868 +
1;
1618 $gy_offset = $gy_gannen;
1619 if ( $gy_gannen == 1 ) {
1622 $gy_offset = '明治' . $gy_offset;
1624 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1625 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1626 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1627 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1628 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1632 $gy_gannen = $gy - 1912 +
1;
1633 $gy_offset = $gy_gannen;
1634 if ( $gy_gannen == 1 ) {
1637 $gy_offset = '大正' . $gy_offset;
1639 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1640 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1641 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1645 $gy_gannen = $gy - 1926 +
1;
1646 $gy_offset = $gy_gannen;
1647 if ( $gy_gannen == 1 ) {
1650 $gy_offset = '昭和' . $gy_offset;
1653 $gy_gannen = $gy - 1989 +
1;
1654 $gy_offset = $gy_gannen;
1655 if ( $gy_gannen == 1 ) {
1658 $gy_offset = '平成' . $gy_offset;
1664 return array( $gy_offset, $gm, $gd );
1668 * Roman number formatting up to 10000
1674 static function romanNumeral( $num ) {
1675 static $table = array(
1676 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1677 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1678 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1679 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM', 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
1682 $num = intval( $num );
1683 if ( $num > 10000 ||
$num <= 0 ) {
1688 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1689 if ( $num >= $pow10 ) {
1690 $s .= $table[$i][(int)floor( $num / $pow10 )];
1692 $num = $num %
$pow10;
1698 * Hebrew Gematria number formatting up to 9999
1704 static function hebrewNumeral( $num ) {
1705 static $table = array(
1706 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1707 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1708 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1709 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1712 $num = intval( $num );
1713 if ( $num > 9999 ||
$num <= 0 ) {
1718 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1719 if ( $num >= $pow10 ) {
1720 if ( $num == 15 ||
$num == 16 ) {
1721 $s .= $table[0][9] . $table[0][$num - 9];
1724 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1725 if ( $pow10 == 1000 ) {
1730 $num = $num %
$pow10;
1732 if ( strlen( $s ) == 2 ) {
1735 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1736 $str .= substr( $s, strlen( $s ) - 2, 2 );
1738 $start = substr( $str, 0, strlen( $str ) - 2 );
1739 $end = substr( $str, strlen( $str ) - 2 );
1742 $str = $start . 'ך';
1745 $str = $start . 'ם';
1748 $str = $start . 'ן';
1751 $str = $start . 'ף';
1754 $str = $start . 'ץ';
1761 * Used by date() and time() to adjust the time output.
1763 * @param $ts Int the time in date('YmdHis') format
1764 * @param $tz Mixed: adjust the time by this amount (default false, mean we
1765 * get user timecorrection setting)
1768 function userAdjust( $ts, $tz = false ) {
1769 global $wgUser, $wgLocalTZoffset;
1771 if ( $tz === false ) {
1772 $tz = $wgUser->getOption( 'timecorrection' );
1775 $data = explode( '|', $tz, 3 );
1777 if ( $data[0] == 'ZoneInfo' ) {
1778 wfSuppressWarnings();
1779 $userTZ = timezone_open( $data[2] );
1780 wfRestoreWarnings();
1781 if ( $userTZ !== false ) {
1782 $date = date_create( $ts, timezone_open( 'UTC' ) );
1783 date_timezone_set( $date, $userTZ );
1784 $date = date_format( $date, 'YmdHis' );
1787 # Unrecognized timezone, default to 'Offset' with the stored offset.
1788 $data[0] = 'Offset';
1792 if ( $data[0] == 'System' ||
$tz == '' ) {
1793 # Global offset in minutes.
1794 if ( isset( $wgLocalTZoffset ) ) {
1795 $minDiff = $wgLocalTZoffset;
1797 } elseif ( $data[0] == 'Offset' ) {
1798 $minDiff = intval( $data[1] );
1800 $data = explode( ':', $tz );
1801 if ( count( $data ) == 2 ) {
1802 $data[0] = intval( $data[0] );
1803 $data[1] = intval( $data[1] );
1804 $minDiff = abs( $data[0] ) * 60 +
$data[1];
1805 if ( $data[0] < 0 ) {
1806 $minDiff = -$minDiff;
1809 $minDiff = intval( $data[0] ) * 60;
1813 # No difference ? Return time unchanged
1814 if ( 0 == $minDiff ) {
1818 wfSuppressWarnings(); // E_STRICT system time bitching
1819 # Generate an adjusted date; take advantage of the fact that mktime
1820 # will normalize out-of-range values so we don't have to split $minDiff
1821 # into hours and minutes.
1823 (int)substr( $ts, 8, 2 ) ), # Hours
1824 (int)substr( $ts, 10, 2 ) +
$minDiff, # Minutes
1825 (int)substr( $ts, 12, 2 ), # Seconds
1826 (int)substr( $ts, 4, 2 ), # Month
1827 (int)substr( $ts, 6, 2 ), # Day
1828 (int)substr( $ts, 0, 4 ) ); # Year
1830 $date = date( 'YmdHis', $t );
1831 wfRestoreWarnings();
1837 * This is meant to be used by time(), date(), and timeanddate() to get
1838 * the date preference they're supposed to use, it should be used in
1842 * function timeanddate([...], $format = true) {
1843 * $datePreference = $this->dateFormat($format);
1848 * @param $usePrefs Mixed: if true, the user's preference is used
1849 * if false, the site/language default is used
1850 * if int/string, assumed to be a format.
1851 * if User object, assumed to be a User to get preference from
1854 function dateFormat( $usePrefs = true ) {
1857 if ( is_bool( $usePrefs ) ) {
1859 $datePreference = $wgUser->getDatePreference();
1861 $datePreference = (string)User
::getDefaultOption( 'date' );
1863 } elseif ( $usePrefs instanceof User
) {
1864 $datePreference = $usePrefs->getDatePreference();
1866 $datePreference = (string)$usePrefs;
1870 if ( $datePreference == '' ) {
1874 return $datePreference;
1878 * Get a format string for a given type and preference
1879 * @param $type string May be date, time or both
1880 * @param $pref string The format name as it appears in Messages*.php
1884 function getDateFormatString( $type, $pref ) {
1885 if ( !isset( $this->dateFormatStrings
[$type][$pref] ) ) {
1886 if ( $pref == 'default' ) {
1887 $pref = $this->getDefaultDateFormat();
1888 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1890 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1892 if ( $type === 'shortdate' && is_null( $df ) ) {
1893 $df = $this->getDateFormatString( 'date', $pref );
1896 if ( is_null( $df ) ) {
1897 $pref = $this->getDefaultDateFormat();
1898 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1902 $this->dateFormatStrings
[$type][$pref] = $df;
1904 return $this->dateFormatStrings
[$type][$pref];
1908 * @param $ts Mixed: the time format which needs to be turned into a
1909 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1910 * @param $adj Bool: whether to adjust the time output according to the
1911 * user configured offset ($timecorrection)
1912 * @param $format Mixed: true to use user's date format preference
1913 * @param $timecorrection String|bool the time offset as returned by
1914 * validateTimeZone() in Special:Preferences
1917 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1918 $ts = wfTimestamp( TS_MW
, $ts );
1920 $ts = $this->userAdjust( $ts, $timecorrection );
1922 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
1923 return $this->sprintfDate( $df, $ts );
1927 * @param $ts Mixed: the time format which needs to be turned into a
1928 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1929 * @param $adj Bool: whether to adjust the time output according to the
1930 * user configured offset ($timecorrection)
1931 * @param $format Mixed: true to use user's date format preference
1932 * @param $timecorrection String|bool the time offset as returned by
1933 * validateTimeZone() in Special:Preferences
1936 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1937 $ts = wfTimestamp( TS_MW
, $ts );
1939 $ts = $this->userAdjust( $ts, $timecorrection );
1941 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
1942 return $this->sprintfDate( $df, $ts );
1946 * @param $ts Mixed: the time format which needs to be turned into a
1947 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1948 * @param $adj Bool: whether to adjust the time output according to the
1949 * user configured offset ($timecorrection)
1950 * @param $format Mixed: what format to return, if it's false output the
1951 * default one (default true)
1952 * @param $timecorrection String|bool the time offset as returned by
1953 * validateTimeZone() in Special:Preferences
1956 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
1957 $ts = wfTimestamp( TS_MW
, $ts );
1959 $ts = $this->userAdjust( $ts, $timecorrection );
1961 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
1962 return $this->sprintfDate( $df, $ts );
1966 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
1970 * @param integer $seconds The amount of seconds.
1971 * @param array $chosenIntervals The intervals to enable.
1975 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
1976 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
1978 $segments = array();
1980 foreach ( $intervals as $intervalName => $intervalValue ) {
1981 $message = new Message( 'duration-' . $intervalName, array( $intervalValue ) );
1982 $segments[] = $message->inLanguage( $this )->escaped();
1985 return $this->listToText( $segments );
1989 * Takes a number of seconds and returns an array with a set of corresponding intervals.
1990 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
1994 * @param integer $seconds The amount of seconds.
1995 * @param array $chosenIntervals The intervals to enable.
1999 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2000 if ( empty( $chosenIntervals ) ) {
2001 $chosenIntervals = array( 'millennia', 'centuries', 'decades', 'years', 'days', 'hours', 'minutes', 'seconds' );
2004 $intervals = array_intersect_key( self
::$durationIntervals, array_flip( $chosenIntervals ) );
2005 $sortedNames = array_keys( $intervals );
2006 $smallestInterval = array_pop( $sortedNames );
2008 $segments = array();
2010 foreach ( $intervals as $name => $length ) {
2011 $value = floor( $seconds / $length );
2013 if ( $value > 0 ||
( $name == $smallestInterval && empty( $segments ) ) ) {
2014 $seconds -= $value * $length;
2015 $segments[$name] = $value;
2023 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2025 * @param $type String: can be 'date', 'time' or 'both'
2026 * @param $ts Mixed: the time format which needs to be turned into a
2027 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2028 * @param $user User object used to get preferences for timezone and format
2029 * @param $options Array, can contain the following keys:
2030 * - 'timecorrection': time correction, can have the following values:
2031 * - true: use user's preference
2032 * - false: don't use time correction
2033 * - integer: value of time correction in minutes
2034 * - 'format': format to use, can have the following values:
2035 * - true: use user's preference
2036 * - false: use default preference
2037 * - string: format to use
2041 private function internalUserTimeAndDate( $type, $ts, User
$user, array $options ) {
2042 $ts = wfTimestamp( TS_MW
, $ts );
2043 $options +
= array( 'timecorrection' => true, 'format' => true );
2044 if ( $options['timecorrection'] !== false ) {
2045 if ( $options['timecorrection'] === true ) {
2046 $offset = $user->getOption( 'timecorrection' );
2048 $offset = $options['timecorrection'];
2050 $ts = $this->userAdjust( $ts, $offset );
2052 if ( $options['format'] === true ) {
2053 $format = $user->getDatePreference();
2055 $format = $options['format'];
2057 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2058 return $this->sprintfDate( $df, $ts );
2062 * Get the formatted date for the given timestamp and formatted for
2065 * @param $ts Mixed: the time format which needs to be turned into a
2066 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2067 * @param $user User object used to get preferences for timezone and format
2068 * @param $options Array, can contain the following keys:
2069 * - 'timecorrection': time correction, can have the following values:
2070 * - true: use user's preference
2071 * - false: don't use time correction
2072 * - integer: value of time correction in minutes
2073 * - 'format': format to use, can have the following values:
2074 * - true: use user's preference
2075 * - false: use default preference
2076 * - string: format to use
2080 public function userDate( $ts, User
$user, array $options = array() ) {
2081 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2085 * Get the formatted time for the given timestamp and formatted for
2088 * @param $ts Mixed: the time format which needs to be turned into a
2089 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2090 * @param $user User object used to get preferences for timezone and format
2091 * @param $options Array, can contain the following keys:
2092 * - 'timecorrection': time correction, can have the following values:
2093 * - true: use user's preference
2094 * - false: don't use time correction
2095 * - integer: value of time correction in minutes
2096 * - 'format': format to use, can have the following values:
2097 * - true: use user's preference
2098 * - false: use default preference
2099 * - string: format to use
2103 public function userTime( $ts, User
$user, array $options = array() ) {
2104 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2108 * Get the formatted date and time for the given timestamp and formatted for
2111 * @param $ts Mixed: the time format which needs to be turned into a
2112 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2113 * @param $user User object used to get preferences for timezone and format
2114 * @param $options Array, can contain the following keys:
2115 * - 'timecorrection': time correction, can have the following values:
2116 * - true: use user's preference
2117 * - false: don't use time correction
2118 * - integer: value of time correction in minutes
2119 * - 'format': format to use, can have the following values:
2120 * - true: use user's preference
2121 * - false: use default preference
2122 * - string: format to use
2126 public function userTimeAndDate( $ts, User
$user, array $options = array() ) {
2127 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2131 * Formats a timestamp in a pretty, human-readable format.
2132 * Instead of "13:04, 16 July 2012", we have:
2136 * - Yesterday at 13:04
2137 * - Wednesday at 13:04
2139 * - July 16 2012 at 13:04
2141 * @todo Port to JavaScript
2143 * @param $ts Mixed: the time format which needs to be turned into a
2144 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2145 * @param $relativeTo Mixed: The timestamp to use as "now"
2146 * @param $user User: The user to format for (needed for timezone information)
2148 * @return string Formatted timestamp
2150 public function prettyTimestamp( $timestamp, $relativeTo = false, $user ) {
2151 // Parameter defaults
2152 if ( $relativeTo === false ) {
2153 $relativeTo = wfTimestampNow();
2156 // Normalise input to UNIX time
2157 $relativeTo = wfTimestamp( TS_UNIX
, $relativeTo );
2158 $timestamp = wfTimestamp( TS_UNIX
, $timestamp );
2159 $timeAgo = $relativeTo - $timestamp;
2161 $adjustedRelativeTo = $this->userAdjust( wfTimestamp( TS_MW
, $relativeTo ), $user->getOption('timecorrection') );
2162 $adjustedRelativeTo = wfTimestamp( TS_UNIX
, $adjustedRelativeTo );
2163 $relativeToYear = gmdate( 'Y', $adjustedRelativeTo );
2165 $adjustedTimestamp = $this->userAdjust( wfTimestamp( TS_MW
, $timestamp ), $user->getOption('timecorrection') );
2166 $adjustedTimestamp = wfTimestamp( TS_UNIX
, $adjustedTimestamp );
2167 $timestampYear = gmdate( 'Y', $adjustedTimestamp );
2169 if ( $timeAgo < 0 ) {
2170 throw new MWException( "Future timestamps not currently supported" );
2171 } elseif ( $timeAgo < 30 ) {
2172 return wfMessage( 'just-now' )
2173 ->inLanguage( $this )
2175 } elseif ( $timeAgo < 5400 ) {
2176 // Less than 90 minutes ago. Return number of hours, minutes or seconds ago.
2177 return $this->formatRelativeTime( $timeAgo );
2178 } elseif ( // Same day
2179 intval( $adjustedRelativeTo / (24*60*60) ) ===
2180 intval( $adjustedTimestamp / (24*60*60) )
2183 $time = $this->time( $adjustedTimestamp );
2184 return wfMessage( 'today-at' )
2185 ->inLanguage( $this )
2188 } elseif ( // Previous day
2189 intval( $adjustedRelativeTo / (24*60*60) ) ===
2190 ( intval( $adjustedTimestamp / (24*60*60) ) +
1 )
2192 // Yesterday at XX:XX
2193 $time = $this->time( $adjustedTimestamp );
2195 return wfMessage( 'yesterday-at' )
2196 ->inLanguage( $this )
2199 } elseif ( $timeAgo < ( $this->mWeekdayAtCutoff
) ) { // Less than 5 days ago
2201 return $this->formatPastWeekTimestamp( $adjustedTimestamp, $adjustedRelativeTo );
2202 } elseif ( $relativeToYear == $timestampYear ) {
2204 $df = $this->getDateFormatString( 'shortdate', $this->dateFormat( $user ) );
2205 $mwTimestamp = wfTimestamp( TS_MW
, $timestamp );
2206 return $this->sprintfDate( $df, $mwTimestamp );
2209 $mwTimestamp = wfTimestamp( TS_MW
, $timestamp );
2210 return $this->userDate( $mwTimestamp, $user );
2215 * For pretty timestamps: Formats the "X {hours,minutes,seconds} ago" message.
2217 * @param $timeAgo The number of seconds ago the event occurred
2218 * @return Formatted string
2220 protected function formatRelativeTime( $timeAgo ) {
2222 if ( $timeAgo < 60 ) {
2225 } elseif ( $timeAgo < 3600 ) {
2227 $count = intval( $timeAgo / 60 );
2230 $count = intval( $timeAgo / 3600 );
2233 return wfMessage( "{$unit}-ago" )->inLanguage( $this )->params( $count )->text();
2237 * For pretty timestamps: Formats the timestamp for events that occurred
2238 * "in the last few days".
2239 * (cutoff is configurable by the member variable $mWeekdayAtCutoff)
2241 * @param $eventTimestamp The timestamp of the event, adjusted to
2242 * the user's local timezone, in UNIX format (# of seconds since epoch)
2243 * @param $relativeTo The timestamp to format relative to, adjusted to
2244 * the user's local timezone, in UNIX format (# of seconds since epoch)
2245 * @return String: The date formatted in a human-friendly way.
2247 protected function formatPastWeekTimestamp( $adjustedTimestamp, $relativeTo ) {
2248 $day = date( 'w', $adjustedTimestamp );
2249 $weekday = self
::$mWeekdayMsgs[$day];
2250 $time = $this->time( $adjustedTimestamp );
2252 return wfMessage( "$weekday-at" )
2253 ->inLanguage( $this )
2259 * @param $key string
2260 * @return array|null
2262 function getMessage( $key ) {
2263 return self
::$dataCache->getSubitem( $this->mCode
, 'messages', $key );
2269 function getAllMessages() {
2270 return self
::$dataCache->getItem( $this->mCode
, 'messages' );
2279 function iconv( $in, $out, $string ) {
2280 # This is a wrapper for iconv in all languages except esperanto,
2281 # which does some nasty x-conversions beforehand
2283 # Even with //IGNORE iconv can whine about illegal characters in
2284 # *input* string. We just ignore those too.
2285 # REF: http://bugs.php.net/bug.php?id=37166
2286 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2287 wfSuppressWarnings();
2288 $text = iconv( $in, $out . '//IGNORE', $string );
2289 wfRestoreWarnings();
2293 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2296 * @param $matches array
2297 * @return mixed|string
2299 function ucwordbreaksCallbackAscii( $matches ) {
2300 return $this->ucfirst( $matches[1] );
2304 * @param $matches array
2307 function ucwordbreaksCallbackMB( $matches ) {
2308 return mb_strtoupper( $matches[0] );
2312 * @param $matches array
2315 function ucCallback( $matches ) {
2316 list( $wikiUpperChars ) = self
::getCaseMaps();
2317 return strtr( $matches[1], $wikiUpperChars );
2321 * @param $matches array
2324 function lcCallback( $matches ) {
2325 list( , $wikiLowerChars ) = self
::getCaseMaps();
2326 return strtr( $matches[1], $wikiLowerChars );
2330 * @param $matches array
2333 function ucwordsCallbackMB( $matches ) {
2334 return mb_strtoupper( $matches[0] );
2338 * @param $matches array
2341 function ucwordsCallbackWiki( $matches ) {
2342 list( $wikiUpperChars ) = self
::getCaseMaps();
2343 return strtr( $matches[0], $wikiUpperChars );
2347 * Make a string's first character uppercase
2349 * @param $str string
2353 function ucfirst( $str ) {
2355 if ( $o < 96 ) { // if already uppercase...
2357 } elseif ( $o < 128 ) {
2358 return ucfirst( $str ); // use PHP's ucfirst()
2360 // fall back to more complex logic in case of multibyte strings
2361 return $this->uc( $str, true );
2366 * Convert a string to uppercase
2368 * @param $str string
2369 * @param $first bool
2373 function uc( $str, $first = false ) {
2374 if ( function_exists( 'mb_strtoupper' ) ) {
2376 if ( $this->isMultibyte( $str ) ) {
2377 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2379 return ucfirst( $str );
2382 return $this->isMultibyte( $str ) ?
mb_strtoupper( $str ) : strtoupper( $str );
2385 if ( $this->isMultibyte( $str ) ) {
2386 $x = $first ?
'^' : '';
2387 return preg_replace_callback(
2388 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2389 array( $this, 'ucCallback' ),
2393 return $first ?
ucfirst( $str ) : strtoupper( $str );
2399 * @param $str string
2400 * @return mixed|string
2402 function lcfirst( $str ) {
2405 return strval( $str );
2406 } elseif ( $o >= 128 ) {
2407 return $this->lc( $str, true );
2408 } elseif ( $o > 96 ) {
2411 $str[0] = strtolower( $str[0] );
2417 * @param $str string
2418 * @param $first bool
2419 * @return mixed|string
2421 function lc( $str, $first = false ) {
2422 if ( function_exists( 'mb_strtolower' ) ) {
2424 if ( $this->isMultibyte( $str ) ) {
2425 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2427 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2430 return $this->isMultibyte( $str ) ?
mb_strtolower( $str ) : strtolower( $str );
2433 if ( $this->isMultibyte( $str ) ) {
2434 $x = $first ?
'^' : '';
2435 return preg_replace_callback(
2436 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2437 array( $this, 'lcCallback' ),
2441 return $first ?
strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2447 * @param $str string
2450 function isMultibyte( $str ) {
2451 return (bool)preg_match( '/[\x80-\xff]/', $str );
2455 * @param $str string
2456 * @return mixed|string
2458 function ucwords( $str ) {
2459 if ( $this->isMultibyte( $str ) ) {
2460 $str = $this->lc( $str );
2462 // regexp to find first letter in each word (i.e. after each space)
2463 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2465 // function to use to capitalize a single char
2466 if ( function_exists( 'mb_strtoupper' ) ) {
2467 return preg_replace_callback(
2469 array( $this, 'ucwordsCallbackMB' ),
2473 return preg_replace_callback(
2475 array( $this, 'ucwordsCallbackWiki' ),
2480 return ucwords( strtolower( $str ) );
2485 * capitalize words at word breaks
2487 * @param $str string
2490 function ucwordbreaks( $str ) {
2491 if ( $this->isMultibyte( $str ) ) {
2492 $str = $this->lc( $str );
2494 // since \b doesn't work for UTF-8, we explicitely define word break chars
2495 $breaks = "[ \-\(\)\}\{\.,\?!]";
2497 // find first letter after word break
2498 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2500 if ( function_exists( 'mb_strtoupper' ) ) {
2501 return preg_replace_callback(
2503 array( $this, 'ucwordbreaksCallbackMB' ),
2507 return preg_replace_callback(
2509 array( $this, 'ucwordsCallbackWiki' ),
2514 return preg_replace_callback(
2515 '/\b([\w\x80-\xff]+)\b/',
2516 array( $this, 'ucwordbreaksCallbackAscii' ),
2523 * Return a case-folded representation of $s
2525 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2526 * and $s2 are the same except for the case of their characters. It is not
2527 * necessary for the value returned to make sense when displayed.
2529 * Do *not* perform any other normalisation in this function. If a caller
2530 * uses this function when it should be using a more general normalisation
2531 * function, then fix the caller.
2537 function caseFold( $s ) {
2538 return $this->uc( $s );
2545 function checkTitleEncoding( $s ) {
2546 if ( is_array( $s ) ) {
2547 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
2549 # Check for non-UTF-8 URLs
2550 $ishigh = preg_match( '/[\x80-\xff]/', $s );
2555 if ( function_exists( 'mb_check_encoding' ) ) {
2556 $isutf8 = mb_check_encoding( $s, 'UTF-8' );
2558 $isutf8 = preg_match( '/^(?>[\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2559 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
2565 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2571 function fallback8bitEncoding() {
2572 return self
::$dataCache->getItem( $this->mCode
, 'fallback8bitEncoding' );
2576 * Most writing systems use whitespace to break up words.
2577 * Some languages such as Chinese don't conventionally do this,
2578 * which requires special handling when breaking up words for
2583 function hasWordBreaks() {
2588 * Some languages such as Chinese require word segmentation,
2589 * Specify such segmentation when overridden in derived class.
2591 * @param $string String
2594 function segmentByWord( $string ) {
2599 * Some languages have special punctuation need to be normalized.
2600 * Make such changes here.
2602 * @param $string String
2605 function normalizeForSearch( $string ) {
2606 return self
::convertDoubleWidth( $string );
2610 * convert double-width roman characters to single-width.
2611 * range: ff00-ff5f ~= 0020-007f
2613 * @param $string string
2617 protected static function convertDoubleWidth( $string ) {
2618 static $full = null;
2619 static $half = null;
2621 if ( $full === null ) {
2622 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2623 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2624 $full = str_split( $fullWidth, 3 );
2625 $half = str_split( $halfWidth );
2628 $string = str_replace( $full, $half, $string );
2633 * @param $string string
2634 * @param $pattern string
2637 protected static function insertSpace( $string, $pattern ) {
2638 $string = preg_replace( $pattern, " $1 ", $string );
2639 $string = preg_replace( '/ +/', ' ', $string );
2644 * @param $termsArray array
2647 function convertForSearchResult( $termsArray ) {
2648 # some languages, e.g. Chinese, need to do a conversion
2649 # in order for search results to be displayed correctly
2654 * Get the first character of a string.
2659 function firstChar( $s ) {
2662 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2663 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2668 if ( isset( $matches[1] ) ) {
2669 if ( strlen( $matches[1] ) != 3 ) {
2673 // Break down Hangul syllables to grab the first jamo
2674 $code = utf8ToCodepoint( $matches[1] );
2675 if ( $code < 0xac00 ||
0xd7a4 <= $code ) {
2677 } elseif ( $code < 0xb098 ) {
2678 return "\xe3\x84\xb1";
2679 } elseif ( $code < 0xb2e4 ) {
2680 return "\xe3\x84\xb4";
2681 } elseif ( $code < 0xb77c ) {
2682 return "\xe3\x84\xb7";
2683 } elseif ( $code < 0xb9c8 ) {
2684 return "\xe3\x84\xb9";
2685 } elseif ( $code < 0xbc14 ) {
2686 return "\xe3\x85\x81";
2687 } elseif ( $code < 0xc0ac ) {
2688 return "\xe3\x85\x82";
2689 } elseif ( $code < 0xc544 ) {
2690 return "\xe3\x85\x85";
2691 } elseif ( $code < 0xc790 ) {
2692 return "\xe3\x85\x87";
2693 } elseif ( $code < 0xcc28 ) {
2694 return "\xe3\x85\x88";
2695 } elseif ( $code < 0xce74 ) {
2696 return "\xe3\x85\x8a";
2697 } elseif ( $code < 0xd0c0 ) {
2698 return "\xe3\x85\x8b";
2699 } elseif ( $code < 0xd30c ) {
2700 return "\xe3\x85\x8c";
2701 } elseif ( $code < 0xd558 ) {
2702 return "\xe3\x85\x8d";
2704 return "\xe3\x85\x8e";
2711 function initEncoding() {
2712 # Some languages may have an alternate char encoding option
2713 # (Esperanto X-coding, Japanese furigana conversion, etc)
2714 # If this language is used as the primary content language,
2715 # an override to the defaults can be set here on startup.
2722 function recodeForEdit( $s ) {
2723 # For some languages we'll want to explicitly specify
2724 # which characters make it into the edit box raw
2725 # or are converted in some way or another.
2726 global $wgEditEncoding;
2727 if ( $wgEditEncoding == '' ||
$wgEditEncoding == 'UTF-8' ) {
2730 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2738 function recodeInput( $s ) {
2739 # Take the previous into account.
2740 global $wgEditEncoding;
2741 if ( $wgEditEncoding != '' ) {
2742 $enc = $wgEditEncoding;
2746 if ( $enc == 'UTF-8' ) {
2749 return $this->iconv( $enc, 'UTF-8', $s );
2754 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2755 * also cleans up certain backwards-compatible sequences, converting them
2756 * to the modern Unicode equivalent.
2758 * This is language-specific for performance reasons only.
2764 function normalize( $s ) {
2765 global $wgAllUnicodeFixes;
2766 $s = UtfNormal
::cleanUp( $s );
2767 if ( $wgAllUnicodeFixes ) {
2768 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2769 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2776 * Transform a string using serialized data stored in the given file (which
2777 * must be in the serialized subdirectory of $IP). The file contains pairs
2778 * mapping source characters to destination characters.
2780 * The data is cached in process memory. This will go faster if you have the
2781 * FastStringSearch extension.
2783 * @param $file string
2784 * @param $string string
2786 * @throws MWException
2789 function transformUsingPairFile( $file, $string ) {
2790 if ( !isset( $this->transformData
[$file] ) ) {
2791 $data = wfGetPrecompiledData( $file );
2792 if ( $data === false ) {
2793 throw new MWException( __METHOD__
. ": The transformation file $file is missing" );
2795 $this->transformData
[$file] = new ReplacementArray( $data );
2797 return $this->transformData
[$file]->replace( $string );
2801 * For right-to-left language support
2806 return self
::$dataCache->getItem( $this->mCode
, 'rtl' );
2810 * Return the correct HTML 'dir' attribute value for this language.
2814 return $this->isRTL() ?
'rtl' : 'ltr';
2818 * Return 'left' or 'right' as appropriate alignment for line-start
2819 * for this language's text direction.
2821 * Should be equivalent to CSS3 'start' text-align value....
2825 function alignStart() {
2826 return $this->isRTL() ?
'right' : 'left';
2830 * Return 'right' or 'left' as appropriate alignment for line-end
2831 * for this language's text direction.
2833 * Should be equivalent to CSS3 'end' text-align value....
2837 function alignEnd() {
2838 return $this->isRTL() ?
'left' : 'right';
2842 * A hidden direction mark (LRM or RLM), depending on the language direction.
2843 * Unlike getDirMark(), this function returns the character as an HTML entity.
2844 * This function should be used when the output is guaranteed to be HTML,
2845 * because it makes the output HTML source code more readable. When
2846 * the output is plain text or can be escaped, getDirMark() should be used.
2848 * @param $opposite Boolean Get the direction mark opposite to your language
2852 function getDirMarkEntity( $opposite = false ) {
2853 if ( $opposite ) { return $this->isRTL() ?
'‎' : '‏'; }
2854 return $this->isRTL() ?
'‏' : '‎';
2858 * A hidden direction mark (LRM or RLM), depending on the language direction.
2859 * This function produces them as invisible Unicode characters and
2860 * the output may be hard to read and debug, so it should only be used
2861 * when the output is plain text or can be escaped. When the output is
2862 * HTML, use getDirMarkEntity() instead.
2864 * @param $opposite Boolean Get the direction mark opposite to your language
2867 function getDirMark( $opposite = false ) {
2868 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
2869 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
2870 if ( $opposite ) { return $this->isRTL() ?
$lrm : $rlm; }
2871 return $this->isRTL() ?
$rlm : $lrm;
2877 function capitalizeAllNouns() {
2878 return self
::$dataCache->getItem( $this->mCode
, 'capitalizeAllNouns' );
2882 * An arrow, depending on the language direction.
2884 * @param $direction String: the direction of the arrow: forwards (default), backwards, left, right, up, down.
2887 function getArrow( $direction = 'forwards' ) {
2888 switch ( $direction ) {
2890 return $this->isRTL() ?
'←' : '→';
2892 return $this->isRTL() ?
'→' : '←';
2905 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
2909 function linkPrefixExtension() {
2910 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixExtension' );
2916 function getMagicWords() {
2917 return self
::$dataCache->getItem( $this->mCode
, 'magicWords' );
2920 protected function doMagicHook() {
2921 if ( $this->mMagicHookDone
) {
2924 $this->mMagicHookDone
= true;
2925 wfProfileIn( 'LanguageGetMagic' );
2926 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions
, $this->getCode() ) );
2927 wfProfileOut( 'LanguageGetMagic' );
2931 * Fill a MagicWord object with data from here
2935 function getMagic( $mw ) {
2936 $this->doMagicHook();
2938 if ( isset( $this->mMagicExtensions
[$mw->mId
] ) ) {
2939 $rawEntry = $this->mMagicExtensions
[$mw->mId
];
2941 $magicWords = $this->getMagicWords();
2942 if ( isset( $magicWords[$mw->mId
] ) ) {
2943 $rawEntry = $magicWords[$mw->mId
];
2949 if ( !is_array( $rawEntry ) ) {
2950 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
2952 $mw->mCaseSensitive
= $rawEntry[0];
2953 $mw->mSynonyms
= array_slice( $rawEntry, 1 );
2958 * Add magic words to the extension array
2960 * @param $newWords array
2962 function addMagicWordsByLang( $newWords ) {
2963 $fallbackChain = $this->getFallbackLanguages();
2964 $fallbackChain = array_reverse( $fallbackChain );
2965 foreach ( $fallbackChain as $code ) {
2966 if ( isset( $newWords[$code] ) ) {
2967 $this->mMagicExtensions
= $newWords[$code] +
$this->mMagicExtensions
;
2973 * Get special page names, as an associative array
2974 * case folded alias => real name
2976 function getSpecialPageAliases() {
2977 // Cache aliases because it may be slow to load them
2978 if ( is_null( $this->mExtendedSpecialPageAliases
) ) {
2980 $this->mExtendedSpecialPageAliases
=
2981 self
::$dataCache->getItem( $this->mCode
, 'specialPageAliases' );
2982 wfRunHooks( 'LanguageGetSpecialPageAliases',
2983 array( &$this->mExtendedSpecialPageAliases
, $this->getCode() ) );
2986 return $this->mExtendedSpecialPageAliases
;
2990 * Italic is unsuitable for some languages
2992 * @param $text String: the text to be emphasized.
2995 function emphasize( $text ) {
2996 return "<em>$text</em>";
3000 * Normally we output all numbers in plain en_US style, that is
3001 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3002 * point twohundredthirtyfive. However this is not suitable for all
3003 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
3004 * Icelandic just want to use commas instead of dots, and dots instead
3005 * of commas like "293.291,235".
3007 * An example of this function being called:
3009 * wfMessage( 'message' )->numParams( $num )->text()
3012 * See LanguageGu.php for the Gujarati implementation and
3013 * $separatorTransformTable on MessageIs.php for
3014 * the , => . and . => , implementation.
3016 * @todo check if it's viable to use localeconv() for the decimal
3018 * @param $number Mixed: the string to be formatted, should be an integer
3019 * or a floating point number.
3020 * @param $nocommafy Bool: set to true for special numbers like dates
3023 public function formatNum( $number, $nocommafy = false ) {
3024 global $wgTranslateNumerals;
3025 if ( !$nocommafy ) {
3026 $number = $this->commafy( $number );
3027 $s = $this->separatorTransformTable();
3029 $number = strtr( $number, $s );
3033 if ( $wgTranslateNumerals ) {
3034 $s = $this->digitTransformTable();
3036 $number = strtr( $number, $s );
3044 * @param $number string
3047 function parseFormattedNumber( $number ) {
3048 $s = $this->digitTransformTable();
3050 $number = strtr( $number, array_flip( $s ) );
3053 $s = $this->separatorTransformTable();
3055 $number = strtr( $number, array_flip( $s ) );
3058 $number = strtr( $number, array( ',' => '' ) );
3063 * Adds commas to a given number
3068 function commafy( $_ ) {
3069 $digitGroupingPattern = $this->digitGroupingPattern();
3070 if ( $_ === null ) {
3074 if ( !$digitGroupingPattern ||
$digitGroupingPattern === "###,###,###" ) {
3075 // default grouping is at thousands, use the same for ###,###,### pattern too.
3076 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $_ ) ) );
3078 // Ref: http://cldr.unicode.org/translation/number-patterns
3080 if ( intval( $_ ) < 0 ) {
3081 // For negative numbers apply the algorithm like positive number and add sign.
3083 $_ = substr( $_, 1 );
3085 $numberpart = array();
3086 $decimalpart = array();
3087 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3088 preg_match( "/\d+/", $_, $numberpart );
3089 preg_match( "/\.\d*/", $_, $decimalpart );
3090 $groupedNumber = ( count( $decimalpart ) > 0 ) ?
$decimalpart[0]:"";
3091 if ( $groupedNumber === $_ ) {
3092 // the string does not have any number part. Eg: .12345
3093 return $sign . $groupedNumber;
3095 $start = $end = strlen( $numberpart[0] );
3096 while ( $start > 0 ) {
3097 $match = $matches[0][$numMatches -1] ;
3098 $matchLen = strlen( $match );
3099 $start = $end - $matchLen;
3103 $groupedNumber = substr( $_ , $start, $end -$start ) . $groupedNumber ;
3105 if ( $numMatches > 1 ) {
3106 // use the last pattern for the rest of the number
3110 $groupedNumber = "," . $groupedNumber;
3113 return $sign . $groupedNumber;
3119 function digitGroupingPattern() {
3120 return self
::$dataCache->getItem( $this->mCode
, 'digitGroupingPattern' );
3126 function digitTransformTable() {
3127 return self
::$dataCache->getItem( $this->mCode
, 'digitTransformTable' );
3133 function separatorTransformTable() {
3134 return self
::$dataCache->getItem( $this->mCode
, 'separatorTransformTable' );
3138 * Take a list of strings and build a locale-friendly comma-separated
3139 * list, using the local comma-separator message.
3140 * The last two strings are chained with an "and".
3141 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3146 function listToText( array $l ) {
3148 $m = count( $l ) - 1;
3152 } elseif ( $m === 1 ) {
3153 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
3155 for ( $i = $m; $i >= 0; $i-- ) {
3158 } elseif ( $i == $m - 1 ) {
3159 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
3161 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
3169 * Take a list of strings and build a locale-friendly comma-separated
3170 * list, using the local comma-separator message.
3171 * @param $list array of strings to put in a comma list
3174 function commaList( array $list ) {
3176 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3182 * Take a list of strings and build a locale-friendly semicolon-separated
3183 * list, using the local semicolon-separator message.
3184 * @param $list array of strings to put in a semicolon list
3187 function semicolonList( array $list ) {
3189 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3195 * Same as commaList, but separate it with the pipe instead.
3196 * @param $list array of strings to put in a pipe list
3199 function pipeList( array $list ) {
3201 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3207 * Truncate a string to a specified length in bytes, appending an optional
3208 * string (e.g. for ellipses)
3210 * The database offers limited byte lengths for some columns in the database;
3211 * multi-byte character sets mean we need to ensure that only whole characters
3212 * are included, otherwise broken characters can be passed to the user
3214 * If $length is negative, the string will be truncated from the beginning
3216 * @param $string String to truncate
3217 * @param $length Int: maximum length (including ellipses)
3218 * @param $ellipsis String to append to the truncated text
3219 * @param $adjustLength Boolean: Subtract length of ellipsis from $length.
3220 * $adjustLength was introduced in 1.18, before that behaved as if false.
3223 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3224 # Use the localized ellipsis character
3225 if ( $ellipsis == '...' ) {
3226 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3228 # Check if there is no need to truncate
3229 if ( $length == 0 ) {
3230 return $ellipsis; // convention
3231 } elseif ( strlen( $string ) <= abs( $length ) ) {
3232 return $string; // no need to truncate
3234 $stringOriginal = $string;
3235 # If ellipsis length is >= $length then we can't apply $adjustLength
3236 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3237 $string = $ellipsis; // this can be slightly unexpected
3238 # Otherwise, truncate and add ellipsis...
3240 $eLength = $adjustLength ?
strlen( $ellipsis ) : 0;
3241 if ( $length > 0 ) {
3242 $length -= $eLength;
3243 $string = substr( $string, 0, $length ); // xyz...
3244 $string = $this->removeBadCharLast( $string );
3245 $string = $string . $ellipsis;
3247 $length +
= $eLength;
3248 $string = substr( $string, $length ); // ...xyz
3249 $string = $this->removeBadCharFirst( $string );
3250 $string = $ellipsis . $string;
3253 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3254 # This check is *not* redundant if $adjustLength, due to the single case where
3255 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3256 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3259 return $stringOriginal;
3264 * Remove bytes that represent an incomplete Unicode character
3265 * at the end of string (e.g. bytes of the char are missing)
3267 * @param $string String
3270 protected function removeBadCharLast( $string ) {
3271 if ( $string != '' ) {
3272 $char = ord( $string[strlen( $string ) - 1] );
3274 if ( $char >= 0xc0 ) {
3275 # We got the first byte only of a multibyte char; remove it.
3276 $string = substr( $string, 0, -1 );
3277 } elseif ( $char >= 0x80 &&
3278 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3279 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) )
3281 # We chopped in the middle of a character; remove it
3289 * Remove bytes that represent an incomplete Unicode character
3290 * at the start of string (e.g. bytes of the char are missing)
3292 * @param $string String
3295 protected function removeBadCharFirst( $string ) {
3296 if ( $string != '' ) {
3297 $char = ord( $string[0] );
3298 if ( $char >= 0x80 && $char < 0xc0 ) {
3299 # We chopped in the middle of a character; remove the whole thing
3300 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3307 * Truncate a string of valid HTML to a specified length in bytes,
3308 * appending an optional string (e.g. for ellipses), and return valid HTML
3310 * This is only intended for styled/linked text, such as HTML with
3311 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3312 * Also, this will not detect things like "display:none" CSS.
3314 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3316 * @param string $text HTML string to truncate
3317 * @param int $length (zero/positive) Maximum length (including ellipses)
3318 * @param string $ellipsis String to append to the truncated text
3321 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3322 # Use the localized ellipsis character
3323 if ( $ellipsis == '...' ) {
3324 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3326 # Check if there is clearly no need to truncate
3327 if ( $length <= 0 ) {
3328 return $ellipsis; // no text shown, nothing to format (convention)
3329 } elseif ( strlen( $text ) <= $length ) {
3330 return $text; // string short enough even *with* HTML (short-circuit)
3333 $dispLen = 0; // innerHTML legth so far
3334 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3335 $tagType = 0; // 0-open, 1-close
3336 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3337 $entityState = 0; // 0-not entity, 1-entity
3338 $tag = $ret = ''; // accumulated tag name, accumulated result string
3339 $openTags = array(); // open tag stack
3340 $maybeState = null; // possible truncation state
3342 $textLen = strlen( $text );
3343 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3344 for ( $pos = 0; true; ++
$pos ) {
3345 # Consider truncation once the display length has reached the maximim.
3346 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3347 # Check that we're not in the middle of a bracket/entity...
3348 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3349 if ( !$testingEllipsis ) {
3350 $testingEllipsis = true;
3351 # Save where we are; we will truncate here unless there turn out to
3352 # be so few remaining characters that truncation is not necessary.
3353 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3354 $maybeState = array( $ret, $openTags ); // save state
3356 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3357 # String in fact does need truncation, the truncation point was OK.
3358 list( $ret, $openTags ) = $maybeState; // reload state
3359 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3360 $ret .= $ellipsis; // add ellipsis
3364 if ( $pos >= $textLen ) break; // extra iteration just for above checks
3366 # Read the next char...
3368 $lastCh = $pos ?
$text[$pos - 1] : '';
3369 $ret .= $ch; // add to result string
3371 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3372 $entityState = 0; // for bad HTML
3373 $bracketState = 1; // tag started (checking for backslash)
3374 } elseif ( $ch == '>' ) {
3375 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3376 $entityState = 0; // for bad HTML
3377 $bracketState = 0; // out of brackets
3378 } elseif ( $bracketState == 1 ) {
3380 $tagType = 1; // close tag (e.g. "</span>")
3382 $tagType = 0; // open tag (e.g. "<span>")
3385 $bracketState = 2; // building tag name
3386 } elseif ( $bracketState == 2 ) {
3390 // Name found (e.g. "<a href=..."), add on tag attributes...
3391 $pos +
= $this->truncate_skip( $ret, $text, "<>", $pos +
1 );
3393 } elseif ( $bracketState == 0 ) {
3394 if ( $entityState ) {
3397 $dispLen++
; // entity is one displayed char
3400 if ( $neLength == 0 && !$maybeState ) {
3401 // Save state without $ch. We want to *hit* the first
3402 // display char (to get tags) but not *use* it if truncating.
3403 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3406 $entityState = 1; // entity found, (e.g. " ")
3408 $dispLen++
; // this char is displayed
3409 // Add the next $max display text chars after this in one swoop...
3410 $max = ( $testingEllipsis ?
$length : $neLength ) - $dispLen;
3411 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos +
1, $max );
3412 $dispLen +
= $skipped;
3418 // Close the last tag if left unclosed by bad HTML
3419 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3420 while ( count( $openTags ) > 0 ) {
3421 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3427 * truncateHtml() helper function
3428 * like strcspn() but adds the skipped chars to $ret
3437 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3438 if ( $len === null ) {
3439 $len = -1; // -1 means "no limit" for strcspn
3440 } elseif ( $len < 0 ) {
3444 if ( $start < strlen( $text ) ) {
3445 $skipCount = strcspn( $text, $search, $start, $len );
3446 $ret .= substr( $text, $start, $skipCount );
3452 * truncateHtml() helper function
3453 * (a) push or pop $tag from $openTags as needed
3454 * (b) clear $tag value
3455 * @param &$tag string Current HTML tag name we are looking at
3456 * @param $tagType int (0-open tag, 1-close tag)
3457 * @param $lastCh string Character before the '>' that ended this tag
3458 * @param &$openTags array Open tag stack (not accounting for $tag)
3460 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3461 $tag = ltrim( $tag );
3463 if ( $tagType == 0 && $lastCh != '/' ) {
3464 $openTags[] = $tag; // tag opened (didn't close itself)
3465 } elseif ( $tagType == 1 ) {
3466 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3467 array_pop( $openTags ); // tag closed
3475 * Grammatical transformations, needed for inflected languages
3476 * Invoked by putting {{grammar:case|word}} in a message
3478 * @param $word string
3479 * @param $case string
3482 function convertGrammar( $word, $case ) {
3483 global $wgGrammarForms;
3484 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3485 return $wgGrammarForms[$this->getCode()][$case][$word];
3490 * Get the grammar forms for the content language
3491 * @return array of grammar forms
3494 function getGrammarForms() {
3495 global $wgGrammarForms;
3496 if ( isset( $wgGrammarForms[$this->getCode()] ) && is_array( $wgGrammarForms[$this->getCode()] ) ) {
3497 return $wgGrammarForms[$this->getCode()];
3502 * Provides an alternative text depending on specified gender.
3503 * Usage {{gender:username|masculine|feminine|neutral}}.
3504 * username is optional, in which case the gender of current user is used,
3505 * but only in (some) interface messages; otherwise default gender is used.
3507 * If no forms are given, an empty string is returned. If only one form is
3508 * given, it will be returned unconditionally. These details are implied by
3509 * the caller and cannot be overridden in subclasses.
3511 * If more than one form is given, the default is to use the neutral one
3512 * if it is specified, and to use the masculine one otherwise. These
3513 * details can be overridden in subclasses.
3515 * @param $gender string
3516 * @param $forms array
3520 function gender( $gender, $forms ) {
3521 if ( !count( $forms ) ) {
3524 $forms = $this->preConvertPlural( $forms, 2 );
3525 if ( $gender === 'male' ) {
3528 if ( $gender === 'female' ) {
3531 return isset( $forms[2] ) ?
$forms[2] : $forms[0];
3535 * Plural form transformations, needed for some languages.
3536 * For example, there are 3 form of plural in Russian and Polish,
3537 * depending on "count mod 10". See [[w:Plural]]
3538 * For English it is pretty simple.
3540 * Invoked by putting {{plural:count|wordform1|wordform2}}
3541 * or {{plural:count|wordform1|wordform2|wordform3}}
3543 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3545 * @param $count Integer: non-localized number
3546 * @param $forms Array: different plural forms
3547 * @return string Correct form of plural for $count in this language
3549 function convertPlural( $count, $forms ) {
3550 if ( !count( $forms ) ) {
3554 // Handle explicit 0= and 1= forms
3555 foreach ( $forms as $index => $form ) {
3556 if ( isset( $form[1] ) && $form[1] === '=' ) {
3557 if ( $form[0] === (string) $count ) {
3558 return substr( $form, 2 );
3560 unset( $forms[$index] );
3563 $forms = array_values( $forms );
3565 $pluralForm = $this->getPluralForm( $count );
3566 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3567 return $forms[$pluralForm];
3571 * Checks that convertPlural was given an array and pads it to requested
3572 * amount of forms by copying the last one.
3574 * @param $count Integer: How many forms should there be at least
3575 * @param $forms Array of forms given to convertPlural
3576 * @return array Padded array of forms or an exception if not an array
3578 protected function preConvertPlural( /* Array */ $forms, $count ) {
3579 while ( count( $forms ) < $count ) {
3580 $forms[] = $forms[count( $forms ) - 1];
3586 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
3587 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3588 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3589 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3592 * @param $str String: the validated block duration in English
3593 * @return string Somehow translated block duration
3594 * @see LanguageFi.php for example implementation
3596 function translateBlockExpiry( $str ) {
3597 $duration = SpecialBlock
::getSuggestedDurations( $this );
3598 foreach ( $duration as $show => $value ) {
3599 if ( strcmp( $str, $value ) == 0 ) {
3600 return htmlspecialchars( trim( $show ) );
3604 // Since usually only infinite or indefinite is only on list, so try
3605 // equivalents if still here.
3606 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3607 if ( in_array( $str, $indefs ) ) {
3608 foreach ( $indefs as $val ) {
3609 $show = array_search( $val, $duration, true );
3610 if ( $show !== false ) {
3611 return htmlspecialchars( trim( $show ) );
3615 // If all else fails, return the original string.
3620 * languages like Chinese need to be segmented in order for the diff
3623 * @param $text String
3626 public function segmentForDiff( $text ) {
3631 * and unsegment to show the result
3633 * @param $text String
3636 public function unsegmentForDiff( $text ) {
3641 * Return the LanguageConverter used in the Language
3644 * @return LanguageConverter
3646 public function getConverter() {
3647 return $this->mConverter
;
3651 * convert text to all supported variants
3653 * @param $text string
3656 public function autoConvertToAllVariants( $text ) {
3657 return $this->mConverter
->autoConvertToAllVariants( $text );
3661 * convert text to different variants of a language.
3663 * @param $text string
3666 public function convert( $text ) {
3667 return $this->mConverter
->convert( $text );
3671 * Convert a Title object to a string in the preferred variant
3673 * @param $title Title
3676 public function convertTitle( $title ) {
3677 return $this->mConverter
->convertTitle( $title );
3681 * Check if this is a language with variants
3685 public function hasVariants() {
3686 return sizeof( $this->getVariants() ) > 1;
3690 * Check if the language has the specific variant
3693 * @param $variant string
3696 public function hasVariant( $variant ) {
3697 return (bool)$this->mConverter
->validateVariant( $variant );
3701 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3703 * @param $text string
3706 public function armourMath( $text ) {
3707 return $this->mConverter
->armourMath( $text );
3711 * Perform output conversion on a string, and encode for safe HTML output.
3712 * @param $text String text to be converted
3713 * @param $isTitle Bool whether this conversion is for the article title
3715 * @todo this should get integrated somewhere sane
3717 public function convertHtml( $text, $isTitle = false ) {
3718 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3722 * @param $key string
3725 public function convertCategoryKey( $key ) {
3726 return $this->mConverter
->convertCategoryKey( $key );
3730 * Get the list of variants supported by this language
3731 * see sample implementation in LanguageZh.php
3733 * @return array an array of language codes
3735 public function getVariants() {
3736 return $this->mConverter
->getVariants();
3742 public function getPreferredVariant() {
3743 return $this->mConverter
->getPreferredVariant();
3749 public function getDefaultVariant() {
3750 return $this->mConverter
->getDefaultVariant();
3756 public function getURLVariant() {
3757 return $this->mConverter
->getURLVariant();
3761 * If a language supports multiple variants, it is
3762 * possible that non-existing link in one variant
3763 * actually exists in another variant. this function
3764 * tries to find it. See e.g. LanguageZh.php
3766 * @param $link String: the name of the link
3767 * @param $nt Mixed: the title object of the link
3768 * @param $ignoreOtherCond Boolean: to disable other conditions when
3769 * we need to transclude a template or update a category's link
3770 * @return null the input parameters may be modified upon return
3772 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
3773 $this->mConverter
->findVariantLink( $link, $nt, $ignoreOtherCond );
3777 * If a language supports multiple variants, converts text
3778 * into an array of all possible variants of the text:
3779 * 'variant' => text in that variant
3781 * @deprecated since 1.17 Use autoConvertToAllVariants()
3783 * @param $text string
3787 public function convertLinkToAllVariants( $text ) {
3788 return $this->mConverter
->convertLinkToAllVariants( $text );
3792 * returns language specific options used by User::getPageRenderHash()
3793 * for example, the preferred language variant
3797 function getExtraHashOptions() {
3798 return $this->mConverter
->getExtraHashOptions();
3802 * For languages that support multiple variants, the title of an
3803 * article may be displayed differently in different variants. this
3804 * function returns the apporiate title defined in the body of the article.
3808 public function getParsedTitle() {
3809 return $this->mConverter
->getParsedTitle();
3813 * Enclose a string with the "no conversion" tag. This is used by
3814 * various functions in the Parser
3816 * @param $text String: text to be tagged for no conversion
3817 * @param $noParse bool
3818 * @return string the tagged text
3820 public function markNoConversion( $text, $noParse = false ) {
3821 return $this->mConverter
->markNoConversion( $text, $noParse );
3825 * A regular expression to match legal word-trailing characters
3826 * which should be merged onto a link of the form [[foo]]bar.
3830 public function linkTrail() {
3831 return self
::$dataCache->getItem( $this->mCode
, 'linkTrail' );
3837 function getLangObj() {
3842 * Get the RFC 3066 code for this language object
3844 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3845 * htmlspecialchars() or similar
3849 public function getCode() {
3850 return $this->mCode
;
3854 * Get the code in Bcp47 format which we can use
3855 * inside of html lang="" tags.
3857 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3858 * htmlspecialchars() or similar.
3863 public function getHtmlCode() {
3864 if ( is_null( $this->mHtmlCode
) ) {
3865 $this->mHtmlCode
= wfBCP47( $this->getCode() );
3867 return $this->mHtmlCode
;
3871 * @param $code string
3873 public function setCode( $code ) {
3874 $this->mCode
= $code;
3875 // Ensure we don't leave an incorrect html code lying around
3876 $this->mHtmlCode
= null;
3880 * Get the name of a file for a certain language code
3881 * @param $prefix string Prepend this to the filename
3882 * @param $code string Language code
3883 * @param $suffix string Append this to the filename
3884 * @throws MWException
3885 * @return string $prefix . $mangledCode . $suffix
3887 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
3888 // Protect against path traversal
3889 if ( !Language
::isValidCode( $code )
3890 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
3892 throw new MWException( "Invalid language code \"$code\"" );
3895 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
3899 * Get the language code from a file name. Inverse of getFileName()
3900 * @param $filename string $prefix . $languageCode . $suffix
3901 * @param $prefix string Prefix before the language code
3902 * @param $suffix string Suffix after the language code
3903 * @return string Language code, or false if $prefix or $suffix isn't found
3905 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
3907 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
3908 preg_quote( $suffix, '/' ) . '/', $filename, $m );
3909 if ( !count( $m ) ) {
3912 return str_replace( '_', '-', strtolower( $m[1] ) );
3916 * @param $code string
3919 public static function getMessagesFileName( $code ) {
3921 $file = self
::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
3922 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
3927 * @param $code string
3930 public static function getClassFileName( $code ) {
3932 return self
::getFileName( "$IP/languages/classes/Language", $code, '.php' );
3936 * Get the first fallback for a given language.
3938 * @param $code string
3940 * @return bool|string
3942 public static function getFallbackFor( $code ) {
3943 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3946 $fallbacks = self
::getFallbacksFor( $code );
3947 $first = array_shift( $fallbacks );
3953 * Get the ordered list of fallback languages.
3956 * @param $code string Language code
3959 public static function getFallbacksFor( $code ) {
3960 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3963 $v = self
::getLocalisationCache()->getItem( $code, 'fallback' );
3964 $v = array_map( 'trim', explode( ',', $v ) );
3965 if ( $v[count( $v ) - 1] !== 'en' ) {
3973 * Get all messages for a given language
3974 * WARNING: this may take a long time. If you just need all message *keys*
3975 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
3977 * @param $code string
3981 public static function getMessagesFor( $code ) {
3982 return self
::getLocalisationCache()->getItem( $code, 'messages' );
3986 * Get a message for a given language
3988 * @param $key string
3989 * @param $code string
3993 public static function getMessageFor( $key, $code ) {
3994 return self
::getLocalisationCache()->getSubitem( $code, 'messages', $key );
3998 * Get all message keys for a given language. This is a faster alternative to
3999 * array_keys( Language::getMessagesFor( $code ) )
4002 * @param $code string Language code
4003 * @return array of message keys (strings)
4005 public static function getMessageKeysFor( $code ) {
4006 return self
::getLocalisationCache()->getSubItemList( $code, 'messages' );
4013 function fixVariableInNamespace( $talk ) {
4014 if ( strpos( $talk, '$1' ) === false ) {
4018 global $wgMetaNamespace;
4019 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4021 # Allow grammar transformations
4022 # Allowing full message-style parsing would make simple requests
4023 # such as action=raw much more expensive than they need to be.
4024 # This will hopefully cover most cases.
4025 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4026 array( &$this, 'replaceGrammarInNamespace' ), $talk );
4027 return str_replace( ' ', '_', $talk );
4034 function replaceGrammarInNamespace( $m ) {
4035 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4039 * @throws MWException
4042 static function getCaseMaps() {
4043 static $wikiUpperChars, $wikiLowerChars;
4044 if ( isset( $wikiUpperChars ) ) {
4045 return array( $wikiUpperChars, $wikiLowerChars );
4048 wfProfileIn( __METHOD__
);
4049 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4050 if ( $arr === false ) {
4051 throw new MWException(
4052 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4054 $wikiUpperChars = $arr['wikiUpperChars'];
4055 $wikiLowerChars = $arr['wikiLowerChars'];
4056 wfProfileOut( __METHOD__
);
4057 return array( $wikiUpperChars, $wikiLowerChars );
4061 * Decode an expiry (block, protection, etc) which has come from the DB
4063 * @todo FIXME: why are we returnings DBMS-dependent strings???
4065 * @param $expiry String: Database expiry String
4066 * @param $format Bool|Int true to process using language functions, or TS_ constant
4067 * to return the expiry in a given timestamp
4071 public function formatExpiry( $expiry, $format = true ) {
4072 static $infinity, $infinityMsg;
4073 if ( $infinity === null ) {
4074 $infinityMsg = wfMessage( 'infiniteblock' );
4075 $infinity = wfGetDB( DB_SLAVE
)->getInfinity();
4078 if ( $expiry == '' ||
$expiry == $infinity ) {
4079 return $format === true
4083 return $format === true
4084 ?
$this->timeanddate( $expiry, /* User preference timezone */ true )
4085 : wfTimestamp( $format, $expiry );
4091 * @param $seconds int|float
4092 * @param $format Array Optional
4093 * If $format['avoid'] == 'avoidseconds' - don't mention seconds if $seconds >= 1 hour
4094 * If $format['avoid'] == 'avoidminutes' - don't mention seconds/minutes if $seconds > 48 hours
4095 * If $format['noabbrevs'] is true - use 'seconds' and friends instead of 'seconds-abbrev' and friends
4096 * For backwards compatibility, $format may also be one of the strings 'avoidseconds' or 'avoidminutes'
4099 function formatTimePeriod( $seconds, $format = array() ) {
4100 if ( !is_array( $format ) ) {
4101 $format = array( 'avoid' => $format ); // For backwards compatibility
4103 if ( !isset( $format['avoid'] ) ) {
4104 $format['avoid'] = false;
4106 if ( !isset( $format['noabbrevs' ] ) ) {
4107 $format['noabbrevs'] = false;
4109 $secondsMsg = wfMessage(
4110 $format['noabbrevs'] ?
'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4111 $minutesMsg = wfMessage(
4112 $format['noabbrevs'] ?
'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4113 $hoursMsg = wfMessage(
4114 $format['noabbrevs'] ?
'hours' : 'hours-abbrev' )->inLanguage( $this );
4115 $daysMsg = wfMessage(
4116 $format['noabbrevs'] ?
'days' : 'days-abbrev' )->inLanguage( $this );
4118 if ( round( $seconds * 10 ) < 100 ) {
4119 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4120 $s = $secondsMsg->params( $s )->text();
4121 } elseif ( round( $seconds ) < 60 ) {
4122 $s = $this->formatNum( round( $seconds ) );
4123 $s = $secondsMsg->params( $s )->text();
4124 } elseif ( round( $seconds ) < 3600 ) {
4125 $minutes = floor( $seconds / 60 );
4126 $secondsPart = round( fmod( $seconds, 60 ) );
4127 if ( $secondsPart == 60 ) {
4131 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4133 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4134 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4135 $hours = floor( $seconds / 3600 );
4136 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4137 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4138 if ( $secondsPart == 60 ) {
4142 if ( $minutes == 60 ) {
4146 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4148 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4149 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4150 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4153 $days = floor( $seconds / 86400 );
4154 if ( $format['avoid'] === 'avoidminutes' ) {
4155 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4156 if ( $hours == 24 ) {
4160 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4162 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4163 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4164 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4165 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4166 if ( $minutes == 60 ) {
4170 if ( $hours == 24 ) {
4174 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4176 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4178 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4180 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4182 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4189 * Format a bitrate for output, using an appropriate
4190 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to the magnitude in question
4192 * This use base 1000. For base 1024 use formatSize(), for another base
4193 * see formatComputingNumbers()
4198 function formatBitrate( $bps ) {
4199 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4203 * @param $size int Size of the unit
4204 * @param $boundary int Size boundary (1000, or 1024 in most cases)
4205 * @param $messageKey string Message key to be uesd
4208 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4210 return str_replace( '$1', $this->formatNum( $size ),
4211 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4214 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4217 $maxIndex = count( $sizes ) - 1;
4218 while ( $size >= $boundary && $index < $maxIndex ) {
4223 // For small sizes no decimal places necessary
4226 // For MB and bigger two decimal places are smarter
4229 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4231 $size = round( $size, $round );
4232 $text = $this->getMessageFromDB( $msg );
4233 return str_replace( '$1', $this->formatNum( $size ), $text );
4237 * Format a size in bytes for output, using an appropriate
4238 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4240 * This method use base 1024. For base 1000 use formatBitrate(), for
4241 * another base see formatComputingNumbers()
4243 * @param $size int Size to format
4244 * @return string Plain text (not HTML)
4246 function formatSize( $size ) {
4247 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4251 * Make a list item, used by various special pages
4253 * @param $page String Page link
4254 * @param $details String Text between brackets
4255 * @param $oppositedm Boolean Add the direction mark opposite to your
4256 * language, to display text properly
4259 function specialList( $page, $details, $oppositedm = true ) {
4260 $dirmark = ( $oppositedm ?
$this->getDirMark( true ) : '' ) .
4261 $this->getDirMark();
4262 $details = $details ?
$dirmark . $this->getMessageFromDB( 'word-separator' ) .
4263 wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : '';
4264 return $page . $details;
4268 * Generate (prev x| next x) (20|50|100...) type links for paging
4270 * @param $title Title object to link
4271 * @param $offset Integer offset parameter
4272 * @param $limit Integer limit parameter
4273 * @param $query array|String optional URL query parameter string
4274 * @param $atend Bool optional param for specified if this is the last page
4277 public function viewPrevNext( Title
$title, $offset, $limit, array $query = array(), $atend = false ) {
4278 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4280 # Make 'previous' link
4281 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4282 if ( $offset > 0 ) {
4283 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4284 $query, $prev, 'prevn-title', 'mw-prevlink' );
4286 $plink = htmlspecialchars( $prev );
4290 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4292 $nlink = htmlspecialchars( $next );
4294 $nlink = $this->numLink( $title, $offset +
$limit, $limit,
4295 $query, $next, 'prevn-title', 'mw-nextlink' );
4298 # Make links to set number of items per page
4299 $numLinks = array();
4300 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4301 $numLinks[] = $this->numLink( $title, $offset, $num,
4302 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4305 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4306 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4310 * Helper function for viewPrevNext() that generates links
4312 * @param $title Title object to link
4313 * @param $offset Integer offset parameter
4314 * @param $limit Integer limit parameter
4315 * @param $query Array extra query parameters
4316 * @param $link String text to use for the link; will be escaped
4317 * @param $tooltipMsg String name of the message to use as tooltip
4318 * @param $class String value of the "class" attribute of the link
4319 * @return String HTML fragment
4321 private function numLink( Title
$title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) {
4322 $query = array( 'limit' => $limit, 'offset' => $offset ) +
$query;
4323 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4324 return Html
::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4325 'title' => $tooltip, 'class' => $class ), $link );
4329 * Get the conversion rule title, if any.
4333 public function getConvRuleTitle() {
4334 return $this->mConverter
->getConvRuleTitle();
4338 * Get the compiled plural rules for the language
4340 * @return array Associative array with plural form, and plural rule as key-value pairs
4342 public function getCompiledPluralRules() {
4343 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'compiledPluralRules' );
4344 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4345 if ( !$pluralRules ) {
4346 foreach ( $fallbacks as $fallbackCode ) {
4347 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4348 if ( $pluralRules ) {
4353 return $pluralRules;
4357 * Get the plural rules for the language
4359 * @return array Associative array with plural form, and plural rule as key-value pairs
4361 public function getPluralRules() {
4362 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'pluralRules' );
4363 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4364 if ( !$pluralRules ) {
4365 foreach ( $fallbacks as $fallbackCode ) {
4366 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4367 if ( $pluralRules ) {
4372 return $pluralRules;
4376 * Find the plural form matching to the given number
4377 * It return the form index.
4378 * @return int The index of the plural form
4380 private function getPluralForm( $number ) {
4381 $pluralRules = $this->getCompiledPluralRules();
4382 $form = CLDRPluralRuleEvaluator
::evaluateCompiled( $number, $pluralRules );