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";
33 if ( function_exists( 'mb_strtoupper' ) ) {
34 mb_internal_encoding( 'UTF-8' );
38 * Internationalisation code
43 * @var LanguageConverter
47 public $mVariants, $mCode, $mLoaded = false;
48 public $mMagicExtensions = array(), $mMagicHookDone = false;
49 private $mHtmlCode = null, $mParentLanguage = false;
51 public $dateFormatStrings = array();
52 public $mExtendedSpecialPageAliases;
54 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
57 * ReplacementArray object caches
59 public $transformData = array();
62 * @var LocalisationCache
64 static public $dataCache;
66 static public $mLangObjCache = array();
68 static public $mWeekdayMsgs = array(
69 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
73 static public $mWeekdayAbbrevMsgs = array(
74 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
77 static public $mMonthMsgs = array(
78 'january', 'february', 'march', 'april', 'may_long', 'june',
79 'july', 'august', 'september', 'october', 'november',
82 static public $mMonthGenMsgs = array(
83 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
84 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
87 static public $mMonthAbbrevMsgs = array(
88 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
89 'sep', 'oct', 'nov', 'dec'
92 static public $mIranianCalendarMonthMsgs = array(
93 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
94 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
95 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
96 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
99 static public $mHebrewCalendarMonthMsgs = array(
100 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
101 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
102 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
103 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
104 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
107 static public $mHebrewCalendarMonthGenMsgs = array(
108 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
109 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
110 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
111 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
112 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
115 static public $mHijriCalendarMonthMsgs = array(
116 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
117 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
118 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
119 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
126 static public $durationIntervals = array(
127 'millennia' => 31556952000,
128 'centuries' => 3155695200,
129 'decades' => 315569520,
130 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
139 * Cache for language fallbacks.
140 * @see Language::getFallbacksIncludingSiteLanguage
144 static private $fallbackLanguageCache = array();
147 * Get a cached or new language object for a given language code
148 * @param string $code
151 static function factory( $code ) {
152 global $wgDummyLanguageCodes, $wgLangObjCacheSize;
154 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
155 $code = $wgDummyLanguageCodes[$code];
158 // get the language object to process
159 $langObj = isset( self
::$mLangObjCache[$code] )
160 ? self
::$mLangObjCache[$code]
161 : self
::newFromCode( $code );
163 // merge the language object in to get it up front in the cache
164 self
::$mLangObjCache = array_merge( array( $code => $langObj ), self
::$mLangObjCache );
165 // get rid of the oldest ones in case we have an overflow
166 self
::$mLangObjCache = array_slice( self
::$mLangObjCache, 0, $wgLangObjCacheSize, true );
172 * Create a language object for a given language code
173 * @param string $code
174 * @throws MWException
177 protected static function newFromCode( $code ) {
178 // Protect against path traversal below
179 if ( !Language
::isValidCode( $code )
180 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code )
182 throw new MWException( "Invalid language code \"$code\"" );
185 if ( !Language
::isValidBuiltInCode( $code ) ) {
186 // It's not possible to customise this code with class files, so
187 // just return a Language object. This is to support uselang= hacks.
188 $lang = new Language
;
189 $lang->setCode( $code );
193 // Check if there is a language class for the code
194 $class = self
::classFromCode( $code );
195 self
::preloadLanguageClass( $class );
196 if ( class_exists( $class ) ) {
201 // Keep trying the fallback list until we find an existing class
202 $fallbacks = Language
::getFallbacksFor( $code );
203 foreach ( $fallbacks as $fallbackCode ) {
204 if ( !Language
::isValidBuiltInCode( $fallbackCode ) ) {
205 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
208 $class = self
::classFromCode( $fallbackCode );
209 self
::preloadLanguageClass( $class );
210 if ( class_exists( $class ) ) {
211 $lang = Language
::newFromCode( $fallbackCode );
212 $lang->setCode( $code );
217 throw new MWException( "Invalid fallback sequence for language '$code'" );
221 * Checks whether any localisation is available for that language tag
222 * in MediaWiki (MessagesXx.php exists).
224 * @param string $code Language tag (in lower case)
225 * @return bool Whether language is supported
228 public static function isSupportedLanguage( $code ) {
229 return self
::isValidBuiltInCode( $code )
230 && ( is_readable( self
::getMessagesFileName( $code ) )
231 ||
is_readable( self
::getJsonMessagesFileName( $code ) )
236 * Returns true if a language code string is a well-formed language tag
237 * according to RFC 5646.
238 * This function only checks well-formedness; it doesn't check that
239 * language, script or variant codes actually exist in the repositories.
241 * Based on regexes by Mark Davis of the Unicode Consortium:
242 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
244 * @param string $code
245 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
250 public static function isWellFormedLanguageTag( $code, $lenient = false ) {
253 $alphanum = '[a-z0-9]';
254 $x = 'x'; # private use singleton
255 $singleton = '[a-wy-z]'; # other singleton
256 $s = $lenient ?
'[-_]' : '-';
258 $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
259 $script = "$alpha{4}"; # ISO 15924
260 $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
261 $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
262 $extension = "$singleton(?:$s$alphanum{2,8})+";
263 $privateUse = "$x(?:$s$alphanum{1,8})+";
265 # Define certain grandfathered codes, since otherwise the regex is pretty useless.
266 # Since these are limited, this is safe even later changes to the registry --
267 # the only oddity is that it might change the type of the tag, and thus
268 # the results from the capturing groups.
269 # http://www.iana.org/assignments/language-subtag-registry
271 $grandfathered = "en{$s}GB{$s}oed"
272 . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
273 . "|no{$s}(?:bok|nyn)"
274 . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
275 . "|zh{$s}min{$s}nan";
277 $variantList = "$variant(?:$s$variant)*";
278 $extensionList = "$extension(?:$s$extension)*";
280 $langtag = "(?:($language)"
283 . "(?:$s$variantList)?"
284 . "(?:$s$extensionList)?"
285 . "(?:$s$privateUse)?)";
287 # The final breakdown, with capturing groups for each of these components
288 # The variants, extensions, grandfathered, and private-use may have interior '-'
290 $root = "^(?:$langtag|$privateUse|$grandfathered)$";
292 return (bool)preg_match( "/$root/", strtolower( $code ) );
296 * Returns true if a language code string is of a valid form, whether or
297 * not it exists. This includes codes which are used solely for
298 * customisation via the MediaWiki namespace.
300 * @param string $code
304 public static function isValidCode( $code ) {
305 static $cache = array();
306 if ( isset( $cache[$code] ) ) {
307 return $cache[$code];
309 // People think language codes are html safe, so enforce it.
310 // Ideally we should only allow a-zA-Z0-9-
311 // but, .+ and other chars are often used for {{int:}} hacks
312 // see bugs 37564, 37587, 36938
314 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
315 && !preg_match( Title
::getTitleInvalidRegex(), $code );
317 return $cache[$code];
321 * Returns true if a language code is of a valid form for the purposes of
322 * internal customisation of MediaWiki, via Messages*.php or *.json.
324 * @param string $code
326 * @throws MWException
330 public static function isValidBuiltInCode( $code ) {
332 if ( !is_string( $code ) ) {
333 if ( is_object( $code ) ) {
334 $addmsg = " of class " . get_class( $code );
338 $type = gettype( $code );
339 throw new MWException( __METHOD__
. " must be passed a string, $type given$addmsg" );
342 return (bool)preg_match( '/^[a-z0-9-]{2,}$/i', $code );
346 * Returns true if a language code is an IETF tag known to MediaWiki.
348 * @param string $code
353 public static function isKnownLanguageTag( $tag ) {
354 static $coreLanguageNames;
356 // Quick escape for invalid input to avoid exceptions down the line
357 // when code tries to process tags which are not valid at all.
358 if ( !self
::isValidBuiltInCode( $tag ) ) {
362 if ( $coreLanguageNames === null ) {
364 include "$IP/languages/Names.php";
367 if ( isset( $coreLanguageNames[$tag] )
368 || self
::fetchLanguageName( $tag, $tag ) !== ''
377 * @param string $code
378 * @return string Name of the language class
380 public static function classFromCode( $code ) {
381 if ( $code == 'en' ) {
384 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
389 * Includes language class files
391 * @param string $class Name of the language class
393 public static function preloadLanguageClass( $class ) {
396 if ( $class === 'Language' ) {
400 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
401 include_once "$IP/languages/classes/$class.php";
406 * Get the LocalisationCache instance
408 * @return LocalisationCache
410 public static function getLocalisationCache() {
411 if ( is_null( self
::$dataCache ) ) {
412 global $wgLocalisationCacheConf;
413 $class = $wgLocalisationCacheConf['class'];
414 self
::$dataCache = new $class( $wgLocalisationCacheConf );
416 return self
::$dataCache;
419 function __construct() {
420 $this->mConverter
= new FakeConverter( $this );
421 // Set the code to the name of the descendant
422 if ( get_class( $this ) == 'Language' ) {
425 $this->mCode
= str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
427 self
::getLocalisationCache();
431 * Reduce memory usage
433 function __destruct() {
434 foreach ( $this as $name => $value ) {
435 unset( $this->$name );
440 * Hook which will be called if this is the content language.
441 * Descendants can use this to register hook functions or modify globals
443 function initContLang() {
447 * Same as getFallbacksFor for current language.
449 * @deprecated since 1.19
451 function getFallbackLanguageCode() {
452 wfDeprecated( __METHOD__
, '1.19' );
454 return self
::getFallbackFor( $this->mCode
);
461 function getFallbackLanguages() {
462 return self
::getFallbacksFor( $this->mCode
);
466 * Exports $wgBookstoreListEn
469 function getBookstoreList() {
470 return self
::$dataCache->getItem( $this->mCode
, 'bookstoreList' );
474 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
475 * available in localised form, it will be included in English.
479 public function getNamespaces() {
480 if ( is_null( $this->namespaceNames
) ) {
481 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
483 $this->namespaceNames
= self
::$dataCache->getItem( $this->mCode
, 'namespaceNames' );
484 $validNamespaces = MWNamespace
::getCanonicalNamespaces();
486 $this->namespaceNames
= $wgExtraNamespaces +
$this->namespaceNames +
$validNamespaces;
488 $this->namespaceNames
[NS_PROJECT
] = $wgMetaNamespace;
489 if ( $wgMetaNamespaceTalk ) {
490 $this->namespaceNames
[NS_PROJECT_TALK
] = $wgMetaNamespaceTalk;
492 $talk = $this->namespaceNames
[NS_PROJECT_TALK
];
493 $this->namespaceNames
[NS_PROJECT_TALK
] =
494 $this->fixVariableInNamespace( $talk );
497 # Sometimes a language will be localised but not actually exist on this wiki.
498 foreach ( $this->namespaceNames
as $key => $text ) {
499 if ( !isset( $validNamespaces[$key] ) ) {
500 unset( $this->namespaceNames
[$key] );
504 # The above mixing may leave namespaces out of canonical order.
505 # Re-order by namespace ID number...
506 ksort( $this->namespaceNames
);
508 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames
) );
511 return $this->namespaceNames
;
515 * Arbitrarily set all of the namespace names at once. Mainly used for testing
516 * @param array $namespaces Array of namespaces (id => name)
518 public function setNamespaces( array $namespaces ) {
519 $this->namespaceNames
= $namespaces;
520 $this->mNamespaceIds
= null;
524 * Resets all of the namespace caches. Mainly used for testing
526 public function resetNamespaces() {
527 $this->namespaceNames
= null;
528 $this->mNamespaceIds
= null;
529 $this->namespaceAliases
= null;
533 * A convenience function that returns the same thing as
534 * getNamespaces() except with the array values changed to ' '
535 * where it found '_', useful for producing output to be displayed
536 * e.g. in <select> forms.
540 function getFormattedNamespaces() {
541 $ns = $this->getNamespaces();
542 foreach ( $ns as $k => $v ) {
543 $ns[$k] = strtr( $v, '_', ' ' );
549 * Get a namespace value by key
551 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
552 * echo $mw_ns; // prints 'MediaWiki'
555 * @param int $index The array key of the namespace to return
556 * @return string|bool String if the namespace value exists, otherwise false
558 function getNsText( $index ) {
559 $ns = $this->getNamespaces();
561 return isset( $ns[$index] ) ?
$ns[$index] : false;
565 * A convenience function that returns the same thing as
566 * getNsText() except with '_' changed to ' ', useful for
570 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
571 * echo $mw_ns; // prints 'MediaWiki talk'
574 * @param int $index The array key of the namespace to return
575 * @return string Namespace name without underscores (empty string if namespace does not exist)
577 function getFormattedNsText( $index ) {
578 $ns = $this->getNsText( $index );
580 return strtr( $ns, '_', ' ' );
584 * Returns gender-dependent namespace alias if available.
585 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
586 * @param int $index Namespace index
587 * @param string $gender Gender key (male, female... )
591 function getGenderNsText( $index, $gender ) {
592 global $wgExtraGenderNamespaces;
594 $ns = $wgExtraGenderNamespaces +
595 self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
597 return isset( $ns[$index][$gender] ) ?
$ns[$index][$gender] : $this->getNsText( $index );
601 * Whether this language uses gender-dependent namespace aliases.
602 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
606 function needsGenderDistinction() {
607 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
608 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
609 // $wgExtraGenderNamespaces overrides everything
611 } elseif ( isset( $wgExtraNamespaces[NS_USER
] ) && isset( $wgExtraNamespaces[NS_USER_TALK
] ) ) {
612 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
613 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
616 // Check what is in i18n files
617 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
618 return count( $aliases ) > 0;
623 * Get a namespace key by value, case insensitive.
624 * Only matches namespace names for the current language, not the
625 * canonical ones defined in Namespace.php.
627 * @param string $text
628 * @return int|bool An integer if $text is a valid value otherwise false
630 function getLocalNsIndex( $text ) {
631 $lctext = $this->lc( $text );
632 $ids = $this->getNamespaceIds();
633 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
639 function getNamespaceAliases() {
640 if ( is_null( $this->namespaceAliases
) ) {
641 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceAliases' );
645 foreach ( $aliases as $name => $index ) {
646 if ( $index === NS_PROJECT_TALK
) {
647 unset( $aliases[$name] );
648 $name = $this->fixVariableInNamespace( $name );
649 $aliases[$name] = $index;
654 global $wgExtraGenderNamespaces;
655 $genders = $wgExtraGenderNamespaces +
656 (array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
657 foreach ( $genders as $index => $forms ) {
658 foreach ( $forms as $alias ) {
659 $aliases[$alias] = $index;
663 # Also add converted namespace names as aliases, to avoid confusion.
664 $convertedNames = array();
665 foreach ( $this->getVariants() as $variant ) {
666 if ( $variant === $this->mCode
) {
669 foreach ( $this->getNamespaces() as $ns => $_ ) {
670 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
674 $this->namespaceAliases
= $aliases +
$convertedNames;
677 return $this->namespaceAliases
;
683 function getNamespaceIds() {
684 if ( is_null( $this->mNamespaceIds
) ) {
685 global $wgNamespaceAliases;
686 # Put namespace names and aliases into a hashtable.
687 # If this is too slow, then we should arrange it so that it is done
688 # before caching. The catch is that at pre-cache time, the above
689 # class-specific fixup hasn't been done.
690 $this->mNamespaceIds
= array();
691 foreach ( $this->getNamespaces() as $index => $name ) {
692 $this->mNamespaceIds
[$this->lc( $name )] = $index;
694 foreach ( $this->getNamespaceAliases() as $name => $index ) {
695 $this->mNamespaceIds
[$this->lc( $name )] = $index;
697 if ( $wgNamespaceAliases ) {
698 foreach ( $wgNamespaceAliases as $name => $index ) {
699 $this->mNamespaceIds
[$this->lc( $name )] = $index;
703 return $this->mNamespaceIds
;
707 * Get a namespace key by value, case insensitive. Canonical namespace
708 * names override custom ones defined for the current language.
710 * @param string $text
711 * @return int|bool An integer if $text is a valid value otherwise false
713 function getNsIndex( $text ) {
714 $lctext = $this->lc( $text );
715 $ns = MWNamespace
::getCanonicalIndex( $lctext );
716 if ( $ns !== null ) {
719 $ids = $this->getNamespaceIds();
720 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
724 * short names for language variants used for language conversion links.
726 * @param string $code
727 * @param bool $usemsg Use the "variantname-xyz" message if it exists
730 function getVariantname( $code, $usemsg = true ) {
731 $msg = "variantname-$code";
732 if ( $usemsg && wfMessage( $msg )->exists() ) {
733 return $this->getMessageFromDB( $msg );
735 $name = self
::fetchLanguageName( $code );
737 return $name; # if it's defined as a language name, show that
739 # otherwise, output the language code
745 * @param string $name
748 function specialPage( $name ) {
749 $aliases = $this->getSpecialPageAliases();
750 if ( isset( $aliases[$name][0] ) ) {
751 $name = $aliases[$name][0];
753 return $this->getNsText( NS_SPECIAL
) . ':' . $name;
759 function getDatePreferences() {
760 return self
::$dataCache->getItem( $this->mCode
, 'datePreferences' );
766 function getDateFormats() {
767 return self
::$dataCache->getItem( $this->mCode
, 'dateFormats' );
771 * @return array|string
773 function getDefaultDateFormat() {
774 $df = self
::$dataCache->getItem( $this->mCode
, 'defaultDateFormat' );
775 if ( $df === 'dmy or mdy' ) {
776 global $wgAmericanDates;
777 return $wgAmericanDates ?
'mdy' : 'dmy';
786 function getDatePreferenceMigrationMap() {
787 return self
::$dataCache->getItem( $this->mCode
, 'datePreferenceMigrationMap' );
791 * @param string $image
794 function getImageFile( $image ) {
795 return self
::$dataCache->getSubitem( $this->mCode
, 'imageFiles', $image );
801 function getExtraUserToggles() {
802 return (array)self
::$dataCache->getItem( $this->mCode
, 'extraUserToggles' );
809 function getUserToggle( $tog ) {
810 return $this->getMessageFromDB( "tog-$tog" );
814 * Get native language names, indexed by code.
815 * Only those defined in MediaWiki, no other data like CLDR.
816 * If $customisedOnly is true, only returns codes with a messages file
818 * @param bool $customisedOnly
821 * @deprecated since 1.20, use fetchLanguageNames()
823 public static function getLanguageNames( $customisedOnly = false ) {
824 return self
::fetchLanguageNames( null, $customisedOnly ?
'mwfile' : 'mw' );
828 * Get translated language names. This is done on best effort and
829 * by default this is exactly the same as Language::getLanguageNames.
830 * The CLDR extension provides translated names.
831 * @param string $code Language code.
832 * @return array Language code => language name
834 * @deprecated since 1.20, use fetchLanguageNames()
836 public static function getTranslatedLanguageNames( $code ) {
837 return self
::fetchLanguageNames( $code, 'all' );
841 * Get an array of language names, indexed by code.
842 * @param null|string $inLanguage Code of language in which to return the names
843 * Use null for autonyms (native names)
844 * @param string $include One of:
845 * 'all' all available languages
846 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
847 * 'mwfile' only if the language is in 'mw' *and* has a message file
848 * @return array Language code => language name
851 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
852 global $wgExtraLanguageNames;
853 static $coreLanguageNames;
855 if ( $coreLanguageNames === null ) {
857 include "$IP/languages/Names.php";
860 // If passed an invalid language code to use, fallback to en
861 if ( $inLanguage !== null && !Language
::isValidCode( $inLanguage ) ) {
868 # TODO: also include when $inLanguage is null, when this code is more efficient
869 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
872 $mwNames = $wgExtraLanguageNames +
$coreLanguageNames;
873 foreach ( $mwNames as $mwCode => $mwName ) {
874 # - Prefer own MediaWiki native name when not using the hook
875 # - For other names just add if not added through the hook
876 if ( $mwCode === $inLanguage ||
!isset( $names[$mwCode] ) ) {
877 $names[$mwCode] = $mwName;
881 if ( $include === 'all' ) {
886 $coreCodes = array_keys( $mwNames );
887 foreach ( $coreCodes as $coreCode ) {
888 $returnMw[$coreCode] = $names[$coreCode];
891 if ( $include === 'mwfile' ) {
892 $namesMwFile = array();
893 # We do this using a foreach over the codes instead of a directory
894 # loop so that messages files in extensions will work correctly.
895 foreach ( $returnMw as $code => $value ) {
896 if ( is_readable( self
::getMessagesFileName( $code ) )
897 ||
is_readable( self
::getJsonMessagesFileName( $code ) )
899 $namesMwFile[$code] = $names[$code];
906 # 'mw' option; default if it's not one of the other two options (all/mwfile)
911 * @param string $code The code of the language for which to get the name
912 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
913 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
914 * @return string Language name or empty
917 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
918 $code = strtolower( $code );
919 $array = self
::fetchLanguageNames( $inLanguage, $include );
920 return !array_key_exists( $code, $array ) ?
'' : $array[$code];
924 * Get a message from the MediaWiki namespace.
926 * @param string $msg Message name
929 function getMessageFromDB( $msg ) {
930 return wfMessage( $msg )->inLanguage( $this )->text();
934 * Get the native language name of $code.
935 * Only if defined in MediaWiki, no other data like CLDR.
936 * @param string $code
938 * @deprecated since 1.20, use fetchLanguageName()
940 function getLanguageName( $code ) {
941 return self
::fetchLanguageName( $code );
948 function getMonthName( $key ) {
949 return $this->getMessageFromDB( self
::$mMonthMsgs[$key - 1] );
955 function getMonthNamesArray() {
956 $monthNames = array( '' );
957 for ( $i = 1; $i < 13; $i++
) {
958 $monthNames[] = $this->getMonthName( $i );
967 function getMonthNameGen( $key ) {
968 return $this->getMessageFromDB( self
::$mMonthGenMsgs[$key - 1] );
975 function getMonthAbbreviation( $key ) {
976 return $this->getMessageFromDB( self
::$mMonthAbbrevMsgs[$key - 1] );
982 function getMonthAbbreviationsArray() {
983 $monthNames = array( '' );
984 for ( $i = 1; $i < 13; $i++
) {
985 $monthNames[] = $this->getMonthAbbreviation( $i );
994 function getWeekdayName( $key ) {
995 return $this->getMessageFromDB( self
::$mWeekdayMsgs[$key - 1] );
1002 function getWeekdayAbbreviation( $key ) {
1003 return $this->getMessageFromDB( self
::$mWeekdayAbbrevMsgs[$key - 1] );
1007 * @param string $key
1010 function getIranianCalendarMonthName( $key ) {
1011 return $this->getMessageFromDB( self
::$mIranianCalendarMonthMsgs[$key - 1] );
1015 * @param string $key
1018 function getHebrewCalendarMonthName( $key ) {
1019 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthMsgs[$key - 1] );
1023 * @param string $key
1026 function getHebrewCalendarMonthNameGen( $key ) {
1027 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1031 * @param string $key
1034 function getHijriCalendarMonthName( $key ) {
1035 return $this->getMessageFromDB( self
::$mHijriCalendarMonthMsgs[$key - 1] );
1039 * Pass through result from $dateTimeObj->format()
1041 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1042 if ( !$dateTimeObj ) {
1043 $dateTimeObj = DateTime
::createFromFormat(
1044 'YmdHis', $ts, $zone ?
: new DateTimeZone( 'UTC' )
1047 return $dateTimeObj->format( $code );
1051 * This is a workalike of PHP's date() function, but with better
1052 * internationalisation, a reduced set of format characters, and a better
1055 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1056 * the PHP manual for definitions. There are a number of extensions, which
1059 * xn Do not translate digits of the next numeric format character
1060 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
1061 * xr Use roman numerals for the next numeric format character
1062 * xh Use hebrew numerals for the next numeric format character
1064 * xg Genitive month name
1066 * xij j (day number) in Iranian calendar
1067 * xiF F (month name) in Iranian calendar
1068 * xin n (month number) in Iranian calendar
1069 * xiy y (two digit year) in Iranian calendar
1070 * xiY Y (full year) in Iranian calendar
1072 * xjj j (day number) in Hebrew calendar
1073 * xjF F (month name) in Hebrew calendar
1074 * xjt t (days in month) in Hebrew calendar
1075 * xjx xg (genitive month name) in Hebrew calendar
1076 * xjn n (month number) in Hebrew calendar
1077 * xjY Y (full year) in Hebrew calendar
1079 * xmj j (day number) in Hijri calendar
1080 * xmF F (month name) in Hijri calendar
1081 * xmn n (month number) in Hijri calendar
1082 * xmY Y (full year) in Hijri calendar
1084 * xkY Y (full year) in Thai solar calendar. Months and days are
1085 * identical to the Gregorian calendar
1086 * xoY Y (full year) in Minguo calendar or Juche year.
1087 * Months and days are identical to the
1088 * Gregorian calendar
1089 * xtY Y (full year) in Japanese nengo. Months and days are
1090 * identical to the Gregorian calendar
1092 * Characters enclosed in double quotes will be considered literal (with
1093 * the quotes themselves removed). Unmatched quotes will be considered
1094 * literal quotes. Example:
1096 * "The month is" F => The month is January
1099 * Backslash escaping is also supported.
1101 * Input timestamp is assumed to be pre-normalized to the desired local
1102 * time zone, if any. Note that the format characters crUeIOPTZ will assume
1103 * $ts is UTC if $zone is not given.
1105 * @param string $format
1106 * @param string $ts 14-character timestamp
1109 * @param DateTimeZone $zone Timezone of $ts
1110 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
1111 * Only makes sense if $ts is the current time.
1112 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1114 * @throws MWException
1117 function sprintfDate( $format, $ts, DateTimeZone
$zone = null, &$ttl = null ) {
1122 $dateTimeObj = false;
1131 $usedSecond = false;
1132 $usedMinute = false;
1139 $usedISOYear = false;
1140 $usedIsLeapYear = false;
1142 $usedHebrewMonth = false;
1143 $usedIranianMonth = false;
1144 $usedHijriMonth = false;
1145 $usedHebrewYear = false;
1146 $usedIranianYear = false;
1147 $usedHijriYear = false;
1148 $usedTennoYear = false;
1150 if ( strlen( $ts ) !== 14 ) {
1151 throw new MWException( __METHOD__
. ": The timestamp $ts should have 14 characters" );
1154 if ( !ctype_digit( $ts ) ) {
1155 throw new MWException( __METHOD__
. ": The timestamp $ts should be a number" );
1158 $formatLength = strlen( $format );
1159 for ( $p = 0; $p < $formatLength; $p++
) {
1161 $code = $format[$p];
1162 if ( $code == 'x' && $p < $formatLength - 1 ) {
1163 $code .= $format[++
$p];
1166 if ( ( $code === 'xi'
1172 && $p < $formatLength - 1 ) {
1173 $code .= $format[++
$p];
1184 $rawToggle = !$rawToggle;
1194 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1197 $usedHebrewMonth = true;
1199 $hebrew = self
::tsToHebrew( $ts );
1201 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1205 $num = substr( $ts, 6, 2 );
1209 $s .= $this->getWeekdayAbbreviation( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) +
1 );
1213 $num = intval( substr( $ts, 6, 2 ) );
1218 $iranian = self
::tsToIranian( $ts );
1225 $hijri = self
::tsToHijri( $ts );
1232 $hebrew = self
::tsToHebrew( $ts );
1238 $s .= $this->getWeekdayName( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) +
1 );
1242 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1245 $usedIranianMonth = true;
1247 $iranian = self
::tsToIranian( $ts );
1249 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1252 $usedHijriMonth = true;
1254 $hijri = self
::tsToHijri( $ts );
1256 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1259 $usedHebrewMonth = true;
1261 $hebrew = self
::tsToHebrew( $ts );
1263 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1267 $num = substr( $ts, 4, 2 );
1271 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1275 $num = intval( substr( $ts, 4, 2 ) );
1278 $usedIranianMonth = true;
1280 $iranian = self
::tsToIranian( $ts );
1285 $usedHijriMonth = true;
1287 $hijri = self
::tsToHijri ( $ts );
1292 $usedHebrewMonth = true;
1294 $hebrew = self
::tsToHebrew( $ts );
1299 $usedHebrewMonth = true;
1301 $hebrew = self
::tsToHebrew( $ts );
1307 $num = substr( $ts, 0, 4 );
1310 $usedIranianYear = true;
1312 $iranian = self
::tsToIranian( $ts );
1317 $usedHijriYear = true;
1319 $hijri = self
::tsToHijri( $ts );
1324 $usedHebrewYear = true;
1326 $hebrew = self
::tsToHebrew( $ts );
1333 $thai = self
::tsToYear( $ts, 'thai' );
1340 $minguo = self
::tsToYear( $ts, 'minguo' );
1345 $usedTennoYear = true;
1347 $tenno = self
::tsToYear( $ts, 'tenno' );
1353 $num = substr( $ts, 2, 2 );
1356 $usedIranianYear = true;
1358 $iranian = self
::tsToIranian( $ts );
1360 $num = substr( $iranian[0], -2 );
1364 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'am' : 'pm';
1368 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'AM' : 'PM';
1372 $h = substr( $ts, 8, 2 );
1373 $num = $h %
12 ?
$h %
12 : 12;
1377 $num = intval( substr( $ts, 8, 2 ) );
1381 $h = substr( $ts, 8, 2 );
1382 $num = sprintf( '%02d', $h %
12 ?
$h %
12 : 12 );
1386 $num = substr( $ts, 8, 2 );
1390 $num = substr( $ts, 10, 2 );
1394 $num = substr( $ts, 12, 2 );
1404 $s .= Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1410 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1414 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1418 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1421 $usedIsLeapYear = true;
1422 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1425 $usedISOYear = true;
1426 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1433 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1436 # Backslash escaping
1437 if ( $p < $formatLength - 1 ) {
1438 $s .= $format[++
$p];
1445 if ( $p < $formatLength - 1 ) {
1446 $endQuote = strpos( $format, '"', $p +
1 );
1447 if ( $endQuote === false ) {
1448 # No terminating quote, assume literal "
1451 $s .= substr( $format, $p +
1, $endQuote - $p - 1 );
1455 # Quote at end of string, assume literal "
1462 if ( $num !== false ) {
1463 if ( $rawToggle ||
$raw ) {
1466 } elseif ( $roman ) {
1467 $s .= Language
::romanNumeral( $num );
1469 } elseif ( $hebrewNum ) {
1470 $s .= self
::hebrewNumeral( $num );
1473 $s .= $this->formatNum( $num, true );
1478 if ( $usedSecond ) {
1480 } elseif ( $usedMinute ) {
1481 $ttl = 60 - substr( $ts, 12, 2 );
1482 } elseif ( $usedHour ) {
1483 $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1484 } elseif ( $usedAMPM ) {
1485 $ttl = 43200 - ( substr( $ts, 8, 2 ) %
12 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1486 } elseif ( $usedDay ||
$usedHebrewMonth ||
$usedIranianMonth ||
$usedHijriMonth ||
$usedHebrewYear ||
$usedIranianYear ||
$usedHijriYear ||
$usedTennoYear ) {
1487 // @todo Someone who understands the non-Gregorian calendars should write proper logic for them
1488 // so that they don't need purged every day.
1489 $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1491 $possibleTtls = array();
1492 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1494 $possibleTtls[] = ( 7 - Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
$timeRemainingInDay;
1495 } elseif ( $usedISOYear ) {
1496 // December 28th falls on the last ISO week of the year, every year.
1497 // The last ISO week of a year can be 52 or 53.
1498 $lastWeekOfISOYear = DateTime
::createFromFormat( 'Ymd', substr( $ts, 0, 4 ) . '1228', $zone ?
: new DateTimeZone( 'UTC' ) )->format( 'W' );
1499 $currentISOWeek = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1500 $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1501 $timeRemainingInWeek = ( 7 - Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
$timeRemainingInDay;
1502 $possibleTtls[] = $weeksRemaining * 604800 +
$timeRemainingInWeek;
1506 $possibleTtls[] = ( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) - substr( $ts, 6, 2 ) ) * 86400 +
$timeRemainingInDay;
1507 } elseif ( $usedYear ) {
1508 $possibleTtls[] = ( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) +
364 - Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1509 +
$timeRemainingInDay;
1510 } elseif ( $usedIsLeapYear ) {
1511 $year = substr( $ts, 0, 4 );
1512 $timeRemainingInYear = ( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) +
364 - Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1513 +
$timeRemainingInDay;
1515 if ( $mod ||
( !( $year %
100 ) && $year %
400 ) ) {
1516 // this isn't a leap year. see when the next one starts
1517 $nextCandidate = $year - $mod +
4;
1518 if ( $nextCandidate %
100 ||
!( $nextCandidate %
400 ) ) {
1519 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
$timeRemainingInYear;
1521 $possibleTtls[] = ( $nextCandidate - $year +
3 ) * 365 * 86400 +
$timeRemainingInYear;
1524 // this is a leap year, so the next year isn't
1525 $possibleTtls[] = $timeRemainingInYear;
1529 if ( $possibleTtls ) {
1530 $ttl = min( $possibleTtls );
1537 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1538 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1541 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1542 * Gregorian dates to Iranian dates. Originally written in C, it
1543 * is released under the terms of GNU Lesser General Public
1544 * License. Conversion to PHP was performed by Niklas Laxström.
1546 * Link: http://www.farsiweb.info/jalali/jalali.c
1552 private static function tsToIranian( $ts ) {
1553 $gy = substr( $ts, 0, 4 ) -1600;
1554 $gm = substr( $ts, 4, 2 ) -1;
1555 $gd = substr( $ts, 6, 2 ) -1;
1557 # Days passed from the beginning (including leap years)
1559 +
floor( ( $gy +
3 ) / 4 )
1560 - floor( ( $gy +
99 ) / 100 )
1561 +
floor( ( $gy +
399 ) / 400 );
1563 // Add days of the past months of this year
1564 for ( $i = 0; $i < $gm; $i++
) {
1565 $gDayNo +
= self
::$GREG_DAYS[$i];
1569 if ( $gm > 1 && ( ( $gy %
4 === 0 && $gy %
100 !== 0 ||
( $gy %
400 == 0 ) ) ) ) {
1573 // Days passed in current month
1574 $gDayNo +
= (int)$gd;
1576 $jDayNo = $gDayNo - 79;
1578 $jNp = floor( $jDayNo / 12053 );
1581 $jy = 979 +
33 * $jNp +
4 * floor( $jDayNo / 1461 );
1584 if ( $jDayNo >= 366 ) {
1585 $jy +
= floor( ( $jDayNo - 1 ) / 365 );
1586 $jDayNo = floor( ( $jDayNo - 1 ) %
365 );
1589 for ( $i = 0; $i < 11 && $jDayNo >= self
::$IRANIAN_DAYS[$i]; $i++
) {
1590 $jDayNo -= self
::$IRANIAN_DAYS[$i];
1596 return array( $jy, $jm, $jd );
1600 * Converting Gregorian dates to Hijri dates.
1602 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1604 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1610 private static function tsToHijri( $ts ) {
1611 $year = substr( $ts, 0, 4 );
1612 $month = substr( $ts, 4, 2 );
1613 $day = substr( $ts, 6, 2 );
1621 ( $zy > 1582 ) ||
( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1622 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1624 $zjd = (int)( ( 1461 * ( $zy +
4800 +
(int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1625 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1626 (int)( ( 3 * (int)( ( ( $zy +
4900 +
(int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1629 $zjd = 367 * $zy - (int)( ( 7 * ( $zy +
5001 +
(int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1630 (int)( ( 275 * $zm ) / 9 ) +
$zd +
1729777;
1633 $zl = $zjd -1948440 +
10632;
1634 $zn = (int)( ( $zl - 1 ) / 10631 );
1635 $zl = $zl - 10631 * $zn +
354;
1636 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1637 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1638 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1639 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) +
29;
1640 $zm = (int)( ( 24 * $zl ) / 709 );
1641 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1642 $zy = 30 * $zn +
$zj - 30;
1644 return array( $zy, $zm, $zd );
1648 * Converting Gregorian dates to Hebrew dates.
1650 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1651 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1652 * to translate the relevant functions into PHP and release them under
1655 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1656 * and Adar II is 14. In a non-leap year, Adar is 6.
1662 private static function tsToHebrew( $ts ) {
1664 $year = substr( $ts, 0, 4 );
1665 $month = substr( $ts, 4, 2 );
1666 $day = substr( $ts, 6, 2 );
1668 # Calculate Hebrew year
1669 $hebrewYear = $year +
3760;
1671 # Month number when September = 1, August = 12
1673 if ( $month > 12 ) {
1680 # Calculate day of year from 1 September
1682 for ( $i = 1; $i < $month; $i++
) {
1686 # Check if the year is leap
1687 if ( $year %
400 == 0 ||
( $year %
4 == 0 && $year %
100 > 0 ) ) {
1690 } elseif ( $i == 8 ||
$i == 10 ||
$i == 1 ||
$i == 3 ) {
1697 # Calculate the start of the Hebrew year
1698 $start = self
::hebrewYearStart( $hebrewYear );
1700 # Calculate next year's start
1701 if ( $dayOfYear <= $start ) {
1702 # Day is before the start of the year - it is the previous year
1704 $nextStart = $start;
1708 # Add days since previous year's 1 September
1710 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1714 # Start of the new (previous) year
1715 $start = self
::hebrewYearStart( $hebrewYear );
1718 $nextStart = self
::hebrewYearStart( $hebrewYear +
1 );
1721 # Calculate Hebrew day of year
1722 $hebrewDayOfYear = $dayOfYear - $start;
1724 # Difference between year's days
1725 $diff = $nextStart - $start;
1726 # Add 12 (or 13 for leap years) days to ignore the difference between
1727 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1728 # difference is only about the year type
1729 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1735 # Check the year pattern, and is leap year
1736 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1737 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1738 # and non-leap years
1739 $yearPattern = $diff %
30;
1740 # Check if leap year
1741 $isLeap = $diff >= 30;
1743 # Calculate day in the month from number of day in the Hebrew year
1744 # Don't check Adar - if the day is not in Adar, we will stop before;
1745 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1746 $hebrewDay = $hebrewDayOfYear;
1749 while ( $hebrewMonth <= 12 ) {
1750 # Calculate days in this month
1751 if ( $isLeap && $hebrewMonth == 6 ) {
1752 # Adar in a leap year
1754 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1756 if ( $hebrewDay <= $days ) {
1760 # Subtract the days of Adar I
1761 $hebrewDay -= $days;
1764 if ( $hebrewDay <= $days ) {
1770 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1771 # Cheshvan in a complete year (otherwise as the rule below)
1773 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1774 # Kislev in an incomplete year (otherwise as the rule below)
1777 # Odd months have 30 days, even have 29
1778 $days = 30 - ( $hebrewMonth - 1 ) %
2;
1780 if ( $hebrewDay <= $days ) {
1781 # In the current month
1784 # Subtract the days of the current month
1785 $hebrewDay -= $days;
1786 # Try in the next month
1791 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1795 * This calculates the Hebrew year start, as days since 1 September.
1796 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1797 * Used for Hebrew date.
1803 private static function hebrewYearStart( $year ) {
1804 $a = intval( ( 12 * ( $year - 1 ) +
17 ) %
19 );
1805 $b = intval( ( $year - 1 ) %
4 );
1806 $m = 32.044093161144 +
1.5542417966212 * $a +
$b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1810 $Mar = intval( $m );
1816 $c = intval( ( $Mar +
3 * ( $year - 1 ) +
5 * $b +
5 ) %
7 );
1817 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1819 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1821 } elseif ( $c == 2 ||
$c == 4 ||
$c == 6 ) {
1825 $Mar +
= intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1830 * Algorithm to convert Gregorian dates to Thai solar dates,
1831 * Minguo dates or Minguo dates.
1833 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1834 * http://en.wikipedia.org/wiki/Minguo_calendar
1835 * http://en.wikipedia.org/wiki/Japanese_era_name
1837 * @param string $ts 14-character timestamp
1838 * @param string $cName Calender name
1839 * @return array Converted year, month, day
1841 private static function tsToYear( $ts, $cName ) {
1842 $gy = substr( $ts, 0, 4 );
1843 $gm = substr( $ts, 4, 2 );
1844 $gd = substr( $ts, 6, 2 );
1846 if ( !strcmp( $cName, 'thai' ) ) {
1848 # Add 543 years to the Gregorian calendar
1849 # Months and days are identical
1850 $gy_offset = $gy +
543;
1851 } elseif ( ( !strcmp( $cName, 'minguo' ) ) ||
!strcmp( $cName, 'juche' ) ) {
1853 # Deduct 1911 years from the Gregorian calendar
1854 # Months and days are identical
1855 $gy_offset = $gy - 1911;
1856 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1857 # Nengō dates up to Meiji period
1858 # Deduct years from the Gregorian calendar
1859 # depending on the nengo periods
1860 # Months and days are identical
1862 ||
( ( $gy == 1912 ) && ( $gm < 7 ) )
1863 ||
( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1866 $gy_gannen = $gy - 1868 +
1;
1867 $gy_offset = $gy_gannen;
1868 if ( $gy_gannen == 1 ) {
1871 $gy_offset = '明治' . $gy_offset;
1873 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1874 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1875 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1876 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1877 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1880 $gy_gannen = $gy - 1912 +
1;
1881 $gy_offset = $gy_gannen;
1882 if ( $gy_gannen == 1 ) {
1885 $gy_offset = '大正' . $gy_offset;
1887 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1888 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1889 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1892 $gy_gannen = $gy - 1926 +
1;
1893 $gy_offset = $gy_gannen;
1894 if ( $gy_gannen == 1 ) {
1897 $gy_offset = '昭和' . $gy_offset;
1900 $gy_gannen = $gy - 1989 +
1;
1901 $gy_offset = $gy_gannen;
1902 if ( $gy_gannen == 1 ) {
1905 $gy_offset = '平成' . $gy_offset;
1911 return array( $gy_offset, $gm, $gd );
1915 * Roman number formatting up to 10000
1921 static function romanNumeral( $num ) {
1922 static $table = array(
1923 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1924 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1925 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1926 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1927 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
1930 $num = intval( $num );
1931 if ( $num > 10000 ||
$num <= 0 ) {
1936 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1937 if ( $num >= $pow10 ) {
1938 $s .= $table[$i][(int)floor( $num / $pow10 )];
1940 $num = $num %
$pow10;
1946 * Hebrew Gematria number formatting up to 9999
1952 static function hebrewNumeral( $num ) {
1953 static $table = array(
1954 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1955 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1956 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1957 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1960 $num = intval( $num );
1961 if ( $num > 9999 ||
$num <= 0 ) {
1966 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1967 if ( $num >= $pow10 ) {
1968 if ( $num == 15 ||
$num == 16 ) {
1969 $s .= $table[0][9] . $table[0][$num - 9];
1972 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1973 if ( $pow10 == 1000 ) {
1978 $num = $num %
$pow10;
1980 if ( strlen( $s ) == 2 ) {
1983 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1984 $str .= substr( $s, strlen( $s ) - 2, 2 );
1986 $start = substr( $str, 0, strlen( $str ) - 2 );
1987 $end = substr( $str, strlen( $str ) - 2 );
1990 $str = $start . 'ך';
1993 $str = $start . 'ם';
1996 $str = $start . 'ן';
1999 $str = $start . 'ף';
2002 $str = $start . 'ץ';
2009 * Used by date() and time() to adjust the time output.
2011 * @param string $ts The time in date('YmdHis') format
2012 * @param mixed $tz Adjust the time by this amount (default false, mean we
2013 * get user timecorrection setting)
2016 function userAdjust( $ts, $tz = false ) {
2017 global $wgUser, $wgLocalTZoffset;
2019 if ( $tz === false ) {
2020 $tz = $wgUser->getOption( 'timecorrection' );
2023 $data = explode( '|', $tz, 3 );
2025 if ( $data[0] == 'ZoneInfo' ) {
2026 wfSuppressWarnings();
2027 $userTZ = timezone_open( $data[2] );
2028 wfRestoreWarnings();
2029 if ( $userTZ !== false ) {
2030 $date = date_create( $ts, timezone_open( 'UTC' ) );
2031 date_timezone_set( $date, $userTZ );
2032 $date = date_format( $date, 'YmdHis' );
2035 # Unrecognized timezone, default to 'Offset' with the stored offset.
2036 $data[0] = 'Offset';
2039 if ( $data[0] == 'System' ||
$tz == '' ) {
2040 # Global offset in minutes.
2041 $minDiff = $wgLocalTZoffset;
2042 } elseif ( $data[0] == 'Offset' ) {
2043 $minDiff = intval( $data[1] );
2045 $data = explode( ':', $tz );
2046 if ( count( $data ) == 2 ) {
2047 $data[0] = intval( $data[0] );
2048 $data[1] = intval( $data[1] );
2049 $minDiff = abs( $data[0] ) * 60 +
$data[1];
2050 if ( $data[0] < 0 ) {
2051 $minDiff = -$minDiff;
2054 $minDiff = intval( $data[0] ) * 60;
2058 # No difference ? Return time unchanged
2059 if ( 0 == $minDiff ) {
2063 wfSuppressWarnings(); // E_STRICT system time bitching
2064 # Generate an adjusted date; take advantage of the fact that mktime
2065 # will normalize out-of-range values so we don't have to split $minDiff
2066 # into hours and minutes.
2068 (int)substr( $ts, 8, 2 ) ), # Hours
2069 (int)substr( $ts, 10, 2 ) +
$minDiff, # Minutes
2070 (int)substr( $ts, 12, 2 ), # Seconds
2071 (int)substr( $ts, 4, 2 ), # Month
2072 (int)substr( $ts, 6, 2 ), # Day
2073 (int)substr( $ts, 0, 4 ) ); # Year
2075 $date = date( 'YmdHis', $t );
2076 wfRestoreWarnings();
2082 * This is meant to be used by time(), date(), and timeanddate() to get
2083 * the date preference they're supposed to use, it should be used in
2087 * function timeanddate([...], $format = true) {
2088 * $datePreference = $this->dateFormat($format);
2093 * @param int|string|bool $usePrefs If true, the user's preference is used
2094 * if false, the site/language default is used
2095 * if int/string, assumed to be a format.
2098 function dateFormat( $usePrefs = true ) {
2101 if ( is_bool( $usePrefs ) ) {
2103 $datePreference = $wgUser->getDatePreference();
2105 $datePreference = (string)User
::getDefaultOption( 'date' );
2108 $datePreference = (string)$usePrefs;
2112 if ( $datePreference == '' ) {
2116 return $datePreference;
2120 * Get a format string for a given type and preference
2121 * @param string $type May be date, time or both
2122 * @param string $pref The format name as it appears in Messages*.php
2124 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2128 function getDateFormatString( $type, $pref ) {
2129 if ( !isset( $this->dateFormatStrings
[$type][$pref] ) ) {
2130 if ( $pref == 'default' ) {
2131 $pref = $this->getDefaultDateFormat();
2132 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
2134 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
2136 if ( $type === 'pretty' && $df === null ) {
2137 $df = $this->getDateFormatString( 'date', $pref );
2140 if ( $df === null ) {
2141 $pref = $this->getDefaultDateFormat();
2142 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
2145 $this->dateFormatStrings
[$type][$pref] = $df;
2147 return $this->dateFormatStrings
[$type][$pref];
2151 * @param string $ts The time format which needs to be turned into a
2152 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2153 * @param bool $adj Whether to adjust the time output according to the
2154 * user configured offset ($timecorrection)
2155 * @param mixed $format True to use user's date format preference
2156 * @param string|bool $timecorrection The time offset as returned by
2157 * validateTimeZone() in Special:Preferences
2160 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2161 $ts = wfTimestamp( TS_MW
, $ts );
2163 $ts = $this->userAdjust( $ts, $timecorrection );
2165 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2166 return $this->sprintfDate( $df, $ts );
2170 * @param string $ts The time format which needs to be turned into a
2171 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2172 * @param bool $adj Whether to adjust the time output according to the
2173 * user configured offset ($timecorrection)
2174 * @param mixed $format True to use user's date format preference
2175 * @param string|bool $timecorrection The time offset as returned by
2176 * validateTimeZone() in Special:Preferences
2179 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2180 $ts = wfTimestamp( TS_MW
, $ts );
2182 $ts = $this->userAdjust( $ts, $timecorrection );
2184 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2185 return $this->sprintfDate( $df, $ts );
2189 * @param string $ts The time format which needs to be turned into a
2190 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2191 * @param bool $adj Whether to adjust the time output according to the
2192 * user configured offset ($timecorrection)
2193 * @param mixed $format What format to return, if it's false output the
2194 * default one (default true)
2195 * @param string|bool $timecorrection The time offset as returned by
2196 * validateTimeZone() in Special:Preferences
2199 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2200 $ts = wfTimestamp( TS_MW
, $ts );
2202 $ts = $this->userAdjust( $ts, $timecorrection );
2204 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2205 return $this->sprintfDate( $df, $ts );
2209 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2213 * @param int $seconds The amount of seconds.
2214 * @param array $chosenIntervals The intervals to enable.
2218 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
2219 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2221 $segments = array();
2223 foreach ( $intervals as $intervalName => $intervalValue ) {
2224 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2225 // duration-years, duration-decades, duration-centuries, duration-millennia
2226 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2227 $segments[] = $message->inLanguage( $this )->escaped();
2230 return $this->listToText( $segments );
2234 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2235 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2239 * @param int $seconds The amount of seconds.
2240 * @param array $chosenIntervals The intervals to enable.
2244 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2245 if ( empty( $chosenIntervals ) ) {
2246 $chosenIntervals = array(
2258 $intervals = array_intersect_key( self
::$durationIntervals, array_flip( $chosenIntervals ) );
2259 $sortedNames = array_keys( $intervals );
2260 $smallestInterval = array_pop( $sortedNames );
2262 $segments = array();
2264 foreach ( $intervals as $name => $length ) {
2265 $value = floor( $seconds / $length );
2267 if ( $value > 0 ||
( $name == $smallestInterval && empty( $segments ) ) ) {
2268 $seconds -= $value * $length;
2269 $segments[$name] = $value;
2277 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2279 * @param string $type Can be 'date', 'time' or 'both'
2280 * @param string $ts The time format which needs to be turned into a
2281 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2282 * @param User $user User object used to get preferences for timezone and format
2283 * @param array $options Array, can contain the following keys:
2284 * - 'timecorrection': time correction, can have the following values:
2285 * - true: use user's preference
2286 * - false: don't use time correction
2287 * - int: value of time correction in minutes
2288 * - 'format': format to use, can have the following values:
2289 * - true: use user's preference
2290 * - false: use default preference
2291 * - string: format to use
2295 private function internalUserTimeAndDate( $type, $ts, User
$user, array $options ) {
2296 $ts = wfTimestamp( TS_MW
, $ts );
2297 $options +
= array( 'timecorrection' => true, 'format' => true );
2298 if ( $options['timecorrection'] !== false ) {
2299 if ( $options['timecorrection'] === true ) {
2300 $offset = $user->getOption( 'timecorrection' );
2302 $offset = $options['timecorrection'];
2304 $ts = $this->userAdjust( $ts, $offset );
2306 if ( $options['format'] === true ) {
2307 $format = $user->getDatePreference();
2309 $format = $options['format'];
2311 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2312 return $this->sprintfDate( $df, $ts );
2316 * Get the formatted date for the given timestamp and formatted for
2319 * @param mixed $ts Mixed: the time format which needs to be turned into a
2320 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2321 * @param User $user User object used to get preferences for timezone and format
2322 * @param array $options Array, can contain the following keys:
2323 * - 'timecorrection': time correction, can have the following values:
2324 * - true: use user's preference
2325 * - false: don't use time correction
2326 * - int: value of time correction in minutes
2327 * - 'format': format to use, can have the following values:
2328 * - true: use user's preference
2329 * - false: use default preference
2330 * - string: format to use
2334 public function userDate( $ts, User
$user, array $options = array() ) {
2335 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2339 * Get the formatted time for the given timestamp and formatted for
2342 * @param mixed $ts The time format which needs to be turned into a
2343 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2344 * @param User $user User object used to get preferences for timezone and format
2345 * @param array $options Array, can contain the following keys:
2346 * - 'timecorrection': time correction, can have the following values:
2347 * - true: use user's preference
2348 * - false: don't use time correction
2349 * - int: value of time correction in minutes
2350 * - 'format': format to use, can have the following values:
2351 * - true: use user's preference
2352 * - false: use default preference
2353 * - string: format to use
2357 public function userTime( $ts, User
$user, array $options = array() ) {
2358 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2362 * Get the formatted date and time for the given timestamp and formatted for
2365 * @param mixed $ts the time format which needs to be turned into a
2366 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2367 * @param User $user User object used to get preferences for timezone and format
2368 * @param array $options Array, can contain the following keys:
2369 * - 'timecorrection': time correction, can have the following values:
2370 * - true: use user's preference
2371 * - false: don't use time correction
2372 * - int: value of time correction in minutes
2373 * - 'format': format to use, can have the following values:
2374 * - true: use user's preference
2375 * - false: use default preference
2376 * - string: format to use
2380 public function userTimeAndDate( $ts, User
$user, array $options = array() ) {
2381 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2385 * Convert an MWTimestamp into a pretty human-readable timestamp using
2386 * the given user preferences and relative base time.
2388 * DO NOT USE THIS FUNCTION DIRECTLY. Instead, call MWTimestamp::getHumanTimestamp
2389 * on your timestamp object, which will then call this function. Calling
2390 * this function directly will cause hooks to be skipped over.
2392 * @see MWTimestamp::getHumanTimestamp
2393 * @param MWTimestamp $ts Timestamp to prettify
2394 * @param MWTimestamp $relativeTo Base timestamp
2395 * @param User $user User preferences to use
2396 * @return string Human timestamp
2399 public function getHumanTimestamp( MWTimestamp
$ts, MWTimestamp
$relativeTo, User
$user ) {
2400 $diff = $ts->diff( $relativeTo );
2401 $diffDay = (bool)( (int)$ts->timestamp
->format( 'w' ) -
2402 (int)$relativeTo->timestamp
->format( 'w' ) );
2403 $days = $diff->days ?
: (int)$diffDay;
2404 if ( $diff->invert ||
$days > 5
2405 && $ts->timestamp
->format( 'Y' ) !== $relativeTo->timestamp
->format( 'Y' )
2407 // Timestamps are in different years: use full timestamp
2408 // Also do full timestamp for future dates
2410 * @FIXME Add better handling of future timestamps.
2412 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?
: 'default' );
2413 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) );
2414 } elseif ( $days > 5 ) {
2415 // Timestamps are in same year, but more than 5 days ago: show day and month only.
2416 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?
: 'default' );
2417 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) );
2418 } elseif ( $days > 1 ) {
2419 // Timestamp within the past week: show the day of the week and time
2420 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?
: 'default' );
2421 $weekday = self
::$mWeekdayMsgs[$ts->timestamp
->format( 'w' )];
2423 // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2424 $ts = wfMessage( "$weekday-at" )
2425 ->inLanguage( $this )
2426 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) ) )
2428 } elseif ( $days == 1 ) {
2429 // Timestamp was yesterday: say 'yesterday' and the time.
2430 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?
: 'default' );
2431 $ts = wfMessage( 'yesterday-at' )
2432 ->inLanguage( $this )
2433 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) ) )
2435 } elseif ( $diff->h
> 1 ||
$diff->h
== 1 && $diff->i
> 30 ) {
2436 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2437 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?
: 'default' );
2438 $ts = wfMessage( 'today-at' )
2439 ->inLanguage( $this )
2440 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) ) )
2443 // From here on in, the timestamp was soon enough ago so that we can simply say
2444 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2445 } elseif ( $diff->h
== 1 ) {
2446 // Less than 90 minutes, but more than an hour ago.
2447 $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2448 } elseif ( $diff->i
>= 1 ) {
2449 // A few minutes ago.
2450 $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i
)->text();
2451 } elseif ( $diff->s
>= 30 ) {
2452 // Less than a minute, but more than 30 sec ago.
2453 $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s
)->text();
2455 // Less than 30 seconds ago.
2456 $ts = wfMessage( 'just-now' )->text();
2463 * @param string $key
2464 * @return array|null
2466 function getMessage( $key ) {
2467 return self
::$dataCache->getSubitem( $this->mCode
, 'messages', $key );
2473 function getAllMessages() {
2474 return self
::$dataCache->getItem( $this->mCode
, 'messages' );
2479 * @param string $out
2480 * @param string $string
2483 function iconv( $in, $out, $string ) {
2484 # This is a wrapper for iconv in all languages except esperanto,
2485 # which does some nasty x-conversions beforehand
2487 # Even with //IGNORE iconv can whine about illegal characters in
2488 # *input* string. We just ignore those too.
2489 # REF: http://bugs.php.net/bug.php?id=37166
2490 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2491 wfSuppressWarnings();
2492 $text = iconv( $in, $out . '//IGNORE', $string );
2493 wfRestoreWarnings();
2497 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2500 * @param array $matches
2501 * @return mixed|string
2503 function ucwordbreaksCallbackAscii( $matches ) {
2504 return $this->ucfirst( $matches[1] );
2508 * @param array $matches
2511 function ucwordbreaksCallbackMB( $matches ) {
2512 return mb_strtoupper( $matches[0] );
2516 * @param array $matches
2519 function ucCallback( $matches ) {
2520 list( $wikiUpperChars ) = self
::getCaseMaps();
2521 return strtr( $matches[1], $wikiUpperChars );
2525 * @param array $matches
2528 function lcCallback( $matches ) {
2529 list( , $wikiLowerChars ) = self
::getCaseMaps();
2530 return strtr( $matches[1], $wikiLowerChars );
2534 * @param array $matches
2537 function ucwordsCallbackMB( $matches ) {
2538 return mb_strtoupper( $matches[0] );
2542 * @param array $matches
2545 function ucwordsCallbackWiki( $matches ) {
2546 list( $wikiUpperChars ) = self
::getCaseMaps();
2547 return strtr( $matches[0], $wikiUpperChars );
2551 * Make a string's first character uppercase
2553 * @param string $str
2557 function ucfirst( $str ) {
2559 if ( $o < 96 ) { // if already uppercase...
2561 } elseif ( $o < 128 ) {
2562 return ucfirst( $str ); // use PHP's ucfirst()
2564 // fall back to more complex logic in case of multibyte strings
2565 return $this->uc( $str, true );
2570 * Convert a string to uppercase
2572 * @param string $str
2573 * @param bool $first
2577 function uc( $str, $first = false ) {
2578 if ( function_exists( 'mb_strtoupper' ) ) {
2580 if ( $this->isMultibyte( $str ) ) {
2581 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2583 return ucfirst( $str );
2586 return $this->isMultibyte( $str ) ?
mb_strtoupper( $str ) : strtoupper( $str );
2589 if ( $this->isMultibyte( $str ) ) {
2590 $x = $first ?
'^' : '';
2591 return preg_replace_callback(
2592 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2593 array( $this, 'ucCallback' ),
2597 return $first ?
ucfirst( $str ) : strtoupper( $str );
2603 * @param string $str
2604 * @return mixed|string
2606 function lcfirst( $str ) {
2609 return strval( $str );
2610 } elseif ( $o >= 128 ) {
2611 return $this->lc( $str, true );
2612 } elseif ( $o > 96 ) {
2615 $str[0] = strtolower( $str[0] );
2621 * @param string $str
2622 * @param bool $first
2623 * @return mixed|string
2625 function lc( $str, $first = false ) {
2626 if ( function_exists( 'mb_strtolower' ) ) {
2628 if ( $this->isMultibyte( $str ) ) {
2629 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2631 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2634 return $this->isMultibyte( $str ) ?
mb_strtolower( $str ) : strtolower( $str );
2637 if ( $this->isMultibyte( $str ) ) {
2638 $x = $first ?
'^' : '';
2639 return preg_replace_callback(
2640 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2641 array( $this, 'lcCallback' ),
2645 return $first ?
strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2651 * @param string $str
2654 function isMultibyte( $str ) {
2655 return (bool)preg_match( '/[\x80-\xff]/', $str );
2659 * @param string $str
2660 * @return mixed|string
2662 function ucwords( $str ) {
2663 if ( $this->isMultibyte( $str ) ) {
2664 $str = $this->lc( $str );
2666 // regexp to find first letter in each word (i.e. after each space)
2667 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2669 // function to use to capitalize a single char
2670 if ( function_exists( 'mb_strtoupper' ) ) {
2671 return preg_replace_callback(
2673 array( $this, 'ucwordsCallbackMB' ),
2677 return preg_replace_callback(
2679 array( $this, 'ucwordsCallbackWiki' ),
2684 return ucwords( strtolower( $str ) );
2689 * capitalize words at word breaks
2691 * @param string $str
2694 function ucwordbreaks( $str ) {
2695 if ( $this->isMultibyte( $str ) ) {
2696 $str = $this->lc( $str );
2698 // since \b doesn't work for UTF-8, we explicitely define word break chars
2699 $breaks = "[ \-\(\)\}\{\.,\?!]";
2701 // find first letter after word break
2702 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2703 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2705 if ( function_exists( 'mb_strtoupper' ) ) {
2706 return preg_replace_callback(
2708 array( $this, 'ucwordbreaksCallbackMB' ),
2712 return preg_replace_callback(
2714 array( $this, 'ucwordsCallbackWiki' ),
2719 return preg_replace_callback(
2720 '/\b([\w\x80-\xff]+)\b/',
2721 array( $this, 'ucwordbreaksCallbackAscii' ),
2728 * Return a case-folded representation of $s
2730 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2731 * and $s2 are the same except for the case of their characters. It is not
2732 * necessary for the value returned to make sense when displayed.
2734 * Do *not* perform any other normalisation in this function. If a caller
2735 * uses this function when it should be using a more general normalisation
2736 * function, then fix the caller.
2742 function caseFold( $s ) {
2743 return $this->uc( $s );
2750 function checkTitleEncoding( $s ) {
2751 if ( is_array( $s ) ) {
2752 throw new MWException( 'Given array to checkTitleEncoding.' );
2754 if ( StringUtils
::isUtf8( $s ) ) {
2758 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2764 function fallback8bitEncoding() {
2765 return self
::$dataCache->getItem( $this->mCode
, 'fallback8bitEncoding' );
2769 * Most writing systems use whitespace to break up words.
2770 * Some languages such as Chinese don't conventionally do this,
2771 * which requires special handling when breaking up words for
2776 function hasWordBreaks() {
2781 * Some languages such as Chinese require word segmentation,
2782 * Specify such segmentation when overridden in derived class.
2784 * @param string $string
2787 function segmentByWord( $string ) {
2792 * Some languages have special punctuation need to be normalized.
2793 * Make such changes here.
2795 * @param string $string
2798 function normalizeForSearch( $string ) {
2799 return self
::convertDoubleWidth( $string );
2803 * convert double-width roman characters to single-width.
2804 * range: ff00-ff5f ~= 0020-007f
2806 * @param string $string
2810 protected static function convertDoubleWidth( $string ) {
2811 static $full = null;
2812 static $half = null;
2814 if ( $full === null ) {
2815 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2816 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2817 $full = str_split( $fullWidth, 3 );
2818 $half = str_split( $halfWidth );
2821 $string = str_replace( $full, $half, $string );
2826 * @param string $string
2827 * @param string $pattern
2830 protected static function insertSpace( $string, $pattern ) {
2831 $string = preg_replace( $pattern, " $1 ", $string );
2832 $string = preg_replace( '/ +/', ' ', $string );
2837 * @param array $termsArray
2840 function convertForSearchResult( $termsArray ) {
2841 # some languages, e.g. Chinese, need to do a conversion
2842 # in order for search results to be displayed correctly
2847 * Get the first character of a string.
2852 function firstChar( $s ) {
2855 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2856 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2861 if ( isset( $matches[1] ) ) {
2862 if ( strlen( $matches[1] ) != 3 ) {
2866 // Break down Hangul syllables to grab the first jamo
2867 $code = utf8ToCodepoint( $matches[1] );
2868 if ( $code < 0xac00 ||
0xd7a4 <= $code ) {
2870 } elseif ( $code < 0xb098 ) {
2871 return "\xe3\x84\xb1";
2872 } elseif ( $code < 0xb2e4 ) {
2873 return "\xe3\x84\xb4";
2874 } elseif ( $code < 0xb77c ) {
2875 return "\xe3\x84\xb7";
2876 } elseif ( $code < 0xb9c8 ) {
2877 return "\xe3\x84\xb9";
2878 } elseif ( $code < 0xbc14 ) {
2879 return "\xe3\x85\x81";
2880 } elseif ( $code < 0xc0ac ) {
2881 return "\xe3\x85\x82";
2882 } elseif ( $code < 0xc544 ) {
2883 return "\xe3\x85\x85";
2884 } elseif ( $code < 0xc790 ) {
2885 return "\xe3\x85\x87";
2886 } elseif ( $code < 0xcc28 ) {
2887 return "\xe3\x85\x88";
2888 } elseif ( $code < 0xce74 ) {
2889 return "\xe3\x85\x8a";
2890 } elseif ( $code < 0xd0c0 ) {
2891 return "\xe3\x85\x8b";
2892 } elseif ( $code < 0xd30c ) {
2893 return "\xe3\x85\x8c";
2894 } elseif ( $code < 0xd558 ) {
2895 return "\xe3\x85\x8d";
2897 return "\xe3\x85\x8e";
2904 function initEncoding() {
2905 # Some languages may have an alternate char encoding option
2906 # (Esperanto X-coding, Japanese furigana conversion, etc)
2907 # If this language is used as the primary content language,
2908 # an override to the defaults can be set here on startup.
2915 function recodeForEdit( $s ) {
2916 # For some languages we'll want to explicitly specify
2917 # which characters make it into the edit box raw
2918 # or are converted in some way or another.
2919 global $wgEditEncoding;
2920 if ( $wgEditEncoding == '' ||
$wgEditEncoding == 'UTF-8' ) {
2923 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2931 function recodeInput( $s ) {
2932 # Take the previous into account.
2933 global $wgEditEncoding;
2934 if ( $wgEditEncoding != '' ) {
2935 $enc = $wgEditEncoding;
2939 if ( $enc == 'UTF-8' ) {
2942 return $this->iconv( $enc, 'UTF-8', $s );
2947 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2948 * also cleans up certain backwards-compatible sequences, converting them
2949 * to the modern Unicode equivalent.
2951 * This is language-specific for performance reasons only.
2957 function normalize( $s ) {
2958 global $wgAllUnicodeFixes;
2959 $s = UtfNormal
::cleanUp( $s );
2960 if ( $wgAllUnicodeFixes ) {
2961 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2962 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2969 * Transform a string using serialized data stored in the given file (which
2970 * must be in the serialized subdirectory of $IP). The file contains pairs
2971 * mapping source characters to destination characters.
2973 * The data is cached in process memory. This will go faster if you have the
2974 * FastStringSearch extension.
2976 * @param string $file
2977 * @param string $string
2979 * @throws MWException
2982 function transformUsingPairFile( $file, $string ) {
2983 if ( !isset( $this->transformData
[$file] ) ) {
2984 $data = wfGetPrecompiledData( $file );
2985 if ( $data === false ) {
2986 throw new MWException( __METHOD__
. ": The transformation file $file is missing" );
2988 $this->transformData
[$file] = new ReplacementArray( $data );
2990 return $this->transformData
[$file]->replace( $string );
2994 * For right-to-left language support
2999 return self
::$dataCache->getItem( $this->mCode
, 'rtl' );
3003 * Return the correct HTML 'dir' attribute value for this language.
3007 return $this->isRTL() ?
'rtl' : 'ltr';
3011 * Return 'left' or 'right' as appropriate alignment for line-start
3012 * for this language's text direction.
3014 * Should be equivalent to CSS3 'start' text-align value....
3018 function alignStart() {
3019 return $this->isRTL() ?
'right' : 'left';
3023 * Return 'right' or 'left' as appropriate alignment for line-end
3024 * for this language's text direction.
3026 * Should be equivalent to CSS3 'end' text-align value....
3030 function alignEnd() {
3031 return $this->isRTL() ?
'left' : 'right';
3035 * A hidden direction mark (LRM or RLM), depending on the language direction.
3036 * Unlike getDirMark(), this function returns the character as an HTML entity.
3037 * This function should be used when the output is guaranteed to be HTML,
3038 * because it makes the output HTML source code more readable. When
3039 * the output is plain text or can be escaped, getDirMark() should be used.
3041 * @param bool $opposite Get the direction mark opposite to your language
3045 function getDirMarkEntity( $opposite = false ) {
3047 return $this->isRTL() ?
'‎' : '‏';
3049 return $this->isRTL() ?
'‏' : '‎';
3053 * A hidden direction mark (LRM or RLM), depending on the language direction.
3054 * This function produces them as invisible Unicode characters and
3055 * the output may be hard to read and debug, so it should only be used
3056 * when the output is plain text or can be escaped. When the output is
3057 * HTML, use getDirMarkEntity() instead.
3059 * @param bool $opposite Get the direction mark opposite to your language
3062 function getDirMark( $opposite = false ) {
3063 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3064 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3066 return $this->isRTL() ?
$lrm : $rlm;
3068 return $this->isRTL() ?
$rlm : $lrm;
3074 function capitalizeAllNouns() {
3075 return self
::$dataCache->getItem( $this->mCode
, 'capitalizeAllNouns' );
3079 * An arrow, depending on the language direction.
3081 * @param string $direction The direction of the arrow: forwards (default),
3082 * backwards, left, right, up, down.
3085 function getArrow( $direction = 'forwards' ) {
3086 switch ( $direction ) {
3088 return $this->isRTL() ?
'←' : '→';
3090 return $this->isRTL() ?
'→' : '←';
3103 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3107 function linkPrefixExtension() {
3108 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixExtension' );
3112 * Get all magic words from cache.
3115 function getMagicWords() {
3116 return self
::$dataCache->getItem( $this->mCode
, 'magicWords' );
3120 * Run the LanguageGetMagic hook once.
3122 protected function doMagicHook() {
3123 if ( $this->mMagicHookDone
) {
3126 $this->mMagicHookDone
= true;
3127 wfProfileIn( 'LanguageGetMagic' );
3128 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions
, $this->getCode() ) );
3129 wfProfileOut( 'LanguageGetMagic' );
3133 * Fill a MagicWord object with data from here
3135 * @param MagicWord $mw
3137 function getMagic( $mw ) {
3138 // Saves a function call
3139 if ( ! $this->mMagicHookDone
) {
3140 $this->doMagicHook();
3143 if ( isset( $this->mMagicExtensions
[$mw->mId
] ) ) {
3144 $rawEntry = $this->mMagicExtensions
[$mw->mId
];
3146 $rawEntry = self
::$dataCache->getSubitem(
3147 $this->mCode
, 'magicWords', $mw->mId
);
3150 if ( !is_array( $rawEntry ) ) {
3151 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3153 $mw->mCaseSensitive
= $rawEntry[0];
3154 $mw->mSynonyms
= array_slice( $rawEntry, 1 );
3159 * Add magic words to the extension array
3161 * @param array $newWords
3163 function addMagicWordsByLang( $newWords ) {
3164 $fallbackChain = $this->getFallbackLanguages();
3165 $fallbackChain = array_reverse( $fallbackChain );
3166 foreach ( $fallbackChain as $code ) {
3167 if ( isset( $newWords[$code] ) ) {
3168 $this->mMagicExtensions
= $newWords[$code] +
$this->mMagicExtensions
;
3174 * Get special page names, as an associative array
3175 * case folded alias => real name
3178 function getSpecialPageAliases() {
3179 // Cache aliases because it may be slow to load them
3180 if ( is_null( $this->mExtendedSpecialPageAliases
) ) {
3182 $this->mExtendedSpecialPageAliases
=
3183 self
::$dataCache->getItem( $this->mCode
, 'specialPageAliases' );
3184 wfRunHooks( 'LanguageGetSpecialPageAliases',
3185 array( &$this->mExtendedSpecialPageAliases
, $this->getCode() ) );
3188 return $this->mExtendedSpecialPageAliases
;
3192 * Italic is unsuitable for some languages
3194 * @param string $text The text to be emphasized.
3197 function emphasize( $text ) {
3198 return "<em>$text</em>";
3202 * Normally we output all numbers in plain en_US style, that is
3203 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3204 * point twohundredthirtyfive. However this is not suitable for all
3205 * languages, some such as Punjabi want ੨੯੩,੨੯੫.੨੩੫ and others such as
3206 * Icelandic just want to use commas instead of dots, and dots instead
3207 * of commas like "293.291,235".
3209 * An example of this function being called:
3211 * wfMessage( 'message' )->numParams( $num )->text()
3214 * See $separatorTransformTable on MessageIs.php for
3215 * the , => . and . => , implementation.
3217 * @todo check if it's viable to use localeconv() for the decimal separator thing.
3218 * @param int|float $number The string to be formatted, should be an integer
3219 * or a floating point number.
3220 * @param bool $nocommafy Set to true for special numbers like dates
3223 public function formatNum( $number, $nocommafy = false ) {
3224 global $wgTranslateNumerals;
3225 if ( !$nocommafy ) {
3226 $number = $this->commafy( $number );
3227 $s = $this->separatorTransformTable();
3229 $number = strtr( $number, $s );
3233 if ( $wgTranslateNumerals ) {
3234 $s = $this->digitTransformTable();
3236 $number = strtr( $number, $s );
3244 * Front-end for non-commafied formatNum
3246 * @param int|float $number The string to be formatted, should be an integer
3247 * or a floating point number.
3251 public function formatNumNoSeparators( $number ) {
3252 return $this->formatNum( $number, true );
3256 * @param string $number
3259 public function parseFormattedNumber( $number ) {
3260 $s = $this->digitTransformTable();
3262 // eliminate empty array values such as ''. (bug 64347)
3263 $s = array_filter( $s );
3264 $number = strtr( $number, array_flip( $s ) );
3267 $s = $this->separatorTransformTable();
3269 // eliminate empty array values such as ''. (bug 64347)
3270 $s = array_filter( $s );
3271 $number = strtr( $number, array_flip( $s ) );
3274 $number = strtr( $number, array( ',' => '' ) );
3279 * Adds commas to a given number
3281 * @param mixed $number
3284 function commafy( $number ) {
3285 $digitGroupingPattern = $this->digitGroupingPattern();
3286 if ( $number === null ) {
3290 if ( !$digitGroupingPattern ||
$digitGroupingPattern === "###,###,###" ) {
3291 // default grouping is at thousands, use the same for ###,###,### pattern too.
3292 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3294 // Ref: http://cldr.unicode.org/translation/number-patterns
3296 if ( intval( $number ) < 0 ) {
3297 // For negative numbers apply the algorithm like positive number and add sign.
3299 $number = substr( $number, 1 );
3301 $integerPart = array();
3302 $decimalPart = array();
3303 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3304 preg_match( "/\d+/", $number, $integerPart );
3305 preg_match( "/\.\d*/", $number, $decimalPart );
3306 $groupedNumber = ( count( $decimalPart ) > 0 ) ?
$decimalPart[0] : "";
3307 if ( $groupedNumber === $number ) {
3308 // the string does not have any number part. Eg: .12345
3309 return $sign . $groupedNumber;
3311 $start = $end = strlen( $integerPart[0] );
3312 while ( $start > 0 ) {
3313 $match = $matches[0][$numMatches - 1];
3314 $matchLen = strlen( $match );
3315 $start = $end - $matchLen;
3319 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3321 if ( $numMatches > 1 ) {
3322 // use the last pattern for the rest of the number
3326 $groupedNumber = "," . $groupedNumber;
3329 return $sign . $groupedNumber;
3336 function digitGroupingPattern() {
3337 return self
::$dataCache->getItem( $this->mCode
, 'digitGroupingPattern' );
3343 function digitTransformTable() {
3344 return self
::$dataCache->getItem( $this->mCode
, 'digitTransformTable' );
3350 function separatorTransformTable() {
3351 return self
::$dataCache->getItem( $this->mCode
, 'separatorTransformTable' );
3355 * Take a list of strings and build a locale-friendly comma-separated
3356 * list, using the local comma-separator message.
3357 * The last two strings are chained with an "and".
3358 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3360 * @param string[] $l
3363 function listToText( array $l ) {
3364 $m = count( $l ) - 1;
3369 $and = $this->getMessageFromDB( 'and' );
3370 $space = $this->getMessageFromDB( 'word-separator' );
3372 $comma = $this->getMessageFromDB( 'comma-separator' );
3376 for ( $i = $m - 1; $i >= 0; $i-- ) {
3377 if ( $i == $m - 1 ) {
3378 $s = $l[$i] . $and . $space . $s;
3380 $s = $l[$i] . $comma . $s;
3387 * Take a list of strings and build a locale-friendly comma-separated
3388 * list, using the local comma-separator message.
3389 * @param string[] $list Array of strings to put in a comma list
3392 function commaList( array $list ) {
3394 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3400 * Take a list of strings and build a locale-friendly semicolon-separated
3401 * list, using the local semicolon-separator message.
3402 * @param string[] $list Array of strings to put in a semicolon list
3405 function semicolonList( array $list ) {
3407 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3413 * Same as commaList, but separate it with the pipe instead.
3414 * @param string[] $list Array of strings to put in a pipe list
3417 function pipeList( array $list ) {
3419 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3425 * Truncate a string to a specified length in bytes, appending an optional
3426 * string (e.g. for ellipses)
3428 * The database offers limited byte lengths for some columns in the database;
3429 * multi-byte character sets mean we need to ensure that only whole characters
3430 * are included, otherwise broken characters can be passed to the user
3432 * If $length is negative, the string will be truncated from the beginning
3434 * @param string $string String to truncate
3435 * @param int $length Maximum length (including ellipses)
3436 * @param string $ellipsis String to append to the truncated text
3437 * @param bool $adjustLength Subtract length of ellipsis from $length.
3438 * $adjustLength was introduced in 1.18, before that behaved as if false.
3441 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3442 # Use the localized ellipsis character
3443 if ( $ellipsis == '...' ) {
3444 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3446 # Check if there is no need to truncate
3447 if ( $length == 0 ) {
3448 return $ellipsis; // convention
3449 } elseif ( strlen( $string ) <= abs( $length ) ) {
3450 return $string; // no need to truncate
3452 $stringOriginal = $string;
3453 # If ellipsis length is >= $length then we can't apply $adjustLength
3454 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3455 $string = $ellipsis; // this can be slightly unexpected
3456 # Otherwise, truncate and add ellipsis...
3458 $eLength = $adjustLength ?
strlen( $ellipsis ) : 0;
3459 if ( $length > 0 ) {
3460 $length -= $eLength;
3461 $string = substr( $string, 0, $length ); // xyz...
3462 $string = $this->removeBadCharLast( $string );
3463 $string = rtrim( $string );
3464 $string = $string . $ellipsis;
3466 $length +
= $eLength;
3467 $string = substr( $string, $length ); // ...xyz
3468 $string = $this->removeBadCharFirst( $string );
3469 $string = ltrim( $string );
3470 $string = $ellipsis . $string;
3473 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3474 # This check is *not* redundant if $adjustLength, due to the single case where
3475 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3476 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3479 return $stringOriginal;
3484 * Remove bytes that represent an incomplete Unicode character
3485 * at the end of string (e.g. bytes of the char are missing)
3487 * @param string $string
3490 protected function removeBadCharLast( $string ) {
3491 if ( $string != '' ) {
3492 $char = ord( $string[strlen( $string ) - 1] );
3494 if ( $char >= 0xc0 ) {
3495 # We got the first byte only of a multibyte char; remove it.
3496 $string = substr( $string, 0, -1 );
3497 } elseif ( $char >= 0x80 &&
3498 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3499 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m )
3501 # We chopped in the middle of a character; remove it
3509 * Remove bytes that represent an incomplete Unicode character
3510 * at the start of string (e.g. bytes of the char are missing)
3512 * @param string $string
3515 protected function removeBadCharFirst( $string ) {
3516 if ( $string != '' ) {
3517 $char = ord( $string[0] );
3518 if ( $char >= 0x80 && $char < 0xc0 ) {
3519 # We chopped in the middle of a character; remove the whole thing
3520 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3527 * Truncate a string of valid HTML to a specified length in bytes,
3528 * appending an optional string (e.g. for ellipses), and return valid HTML
3530 * This is only intended for styled/linked text, such as HTML with
3531 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3532 * Also, this will not detect things like "display:none" CSS.
3534 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3536 * @param string $text HTML string to truncate
3537 * @param int $length (zero/positive) Maximum length (including ellipses)
3538 * @param string $ellipsis String to append to the truncated text
3541 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3542 # Use the localized ellipsis character
3543 if ( $ellipsis == '...' ) {
3544 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3546 # Check if there is clearly no need to truncate
3547 if ( $length <= 0 ) {
3548 return $ellipsis; // no text shown, nothing to format (convention)
3549 } elseif ( strlen( $text ) <= $length ) {
3550 return $text; // string short enough even *with* HTML (short-circuit)
3553 $dispLen = 0; // innerHTML legth so far
3554 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3555 $tagType = 0; // 0-open, 1-close
3556 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3557 $entityState = 0; // 0-not entity, 1-entity
3558 $tag = $ret = ''; // accumulated tag name, accumulated result string
3559 $openTags = array(); // open tag stack
3560 $maybeState = null; // possible truncation state
3562 $textLen = strlen( $text );
3563 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3564 for ( $pos = 0; true; ++
$pos ) {
3565 # Consider truncation once the display length has reached the maximim.
3566 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3567 # Check that we're not in the middle of a bracket/entity...
3568 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3569 if ( !$testingEllipsis ) {
3570 $testingEllipsis = true;
3571 # Save where we are; we will truncate here unless there turn out to
3572 # be so few remaining characters that truncation is not necessary.
3573 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3574 $maybeState = array( $ret, $openTags ); // save state
3576 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3577 # String in fact does need truncation, the truncation point was OK.
3578 list( $ret, $openTags ) = $maybeState; // reload state
3579 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3580 $ret .= $ellipsis; // add ellipsis
3584 if ( $pos >= $textLen ) {
3585 break; // extra iteration just for above checks
3588 # Read the next char...
3590 $lastCh = $pos ?
$text[$pos - 1] : '';
3591 $ret .= $ch; // add to result string
3593 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3594 $entityState = 0; // for bad HTML
3595 $bracketState = 1; // tag started (checking for backslash)
3596 } elseif ( $ch == '>' ) {
3597 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3598 $entityState = 0; // for bad HTML
3599 $bracketState = 0; // out of brackets
3600 } elseif ( $bracketState == 1 ) {
3602 $tagType = 1; // close tag (e.g. "</span>")
3604 $tagType = 0; // open tag (e.g. "<span>")
3607 $bracketState = 2; // building tag name
3608 } elseif ( $bracketState == 2 ) {
3612 // Name found (e.g. "<a href=..."), add on tag attributes...
3613 $pos +
= $this->truncate_skip( $ret, $text, "<>", $pos +
1 );
3615 } elseif ( $bracketState == 0 ) {
3616 if ( $entityState ) {
3619 $dispLen++
; // entity is one displayed char
3622 if ( $neLength == 0 && !$maybeState ) {
3623 // Save state without $ch. We want to *hit* the first
3624 // display char (to get tags) but not *use* it if truncating.
3625 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3628 $entityState = 1; // entity found, (e.g. " ")
3630 $dispLen++
; // this char is displayed
3631 // Add the next $max display text chars after this in one swoop...
3632 $max = ( $testingEllipsis ?
$length : $neLength ) - $dispLen;
3633 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos +
1, $max );
3634 $dispLen +
= $skipped;
3640 // Close the last tag if left unclosed by bad HTML
3641 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3642 while ( count( $openTags ) > 0 ) {
3643 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3649 * truncateHtml() helper function
3650 * like strcspn() but adds the skipped chars to $ret
3652 * @param string $ret
3653 * @param string $text
3654 * @param string $search
3656 * @param null|int $len
3659 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3660 if ( $len === null ) {
3661 $len = -1; // -1 means "no limit" for strcspn
3662 } elseif ( $len < 0 ) {
3666 if ( $start < strlen( $text ) ) {
3667 $skipCount = strcspn( $text, $search, $start, $len );
3668 $ret .= substr( $text, $start, $skipCount );
3674 * truncateHtml() helper function
3675 * (a) push or pop $tag from $openTags as needed
3676 * (b) clear $tag value
3677 * @param string &$tag Current HTML tag name we are looking at
3678 * @param int $tagType (0-open tag, 1-close tag)
3679 * @param string $lastCh Character before the '>' that ended this tag
3680 * @param array &$openTags Open tag stack (not accounting for $tag)
3682 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3683 $tag = ltrim( $tag );
3685 if ( $tagType == 0 && $lastCh != '/' ) {
3686 $openTags[] = $tag; // tag opened (didn't close itself)
3687 } elseif ( $tagType == 1 ) {
3688 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3689 array_pop( $openTags ); // tag closed
3697 * Grammatical transformations, needed for inflected languages
3698 * Invoked by putting {{grammar:case|word}} in a message
3700 * @param string $word
3701 * @param string $case
3704 function convertGrammar( $word, $case ) {
3705 global $wgGrammarForms;
3706 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3707 return $wgGrammarForms[$this->getCode()][$case][$word];
3713 * Get the grammar forms for the content language
3714 * @return array Array of grammar forms
3717 function getGrammarForms() {
3718 global $wgGrammarForms;
3719 if ( isset( $wgGrammarForms[$this->getCode()] )
3720 && is_array( $wgGrammarForms[$this->getCode()] )
3722 return $wgGrammarForms[$this->getCode()];
3728 * Provides an alternative text depending on specified gender.
3729 * Usage {{gender:username|masculine|feminine|unknown}}.
3730 * username is optional, in which case the gender of current user is used,
3731 * but only in (some) interface messages; otherwise default gender is used.
3733 * If no forms are given, an empty string is returned. If only one form is
3734 * given, it will be returned unconditionally. These details are implied by
3735 * the caller and cannot be overridden in subclasses.
3737 * If three forms are given, the default is to use the third (unknown) form.
3738 * If fewer than three forms are given, the default is to use the first (masculine) form.
3739 * These details can be overridden in subclasses.
3741 * @param string $gender
3742 * @param array $forms
3746 function gender( $gender, $forms ) {
3747 if ( !count( $forms ) ) {
3750 $forms = $this->preConvertPlural( $forms, 2 );
3751 if ( $gender === 'male' ) {
3754 if ( $gender === 'female' ) {
3757 return isset( $forms[2] ) ?
$forms[2] : $forms[0];
3761 * Plural form transformations, needed for some languages.
3762 * For example, there are 3 form of plural in Russian and Polish,
3763 * depending on "count mod 10". See [[w:Plural]]
3764 * For English it is pretty simple.
3766 * Invoked by putting {{plural:count|wordform1|wordform2}}
3767 * or {{plural:count|wordform1|wordform2|wordform3}}
3769 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3771 * @param int $count Non-localized number
3772 * @param array $forms Different plural forms
3773 * @return string Correct form of plural for $count in this language
3775 function convertPlural( $count, $forms ) {
3776 // Handle explicit n=pluralform cases
3777 $forms = $this->handleExplicitPluralForms( $count, $forms );
3778 if ( is_string( $forms ) ) {
3781 if ( !count( $forms ) ) {
3785 $pluralForm = $this->getPluralRuleIndexNumber( $count );
3786 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3787 return $forms[$pluralForm];
3791 * Handles explicit plural forms for Language::convertPlural()
3793 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3794 * If an explicitly defined plural form matches the $count, then
3795 * string value returned, otherwise array returned for further consideration
3796 * by CLDR rules or overridden convertPlural().
3800 * @param int $count non-localized number
3801 * @param array $forms different plural forms
3803 * @return array|string
3805 protected function handleExplicitPluralForms( $count, array $forms ) {
3806 foreach ( $forms as $index => $form ) {
3807 if ( preg_match( '/\d+=/i', $form ) ) {
3808 $pos = strpos( $form, '=' );
3809 if ( substr( $form, 0, $pos ) === (string) $count ) {
3810 return substr( $form, $pos +
1 );
3812 unset( $forms[$index] );
3815 return array_values( $forms );
3819 * Checks that convertPlural was given an array and pads it to requested
3820 * amount of forms by copying the last one.
3822 * @param int $count How many forms should there be at least
3823 * @param array $forms Array of forms given to convertPlural
3824 * @return array Padded array of forms or an exception if not an array
3826 protected function preConvertPlural( /* Array */ $forms, $count ) {
3827 while ( count( $forms ) < $count ) {
3828 $forms[] = $forms[count( $forms ) - 1];
3834 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
3835 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3836 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3837 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3840 * @param string $str The validated block duration in English
3841 * @return string Somehow translated block duration
3842 * @see LanguageFi.php for example implementation
3844 function translateBlockExpiry( $str ) {
3845 $duration = SpecialBlock
::getSuggestedDurations( $this );
3846 foreach ( $duration as $show => $value ) {
3847 if ( strcmp( $str, $value ) == 0 ) {
3848 return htmlspecialchars( trim( $show ) );
3852 // Since usually only infinite or indefinite is only on list, so try
3853 // equivalents if still here.
3854 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3855 if ( in_array( $str, $indefs ) ) {
3856 foreach ( $indefs as $val ) {
3857 $show = array_search( $val, $duration, true );
3858 if ( $show !== false ) {
3859 return htmlspecialchars( trim( $show ) );
3864 // If all else fails, return a standard duration or timestamp description.
3865 $time = strtotime( $str, 0 );
3866 if ( $time === false ) { // Unknown format. Return it as-is in case.
3868 } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
3869 // $time is relative to 0 so it's a duration length.
3870 return $this->formatDuration( $time );
3871 } else { // It's an absolute timestamp.
3872 if ( $time === 0 ) {
3873 // wfTimestamp() handles 0 as current time instead of epoch.
3874 return $this->timeanddate( '19700101000000' );
3876 return $this->timeanddate( $time );
3882 * languages like Chinese need to be segmented in order for the diff
3885 * @param string $text
3888 public function segmentForDiff( $text ) {
3893 * and unsegment to show the result
3895 * @param string $text
3898 public function unsegmentForDiff( $text ) {
3903 * Return the LanguageConverter used in the Language
3906 * @return LanguageConverter
3908 public function getConverter() {
3909 return $this->mConverter
;
3913 * convert text to all supported variants
3915 * @param string $text
3918 public function autoConvertToAllVariants( $text ) {
3919 return $this->mConverter
->autoConvertToAllVariants( $text );
3923 * convert text to different variants of a language.
3925 * @param string $text
3928 public function convert( $text ) {
3929 return $this->mConverter
->convert( $text );
3933 * Convert a Title object to a string in the preferred variant
3935 * @param Title $title
3938 public function convertTitle( $title ) {
3939 return $this->mConverter
->convertTitle( $title );
3943 * Convert a namespace index to a string in the preferred variant
3948 public function convertNamespace( $ns ) {
3949 return $this->mConverter
->convertNamespace( $ns );
3953 * Check if this is a language with variants
3957 public function hasVariants() {
3958 return count( $this->getVariants() ) > 1;
3962 * Check if the language has the specific variant
3965 * @param string $variant
3968 public function hasVariant( $variant ) {
3969 return (bool)$this->mConverter
->validateVariant( $variant );
3973 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3975 * @param string $text
3977 * @deprecated since 1.22 is no longer used
3979 public function armourMath( $text ) {
3980 return $this->mConverter
->armourMath( $text );
3984 * Perform output conversion on a string, and encode for safe HTML output.
3985 * @param string $text Text to be converted
3986 * @param bool $isTitle Whether this conversion is for the article title
3988 * @todo this should get integrated somewhere sane
3990 public function convertHtml( $text, $isTitle = false ) {
3991 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3995 * @param string $key
3998 public function convertCategoryKey( $key ) {
3999 return $this->mConverter
->convertCategoryKey( $key );
4003 * Get the list of variants supported by this language
4004 * see sample implementation in LanguageZh.php
4006 * @return array an array of language codes
4008 public function getVariants() {
4009 return $this->mConverter
->getVariants();
4015 public function getPreferredVariant() {
4016 return $this->mConverter
->getPreferredVariant();
4022 public function getDefaultVariant() {
4023 return $this->mConverter
->getDefaultVariant();
4029 public function getURLVariant() {
4030 return $this->mConverter
->getURLVariant();
4034 * If a language supports multiple variants, it is
4035 * possible that non-existing link in one variant
4036 * actually exists in another variant. this function
4037 * tries to find it. See e.g. LanguageZh.php
4039 * @param string $link The name of the link
4040 * @param Title $nt The title object of the link
4041 * @param bool $ignoreOtherCond To disable other conditions when
4042 * we need to transclude a template or update a category's link
4043 * @return null the input parameters may be modified upon return
4045 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4046 $this->mConverter
->findVariantLink( $link, $nt, $ignoreOtherCond );
4050 * returns language specific options used by User::getPageRenderHash()
4051 * for example, the preferred language variant
4055 function getExtraHashOptions() {
4056 return $this->mConverter
->getExtraHashOptions();
4060 * For languages that support multiple variants, the title of an
4061 * article may be displayed differently in different variants. this
4062 * function returns the apporiate title defined in the body of the article.
4066 public function getParsedTitle() {
4067 return $this->mConverter
->getParsedTitle();
4071 * Prepare external link text for conversion. When the text is
4072 * a URL, it shouldn't be converted, and it'll be wrapped in
4073 * the "raw" tag (-{R| }-) to prevent conversion.
4075 * This function is called "markNoConversion" for historical
4078 * @param string $text Text to be used for external link
4079 * @param bool $noParse Wrap it without confirming it's a real URL first
4080 * @return string The tagged text
4082 public function markNoConversion( $text, $noParse = false ) {
4083 // Excluding protocal-relative URLs may avoid many false positives.
4084 if ( $noParse ||
preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4085 return $this->mConverter
->markNoConversion( $text );
4092 * A regular expression to match legal word-trailing characters
4093 * which should be merged onto a link of the form [[foo]]bar.
4097 public function linkTrail() {
4098 return self
::$dataCache->getItem( $this->mCode
, 'linkTrail' );
4102 * A regular expression character set to match legal word-prefixing
4103 * characters which should be merged onto a link of the form foo[[bar]].
4107 public function linkPrefixCharset() {
4108 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixCharset' );
4112 * @deprecated since 1.24, will be removed in 1.25
4115 function getLangObj() {
4116 wfDeprecated( __METHOD__
, '1.24' );
4121 * Get the "parent" language which has a converter to convert a "compatible" language
4122 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4124 * @return Language|null
4127 public function getParentLanguage() {
4128 if ( $this->mParentLanguage
!== false ) {
4129 return $this->mParentLanguage
;
4132 $pieces = explode( '-', $this->getCode() );
4134 if ( !in_array( $code, LanguageConverter
::$languagesWithVariants ) ) {
4135 $this->mParentLanguage
= null;
4138 $lang = Language
::factory( $code );
4139 if ( !$lang->hasVariant( $this->getCode() ) ) {
4140 $this->mParentLanguage
= null;
4144 $this->mParentLanguage
= $lang;
4149 * Get the RFC 3066 code for this language object
4151 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4152 * htmlspecialchars() or similar
4156 public function getCode() {
4157 return $this->mCode
;
4161 * Get the code in Bcp47 format which we can use
4162 * inside of html lang="" tags.
4164 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4165 * htmlspecialchars() or similar.
4170 public function getHtmlCode() {
4171 if ( is_null( $this->mHtmlCode
) ) {
4172 $this->mHtmlCode
= wfBCP47( $this->getCode() );
4174 return $this->mHtmlCode
;
4178 * @param string $code
4180 public function setCode( $code ) {
4181 $this->mCode
= $code;
4182 // Ensure we don't leave incorrect cached data lying around
4183 $this->mHtmlCode
= null;
4184 $this->mParentLanguage
= false;
4188 * Get the name of a file for a certain language code
4189 * @param string $prefix Prepend this to the filename
4190 * @param string $code Language code
4191 * @param string $suffix Append this to the filename
4192 * @throws MWException
4193 * @return string $prefix . $mangledCode . $suffix
4195 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4196 if ( !self
::isValidBuiltInCode( $code ) ) {
4197 throw new MWException( "Invalid language code \"$code\"" );
4200 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4204 * Get the language code from a file name. Inverse of getFileName()
4205 * @param string $filename $prefix . $languageCode . $suffix
4206 * @param string $prefix Prefix before the language code
4207 * @param string $suffix Suffix after the language code
4208 * @return string Language code, or false if $prefix or $suffix isn't found
4210 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4212 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4213 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4214 if ( !count( $m ) ) {
4217 return str_replace( '_', '-', strtolower( $m[1] ) );
4221 * @param string $code
4224 public static function getMessagesFileName( $code ) {
4226 $file = self
::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4227 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
4232 * @param string $code
4236 public static function getJsonMessagesFileName( $code ) {
4239 if ( !self
::isValidBuiltInCode( $code ) ) {
4240 throw new MWException( "Invalid language code \"$code\"" );
4243 return "$IP/languages/i18n/$code.json";
4247 * @param string $code
4250 public static function getClassFileName( $code ) {
4252 return self
::getFileName( "$IP/languages/classes/Language", $code, '.php' );
4256 * Get the first fallback for a given language.
4258 * @param string $code
4260 * @return bool|string
4262 public static function getFallbackFor( $code ) {
4263 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
4266 $fallbacks = self
::getFallbacksFor( $code );
4267 $first = array_shift( $fallbacks );
4273 * Get the ordered list of fallback languages.
4276 * @param string $code Language code
4279 public static function getFallbacksFor( $code ) {
4280 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
4283 $v = self
::getLocalisationCache()->getItem( $code, 'fallback' );
4284 $v = array_map( 'trim', explode( ',', $v ) );
4285 if ( $v[count( $v ) - 1] !== 'en' ) {
4293 * Get the ordered list of fallback languages, ending with the fallback
4294 * language chain for the site language.
4297 * @param string $code Language code
4298 * @return array array( fallbacks, site fallbacks )
4300 public static function getFallbacksIncludingSiteLanguage( $code ) {
4301 global $wgLanguageCode;
4303 // Usually, we will only store a tiny number of fallback chains, so we
4304 // keep them in static memory.
4305 $cacheKey = "{$code}-{$wgLanguageCode}";
4307 if ( !array_key_exists( $cacheKey, self
::$fallbackLanguageCache ) ) {
4308 $fallbacks = self
::getFallbacksFor( $code );
4310 // Append the site's fallback chain, including the site language itself
4311 $siteFallbacks = self
::getFallbacksFor( $wgLanguageCode );
4312 array_unshift( $siteFallbacks, $wgLanguageCode );
4314 // Eliminate any languages already included in the chain
4315 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4317 self
::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
4319 return self
::$fallbackLanguageCache[$cacheKey];
4323 * Get all messages for a given language
4324 * WARNING: this may take a long time. If you just need all message *keys*
4325 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4327 * @param string $code
4331 public static function getMessagesFor( $code ) {
4332 return self
::getLocalisationCache()->getItem( $code, 'messages' );
4336 * Get a message for a given language
4338 * @param string $key
4339 * @param string $code
4343 public static function getMessageFor( $key, $code ) {
4344 return self
::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4348 * Get all message keys for a given language. This is a faster alternative to
4349 * array_keys( Language::getMessagesFor( $code ) )
4352 * @param string $code Language code
4353 * @return array of message keys (strings)
4355 public static function getMessageKeysFor( $code ) {
4356 return self
::getLocalisationCache()->getSubItemList( $code, 'messages' );
4360 * @param string $talk
4363 function fixVariableInNamespace( $talk ) {
4364 if ( strpos( $talk, '$1' ) === false ) {
4368 global $wgMetaNamespace;
4369 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4371 # Allow grammar transformations
4372 # Allowing full message-style parsing would make simple requests
4373 # such as action=raw much more expensive than they need to be.
4374 # This will hopefully cover most cases.
4375 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4376 array( &$this, 'replaceGrammarInNamespace' ), $talk );
4377 return str_replace( ' ', '_', $talk );
4384 function replaceGrammarInNamespace( $m ) {
4385 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4389 * @throws MWException
4392 static function getCaseMaps() {
4393 static $wikiUpperChars, $wikiLowerChars;
4394 if ( isset( $wikiUpperChars ) ) {
4395 return array( $wikiUpperChars, $wikiLowerChars );
4398 wfProfileIn( __METHOD__
);
4399 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4400 if ( $arr === false ) {
4401 throw new MWException(
4402 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4404 $wikiUpperChars = $arr['wikiUpperChars'];
4405 $wikiLowerChars = $arr['wikiLowerChars'];
4406 wfProfileOut( __METHOD__
);
4407 return array( $wikiUpperChars, $wikiLowerChars );
4411 * Decode an expiry (block, protection, etc) which has come from the DB
4413 * @todo FIXME: why are we returnings DBMS-dependent strings???
4415 * @param string $expiry Database expiry String
4416 * @param bool|int $format True to process using language functions, or TS_ constant
4417 * to return the expiry in a given timestamp
4421 public function formatExpiry( $expiry, $format = true ) {
4423 if ( $infinity === null ) {
4424 $infinity = wfGetDB( DB_SLAVE
)->getInfinity();
4427 if ( $expiry == '' ||
$expiry == $infinity ) {
4428 return $format === true
4429 ?
$this->getMessageFromDB( 'infiniteblock' )
4432 return $format === true
4433 ?
$this->timeanddate( $expiry, /* User preference timezone */ true )
4434 : wfTimestamp( $format, $expiry );
4440 * @param int|float $seconds
4441 * @param array $format Optional
4442 * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4443 * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4444 * If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4446 * For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4447 * or 'avoidminutes'.
4450 function formatTimePeriod( $seconds, $format = array() ) {
4451 if ( !is_array( $format ) ) {
4452 $format = array( 'avoid' => $format ); // For backwards compatibility
4454 if ( !isset( $format['avoid'] ) ) {
4455 $format['avoid'] = false;
4457 if ( !isset( $format['noabbrevs' ] ) ) {
4458 $format['noabbrevs'] = false;
4460 $secondsMsg = wfMessage(
4461 $format['noabbrevs'] ?
'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4462 $minutesMsg = wfMessage(
4463 $format['noabbrevs'] ?
'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4464 $hoursMsg = wfMessage(
4465 $format['noabbrevs'] ?
'hours' : 'hours-abbrev' )->inLanguage( $this );
4466 $daysMsg = wfMessage(
4467 $format['noabbrevs'] ?
'days' : 'days-abbrev' )->inLanguage( $this );
4469 if ( round( $seconds * 10 ) < 100 ) {
4470 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4471 $s = $secondsMsg->params( $s )->text();
4472 } elseif ( round( $seconds ) < 60 ) {
4473 $s = $this->formatNum( round( $seconds ) );
4474 $s = $secondsMsg->params( $s )->text();
4475 } elseif ( round( $seconds ) < 3600 ) {
4476 $minutes = floor( $seconds / 60 );
4477 $secondsPart = round( fmod( $seconds, 60 ) );
4478 if ( $secondsPart == 60 ) {
4482 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4484 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4485 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4486 $hours = floor( $seconds / 3600 );
4487 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4488 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4489 if ( $secondsPart == 60 ) {
4493 if ( $minutes == 60 ) {
4497 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4499 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4500 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4501 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4504 $days = floor( $seconds / 86400 );
4505 if ( $format['avoid'] === 'avoidminutes' ) {
4506 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4507 if ( $hours == 24 ) {
4511 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4513 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4514 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4515 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4516 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4517 if ( $minutes == 60 ) {
4521 if ( $hours == 24 ) {
4525 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4527 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4529 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4531 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4533 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4540 * Format a bitrate for output, using an appropriate
4541 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4542 * the magnitude in question.
4544 * This use base 1000. For base 1024 use formatSize(), for another base
4545 * see formatComputingNumbers().
4550 function formatBitrate( $bps ) {
4551 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4555 * @param int $size Size of the unit
4556 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4557 * @param string $messageKey Message key to be uesd
4560 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4562 return str_replace( '$1', $this->formatNum( $size ),
4563 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4566 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4569 $maxIndex = count( $sizes ) - 1;
4570 while ( $size >= $boundary && $index < $maxIndex ) {
4575 // For small sizes no decimal places necessary
4578 // For MB and bigger two decimal places are smarter
4581 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4583 $size = round( $size, $round );
4584 $text = $this->getMessageFromDB( $msg );
4585 return str_replace( '$1', $this->formatNum( $size ), $text );
4589 * Format a size in bytes for output, using an appropriate
4590 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4592 * This method use base 1024. For base 1000 use formatBitrate(), for
4593 * another base see formatComputingNumbers()
4595 * @param int $size Size to format
4596 * @return string Plain text (not HTML)
4598 function formatSize( $size ) {
4599 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4603 * Make a list item, used by various special pages
4605 * @param string $page Page link
4606 * @param string $details Text between brackets
4607 * @param bool $oppositedm Add the direction mark opposite to your
4608 * language, to display text properly
4611 function specialList( $page, $details, $oppositedm = true ) {
4612 $dirmark = ( $oppositedm ?
$this->getDirMark( true ) : '' ) .
4613 $this->getDirMark();
4614 $details = $details ?
$dirmark . $this->getMessageFromDB( 'word-separator' ) .
4615 wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : '';
4616 return $page . $details;
4620 * Generate (prev x| next x) (20|50|100...) type links for paging
4622 * @param Title $title Title object to link
4623 * @param int $offset
4625 * @param array|string $query Optional URL query parameter string
4626 * @param bool $atend Optional param for specified if this is the last page
4629 public function viewPrevNext( Title
$title, $offset, $limit,
4630 array $query = array(), $atend = false
4632 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4634 # Make 'previous' link
4635 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4636 if ( $offset > 0 ) {
4637 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4638 $query, $prev, 'prevn-title', 'mw-prevlink' );
4640 $plink = htmlspecialchars( $prev );
4644 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4646 $nlink = htmlspecialchars( $next );
4648 $nlink = $this->numLink( $title, $offset +
$limit, $limit,
4649 $query, $next, 'nextn-title', 'mw-nextlink' );
4652 # Make links to set number of items per page
4653 $numLinks = array();
4654 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4655 $numLinks[] = $this->numLink( $title, $offset, $num,
4656 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4659 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4660 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4664 * Helper function for viewPrevNext() that generates links
4666 * @param Title $title Title object to link
4667 * @param int $offset
4669 * @param array $query Extra query parameters
4670 * @param string $link Text to use for the link; will be escaped
4671 * @param string $tooltipMsg Name of the message to use as tooltip
4672 * @param string $class Value of the "class" attribute of the link
4673 * @return string HTML fragment
4675 private function numLink( Title
$title, $offset, $limit, array $query, $link,
4678 $query = array( 'limit' => $limit, 'offset' => $offset ) +
$query;
4679 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4680 ->numParams( $limit )->text();
4682 return Html
::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4683 'title' => $tooltip, 'class' => $class ), $link );
4687 * Get the conversion rule title, if any.
4691 public function getConvRuleTitle() {
4692 return $this->mConverter
->getConvRuleTitle();
4696 * Get the compiled plural rules for the language
4698 * @return array Associative array with plural form, and plural rule as key-value pairs
4700 public function getCompiledPluralRules() {
4701 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'compiledPluralRules' );
4702 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4703 if ( !$pluralRules ) {
4704 foreach ( $fallbacks as $fallbackCode ) {
4705 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4706 if ( $pluralRules ) {
4711 return $pluralRules;
4715 * Get the plural rules for the language
4717 * @return array Associative array with plural form number and plural rule as key-value pairs
4719 public function getPluralRules() {
4720 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'pluralRules' );
4721 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4722 if ( !$pluralRules ) {
4723 foreach ( $fallbacks as $fallbackCode ) {
4724 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4725 if ( $pluralRules ) {
4730 return $pluralRules;
4734 * Get the plural rule types for the language
4736 * @return array Associative array with plural form number and plural rule type as key-value pairs
4738 public function getPluralRuleTypes() {
4739 $pluralRuleTypes = self
::$dataCache->getItem( strtolower( $this->mCode
), 'pluralRuleTypes' );
4740 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4741 if ( !$pluralRuleTypes ) {
4742 foreach ( $fallbacks as $fallbackCode ) {
4743 $pluralRuleTypes = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4744 if ( $pluralRuleTypes ) {
4749 return $pluralRuleTypes;
4753 * Find the index number of the plural rule appropriate for the given number
4754 * @return int The index number of the plural rule
4756 public function getPluralRuleIndexNumber( $number ) {
4757 $pluralRules = $this->getCompiledPluralRules();
4758 $form = CLDRPluralRuleEvaluator
::evaluateCompiled( $number, $pluralRules );
4763 * Find the plural rule type appropriate for the given number
4764 * For example, if the language is set to Arabic, getPluralType(5) should
4767 * @return string The name of the plural rule type, e.g. one, two, few, many
4769 public function getPluralRuleType( $number ) {
4770 $index = $this->getPluralRuleIndexNumber( $number );
4771 $pluralRuleTypes = $this->getPluralRuleTypes();
4772 if ( isset( $pluralRuleTypes[$index] ) ) {
4773 return $pluralRuleTypes[$index];