3 * Internationalisation code
10 * @defgroup Language Language
13 if ( !defined( 'MEDIAWIKI' ) ) {
14 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
19 global $wgLanguageNames;
20 require_once( dirname( __FILE__
) . '/Names.php' );
22 if ( function_exists( 'mb_strtoupper' ) ) {
23 mb_internal_encoding( 'UTF-8' );
27 * a fake language converter
37 function __construct( $langobj ) { $this->mLang
= $langobj; }
38 function autoConvertToAllVariants( $text ) { return array( $this->mLang
->getCode() => $text ); }
39 function convert( $t ) { return $t; }
40 function convertTo( $text, $variant ) { return $text; }
41 function convertTitle( $t ) { return $t->getPrefixedText(); }
42 function getVariants() { return array( $this->mLang
->getCode() ); }
43 function getPreferredVariant() { return $this->mLang
->getCode(); }
44 function getDefaultVariant() { return $this->mLang
->getCode(); }
45 function getURLVariant() { return ''; }
46 function getConvRuleTitle() { return false; }
47 function findVariantLink( &$l, &$n, $ignoreOtherCond = false ) { }
48 function getExtraHashOptions() { return ''; }
49 function getParsedTitle() { return ''; }
50 function markNoConversion( $text, $noParse = false ) { return $text; }
51 function convertCategoryKey( $key ) { return $key; }
52 function convertLinkToAllVariants( $text ) { return $this->autoConvertToAllVariants( $text ); }
53 function armourMath( $text ) { return $text; }
57 * Internationalisation code
63 * @var LanguageConverter
67 var $mVariants, $mCode, $mLoaded = false;
68 var $mMagicExtensions = array(), $mMagicHookDone = false;
69 private $mHtmlCode = null;
71 var $dateFormatStrings = array();
72 var $mExtendedSpecialPageAliases;
74 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
77 * ReplacementArray object caches
79 var $transformData = array();
82 * @var LocalisationCache
84 static public $dataCache;
86 static public $mLangObjCache = array();
88 static public $mWeekdayMsgs = array(
89 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
93 static public $mWeekdayAbbrevMsgs = array(
94 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
97 static public $mMonthMsgs = array(
98 'january', 'february', 'march', 'april', 'may_long', 'june',
99 'july', 'august', 'september', 'october', 'november',
102 static public $mMonthGenMsgs = array(
103 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
104 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
107 static public $mMonthAbbrevMsgs = array(
108 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
109 'sep', 'oct', 'nov', 'dec'
112 static public $mIranianCalendarMonthMsgs = array(
113 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
114 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
115 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
116 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
119 static public $mHebrewCalendarMonthMsgs = array(
120 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
121 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
122 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
123 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
124 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
127 static public $mHebrewCalendarMonthGenMsgs = array(
128 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
129 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
130 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
131 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
132 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
135 static public $mHijriCalendarMonthMsgs = array(
136 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
137 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
138 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
139 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
146 static public $durationIntervals = array(
147 'millennia' => 31557600000,
148 'centuries' => 3155760000,
149 'decades' => 315576000,
150 'years' => 31557600, // 86400 * 365.25
159 * Get a cached language object for a given language code
160 * @param $code String
163 static function factory( $code ) {
164 if ( !isset( self
::$mLangObjCache[$code] ) ) {
165 if ( count( self
::$mLangObjCache ) > 10 ) {
166 // Don't keep a billion objects around, that's stupid.
167 self
::$mLangObjCache = array();
169 self
::$mLangObjCache[$code] = self
::newFromCode( $code );
171 return self
::$mLangObjCache[$code];
175 * Create a language object for a given language code
176 * @param $code String
177 * @throws MWException
180 protected static function newFromCode( $code ) {
181 // Protect against path traversal below
182 if ( !Language
::isValidCode( $code )
183 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
185 throw new MWException( "Invalid language code \"$code\"" );
188 if ( !Language
::isValidBuiltInCode( $code ) ) {
189 // It's not possible to customise this code with class files, so
190 // just return a Language object. This is to support uselang= hacks.
191 $lang = new Language
;
192 $lang->setCode( $code );
196 // Check if there is a language class for the code
197 $class = self
::classFromCode( $code );
198 self
::preloadLanguageClass( $class );
199 if ( MWInit
::classExists( $class ) ) {
204 // Keep trying the fallback list until we find an existing class
205 $fallbacks = Language
::getFallbacksFor( $code );
206 foreach ( $fallbacks as $fallbackCode ) {
207 if ( !Language
::isValidBuiltInCode( $fallbackCode ) ) {
208 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
211 $class = self
::classFromCode( $fallbackCode );
212 self
::preloadLanguageClass( $class );
213 if ( MWInit
::classExists( $class ) ) {
214 $lang = Language
::newFromCode( $fallbackCode );
215 $lang->setCode( $code );
220 throw new MWException( "Invalid fallback sequence for language '$code'" );
224 * Returns true if a language code string is of a valid form, whether or
225 * not it exists. This includes codes which are used solely for
226 * customisation via the MediaWiki namespace.
228 * @param $code string
232 public static function isValidCode( $code ) {
234 strcspn( $code, ":/\\\000" ) === strlen( $code )
235 && !preg_match( Title
::getTitleInvalidRegex(), $code );
239 * Returns true if a language code is of a valid form for the purposes of
240 * internal customisation of MediaWiki, via Messages*.php.
242 * @param $code string
247 public static function isValidBuiltInCode( $code ) {
248 return preg_match( '/^[a-z0-9-]+$/i', $code );
253 * @return String Name of the language class
255 public static function classFromCode( $code ) {
256 if ( $code == 'en' ) {
259 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
264 * Includes language class files
266 * @param $class string Name of the language class
268 public static function preloadLanguageClass( $class ) {
271 if ( $class === 'Language' ) {
275 if ( !defined( 'MW_COMPILED' ) ) {
276 // Preload base classes to work around APC/PHP5 bug
277 if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
278 include_once( "$IP/languages/classes/$class.deps.php" );
280 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
281 include_once( "$IP/languages/classes/$class.php" );
287 * Get the LocalisationCache instance
289 * @return LocalisationCache
291 public static function getLocalisationCache() {
292 if ( is_null( self
::$dataCache ) ) {
293 global $wgLocalisationCacheConf;
294 $class = $wgLocalisationCacheConf['class'];
295 self
::$dataCache = new $class( $wgLocalisationCacheConf );
297 return self
::$dataCache;
300 function __construct() {
301 $this->mConverter
= new FakeConverter( $this );
302 // Set the code to the name of the descendant
303 if ( get_class( $this ) == 'Language' ) {
306 $this->mCode
= str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
308 self
::getLocalisationCache();
312 * Reduce memory usage
314 function __destruct() {
315 foreach ( $this as $name => $value ) {
316 unset( $this->$name );
321 * Hook which will be called if this is the content language.
322 * Descendants can use this to register hook functions or modify globals
324 function initContLang() { }
327 * Same as getFallbacksFor for current language.
329 * @deprecated in 1.19
331 function getFallbackLanguageCode() {
332 wfDeprecated( __METHOD__
);
333 return self
::getFallbackFor( $this->mCode
);
340 function getFallbackLanguages() {
341 return self
::getFallbacksFor( $this->mCode
);
345 * Exports $wgBookstoreListEn
348 function getBookstoreList() {
349 return self
::$dataCache->getItem( $this->mCode
, 'bookstoreList' );
355 public function getNamespaces() {
356 if ( is_null( $this->namespaceNames
) ) {
357 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
359 $this->namespaceNames
= self
::$dataCache->getItem( $this->mCode
, 'namespaceNames' );
360 $validNamespaces = MWNamespace
::getCanonicalNamespaces();
362 $this->namespaceNames
= $wgExtraNamespaces +
$this->namespaceNames +
$validNamespaces;
364 $this->namespaceNames
[NS_PROJECT
] = $wgMetaNamespace;
365 if ( $wgMetaNamespaceTalk ) {
366 $this->namespaceNames
[NS_PROJECT_TALK
] = $wgMetaNamespaceTalk;
368 $talk = $this->namespaceNames
[NS_PROJECT_TALK
];
369 $this->namespaceNames
[NS_PROJECT_TALK
] =
370 $this->fixVariableInNamespace( $talk );
373 # Sometimes a language will be localised but not actually exist on this wiki.
374 foreach ( $this->namespaceNames
as $key => $text ) {
375 if ( !isset( $validNamespaces[$key] ) ) {
376 unset( $this->namespaceNames
[$key] );
380 # The above mixing may leave namespaces out of canonical order.
381 # Re-order by namespace ID number...
382 ksort( $this->namespaceNames
);
384 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames
) );
386 return $this->namespaceNames
;
390 * Arbitrarily set all of the namespace names at once. Mainly used for testing
391 * @param $namespaces Array of namespaces (id => name)
393 public function setNamespaces( array $namespaces ) {
394 $this->namespaceNames
= $namespaces;
395 $this->mNamespaceIds
= null;
399 * Resets all of the namespace caches. Mainly used for testing
401 public function resetNamespaces( ) {
402 $this->namespaceNames
= null;
403 $this->mNamespaceIds
= null;
404 $this->namespaceAliases
= null;
408 * A convenience function that returns the same thing as
409 * getNamespaces() except with the array values changed to ' '
410 * where it found '_', useful for producing output to be displayed
411 * e.g. in <select> forms.
415 function getFormattedNamespaces() {
416 $ns = $this->getNamespaces();
417 foreach ( $ns as $k => $v ) {
418 $ns[$k] = strtr( $v, '_', ' ' );
424 * Get a namespace value by key
426 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
427 * echo $mw_ns; // prints 'MediaWiki'
430 * @param $index Int: the array key of the namespace to return
431 * @return mixed, string if the namespace value exists, otherwise false
433 function getNsText( $index ) {
434 $ns = $this->getNamespaces();
435 return isset( $ns[$index] ) ?
$ns[$index] : false;
439 * A convenience function that returns the same thing as
440 * getNsText() except with '_' changed to ' ', useful for
443 * @param $index string
447 function getFormattedNsText( $index ) {
448 $ns = $this->getNsText( $index );
449 return strtr( $ns, '_', ' ' );
453 * Returns gender-dependent namespace alias if available.
454 * @param $index Int: namespace index
455 * @param $gender String: gender key (male, female... )
459 function getGenderNsText( $index, $gender ) {
460 global $wgExtraGenderNamespaces;
462 $ns = $wgExtraGenderNamespaces + self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
463 return isset( $ns[$index][$gender] ) ?
$ns[$index][$gender] : $this->getNsText( $index );
467 * Whether this language makes distinguishes genders for example in
472 function needsGenderDistinction() {
473 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
474 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
475 // $wgExtraGenderNamespaces overrides everything
477 } elseif ( isset( $wgExtraNamespaces[NS_USER
] ) && isset( $wgExtraNamespaces[NS_USER_TALK
] ) ) {
478 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
479 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
482 // Check what is in i18n files
483 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
484 return count( $aliases ) > 0;
489 * Get a namespace key by value, case insensitive.
490 * Only matches namespace names for the current language, not the
491 * canonical ones defined in Namespace.php.
493 * @param $text String
494 * @return mixed An integer if $text is a valid value otherwise false
496 function getLocalNsIndex( $text ) {
497 $lctext = $this->lc( $text );
498 $ids = $this->getNamespaceIds();
499 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
505 function getNamespaceAliases() {
506 if ( is_null( $this->namespaceAliases
) ) {
507 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceAliases' );
511 foreach ( $aliases as $name => $index ) {
512 if ( $index === NS_PROJECT_TALK
) {
513 unset( $aliases[$name] );
514 $name = $this->fixVariableInNamespace( $name );
515 $aliases[$name] = $index;
520 global $wgExtraGenderNamespaces;
521 $genders = $wgExtraGenderNamespaces +
(array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
522 foreach ( $genders as $index => $forms ) {
523 foreach ( $forms as $alias ) {
524 $aliases[$alias] = $index;
528 $this->namespaceAliases
= $aliases;
530 return $this->namespaceAliases
;
536 function getNamespaceIds() {
537 if ( is_null( $this->mNamespaceIds
) ) {
538 global $wgNamespaceAliases;
539 # Put namespace names and aliases into a hashtable.
540 # If this is too slow, then we should arrange it so that it is done
541 # before caching. The catch is that at pre-cache time, the above
542 # class-specific fixup hasn't been done.
543 $this->mNamespaceIds
= array();
544 foreach ( $this->getNamespaces() as $index => $name ) {
545 $this->mNamespaceIds
[$this->lc( $name )] = $index;
547 foreach ( $this->getNamespaceAliases() as $name => $index ) {
548 $this->mNamespaceIds
[$this->lc( $name )] = $index;
550 if ( $wgNamespaceAliases ) {
551 foreach ( $wgNamespaceAliases as $name => $index ) {
552 $this->mNamespaceIds
[$this->lc( $name )] = $index;
556 return $this->mNamespaceIds
;
560 * Get a namespace key by value, case insensitive. Canonical namespace
561 * names override custom ones defined for the current language.
563 * @param $text String
564 * @return mixed An integer if $text is a valid value otherwise false
566 function getNsIndex( $text ) {
567 $lctext = $this->lc( $text );
568 $ns = MWNamespace
::getCanonicalIndex( $lctext );
569 if ( $ns !== null ) {
572 $ids = $this->getNamespaceIds();
573 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
577 * short names for language variants used for language conversion links.
579 * @param $code String
580 * @param $usemsg bool Use the "variantname-xyz" message if it exists
583 function getVariantname( $code, $usemsg = true ) {
584 $msg = "variantname-$code";
585 if ( $usemsg && wfMessage( $msg )->exists() ) {
586 return $this->getMessageFromDB( $msg );
588 $name = self
::fetchLanguageName( $code );
590 return $name; # if it's defined as a language name, show that
592 # otherwise, output the language code
598 * @param $name string
601 function specialPage( $name ) {
602 $aliases = $this->getSpecialPageAliases();
603 if ( isset( $aliases[$name][0] ) ) {
604 $name = $aliases[$name][0];
606 return $this->getNsText( NS_SPECIAL
) . ':' . $name;
612 function getQuickbarSettings() {
614 $this->getMessage( 'qbsettings-none' ),
615 $this->getMessage( 'qbsettings-fixedleft' ),
616 $this->getMessage( 'qbsettings-fixedright' ),
617 $this->getMessage( 'qbsettings-floatingleft' ),
618 $this->getMessage( 'qbsettings-floatingright' ),
619 $this->getMessage( 'qbsettings-directionality' )
626 function getDatePreferences() {
627 return self
::$dataCache->getItem( $this->mCode
, 'datePreferences' );
633 function getDateFormats() {
634 return self
::$dataCache->getItem( $this->mCode
, 'dateFormats' );
638 * @return array|string
640 function getDefaultDateFormat() {
641 $df = self
::$dataCache->getItem( $this->mCode
, 'defaultDateFormat' );
642 if ( $df === 'dmy or mdy' ) {
643 global $wgAmericanDates;
644 return $wgAmericanDates ?
'mdy' : 'dmy';
653 function getDatePreferenceMigrationMap() {
654 return self
::$dataCache->getItem( $this->mCode
, 'datePreferenceMigrationMap' );
661 function getImageFile( $image ) {
662 return self
::$dataCache->getSubitem( $this->mCode
, 'imageFiles', $image );
668 function getExtraUserToggles() {
669 return (array)self
::$dataCache->getItem( $this->mCode
, 'extraUserToggles' );
676 function getUserToggle( $tog ) {
677 return $this->getMessageFromDB( "tog-$tog" );
681 * Get native language names, indexed by code.
682 * Only those defined in MediaWiki, no other data like CLDR.
683 * If $customisedOnly is true, only returns codes with a messages file
685 * @param $customisedOnly bool
688 * @deprecated in 1.20, use fetchLanguageNames()
690 public static function getLanguageNames( $customisedOnly = false ) {
691 return self
::fetchLanguageNames( null, $customisedOnly ?
'mwfile' : 'mw' );
695 * Get translated language names. This is done on best effort and
696 * by default this is exactly the same as Language::getLanguageNames.
697 * The CLDR extension provides translated names.
698 * @param $code String Language code.
699 * @return Array language code => language name
701 * @deprecated in 1.20, use fetchLanguageNames()
703 public static function getTranslatedLanguageNames( $code ) {
704 return self
::fetchLanguageNames( $code, 'all' );
708 * Get an array of language names, indexed by code.
709 * @param $inLanguage null|string: Code of language in which to return the names
710 * Use null for autonyms (native names)
711 * @param $include string:
712 * 'all' all available languages
713 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames
714 * 'mwfile' only if the language is in 'mw' *and* has a message file
715 * @return array|bool: language code => language name, false if $include is wrong
718 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
719 global $wgExtraLanguageNames;
720 static $coreLanguageNames;
722 if ( $coreLanguageNames === null ) {
723 include( MWInit
::compiledPath( 'languages/Names.php' ) );
729 # TODO: also include when $inLanguage is null, when this code is more efficient
730 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
733 $mwNames = $wgExtraLanguageNames +
$coreLanguageNames;
734 foreach ( $mwNames as $mwCode => $mwName ) {
735 # - Prefer own MediaWiki native name when not using the hook
736 # TODO: prefer it always to make it consistent, but casing is different in CLDR
737 # - For other names just add if not added through the hook
738 if ( ( $mwCode === $inLanguage && !$inLanguage ) ||
!isset( $names[$mwCode] ) ) {
739 $names[$mwCode] = $mwName;
743 if ( $include === 'all' ) {
748 $coreCodes = array_keys( $mwNames );
749 foreach( $coreCodes as $coreCode ) {
750 $returnMw[$coreCode] = $names[$coreCode];
753 if( $include === 'mw' ) {
755 } elseif( $include === 'mwfile' ) {
756 $namesMwFile = array();
757 # We do this using a foreach over the codes instead of a directory
758 # loop so that messages files in extensions will work correctly.
759 foreach ( $returnMw as $code => $value ) {
760 if ( is_readable( self
::getMessagesFileName( $code ) ) ) {
761 $namesMwFile[$code] = $names[$code];
770 * @param $code string: The code of the language for which to get the name
771 * @param $inLanguage null|string: Code of language in which to return the name (null for autonyms)
772 * @param $include string: 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
773 * @return string: Language name or empty
776 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
777 $array = self
::fetchLanguageNames( $inLanguage, $include );
778 return !array_key_exists( $code, $array ) ?
'' : $array[$code];
782 * Get a message from the MediaWiki namespace.
784 * @param $msg String: message name
787 function getMessageFromDB( $msg ) {
788 return wfMsgExt( $msg, array( 'parsemag', 'language' => $this ) );
792 * Get the native language name of $code.
793 * Only if defined in MediaWiki, no other data like CLDR.
794 * @param $code string
796 * @deprecated in 1.20, use fetchLanguageName()
798 function getLanguageName( $code ) {
799 return self
::fetchLanguageName( $code );
806 function getMonthName( $key ) {
807 return $this->getMessageFromDB( self
::$mMonthMsgs[$key - 1] );
813 function getMonthNamesArray() {
814 $monthNames = array( '' );
815 for ( $i = 1; $i < 13; $i++
) {
816 $monthNames[] = $this->getMonthName( $i );
825 function getMonthNameGen( $key ) {
826 return $this->getMessageFromDB( self
::$mMonthGenMsgs[$key - 1] );
833 function getMonthAbbreviation( $key ) {
834 return $this->getMessageFromDB( self
::$mMonthAbbrevMsgs[$key - 1] );
840 function getMonthAbbreviationsArray() {
841 $monthNames = array( '' );
842 for ( $i = 1; $i < 13; $i++
) {
843 $monthNames[] = $this->getMonthAbbreviation( $i );
852 function getWeekdayName( $key ) {
853 return $this->getMessageFromDB( self
::$mWeekdayMsgs[$key - 1] );
860 function getWeekdayAbbreviation( $key ) {
861 return $this->getMessageFromDB( self
::$mWeekdayAbbrevMsgs[$key - 1] );
868 function getIranianCalendarMonthName( $key ) {
869 return $this->getMessageFromDB( self
::$mIranianCalendarMonthMsgs[$key - 1] );
876 function getHebrewCalendarMonthName( $key ) {
877 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthMsgs[$key - 1] );
884 function getHebrewCalendarMonthNameGen( $key ) {
885 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthGenMsgs[$key - 1] );
892 function getHijriCalendarMonthName( $key ) {
893 return $this->getMessageFromDB( self
::$mHijriCalendarMonthMsgs[$key - 1] );
897 * This is a workalike of PHP's date() function, but with better
898 * internationalisation, a reduced set of format characters, and a better
901 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
902 * PHP manual for definitions. There are a number of extensions, which
905 * xn Do not translate digits of the next numeric format character
906 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
907 * xr Use roman numerals for the next numeric format character
908 * xh Use hebrew numerals for the next numeric format character
910 * xg Genitive month name
912 * xij j (day number) in Iranian calendar
913 * xiF F (month name) in Iranian calendar
914 * xin n (month number) in Iranian calendar
915 * xiy y (two digit year) in Iranian calendar
916 * xiY Y (full year) in Iranian calendar
918 * xjj j (day number) in Hebrew calendar
919 * xjF F (month name) in Hebrew calendar
920 * xjt t (days in month) in Hebrew calendar
921 * xjx xg (genitive month name) in Hebrew calendar
922 * xjn n (month number) in Hebrew calendar
923 * xjY Y (full year) in Hebrew calendar
925 * xmj j (day number) in Hijri calendar
926 * xmF F (month name) in Hijri calendar
927 * xmn n (month number) in Hijri calendar
928 * xmY Y (full year) in Hijri calendar
930 * xkY Y (full year) in Thai solar calendar. Months and days are
931 * identical to the Gregorian calendar
932 * xoY Y (full year) in Minguo calendar or Juche year.
933 * Months and days are identical to the
935 * xtY Y (full year) in Japanese nengo. Months and days are
936 * identical to the Gregorian calendar
938 * Characters enclosed in double quotes will be considered literal (with
939 * the quotes themselves removed). Unmatched quotes will be considered
940 * literal quotes. Example:
942 * "The month is" F => The month is January
945 * Backslash escaping is also supported.
947 * Input timestamp is assumed to be pre-normalized to the desired local
950 * @param $format String
951 * @param $ts String: 14-character timestamp
954 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
958 function sprintfDate( $format, $ts ) {
971 for ( $p = 0; $p < strlen( $format ); $p++
) {
974 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
975 $code .= $format[++
$p];
978 if ( ( $code === 'xi' ||
$code == 'xj' ||
$code == 'xk' ||
$code == 'xm' ||
$code == 'xo' ||
$code == 'xt' ) && $p < strlen( $format ) - 1 ) {
979 $code .= $format[++
$p];
990 $rawToggle = !$rawToggle;
999 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1002 if ( !$hebrew ) $hebrew = self
::tsToHebrew( $ts );
1003 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1006 $num = substr( $ts, 6, 2 );
1009 if ( !$unix ) $unix = wfTimestamp( TS_UNIX
, $ts );
1010 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) +
1 );
1013 $num = intval( substr( $ts, 6, 2 ) );
1017 $iranian = self
::tsToIranian( $ts );
1023 $hijri = self
::tsToHijri( $ts );
1029 $hebrew = self
::tsToHebrew( $ts );
1035 $unix = wfTimestamp( TS_UNIX
, $ts );
1037 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) +
1 );
1041 $unix = wfTimestamp( TS_UNIX
, $ts );
1043 $w = gmdate( 'w', $unix );
1048 $unix = wfTimestamp( TS_UNIX
, $ts );
1050 $num = gmdate( 'w', $unix );
1054 $unix = wfTimestamp( TS_UNIX
, $ts );
1056 $num = gmdate( 'z', $unix );
1060 $unix = wfTimestamp( TS_UNIX
, $ts );
1062 $num = gmdate( 'W', $unix );
1065 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1069 $iranian = self
::tsToIranian( $ts );
1071 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1075 $hijri = self
::tsToHijri( $ts );
1077 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1081 $hebrew = self
::tsToHebrew( $ts );
1083 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1086 $num = substr( $ts, 4, 2 );
1089 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1092 $num = intval( substr( $ts, 4, 2 ) );
1096 $iranian = self
::tsToIranian( $ts );
1102 $hijri = self
::tsToHijri ( $ts );
1108 $hebrew = self
::tsToHebrew( $ts );
1114 $unix = wfTimestamp( TS_UNIX
, $ts );
1116 $num = gmdate( 't', $unix );
1120 $hebrew = self
::tsToHebrew( $ts );
1126 $unix = wfTimestamp( TS_UNIX
, $ts );
1128 $num = gmdate( 'L', $unix );
1132 $unix = wfTimestamp( TS_UNIX
, $ts );
1134 $num = gmdate( 'o', $unix );
1137 $num = substr( $ts, 0, 4 );
1141 $iranian = self
::tsToIranian( $ts );
1147 $hijri = self
::tsToHijri( $ts );
1153 $hebrew = self
::tsToHebrew( $ts );
1159 $thai = self
::tsToYear( $ts, 'thai' );
1165 $minguo = self
::tsToYear( $ts, 'minguo' );
1171 $tenno = self
::tsToYear( $ts, 'tenno' );
1176 $num = substr( $ts, 2, 2 );
1180 $iranian = self
::tsToIranian( $ts );
1182 $num = substr( $iranian[0], -2 );
1185 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'am' : 'pm';
1188 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'AM' : 'PM';
1191 $h = substr( $ts, 8, 2 );
1192 $num = $h %
12 ?
$h %
12 : 12;
1195 $num = intval( substr( $ts, 8, 2 ) );
1198 $h = substr( $ts, 8, 2 );
1199 $num = sprintf( '%02d', $h %
12 ?
$h %
12 : 12 );
1202 $num = substr( $ts, 8, 2 );
1205 $num = substr( $ts, 10, 2 );
1208 $num = substr( $ts, 12, 2 );
1212 $unix = wfTimestamp( TS_UNIX
, $ts );
1214 $s .= gmdate( 'c', $unix );
1218 $unix = wfTimestamp( TS_UNIX
, $ts );
1220 $s .= gmdate( 'r', $unix );
1224 $unix = wfTimestamp( TS_UNIX
, $ts );
1229 # Backslash escaping
1230 if ( $p < strlen( $format ) - 1 ) {
1231 $s .= $format[++
$p];
1238 if ( $p < strlen( $format ) - 1 ) {
1239 $endQuote = strpos( $format, '"', $p +
1 );
1240 if ( $endQuote === false ) {
1241 # No terminating quote, assume literal "
1244 $s .= substr( $format, $p +
1, $endQuote - $p - 1 );
1248 # Quote at end of string, assume literal "
1255 if ( $num !== false ) {
1256 if ( $rawToggle ||
$raw ) {
1259 } elseif ( $roman ) {
1260 $s .= self
::romanNumeral( $num );
1262 } elseif ( $hebrewNum ) {
1263 $s .= self
::hebrewNumeral( $num );
1266 $s .= $this->formatNum( $num, true );
1273 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1274 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1277 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1278 * Gregorian dates to Iranian dates. Originally written in C, it
1279 * is released under the terms of GNU Lesser General Public
1280 * License. Conversion to PHP was performed by Niklas Laxström.
1282 * Link: http://www.farsiweb.info/jalali/jalali.c
1288 private static function tsToIranian( $ts ) {
1289 $gy = substr( $ts, 0, 4 ) -1600;
1290 $gm = substr( $ts, 4, 2 ) -1;
1291 $gd = substr( $ts, 6, 2 ) -1;
1293 # Days passed from the beginning (including leap years)
1295 +
floor( ( $gy +
3 ) / 4 )
1296 - floor( ( $gy +
99 ) / 100 )
1297 +
floor( ( $gy +
399 ) / 400 );
1299 // Add days of the past months of this year
1300 for ( $i = 0; $i < $gm; $i++
) {
1301 $gDayNo +
= self
::$GREG_DAYS[$i];
1305 if ( $gm > 1 && ( ( $gy %
4 === 0 && $gy %
100 !== 0 ||
( $gy %
400 == 0 ) ) ) ) {
1309 // Days passed in current month
1310 $gDayNo +
= (int)$gd;
1312 $jDayNo = $gDayNo - 79;
1314 $jNp = floor( $jDayNo / 12053 );
1317 $jy = 979 +
33 * $jNp +
4 * floor( $jDayNo / 1461 );
1320 if ( $jDayNo >= 366 ) {
1321 $jy +
= floor( ( $jDayNo - 1 ) / 365 );
1322 $jDayNo = floor( ( $jDayNo - 1 ) %
365 );
1325 for ( $i = 0; $i < 11 && $jDayNo >= self
::$IRANIAN_DAYS[$i]; $i++
) {
1326 $jDayNo -= self
::$IRANIAN_DAYS[$i];
1332 return array( $jy, $jm, $jd );
1336 * Converting Gregorian dates to Hijri dates.
1338 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1340 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1346 private static function tsToHijri( $ts ) {
1347 $year = substr( $ts, 0, 4 );
1348 $month = substr( $ts, 4, 2 );
1349 $day = substr( $ts, 6, 2 );
1357 ( $zy > 1582 ) ||
( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1358 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1361 $zjd = (int)( ( 1461 * ( $zy +
4800 +
(int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1362 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1363 (int)( ( 3 * (int)( ( ( $zy +
4900 +
(int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1366 $zjd = 367 * $zy - (int)( ( 7 * ( $zy +
5001 +
(int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1367 (int)( ( 275 * $zm ) / 9 ) +
$zd +
1729777;
1370 $zl = $zjd -1948440 +
10632;
1371 $zn = (int)( ( $zl - 1 ) / 10631 );
1372 $zl = $zl - 10631 * $zn +
354;
1373 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1374 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) +
29;
1375 $zm = (int)( ( 24 * $zl ) / 709 );
1376 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1377 $zy = 30 * $zn +
$zj - 30;
1379 return array( $zy, $zm, $zd );
1383 * Converting Gregorian dates to Hebrew dates.
1385 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1386 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1387 * to translate the relevant functions into PHP and release them under
1390 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1391 * and Adar II is 14. In a non-leap year, Adar is 6.
1397 private static function tsToHebrew( $ts ) {
1399 $year = substr( $ts, 0, 4 );
1400 $month = substr( $ts, 4, 2 );
1401 $day = substr( $ts, 6, 2 );
1403 # Calculate Hebrew year
1404 $hebrewYear = $year +
3760;
1406 # Month number when September = 1, August = 12
1408 if ( $month > 12 ) {
1415 # Calculate day of year from 1 September
1417 for ( $i = 1; $i < $month; $i++
) {
1421 # Check if the year is leap
1422 if ( $year %
400 == 0 ||
( $year %
4 == 0 && $year %
100 > 0 ) ) {
1425 } elseif ( $i == 8 ||
$i == 10 ||
$i == 1 ||
$i == 3 ) {
1432 # Calculate the start of the Hebrew year
1433 $start = self
::hebrewYearStart( $hebrewYear );
1435 # Calculate next year's start
1436 if ( $dayOfYear <= $start ) {
1437 # Day is before the start of the year - it is the previous year
1439 $nextStart = $start;
1443 # Add days since previous year's 1 September
1445 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1449 # Start of the new (previous) year
1450 $start = self
::hebrewYearStart( $hebrewYear );
1453 $nextStart = self
::hebrewYearStart( $hebrewYear +
1 );
1456 # Calculate Hebrew day of year
1457 $hebrewDayOfYear = $dayOfYear - $start;
1459 # Difference between year's days
1460 $diff = $nextStart - $start;
1461 # Add 12 (or 13 for leap years) days to ignore the difference between
1462 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1463 # difference is only about the year type
1464 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1470 # Check the year pattern, and is leap year
1471 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1472 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1473 # and non-leap years
1474 $yearPattern = $diff %
30;
1475 # Check if leap year
1476 $isLeap = $diff >= 30;
1478 # Calculate day in the month from number of day in the Hebrew year
1479 # Don't check Adar - if the day is not in Adar, we will stop before;
1480 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1481 $hebrewDay = $hebrewDayOfYear;
1484 while ( $hebrewMonth <= 12 ) {
1485 # Calculate days in this month
1486 if ( $isLeap && $hebrewMonth == 6 ) {
1487 # Adar in a leap year
1489 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1491 if ( $hebrewDay <= $days ) {
1495 # Subtract the days of Adar I
1496 $hebrewDay -= $days;
1499 if ( $hebrewDay <= $days ) {
1505 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1506 # Cheshvan in a complete year (otherwise as the rule below)
1508 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1509 # Kislev in an incomplete year (otherwise as the rule below)
1512 # Odd months have 30 days, even have 29
1513 $days = 30 - ( $hebrewMonth - 1 ) %
2;
1515 if ( $hebrewDay <= $days ) {
1516 # In the current month
1519 # Subtract the days of the current month
1520 $hebrewDay -= $days;
1521 # Try in the next month
1526 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1530 * This calculates the Hebrew year start, as days since 1 September.
1531 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1532 * Used for Hebrew date.
1538 private static function hebrewYearStart( $year ) {
1539 $a = intval( ( 12 * ( $year - 1 ) +
17 ) %
19 );
1540 $b = intval( ( $year - 1 ) %
4 );
1541 $m = 32.044093161144 +
1.5542417966212 * $a +
$b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1545 $Mar = intval( $m );
1551 $c = intval( ( $Mar +
3 * ( $year - 1 ) +
5 * $b +
5 ) %
7 );
1552 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1554 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1556 } elseif ( $c == 2 ||
$c == 4 ||
$c == 6 ) {
1560 $Mar +
= intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1565 * Algorithm to convert Gregorian dates to Thai solar dates,
1566 * Minguo dates or Minguo dates.
1568 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1569 * http://en.wikipedia.org/wiki/Minguo_calendar
1570 * http://en.wikipedia.org/wiki/Japanese_era_name
1572 * @param $ts String: 14-character timestamp
1573 * @param $cName String: calender name
1574 * @return Array: converted year, month, day
1576 private static function tsToYear( $ts, $cName ) {
1577 $gy = substr( $ts, 0, 4 );
1578 $gm = substr( $ts, 4, 2 );
1579 $gd = substr( $ts, 6, 2 );
1581 if ( !strcmp( $cName, 'thai' ) ) {
1583 # Add 543 years to the Gregorian calendar
1584 # Months and days are identical
1585 $gy_offset = $gy +
543;
1586 } elseif ( ( !strcmp( $cName, 'minguo' ) ) ||
!strcmp( $cName, 'juche' ) ) {
1588 # Deduct 1911 years from the Gregorian calendar
1589 # Months and days are identical
1590 $gy_offset = $gy - 1911;
1591 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1592 # Nengō dates up to Meiji period
1593 # Deduct years from the Gregorian calendar
1594 # depending on the nengo periods
1595 # Months and days are identical
1596 if ( ( $gy < 1912 ) ||
( ( $gy == 1912 ) && ( $gm < 7 ) ) ||
( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
1598 $gy_gannen = $gy - 1868 +
1;
1599 $gy_offset = $gy_gannen;
1600 if ( $gy_gannen == 1 ) {
1603 $gy_offset = '明治' . $gy_offset;
1605 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1606 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1607 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1608 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1609 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1613 $gy_gannen = $gy - 1912 +
1;
1614 $gy_offset = $gy_gannen;
1615 if ( $gy_gannen == 1 ) {
1618 $gy_offset = '大正' . $gy_offset;
1620 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1621 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1622 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1626 $gy_gannen = $gy - 1926 +
1;
1627 $gy_offset = $gy_gannen;
1628 if ( $gy_gannen == 1 ) {
1631 $gy_offset = '昭和' . $gy_offset;
1634 $gy_gannen = $gy - 1989 +
1;
1635 $gy_offset = $gy_gannen;
1636 if ( $gy_gannen == 1 ) {
1639 $gy_offset = '平成' . $gy_offset;
1645 return array( $gy_offset, $gm, $gd );
1649 * Roman number formatting up to 3000
1655 static function romanNumeral( $num ) {
1656 static $table = array(
1657 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1658 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1659 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1660 array( '', 'M', 'MM', 'MMM' )
1663 $num = intval( $num );
1664 if ( $num > 3000 ||
$num <= 0 ) {
1669 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1670 if ( $num >= $pow10 ) {
1671 $s .= $table[$i][(int)floor( $num / $pow10 )];
1673 $num = $num %
$pow10;
1679 * Hebrew Gematria number formatting up to 9999
1685 static function hebrewNumeral( $num ) {
1686 static $table = array(
1687 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1688 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1689 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1690 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1693 $num = intval( $num );
1694 if ( $num > 9999 ||
$num <= 0 ) {
1699 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1700 if ( $num >= $pow10 ) {
1701 if ( $num == 15 ||
$num == 16 ) {
1702 $s .= $table[0][9] . $table[0][$num - 9];
1705 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1706 if ( $pow10 == 1000 ) {
1711 $num = $num %
$pow10;
1713 if ( strlen( $s ) == 2 ) {
1716 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1717 $str .= substr( $s, strlen( $s ) - 2, 2 );
1719 $start = substr( $str, 0, strlen( $str ) - 2 );
1720 $end = substr( $str, strlen( $str ) - 2 );
1723 $str = $start . 'ך';
1726 $str = $start . 'ם';
1729 $str = $start . 'ן';
1732 $str = $start . 'ף';
1735 $str = $start . 'ץ';
1742 * Used by date() and time() to adjust the time output.
1744 * @param $ts Int the time in date('YmdHis') format
1745 * @param $tz Mixed: adjust the time by this amount (default false, mean we
1746 * get user timecorrection setting)
1749 function userAdjust( $ts, $tz = false ) {
1750 global $wgUser, $wgLocalTZoffset;
1752 if ( $tz === false ) {
1753 $tz = $wgUser->getOption( 'timecorrection' );
1756 $data = explode( '|', $tz, 3 );
1758 if ( $data[0] == 'ZoneInfo' ) {
1759 wfSuppressWarnings();
1760 $userTZ = timezone_open( $data[2] );
1761 wfRestoreWarnings();
1762 if ( $userTZ !== false ) {
1763 $date = date_create( $ts, timezone_open( 'UTC' ) );
1764 date_timezone_set( $date, $userTZ );
1765 $date = date_format( $date, 'YmdHis' );
1768 # Unrecognized timezone, default to 'Offset' with the stored offset.
1769 $data[0] = 'Offset';
1773 if ( $data[0] == 'System' ||
$tz == '' ) {
1774 # Global offset in minutes.
1775 if ( isset( $wgLocalTZoffset ) ) {
1776 $minDiff = $wgLocalTZoffset;
1778 } elseif ( $data[0] == 'Offset' ) {
1779 $minDiff = intval( $data[1] );
1781 $data = explode( ':', $tz );
1782 if ( count( $data ) == 2 ) {
1783 $data[0] = intval( $data[0] );
1784 $data[1] = intval( $data[1] );
1785 $minDiff = abs( $data[0] ) * 60 +
$data[1];
1786 if ( $data[0] < 0 ) {
1787 $minDiff = -$minDiff;
1790 $minDiff = intval( $data[0] ) * 60;
1794 # No difference ? Return time unchanged
1795 if ( 0 == $minDiff ) {
1799 wfSuppressWarnings(); // E_STRICT system time bitching
1800 # Generate an adjusted date; take advantage of the fact that mktime
1801 # will normalize out-of-range values so we don't have to split $minDiff
1802 # into hours and minutes.
1804 (int)substr( $ts, 8, 2 ) ), # Hours
1805 (int)substr( $ts, 10, 2 ) +
$minDiff, # Minutes
1806 (int)substr( $ts, 12, 2 ), # Seconds
1807 (int)substr( $ts, 4, 2 ), # Month
1808 (int)substr( $ts, 6, 2 ), # Day
1809 (int)substr( $ts, 0, 4 ) ); # Year
1811 $date = date( 'YmdHis', $t );
1812 wfRestoreWarnings();
1818 * This is meant to be used by time(), date(), and timeanddate() to get
1819 * the date preference they're supposed to use, it should be used in
1823 * function timeanddate([...], $format = true) {
1824 * $datePreference = $this->dateFormat($format);
1829 * @param $usePrefs Mixed: if true, the user's preference is used
1830 * if false, the site/language default is used
1831 * if int/string, assumed to be a format.
1834 function dateFormat( $usePrefs = true ) {
1837 if ( is_bool( $usePrefs ) ) {
1839 $datePreference = $wgUser->getDatePreference();
1841 $datePreference = (string)User
::getDefaultOption( 'date' );
1844 $datePreference = (string)$usePrefs;
1848 if ( $datePreference == '' ) {
1852 return $datePreference;
1856 * Get a format string for a given type and preference
1857 * @param $type string May be date, time or both
1858 * @param $pref string The format name as it appears in Messages*.php
1862 function getDateFormatString( $type, $pref ) {
1863 if ( !isset( $this->dateFormatStrings
[$type][$pref] ) ) {
1864 if ( $pref == 'default' ) {
1865 $pref = $this->getDefaultDateFormat();
1866 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1868 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1869 if ( is_null( $df ) ) {
1870 $pref = $this->getDefaultDateFormat();
1871 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1874 $this->dateFormatStrings
[$type][$pref] = $df;
1876 return $this->dateFormatStrings
[$type][$pref];
1880 * @param $ts Mixed: the time format which needs to be turned into a
1881 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1882 * @param $adj Bool: whether to adjust the time output according to the
1883 * user configured offset ($timecorrection)
1884 * @param $format Mixed: true to use user's date format preference
1885 * @param $timecorrection String|bool the time offset as returned by
1886 * validateTimeZone() in Special:Preferences
1889 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1890 $ts = wfTimestamp( TS_MW
, $ts );
1892 $ts = $this->userAdjust( $ts, $timecorrection );
1894 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
1895 return $this->sprintfDate( $df, $ts );
1899 * @param $ts Mixed: the time format which needs to be turned into a
1900 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1901 * @param $adj Bool: whether to adjust the time output according to the
1902 * user configured offset ($timecorrection)
1903 * @param $format Mixed: true to use user's date format preference
1904 * @param $timecorrection String|bool the time offset as returned by
1905 * validateTimeZone() in Special:Preferences
1908 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1909 $ts = wfTimestamp( TS_MW
, $ts );
1911 $ts = $this->userAdjust( $ts, $timecorrection );
1913 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
1914 return $this->sprintfDate( $df, $ts );
1918 * @param $ts Mixed: the time format which needs to be turned into a
1919 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1920 * @param $adj Bool: whether to adjust the time output according to the
1921 * user configured offset ($timecorrection)
1922 * @param $format Mixed: what format to return, if it's false output the
1923 * default one (default true)
1924 * @param $timecorrection String|bool the time offset as returned by
1925 * validateTimeZone() in Special:Preferences
1928 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
1929 $ts = wfTimestamp( TS_MW
, $ts );
1931 $ts = $this->userAdjust( $ts, $timecorrection );
1933 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
1934 return $this->sprintfDate( $df, $ts );
1938 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
1942 * @param integer $seconds The amount of seconds.
1943 * @param array $chosenIntervals The intervals to enable.
1947 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
1948 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
1950 $segments = array();
1952 foreach ( $intervals as $intervalName => $intervalValue ) {
1953 $message = new Message( 'duration-' . $intervalName, array( $intervalValue ) );
1954 $segments[] = $message->inLanguage( $this )->escaped();
1957 return $this->listToText( $segments );
1961 * Takes a number of seconds and returns an array with a set of corresponding intervals.
1962 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
1966 * @param integer $seconds The amount of seconds.
1967 * @param array $chosenIntervals The intervals to enable.
1971 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
1972 if ( empty( $chosenIntervals ) ) {
1973 $chosenIntervals = array( 'millennia', 'centuries', 'decades', 'years', 'days', 'hours', 'minutes', 'seconds' );
1976 $intervals = array_intersect_key( self
::$durationIntervals, array_flip( $chosenIntervals ) );
1977 $sortedNames = array_keys( $intervals );
1978 $smallestInterval = array_pop( $sortedNames );
1980 $segments = array();
1982 foreach ( $intervals as $name => $length ) {
1983 $value = floor( $seconds / $length );
1985 if ( $value > 0 ||
( $name == $smallestInterval && empty( $segments ) ) ) {
1986 $seconds -= $value * $length;
1987 $segments[$name] = $value;
1995 * Internal helper function for userDate(), userTime() and userTimeAndDate()
1997 * @param $type String: can be 'date', 'time' or 'both'
1998 * @param $ts Mixed: the time format which needs to be turned into a
1999 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2000 * @param $user User object used to get preferences for timezone and format
2001 * @param $options Array, can contain the following keys:
2002 * - 'timecorrection': time correction, can have the following values:
2003 * - true: use user's preference
2004 * - false: don't use time correction
2005 * - integer: value of time correction in minutes
2006 * - 'format': format to use, can have the following values:
2007 * - true: use user's preference
2008 * - false: use default preference
2009 * - string: format to use
2013 private function internalUserTimeAndDate( $type, $ts, User
$user, array $options ) {
2014 $ts = wfTimestamp( TS_MW
, $ts );
2015 $options +
= array( 'timecorrection' => true, 'format' => true );
2016 if ( $options['timecorrection'] !== false ) {
2017 if ( $options['timecorrection'] === true ) {
2018 $offset = $user->getOption( 'timecorrection' );
2020 $offset = $options['timecorrection'];
2022 $ts = $this->userAdjust( $ts, $offset );
2024 if ( $options['format'] === true ) {
2025 $format = $user->getDatePreference();
2027 $format = $options['format'];
2029 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2030 return $this->sprintfDate( $df, $ts );
2034 * Get the formatted date for the given timestamp and formatted for
2037 * @param $ts Mixed: the time format which needs to be turned into a
2038 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2039 * @param $user User object used to get preferences for timezone and format
2040 * @param $options Array, can contain the following keys:
2041 * - 'timecorrection': time correction, can have the following values:
2042 * - true: use user's preference
2043 * - false: don't use time correction
2044 * - integer: value of time correction in minutes
2045 * - 'format': format to use, can have the following values:
2046 * - true: use user's preference
2047 * - false: use default preference
2048 * - string: format to use
2052 public function userDate( $ts, User
$user, array $options = array() ) {
2053 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2057 * Get the formatted time for the given timestamp and formatted for
2060 * @param $ts Mixed: the time format which needs to be turned into a
2061 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2062 * @param $user User object used to get preferences for timezone and format
2063 * @param $options Array, can contain the following keys:
2064 * - 'timecorrection': time correction, can have the following values:
2065 * - true: use user's preference
2066 * - false: don't use time correction
2067 * - integer: value of time correction in minutes
2068 * - 'format': format to use, can have the following values:
2069 * - true: use user's preference
2070 * - false: use default preference
2071 * - string: format to use
2075 public function userTime( $ts, User
$user, array $options = array() ) {
2076 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2080 * Get the formatted date and time for the given timestamp and formatted for
2083 * @param $ts Mixed: the time format which needs to be turned into a
2084 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2085 * @param $user User object used to get preferences for timezone and format
2086 * @param $options Array, can contain the following keys:
2087 * - 'timecorrection': time correction, can have the following values:
2088 * - true: use user's preference
2089 * - false: don't use time correction
2090 * - integer: value of time correction in minutes
2091 * - 'format': format to use, can have the following values:
2092 * - true: use user's preference
2093 * - false: use default preference
2094 * - string: format to use
2098 public function userTimeAndDate( $ts, User
$user, array $options = array() ) {
2099 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2103 * @param $key string
2104 * @return array|null
2106 function getMessage( $key ) {
2107 return self
::$dataCache->getSubitem( $this->mCode
, 'messages', $key );
2113 function getAllMessages() {
2114 return self
::$dataCache->getItem( $this->mCode
, 'messages' );
2123 function iconv( $in, $out, $string ) {
2124 # This is a wrapper for iconv in all languages except esperanto,
2125 # which does some nasty x-conversions beforehand
2127 # Even with //IGNORE iconv can whine about illegal characters in
2128 # *input* string. We just ignore those too.
2129 # REF: http://bugs.php.net/bug.php?id=37166
2130 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2131 wfSuppressWarnings();
2132 $text = iconv( $in, $out . '//IGNORE', $string );
2133 wfRestoreWarnings();
2137 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2140 * @param $matches array
2141 * @return mixed|string
2143 function ucwordbreaksCallbackAscii( $matches ) {
2144 return $this->ucfirst( $matches[1] );
2148 * @param $matches array
2151 function ucwordbreaksCallbackMB( $matches ) {
2152 return mb_strtoupper( $matches[0] );
2156 * @param $matches array
2159 function ucCallback( $matches ) {
2160 list( $wikiUpperChars ) = self
::getCaseMaps();
2161 return strtr( $matches[1], $wikiUpperChars );
2165 * @param $matches array
2168 function lcCallback( $matches ) {
2169 list( , $wikiLowerChars ) = self
::getCaseMaps();
2170 return strtr( $matches[1], $wikiLowerChars );
2174 * @param $matches array
2177 function ucwordsCallbackMB( $matches ) {
2178 return mb_strtoupper( $matches[0] );
2182 * @param $matches array
2185 function ucwordsCallbackWiki( $matches ) {
2186 list( $wikiUpperChars ) = self
::getCaseMaps();
2187 return strtr( $matches[0], $wikiUpperChars );
2191 * Make a string's first character uppercase
2193 * @param $str string
2197 function ucfirst( $str ) {
2199 if ( $o < 96 ) { // if already uppercase...
2201 } elseif ( $o < 128 ) {
2202 return ucfirst( $str ); // use PHP's ucfirst()
2204 // fall back to more complex logic in case of multibyte strings
2205 return $this->uc( $str, true );
2210 * Convert a string to uppercase
2212 * @param $str string
2213 * @param $first bool
2217 function uc( $str, $first = false ) {
2218 if ( function_exists( 'mb_strtoupper' ) ) {
2220 if ( $this->isMultibyte( $str ) ) {
2221 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2223 return ucfirst( $str );
2226 return $this->isMultibyte( $str ) ?
mb_strtoupper( $str ) : strtoupper( $str );
2229 if ( $this->isMultibyte( $str ) ) {
2230 $x = $first ?
'^' : '';
2231 return preg_replace_callback(
2232 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2233 array( $this, 'ucCallback' ),
2237 return $first ?
ucfirst( $str ) : strtoupper( $str );
2243 * @param $str string
2244 * @return mixed|string
2246 function lcfirst( $str ) {
2249 return strval( $str );
2250 } elseif ( $o >= 128 ) {
2251 return $this->lc( $str, true );
2252 } elseif ( $o > 96 ) {
2255 $str[0] = strtolower( $str[0] );
2261 * @param $str string
2262 * @param $first bool
2263 * @return mixed|string
2265 function lc( $str, $first = false ) {
2266 if ( function_exists( 'mb_strtolower' ) ) {
2268 if ( $this->isMultibyte( $str ) ) {
2269 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2271 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2274 return $this->isMultibyte( $str ) ?
mb_strtolower( $str ) : strtolower( $str );
2277 if ( $this->isMultibyte( $str ) ) {
2278 $x = $first ?
'^' : '';
2279 return preg_replace_callback(
2280 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2281 array( $this, 'lcCallback' ),
2285 return $first ?
strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2291 * @param $str string
2294 function isMultibyte( $str ) {
2295 return (bool)preg_match( '/[\x80-\xff]/', $str );
2299 * @param $str string
2300 * @return mixed|string
2302 function ucwords( $str ) {
2303 if ( $this->isMultibyte( $str ) ) {
2304 $str = $this->lc( $str );
2306 // regexp to find first letter in each word (i.e. after each space)
2307 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2309 // function to use to capitalize a single char
2310 if ( function_exists( 'mb_strtoupper' ) ) {
2311 return preg_replace_callback(
2313 array( $this, 'ucwordsCallbackMB' ),
2317 return preg_replace_callback(
2319 array( $this, 'ucwordsCallbackWiki' ),
2324 return ucwords( strtolower( $str ) );
2329 * capitalize words at word breaks
2331 * @param $str string
2334 function ucwordbreaks( $str ) {
2335 if ( $this->isMultibyte( $str ) ) {
2336 $str = $this->lc( $str );
2338 // since \b doesn't work for UTF-8, we explicitely define word break chars
2339 $breaks = "[ \-\(\)\}\{\.,\?!]";
2341 // find first letter after word break
2342 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2344 if ( function_exists( 'mb_strtoupper' ) ) {
2345 return preg_replace_callback(
2347 array( $this, 'ucwordbreaksCallbackMB' ),
2351 return preg_replace_callback(
2353 array( $this, 'ucwordsCallbackWiki' ),
2358 return preg_replace_callback(
2359 '/\b([\w\x80-\xff]+)\b/',
2360 array( $this, 'ucwordbreaksCallbackAscii' ),
2367 * Return a case-folded representation of $s
2369 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2370 * and $s2 are the same except for the case of their characters. It is not
2371 * necessary for the value returned to make sense when displayed.
2373 * Do *not* perform any other normalisation in this function. If a caller
2374 * uses this function when it should be using a more general normalisation
2375 * function, then fix the caller.
2381 function caseFold( $s ) {
2382 return $this->uc( $s );
2389 function checkTitleEncoding( $s ) {
2390 if ( is_array( $s ) ) {
2391 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
2393 # Check for non-UTF-8 URLs
2394 $ishigh = preg_match( '/[\x80-\xff]/', $s );
2399 $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2400 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
2405 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2411 function fallback8bitEncoding() {
2412 return self
::$dataCache->getItem( $this->mCode
, 'fallback8bitEncoding' );
2416 * Most writing systems use whitespace to break up words.
2417 * Some languages such as Chinese don't conventionally do this,
2418 * which requires special handling when breaking up words for
2423 function hasWordBreaks() {
2428 * Some languages such as Chinese require word segmentation,
2429 * Specify such segmentation when overridden in derived class.
2431 * @param $string String
2434 function segmentByWord( $string ) {
2439 * Some languages have special punctuation need to be normalized.
2440 * Make such changes here.
2442 * @param $string String
2445 function normalizeForSearch( $string ) {
2446 return self
::convertDoubleWidth( $string );
2450 * convert double-width roman characters to single-width.
2451 * range: ff00-ff5f ~= 0020-007f
2453 * @param $string string
2457 protected static function convertDoubleWidth( $string ) {
2458 static $full = null;
2459 static $half = null;
2461 if ( $full === null ) {
2462 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2463 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2464 $full = str_split( $fullWidth, 3 );
2465 $half = str_split( $halfWidth );
2468 $string = str_replace( $full, $half, $string );
2473 * @param $string string
2474 * @param $pattern string
2477 protected static function insertSpace( $string, $pattern ) {
2478 $string = preg_replace( $pattern, " $1 ", $string );
2479 $string = preg_replace( '/ +/', ' ', $string );
2484 * @param $termsArray array
2487 function convertForSearchResult( $termsArray ) {
2488 # some languages, e.g. Chinese, need to do a conversion
2489 # in order for search results to be displayed correctly
2494 * Get the first character of a string.
2499 function firstChar( $s ) {
2502 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2503 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2508 if ( isset( $matches[1] ) ) {
2509 if ( strlen( $matches[1] ) != 3 ) {
2513 // Break down Hangul syllables to grab the first jamo
2514 $code = utf8ToCodepoint( $matches[1] );
2515 if ( $code < 0xac00 ||
0xd7a4 <= $code ) {
2517 } elseif ( $code < 0xb098 ) {
2518 return "\xe3\x84\xb1";
2519 } elseif ( $code < 0xb2e4 ) {
2520 return "\xe3\x84\xb4";
2521 } elseif ( $code < 0xb77c ) {
2522 return "\xe3\x84\xb7";
2523 } elseif ( $code < 0xb9c8 ) {
2524 return "\xe3\x84\xb9";
2525 } elseif ( $code < 0xbc14 ) {
2526 return "\xe3\x85\x81";
2527 } elseif ( $code < 0xc0ac ) {
2528 return "\xe3\x85\x82";
2529 } elseif ( $code < 0xc544 ) {
2530 return "\xe3\x85\x85";
2531 } elseif ( $code < 0xc790 ) {
2532 return "\xe3\x85\x87";
2533 } elseif ( $code < 0xcc28 ) {
2534 return "\xe3\x85\x88";
2535 } elseif ( $code < 0xce74 ) {
2536 return "\xe3\x85\x8a";
2537 } elseif ( $code < 0xd0c0 ) {
2538 return "\xe3\x85\x8b";
2539 } elseif ( $code < 0xd30c ) {
2540 return "\xe3\x85\x8c";
2541 } elseif ( $code < 0xd558 ) {
2542 return "\xe3\x85\x8d";
2544 return "\xe3\x85\x8e";
2551 function initEncoding() {
2552 # Some languages may have an alternate char encoding option
2553 # (Esperanto X-coding, Japanese furigana conversion, etc)
2554 # If this language is used as the primary content language,
2555 # an override to the defaults can be set here on startup.
2562 function recodeForEdit( $s ) {
2563 # For some languages we'll want to explicitly specify
2564 # which characters make it into the edit box raw
2565 # or are converted in some way or another.
2566 global $wgEditEncoding;
2567 if ( $wgEditEncoding == '' ||
$wgEditEncoding == 'UTF-8' ) {
2570 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2578 function recodeInput( $s ) {
2579 # Take the previous into account.
2580 global $wgEditEncoding;
2581 if ( $wgEditEncoding != '' ) {
2582 $enc = $wgEditEncoding;
2586 if ( $enc == 'UTF-8' ) {
2589 return $this->iconv( $enc, 'UTF-8', $s );
2594 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2595 * also cleans up certain backwards-compatible sequences, converting them
2596 * to the modern Unicode equivalent.
2598 * This is language-specific for performance reasons only.
2604 function normalize( $s ) {
2605 global $wgAllUnicodeFixes;
2606 $s = UtfNormal
::cleanUp( $s );
2607 if ( $wgAllUnicodeFixes ) {
2608 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2609 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2616 * Transform a string using serialized data stored in the given file (which
2617 * must be in the serialized subdirectory of $IP). The file contains pairs
2618 * mapping source characters to destination characters.
2620 * The data is cached in process memory. This will go faster if you have the
2621 * FastStringSearch extension.
2623 * @param $file string
2624 * @param $string string
2626 * @throws MWException
2629 function transformUsingPairFile( $file, $string ) {
2630 if ( !isset( $this->transformData
[$file] ) ) {
2631 $data = wfGetPrecompiledData( $file );
2632 if ( $data === false ) {
2633 throw new MWException( __METHOD__
. ": The transformation file $file is missing" );
2635 $this->transformData
[$file] = new ReplacementArray( $data );
2637 return $this->transformData
[$file]->replace( $string );
2641 * For right-to-left language support
2646 return self
::$dataCache->getItem( $this->mCode
, 'rtl' );
2650 * Return the correct HTML 'dir' attribute value for this language.
2654 return $this->isRTL() ?
'rtl' : 'ltr';
2658 * Return 'left' or 'right' as appropriate alignment for line-start
2659 * for this language's text direction.
2661 * Should be equivalent to CSS3 'start' text-align value....
2665 function alignStart() {
2666 return $this->isRTL() ?
'right' : 'left';
2670 * Return 'right' or 'left' as appropriate alignment for line-end
2671 * for this language's text direction.
2673 * Should be equivalent to CSS3 'end' text-align value....
2677 function alignEnd() {
2678 return $this->isRTL() ?
'left' : 'right';
2682 * A hidden direction mark (LRM or RLM), depending on the language direction.
2683 * Unlike getDirMark(), this function returns the character as an HTML entity.
2684 * This function should be used when the output is guaranteed to be HTML,
2685 * because it makes the output HTML source code more readable. When
2686 * the output is plain text or can be escaped, getDirMark() should be used.
2688 * @param $opposite Boolean Get the direction mark opposite to your language
2691 function getDirMarkEntity( $opposite = false ) {
2692 if ( $opposite ) { return $this->isRTL() ?
'‎' : '‏'; }
2693 return $this->isRTL() ?
'‏' : '‎';
2697 * A hidden direction mark (LRM or RLM), depending on the language direction.
2698 * This function produces them as invisible Unicode characters and
2699 * the output may be hard to read and debug, so it should only be used
2700 * when the output is plain text or can be escaped. When the output is
2701 * HTML, use getDirMarkEntity() instead.
2703 * @param $opposite Boolean Get the direction mark opposite to your language
2706 function getDirMark( $opposite = false ) {
2707 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
2708 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
2709 if ( $opposite ) { return $this->isRTL() ?
$lrm : $rlm; }
2710 return $this->isRTL() ?
$rlm : $lrm;
2716 function capitalizeAllNouns() {
2717 return self
::$dataCache->getItem( $this->mCode
, 'capitalizeAllNouns' );
2721 * An arrow, depending on the language direction
2725 function getArrow() {
2726 return $this->isRTL() ?
'←' : '→';
2730 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
2734 function linkPrefixExtension() {
2735 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixExtension' );
2741 function getMagicWords() {
2742 return self
::$dataCache->getItem( $this->mCode
, 'magicWords' );
2745 protected function doMagicHook() {
2746 if ( $this->mMagicHookDone
) {
2749 $this->mMagicHookDone
= true;
2750 wfProfileIn( 'LanguageGetMagic' );
2751 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions
, $this->getCode() ) );
2752 wfProfileOut( 'LanguageGetMagic' );
2756 * Fill a MagicWord object with data from here
2760 function getMagic( $mw ) {
2761 $this->doMagicHook();
2763 if ( isset( $this->mMagicExtensions
[$mw->mId
] ) ) {
2764 $rawEntry = $this->mMagicExtensions
[$mw->mId
];
2766 $magicWords = $this->getMagicWords();
2767 if ( isset( $magicWords[$mw->mId
] ) ) {
2768 $rawEntry = $magicWords[$mw->mId
];
2774 if ( !is_array( $rawEntry ) ) {
2775 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
2777 $mw->mCaseSensitive
= $rawEntry[0];
2778 $mw->mSynonyms
= array_slice( $rawEntry, 1 );
2783 * Add magic words to the extension array
2785 * @param $newWords array
2787 function addMagicWordsByLang( $newWords ) {
2788 $fallbackChain = $this->getFallbackLanguages();
2789 $fallbackChain = array_reverse( $fallbackChain );
2790 foreach ( $fallbackChain as $code ) {
2791 if ( isset( $newWords[$code] ) ) {
2792 $this->mMagicExtensions
= $newWords[$code] +
$this->mMagicExtensions
;
2798 * Get special page names, as an associative array
2799 * case folded alias => real name
2801 function getSpecialPageAliases() {
2802 // Cache aliases because it may be slow to load them
2803 if ( is_null( $this->mExtendedSpecialPageAliases
) ) {
2805 $this->mExtendedSpecialPageAliases
=
2806 self
::$dataCache->getItem( $this->mCode
, 'specialPageAliases' );
2807 wfRunHooks( 'LanguageGetSpecialPageAliases',
2808 array( &$this->mExtendedSpecialPageAliases
, $this->getCode() ) );
2811 return $this->mExtendedSpecialPageAliases
;
2815 * Italic is unsuitable for some languages
2817 * @param $text String: the text to be emphasized.
2820 function emphasize( $text ) {
2821 return "<em>$text</em>";
2825 * Normally we output all numbers in plain en_US style, that is
2826 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
2827 * point twohundredthirtyfive. However this is not suitable for all
2828 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
2829 * Icelandic just want to use commas instead of dots, and dots instead
2830 * of commas like "293.291,235".
2832 * An example of this function being called:
2834 * wfMsg( 'message', $wgLang->formatNum( $num ) )
2837 * See LanguageGu.php for the Gujarati implementation and
2838 * $separatorTransformTable on MessageIs.php for
2839 * the , => . and . => , implementation.
2841 * @todo check if it's viable to use localeconv() for the decimal
2843 * @param $number Mixed: the string to be formatted, should be an integer
2844 * or a floating point number.
2845 * @param $nocommafy Bool: set to true for special numbers like dates
2848 public function formatNum( $number, $nocommafy = false ) {
2849 global $wgTranslateNumerals;
2850 if ( !$nocommafy ) {
2851 $number = $this->commafy( $number );
2852 $s = $this->separatorTransformTable();
2854 $number = strtr( $number, $s );
2858 if ( $wgTranslateNumerals ) {
2859 $s = $this->digitTransformTable();
2861 $number = strtr( $number, $s );
2869 * @param $number string
2872 function parseFormattedNumber( $number ) {
2873 $s = $this->digitTransformTable();
2875 $number = strtr( $number, array_flip( $s ) );
2878 $s = $this->separatorTransformTable();
2880 $number = strtr( $number, array_flip( $s ) );
2883 $number = strtr( $number, array( ',' => '' ) );
2888 * Adds commas to a given number
2893 function commafy( $_ ) {
2894 $digitGroupingPattern = $this->digitGroupingPattern();
2895 if ( $_ === null ) {
2899 if ( !$digitGroupingPattern ||
$digitGroupingPattern === "###,###,###" ) {
2900 // default grouping is at thousands, use the same for ###,###,### pattern too.
2901 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $_ ) ) );
2903 // Ref: http://cldr.unicode.org/translation/number-patterns
2905 if ( intval( $_ ) < 0 ) {
2906 // For negative numbers apply the algorithm like positive number and add sign.
2908 $_ = substr( $_, 1 );
2910 $numberpart = array();
2911 $decimalpart = array();
2912 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
2913 preg_match( "/\d+/", $_, $numberpart );
2914 preg_match( "/\.\d*/", $_, $decimalpart );
2915 $groupedNumber = ( count( $decimalpart ) > 0 ) ?
$decimalpart[0]:"";
2916 if ( $groupedNumber === $_ ) {
2917 // the string does not have any number part. Eg: .12345
2918 return $sign . $groupedNumber;
2920 $start = $end = strlen( $numberpart[0] );
2921 while ( $start > 0 ) {
2922 $match = $matches[0][$numMatches -1] ;
2923 $matchLen = strlen( $match );
2924 $start = $end - $matchLen;
2928 $groupedNumber = substr( $_ , $start, $end -$start ) . $groupedNumber ;
2930 if ( $numMatches > 1 ) {
2931 // use the last pattern for the rest of the number
2935 $groupedNumber = "," . $groupedNumber;
2938 return $sign . $groupedNumber;
2944 function digitGroupingPattern() {
2945 return self
::$dataCache->getItem( $this->mCode
, 'digitGroupingPattern' );
2951 function digitTransformTable() {
2952 return self
::$dataCache->getItem( $this->mCode
, 'digitTransformTable' );
2958 function separatorTransformTable() {
2959 return self
::$dataCache->getItem( $this->mCode
, 'separatorTransformTable' );
2963 * Take a list of strings and build a locale-friendly comma-separated
2964 * list, using the local comma-separator message.
2965 * The last two strings are chained with an "and".
2970 function listToText( array $l ) {
2972 $m = count( $l ) - 1;
2974 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
2976 for ( $i = $m; $i >= 0; $i-- ) {
2979 } elseif ( $i == $m - 1 ) {
2980 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
2982 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
2990 * Take a list of strings and build a locale-friendly comma-separated
2991 * list, using the local comma-separator message.
2992 * @param $list array of strings to put in a comma list
2995 function commaList( array $list ) {
2999 array( 'parsemag', 'escapenoentities', 'language' => $this )
3006 * Take a list of strings and build a locale-friendly semicolon-separated
3007 * list, using the local semicolon-separator message.
3008 * @param $list array of strings to put in a semicolon list
3011 function semicolonList( array $list ) {
3014 'semicolon-separator',
3015 array( 'parsemag', 'escapenoentities', 'language' => $this )
3022 * Same as commaList, but separate it with the pipe instead.
3023 * @param $list array of strings to put in a pipe list
3026 function pipeList( array $list ) {
3030 array( 'escapenoentities', 'language' => $this )
3037 * Truncate a string to a specified length in bytes, appending an optional
3038 * string (e.g. for ellipses)
3040 * The database offers limited byte lengths for some columns in the database;
3041 * multi-byte character sets mean we need to ensure that only whole characters
3042 * are included, otherwise broken characters can be passed to the user
3044 * If $length is negative, the string will be truncated from the beginning
3046 * @param $string String to truncate
3047 * @param $length Int: maximum length (including ellipses)
3048 * @param $ellipsis String to append to the truncated text
3049 * @param $adjustLength Boolean: Subtract length of ellipsis from $length.
3050 * $adjustLength was introduced in 1.18, before that behaved as if false.
3053 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3054 # Use the localized ellipsis character
3055 if ( $ellipsis == '...' ) {
3056 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
3058 # Check if there is no need to truncate
3059 if ( $length == 0 ) {
3060 return $ellipsis; // convention
3061 } elseif ( strlen( $string ) <= abs( $length ) ) {
3062 return $string; // no need to truncate
3064 $stringOriginal = $string;
3065 # If ellipsis length is >= $length then we can't apply $adjustLength
3066 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3067 $string = $ellipsis; // this can be slightly unexpected
3068 # Otherwise, truncate and add ellipsis...
3070 $eLength = $adjustLength ?
strlen( $ellipsis ) : 0;
3071 if ( $length > 0 ) {
3072 $length -= $eLength;
3073 $string = substr( $string, 0, $length ); // xyz...
3074 $string = $this->removeBadCharLast( $string );
3075 $string = $string . $ellipsis;
3077 $length +
= $eLength;
3078 $string = substr( $string, $length ); // ...xyz
3079 $string = $this->removeBadCharFirst( $string );
3080 $string = $ellipsis . $string;
3083 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3084 # This check is *not* redundant if $adjustLength, due to the single case where
3085 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3086 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3089 return $stringOriginal;
3094 * Remove bytes that represent an incomplete Unicode character
3095 * at the end of string (e.g. bytes of the char are missing)
3097 * @param $string String
3100 protected function removeBadCharLast( $string ) {
3101 if ( $string != '' ) {
3102 $char = ord( $string[strlen( $string ) - 1] );
3104 if ( $char >= 0xc0 ) {
3105 # We got the first byte only of a multibyte char; remove it.
3106 $string = substr( $string, 0, -1 );
3107 } elseif ( $char >= 0x80 &&
3108 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3109 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) )
3111 # We chopped in the middle of a character; remove it
3119 * Remove bytes that represent an incomplete Unicode character
3120 * at the start of string (e.g. bytes of the char are missing)
3122 * @param $string String
3125 protected function removeBadCharFirst( $string ) {
3126 if ( $string != '' ) {
3127 $char = ord( $string[0] );
3128 if ( $char >= 0x80 && $char < 0xc0 ) {
3129 # We chopped in the middle of a character; remove the whole thing
3130 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3137 * Truncate a string of valid HTML to a specified length in bytes,
3138 * appending an optional string (e.g. for ellipses), and return valid HTML
3140 * This is only intended for styled/linked text, such as HTML with
3141 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3142 * Also, this will not detect things like "display:none" CSS.
3144 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3146 * @param string $text HTML string to truncate
3147 * @param int $length (zero/positive) Maximum length (including ellipses)
3148 * @param string $ellipsis String to append to the truncated text
3151 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3152 # Use the localized ellipsis character
3153 if ( $ellipsis == '...' ) {
3154 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
3156 # Check if there is clearly no need to truncate
3157 if ( $length <= 0 ) {
3158 return $ellipsis; // no text shown, nothing to format (convention)
3159 } elseif ( strlen( $text ) <= $length ) {
3160 return $text; // string short enough even *with* HTML (short-circuit)
3163 $dispLen = 0; // innerHTML legth so far
3164 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3165 $tagType = 0; // 0-open, 1-close
3166 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3167 $entityState = 0; // 0-not entity, 1-entity
3168 $tag = $ret = ''; // accumulated tag name, accumulated result string
3169 $openTags = array(); // open tag stack
3170 $maybeState = null; // possible truncation state
3172 $textLen = strlen( $text );
3173 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3174 for ( $pos = 0; true; ++
$pos ) {
3175 # Consider truncation once the display length has reached the maximim.
3176 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3177 # Check that we're not in the middle of a bracket/entity...
3178 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3179 if ( !$testingEllipsis ) {
3180 $testingEllipsis = true;
3181 # Save where we are; we will truncate here unless there turn out to
3182 # be so few remaining characters that truncation is not necessary.
3183 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3184 $maybeState = array( $ret, $openTags ); // save state
3186 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3187 # String in fact does need truncation, the truncation point was OK.
3188 list( $ret, $openTags ) = $maybeState; // reload state
3189 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3190 $ret .= $ellipsis; // add ellipsis
3194 if ( $pos >= $textLen ) break; // extra iteration just for above checks
3196 # Read the next char...
3198 $lastCh = $pos ?
$text[$pos - 1] : '';
3199 $ret .= $ch; // add to result string
3201 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3202 $entityState = 0; // for bad HTML
3203 $bracketState = 1; // tag started (checking for backslash)
3204 } elseif ( $ch == '>' ) {
3205 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3206 $entityState = 0; // for bad HTML
3207 $bracketState = 0; // out of brackets
3208 } elseif ( $bracketState == 1 ) {
3210 $tagType = 1; // close tag (e.g. "</span>")
3212 $tagType = 0; // open tag (e.g. "<span>")
3215 $bracketState = 2; // building tag name
3216 } elseif ( $bracketState == 2 ) {
3220 // Name found (e.g. "<a href=..."), add on tag attributes...
3221 $pos +
= $this->truncate_skip( $ret, $text, "<>", $pos +
1 );
3223 } elseif ( $bracketState == 0 ) {
3224 if ( $entityState ) {
3227 $dispLen++
; // entity is one displayed char
3230 if ( $neLength == 0 && !$maybeState ) {
3231 // Save state without $ch. We want to *hit* the first
3232 // display char (to get tags) but not *use* it if truncating.
3233 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3236 $entityState = 1; // entity found, (e.g. " ")
3238 $dispLen++
; // this char is displayed
3239 // Add the next $max display text chars after this in one swoop...
3240 $max = ( $testingEllipsis ?
$length : $neLength ) - $dispLen;
3241 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos +
1, $max );
3242 $dispLen +
= $skipped;
3248 // Close the last tag if left unclosed by bad HTML
3249 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3250 while ( count( $openTags ) > 0 ) {
3251 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3257 * truncateHtml() helper function
3258 * like strcspn() but adds the skipped chars to $ret
3267 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3268 if ( $len === null ) {
3269 $len = -1; // -1 means "no limit" for strcspn
3270 } elseif ( $len < 0 ) {
3274 if ( $start < strlen( $text ) ) {
3275 $skipCount = strcspn( $text, $search, $start, $len );
3276 $ret .= substr( $text, $start, $skipCount );
3282 * truncateHtml() helper function
3283 * (a) push or pop $tag from $openTags as needed
3284 * (b) clear $tag value
3285 * @param &$tag string Current HTML tag name we are looking at
3286 * @param $tagType int (0-open tag, 1-close tag)
3287 * @param $lastCh string Character before the '>' that ended this tag
3288 * @param &$openTags array Open tag stack (not accounting for $tag)
3290 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3291 $tag = ltrim( $tag );
3293 if ( $tagType == 0 && $lastCh != '/' ) {
3294 $openTags[] = $tag; // tag opened (didn't close itself)
3295 } elseif ( $tagType == 1 ) {
3296 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3297 array_pop( $openTags ); // tag closed
3305 * Grammatical transformations, needed for inflected languages
3306 * Invoked by putting {{grammar:case|word}} in a message
3308 * @param $word string
3309 * @param $case string
3312 function convertGrammar( $word, $case ) {
3313 global $wgGrammarForms;
3314 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3315 return $wgGrammarForms[$this->getCode()][$case][$word];
3320 * Get the grammar forms for the content language
3321 * @return array of grammar forms
3324 function getGrammarForms() {
3325 global $wgGrammarForms;
3326 if ( isset( $wgGrammarForms[$this->getCode()] ) && is_array( $wgGrammarForms[$this->getCode()] ) ) {
3327 return $wgGrammarForms[$this->getCode()];
3332 * Provides an alternative text depending on specified gender.
3333 * Usage {{gender:username|masculine|feminine|neutral}}.
3334 * username is optional, in which case the gender of current user is used,
3335 * but only in (some) interface messages; otherwise default gender is used.
3337 * If no forms are given, an empty string is returned. If only one form is
3338 * given, it will be returned unconditionally. These details are implied by
3339 * the caller and cannot be overridden in subclasses.
3341 * If more than one form is given, the default is to use the neutral one
3342 * if it is specified, and to use the masculine one otherwise. These
3343 * details can be overridden in subclasses.
3345 * @param $gender string
3346 * @param $forms array
3350 function gender( $gender, $forms ) {
3351 if ( !count( $forms ) ) {
3354 $forms = $this->preConvertPlural( $forms, 2 );
3355 if ( $gender === 'male' ) {
3358 if ( $gender === 'female' ) {
3361 return isset( $forms[2] ) ?
$forms[2] : $forms[0];
3365 * Plural form transformations, needed for some languages.
3366 * For example, there are 3 form of plural in Russian and Polish,
3367 * depending on "count mod 10". See [[w:Plural]]
3368 * For English it is pretty simple.
3370 * Invoked by putting {{plural:count|wordform1|wordform2}}
3371 * or {{plural:count|wordform1|wordform2|wordform3}}
3373 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3375 * @param $count Integer: non-localized number
3376 * @param $forms Array: different plural forms
3377 * @return string Correct form of plural for $count in this language
3379 function convertPlural( $count, $forms ) {
3380 if ( !count( $forms ) ) {
3383 $forms = $this->preConvertPlural( $forms, 2 );
3385 return ( $count == 1 ) ?
$forms[0] : $forms[1];
3389 * Checks that convertPlural was given an array and pads it to requested
3390 * amount of forms by copying the last one.
3392 * @param $count Integer: How many forms should there be at least
3393 * @param $forms Array of forms given to convertPlural
3394 * @return array Padded array of forms or an exception if not an array
3396 protected function preConvertPlural( /* Array */ $forms, $count ) {
3397 while ( count( $forms ) < $count ) {
3398 $forms[] = $forms[count( $forms ) - 1];
3404 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
3405 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3406 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3407 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3410 * @param $str String: the validated block duration in English
3411 * @return string Somehow translated block duration
3412 * @see LanguageFi.php for example implementation
3414 function translateBlockExpiry( $str ) {
3415 $duration = SpecialBlock
::getSuggestedDurations( $this );
3416 foreach ( $duration as $show => $value ) {
3417 if ( strcmp( $str, $value ) == 0 ) {
3418 return htmlspecialchars( trim( $show ) );
3422 // Since usually only infinite or indefinite is only on list, so try
3423 // equivalents if still here.
3424 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3425 if ( in_array( $str, $indefs ) ) {
3426 foreach ( $indefs as $val ) {
3427 $show = array_search( $val, $duration, true );
3428 if ( $show !== false ) {
3429 return htmlspecialchars( trim( $show ) );
3433 // If all else fails, return the original string.
3438 * languages like Chinese need to be segmented in order for the diff
3441 * @param $text String
3444 public function segmentForDiff( $text ) {
3449 * and unsegment to show the result
3451 * @param $text String
3454 public function unsegmentForDiff( $text ) {
3459 * Return the LanguageConverter used in the Language
3462 * @return LanguageConverter
3464 public function getConverter() {
3465 return $this->mConverter
;
3469 * convert text to all supported variants
3471 * @param $text string
3474 public function autoConvertToAllVariants( $text ) {
3475 return $this->mConverter
->autoConvertToAllVariants( $text );
3479 * convert text to different variants of a language.
3481 * @param $text string
3484 public function convert( $text ) {
3485 return $this->mConverter
->convert( $text );
3489 * Convert a Title object to a string in the preferred variant
3491 * @param $title Title
3494 public function convertTitle( $title ) {
3495 return $this->mConverter
->convertTitle( $title );
3499 * Check if this is a language with variants
3503 public function hasVariants() {
3504 return sizeof( $this->getVariants() ) > 1;
3508 * Check if the language has the specific variant
3511 * @param $variant string
3514 public function hasVariant( $variant ) {
3515 return (bool)$this->mConverter
->validateVariant( $variant );
3519 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3521 * @param $text string
3524 public function armourMath( $text ) {
3525 return $this->mConverter
->armourMath( $text );
3529 * Perform output conversion on a string, and encode for safe HTML output.
3530 * @param $text String text to be converted
3531 * @param $isTitle Bool whether this conversion is for the article title
3533 * @todo this should get integrated somewhere sane
3535 public function convertHtml( $text, $isTitle = false ) {
3536 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3540 * @param $key string
3543 public function convertCategoryKey( $key ) {
3544 return $this->mConverter
->convertCategoryKey( $key );
3548 * Get the list of variants supported by this language
3549 * see sample implementation in LanguageZh.php
3551 * @return array an array of language codes
3553 public function getVariants() {
3554 return $this->mConverter
->getVariants();
3560 public function getPreferredVariant() {
3561 return $this->mConverter
->getPreferredVariant();
3567 public function getDefaultVariant() {
3568 return $this->mConverter
->getDefaultVariant();
3574 public function getURLVariant() {
3575 return $this->mConverter
->getURLVariant();
3579 * If a language supports multiple variants, it is
3580 * possible that non-existing link in one variant
3581 * actually exists in another variant. this function
3582 * tries to find it. See e.g. LanguageZh.php
3584 * @param $link String: the name of the link
3585 * @param $nt Mixed: the title object of the link
3586 * @param $ignoreOtherCond Boolean: to disable other conditions when
3587 * we need to transclude a template or update a category's link
3588 * @return null the input parameters may be modified upon return
3590 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
3591 $this->mConverter
->findVariantLink( $link, $nt, $ignoreOtherCond );
3595 * If a language supports multiple variants, converts text
3596 * into an array of all possible variants of the text:
3597 * 'variant' => text in that variant
3599 * @deprecated since 1.17 Use autoConvertToAllVariants()
3601 * @param $text string
3605 public function convertLinkToAllVariants( $text ) {
3606 return $this->mConverter
->convertLinkToAllVariants( $text );
3610 * returns language specific options used by User::getPageRenderHash()
3611 * for example, the preferred language variant
3615 function getExtraHashOptions() {
3616 return $this->mConverter
->getExtraHashOptions();
3620 * For languages that support multiple variants, the title of an
3621 * article may be displayed differently in different variants. this
3622 * function returns the apporiate title defined in the body of the article.
3626 public function getParsedTitle() {
3627 return $this->mConverter
->getParsedTitle();
3631 * Enclose a string with the "no conversion" tag. This is used by
3632 * various functions in the Parser
3634 * @param $text String: text to be tagged for no conversion
3635 * @param $noParse bool
3636 * @return string the tagged text
3638 public function markNoConversion( $text, $noParse = false ) {
3639 return $this->mConverter
->markNoConversion( $text, $noParse );
3643 * A regular expression to match legal word-trailing characters
3644 * which should be merged onto a link of the form [[foo]]bar.
3648 public function linkTrail() {
3649 return self
::$dataCache->getItem( $this->mCode
, 'linkTrail' );
3655 function getLangObj() {
3660 * Get the RFC 3066 code for this language object
3664 public function getCode() {
3665 return $this->mCode
;
3669 * Get the code in Bcp47 format which we can use
3670 * inside of html lang="" tags.
3674 public function getHtmlCode() {
3675 if ( is_null( $this->mHtmlCode
) ) {
3676 $this->mHtmlCode
= wfBCP47( $this->getCode() );
3678 return $this->mHtmlCode
;
3682 * @param $code string
3684 public function setCode( $code ) {
3685 $this->mCode
= $code;
3686 // Ensure we don't leave an incorrect html code lying around
3687 $this->mHtmlCode
= null;
3691 * Get the name of a file for a certain language code
3692 * @param $prefix string Prepend this to the filename
3693 * @param $code string Language code
3694 * @param $suffix string Append this to the filename
3695 * @throws MWException
3696 * @return string $prefix . $mangledCode . $suffix
3698 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
3699 // Protect against path traversal
3700 if ( !Language
::isValidCode( $code )
3701 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
3703 throw new MWException( "Invalid language code \"$code\"" );
3706 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
3710 * Get the language code from a file name. Inverse of getFileName()
3711 * @param $filename string $prefix . $languageCode . $suffix
3712 * @param $prefix string Prefix before the language code
3713 * @param $suffix string Suffix after the language code
3714 * @return string Language code, or false if $prefix or $suffix isn't found
3716 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
3718 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
3719 preg_quote( $suffix, '/' ) . '/', $filename, $m );
3720 if ( !count( $m ) ) {
3723 return str_replace( '_', '-', strtolower( $m[1] ) );
3727 * @param $code string
3730 public static function getMessagesFileName( $code ) {
3732 $file = self
::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
3733 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
3738 * @param $code string
3741 public static function getClassFileName( $code ) {
3743 return self
::getFileName( "$IP/languages/classes/Language", $code, '.php' );
3747 * Get the first fallback for a given language.
3749 * @param $code string
3751 * @return bool|string
3753 public static function getFallbackFor( $code ) {
3754 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3757 $fallbacks = self
::getFallbacksFor( $code );
3758 $first = array_shift( $fallbacks );
3764 * Get the ordered list of fallback languages.
3767 * @param $code string Language code
3770 public static function getFallbacksFor( $code ) {
3771 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3774 $v = self
::getLocalisationCache()->getItem( $code, 'fallback' );
3775 $v = array_map( 'trim', explode( ',', $v ) );
3776 if ( $v[count( $v ) - 1] !== 'en' ) {
3784 * Get all messages for a given language
3785 * WARNING: this may take a long time. If you just need all message *keys*
3786 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
3788 * @param $code string
3792 public static function getMessagesFor( $code ) {
3793 return self
::getLocalisationCache()->getItem( $code, 'messages' );
3797 * Get a message for a given language
3799 * @param $key string
3800 * @param $code string
3804 public static function getMessageFor( $key, $code ) {
3805 return self
::getLocalisationCache()->getSubitem( $code, 'messages', $key );
3809 * Get all message keys for a given language. This is a faster alternative to
3810 * array_keys( Language::getMessagesFor( $code ) )
3813 * @param $code string Language code
3814 * @return array of message keys (strings)
3816 public static function getMessageKeysFor( $code ) {
3817 return self
::getLocalisationCache()->getSubItemList( $code, 'messages' );
3824 function fixVariableInNamespace( $talk ) {
3825 if ( strpos( $talk, '$1' ) === false ) {
3829 global $wgMetaNamespace;
3830 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
3832 # Allow grammar transformations
3833 # Allowing full message-style parsing would make simple requests
3834 # such as action=raw much more expensive than they need to be.
3835 # This will hopefully cover most cases.
3836 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
3837 array( &$this, 'replaceGrammarInNamespace' ), $talk );
3838 return str_replace( ' ', '_', $talk );
3845 function replaceGrammarInNamespace( $m ) {
3846 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
3850 * @throws MWException
3853 static function getCaseMaps() {
3854 static $wikiUpperChars, $wikiLowerChars;
3855 if ( isset( $wikiUpperChars ) ) {
3856 return array( $wikiUpperChars, $wikiLowerChars );
3859 wfProfileIn( __METHOD__
);
3860 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
3861 if ( $arr === false ) {
3862 throw new MWException(
3863 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
3865 $wikiUpperChars = $arr['wikiUpperChars'];
3866 $wikiLowerChars = $arr['wikiLowerChars'];
3867 wfProfileOut( __METHOD__
);
3868 return array( $wikiUpperChars, $wikiLowerChars );
3872 * Decode an expiry (block, protection, etc) which has come from the DB
3874 * @FIXME: why are we returnings DBMS-dependent strings???
3876 * @param $expiry String: Database expiry String
3877 * @param $format Bool|Int true to process using language functions, or TS_ constant
3878 * to return the expiry in a given timestamp
3881 public function formatExpiry( $expiry, $format = true ) {
3882 static $infinity, $infinityMsg;
3883 if ( $infinity === null ) {
3884 $infinityMsg = wfMessage( 'infiniteblock' );
3885 $infinity = wfGetDB( DB_SLAVE
)->getInfinity();
3888 if ( $expiry == '' ||
$expiry == $infinity ) {
3889 return $format === true
3893 return $format === true
3894 ?
$this->timeanddate( $expiry, /* User preference timezone */ true )
3895 : wfTimestamp( $format, $expiry );
3901 * @param $seconds int|float
3902 * @param $format Array Optional
3903 * If $format['avoid'] == 'avoidseconds' - don't mention seconds if $seconds >= 1 hour
3904 * If $format['avoid'] == 'avoidminutes' - don't mention seconds/minutes if $seconds > 48 hours
3905 * If $format['noabbrevs'] is true - use 'seconds' and friends instead of 'seconds-abbrev' and friends
3906 * For backwards compatibility, $format may also be one of the strings 'avoidseconds' or 'avoidminutes'
3909 function formatTimePeriod( $seconds, $format = array() ) {
3910 if ( !is_array( $format ) ) {
3911 $format = array( 'avoid' => $format ); // For backwards compatibility
3913 if ( !isset( $format['avoid'] ) ) {
3914 $format['avoid'] = false;
3916 if ( !isset( $format['noabbrevs' ] ) ) {
3917 $format['noabbrevs'] = false;
3919 $secondsMsg = wfMessage(
3920 $format['noabbrevs'] ?
'seconds' : 'seconds-abbrev' )->inLanguage( $this );
3921 $minutesMsg = wfMessage(
3922 $format['noabbrevs'] ?
'minutes' : 'minutes-abbrev' )->inLanguage( $this );
3923 $hoursMsg = wfMessage(
3924 $format['noabbrevs'] ?
'hours' : 'hours-abbrev' )->inLanguage( $this );
3925 $daysMsg = wfMessage(
3926 $format['noabbrevs'] ?
'days' : 'days-abbrev' )->inLanguage( $this );
3928 if ( round( $seconds * 10 ) < 100 ) {
3929 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
3930 $s = $secondsMsg->params( $s )->text();
3931 } elseif ( round( $seconds ) < 60 ) {
3932 $s = $this->formatNum( round( $seconds ) );
3933 $s = $secondsMsg->params( $s )->text();
3934 } elseif ( round( $seconds ) < 3600 ) {
3935 $minutes = floor( $seconds / 60 );
3936 $secondsPart = round( fmod( $seconds, 60 ) );
3937 if ( $secondsPart == 60 ) {
3941 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
3943 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
3944 } elseif ( round( $seconds ) <= 2 * 86400 ) {
3945 $hours = floor( $seconds / 3600 );
3946 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
3947 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
3948 if ( $secondsPart == 60 ) {
3952 if ( $minutes == 60 ) {
3956 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
3958 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
3959 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
3960 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
3963 $days = floor( $seconds / 86400 );
3964 if ( $format['avoid'] === 'avoidminutes' ) {
3965 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
3966 if ( $hours == 24 ) {
3970 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
3972 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
3973 } elseif ( $format['avoid'] === 'avoidseconds' ) {
3974 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
3975 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
3976 if ( $minutes == 60 ) {
3980 if ( $hours == 24 ) {
3984 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
3986 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
3988 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
3990 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
3992 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
3999 * Format a bitrate for output, using an appropriate
4000 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to the magnitude in question
4002 * This use base 1000. For base 1024 use formatSize(), for another base
4003 * see formatComputingNumbers()
4008 function formatBitrate( $bps ) {
4009 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4013 * @param $size int Size of the unit
4014 * @param $boundary int Size boundary (1000, or 1024 in most cases)
4015 * @param $messageKey string Message key to be uesd
4018 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4020 return str_replace( '$1', $this->formatNum( $size ),
4021 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4024 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4027 $maxIndex = count( $sizes ) - 1;
4028 while ( $size >= $boundary && $index < $maxIndex ) {
4033 // For small sizes no decimal places necessary
4036 // For MB and bigger two decimal places are smarter
4039 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4041 $size = round( $size, $round );
4042 $text = $this->getMessageFromDB( $msg );
4043 return str_replace( '$1', $this->formatNum( $size ), $text );
4047 * Format a size in bytes for output, using an appropriate
4048 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4050 * This method use base 1024. For base 1000 use formatBitrate(), for
4051 * another base see formatComputingNumbers()
4053 * @param $size int Size to format
4054 * @return string Plain text (not HTML)
4056 function formatSize( $size ) {
4057 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4061 * Make a list item, used by various special pages
4063 * @param $page String Page link
4064 * @param $details String Text between brackets
4065 * @param $oppositedm Boolean Add the direction mark opposite to your
4066 * language, to display text properly
4069 function specialList( $page, $details, $oppositedm = true ) {
4070 $dirmark = ( $oppositedm ?
$this->getDirMark( true ) : '' ) .
4071 $this->getDirMark();
4072 $details = $details ?
$dirmark . $this->getMessageFromDB( 'word-separator' ) .
4073 wfMsgExt( 'parentheses', array( 'escape', 'replaceafter', 'language' => $this ), $details ) : '';
4074 return $page . $details;
4078 * Generate (prev x| next x) (20|50|100...) type links for paging
4080 * @param $title Title object to link
4081 * @param $offset Integer offset parameter
4082 * @param $limit Integer limit parameter
4083 * @param $query String optional URL query parameter string
4084 * @param $atend Bool optional param for specified if this is the last page
4087 public function viewPrevNext( Title
$title, $offset, $limit, array $query = array(), $atend = false ) {
4088 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4090 # Make 'previous' link
4091 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4092 if ( $offset > 0 ) {
4093 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4094 $query, $prev, 'prevn-title', 'mw-prevlink' );
4096 $plink = htmlspecialchars( $prev );
4100 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4102 $nlink = htmlspecialchars( $next );
4104 $nlink = $this->numLink( $title, $offset +
$limit, $limit,
4105 $query, $next, 'prevn-title', 'mw-nextlink' );
4108 # Make links to set number of items per page
4109 $numLinks = array();
4110 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4111 $numLinks[] = $this->numLink( $title, $offset, $num,
4112 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4115 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4116 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4120 * Helper function for viewPrevNext() that generates links
4122 * @param $title Title object to link
4123 * @param $offset Integer offset parameter
4124 * @param $limit Integer limit parameter
4125 * @param $query Array extra query parameters
4126 * @param $link String text to use for the link; will be escaped
4127 * @param $tooltipMsg String name of the message to use as tooltip
4128 * @param $class String value of the "class" attribute of the link
4129 * @return String HTML fragment
4131 private function numLink( Title
$title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) {
4132 $query = array( 'limit' => $limit, 'offset' => $offset ) +
$query;
4133 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4134 return Html
::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4135 'title' => $tooltip, 'class' => $class ), $link );
4139 * Get the conversion rule title, if any.
4143 public function getConvRuleTitle() {
4144 return $this->mConverter
->getConvRuleTitle();