3 * Internationalisation code.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
25 * @defgroup Language Language
28 if ( !defined( 'MEDIAWIKI' ) ) {
29 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
34 global $wgLanguageNames;
35 require_once( __DIR__
. '/Names.php' );
37 if ( function_exists( 'mb_strtoupper' ) ) {
38 mb_internal_encoding( 'UTF-8' );
42 * a fake language converter
52 function __construct( $langobj ) { $this->mLang
= $langobj; }
53 function autoConvertToAllVariants( $text ) { return array( $this->mLang
->getCode() => $text ); }
54 function convert( $t ) { return $t; }
55 function convertTo( $text, $variant ) { return $text; }
56 function convertTitle( $t ) { return $t->getPrefixedText(); }
57 function convertNamespace( $ns ) { return $this->mLang
->getFormattedNsText( $ns ); }
58 function getVariants() { return array( $this->mLang
->getCode() ); }
59 function getPreferredVariant() { return $this->mLang
->getCode(); }
60 function getDefaultVariant() { return $this->mLang
->getCode(); }
61 function getURLVariant() { return ''; }
62 function getConvRuleTitle() { return false; }
63 function findVariantLink( &$l, &$n, $ignoreOtherCond = false ) { }
64 function getExtraHashOptions() { return ''; }
65 function getParsedTitle() { return ''; }
66 function markNoConversion( $text, $noParse = false ) { return $text; }
67 function convertCategoryKey( $key ) { return $key; }
68 function convertLinkToAllVariants( $text ) { return $this->autoConvertToAllVariants( $text ); }
69 function armourMath( $text ) { return $text; }
73 * Internationalisation code
79 * @var LanguageConverter
83 public $mVariants, $mCode, $mLoaded = false;
84 public $mMagicExtensions = array(), $mMagicHookDone = false;
85 private $mHtmlCode = null;
87 public $dateFormatStrings = array();
88 public $mExtendedSpecialPageAliases;
90 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
93 * ReplacementArray object caches
95 public $transformData = array();
98 * @var LocalisationCache
100 static public $dataCache;
102 static public $mLangObjCache = array();
104 static public $mWeekdayMsgs = array(
105 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
109 static public $mWeekdayAbbrevMsgs = array(
110 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
113 static public $mMonthMsgs = array(
114 'january', 'february', 'march', 'april', 'may_long', 'june',
115 'july', 'august', 'september', 'october', 'november',
118 static public $mMonthGenMsgs = array(
119 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
120 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
123 static public $mMonthAbbrevMsgs = array(
124 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
125 'sep', 'oct', 'nov', 'dec'
128 static public $mIranianCalendarMonthMsgs = array(
129 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
130 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
131 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
132 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
135 static public $mHebrewCalendarMonthMsgs = array(
136 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
137 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
138 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
139 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
140 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
143 static public $mHebrewCalendarMonthGenMsgs = array(
144 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
145 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
146 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
147 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
148 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
151 static public $mHijriCalendarMonthMsgs = array(
152 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
153 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
154 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
155 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
162 static public $durationIntervals = array(
163 'millennia' => 31556952000,
164 'centuries' => 3155695200,
165 'decades' => 315569520,
166 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
175 * Get a cached or new language object for a given language code
176 * @param $code String
179 static function factory( $code ) {
180 global $wgDummyLanguageCodes, $wgLangObjCacheSize;
182 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
183 $code = $wgDummyLanguageCodes[$code];
186 // get the language object to process
187 $langObj = isset( self
::$mLangObjCache[$code] )
188 ? self
::$mLangObjCache[$code]
189 : self
::newFromCode( $code );
191 // merge the language object in to get it up front in the cache
192 self
::$mLangObjCache = array_merge( array( $code => $langObj ), self
::$mLangObjCache );
193 // get rid of the oldest ones in case we have an overflow
194 self
::$mLangObjCache = array_slice( self
::$mLangObjCache, 0, $wgLangObjCacheSize, true );
200 * Create a language object for a given language code
201 * @param $code String
202 * @throws MWException
205 protected static function newFromCode( $code ) {
206 // Protect against path traversal below
207 if ( !Language
::isValidCode( $code )
208 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
210 throw new MWException( "Invalid language code \"$code\"" );
213 if ( !Language
::isValidBuiltInCode( $code ) ) {
214 // It's not possible to customise this code with class files, so
215 // just return a Language object. This is to support uselang= hacks.
216 $lang = new Language
;
217 $lang->setCode( $code );
221 // Check if there is a language class for the code
222 $class = self
::classFromCode( $code );
223 self
::preloadLanguageClass( $class );
224 if ( MWInit
::classExists( $class ) ) {
229 // Keep trying the fallback list until we find an existing class
230 $fallbacks = Language
::getFallbacksFor( $code );
231 foreach ( $fallbacks as $fallbackCode ) {
232 if ( !Language
::isValidBuiltInCode( $fallbackCode ) ) {
233 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
236 $class = self
::classFromCode( $fallbackCode );
237 self
::preloadLanguageClass( $class );
238 if ( MWInit
::classExists( $class ) ) {
239 $lang = Language
::newFromCode( $fallbackCode );
240 $lang->setCode( $code );
245 throw new MWException( "Invalid fallback sequence for language '$code'" );
249 * Checks whether any localisation is available for that language tag
250 * in MediaWiki (MessagesXx.php exists).
252 * @param string $code Language tag (in lower case)
253 * @return bool Whether language is supported
256 public static function isSupportedLanguage( $code ) {
257 return is_readable( self
::getMessagesFileName( $code ) );
261 * Returns true if a language code string is of a valid form, whether or
262 * not it exists. This includes codes which are used solely for
263 * customisation via the MediaWiki namespace.
265 * @param $code string
269 public static function isValidCode( $code ) {
271 // People think language codes are html safe, so enforce it.
272 // Ideally we should only allow a-zA-Z0-9-
273 // but, .+ and other chars are often used for {{int:}} hacks
274 // see bugs 37564, 37587, 36938
275 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
276 && !preg_match( Title
::getTitleInvalidRegex(), $code );
280 * Returns true if a language code is of a valid form for the purposes of
281 * internal customisation of MediaWiki, via Messages*.php.
283 * @param $code string
285 * @throws MWException
289 public static function isValidBuiltInCode( $code ) {
291 if ( !is_string( $code ) ) {
292 $type = gettype( $code );
293 if ( $type === 'object' ) {
294 $addmsg = " of class " . get_class( $code );
298 throw new MWException( __METHOD__
. " must be passed a string, $type given$addmsg" );
301 return (bool)preg_match( '/^[a-z0-9-]+$/i', $code );
306 * @return String Name of the language class
308 public static function classFromCode( $code ) {
309 if ( $code == 'en' ) {
312 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
317 * Includes language class files
319 * @param $class string Name of the language class
321 public static function preloadLanguageClass( $class ) {
324 if ( $class === 'Language' ) {
328 if ( !defined( 'MW_COMPILED' ) ) {
329 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
330 include_once( "$IP/languages/classes/$class.php" );
336 * Get the LocalisationCache instance
338 * @return LocalisationCache
340 public static function getLocalisationCache() {
341 if ( is_null( self
::$dataCache ) ) {
342 global $wgLocalisationCacheConf;
343 $class = $wgLocalisationCacheConf['class'];
344 self
::$dataCache = new $class( $wgLocalisationCacheConf );
346 return self
::$dataCache;
349 function __construct() {
350 $this->mConverter
= new FakeConverter( $this );
351 // Set the code to the name of the descendant
352 if ( get_class( $this ) == 'Language' ) {
355 $this->mCode
= str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
357 self
::getLocalisationCache();
361 * Reduce memory usage
363 function __destruct() {
364 foreach ( $this as $name => $value ) {
365 unset( $this->$name );
370 * Hook which will be called if this is the content language.
371 * Descendants can use this to register hook functions or modify globals
373 function initContLang() { }
376 * Same as getFallbacksFor for current language.
378 * @deprecated in 1.19
380 function getFallbackLanguageCode() {
381 wfDeprecated( __METHOD__
, '1.19' );
382 return self
::getFallbackFor( $this->mCode
);
389 function getFallbackLanguages() {
390 return self
::getFallbacksFor( $this->mCode
);
394 * Exports $wgBookstoreListEn
397 function getBookstoreList() {
398 return self
::$dataCache->getItem( $this->mCode
, 'bookstoreList' );
404 public function getNamespaces() {
405 if ( is_null( $this->namespaceNames
) ) {
406 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
408 $this->namespaceNames
= self
::$dataCache->getItem( $this->mCode
, 'namespaceNames' );
409 $validNamespaces = MWNamespace
::getCanonicalNamespaces();
411 $this->namespaceNames
= $wgExtraNamespaces +
$this->namespaceNames +
$validNamespaces;
413 $this->namespaceNames
[NS_PROJECT
] = $wgMetaNamespace;
414 if ( $wgMetaNamespaceTalk ) {
415 $this->namespaceNames
[NS_PROJECT_TALK
] = $wgMetaNamespaceTalk;
417 $talk = $this->namespaceNames
[NS_PROJECT_TALK
];
418 $this->namespaceNames
[NS_PROJECT_TALK
] =
419 $this->fixVariableInNamespace( $talk );
422 # Sometimes a language will be localised but not actually exist on this wiki.
423 foreach ( $this->namespaceNames
as $key => $text ) {
424 if ( !isset( $validNamespaces[$key] ) ) {
425 unset( $this->namespaceNames
[$key] );
429 # The above mixing may leave namespaces out of canonical order.
430 # Re-order by namespace ID number...
431 ksort( $this->namespaceNames
);
433 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames
) );
435 return $this->namespaceNames
;
439 * Arbitrarily set all of the namespace names at once. Mainly used for testing
440 * @param $namespaces Array of namespaces (id => name)
442 public function setNamespaces( array $namespaces ) {
443 $this->namespaceNames
= $namespaces;
444 $this->mNamespaceIds
= null;
448 * Resets all of the namespace caches. Mainly used for testing
450 public function resetNamespaces( ) {
451 $this->namespaceNames
= null;
452 $this->mNamespaceIds
= null;
453 $this->namespaceAliases
= null;
457 * A convenience function that returns the same thing as
458 * getNamespaces() except with the array values changed to ' '
459 * where it found '_', useful for producing output to be displayed
460 * e.g. in <select> forms.
464 function getFormattedNamespaces() {
465 $ns = $this->getNamespaces();
466 foreach ( $ns as $k => $v ) {
467 $ns[$k] = strtr( $v, '_', ' ' );
473 * Get a namespace value by key
475 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
476 * echo $mw_ns; // prints 'MediaWiki'
479 * @param $index Int: the array key of the namespace to return
480 * @return mixed, string if the namespace value exists, otherwise false
482 function getNsText( $index ) {
483 $ns = $this->getNamespaces();
484 return isset( $ns[$index] ) ?
$ns[$index] : false;
488 * A convenience function that returns the same thing as
489 * getNsText() except with '_' changed to ' ', useful for
493 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
494 * echo $mw_ns; // prints 'MediaWiki talk'
497 * @param int $index The array key of the namespace to return
498 * @return string Namespace name without underscores (empty string if namespace does not exist)
500 function getFormattedNsText( $index ) {
501 $ns = $this->getNsText( $index );
502 return strtr( $ns, '_', ' ' );
506 * Returns gender-dependent namespace alias if available.
507 * @param $index Int: namespace index
508 * @param $gender String: gender key (male, female... )
512 function getGenderNsText( $index, $gender ) {
513 global $wgExtraGenderNamespaces;
515 $ns = $wgExtraGenderNamespaces + self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
516 return isset( $ns[$index][$gender] ) ?
$ns[$index][$gender] : $this->getNsText( $index );
520 * Whether this language makes distinguishes genders for example in
525 function needsGenderDistinction() {
526 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
527 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
528 // $wgExtraGenderNamespaces overrides everything
530 } elseif ( isset( $wgExtraNamespaces[NS_USER
] ) && isset( $wgExtraNamespaces[NS_USER_TALK
] ) ) {
531 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
532 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
535 // Check what is in i18n files
536 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
537 return count( $aliases ) > 0;
542 * Get a namespace key by value, case insensitive.
543 * Only matches namespace names for the current language, not the
544 * canonical ones defined in Namespace.php.
546 * @param $text String
547 * @return mixed An integer if $text is a valid value otherwise false
549 function getLocalNsIndex( $text ) {
550 $lctext = $this->lc( $text );
551 $ids = $this->getNamespaceIds();
552 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
558 function getNamespaceAliases() {
559 if ( is_null( $this->namespaceAliases
) ) {
560 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceAliases' );
564 foreach ( $aliases as $name => $index ) {
565 if ( $index === NS_PROJECT_TALK
) {
566 unset( $aliases[$name] );
567 $name = $this->fixVariableInNamespace( $name );
568 $aliases[$name] = $index;
573 global $wgExtraGenderNamespaces;
574 $genders = $wgExtraGenderNamespaces +
(array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
575 foreach ( $genders as $index => $forms ) {
576 foreach ( $forms as $alias ) {
577 $aliases[$alias] = $index;
581 $this->namespaceAliases
= $aliases;
583 return $this->namespaceAliases
;
589 function getNamespaceIds() {
590 if ( is_null( $this->mNamespaceIds
) ) {
591 global $wgNamespaceAliases;
592 # Put namespace names and aliases into a hashtable.
593 # If this is too slow, then we should arrange it so that it is done
594 # before caching. The catch is that at pre-cache time, the above
595 # class-specific fixup hasn't been done.
596 $this->mNamespaceIds
= array();
597 foreach ( $this->getNamespaces() as $index => $name ) {
598 $this->mNamespaceIds
[$this->lc( $name )] = $index;
600 foreach ( $this->getNamespaceAliases() as $name => $index ) {
601 $this->mNamespaceIds
[$this->lc( $name )] = $index;
603 if ( $wgNamespaceAliases ) {
604 foreach ( $wgNamespaceAliases as $name => $index ) {
605 $this->mNamespaceIds
[$this->lc( $name )] = $index;
609 return $this->mNamespaceIds
;
613 * Get a namespace key by value, case insensitive. Canonical namespace
614 * names override custom ones defined for the current language.
616 * @param $text String
617 * @return mixed An integer if $text is a valid value otherwise false
619 function getNsIndex( $text ) {
620 $lctext = $this->lc( $text );
621 $ns = MWNamespace
::getCanonicalIndex( $lctext );
622 if ( $ns !== null ) {
625 $ids = $this->getNamespaceIds();
626 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
630 * short names for language variants used for language conversion links.
632 * @param $code String
633 * @param $usemsg bool Use the "variantname-xyz" message if it exists
636 function getVariantname( $code, $usemsg = true ) {
637 $msg = "variantname-$code";
638 if ( $usemsg && wfMessage( $msg )->exists() ) {
639 return $this->getMessageFromDB( $msg );
641 $name = self
::fetchLanguageName( $code );
643 return $name; # if it's defined as a language name, show that
645 # otherwise, output the language code
651 * @param $name string
654 function specialPage( $name ) {
655 $aliases = $this->getSpecialPageAliases();
656 if ( isset( $aliases[$name][0] ) ) {
657 $name = $aliases[$name][0];
659 return $this->getNsText( NS_SPECIAL
) . ':' . $name;
665 function getQuickbarSettings() {
667 $this->getMessage( 'qbsettings-none' ),
668 $this->getMessage( 'qbsettings-fixedleft' ),
669 $this->getMessage( 'qbsettings-fixedright' ),
670 $this->getMessage( 'qbsettings-floatingleft' ),
671 $this->getMessage( 'qbsettings-floatingright' ),
672 $this->getMessage( 'qbsettings-directionality' )
679 function getDatePreferences() {
680 return self
::$dataCache->getItem( $this->mCode
, 'datePreferences' );
686 function getDateFormats() {
687 return self
::$dataCache->getItem( $this->mCode
, 'dateFormats' );
691 * @return array|string
693 function getDefaultDateFormat() {
694 $df = self
::$dataCache->getItem( $this->mCode
, 'defaultDateFormat' );
695 if ( $df === 'dmy or mdy' ) {
696 global $wgAmericanDates;
697 return $wgAmericanDates ?
'mdy' : 'dmy';
706 function getDatePreferenceMigrationMap() {
707 return self
::$dataCache->getItem( $this->mCode
, 'datePreferenceMigrationMap' );
714 function getImageFile( $image ) {
715 return self
::$dataCache->getSubitem( $this->mCode
, 'imageFiles', $image );
721 function getExtraUserToggles() {
722 return (array)self
::$dataCache->getItem( $this->mCode
, 'extraUserToggles' );
729 function getUserToggle( $tog ) {
730 return $this->getMessageFromDB( "tog-$tog" );
734 * Get native language names, indexed by code.
735 * Only those defined in MediaWiki, no other data like CLDR.
736 * If $customisedOnly is true, only returns codes with a messages file
738 * @param $customisedOnly bool
741 * @deprecated in 1.20, use fetchLanguageNames()
743 public static function getLanguageNames( $customisedOnly = false ) {
744 return self
::fetchLanguageNames( null, $customisedOnly ?
'mwfile' : 'mw' );
748 * Get translated language names. This is done on best effort and
749 * by default this is exactly the same as Language::getLanguageNames.
750 * The CLDR extension provides translated names.
751 * @param $code String Language code.
752 * @return Array language code => language name
754 * @deprecated in 1.20, use fetchLanguageNames()
756 public static function getTranslatedLanguageNames( $code ) {
757 return self
::fetchLanguageNames( $code, 'all' );
761 * Get an array of language names, indexed by code.
762 * @param $inLanguage null|string: Code of language in which to return the names
763 * Use null for autonyms (native names)
764 * @param $include string:
765 * 'all' all available languages
766 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
767 * 'mwfile' only if the language is in 'mw' *and* has a message file
768 * @return array: language code => language name
771 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
772 global $wgExtraLanguageNames;
773 static $coreLanguageNames;
775 if ( $coreLanguageNames === null ) {
776 include( MWInit
::compiledPath( 'languages/Names.php' ) );
782 # TODO: also include when $inLanguage is null, when this code is more efficient
783 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
786 $mwNames = $wgExtraLanguageNames +
$coreLanguageNames;
787 foreach ( $mwNames as $mwCode => $mwName ) {
788 # - Prefer own MediaWiki native name when not using the hook
789 # - For other names just add if not added through the hook
790 if ( $mwCode === $inLanguage ||
!isset( $names[$mwCode] ) ) {
791 $names[$mwCode] = $mwName;
795 if ( $include === 'all' ) {
800 $coreCodes = array_keys( $mwNames );
801 foreach ( $coreCodes as $coreCode ) {
802 $returnMw[$coreCode] = $names[$coreCode];
805 if ( $include === 'mwfile' ) {
806 $namesMwFile = array();
807 # We do this using a foreach over the codes instead of a directory
808 # loop so that messages files in extensions will work correctly.
809 foreach ( $returnMw as $code => $value ) {
810 if ( is_readable( self
::getMessagesFileName( $code ) ) ) {
811 $namesMwFile[$code] = $names[$code];
816 # 'mw' option; default if it's not one of the other two options (all/mwfile)
821 * @param $code string: The code of the language for which to get the name
822 * @param $inLanguage null|string: Code of language in which to return the name (null for autonyms)
823 * @param $include string: 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
824 * @return string: Language name or empty
827 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
828 $array = self
::fetchLanguageNames( $inLanguage, $include );
829 return !array_key_exists( $code, $array ) ?
'' : $array[$code];
833 * Get a message from the MediaWiki namespace.
835 * @param $msg String: message name
838 function getMessageFromDB( $msg ) {
839 return wfMessage( $msg )->inLanguage( $this )->text();
843 * Get the native language name of $code.
844 * Only if defined in MediaWiki, no other data like CLDR.
845 * @param $code string
847 * @deprecated in 1.20, use fetchLanguageName()
849 function getLanguageName( $code ) {
850 return self
::fetchLanguageName( $code );
857 function getMonthName( $key ) {
858 return $this->getMessageFromDB( self
::$mMonthMsgs[$key - 1] );
864 function getMonthNamesArray() {
865 $monthNames = array( '' );
866 for ( $i = 1; $i < 13; $i++
) {
867 $monthNames[] = $this->getMonthName( $i );
876 function getMonthNameGen( $key ) {
877 return $this->getMessageFromDB( self
::$mMonthGenMsgs[$key - 1] );
884 function getMonthAbbreviation( $key ) {
885 return $this->getMessageFromDB( self
::$mMonthAbbrevMsgs[$key - 1] );
891 function getMonthAbbreviationsArray() {
892 $monthNames = array( '' );
893 for ( $i = 1; $i < 13; $i++
) {
894 $monthNames[] = $this->getMonthAbbreviation( $i );
903 function getWeekdayName( $key ) {
904 return $this->getMessageFromDB( self
::$mWeekdayMsgs[$key - 1] );
911 function getWeekdayAbbreviation( $key ) {
912 return $this->getMessageFromDB( self
::$mWeekdayAbbrevMsgs[$key - 1] );
919 function getIranianCalendarMonthName( $key ) {
920 return $this->getMessageFromDB( self
::$mIranianCalendarMonthMsgs[$key - 1] );
927 function getHebrewCalendarMonthName( $key ) {
928 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthMsgs[$key - 1] );
935 function getHebrewCalendarMonthNameGen( $key ) {
936 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthGenMsgs[$key - 1] );
943 function getHijriCalendarMonthName( $key ) {
944 return $this->getMessageFromDB( self
::$mHijriCalendarMonthMsgs[$key - 1] );
948 * This is a workalike of PHP's date() function, but with better
949 * internationalisation, a reduced set of format characters, and a better
952 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
953 * PHP manual for definitions. There are a number of extensions, which
956 * xn Do not translate digits of the next numeric format character
957 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
958 * xr Use roman numerals for the next numeric format character
959 * xh Use hebrew numerals for the next numeric format character
961 * xg Genitive month name
963 * xij j (day number) in Iranian calendar
964 * xiF F (month name) in Iranian calendar
965 * xin n (month number) in Iranian calendar
966 * xiy y (two digit year) in Iranian calendar
967 * xiY Y (full year) in Iranian calendar
969 * xjj j (day number) in Hebrew calendar
970 * xjF F (month name) in Hebrew calendar
971 * xjt t (days in month) in Hebrew calendar
972 * xjx xg (genitive month name) in Hebrew calendar
973 * xjn n (month number) in Hebrew calendar
974 * xjY Y (full year) in Hebrew calendar
976 * xmj j (day number) in Hijri calendar
977 * xmF F (month name) in Hijri calendar
978 * xmn n (month number) in Hijri calendar
979 * xmY Y (full year) in Hijri calendar
981 * xkY Y (full year) in Thai solar calendar. Months and days are
982 * identical to the Gregorian calendar
983 * xoY Y (full year) in Minguo calendar or Juche year.
984 * Months and days are identical to the
986 * xtY Y (full year) in Japanese nengo. Months and days are
987 * identical to the Gregorian calendar
989 * Characters enclosed in double quotes will be considered literal (with
990 * the quotes themselves removed). Unmatched quotes will be considered
991 * literal quotes. Example:
993 * "The month is" F => The month is January
996 * Backslash escaping is also supported.
998 * Input timestamp is assumed to be pre-normalized to the desired local
1001 * @param $format String
1002 * @param $ts String: 14-character timestamp
1005 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1009 function sprintfDate( $format, $ts ) {
1022 for ( $p = 0; $p < strlen( $format ); $p++
) {
1024 $code = $format[$p];
1025 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
1026 $code .= $format[++
$p];
1029 if ( ( $code === 'xi' ||
$code == 'xj' ||
$code == 'xk' ||
$code == 'xm' ||
$code == 'xo' ||
$code == 'xt' ) && $p < strlen( $format ) - 1 ) {
1030 $code .= $format[++
$p];
1041 $rawToggle = !$rawToggle;
1050 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1053 if ( !$hebrew ) $hebrew = self
::tsToHebrew( $ts );
1054 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1057 $num = substr( $ts, 6, 2 );
1060 if ( !$unix ) $unix = wfTimestamp( TS_UNIX
, $ts );
1061 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) +
1 );
1064 $num = intval( substr( $ts, 6, 2 ) );
1068 $iranian = self
::tsToIranian( $ts );
1074 $hijri = self
::tsToHijri( $ts );
1080 $hebrew = self
::tsToHebrew( $ts );
1086 $unix = wfTimestamp( TS_UNIX
, $ts );
1088 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) +
1 );
1092 $unix = wfTimestamp( TS_UNIX
, $ts );
1094 $w = gmdate( 'w', $unix );
1099 $unix = wfTimestamp( TS_UNIX
, $ts );
1101 $num = gmdate( 'w', $unix );
1105 $unix = wfTimestamp( TS_UNIX
, $ts );
1107 $num = gmdate( 'z', $unix );
1111 $unix = wfTimestamp( TS_UNIX
, $ts );
1113 $num = gmdate( 'W', $unix );
1116 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1120 $iranian = self
::tsToIranian( $ts );
1122 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1126 $hijri = self
::tsToHijri( $ts );
1128 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1132 $hebrew = self
::tsToHebrew( $ts );
1134 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1137 $num = substr( $ts, 4, 2 );
1140 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1143 $num = intval( substr( $ts, 4, 2 ) );
1147 $iranian = self
::tsToIranian( $ts );
1153 $hijri = self
::tsToHijri ( $ts );
1159 $hebrew = self
::tsToHebrew( $ts );
1165 $unix = wfTimestamp( TS_UNIX
, $ts );
1167 $num = gmdate( 't', $unix );
1171 $hebrew = self
::tsToHebrew( $ts );
1177 $unix = wfTimestamp( TS_UNIX
, $ts );
1179 $num = gmdate( 'L', $unix );
1183 $unix = wfTimestamp( TS_UNIX
, $ts );
1185 $num = gmdate( 'o', $unix );
1188 $num = substr( $ts, 0, 4 );
1192 $iranian = self
::tsToIranian( $ts );
1198 $hijri = self
::tsToHijri( $ts );
1204 $hebrew = self
::tsToHebrew( $ts );
1210 $thai = self
::tsToYear( $ts, 'thai' );
1216 $minguo = self
::tsToYear( $ts, 'minguo' );
1222 $tenno = self
::tsToYear( $ts, 'tenno' );
1227 $num = substr( $ts, 2, 2 );
1231 $iranian = self
::tsToIranian( $ts );
1233 $num = substr( $iranian[0], -2 );
1236 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'am' : 'pm';
1239 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'AM' : 'PM';
1242 $h = substr( $ts, 8, 2 );
1243 $num = $h %
12 ?
$h %
12 : 12;
1246 $num = intval( substr( $ts, 8, 2 ) );
1249 $h = substr( $ts, 8, 2 );
1250 $num = sprintf( '%02d', $h %
12 ?
$h %
12 : 12 );
1253 $num = substr( $ts, 8, 2 );
1256 $num = substr( $ts, 10, 2 );
1259 $num = substr( $ts, 12, 2 );
1263 $unix = wfTimestamp( TS_UNIX
, $ts );
1265 $s .= gmdate( 'c', $unix );
1269 $unix = wfTimestamp( TS_UNIX
, $ts );
1271 $s .= gmdate( 'r', $unix );
1275 $unix = wfTimestamp( TS_UNIX
, $ts );
1280 # Backslash escaping
1281 if ( $p < strlen( $format ) - 1 ) {
1282 $s .= $format[++
$p];
1289 if ( $p < strlen( $format ) - 1 ) {
1290 $endQuote = strpos( $format, '"', $p +
1 );
1291 if ( $endQuote === false ) {
1292 # No terminating quote, assume literal "
1295 $s .= substr( $format, $p +
1, $endQuote - $p - 1 );
1299 # Quote at end of string, assume literal "
1306 if ( $num !== false ) {
1307 if ( $rawToggle ||
$raw ) {
1310 } elseif ( $roman ) {
1311 $s .= Language
::romanNumeral( $num );
1313 } elseif ( $hebrewNum ) {
1314 $s .= self
::hebrewNumeral( $num );
1317 $s .= $this->formatNum( $num, true );
1324 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1325 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1328 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1329 * Gregorian dates to Iranian dates. Originally written in C, it
1330 * is released under the terms of GNU Lesser General Public
1331 * License. Conversion to PHP was performed by Niklas Laxström.
1333 * Link: http://www.farsiweb.info/jalali/jalali.c
1339 private static function tsToIranian( $ts ) {
1340 $gy = substr( $ts, 0, 4 ) -1600;
1341 $gm = substr( $ts, 4, 2 ) -1;
1342 $gd = substr( $ts, 6, 2 ) -1;
1344 # Days passed from the beginning (including leap years)
1346 +
floor( ( $gy +
3 ) / 4 )
1347 - floor( ( $gy +
99 ) / 100 )
1348 +
floor( ( $gy +
399 ) / 400 );
1350 // Add days of the past months of this year
1351 for ( $i = 0; $i < $gm; $i++
) {
1352 $gDayNo +
= self
::$GREG_DAYS[$i];
1356 if ( $gm > 1 && ( ( $gy %
4 === 0 && $gy %
100 !== 0 ||
( $gy %
400 == 0 ) ) ) ) {
1360 // Days passed in current month
1361 $gDayNo +
= (int)$gd;
1363 $jDayNo = $gDayNo - 79;
1365 $jNp = floor( $jDayNo / 12053 );
1368 $jy = 979 +
33 * $jNp +
4 * floor( $jDayNo / 1461 );
1371 if ( $jDayNo >= 366 ) {
1372 $jy +
= floor( ( $jDayNo - 1 ) / 365 );
1373 $jDayNo = floor( ( $jDayNo - 1 ) %
365 );
1376 for ( $i = 0; $i < 11 && $jDayNo >= self
::$IRANIAN_DAYS[$i]; $i++
) {
1377 $jDayNo -= self
::$IRANIAN_DAYS[$i];
1383 return array( $jy, $jm, $jd );
1387 * Converting Gregorian dates to Hijri dates.
1389 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1391 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1397 private static function tsToHijri( $ts ) {
1398 $year = substr( $ts, 0, 4 );
1399 $month = substr( $ts, 4, 2 );
1400 $day = substr( $ts, 6, 2 );
1408 ( $zy > 1582 ) ||
( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1409 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1412 $zjd = (int)( ( 1461 * ( $zy +
4800 +
(int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1413 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1414 (int)( ( 3 * (int)( ( ( $zy +
4900 +
(int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1417 $zjd = 367 * $zy - (int)( ( 7 * ( $zy +
5001 +
(int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1418 (int)( ( 275 * $zm ) / 9 ) +
$zd +
1729777;
1421 $zl = $zjd -1948440 +
10632;
1422 $zn = (int)( ( $zl - 1 ) / 10631 );
1423 $zl = $zl - 10631 * $zn +
354;
1424 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1425 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) +
29;
1426 $zm = (int)( ( 24 * $zl ) / 709 );
1427 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1428 $zy = 30 * $zn +
$zj - 30;
1430 return array( $zy, $zm, $zd );
1434 * Converting Gregorian dates to Hebrew dates.
1436 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1437 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1438 * to translate the relevant functions into PHP and release them under
1441 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1442 * and Adar II is 14. In a non-leap year, Adar is 6.
1448 private static function tsToHebrew( $ts ) {
1450 $year = substr( $ts, 0, 4 );
1451 $month = substr( $ts, 4, 2 );
1452 $day = substr( $ts, 6, 2 );
1454 # Calculate Hebrew year
1455 $hebrewYear = $year +
3760;
1457 # Month number when September = 1, August = 12
1459 if ( $month > 12 ) {
1466 # Calculate day of year from 1 September
1468 for ( $i = 1; $i < $month; $i++
) {
1472 # Check if the year is leap
1473 if ( $year %
400 == 0 ||
( $year %
4 == 0 && $year %
100 > 0 ) ) {
1476 } elseif ( $i == 8 ||
$i == 10 ||
$i == 1 ||
$i == 3 ) {
1483 # Calculate the start of the Hebrew year
1484 $start = self
::hebrewYearStart( $hebrewYear );
1486 # Calculate next year's start
1487 if ( $dayOfYear <= $start ) {
1488 # Day is before the start of the year - it is the previous year
1490 $nextStart = $start;
1494 # Add days since previous year's 1 September
1496 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1500 # Start of the new (previous) year
1501 $start = self
::hebrewYearStart( $hebrewYear );
1504 $nextStart = self
::hebrewYearStart( $hebrewYear +
1 );
1507 # Calculate Hebrew day of year
1508 $hebrewDayOfYear = $dayOfYear - $start;
1510 # Difference between year's days
1511 $diff = $nextStart - $start;
1512 # Add 12 (or 13 for leap years) days to ignore the difference between
1513 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1514 # difference is only about the year type
1515 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1521 # Check the year pattern, and is leap year
1522 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1523 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1524 # and non-leap years
1525 $yearPattern = $diff %
30;
1526 # Check if leap year
1527 $isLeap = $diff >= 30;
1529 # Calculate day in the month from number of day in the Hebrew year
1530 # Don't check Adar - if the day is not in Adar, we will stop before;
1531 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1532 $hebrewDay = $hebrewDayOfYear;
1535 while ( $hebrewMonth <= 12 ) {
1536 # Calculate days in this month
1537 if ( $isLeap && $hebrewMonth == 6 ) {
1538 # Adar in a leap year
1540 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1542 if ( $hebrewDay <= $days ) {
1546 # Subtract the days of Adar I
1547 $hebrewDay -= $days;
1550 if ( $hebrewDay <= $days ) {
1556 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1557 # Cheshvan in a complete year (otherwise as the rule below)
1559 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1560 # Kislev in an incomplete year (otherwise as the rule below)
1563 # Odd months have 30 days, even have 29
1564 $days = 30 - ( $hebrewMonth - 1 ) %
2;
1566 if ( $hebrewDay <= $days ) {
1567 # In the current month
1570 # Subtract the days of the current month
1571 $hebrewDay -= $days;
1572 # Try in the next month
1577 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1581 * This calculates the Hebrew year start, as days since 1 September.
1582 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1583 * Used for Hebrew date.
1589 private static function hebrewYearStart( $year ) {
1590 $a = intval( ( 12 * ( $year - 1 ) +
17 ) %
19 );
1591 $b = intval( ( $year - 1 ) %
4 );
1592 $m = 32.044093161144 +
1.5542417966212 * $a +
$b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1596 $Mar = intval( $m );
1602 $c = intval( ( $Mar +
3 * ( $year - 1 ) +
5 * $b +
5 ) %
7 );
1603 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1605 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1607 } elseif ( $c == 2 ||
$c == 4 ||
$c == 6 ) {
1611 $Mar +
= intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1616 * Algorithm to convert Gregorian dates to Thai solar dates,
1617 * Minguo dates or Minguo dates.
1619 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1620 * http://en.wikipedia.org/wiki/Minguo_calendar
1621 * http://en.wikipedia.org/wiki/Japanese_era_name
1623 * @param $ts String: 14-character timestamp
1624 * @param $cName String: calender name
1625 * @return Array: converted year, month, day
1627 private static function tsToYear( $ts, $cName ) {
1628 $gy = substr( $ts, 0, 4 );
1629 $gm = substr( $ts, 4, 2 );
1630 $gd = substr( $ts, 6, 2 );
1632 if ( !strcmp( $cName, 'thai' ) ) {
1634 # Add 543 years to the Gregorian calendar
1635 # Months and days are identical
1636 $gy_offset = $gy +
543;
1637 } elseif ( ( !strcmp( $cName, 'minguo' ) ) ||
!strcmp( $cName, 'juche' ) ) {
1639 # Deduct 1911 years from the Gregorian calendar
1640 # Months and days are identical
1641 $gy_offset = $gy - 1911;
1642 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1643 # Nengō dates up to Meiji period
1644 # Deduct years from the Gregorian calendar
1645 # depending on the nengo periods
1646 # Months and days are identical
1647 if ( ( $gy < 1912 ) ||
( ( $gy == 1912 ) && ( $gm < 7 ) ) ||
( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
1649 $gy_gannen = $gy - 1868 +
1;
1650 $gy_offset = $gy_gannen;
1651 if ( $gy_gannen == 1 ) {
1654 $gy_offset = '明治' . $gy_offset;
1656 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1657 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1658 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1659 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1660 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1664 $gy_gannen = $gy - 1912 +
1;
1665 $gy_offset = $gy_gannen;
1666 if ( $gy_gannen == 1 ) {
1669 $gy_offset = '大正' . $gy_offset;
1671 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1672 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1673 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1677 $gy_gannen = $gy - 1926 +
1;
1678 $gy_offset = $gy_gannen;
1679 if ( $gy_gannen == 1 ) {
1682 $gy_offset = '昭和' . $gy_offset;
1685 $gy_gannen = $gy - 1989 +
1;
1686 $gy_offset = $gy_gannen;
1687 if ( $gy_gannen == 1 ) {
1690 $gy_offset = '平成' . $gy_offset;
1696 return array( $gy_offset, $gm, $gd );
1700 * Roman number formatting up to 10000
1706 static function romanNumeral( $num ) {
1707 static $table = array(
1708 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1709 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1710 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1711 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM', 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
1714 $num = intval( $num );
1715 if ( $num > 10000 ||
$num <= 0 ) {
1720 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1721 if ( $num >= $pow10 ) {
1722 $s .= $table[$i][(int)floor( $num / $pow10 )];
1724 $num = $num %
$pow10;
1730 * Hebrew Gematria number formatting up to 9999
1736 static function hebrewNumeral( $num ) {
1737 static $table = array(
1738 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1739 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1740 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1741 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1744 $num = intval( $num );
1745 if ( $num > 9999 ||
$num <= 0 ) {
1750 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1751 if ( $num >= $pow10 ) {
1752 if ( $num == 15 ||
$num == 16 ) {
1753 $s .= $table[0][9] . $table[0][$num - 9];
1756 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1757 if ( $pow10 == 1000 ) {
1762 $num = $num %
$pow10;
1764 if ( strlen( $s ) == 2 ) {
1767 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1768 $str .= substr( $s, strlen( $s ) - 2, 2 );
1770 $start = substr( $str, 0, strlen( $str ) - 2 );
1771 $end = substr( $str, strlen( $str ) - 2 );
1774 $str = $start . 'ך';
1777 $str = $start . 'ם';
1780 $str = $start . 'ן';
1783 $str = $start . 'ף';
1786 $str = $start . 'ץ';
1793 * Used by date() and time() to adjust the time output.
1795 * @param $ts Int the time in date('YmdHis') format
1796 * @param $tz Mixed: adjust the time by this amount (default false, mean we
1797 * get user timecorrection setting)
1800 function userAdjust( $ts, $tz = false ) {
1801 global $wgUser, $wgLocalTZoffset;
1803 if ( $tz === false ) {
1804 $tz = $wgUser->getOption( 'timecorrection' );
1807 $data = explode( '|', $tz, 3 );
1809 if ( $data[0] == 'ZoneInfo' ) {
1810 wfSuppressWarnings();
1811 $userTZ = timezone_open( $data[2] );
1812 wfRestoreWarnings();
1813 if ( $userTZ !== false ) {
1814 $date = date_create( $ts, timezone_open( 'UTC' ) );
1815 date_timezone_set( $date, $userTZ );
1816 $date = date_format( $date, 'YmdHis' );
1819 # Unrecognized timezone, default to 'Offset' with the stored offset.
1820 $data[0] = 'Offset';
1824 if ( $data[0] == 'System' ||
$tz == '' ) {
1825 # Global offset in minutes.
1826 if ( isset( $wgLocalTZoffset ) ) {
1827 $minDiff = $wgLocalTZoffset;
1829 } elseif ( $data[0] == 'Offset' ) {
1830 $minDiff = intval( $data[1] );
1832 $data = explode( ':', $tz );
1833 if ( count( $data ) == 2 ) {
1834 $data[0] = intval( $data[0] );
1835 $data[1] = intval( $data[1] );
1836 $minDiff = abs( $data[0] ) * 60 +
$data[1];
1837 if ( $data[0] < 0 ) {
1838 $minDiff = -$minDiff;
1841 $minDiff = intval( $data[0] ) * 60;
1845 # No difference ? Return time unchanged
1846 if ( 0 == $minDiff ) {
1850 wfSuppressWarnings(); // E_STRICT system time bitching
1851 # Generate an adjusted date; take advantage of the fact that mktime
1852 # will normalize out-of-range values so we don't have to split $minDiff
1853 # into hours and minutes.
1855 (int)substr( $ts, 8, 2 ) ), # Hours
1856 (int)substr( $ts, 10, 2 ) +
$minDiff, # Minutes
1857 (int)substr( $ts, 12, 2 ), # Seconds
1858 (int)substr( $ts, 4, 2 ), # Month
1859 (int)substr( $ts, 6, 2 ), # Day
1860 (int)substr( $ts, 0, 4 ) ); # Year
1862 $date = date( 'YmdHis', $t );
1863 wfRestoreWarnings();
1869 * This is meant to be used by time(), date(), and timeanddate() to get
1870 * the date preference they're supposed to use, it should be used in
1874 * function timeanddate([...], $format = true) {
1875 * $datePreference = $this->dateFormat($format);
1880 * @param $usePrefs Mixed: if true, the user's preference is used
1881 * if false, the site/language default is used
1882 * if int/string, assumed to be a format.
1885 function dateFormat( $usePrefs = true ) {
1888 if ( is_bool( $usePrefs ) ) {
1890 $datePreference = $wgUser->getDatePreference();
1892 $datePreference = (string)User
::getDefaultOption( 'date' );
1895 $datePreference = (string)$usePrefs;
1899 if ( $datePreference == '' ) {
1903 return $datePreference;
1907 * Get a format string for a given type and preference
1908 * @param $type string May be date, time or both
1909 * @param $pref string The format name as it appears in Messages*.php
1913 function getDateFormatString( $type, $pref ) {
1914 if ( !isset( $this->dateFormatStrings
[$type][$pref] ) ) {
1915 if ( $pref == 'default' ) {
1916 $pref = $this->getDefaultDateFormat();
1917 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1919 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1920 if ( is_null( $df ) ) {
1921 $pref = $this->getDefaultDateFormat();
1922 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1925 $this->dateFormatStrings
[$type][$pref] = $df;
1927 return $this->dateFormatStrings
[$type][$pref];
1931 * @param $ts Mixed: the time format which needs to be turned into a
1932 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1933 * @param $adj Bool: whether to adjust the time output according to the
1934 * user configured offset ($timecorrection)
1935 * @param $format Mixed: true to use user's date format preference
1936 * @param $timecorrection String|bool the time offset as returned by
1937 * validateTimeZone() in Special:Preferences
1940 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1941 $ts = wfTimestamp( TS_MW
, $ts );
1943 $ts = $this->userAdjust( $ts, $timecorrection );
1945 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
1946 return $this->sprintfDate( $df, $ts );
1950 * @param $ts Mixed: the time format which needs to be turned into a
1951 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1952 * @param $adj Bool: whether to adjust the time output according to the
1953 * user configured offset ($timecorrection)
1954 * @param $format Mixed: true to use user's date format preference
1955 * @param $timecorrection String|bool the time offset as returned by
1956 * validateTimeZone() in Special:Preferences
1959 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1960 $ts = wfTimestamp( TS_MW
, $ts );
1962 $ts = $this->userAdjust( $ts, $timecorrection );
1964 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
1965 return $this->sprintfDate( $df, $ts );
1969 * @param $ts Mixed: the time format which needs to be turned into a
1970 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1971 * @param $adj Bool: whether to adjust the time output according to the
1972 * user configured offset ($timecorrection)
1973 * @param $format Mixed: what format to return, if it's false output the
1974 * default one (default true)
1975 * @param $timecorrection String|bool the time offset as returned by
1976 * validateTimeZone() in Special:Preferences
1979 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
1980 $ts = wfTimestamp( TS_MW
, $ts );
1982 $ts = $this->userAdjust( $ts, $timecorrection );
1984 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
1985 return $this->sprintfDate( $df, $ts );
1989 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
1993 * @param integer $seconds The amount of seconds.
1994 * @param array $chosenIntervals The intervals to enable.
1998 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
1999 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2001 $segments = array();
2003 foreach ( $intervals as $intervalName => $intervalValue ) {
2004 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2005 $segments[] = $message->inLanguage( $this )->escaped();
2008 return $this->listToText( $segments );
2012 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2013 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2017 * @param integer $seconds The amount of seconds.
2018 * @param array $chosenIntervals The intervals to enable.
2022 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2023 if ( empty( $chosenIntervals ) ) {
2024 $chosenIntervals = array( 'millennia', 'centuries', 'decades', 'years', 'days', 'hours', 'minutes', 'seconds' );
2027 $intervals = array_intersect_key( self
::$durationIntervals, array_flip( $chosenIntervals ) );
2028 $sortedNames = array_keys( $intervals );
2029 $smallestInterval = array_pop( $sortedNames );
2031 $segments = array();
2033 foreach ( $intervals as $name => $length ) {
2034 $value = floor( $seconds / $length );
2036 if ( $value > 0 ||
( $name == $smallestInterval && empty( $segments ) ) ) {
2037 $seconds -= $value * $length;
2038 $segments[$name] = $value;
2046 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2048 * @param $type String: can be 'date', 'time' or 'both'
2049 * @param $ts Mixed: the time format which needs to be turned into a
2050 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2051 * @param $user User object used to get preferences for timezone and format
2052 * @param $options Array, can contain the following keys:
2053 * - 'timecorrection': time correction, can have the following values:
2054 * - true: use user's preference
2055 * - false: don't use time correction
2056 * - integer: value of time correction in minutes
2057 * - 'format': format to use, can have the following values:
2058 * - true: use user's preference
2059 * - false: use default preference
2060 * - string: format to use
2064 private function internalUserTimeAndDate( $type, $ts, User
$user, array $options ) {
2065 $ts = wfTimestamp( TS_MW
, $ts );
2066 $options +
= array( 'timecorrection' => true, 'format' => true );
2067 if ( $options['timecorrection'] !== false ) {
2068 if ( $options['timecorrection'] === true ) {
2069 $offset = $user->getOption( 'timecorrection' );
2071 $offset = $options['timecorrection'];
2073 $ts = $this->userAdjust( $ts, $offset );
2075 if ( $options['format'] === true ) {
2076 $format = $user->getDatePreference();
2078 $format = $options['format'];
2080 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2081 return $this->sprintfDate( $df, $ts );
2085 * Get the formatted date for the given timestamp and formatted for
2088 * @param $ts Mixed: the time format which needs to be turned into a
2089 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2090 * @param $user User object used to get preferences for timezone and format
2091 * @param $options Array, can contain the following keys:
2092 * - 'timecorrection': time correction, can have the following values:
2093 * - true: use user's preference
2094 * - false: don't use time correction
2095 * - integer: value of time correction in minutes
2096 * - 'format': format to use, can have the following values:
2097 * - true: use user's preference
2098 * - false: use default preference
2099 * - string: format to use
2103 public function userDate( $ts, User
$user, array $options = array() ) {
2104 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2108 * Get the formatted time for the given timestamp and formatted for
2111 * @param $ts Mixed: the time format which needs to be turned into a
2112 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2113 * @param $user User object used to get preferences for timezone and format
2114 * @param $options Array, can contain the following keys:
2115 * - 'timecorrection': time correction, can have the following values:
2116 * - true: use user's preference
2117 * - false: don't use time correction
2118 * - integer: value of time correction in minutes
2119 * - 'format': format to use, can have the following values:
2120 * - true: use user's preference
2121 * - false: use default preference
2122 * - string: format to use
2126 public function userTime( $ts, User
$user, array $options = array() ) {
2127 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2131 * Get the formatted date and time for the given timestamp and formatted for
2134 * @param $ts Mixed: the time format which needs to be turned into a
2135 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2136 * @param $user User object used to get preferences for timezone and format
2137 * @param $options Array, can contain the following keys:
2138 * - 'timecorrection': time correction, can have the following values:
2139 * - true: use user's preference
2140 * - false: don't use time correction
2141 * - integer: value of time correction in minutes
2142 * - 'format': format to use, can have the following values:
2143 * - true: use user's preference
2144 * - false: use default preference
2145 * - string: format to use
2149 public function userTimeAndDate( $ts, User
$user, array $options = array() ) {
2150 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2154 * @param $key string
2155 * @return array|null
2157 function getMessage( $key ) {
2158 return self
::$dataCache->getSubitem( $this->mCode
, 'messages', $key );
2164 function getAllMessages() {
2165 return self
::$dataCache->getItem( $this->mCode
, 'messages' );
2174 function iconv( $in, $out, $string ) {
2175 # This is a wrapper for iconv in all languages except esperanto,
2176 # which does some nasty x-conversions beforehand
2178 # Even with //IGNORE iconv can whine about illegal characters in
2179 # *input* string. We just ignore those too.
2180 # REF: http://bugs.php.net/bug.php?id=37166
2181 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2182 wfSuppressWarnings();
2183 $text = iconv( $in, $out . '//IGNORE', $string );
2184 wfRestoreWarnings();
2188 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2191 * @param $matches array
2192 * @return mixed|string
2194 function ucwordbreaksCallbackAscii( $matches ) {
2195 return $this->ucfirst( $matches[1] );
2199 * @param $matches array
2202 function ucwordbreaksCallbackMB( $matches ) {
2203 return mb_strtoupper( $matches[0] );
2207 * @param $matches array
2210 function ucCallback( $matches ) {
2211 list( $wikiUpperChars ) = self
::getCaseMaps();
2212 return strtr( $matches[1], $wikiUpperChars );
2216 * @param $matches array
2219 function lcCallback( $matches ) {
2220 list( , $wikiLowerChars ) = self
::getCaseMaps();
2221 return strtr( $matches[1], $wikiLowerChars );
2225 * @param $matches array
2228 function ucwordsCallbackMB( $matches ) {
2229 return mb_strtoupper( $matches[0] );
2233 * @param $matches array
2236 function ucwordsCallbackWiki( $matches ) {
2237 list( $wikiUpperChars ) = self
::getCaseMaps();
2238 return strtr( $matches[0], $wikiUpperChars );
2242 * Make a string's first character uppercase
2244 * @param $str string
2248 function ucfirst( $str ) {
2250 if ( $o < 96 ) { // if already uppercase...
2252 } elseif ( $o < 128 ) {
2253 return ucfirst( $str ); // use PHP's ucfirst()
2255 // fall back to more complex logic in case of multibyte strings
2256 return $this->uc( $str, true );
2261 * Convert a string to uppercase
2263 * @param $str string
2264 * @param $first bool
2268 function uc( $str, $first = false ) {
2269 if ( function_exists( 'mb_strtoupper' ) ) {
2271 if ( $this->isMultibyte( $str ) ) {
2272 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2274 return ucfirst( $str );
2277 return $this->isMultibyte( $str ) ?
mb_strtoupper( $str ) : strtoupper( $str );
2280 if ( $this->isMultibyte( $str ) ) {
2281 $x = $first ?
'^' : '';
2282 return preg_replace_callback(
2283 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2284 array( $this, 'ucCallback' ),
2288 return $first ?
ucfirst( $str ) : strtoupper( $str );
2294 * @param $str string
2295 * @return mixed|string
2297 function lcfirst( $str ) {
2300 return strval( $str );
2301 } elseif ( $o >= 128 ) {
2302 return $this->lc( $str, true );
2303 } elseif ( $o > 96 ) {
2306 $str[0] = strtolower( $str[0] );
2312 * @param $str string
2313 * @param $first bool
2314 * @return mixed|string
2316 function lc( $str, $first = false ) {
2317 if ( function_exists( 'mb_strtolower' ) ) {
2319 if ( $this->isMultibyte( $str ) ) {
2320 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2322 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2325 return $this->isMultibyte( $str ) ?
mb_strtolower( $str ) : strtolower( $str );
2328 if ( $this->isMultibyte( $str ) ) {
2329 $x = $first ?
'^' : '';
2330 return preg_replace_callback(
2331 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2332 array( $this, 'lcCallback' ),
2336 return $first ?
strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2342 * @param $str string
2345 function isMultibyte( $str ) {
2346 return (bool)preg_match( '/[\x80-\xff]/', $str );
2350 * @param $str string
2351 * @return mixed|string
2353 function ucwords( $str ) {
2354 if ( $this->isMultibyte( $str ) ) {
2355 $str = $this->lc( $str );
2357 // regexp to find first letter in each word (i.e. after each space)
2358 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2360 // function to use to capitalize a single char
2361 if ( function_exists( 'mb_strtoupper' ) ) {
2362 return preg_replace_callback(
2364 array( $this, 'ucwordsCallbackMB' ),
2368 return preg_replace_callback(
2370 array( $this, 'ucwordsCallbackWiki' ),
2375 return ucwords( strtolower( $str ) );
2380 * capitalize words at word breaks
2382 * @param $str string
2385 function ucwordbreaks( $str ) {
2386 if ( $this->isMultibyte( $str ) ) {
2387 $str = $this->lc( $str );
2389 // since \b doesn't work for UTF-8, we explicitely define word break chars
2390 $breaks = "[ \-\(\)\}\{\.,\?!]";
2392 // find first letter after word break
2393 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2395 if ( function_exists( 'mb_strtoupper' ) ) {
2396 return preg_replace_callback(
2398 array( $this, 'ucwordbreaksCallbackMB' ),
2402 return preg_replace_callback(
2404 array( $this, 'ucwordsCallbackWiki' ),
2409 return preg_replace_callback(
2410 '/\b([\w\x80-\xff]+)\b/',
2411 array( $this, 'ucwordbreaksCallbackAscii' ),
2418 * Return a case-folded representation of $s
2420 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2421 * and $s2 are the same except for the case of their characters. It is not
2422 * necessary for the value returned to make sense when displayed.
2424 * Do *not* perform any other normalisation in this function. If a caller
2425 * uses this function when it should be using a more general normalisation
2426 * function, then fix the caller.
2432 function caseFold( $s ) {
2433 return $this->uc( $s );
2440 function checkTitleEncoding( $s ) {
2441 if ( is_array( $s ) ) {
2442 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
2444 if ( StringUtils
::isUtf8( $s ) ) {
2448 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2454 function fallback8bitEncoding() {
2455 return self
::$dataCache->getItem( $this->mCode
, 'fallback8bitEncoding' );
2459 * Most writing systems use whitespace to break up words.
2460 * Some languages such as Chinese don't conventionally do this,
2461 * which requires special handling when breaking up words for
2466 function hasWordBreaks() {
2471 * Some languages such as Chinese require word segmentation,
2472 * Specify such segmentation when overridden in derived class.
2474 * @param $string String
2477 function segmentByWord( $string ) {
2482 * Some languages have special punctuation need to be normalized.
2483 * Make such changes here.
2485 * @param $string String
2488 function normalizeForSearch( $string ) {
2489 return self
::convertDoubleWidth( $string );
2493 * convert double-width roman characters to single-width.
2494 * range: ff00-ff5f ~= 0020-007f
2496 * @param $string string
2500 protected static function convertDoubleWidth( $string ) {
2501 static $full = null;
2502 static $half = null;
2504 if ( $full === null ) {
2505 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2506 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2507 $full = str_split( $fullWidth, 3 );
2508 $half = str_split( $halfWidth );
2511 $string = str_replace( $full, $half, $string );
2516 * @param $string string
2517 * @param $pattern string
2520 protected static function insertSpace( $string, $pattern ) {
2521 $string = preg_replace( $pattern, " $1 ", $string );
2522 $string = preg_replace( '/ +/', ' ', $string );
2527 * @param $termsArray array
2530 function convertForSearchResult( $termsArray ) {
2531 # some languages, e.g. Chinese, need to do a conversion
2532 # in order for search results to be displayed correctly
2537 * Get the first character of a string.
2542 function firstChar( $s ) {
2545 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2546 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2551 if ( isset( $matches[1] ) ) {
2552 if ( strlen( $matches[1] ) != 3 ) {
2556 // Break down Hangul syllables to grab the first jamo
2557 $code = utf8ToCodepoint( $matches[1] );
2558 if ( $code < 0xac00 ||
0xd7a4 <= $code ) {
2560 } elseif ( $code < 0xb098 ) {
2561 return "\xe3\x84\xb1";
2562 } elseif ( $code < 0xb2e4 ) {
2563 return "\xe3\x84\xb4";
2564 } elseif ( $code < 0xb77c ) {
2565 return "\xe3\x84\xb7";
2566 } elseif ( $code < 0xb9c8 ) {
2567 return "\xe3\x84\xb9";
2568 } elseif ( $code < 0xbc14 ) {
2569 return "\xe3\x85\x81";
2570 } elseif ( $code < 0xc0ac ) {
2571 return "\xe3\x85\x82";
2572 } elseif ( $code < 0xc544 ) {
2573 return "\xe3\x85\x85";
2574 } elseif ( $code < 0xc790 ) {
2575 return "\xe3\x85\x87";
2576 } elseif ( $code < 0xcc28 ) {
2577 return "\xe3\x85\x88";
2578 } elseif ( $code < 0xce74 ) {
2579 return "\xe3\x85\x8a";
2580 } elseif ( $code < 0xd0c0 ) {
2581 return "\xe3\x85\x8b";
2582 } elseif ( $code < 0xd30c ) {
2583 return "\xe3\x85\x8c";
2584 } elseif ( $code < 0xd558 ) {
2585 return "\xe3\x85\x8d";
2587 return "\xe3\x85\x8e";
2594 function initEncoding() {
2595 # Some languages may have an alternate char encoding option
2596 # (Esperanto X-coding, Japanese furigana conversion, etc)
2597 # If this language is used as the primary content language,
2598 # an override to the defaults can be set here on startup.
2605 function recodeForEdit( $s ) {
2606 # For some languages we'll want to explicitly specify
2607 # which characters make it into the edit box raw
2608 # or are converted in some way or another.
2609 global $wgEditEncoding;
2610 if ( $wgEditEncoding == '' ||
$wgEditEncoding == 'UTF-8' ) {
2613 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2621 function recodeInput( $s ) {
2622 # Take the previous into account.
2623 global $wgEditEncoding;
2624 if ( $wgEditEncoding != '' ) {
2625 $enc = $wgEditEncoding;
2629 if ( $enc == 'UTF-8' ) {
2632 return $this->iconv( $enc, 'UTF-8', $s );
2637 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2638 * also cleans up certain backwards-compatible sequences, converting them
2639 * to the modern Unicode equivalent.
2641 * This is language-specific for performance reasons only.
2647 function normalize( $s ) {
2648 global $wgAllUnicodeFixes;
2649 $s = UtfNormal
::cleanUp( $s );
2650 if ( $wgAllUnicodeFixes ) {
2651 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2652 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2659 * Transform a string using serialized data stored in the given file (which
2660 * must be in the serialized subdirectory of $IP). The file contains pairs
2661 * mapping source characters to destination characters.
2663 * The data is cached in process memory. This will go faster if you have the
2664 * FastStringSearch extension.
2666 * @param $file string
2667 * @param $string string
2669 * @throws MWException
2672 function transformUsingPairFile( $file, $string ) {
2673 if ( !isset( $this->transformData
[$file] ) ) {
2674 $data = wfGetPrecompiledData( $file );
2675 if ( $data === false ) {
2676 throw new MWException( __METHOD__
. ": The transformation file $file is missing" );
2678 $this->transformData
[$file] = new ReplacementArray( $data );
2680 return $this->transformData
[$file]->replace( $string );
2684 * For right-to-left language support
2689 return self
::$dataCache->getItem( $this->mCode
, 'rtl' );
2693 * Return the correct HTML 'dir' attribute value for this language.
2697 return $this->isRTL() ?
'rtl' : 'ltr';
2701 * Return 'left' or 'right' as appropriate alignment for line-start
2702 * for this language's text direction.
2704 * Should be equivalent to CSS3 'start' text-align value....
2708 function alignStart() {
2709 return $this->isRTL() ?
'right' : 'left';
2713 * Return 'right' or 'left' as appropriate alignment for line-end
2714 * for this language's text direction.
2716 * Should be equivalent to CSS3 'end' text-align value....
2720 function alignEnd() {
2721 return $this->isRTL() ?
'left' : 'right';
2725 * A hidden direction mark (LRM or RLM), depending on the language direction.
2726 * Unlike getDirMark(), this function returns the character as an HTML entity.
2727 * This function should be used when the output is guaranteed to be HTML,
2728 * because it makes the output HTML source code more readable. When
2729 * the output is plain text or can be escaped, getDirMark() should be used.
2731 * @param $opposite Boolean Get the direction mark opposite to your language
2735 function getDirMarkEntity( $opposite = false ) {
2736 if ( $opposite ) { return $this->isRTL() ?
'‎' : '‏'; }
2737 return $this->isRTL() ?
'‏' : '‎';
2741 * A hidden direction mark (LRM or RLM), depending on the language direction.
2742 * This function produces them as invisible Unicode characters and
2743 * the output may be hard to read and debug, so it should only be used
2744 * when the output is plain text or can be escaped. When the output is
2745 * HTML, use getDirMarkEntity() instead.
2747 * @param $opposite Boolean Get the direction mark opposite to your language
2750 function getDirMark( $opposite = false ) {
2751 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
2752 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
2753 if ( $opposite ) { return $this->isRTL() ?
$lrm : $rlm; }
2754 return $this->isRTL() ?
$rlm : $lrm;
2760 function capitalizeAllNouns() {
2761 return self
::$dataCache->getItem( $this->mCode
, 'capitalizeAllNouns' );
2765 * An arrow, depending on the language direction.
2767 * @param $direction String: the direction of the arrow: forwards (default), backwards, left, right, up, down.
2770 function getArrow( $direction = 'forwards' ) {
2771 switch ( $direction ) {
2773 return $this->isRTL() ?
'←' : '→';
2775 return $this->isRTL() ?
'→' : '←';
2788 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
2792 function linkPrefixExtension() {
2793 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixExtension' );
2799 function getMagicWords() {
2800 return self
::$dataCache->getItem( $this->mCode
, 'magicWords' );
2803 protected function doMagicHook() {
2804 if ( $this->mMagicHookDone
) {
2807 $this->mMagicHookDone
= true;
2808 wfProfileIn( 'LanguageGetMagic' );
2809 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions
, $this->getCode() ) );
2810 wfProfileOut( 'LanguageGetMagic' );
2814 * Fill a MagicWord object with data from here
2818 function getMagic( $mw ) {
2819 $this->doMagicHook();
2821 if ( isset( $this->mMagicExtensions
[$mw->mId
] ) ) {
2822 $rawEntry = $this->mMagicExtensions
[$mw->mId
];
2824 $magicWords = $this->getMagicWords();
2825 if ( isset( $magicWords[$mw->mId
] ) ) {
2826 $rawEntry = $magicWords[$mw->mId
];
2832 if ( !is_array( $rawEntry ) ) {
2833 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
2835 $mw->mCaseSensitive
= $rawEntry[0];
2836 $mw->mSynonyms
= array_slice( $rawEntry, 1 );
2841 * Add magic words to the extension array
2843 * @param $newWords array
2845 function addMagicWordsByLang( $newWords ) {
2846 $fallbackChain = $this->getFallbackLanguages();
2847 $fallbackChain = array_reverse( $fallbackChain );
2848 foreach ( $fallbackChain as $code ) {
2849 if ( isset( $newWords[$code] ) ) {
2850 $this->mMagicExtensions
= $newWords[$code] +
$this->mMagicExtensions
;
2856 * Get special page names, as an associative array
2857 * case folded alias => real name
2859 function getSpecialPageAliases() {
2860 // Cache aliases because it may be slow to load them
2861 if ( is_null( $this->mExtendedSpecialPageAliases
) ) {
2863 $this->mExtendedSpecialPageAliases
=
2864 self
::$dataCache->getItem( $this->mCode
, 'specialPageAliases' );
2865 wfRunHooks( 'LanguageGetSpecialPageAliases',
2866 array( &$this->mExtendedSpecialPageAliases
, $this->getCode() ) );
2869 return $this->mExtendedSpecialPageAliases
;
2873 * Italic is unsuitable for some languages
2875 * @param $text String: the text to be emphasized.
2878 function emphasize( $text ) {
2879 return "<em>$text</em>";
2883 * Normally we output all numbers in plain en_US style, that is
2884 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
2885 * point twohundredthirtyfive. However this is not suitable for all
2886 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
2887 * Icelandic just want to use commas instead of dots, and dots instead
2888 * of commas like "293.291,235".
2890 * An example of this function being called:
2892 * wfMessage( 'message' )->numParams( $num )->text()
2895 * See LanguageGu.php for the Gujarati implementation and
2896 * $separatorTransformTable on MessageIs.php for
2897 * the , => . and . => , implementation.
2899 * @todo check if it's viable to use localeconv() for the decimal
2901 * @param $number Mixed: the string to be formatted, should be an integer
2902 * or a floating point number.
2903 * @param $nocommafy Bool: set to true for special numbers like dates
2906 public function formatNum( $number, $nocommafy = false ) {
2907 global $wgTranslateNumerals;
2908 if ( !$nocommafy ) {
2909 $number = $this->commafy( $number );
2910 $s = $this->separatorTransformTable();
2912 $number = strtr( $number, $s );
2916 if ( $wgTranslateNumerals ) {
2917 $s = $this->digitTransformTable();
2919 $number = strtr( $number, $s );
2927 * @param $number string
2930 function parseFormattedNumber( $number ) {
2931 $s = $this->digitTransformTable();
2933 $number = strtr( $number, array_flip( $s ) );
2936 $s = $this->separatorTransformTable();
2938 $number = strtr( $number, array_flip( $s ) );
2941 $number = strtr( $number, array( ',' => '' ) );
2946 * Adds commas to a given number
2948 * @param $number mixed
2951 function commafy( $number ) {
2952 $digitGroupingPattern = $this->digitGroupingPattern();
2953 if ( $number === null ) {
2957 if ( !$digitGroupingPattern ||
$digitGroupingPattern === "###,###,###" ) {
2958 // default grouping is at thousands, use the same for ###,###,### pattern too.
2959 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
2961 // Ref: http://cldr.unicode.org/translation/number-patterns
2963 if ( intval( $number ) < 0 ) {
2964 // For negative numbers apply the algorithm like positive number and add sign.
2966 $number = substr( $number, 1 );
2968 $integerPart = array();
2969 $decimalPart = array();
2970 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
2971 preg_match( "/\d+/", $number, $integerPart );
2972 preg_match( "/\.\d*/", $number, $decimalPart );
2973 $groupedNumber = ( count( $decimalPart ) > 0 ) ?
$decimalPart[0]:"";
2974 if ( $groupedNumber === $number ) {
2975 // the string does not have any number part. Eg: .12345
2976 return $sign . $groupedNumber;
2978 $start = $end = strlen( $integerPart[0] );
2979 while ( $start > 0 ) {
2980 $match = $matches[0][$numMatches -1] ;
2981 $matchLen = strlen( $match );
2982 $start = $end - $matchLen;
2986 $groupedNumber = substr( $number , $start, $end -$start ) . $groupedNumber ;
2988 if ( $numMatches > 1 ) {
2989 // use the last pattern for the rest of the number
2993 $groupedNumber = "," . $groupedNumber;
2996 return $sign . $groupedNumber;
3003 function digitGroupingPattern() {
3004 return self
::$dataCache->getItem( $this->mCode
, 'digitGroupingPattern' );
3010 function digitTransformTable() {
3011 return self
::$dataCache->getItem( $this->mCode
, 'digitTransformTable' );
3017 function separatorTransformTable() {
3018 return self
::$dataCache->getItem( $this->mCode
, 'separatorTransformTable' );
3022 * Take a list of strings and build a locale-friendly comma-separated
3023 * list, using the local comma-separator message.
3024 * The last two strings are chained with an "and".
3025 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3030 function listToText( array $l ) {
3031 $m = count( $l ) - 1;
3036 $and = $this->getMessageFromDB( 'and' );
3037 $space = $this->getMessageFromDB( 'word-separator' );
3039 $comma = $this->getMessageFromDB( 'comma-separator' );
3043 for ( $i = $m - 1; $i >= 0; $i-- ) {
3044 if ( $i == $m - 1 ) {
3045 $s = $l[$i] . $and . $space . $s;
3047 $s = $l[$i] . $comma . $s;
3054 * Take a list of strings and build a locale-friendly comma-separated
3055 * list, using the local comma-separator message.
3056 * @param $list array of strings to put in a comma list
3059 function commaList( array $list ) {
3061 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3067 * Take a list of strings and build a locale-friendly semicolon-separated
3068 * list, using the local semicolon-separator message.
3069 * @param $list array of strings to put in a semicolon list
3072 function semicolonList( array $list ) {
3074 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3080 * Same as commaList, but separate it with the pipe instead.
3081 * @param $list array of strings to put in a pipe list
3084 function pipeList( array $list ) {
3086 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3092 * Truncate a string to a specified length in bytes, appending an optional
3093 * string (e.g. for ellipses)
3095 * The database offers limited byte lengths for some columns in the database;
3096 * multi-byte character sets mean we need to ensure that only whole characters
3097 * are included, otherwise broken characters can be passed to the user
3099 * If $length is negative, the string will be truncated from the beginning
3101 * @param $string String to truncate
3102 * @param $length Int: maximum length (including ellipses)
3103 * @param $ellipsis String to append to the truncated text
3104 * @param $adjustLength Boolean: Subtract length of ellipsis from $length.
3105 * $adjustLength was introduced in 1.18, before that behaved as if false.
3108 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3109 # Use the localized ellipsis character
3110 if ( $ellipsis == '...' ) {
3111 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3113 # Check if there is no need to truncate
3114 if ( $length == 0 ) {
3115 return $ellipsis; // convention
3116 } elseif ( strlen( $string ) <= abs( $length ) ) {
3117 return $string; // no need to truncate
3119 $stringOriginal = $string;
3120 # If ellipsis length is >= $length then we can't apply $adjustLength
3121 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3122 $string = $ellipsis; // this can be slightly unexpected
3123 # Otherwise, truncate and add ellipsis...
3125 $eLength = $adjustLength ?
strlen( $ellipsis ) : 0;
3126 if ( $length > 0 ) {
3127 $length -= $eLength;
3128 $string = substr( $string, 0, $length ); // xyz...
3129 $string = $this->removeBadCharLast( $string );
3130 $string = $string . $ellipsis;
3132 $length +
= $eLength;
3133 $string = substr( $string, $length ); // ...xyz
3134 $string = $this->removeBadCharFirst( $string );
3135 $string = $ellipsis . $string;
3138 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3139 # This check is *not* redundant if $adjustLength, due to the single case where
3140 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3141 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3144 return $stringOriginal;
3149 * Remove bytes that represent an incomplete Unicode character
3150 * at the end of string (e.g. bytes of the char are missing)
3152 * @param $string String
3155 protected function removeBadCharLast( $string ) {
3156 if ( $string != '' ) {
3157 $char = ord( $string[strlen( $string ) - 1] );
3159 if ( $char >= 0xc0 ) {
3160 # We got the first byte only of a multibyte char; remove it.
3161 $string = substr( $string, 0, -1 );
3162 } elseif ( $char >= 0x80 &&
3163 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3164 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) )
3166 # We chopped in the middle of a character; remove it
3174 * Remove bytes that represent an incomplete Unicode character
3175 * at the start of string (e.g. bytes of the char are missing)
3177 * @param $string String
3180 protected function removeBadCharFirst( $string ) {
3181 if ( $string != '' ) {
3182 $char = ord( $string[0] );
3183 if ( $char >= 0x80 && $char < 0xc0 ) {
3184 # We chopped in the middle of a character; remove the whole thing
3185 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3192 * Truncate a string of valid HTML to a specified length in bytes,
3193 * appending an optional string (e.g. for ellipses), and return valid HTML
3195 * This is only intended for styled/linked text, such as HTML with
3196 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3197 * Also, this will not detect things like "display:none" CSS.
3199 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3201 * @param string $text HTML string to truncate
3202 * @param int $length (zero/positive) Maximum length (including ellipses)
3203 * @param string $ellipsis String to append to the truncated text
3206 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3207 # Use the localized ellipsis character
3208 if ( $ellipsis == '...' ) {
3209 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3211 # Check if there is clearly no need to truncate
3212 if ( $length <= 0 ) {
3213 return $ellipsis; // no text shown, nothing to format (convention)
3214 } elseif ( strlen( $text ) <= $length ) {
3215 return $text; // string short enough even *with* HTML (short-circuit)
3218 $dispLen = 0; // innerHTML legth so far
3219 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3220 $tagType = 0; // 0-open, 1-close
3221 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3222 $entityState = 0; // 0-not entity, 1-entity
3223 $tag = $ret = ''; // accumulated tag name, accumulated result string
3224 $openTags = array(); // open tag stack
3225 $maybeState = null; // possible truncation state
3227 $textLen = strlen( $text );
3228 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3229 for ( $pos = 0; true; ++
$pos ) {
3230 # Consider truncation once the display length has reached the maximim.
3231 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3232 # Check that we're not in the middle of a bracket/entity...
3233 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3234 if ( !$testingEllipsis ) {
3235 $testingEllipsis = true;
3236 # Save where we are; we will truncate here unless there turn out to
3237 # be so few remaining characters that truncation is not necessary.
3238 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3239 $maybeState = array( $ret, $openTags ); // save state
3241 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3242 # String in fact does need truncation, the truncation point was OK.
3243 list( $ret, $openTags ) = $maybeState; // reload state
3244 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3245 $ret .= $ellipsis; // add ellipsis
3249 if ( $pos >= $textLen ) break; // extra iteration just for above checks
3251 # Read the next char...
3253 $lastCh = $pos ?
$text[$pos - 1] : '';
3254 $ret .= $ch; // add to result string
3256 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3257 $entityState = 0; // for bad HTML
3258 $bracketState = 1; // tag started (checking for backslash)
3259 } elseif ( $ch == '>' ) {
3260 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3261 $entityState = 0; // for bad HTML
3262 $bracketState = 0; // out of brackets
3263 } elseif ( $bracketState == 1 ) {
3265 $tagType = 1; // close tag (e.g. "</span>")
3267 $tagType = 0; // open tag (e.g. "<span>")
3270 $bracketState = 2; // building tag name
3271 } elseif ( $bracketState == 2 ) {
3275 // Name found (e.g. "<a href=..."), add on tag attributes...
3276 $pos +
= $this->truncate_skip( $ret, $text, "<>", $pos +
1 );
3278 } elseif ( $bracketState == 0 ) {
3279 if ( $entityState ) {
3282 $dispLen++
; // entity is one displayed char
3285 if ( $neLength == 0 && !$maybeState ) {
3286 // Save state without $ch. We want to *hit* the first
3287 // display char (to get tags) but not *use* it if truncating.
3288 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3291 $entityState = 1; // entity found, (e.g. " ")
3293 $dispLen++
; // this char is displayed
3294 // Add the next $max display text chars after this in one swoop...
3295 $max = ( $testingEllipsis ?
$length : $neLength ) - $dispLen;
3296 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos +
1, $max );
3297 $dispLen +
= $skipped;
3303 // Close the last tag if left unclosed by bad HTML
3304 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3305 while ( count( $openTags ) > 0 ) {
3306 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3312 * truncateHtml() helper function
3313 * like strcspn() but adds the skipped chars to $ret
3322 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3323 if ( $len === null ) {
3324 $len = -1; // -1 means "no limit" for strcspn
3325 } elseif ( $len < 0 ) {
3329 if ( $start < strlen( $text ) ) {
3330 $skipCount = strcspn( $text, $search, $start, $len );
3331 $ret .= substr( $text, $start, $skipCount );
3337 * truncateHtml() helper function
3338 * (a) push or pop $tag from $openTags as needed
3339 * (b) clear $tag value
3340 * @param &$tag string Current HTML tag name we are looking at
3341 * @param $tagType int (0-open tag, 1-close tag)
3342 * @param $lastCh string Character before the '>' that ended this tag
3343 * @param &$openTags array Open tag stack (not accounting for $tag)
3345 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3346 $tag = ltrim( $tag );
3348 if ( $tagType == 0 && $lastCh != '/' ) {
3349 $openTags[] = $tag; // tag opened (didn't close itself)
3350 } elseif ( $tagType == 1 ) {
3351 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3352 array_pop( $openTags ); // tag closed
3360 * Grammatical transformations, needed for inflected languages
3361 * Invoked by putting {{grammar:case|word}} in a message
3363 * @param $word string
3364 * @param $case string
3367 function convertGrammar( $word, $case ) {
3368 global $wgGrammarForms;
3369 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3370 return $wgGrammarForms[$this->getCode()][$case][$word];
3375 * Get the grammar forms for the content language
3376 * @return array of grammar forms
3379 function getGrammarForms() {
3380 global $wgGrammarForms;
3381 if ( isset( $wgGrammarForms[$this->getCode()] ) && is_array( $wgGrammarForms[$this->getCode()] ) ) {
3382 return $wgGrammarForms[$this->getCode()];
3387 * Provides an alternative text depending on specified gender.
3388 * Usage {{gender:username|masculine|feminine|neutral}}.
3389 * username is optional, in which case the gender of current user is used,
3390 * but only in (some) interface messages; otherwise default gender is used.
3392 * If no forms are given, an empty string is returned. If only one form is
3393 * given, it will be returned unconditionally. These details are implied by
3394 * the caller and cannot be overridden in subclasses.
3396 * If more than one form is given, the default is to use the neutral one
3397 * if it is specified, and to use the masculine one otherwise. These
3398 * details can be overridden in subclasses.
3400 * @param $gender string
3401 * @param $forms array
3405 function gender( $gender, $forms ) {
3406 if ( !count( $forms ) ) {
3409 $forms = $this->preConvertPlural( $forms, 2 );
3410 if ( $gender === 'male' ) {
3413 if ( $gender === 'female' ) {
3416 return isset( $forms[2] ) ?
$forms[2] : $forms[0];
3420 * Plural form transformations, needed for some languages.
3421 * For example, there are 3 form of plural in Russian and Polish,
3422 * depending on "count mod 10". See [[w:Plural]]
3423 * For English it is pretty simple.
3425 * Invoked by putting {{plural:count|wordform1|wordform2}}
3426 * or {{plural:count|wordform1|wordform2|wordform3}}
3428 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3430 * @param $count Integer: non-localized number
3431 * @param $forms Array: different plural forms
3432 * @return string Correct form of plural for $count in this language
3434 function convertPlural( $count, $forms ) {
3435 if ( !count( $forms ) ) {
3439 // Handle explicit 0= and 1= forms
3440 foreach ( $forms as $index => $form ) {
3441 if ( isset( $form[1] ) && $form[1] === '=' ) {
3442 if ( $form[0] === (string) $count ) {
3443 return substr( $form, 2 );
3445 unset( $forms[$index] );
3448 $forms = array_values( $forms );
3450 $pluralForm = $this->getPluralForm( $count );
3451 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3452 return $forms[$pluralForm];
3456 * Checks that convertPlural was given an array and pads it to requested
3457 * amount of forms by copying the last one.
3459 * @param $count Integer: How many forms should there be at least
3460 * @param $forms Array of forms given to convertPlural
3461 * @return array Padded array of forms or an exception if not an array
3463 protected function preConvertPlural( /* Array */ $forms, $count ) {
3464 while ( count( $forms ) < $count ) {
3465 $forms[] = $forms[count( $forms ) - 1];
3471 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
3472 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3473 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3474 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3477 * @param $str String: the validated block duration in English
3478 * @return string Somehow translated block duration
3479 * @see LanguageFi.php for example implementation
3481 function translateBlockExpiry( $str ) {
3482 $duration = SpecialBlock
::getSuggestedDurations( $this );
3483 foreach ( $duration as $show => $value ) {
3484 if ( strcmp( $str, $value ) == 0 ) {
3485 return htmlspecialchars( trim( $show ) );
3489 // Since usually only infinite or indefinite is only on list, so try
3490 // equivalents if still here.
3491 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3492 if ( in_array( $str, $indefs ) ) {
3493 foreach ( $indefs as $val ) {
3494 $show = array_search( $val, $duration, true );
3495 if ( $show !== false ) {
3496 return htmlspecialchars( trim( $show ) );
3501 // If all else fails, return a standard duration or timestamp description.
3502 $time = strtotime( $str, 0 );
3503 if ( $time === false ) { // Unknown format. Return it as-is in case.
3505 } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
3506 // $time is relative to 0 so it's a duration length.
3507 return $this->formatDuration( $time );
3508 } else { // It's an absolute timestamp.
3509 if ( $time === 0 ) {
3510 // wfTimestamp() handles 0 as current time instead of epoch.
3511 return $this->timeanddate( '19700101000000' );
3513 return $this->timeanddate( $time );
3519 * languages like Chinese need to be segmented in order for the diff
3522 * @param $text String
3525 public function segmentForDiff( $text ) {
3530 * and unsegment to show the result
3532 * @param $text String
3535 public function unsegmentForDiff( $text ) {
3540 * Return the LanguageConverter used in the Language
3543 * @return LanguageConverter
3545 public function getConverter() {
3546 return $this->mConverter
;
3550 * convert text to all supported variants
3552 * @param $text string
3555 public function autoConvertToAllVariants( $text ) {
3556 return $this->mConverter
->autoConvertToAllVariants( $text );
3560 * convert text to different variants of a language.
3562 * @param $text string
3565 public function convert( $text ) {
3566 return $this->mConverter
->convert( $text );
3570 * Convert a Title object to a string in the preferred variant
3572 * @param $title Title
3575 public function convertTitle( $title ) {
3576 return $this->mConverter
->convertTitle( $title );
3580 * Convert a namespace index to a string in the preferred variant
3585 public function convertNamespace( $ns ) {
3586 return $this->mConverter
->convertNamespace( $ns );
3590 * Check if this is a language with variants
3594 public function hasVariants() {
3595 return sizeof( $this->getVariants() ) > 1;
3599 * Check if the language has the specific variant
3602 * @param $variant string
3605 public function hasVariant( $variant ) {
3606 return (bool)$this->mConverter
->validateVariant( $variant );
3610 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3612 * @param $text string
3615 public function armourMath( $text ) {
3616 return $this->mConverter
->armourMath( $text );
3620 * Perform output conversion on a string, and encode for safe HTML output.
3621 * @param $text String text to be converted
3622 * @param $isTitle Bool whether this conversion is for the article title
3624 * @todo this should get integrated somewhere sane
3626 public function convertHtml( $text, $isTitle = false ) {
3627 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3631 * @param $key string
3634 public function convertCategoryKey( $key ) {
3635 return $this->mConverter
->convertCategoryKey( $key );
3639 * Get the list of variants supported by this language
3640 * see sample implementation in LanguageZh.php
3642 * @return array an array of language codes
3644 public function getVariants() {
3645 return $this->mConverter
->getVariants();
3651 public function getPreferredVariant() {
3652 return $this->mConverter
->getPreferredVariant();
3658 public function getDefaultVariant() {
3659 return $this->mConverter
->getDefaultVariant();
3665 public function getURLVariant() {
3666 return $this->mConverter
->getURLVariant();
3670 * If a language supports multiple variants, it is
3671 * possible that non-existing link in one variant
3672 * actually exists in another variant. this function
3673 * tries to find it. See e.g. LanguageZh.php
3675 * @param $link String: the name of the link
3676 * @param $nt Mixed: the title object of the link
3677 * @param $ignoreOtherCond Boolean: to disable other conditions when
3678 * we need to transclude a template or update a category's link
3679 * @return null the input parameters may be modified upon return
3681 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
3682 $this->mConverter
->findVariantLink( $link, $nt, $ignoreOtherCond );
3686 * If a language supports multiple variants, converts text
3687 * into an array of all possible variants of the text:
3688 * 'variant' => text in that variant
3690 * @deprecated since 1.17 Use autoConvertToAllVariants()
3692 * @param $text string
3696 public function convertLinkToAllVariants( $text ) {
3697 return $this->mConverter
->convertLinkToAllVariants( $text );
3701 * returns language specific options used by User::getPageRenderHash()
3702 * for example, the preferred language variant
3706 function getExtraHashOptions() {
3707 return $this->mConverter
->getExtraHashOptions();
3711 * For languages that support multiple variants, the title of an
3712 * article may be displayed differently in different variants. this
3713 * function returns the apporiate title defined in the body of the article.
3717 public function getParsedTitle() {
3718 return $this->mConverter
->getParsedTitle();
3722 * Prepare external link text for conversion. When the text is
3723 * a URL, it shouldn't be converted, and it'll be wrapped in
3724 * the "raw" tag (-{R| }-) to prevent conversion.
3726 * This function is called "markNoConversion" for historical
3729 * @param $text String: text to be used for external link
3730 * @param $noParse bool: wrap it without confirming it's a real URL first
3731 * @return string the tagged text
3733 public function markNoConversion( $text, $noParse = false ) {
3734 // Excluding protocal-relative URLs may avoid many false positives.
3735 if ( $noParse ||
preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
3736 return $this->mConverter
->markNoConversion( $text );
3743 * A regular expression to match legal word-trailing characters
3744 * which should be merged onto a link of the form [[foo]]bar.
3748 public function linkTrail() {
3749 return self
::$dataCache->getItem( $this->mCode
, 'linkTrail' );
3755 function getLangObj() {
3760 * Get the RFC 3066 code for this language object
3762 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3763 * htmlspecialchars() or similar
3767 public function getCode() {
3768 return $this->mCode
;
3772 * Get the code in Bcp47 format which we can use
3773 * inside of html lang="" tags.
3775 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3776 * htmlspecialchars() or similar.
3781 public function getHtmlCode() {
3782 if ( is_null( $this->mHtmlCode
) ) {
3783 $this->mHtmlCode
= wfBCP47( $this->getCode() );
3785 return $this->mHtmlCode
;
3789 * @param $code string
3791 public function setCode( $code ) {
3792 $this->mCode
= $code;
3793 // Ensure we don't leave an incorrect html code lying around
3794 $this->mHtmlCode
= null;
3798 * Get the name of a file for a certain language code
3799 * @param $prefix string Prepend this to the filename
3800 * @param $code string Language code
3801 * @param $suffix string Append this to the filename
3802 * @throws MWException
3803 * @return string $prefix . $mangledCode . $suffix
3805 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
3806 // Protect against path traversal
3807 if ( !Language
::isValidCode( $code )
3808 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
3810 throw new MWException( "Invalid language code \"$code\"" );
3813 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
3817 * Get the language code from a file name. Inverse of getFileName()
3818 * @param $filename string $prefix . $languageCode . $suffix
3819 * @param $prefix string Prefix before the language code
3820 * @param $suffix string Suffix after the language code
3821 * @return string Language code, or false if $prefix or $suffix isn't found
3823 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
3825 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
3826 preg_quote( $suffix, '/' ) . '/', $filename, $m );
3827 if ( !count( $m ) ) {
3830 return str_replace( '_', '-', strtolower( $m[1] ) );
3834 * @param $code string
3837 public static function getMessagesFileName( $code ) {
3839 $file = self
::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
3840 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
3845 * @param $code string
3848 public static function getClassFileName( $code ) {
3850 return self
::getFileName( "$IP/languages/classes/Language", $code, '.php' );
3854 * Get the first fallback for a given language.
3856 * @param $code string
3858 * @return bool|string
3860 public static function getFallbackFor( $code ) {
3861 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3864 $fallbacks = self
::getFallbacksFor( $code );
3865 $first = array_shift( $fallbacks );
3871 * Get the ordered list of fallback languages.
3874 * @param $code string Language code
3877 public static function getFallbacksFor( $code ) {
3878 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3881 $v = self
::getLocalisationCache()->getItem( $code, 'fallback' );
3882 $v = array_map( 'trim', explode( ',', $v ) );
3883 if ( $v[count( $v ) - 1] !== 'en' ) {
3891 * Get all messages for a given language
3892 * WARNING: this may take a long time. If you just need all message *keys*
3893 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
3895 * @param $code string
3899 public static function getMessagesFor( $code ) {
3900 return self
::getLocalisationCache()->getItem( $code, 'messages' );
3904 * Get a message for a given language
3906 * @param $key string
3907 * @param $code string
3911 public static function getMessageFor( $key, $code ) {
3912 return self
::getLocalisationCache()->getSubitem( $code, 'messages', $key );
3916 * Get all message keys for a given language. This is a faster alternative to
3917 * array_keys( Language::getMessagesFor( $code ) )
3920 * @param $code string Language code
3921 * @return array of message keys (strings)
3923 public static function getMessageKeysFor( $code ) {
3924 return self
::getLocalisationCache()->getSubItemList( $code, 'messages' );
3931 function fixVariableInNamespace( $talk ) {
3932 if ( strpos( $talk, '$1' ) === false ) {
3936 global $wgMetaNamespace;
3937 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
3939 # Allow grammar transformations
3940 # Allowing full message-style parsing would make simple requests
3941 # such as action=raw much more expensive than they need to be.
3942 # This will hopefully cover most cases.
3943 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
3944 array( &$this, 'replaceGrammarInNamespace' ), $talk );
3945 return str_replace( ' ', '_', $talk );
3952 function replaceGrammarInNamespace( $m ) {
3953 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
3957 * @throws MWException
3960 static function getCaseMaps() {
3961 static $wikiUpperChars, $wikiLowerChars;
3962 if ( isset( $wikiUpperChars ) ) {
3963 return array( $wikiUpperChars, $wikiLowerChars );
3966 wfProfileIn( __METHOD__
);
3967 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
3968 if ( $arr === false ) {
3969 throw new MWException(
3970 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
3972 $wikiUpperChars = $arr['wikiUpperChars'];
3973 $wikiLowerChars = $arr['wikiLowerChars'];
3974 wfProfileOut( __METHOD__
);
3975 return array( $wikiUpperChars, $wikiLowerChars );
3979 * Decode an expiry (block, protection, etc) which has come from the DB
3981 * @todo FIXME: why are we returnings DBMS-dependent strings???
3983 * @param $expiry String: Database expiry String
3984 * @param $format Bool|Int true to process using language functions, or TS_ constant
3985 * to return the expiry in a given timestamp
3989 public function formatExpiry( $expiry, $format = true ) {
3990 static $infinity, $infinityMsg;
3991 if ( $infinity === null ) {
3992 $infinityMsg = wfMessage( 'infiniteblock' );
3993 $infinity = wfGetDB( DB_SLAVE
)->getInfinity();
3996 if ( $expiry == '' ||
$expiry == $infinity ) {
3997 return $format === true
4001 return $format === true
4002 ?
$this->timeanddate( $expiry, /* User preference timezone */ true )
4003 : wfTimestamp( $format, $expiry );
4009 * @param $seconds int|float
4010 * @param $format Array Optional
4011 * If $format['avoid'] == 'avoidseconds' - don't mention seconds if $seconds >= 1 hour
4012 * If $format['avoid'] == 'avoidminutes' - don't mention seconds/minutes if $seconds > 48 hours
4013 * If $format['noabbrevs'] is true - use 'seconds' and friends instead of 'seconds-abbrev' and friends
4014 * For backwards compatibility, $format may also be one of the strings 'avoidseconds' or 'avoidminutes'
4017 function formatTimePeriod( $seconds, $format = array() ) {
4018 if ( !is_array( $format ) ) {
4019 $format = array( 'avoid' => $format ); // For backwards compatibility
4021 if ( !isset( $format['avoid'] ) ) {
4022 $format['avoid'] = false;
4024 if ( !isset( $format['noabbrevs' ] ) ) {
4025 $format['noabbrevs'] = false;
4027 $secondsMsg = wfMessage(
4028 $format['noabbrevs'] ?
'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4029 $minutesMsg = wfMessage(
4030 $format['noabbrevs'] ?
'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4031 $hoursMsg = wfMessage(
4032 $format['noabbrevs'] ?
'hours' : 'hours-abbrev' )->inLanguage( $this );
4033 $daysMsg = wfMessage(
4034 $format['noabbrevs'] ?
'days' : 'days-abbrev' )->inLanguage( $this );
4036 if ( round( $seconds * 10 ) < 100 ) {
4037 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4038 $s = $secondsMsg->params( $s )->text();
4039 } elseif ( round( $seconds ) < 60 ) {
4040 $s = $this->formatNum( round( $seconds ) );
4041 $s = $secondsMsg->params( $s )->text();
4042 } elseif ( round( $seconds ) < 3600 ) {
4043 $minutes = floor( $seconds / 60 );
4044 $secondsPart = round( fmod( $seconds, 60 ) );
4045 if ( $secondsPart == 60 ) {
4049 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4051 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4052 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4053 $hours = floor( $seconds / 3600 );
4054 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4055 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4056 if ( $secondsPart == 60 ) {
4060 if ( $minutes == 60 ) {
4064 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4066 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4067 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4068 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4071 $days = floor( $seconds / 86400 );
4072 if ( $format['avoid'] === 'avoidminutes' ) {
4073 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4074 if ( $hours == 24 ) {
4078 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4080 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4081 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4082 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4083 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4084 if ( $minutes == 60 ) {
4088 if ( $hours == 24 ) {
4092 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4094 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4096 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4098 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4100 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4107 * Format a bitrate for output, using an appropriate
4108 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to the magnitude in question
4110 * This use base 1000. For base 1024 use formatSize(), for another base
4111 * see formatComputingNumbers()
4116 function formatBitrate( $bps ) {
4117 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4121 * @param $size int Size of the unit
4122 * @param $boundary int Size boundary (1000, or 1024 in most cases)
4123 * @param $messageKey string Message key to be uesd
4126 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4128 return str_replace( '$1', $this->formatNum( $size ),
4129 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4132 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4135 $maxIndex = count( $sizes ) - 1;
4136 while ( $size >= $boundary && $index < $maxIndex ) {
4141 // For small sizes no decimal places necessary
4144 // For MB and bigger two decimal places are smarter
4147 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4149 $size = round( $size, $round );
4150 $text = $this->getMessageFromDB( $msg );
4151 return str_replace( '$1', $this->formatNum( $size ), $text );
4155 * Format a size in bytes for output, using an appropriate
4156 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4158 * This method use base 1024. For base 1000 use formatBitrate(), for
4159 * another base see formatComputingNumbers()
4161 * @param $size int Size to format
4162 * @return string Plain text (not HTML)
4164 function formatSize( $size ) {
4165 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4169 * Make a list item, used by various special pages
4171 * @param $page String Page link
4172 * @param $details String Text between brackets
4173 * @param $oppositedm Boolean Add the direction mark opposite to your
4174 * language, to display text properly
4177 function specialList( $page, $details, $oppositedm = true ) {
4178 $dirmark = ( $oppositedm ?
$this->getDirMark( true ) : '' ) .
4179 $this->getDirMark();
4180 $details = $details ?
$dirmark . $this->getMessageFromDB( 'word-separator' ) .
4181 wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : '';
4182 return $page . $details;
4186 * Generate (prev x| next x) (20|50|100...) type links for paging
4188 * @param $title Title object to link
4189 * @param $offset Integer offset parameter
4190 * @param $limit Integer limit parameter
4191 * @param $query array|String optional URL query parameter string
4192 * @param $atend Bool optional param for specified if this is the last page
4195 public function viewPrevNext( Title
$title, $offset, $limit, array $query = array(), $atend = false ) {
4196 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4198 # Make 'previous' link
4199 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4200 if ( $offset > 0 ) {
4201 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4202 $query, $prev, 'prevn-title', 'mw-prevlink' );
4204 $plink = htmlspecialchars( $prev );
4208 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4210 $nlink = htmlspecialchars( $next );
4212 $nlink = $this->numLink( $title, $offset +
$limit, $limit,
4213 $query, $next, 'prevn-title', 'mw-nextlink' );
4216 # Make links to set number of items per page
4217 $numLinks = array();
4218 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4219 $numLinks[] = $this->numLink( $title, $offset, $num,
4220 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4223 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4224 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4228 * Helper function for viewPrevNext() that generates links
4230 * @param $title Title object to link
4231 * @param $offset Integer offset parameter
4232 * @param $limit Integer limit parameter
4233 * @param $query Array extra query parameters
4234 * @param $link String text to use for the link; will be escaped
4235 * @param $tooltipMsg String name of the message to use as tooltip
4236 * @param $class String value of the "class" attribute of the link
4237 * @return String HTML fragment
4239 private function numLink( Title
$title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) {
4240 $query = array( 'limit' => $limit, 'offset' => $offset ) +
$query;
4241 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4242 return Html
::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4243 'title' => $tooltip, 'class' => $class ), $link );
4247 * Get the conversion rule title, if any.
4251 public function getConvRuleTitle() {
4252 return $this->mConverter
->getConvRuleTitle();
4256 * Get the compiled plural rules for the language
4258 * @return array Associative array with plural form, and plural rule as key-value pairs
4260 public function getCompiledPluralRules() {
4261 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'compiledPluralRules' );
4262 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4263 if ( !$pluralRules ) {
4264 foreach ( $fallbacks as $fallbackCode ) {
4265 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4266 if ( $pluralRules ) {
4271 return $pluralRules;
4275 * Get the plural rules for the language
4277 * @return array Associative array with plural form, and plural rule as key-value pairs
4279 public function getPluralRules() {
4280 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'pluralRules' );
4281 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4282 if ( !$pluralRules ) {
4283 foreach ( $fallbacks as $fallbackCode ) {
4284 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4285 if ( $pluralRules ) {
4290 return $pluralRules;
4294 * Find the plural form matching to the given number
4295 * It return the form index.
4296 * @return int The index of the plural form
4298 private function getPluralForm( $number ) {
4299 $pluralRules = $this->getCompiledPluralRules();
4300 $form = CLDRPluralRuleEvaluator
::evaluateCompiled( $number, $pluralRules );