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'
158 // For pretty timestamps
159 // Cutoff for specifying "weekday at XX:XX" format
160 protected $mWeekdayAtCutoff = 432000; // 5 days
166 static public $durationIntervals = array(
167 'millennia' => 31557600000,
168 'centuries' => 3155760000,
169 'decades' => 315576000,
170 'years' => 31557600, // 86400 * 365.25
179 * Get a cached language object for a given language code
180 * @param $code String
183 static function factory( $code ) {
184 if ( !isset( self
::$mLangObjCache[$code] ) ) {
185 if ( count( self
::$mLangObjCache ) > 10 ) {
186 // Don't keep a billion objects around, that's stupid.
187 self
::$mLangObjCache = array();
189 self
::$mLangObjCache[$code] = self
::newFromCode( $code );
191 return self
::$mLangObjCache[$code];
195 * Create a language object for a given language code
196 * @param $code String
197 * @throws MWException
200 protected static function newFromCode( $code ) {
201 // Protect against path traversal below
202 if ( !Language
::isValidCode( $code )
203 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
205 throw new MWException( "Invalid language code \"$code\"" );
208 if ( !Language
::isValidBuiltInCode( $code ) ) {
209 // It's not possible to customise this code with class files, so
210 // just return a Language object. This is to support uselang= hacks.
211 $lang = new Language
;
212 $lang->setCode( $code );
216 // Check if there is a language class for the code
217 $class = self
::classFromCode( $code );
218 self
::preloadLanguageClass( $class );
219 if ( MWInit
::classExists( $class ) ) {
224 // Keep trying the fallback list until we find an existing class
225 $fallbacks = Language
::getFallbacksFor( $code );
226 foreach ( $fallbacks as $fallbackCode ) {
227 if ( !Language
::isValidBuiltInCode( $fallbackCode ) ) {
228 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
231 $class = self
::classFromCode( $fallbackCode );
232 self
::preloadLanguageClass( $class );
233 if ( MWInit
::classExists( $class ) ) {
234 $lang = Language
::newFromCode( $fallbackCode );
235 $lang->setCode( $code );
240 throw new MWException( "Invalid fallback sequence for language '$code'" );
244 * Returns true if a language code string is of a valid form, whether or
245 * not it exists. This includes codes which are used solely for
246 * customisation via the MediaWiki namespace.
248 * @param $code string
252 public static function isValidCode( $code ) {
254 // People think language codes are html safe, so enforce it.
255 // Ideally we should only allow a-zA-Z0-9-
256 // but, .+ and other chars are often used for {{int:}} hacks
257 // see bugs 37564, 37587, 36938
258 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
259 && !preg_match( Title
::getTitleInvalidRegex(), $code );
263 * Returns true if a language code is of a valid form for the purposes of
264 * internal customisation of MediaWiki, via Messages*.php.
266 * @param $code string
268 * @throws MWException
272 public static function isValidBuiltInCode( $code ) {
274 if ( !is_string( $code ) ) {
275 $type = gettype( $code );
276 if ( $type === 'object' ) {
277 $addmsg = " of class " . get_class( $code );
281 throw new MWException( __METHOD__
. " must be passed a string, $type given$addmsg" );
284 return preg_match( '/^[a-z0-9-]+$/i', $code );
289 * @return String Name of the language class
291 public static function classFromCode( $code ) {
292 if ( $code == 'en' ) {
295 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
300 * Includes language class files
302 * @param $class string Name of the language class
304 public static function preloadLanguageClass( $class ) {
307 if ( $class === 'Language' ) {
311 if ( !defined( 'MW_COMPILED' ) ) {
312 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
313 include_once( "$IP/languages/classes/$class.php" );
319 * Get the LocalisationCache instance
321 * @return LocalisationCache
323 public static function getLocalisationCache() {
324 if ( is_null( self
::$dataCache ) ) {
325 global $wgLocalisationCacheConf;
326 $class = $wgLocalisationCacheConf['class'];
327 self
::$dataCache = new $class( $wgLocalisationCacheConf );
329 return self
::$dataCache;
332 function __construct() {
333 $this->mConverter
= new FakeConverter( $this );
334 // Set the code to the name of the descendant
335 if ( get_class( $this ) == 'Language' ) {
338 $this->mCode
= str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
340 self
::getLocalisationCache();
344 * Reduce memory usage
346 function __destruct() {
347 foreach ( $this as $name => $value ) {
348 unset( $this->$name );
353 * Hook which will be called if this is the content language.
354 * Descendants can use this to register hook functions or modify globals
356 function initContLang() { }
359 * Same as getFallbacksFor for current language.
361 * @deprecated in 1.19
363 function getFallbackLanguageCode() {
364 wfDeprecated( __METHOD__
, '1.19' );
365 return self
::getFallbackFor( $this->mCode
);
372 function getFallbackLanguages() {
373 return self
::getFallbacksFor( $this->mCode
);
377 * Exports $wgBookstoreListEn
380 function getBookstoreList() {
381 return self
::$dataCache->getItem( $this->mCode
, 'bookstoreList' );
387 public function getNamespaces() {
388 if ( is_null( $this->namespaceNames
) ) {
389 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
391 $this->namespaceNames
= self
::$dataCache->getItem( $this->mCode
, 'namespaceNames' );
392 $validNamespaces = MWNamespace
::getCanonicalNamespaces();
394 $this->namespaceNames
= $wgExtraNamespaces +
$this->namespaceNames +
$validNamespaces;
396 $this->namespaceNames
[NS_PROJECT
] = $wgMetaNamespace;
397 if ( $wgMetaNamespaceTalk ) {
398 $this->namespaceNames
[NS_PROJECT_TALK
] = $wgMetaNamespaceTalk;
400 $talk = $this->namespaceNames
[NS_PROJECT_TALK
];
401 $this->namespaceNames
[NS_PROJECT_TALK
] =
402 $this->fixVariableInNamespace( $talk );
405 # Sometimes a language will be localised but not actually exist on this wiki.
406 foreach ( $this->namespaceNames
as $key => $text ) {
407 if ( !isset( $validNamespaces[$key] ) ) {
408 unset( $this->namespaceNames
[$key] );
412 # The above mixing may leave namespaces out of canonical order.
413 # Re-order by namespace ID number...
414 ksort( $this->namespaceNames
);
416 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames
) );
418 return $this->namespaceNames
;
422 * Arbitrarily set all of the namespace names at once. Mainly used for testing
423 * @param $namespaces Array of namespaces (id => name)
425 public function setNamespaces( array $namespaces ) {
426 $this->namespaceNames
= $namespaces;
427 $this->mNamespaceIds
= null;
431 * Resets all of the namespace caches. Mainly used for testing
433 public function resetNamespaces( ) {
434 $this->namespaceNames
= null;
435 $this->mNamespaceIds
= null;
436 $this->namespaceAliases
= null;
440 * A convenience function that returns the same thing as
441 * getNamespaces() except with the array values changed to ' '
442 * where it found '_', useful for producing output to be displayed
443 * e.g. in <select> forms.
447 function getFormattedNamespaces() {
448 $ns = $this->getNamespaces();
449 foreach ( $ns as $k => $v ) {
450 $ns[$k] = strtr( $v, '_', ' ' );
456 * Get a namespace value by key
458 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
459 * echo $mw_ns; // prints 'MediaWiki'
462 * @param $index Int: the array key of the namespace to return
463 * @return mixed, string if the namespace value exists, otherwise false
465 function getNsText( $index ) {
466 $ns = $this->getNamespaces();
467 return isset( $ns[$index] ) ?
$ns[$index] : false;
471 * A convenience function that returns the same thing as
472 * getNsText() except with '_' changed to ' ', useful for
475 * @param $index string
479 function getFormattedNsText( $index ) {
480 $ns = $this->getNsText( $index );
481 return strtr( $ns, '_', ' ' );
485 * Returns gender-dependent namespace alias if available.
486 * @param $index Int: namespace index
487 * @param $gender String: gender key (male, female... )
491 function getGenderNsText( $index, $gender ) {
492 global $wgExtraGenderNamespaces;
494 $ns = $wgExtraGenderNamespaces + self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
495 return isset( $ns[$index][$gender] ) ?
$ns[$index][$gender] : $this->getNsText( $index );
499 * Whether this language makes distinguishes genders for example in
504 function needsGenderDistinction() {
505 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
506 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
507 // $wgExtraGenderNamespaces overrides everything
509 } elseif ( isset( $wgExtraNamespaces[NS_USER
] ) && isset( $wgExtraNamespaces[NS_USER_TALK
] ) ) {
510 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
511 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
514 // Check what is in i18n files
515 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
516 return count( $aliases ) > 0;
521 * Get a namespace key by value, case insensitive.
522 * Only matches namespace names for the current language, not the
523 * canonical ones defined in Namespace.php.
525 * @param $text String
526 * @return mixed An integer if $text is a valid value otherwise false
528 function getLocalNsIndex( $text ) {
529 $lctext = $this->lc( $text );
530 $ids = $this->getNamespaceIds();
531 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
537 function getNamespaceAliases() {
538 if ( is_null( $this->namespaceAliases
) ) {
539 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceAliases' );
543 foreach ( $aliases as $name => $index ) {
544 if ( $index === NS_PROJECT_TALK
) {
545 unset( $aliases[$name] );
546 $name = $this->fixVariableInNamespace( $name );
547 $aliases[$name] = $index;
552 global $wgExtraGenderNamespaces;
553 $genders = $wgExtraGenderNamespaces +
(array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
554 foreach ( $genders as $index => $forms ) {
555 foreach ( $forms as $alias ) {
556 $aliases[$alias] = $index;
560 $this->namespaceAliases
= $aliases;
562 return $this->namespaceAliases
;
568 function getNamespaceIds() {
569 if ( is_null( $this->mNamespaceIds
) ) {
570 global $wgNamespaceAliases;
571 # Put namespace names and aliases into a hashtable.
572 # If this is too slow, then we should arrange it so that it is done
573 # before caching. The catch is that at pre-cache time, the above
574 # class-specific fixup hasn't been done.
575 $this->mNamespaceIds
= array();
576 foreach ( $this->getNamespaces() as $index => $name ) {
577 $this->mNamespaceIds
[$this->lc( $name )] = $index;
579 foreach ( $this->getNamespaceAliases() as $name => $index ) {
580 $this->mNamespaceIds
[$this->lc( $name )] = $index;
582 if ( $wgNamespaceAliases ) {
583 foreach ( $wgNamespaceAliases as $name => $index ) {
584 $this->mNamespaceIds
[$this->lc( $name )] = $index;
588 return $this->mNamespaceIds
;
592 * Get a namespace key by value, case insensitive. Canonical namespace
593 * names override custom ones defined for the current language.
595 * @param $text String
596 * @return mixed An integer if $text is a valid value otherwise false
598 function getNsIndex( $text ) {
599 $lctext = $this->lc( $text );
600 $ns = MWNamespace
::getCanonicalIndex( $lctext );
601 if ( $ns !== null ) {
604 $ids = $this->getNamespaceIds();
605 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
609 * short names for language variants used for language conversion links.
611 * @param $code String
612 * @param $usemsg bool Use the "variantname-xyz" message if it exists
615 function getVariantname( $code, $usemsg = true ) {
616 $msg = "variantname-$code";
617 if ( $usemsg && wfMessage( $msg )->exists() ) {
618 return $this->getMessageFromDB( $msg );
620 $name = self
::fetchLanguageName( $code );
622 return $name; # if it's defined as a language name, show that
624 # otherwise, output the language code
630 * @param $name string
633 function specialPage( $name ) {
634 $aliases = $this->getSpecialPageAliases();
635 if ( isset( $aliases[$name][0] ) ) {
636 $name = $aliases[$name][0];
638 return $this->getNsText( NS_SPECIAL
) . ':' . $name;
644 function getQuickbarSettings() {
646 $this->getMessage( 'qbsettings-none' ),
647 $this->getMessage( 'qbsettings-fixedleft' ),
648 $this->getMessage( 'qbsettings-fixedright' ),
649 $this->getMessage( 'qbsettings-floatingleft' ),
650 $this->getMessage( 'qbsettings-floatingright' ),
651 $this->getMessage( 'qbsettings-directionality' )
658 function getDatePreferences() {
659 return self
::$dataCache->getItem( $this->mCode
, 'datePreferences' );
665 function getDateFormats() {
666 return self
::$dataCache->getItem( $this->mCode
, 'dateFormats' );
670 * @return array|string
672 function getDefaultDateFormat() {
673 $df = self
::$dataCache->getItem( $this->mCode
, 'defaultDateFormat' );
674 if ( $df === 'dmy or mdy' ) {
675 global $wgAmericanDates;
676 return $wgAmericanDates ?
'mdy' : 'dmy';
685 function getDatePreferenceMigrationMap() {
686 return self
::$dataCache->getItem( $this->mCode
, 'datePreferenceMigrationMap' );
693 function getImageFile( $image ) {
694 return self
::$dataCache->getSubitem( $this->mCode
, 'imageFiles', $image );
700 function getExtraUserToggles() {
701 return (array)self
::$dataCache->getItem( $this->mCode
, 'extraUserToggles' );
708 function getUserToggle( $tog ) {
709 return $this->getMessageFromDB( "tog-$tog" );
713 * Get native language names, indexed by code.
714 * Only those defined in MediaWiki, no other data like CLDR.
715 * If $customisedOnly is true, only returns codes with a messages file
717 * @param $customisedOnly bool
720 * @deprecated in 1.20, use fetchLanguageNames()
722 public static function getLanguageNames( $customisedOnly = false ) {
723 return self
::fetchLanguageNames( null, $customisedOnly ?
'mwfile' : 'mw' );
727 * Get translated language names. This is done on best effort and
728 * by default this is exactly the same as Language::getLanguageNames.
729 * The CLDR extension provides translated names.
730 * @param $code String Language code.
731 * @return Array language code => language name
733 * @deprecated in 1.20, use fetchLanguageNames()
735 public static function getTranslatedLanguageNames( $code ) {
736 return self
::fetchLanguageNames( $code, 'all' );
740 * Get an array of language names, indexed by code.
741 * @param $inLanguage null|string: Code of language in which to return the names
742 * Use null for autonyms (native names)
743 * @param $include string:
744 * 'all' all available languages
745 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
746 * 'mwfile' only if the language is in 'mw' *and* has a message file
747 * @return array: language code => language name
750 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
751 global $wgExtraLanguageNames;
752 static $coreLanguageNames;
754 if ( $coreLanguageNames === null ) {
755 include( MWInit
::compiledPath( 'languages/Names.php' ) );
761 # TODO: also include when $inLanguage is null, when this code is more efficient
762 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
765 $mwNames = $wgExtraLanguageNames +
$coreLanguageNames;
766 foreach ( $mwNames as $mwCode => $mwName ) {
767 # - Prefer own MediaWiki native name when not using the hook
768 # - For other names just add if not added through the hook
769 if ( $mwCode === $inLanguage ||
!isset( $names[$mwCode] ) ) {
770 $names[$mwCode] = $mwName;
774 if ( $include === 'all' ) {
779 $coreCodes = array_keys( $mwNames );
780 foreach ( $coreCodes as $coreCode ) {
781 $returnMw[$coreCode] = $names[$coreCode];
784 if ( $include === 'mwfile' ) {
785 $namesMwFile = array();
786 # We do this using a foreach over the codes instead of a directory
787 # loop so that messages files in extensions will work correctly.
788 foreach ( $returnMw as $code => $value ) {
789 if ( is_readable( self
::getMessagesFileName( $code ) ) ) {
790 $namesMwFile[$code] = $names[$code];
795 # 'mw' option; default if it's not one of the other two options (all/mwfile)
800 * @param $code string: The code of the language for which to get the name
801 * @param $inLanguage null|string: Code of language in which to return the name (null for autonyms)
802 * @param $include string: 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
803 * @return string: Language name or empty
806 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
807 $array = self
::fetchLanguageNames( $inLanguage, $include );
808 return !array_key_exists( $code, $array ) ?
'' : $array[$code];
812 * Get a message from the MediaWiki namespace.
814 * @param $msg String: message name
817 function getMessageFromDB( $msg ) {
818 return wfMessage( $msg )->inLanguage( $this )->text();
822 * Get the native language name of $code.
823 * Only if defined in MediaWiki, no other data like CLDR.
824 * @param $code string
826 * @deprecated in 1.20, use fetchLanguageName()
828 function getLanguageName( $code ) {
829 return self
::fetchLanguageName( $code );
836 function getMonthName( $key ) {
837 return $this->getMessageFromDB( self
::$mMonthMsgs[$key - 1] );
843 function getMonthNamesArray() {
844 $monthNames = array( '' );
845 for ( $i = 1; $i < 13; $i++
) {
846 $monthNames[] = $this->getMonthName( $i );
855 function getMonthNameGen( $key ) {
856 return $this->getMessageFromDB( self
::$mMonthGenMsgs[$key - 1] );
863 function getMonthAbbreviation( $key ) {
864 return $this->getMessageFromDB( self
::$mMonthAbbrevMsgs[$key - 1] );
870 function getMonthAbbreviationsArray() {
871 $monthNames = array( '' );
872 for ( $i = 1; $i < 13; $i++
) {
873 $monthNames[] = $this->getMonthAbbreviation( $i );
882 function getWeekdayName( $key ) {
883 return $this->getMessageFromDB( self
::$mWeekdayMsgs[$key - 1] );
890 function getWeekdayAbbreviation( $key ) {
891 return $this->getMessageFromDB( self
::$mWeekdayAbbrevMsgs[$key - 1] );
898 function getIranianCalendarMonthName( $key ) {
899 return $this->getMessageFromDB( self
::$mIranianCalendarMonthMsgs[$key - 1] );
906 function getHebrewCalendarMonthName( $key ) {
907 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthMsgs[$key - 1] );
914 function getHebrewCalendarMonthNameGen( $key ) {
915 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthGenMsgs[$key - 1] );
922 function getHijriCalendarMonthName( $key ) {
923 return $this->getMessageFromDB( self
::$mHijriCalendarMonthMsgs[$key - 1] );
927 * This is a workalike of PHP's date() function, but with better
928 * internationalisation, a reduced set of format characters, and a better
931 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
932 * PHP manual for definitions. There are a number of extensions, which
935 * xn Do not translate digits of the next numeric format character
936 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
937 * xr Use roman numerals for the next numeric format character
938 * xh Use hebrew numerals for the next numeric format character
940 * xg Genitive month name
942 * xij j (day number) in Iranian calendar
943 * xiF F (month name) in Iranian calendar
944 * xin n (month number) in Iranian calendar
945 * xiy y (two digit year) in Iranian calendar
946 * xiY Y (full year) in Iranian calendar
948 * xjj j (day number) in Hebrew calendar
949 * xjF F (month name) in Hebrew calendar
950 * xjt t (days in month) in Hebrew calendar
951 * xjx xg (genitive month name) in Hebrew calendar
952 * xjn n (month number) in Hebrew calendar
953 * xjY Y (full year) in Hebrew calendar
955 * xmj j (day number) in Hijri calendar
956 * xmF F (month name) in Hijri calendar
957 * xmn n (month number) in Hijri calendar
958 * xmY Y (full year) in Hijri calendar
960 * xkY Y (full year) in Thai solar calendar. Months and days are
961 * identical to the Gregorian calendar
962 * xoY Y (full year) in Minguo calendar or Juche year.
963 * Months and days are identical to the
965 * xtY Y (full year) in Japanese nengo. Months and days are
966 * identical to the Gregorian calendar
968 * Characters enclosed in double quotes will be considered literal (with
969 * the quotes themselves removed). Unmatched quotes will be considered
970 * literal quotes. Example:
972 * "The month is" F => The month is January
975 * Backslash escaping is also supported.
977 * Input timestamp is assumed to be pre-normalized to the desired local
980 * @param $format String
981 * @param $ts String: 14-character timestamp
984 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
988 function sprintfDate( $format, $ts ) {
1001 for ( $p = 0; $p < strlen( $format ); $p++
) {
1003 $code = $format[$p];
1004 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
1005 $code .= $format[++
$p];
1008 if ( ( $code === 'xi' ||
$code == 'xj' ||
$code == 'xk' ||
$code == 'xm' ||
$code == 'xo' ||
$code == 'xt' ) && $p < strlen( $format ) - 1 ) {
1009 $code .= $format[++
$p];
1020 $rawToggle = !$rawToggle;
1029 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1032 if ( !$hebrew ) $hebrew = self
::tsToHebrew( $ts );
1033 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1036 $num = substr( $ts, 6, 2 );
1039 if ( !$unix ) $unix = wfTimestamp( TS_UNIX
, $ts );
1040 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) +
1 );
1043 $num = intval( substr( $ts, 6, 2 ) );
1047 $iranian = self
::tsToIranian( $ts );
1053 $hijri = self
::tsToHijri( $ts );
1059 $hebrew = self
::tsToHebrew( $ts );
1065 $unix = wfTimestamp( TS_UNIX
, $ts );
1067 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) +
1 );
1071 $unix = wfTimestamp( TS_UNIX
, $ts );
1073 $w = gmdate( 'w', $unix );
1078 $unix = wfTimestamp( TS_UNIX
, $ts );
1080 $num = gmdate( 'w', $unix );
1084 $unix = wfTimestamp( TS_UNIX
, $ts );
1086 $num = gmdate( 'z', $unix );
1090 $unix = wfTimestamp( TS_UNIX
, $ts );
1092 $num = gmdate( 'W', $unix );
1095 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1099 $iranian = self
::tsToIranian( $ts );
1101 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1105 $hijri = self
::tsToHijri( $ts );
1107 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1111 $hebrew = self
::tsToHebrew( $ts );
1113 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1116 $num = substr( $ts, 4, 2 );
1119 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1122 $num = intval( substr( $ts, 4, 2 ) );
1126 $iranian = self
::tsToIranian( $ts );
1132 $hijri = self
::tsToHijri ( $ts );
1138 $hebrew = self
::tsToHebrew( $ts );
1144 $unix = wfTimestamp( TS_UNIX
, $ts );
1146 $num = gmdate( 't', $unix );
1150 $hebrew = self
::tsToHebrew( $ts );
1156 $unix = wfTimestamp( TS_UNIX
, $ts );
1158 $num = gmdate( 'L', $unix );
1162 $unix = wfTimestamp( TS_UNIX
, $ts );
1164 $num = gmdate( 'o', $unix );
1167 $num = substr( $ts, 0, 4 );
1171 $iranian = self
::tsToIranian( $ts );
1177 $hijri = self
::tsToHijri( $ts );
1183 $hebrew = self
::tsToHebrew( $ts );
1189 $thai = self
::tsToYear( $ts, 'thai' );
1195 $minguo = self
::tsToYear( $ts, 'minguo' );
1201 $tenno = self
::tsToYear( $ts, 'tenno' );
1206 $num = substr( $ts, 2, 2 );
1210 $iranian = self
::tsToIranian( $ts );
1212 $num = substr( $iranian[0], -2 );
1215 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'am' : 'pm';
1218 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'AM' : 'PM';
1221 $h = substr( $ts, 8, 2 );
1222 $num = $h %
12 ?
$h %
12 : 12;
1225 $num = intval( substr( $ts, 8, 2 ) );
1228 $h = substr( $ts, 8, 2 );
1229 $num = sprintf( '%02d', $h %
12 ?
$h %
12 : 12 );
1232 $num = substr( $ts, 8, 2 );
1235 $num = substr( $ts, 10, 2 );
1238 $num = substr( $ts, 12, 2 );
1242 $unix = wfTimestamp( TS_UNIX
, $ts );
1244 $s .= gmdate( 'c', $unix );
1248 $unix = wfTimestamp( TS_UNIX
, $ts );
1250 $s .= gmdate( 'r', $unix );
1254 $unix = wfTimestamp( TS_UNIX
, $ts );
1259 # Backslash escaping
1260 if ( $p < strlen( $format ) - 1 ) {
1261 $s .= $format[++
$p];
1268 if ( $p < strlen( $format ) - 1 ) {
1269 $endQuote = strpos( $format, '"', $p +
1 );
1270 if ( $endQuote === false ) {
1271 # No terminating quote, assume literal "
1274 $s .= substr( $format, $p +
1, $endQuote - $p - 1 );
1278 # Quote at end of string, assume literal "
1285 if ( $num !== false ) {
1286 if ( $rawToggle ||
$raw ) {
1289 } elseif ( $roman ) {
1290 $s .= Language
::romanNumeral( $num );
1292 } elseif ( $hebrewNum ) {
1293 $s .= self
::hebrewNumeral( $num );
1296 $s .= $this->formatNum( $num, true );
1303 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1304 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1307 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1308 * Gregorian dates to Iranian dates. Originally written in C, it
1309 * is released under the terms of GNU Lesser General Public
1310 * License. Conversion to PHP was performed by Niklas Laxström.
1312 * Link: http://www.farsiweb.info/jalali/jalali.c
1318 private static function tsToIranian( $ts ) {
1319 $gy = substr( $ts, 0, 4 ) -1600;
1320 $gm = substr( $ts, 4, 2 ) -1;
1321 $gd = substr( $ts, 6, 2 ) -1;
1323 # Days passed from the beginning (including leap years)
1325 +
floor( ( $gy +
3 ) / 4 )
1326 - floor( ( $gy +
99 ) / 100 )
1327 +
floor( ( $gy +
399 ) / 400 );
1329 // Add days of the past months of this year
1330 for ( $i = 0; $i < $gm; $i++
) {
1331 $gDayNo +
= self
::$GREG_DAYS[$i];
1335 if ( $gm > 1 && ( ( $gy %
4 === 0 && $gy %
100 !== 0 ||
( $gy %
400 == 0 ) ) ) ) {
1339 // Days passed in current month
1340 $gDayNo +
= (int)$gd;
1342 $jDayNo = $gDayNo - 79;
1344 $jNp = floor( $jDayNo / 12053 );
1347 $jy = 979 +
33 * $jNp +
4 * floor( $jDayNo / 1461 );
1350 if ( $jDayNo >= 366 ) {
1351 $jy +
= floor( ( $jDayNo - 1 ) / 365 );
1352 $jDayNo = floor( ( $jDayNo - 1 ) %
365 );
1355 for ( $i = 0; $i < 11 && $jDayNo >= self
::$IRANIAN_DAYS[$i]; $i++
) {
1356 $jDayNo -= self
::$IRANIAN_DAYS[$i];
1362 return array( $jy, $jm, $jd );
1366 * Converting Gregorian dates to Hijri dates.
1368 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1370 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1376 private static function tsToHijri( $ts ) {
1377 $year = substr( $ts, 0, 4 );
1378 $month = substr( $ts, 4, 2 );
1379 $day = substr( $ts, 6, 2 );
1387 ( $zy > 1582 ) ||
( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1388 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1391 $zjd = (int)( ( 1461 * ( $zy +
4800 +
(int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1392 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1393 (int)( ( 3 * (int)( ( ( $zy +
4900 +
(int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1396 $zjd = 367 * $zy - (int)( ( 7 * ( $zy +
5001 +
(int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1397 (int)( ( 275 * $zm ) / 9 ) +
$zd +
1729777;
1400 $zl = $zjd -1948440 +
10632;
1401 $zn = (int)( ( $zl - 1 ) / 10631 );
1402 $zl = $zl - 10631 * $zn +
354;
1403 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1404 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) +
29;
1405 $zm = (int)( ( 24 * $zl ) / 709 );
1406 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1407 $zy = 30 * $zn +
$zj - 30;
1409 return array( $zy, $zm, $zd );
1413 * Converting Gregorian dates to Hebrew dates.
1415 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1416 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1417 * to translate the relevant functions into PHP and release them under
1420 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1421 * and Adar II is 14. In a non-leap year, Adar is 6.
1427 private static function tsToHebrew( $ts ) {
1429 $year = substr( $ts, 0, 4 );
1430 $month = substr( $ts, 4, 2 );
1431 $day = substr( $ts, 6, 2 );
1433 # Calculate Hebrew year
1434 $hebrewYear = $year +
3760;
1436 # Month number when September = 1, August = 12
1438 if ( $month > 12 ) {
1445 # Calculate day of year from 1 September
1447 for ( $i = 1; $i < $month; $i++
) {
1451 # Check if the year is leap
1452 if ( $year %
400 == 0 ||
( $year %
4 == 0 && $year %
100 > 0 ) ) {
1455 } elseif ( $i == 8 ||
$i == 10 ||
$i == 1 ||
$i == 3 ) {
1462 # Calculate the start of the Hebrew year
1463 $start = self
::hebrewYearStart( $hebrewYear );
1465 # Calculate next year's start
1466 if ( $dayOfYear <= $start ) {
1467 # Day is before the start of the year - it is the previous year
1469 $nextStart = $start;
1473 # Add days since previous year's 1 September
1475 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1479 # Start of the new (previous) year
1480 $start = self
::hebrewYearStart( $hebrewYear );
1483 $nextStart = self
::hebrewYearStart( $hebrewYear +
1 );
1486 # Calculate Hebrew day of year
1487 $hebrewDayOfYear = $dayOfYear - $start;
1489 # Difference between year's days
1490 $diff = $nextStart - $start;
1491 # Add 12 (or 13 for leap years) days to ignore the difference between
1492 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1493 # difference is only about the year type
1494 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1500 # Check the year pattern, and is leap year
1501 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1502 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1503 # and non-leap years
1504 $yearPattern = $diff %
30;
1505 # Check if leap year
1506 $isLeap = $diff >= 30;
1508 # Calculate day in the month from number of day in the Hebrew year
1509 # Don't check Adar - if the day is not in Adar, we will stop before;
1510 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1511 $hebrewDay = $hebrewDayOfYear;
1514 while ( $hebrewMonth <= 12 ) {
1515 # Calculate days in this month
1516 if ( $isLeap && $hebrewMonth == 6 ) {
1517 # Adar in a leap year
1519 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1521 if ( $hebrewDay <= $days ) {
1525 # Subtract the days of Adar I
1526 $hebrewDay -= $days;
1529 if ( $hebrewDay <= $days ) {
1535 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1536 # Cheshvan in a complete year (otherwise as the rule below)
1538 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1539 # Kislev in an incomplete year (otherwise as the rule below)
1542 # Odd months have 30 days, even have 29
1543 $days = 30 - ( $hebrewMonth - 1 ) %
2;
1545 if ( $hebrewDay <= $days ) {
1546 # In the current month
1549 # Subtract the days of the current month
1550 $hebrewDay -= $days;
1551 # Try in the next month
1556 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1560 * This calculates the Hebrew year start, as days since 1 September.
1561 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1562 * Used for Hebrew date.
1568 private static function hebrewYearStart( $year ) {
1569 $a = intval( ( 12 * ( $year - 1 ) +
17 ) %
19 );
1570 $b = intval( ( $year - 1 ) %
4 );
1571 $m = 32.044093161144 +
1.5542417966212 * $a +
$b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1575 $Mar = intval( $m );
1581 $c = intval( ( $Mar +
3 * ( $year - 1 ) +
5 * $b +
5 ) %
7 );
1582 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1584 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1586 } elseif ( $c == 2 ||
$c == 4 ||
$c == 6 ) {
1590 $Mar +
= intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1595 * Algorithm to convert Gregorian dates to Thai solar dates,
1596 * Minguo dates or Minguo dates.
1598 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1599 * http://en.wikipedia.org/wiki/Minguo_calendar
1600 * http://en.wikipedia.org/wiki/Japanese_era_name
1602 * @param $ts String: 14-character timestamp
1603 * @param $cName String: calender name
1604 * @return Array: converted year, month, day
1606 private static function tsToYear( $ts, $cName ) {
1607 $gy = substr( $ts, 0, 4 );
1608 $gm = substr( $ts, 4, 2 );
1609 $gd = substr( $ts, 6, 2 );
1611 if ( !strcmp( $cName, 'thai' ) ) {
1613 # Add 543 years to the Gregorian calendar
1614 # Months and days are identical
1615 $gy_offset = $gy +
543;
1616 } elseif ( ( !strcmp( $cName, 'minguo' ) ) ||
!strcmp( $cName, 'juche' ) ) {
1618 # Deduct 1911 years from the Gregorian calendar
1619 # Months and days are identical
1620 $gy_offset = $gy - 1911;
1621 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1622 # Nengō dates up to Meiji period
1623 # Deduct years from the Gregorian calendar
1624 # depending on the nengo periods
1625 # Months and days are identical
1626 if ( ( $gy < 1912 ) ||
( ( $gy == 1912 ) && ( $gm < 7 ) ) ||
( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
1628 $gy_gannen = $gy - 1868 +
1;
1629 $gy_offset = $gy_gannen;
1630 if ( $gy_gannen == 1 ) {
1633 $gy_offset = '明治' . $gy_offset;
1635 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1636 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1637 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1638 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1639 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1643 $gy_gannen = $gy - 1912 +
1;
1644 $gy_offset = $gy_gannen;
1645 if ( $gy_gannen == 1 ) {
1648 $gy_offset = '大正' . $gy_offset;
1650 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1651 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1652 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1656 $gy_gannen = $gy - 1926 +
1;
1657 $gy_offset = $gy_gannen;
1658 if ( $gy_gannen == 1 ) {
1661 $gy_offset = '昭和' . $gy_offset;
1664 $gy_gannen = $gy - 1989 +
1;
1665 $gy_offset = $gy_gannen;
1666 if ( $gy_gannen == 1 ) {
1669 $gy_offset = '平成' . $gy_offset;
1675 return array( $gy_offset, $gm, $gd );
1679 * Roman number formatting up to 10000
1685 static function romanNumeral( $num ) {
1686 static $table = array(
1687 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1688 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1689 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1690 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM', 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
1693 $num = intval( $num );
1694 if ( $num > 10000 ||
$num <= 0 ) {
1699 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1700 if ( $num >= $pow10 ) {
1701 $s .= $table[$i][(int)floor( $num / $pow10 )];
1703 $num = $num %
$pow10;
1709 * Hebrew Gematria number formatting up to 9999
1715 static function hebrewNumeral( $num ) {
1716 static $table = array(
1717 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1718 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1719 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1720 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1723 $num = intval( $num );
1724 if ( $num > 9999 ||
$num <= 0 ) {
1729 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1730 if ( $num >= $pow10 ) {
1731 if ( $num == 15 ||
$num == 16 ) {
1732 $s .= $table[0][9] . $table[0][$num - 9];
1735 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1736 if ( $pow10 == 1000 ) {
1741 $num = $num %
$pow10;
1743 if ( strlen( $s ) == 2 ) {
1746 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1747 $str .= substr( $s, strlen( $s ) - 2, 2 );
1749 $start = substr( $str, 0, strlen( $str ) - 2 );
1750 $end = substr( $str, strlen( $str ) - 2 );
1753 $str = $start . 'ך';
1756 $str = $start . 'ם';
1759 $str = $start . 'ן';
1762 $str = $start . 'ף';
1765 $str = $start . 'ץ';
1772 * Used by date() and time() to adjust the time output.
1774 * @param $ts Int the time in date('YmdHis') format
1775 * @param $tz Mixed: adjust the time by this amount (default false, mean we
1776 * get user timecorrection setting)
1779 function userAdjust( $ts, $tz = false ) {
1780 global $wgUser, $wgLocalTZoffset;
1782 if ( $tz === false ) {
1783 $tz = $wgUser->getOption( 'timecorrection' );
1786 $data = explode( '|', $tz, 3 );
1788 if ( $data[0] == 'ZoneInfo' ) {
1789 wfSuppressWarnings();
1790 $userTZ = timezone_open( $data[2] );
1791 wfRestoreWarnings();
1792 if ( $userTZ !== false ) {
1793 $date = date_create( $ts, timezone_open( 'UTC' ) );
1794 date_timezone_set( $date, $userTZ );
1795 $date = date_format( $date, 'YmdHis' );
1798 # Unrecognized timezone, default to 'Offset' with the stored offset.
1799 $data[0] = 'Offset';
1803 if ( $data[0] == 'System' ||
$tz == '' ) {
1804 # Global offset in minutes.
1805 if ( isset( $wgLocalTZoffset ) ) {
1806 $minDiff = $wgLocalTZoffset;
1808 } elseif ( $data[0] == 'Offset' ) {
1809 $minDiff = intval( $data[1] );
1811 $data = explode( ':', $tz );
1812 if ( count( $data ) == 2 ) {
1813 $data[0] = intval( $data[0] );
1814 $data[1] = intval( $data[1] );
1815 $minDiff = abs( $data[0] ) * 60 +
$data[1];
1816 if ( $data[0] < 0 ) {
1817 $minDiff = -$minDiff;
1820 $minDiff = intval( $data[0] ) * 60;
1824 # No difference ? Return time unchanged
1825 if ( 0 == $minDiff ) {
1829 wfSuppressWarnings(); // E_STRICT system time bitching
1830 # Generate an adjusted date; take advantage of the fact that mktime
1831 # will normalize out-of-range values so we don't have to split $minDiff
1832 # into hours and minutes.
1834 (int)substr( $ts, 8, 2 ) ), # Hours
1835 (int)substr( $ts, 10, 2 ) +
$minDiff, # Minutes
1836 (int)substr( $ts, 12, 2 ), # Seconds
1837 (int)substr( $ts, 4, 2 ), # Month
1838 (int)substr( $ts, 6, 2 ), # Day
1839 (int)substr( $ts, 0, 4 ) ); # Year
1841 $date = date( 'YmdHis', $t );
1842 wfRestoreWarnings();
1848 * This is meant to be used by time(), date(), and timeanddate() to get
1849 * the date preference they're supposed to use, it should be used in
1853 * function timeanddate([...], $format = true) {
1854 * $datePreference = $this->dateFormat($format);
1859 * @param $usePrefs Mixed: if true, the user's preference is used
1860 * if false, the site/language default is used
1861 * if int/string, assumed to be a format.
1862 * if User object, assumed to be a User to get preference from
1865 function dateFormat( $usePrefs = true ) {
1868 if ( is_bool( $usePrefs ) ) {
1870 $datePreference = $wgUser->getDatePreference();
1872 $datePreference = (string)User
::getDefaultOption( 'date' );
1874 } elseif ( $usePrefs instanceof User
) {
1875 $datePreference = $usePrefs->getDatePreference();
1877 $datePreference = (string)$usePrefs;
1881 if ( $datePreference == '' ) {
1885 return $datePreference;
1889 * Get a format string for a given type and preference
1890 * @param $type string May be date, time or both
1891 * @param $pref string The format name as it appears in Messages*.php
1895 function getDateFormatString( $type, $pref ) {
1896 if ( !isset( $this->dateFormatStrings
[$type][$pref] ) ) {
1897 if ( $pref == 'default' ) {
1898 $pref = $this->getDefaultDateFormat();
1899 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1901 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1903 if ( $type === 'shortdate' && is_null( $df ) ) {
1904 $df = $this->getDateFormatString( 'date', $pref );
1907 if ( is_null( $df ) ) {
1908 $pref = $this->getDefaultDateFormat();
1909 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1913 $this->dateFormatStrings
[$type][$pref] = $df;
1915 return $this->dateFormatStrings
[$type][$pref];
1919 * @param $ts Mixed: the time format which needs to be turned into a
1920 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1921 * @param $adj Bool: whether to adjust the time output according to the
1922 * user configured offset ($timecorrection)
1923 * @param $format Mixed: true to use user's date format preference
1924 * @param $timecorrection String|bool the time offset as returned by
1925 * validateTimeZone() in Special:Preferences
1928 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1929 $ts = wfTimestamp( TS_MW
, $ts );
1931 $ts = $this->userAdjust( $ts, $timecorrection );
1933 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
1934 return $this->sprintfDate( $df, $ts );
1938 * @param $ts Mixed: the time format which needs to be turned into a
1939 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1940 * @param $adj Bool: whether to adjust the time output according to the
1941 * user configured offset ($timecorrection)
1942 * @param $format Mixed: true to use user's date format preference
1943 * @param $timecorrection String|bool the time offset as returned by
1944 * validateTimeZone() in Special:Preferences
1947 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1948 $ts = wfTimestamp( TS_MW
, $ts );
1950 $ts = $this->userAdjust( $ts, $timecorrection );
1952 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
1953 return $this->sprintfDate( $df, $ts );
1957 * @param $ts Mixed: the time format which needs to be turned into a
1958 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1959 * @param $adj Bool: whether to adjust the time output according to the
1960 * user configured offset ($timecorrection)
1961 * @param $format Mixed: what format to return, if it's false output the
1962 * default one (default true)
1963 * @param $timecorrection String|bool the time offset as returned by
1964 * validateTimeZone() in Special:Preferences
1967 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
1968 $ts = wfTimestamp( TS_MW
, $ts );
1970 $ts = $this->userAdjust( $ts, $timecorrection );
1972 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
1973 return $this->sprintfDate( $df, $ts );
1977 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
1981 * @param integer $seconds The amount of seconds.
1982 * @param array $chosenIntervals The intervals to enable.
1986 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
1987 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
1989 $segments = array();
1991 foreach ( $intervals as $intervalName => $intervalValue ) {
1992 $message = new Message( 'duration-' . $intervalName, array( $intervalValue ) );
1993 $segments[] = $message->inLanguage( $this )->escaped();
1996 return $this->listToText( $segments );
2000 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2001 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2005 * @param integer $seconds The amount of seconds.
2006 * @param array $chosenIntervals The intervals to enable.
2010 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2011 if ( empty( $chosenIntervals ) ) {
2012 $chosenIntervals = array( 'millennia', 'centuries', 'decades', 'years', 'days', 'hours', 'minutes', 'seconds' );
2015 $intervals = array_intersect_key( self
::$durationIntervals, array_flip( $chosenIntervals ) );
2016 $sortedNames = array_keys( $intervals );
2017 $smallestInterval = array_pop( $sortedNames );
2019 $segments = array();
2021 foreach ( $intervals as $name => $length ) {
2022 $value = floor( $seconds / $length );
2024 if ( $value > 0 ||
( $name == $smallestInterval && empty( $segments ) ) ) {
2025 $seconds -= $value * $length;
2026 $segments[$name] = $value;
2034 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2036 * @param $type String: can be 'date', 'time' or 'both'
2037 * @param $ts Mixed: the time format which needs to be turned into a
2038 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2039 * @param $user User object used to get preferences for timezone and format
2040 * @param $options Array, can contain the following keys:
2041 * - 'timecorrection': time correction, can have the following values:
2042 * - true: use user's preference
2043 * - false: don't use time correction
2044 * - integer: value of time correction in minutes
2045 * - 'format': format to use, can have the following values:
2046 * - true: use user's preference
2047 * - false: use default preference
2048 * - string: format to use
2052 private function internalUserTimeAndDate( $type, $ts, User
$user, array $options ) {
2053 $ts = wfTimestamp( TS_MW
, $ts );
2054 $options +
= array( 'timecorrection' => true, 'format' => true );
2055 if ( $options['timecorrection'] !== false ) {
2056 if ( $options['timecorrection'] === true ) {
2057 $offset = $user->getOption( 'timecorrection' );
2059 $offset = $options['timecorrection'];
2061 $ts = $this->userAdjust( $ts, $offset );
2063 if ( $options['format'] === true ) {
2064 $format = $user->getDatePreference();
2066 $format = $options['format'];
2068 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2069 return $this->sprintfDate( $df, $ts );
2073 * Get the formatted date for the given timestamp and formatted for
2076 * @param $ts Mixed: the time format which needs to be turned into a
2077 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2078 * @param $user User object used to get preferences for timezone and format
2079 * @param $options Array, can contain the following keys:
2080 * - 'timecorrection': time correction, can have the following values:
2081 * - true: use user's preference
2082 * - false: don't use time correction
2083 * - integer: value of time correction in minutes
2084 * - 'format': format to use, can have the following values:
2085 * - true: use user's preference
2086 * - false: use default preference
2087 * - string: format to use
2091 public function userDate( $ts, User
$user, array $options = array() ) {
2092 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2096 * Get the formatted time for the given timestamp and formatted for
2099 * @param $ts Mixed: the time format which needs to be turned into a
2100 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2101 * @param $user User object used to get preferences for timezone and format
2102 * @param $options Array, can contain the following keys:
2103 * - 'timecorrection': time correction, can have the following values:
2104 * - true: use user's preference
2105 * - false: don't use time correction
2106 * - integer: value of time correction in minutes
2107 * - 'format': format to use, can have the following values:
2108 * - true: use user's preference
2109 * - false: use default preference
2110 * - string: format to use
2114 public function userTime( $ts, User
$user, array $options = array() ) {
2115 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2119 * Get the formatted date and time for the given timestamp and formatted for
2122 * @param $ts Mixed: the time format which needs to be turned into a
2123 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2124 * @param $user User object used to get preferences for timezone and format
2125 * @param $options Array, can contain the following keys:
2126 * - 'timecorrection': time correction, can have the following values:
2127 * - true: use user's preference
2128 * - false: don't use time correction
2129 * - integer: value of time correction in minutes
2130 * - 'format': format to use, can have the following values:
2131 * - true: use user's preference
2132 * - false: use default preference
2133 * - string: format to use
2137 public function userTimeAndDate( $ts, User
$user, array $options = array() ) {
2138 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2142 * Formats a timestamp in a pretty, human-readable format.
2143 * Instead of "13:04, 16 July 2012", we have:
2147 * - Yesterday at 13:04
2148 * - Wednesday at 13:04
2150 * - July 16 2012 at 13:04
2152 * @todo Port to JavaScript
2154 * @param $ts Mixed: the time format which needs to be turned into a
2155 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2156 * @param $relativeTo Mixed: The timestamp to use as "now"
2157 * @param $user User: The user to format for (needed for timezone information)
2159 * @return string Formatted timestamp
2161 public function prettyTimestamp( $timestamp, $relativeTo = false, $user ) {
2162 // Parameter defaults
2163 if ( $relativeTo === false ) {
2164 $relativeTo = wfTimestampNow();
2167 // Normalise input to UNIX time
2168 $relativeTo = wfTimestamp( TS_UNIX
, $relativeTo );
2169 $timestamp = wfTimestamp( TS_UNIX
, $timestamp );
2170 $timeAgo = $relativeTo - $timestamp;
2172 $adjustedRelativeTo = $this->userAdjust( wfTimestamp( TS_MW
, $relativeTo ), $user->getOption('timecorrection') );
2173 $adjustedRelativeTo = wfTimestamp( TS_UNIX
, $adjustedRelativeTo );
2174 $relativeToYear = gmdate( 'Y', $adjustedRelativeTo );
2176 $adjustedTimestamp = $this->userAdjust( wfTimestamp( TS_MW
, $timestamp ), $user->getOption('timecorrection') );
2177 $adjustedTimestamp = wfTimestamp( TS_UNIX
, $adjustedTimestamp );
2178 $timestampYear = gmdate( 'Y', $adjustedTimestamp );
2180 if ( $timeAgo < 0 ) {
2181 throw new MWException( "Future timestamps not currently supported" );
2182 } elseif ( $timeAgo < 30 ) {
2183 return wfMessage( 'just-now' )
2184 ->inLanguage( $this )
2186 } elseif ( $timeAgo < 5400 ) {
2187 // Less than 90 minutes ago. Return number of hours, minutes or seconds ago.
2188 return $this->formatRelativeTime( $timeAgo );
2189 } elseif ( // Same day
2190 intval( $adjustedRelativeTo / (24*60*60) ) ===
2191 intval( $adjustedTimestamp / (24*60*60) )
2194 $time = $this->time( $adjustedTimestamp );
2195 return wfMessage( 'today-at' )
2196 ->inLanguage( $this )
2199 } elseif ( // Previous day
2200 intval( $adjustedRelativeTo / (24*60*60) ) ===
2201 ( intval( $adjustedTimestamp / (24*60*60) ) +
1 )
2203 // Yesterday at XX:XX
2204 $time = $this->time( $adjustedTimestamp );
2206 return wfMessage( 'yesterday-at' )
2207 ->inLanguage( $this )
2210 } elseif ( $timeAgo < ( $this->mWeekdayAtCutoff
) ) { // Less than 5 days ago
2212 return $this->formatPastWeekTimestamp( $adjustedTimestamp, $adjustedRelativeTo );
2213 } elseif ( $relativeToYear == $timestampYear ) {
2215 $df = $this->getDateFormatString( 'shortdate', $this->dateFormat( $user ) );
2216 $mwTimestamp = wfTimestamp( TS_MW
, $timestamp );
2217 return $this->sprintfDate( $df, $mwTimestamp );
2220 $mwTimestamp = wfTimestamp( TS_MW
, $timestamp );
2221 return $this->userDate( $mwTimestamp, $user );
2226 * For pretty timestamps: Formats the "X {hours,minutes,seconds} ago" message.
2228 * @param $timeAgo The number of seconds ago the event occurred
2229 * @return Formatted string
2231 protected function formatRelativeTime( $timeAgo ) {
2233 if ( $timeAgo < 60 ) {
2236 } elseif ( $timeAgo < 3600 ) {
2238 $count = intval( $timeAgo / 60 );
2241 $count = intval( $timeAgo / 3600 );
2244 return wfMessage( "{$unit}-ago" )->inLanguage( $this )->params( $count )->text();
2248 * For pretty timestamps: Formats the timestamp for events that occurred
2249 * "in the last few days".
2250 * (cutoff is configurable by the member variable $mWeekdayAtCutoff)
2252 * @param $eventTimestamp The timestamp of the event, adjusted to
2253 * the user's local timezone, in UNIX format (# of seconds since epoch)
2254 * @param $relativeTo The timestamp to format relative to, adjusted to
2255 * the user's local timezone, in UNIX format (# of seconds since epoch)
2256 * @return String: The date formatted in a human-friendly way.
2258 protected function formatPastWeekTimestamp( $adjustedTimestamp, $relativeTo ) {
2259 $day = date( 'w', $adjustedTimestamp );
2260 $weekday = self
::$mWeekdayMsgs[$day];
2261 $time = $this->time( $adjustedTimestamp );
2263 return wfMessage( "$weekday-at" )
2264 ->inLanguage( $this )
2270 * @param $key string
2271 * @return array|null
2273 function getMessage( $key ) {
2274 return self
::$dataCache->getSubitem( $this->mCode
, 'messages', $key );
2280 function getAllMessages() {
2281 return self
::$dataCache->getItem( $this->mCode
, 'messages' );
2290 function iconv( $in, $out, $string ) {
2291 # This is a wrapper for iconv in all languages except esperanto,
2292 # which does some nasty x-conversions beforehand
2294 # Even with //IGNORE iconv can whine about illegal characters in
2295 # *input* string. We just ignore those too.
2296 # REF: http://bugs.php.net/bug.php?id=37166
2297 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2298 wfSuppressWarnings();
2299 $text = iconv( $in, $out . '//IGNORE', $string );
2300 wfRestoreWarnings();
2304 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2307 * @param $matches array
2308 * @return mixed|string
2310 function ucwordbreaksCallbackAscii( $matches ) {
2311 return $this->ucfirst( $matches[1] );
2315 * @param $matches array
2318 function ucwordbreaksCallbackMB( $matches ) {
2319 return mb_strtoupper( $matches[0] );
2323 * @param $matches array
2326 function ucCallback( $matches ) {
2327 list( $wikiUpperChars ) = self
::getCaseMaps();
2328 return strtr( $matches[1], $wikiUpperChars );
2332 * @param $matches array
2335 function lcCallback( $matches ) {
2336 list( , $wikiLowerChars ) = self
::getCaseMaps();
2337 return strtr( $matches[1], $wikiLowerChars );
2341 * @param $matches array
2344 function ucwordsCallbackMB( $matches ) {
2345 return mb_strtoupper( $matches[0] );
2349 * @param $matches array
2352 function ucwordsCallbackWiki( $matches ) {
2353 list( $wikiUpperChars ) = self
::getCaseMaps();
2354 return strtr( $matches[0], $wikiUpperChars );
2358 * Make a string's first character uppercase
2360 * @param $str string
2364 function ucfirst( $str ) {
2366 if ( $o < 96 ) { // if already uppercase...
2368 } elseif ( $o < 128 ) {
2369 return ucfirst( $str ); // use PHP's ucfirst()
2371 // fall back to more complex logic in case of multibyte strings
2372 return $this->uc( $str, true );
2377 * Convert a string to uppercase
2379 * @param $str string
2380 * @param $first bool
2384 function uc( $str, $first = false ) {
2385 if ( function_exists( 'mb_strtoupper' ) ) {
2387 if ( $this->isMultibyte( $str ) ) {
2388 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2390 return ucfirst( $str );
2393 return $this->isMultibyte( $str ) ?
mb_strtoupper( $str ) : strtoupper( $str );
2396 if ( $this->isMultibyte( $str ) ) {
2397 $x = $first ?
'^' : '';
2398 return preg_replace_callback(
2399 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2400 array( $this, 'ucCallback' ),
2404 return $first ?
ucfirst( $str ) : strtoupper( $str );
2410 * @param $str string
2411 * @return mixed|string
2413 function lcfirst( $str ) {
2416 return strval( $str );
2417 } elseif ( $o >= 128 ) {
2418 return $this->lc( $str, true );
2419 } elseif ( $o > 96 ) {
2422 $str[0] = strtolower( $str[0] );
2428 * @param $str string
2429 * @param $first bool
2430 * @return mixed|string
2432 function lc( $str, $first = false ) {
2433 if ( function_exists( 'mb_strtolower' ) ) {
2435 if ( $this->isMultibyte( $str ) ) {
2436 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2438 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2441 return $this->isMultibyte( $str ) ?
mb_strtolower( $str ) : strtolower( $str );
2444 if ( $this->isMultibyte( $str ) ) {
2445 $x = $first ?
'^' : '';
2446 return preg_replace_callback(
2447 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2448 array( $this, 'lcCallback' ),
2452 return $first ?
strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2458 * @param $str string
2461 function isMultibyte( $str ) {
2462 return (bool)preg_match( '/[\x80-\xff]/', $str );
2466 * @param $str string
2467 * @return mixed|string
2469 function ucwords( $str ) {
2470 if ( $this->isMultibyte( $str ) ) {
2471 $str = $this->lc( $str );
2473 // regexp to find first letter in each word (i.e. after each space)
2474 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2476 // function to use to capitalize a single char
2477 if ( function_exists( 'mb_strtoupper' ) ) {
2478 return preg_replace_callback(
2480 array( $this, 'ucwordsCallbackMB' ),
2484 return preg_replace_callback(
2486 array( $this, 'ucwordsCallbackWiki' ),
2491 return ucwords( strtolower( $str ) );
2496 * capitalize words at word breaks
2498 * @param $str string
2501 function ucwordbreaks( $str ) {
2502 if ( $this->isMultibyte( $str ) ) {
2503 $str = $this->lc( $str );
2505 // since \b doesn't work for UTF-8, we explicitely define word break chars
2506 $breaks = "[ \-\(\)\}\{\.,\?!]";
2508 // find first letter after word break
2509 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2511 if ( function_exists( 'mb_strtoupper' ) ) {
2512 return preg_replace_callback(
2514 array( $this, 'ucwordbreaksCallbackMB' ),
2518 return preg_replace_callback(
2520 array( $this, 'ucwordsCallbackWiki' ),
2525 return preg_replace_callback(
2526 '/\b([\w\x80-\xff]+)\b/',
2527 array( $this, 'ucwordbreaksCallbackAscii' ),
2534 * Return a case-folded representation of $s
2536 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2537 * and $s2 are the same except for the case of their characters. It is not
2538 * necessary for the value returned to make sense when displayed.
2540 * Do *not* perform any other normalisation in this function. If a caller
2541 * uses this function when it should be using a more general normalisation
2542 * function, then fix the caller.
2548 function caseFold( $s ) {
2549 return $this->uc( $s );
2556 function checkTitleEncoding( $s ) {
2557 if ( is_array( $s ) ) {
2558 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
2560 # Check for non-UTF-8 URLs
2561 $ishigh = preg_match( '/[\x80-\xff]/', $s );
2566 if ( function_exists( 'mb_check_encoding' ) ) {
2567 $isutf8 = mb_check_encoding( $s, 'UTF-8' );
2569 $isutf8 = preg_match( '/^(?>[\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2570 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
2576 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2582 function fallback8bitEncoding() {
2583 return self
::$dataCache->getItem( $this->mCode
, 'fallback8bitEncoding' );
2587 * Most writing systems use whitespace to break up words.
2588 * Some languages such as Chinese don't conventionally do this,
2589 * which requires special handling when breaking up words for
2594 function hasWordBreaks() {
2599 * Some languages such as Chinese require word segmentation,
2600 * Specify such segmentation when overridden in derived class.
2602 * @param $string String
2605 function segmentByWord( $string ) {
2610 * Some languages have special punctuation need to be normalized.
2611 * Make such changes here.
2613 * @param $string String
2616 function normalizeForSearch( $string ) {
2617 return self
::convertDoubleWidth( $string );
2621 * convert double-width roman characters to single-width.
2622 * range: ff00-ff5f ~= 0020-007f
2624 * @param $string string
2628 protected static function convertDoubleWidth( $string ) {
2629 static $full = null;
2630 static $half = null;
2632 if ( $full === null ) {
2633 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2634 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2635 $full = str_split( $fullWidth, 3 );
2636 $half = str_split( $halfWidth );
2639 $string = str_replace( $full, $half, $string );
2644 * @param $string string
2645 * @param $pattern string
2648 protected static function insertSpace( $string, $pattern ) {
2649 $string = preg_replace( $pattern, " $1 ", $string );
2650 $string = preg_replace( '/ +/', ' ', $string );
2655 * @param $termsArray array
2658 function convertForSearchResult( $termsArray ) {
2659 # some languages, e.g. Chinese, need to do a conversion
2660 # in order for search results to be displayed correctly
2665 * Get the first character of a string.
2670 function firstChar( $s ) {
2673 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2674 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2679 if ( isset( $matches[1] ) ) {
2680 if ( strlen( $matches[1] ) != 3 ) {
2684 // Break down Hangul syllables to grab the first jamo
2685 $code = utf8ToCodepoint( $matches[1] );
2686 if ( $code < 0xac00 ||
0xd7a4 <= $code ) {
2688 } elseif ( $code < 0xb098 ) {
2689 return "\xe3\x84\xb1";
2690 } elseif ( $code < 0xb2e4 ) {
2691 return "\xe3\x84\xb4";
2692 } elseif ( $code < 0xb77c ) {
2693 return "\xe3\x84\xb7";
2694 } elseif ( $code < 0xb9c8 ) {
2695 return "\xe3\x84\xb9";
2696 } elseif ( $code < 0xbc14 ) {
2697 return "\xe3\x85\x81";
2698 } elseif ( $code < 0xc0ac ) {
2699 return "\xe3\x85\x82";
2700 } elseif ( $code < 0xc544 ) {
2701 return "\xe3\x85\x85";
2702 } elseif ( $code < 0xc790 ) {
2703 return "\xe3\x85\x87";
2704 } elseif ( $code < 0xcc28 ) {
2705 return "\xe3\x85\x88";
2706 } elseif ( $code < 0xce74 ) {
2707 return "\xe3\x85\x8a";
2708 } elseif ( $code < 0xd0c0 ) {
2709 return "\xe3\x85\x8b";
2710 } elseif ( $code < 0xd30c ) {
2711 return "\xe3\x85\x8c";
2712 } elseif ( $code < 0xd558 ) {
2713 return "\xe3\x85\x8d";
2715 return "\xe3\x85\x8e";
2722 function initEncoding() {
2723 # Some languages may have an alternate char encoding option
2724 # (Esperanto X-coding, Japanese furigana conversion, etc)
2725 # If this language is used as the primary content language,
2726 # an override to the defaults can be set here on startup.
2733 function recodeForEdit( $s ) {
2734 # For some languages we'll want to explicitly specify
2735 # which characters make it into the edit box raw
2736 # or are converted in some way or another.
2737 global $wgEditEncoding;
2738 if ( $wgEditEncoding == '' ||
$wgEditEncoding == 'UTF-8' ) {
2741 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2749 function recodeInput( $s ) {
2750 # Take the previous into account.
2751 global $wgEditEncoding;
2752 if ( $wgEditEncoding != '' ) {
2753 $enc = $wgEditEncoding;
2757 if ( $enc == 'UTF-8' ) {
2760 return $this->iconv( $enc, 'UTF-8', $s );
2765 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2766 * also cleans up certain backwards-compatible sequences, converting them
2767 * to the modern Unicode equivalent.
2769 * This is language-specific for performance reasons only.
2775 function normalize( $s ) {
2776 global $wgAllUnicodeFixes;
2777 $s = UtfNormal
::cleanUp( $s );
2778 if ( $wgAllUnicodeFixes ) {
2779 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2780 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2787 * Transform a string using serialized data stored in the given file (which
2788 * must be in the serialized subdirectory of $IP). The file contains pairs
2789 * mapping source characters to destination characters.
2791 * The data is cached in process memory. This will go faster if you have the
2792 * FastStringSearch extension.
2794 * @param $file string
2795 * @param $string string
2797 * @throws MWException
2800 function transformUsingPairFile( $file, $string ) {
2801 if ( !isset( $this->transformData
[$file] ) ) {
2802 $data = wfGetPrecompiledData( $file );
2803 if ( $data === false ) {
2804 throw new MWException( __METHOD__
. ": The transformation file $file is missing" );
2806 $this->transformData
[$file] = new ReplacementArray( $data );
2808 return $this->transformData
[$file]->replace( $string );
2812 * For right-to-left language support
2817 return self
::$dataCache->getItem( $this->mCode
, 'rtl' );
2821 * Return the correct HTML 'dir' attribute value for this language.
2825 return $this->isRTL() ?
'rtl' : 'ltr';
2829 * Return 'left' or 'right' as appropriate alignment for line-start
2830 * for this language's text direction.
2832 * Should be equivalent to CSS3 'start' text-align value....
2836 function alignStart() {
2837 return $this->isRTL() ?
'right' : 'left';
2841 * Return 'right' or 'left' as appropriate alignment for line-end
2842 * for this language's text direction.
2844 * Should be equivalent to CSS3 'end' text-align value....
2848 function alignEnd() {
2849 return $this->isRTL() ?
'left' : 'right';
2853 * A hidden direction mark (LRM or RLM), depending on the language direction.
2854 * Unlike getDirMark(), this function returns the character as an HTML entity.
2855 * This function should be used when the output is guaranteed to be HTML,
2856 * because it makes the output HTML source code more readable. When
2857 * the output is plain text or can be escaped, getDirMark() should be used.
2859 * @param $opposite Boolean Get the direction mark opposite to your language
2863 function getDirMarkEntity( $opposite = false ) {
2864 if ( $opposite ) { return $this->isRTL() ?
'‎' : '‏'; }
2865 return $this->isRTL() ?
'‏' : '‎';
2869 * A hidden direction mark (LRM or RLM), depending on the language direction.
2870 * This function produces them as invisible Unicode characters and
2871 * the output may be hard to read and debug, so it should only be used
2872 * when the output is plain text or can be escaped. When the output is
2873 * HTML, use getDirMarkEntity() instead.
2875 * @param $opposite Boolean Get the direction mark opposite to your language
2878 function getDirMark( $opposite = false ) {
2879 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
2880 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
2881 if ( $opposite ) { return $this->isRTL() ?
$lrm : $rlm; }
2882 return $this->isRTL() ?
$rlm : $lrm;
2888 function capitalizeAllNouns() {
2889 return self
::$dataCache->getItem( $this->mCode
, 'capitalizeAllNouns' );
2893 * An arrow, depending on the language direction.
2895 * @param $direction String: the direction of the arrow: forwards (default), backwards, left, right, up, down.
2898 function getArrow( $direction = 'forwards' ) {
2899 switch ( $direction ) {
2901 return $this->isRTL() ?
'←' : '→';
2903 return $this->isRTL() ?
'→' : '←';
2916 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
2920 function linkPrefixExtension() {
2921 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixExtension' );
2927 function getMagicWords() {
2928 return self
::$dataCache->getItem( $this->mCode
, 'magicWords' );
2931 protected function doMagicHook() {
2932 if ( $this->mMagicHookDone
) {
2935 $this->mMagicHookDone
= true;
2936 wfProfileIn( 'LanguageGetMagic' );
2937 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions
, $this->getCode() ) );
2938 wfProfileOut( 'LanguageGetMagic' );
2942 * Fill a MagicWord object with data from here
2946 function getMagic( $mw ) {
2947 $this->doMagicHook();
2949 if ( isset( $this->mMagicExtensions
[$mw->mId
] ) ) {
2950 $rawEntry = $this->mMagicExtensions
[$mw->mId
];
2952 $magicWords = $this->getMagicWords();
2953 if ( isset( $magicWords[$mw->mId
] ) ) {
2954 $rawEntry = $magicWords[$mw->mId
];
2960 if ( !is_array( $rawEntry ) ) {
2961 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
2963 $mw->mCaseSensitive
= $rawEntry[0];
2964 $mw->mSynonyms
= array_slice( $rawEntry, 1 );
2969 * Add magic words to the extension array
2971 * @param $newWords array
2973 function addMagicWordsByLang( $newWords ) {
2974 $fallbackChain = $this->getFallbackLanguages();
2975 $fallbackChain = array_reverse( $fallbackChain );
2976 foreach ( $fallbackChain as $code ) {
2977 if ( isset( $newWords[$code] ) ) {
2978 $this->mMagicExtensions
= $newWords[$code] +
$this->mMagicExtensions
;
2984 * Get special page names, as an associative array
2985 * case folded alias => real name
2987 function getSpecialPageAliases() {
2988 // Cache aliases because it may be slow to load them
2989 if ( is_null( $this->mExtendedSpecialPageAliases
) ) {
2991 $this->mExtendedSpecialPageAliases
=
2992 self
::$dataCache->getItem( $this->mCode
, 'specialPageAliases' );
2993 wfRunHooks( 'LanguageGetSpecialPageAliases',
2994 array( &$this->mExtendedSpecialPageAliases
, $this->getCode() ) );
2997 return $this->mExtendedSpecialPageAliases
;
3001 * Italic is unsuitable for some languages
3003 * @param $text String: the text to be emphasized.
3006 function emphasize( $text ) {
3007 return "<em>$text</em>";
3011 * Normally we output all numbers in plain en_US style, that is
3012 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3013 * point twohundredthirtyfive. However this is not suitable for all
3014 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
3015 * Icelandic just want to use commas instead of dots, and dots instead
3016 * of commas like "293.291,235".
3018 * An example of this function being called:
3020 * wfMessage( 'message' )->numParams( $num )->text()
3023 * See LanguageGu.php for the Gujarati implementation and
3024 * $separatorTransformTable on MessageIs.php for
3025 * the , => . and . => , implementation.
3027 * @todo check if it's viable to use localeconv() for the decimal
3029 * @param $number Mixed: the string to be formatted, should be an integer
3030 * or a floating point number.
3031 * @param $nocommafy Bool: set to true for special numbers like dates
3034 public function formatNum( $number, $nocommafy = false ) {
3035 global $wgTranslateNumerals;
3036 if ( !$nocommafy ) {
3037 $number = $this->commafy( $number );
3038 $s = $this->separatorTransformTable();
3040 $number = strtr( $number, $s );
3044 if ( $wgTranslateNumerals ) {
3045 $s = $this->digitTransformTable();
3047 $number = strtr( $number, $s );
3055 * @param $number string
3058 function parseFormattedNumber( $number ) {
3059 $s = $this->digitTransformTable();
3061 $number = strtr( $number, array_flip( $s ) );
3064 $s = $this->separatorTransformTable();
3066 $number = strtr( $number, array_flip( $s ) );
3069 $number = strtr( $number, array( ',' => '' ) );
3074 * Adds commas to a given number
3079 function commafy( $_ ) {
3080 $digitGroupingPattern = $this->digitGroupingPattern();
3081 if ( $_ === null ) {
3085 if ( !$digitGroupingPattern ||
$digitGroupingPattern === "###,###,###" ) {
3086 // default grouping is at thousands, use the same for ###,###,### pattern too.
3087 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $_ ) ) );
3089 // Ref: http://cldr.unicode.org/translation/number-patterns
3091 if ( intval( $_ ) < 0 ) {
3092 // For negative numbers apply the algorithm like positive number and add sign.
3094 $_ = substr( $_, 1 );
3096 $numberpart = array();
3097 $decimalpart = array();
3098 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3099 preg_match( "/\d+/", $_, $numberpart );
3100 preg_match( "/\.\d*/", $_, $decimalpart );
3101 $groupedNumber = ( count( $decimalpart ) > 0 ) ?
$decimalpart[0]:"";
3102 if ( $groupedNumber === $_ ) {
3103 // the string does not have any number part. Eg: .12345
3104 return $sign . $groupedNumber;
3106 $start = $end = strlen( $numberpart[0] );
3107 while ( $start > 0 ) {
3108 $match = $matches[0][$numMatches -1] ;
3109 $matchLen = strlen( $match );
3110 $start = $end - $matchLen;
3114 $groupedNumber = substr( $_ , $start, $end -$start ) . $groupedNumber ;
3116 if ( $numMatches > 1 ) {
3117 // use the last pattern for the rest of the number
3121 $groupedNumber = "," . $groupedNumber;
3124 return $sign . $groupedNumber;
3130 function digitGroupingPattern() {
3131 return self
::$dataCache->getItem( $this->mCode
, 'digitGroupingPattern' );
3137 function digitTransformTable() {
3138 return self
::$dataCache->getItem( $this->mCode
, 'digitTransformTable' );
3144 function separatorTransformTable() {
3145 return self
::$dataCache->getItem( $this->mCode
, 'separatorTransformTable' );
3149 * Take a list of strings and build a locale-friendly comma-separated
3150 * list, using the local comma-separator message.
3151 * The last two strings are chained with an "and".
3152 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3157 function listToText( array $l ) {
3159 $m = count( $l ) - 1;
3163 } elseif ( $m === 1 ) {
3164 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
3166 for ( $i = $m; $i >= 0; $i-- ) {
3169 } elseif ( $i == $m - 1 ) {
3170 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
3172 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
3180 * Take a list of strings and build a locale-friendly comma-separated
3181 * list, using the local comma-separator message.
3182 * @param $list array of strings to put in a comma list
3185 function commaList( array $list ) {
3187 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3193 * Take a list of strings and build a locale-friendly semicolon-separated
3194 * list, using the local semicolon-separator message.
3195 * @param $list array of strings to put in a semicolon list
3198 function semicolonList( array $list ) {
3200 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3206 * Same as commaList, but separate it with the pipe instead.
3207 * @param $list array of strings to put in a pipe list
3210 function pipeList( array $list ) {
3212 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3218 * Truncate a string to a specified length in bytes, appending an optional
3219 * string (e.g. for ellipses)
3221 * The database offers limited byte lengths for some columns in the database;
3222 * multi-byte character sets mean we need to ensure that only whole characters
3223 * are included, otherwise broken characters can be passed to the user
3225 * If $length is negative, the string will be truncated from the beginning
3227 * @param $string String to truncate
3228 * @param $length Int: maximum length (including ellipses)
3229 * @param $ellipsis String to append to the truncated text
3230 * @param $adjustLength Boolean: Subtract length of ellipsis from $length.
3231 * $adjustLength was introduced in 1.18, before that behaved as if false.
3234 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3235 # Use the localized ellipsis character
3236 if ( $ellipsis == '...' ) {
3237 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3239 # Check if there is no need to truncate
3240 if ( $length == 0 ) {
3241 return $ellipsis; // convention
3242 } elseif ( strlen( $string ) <= abs( $length ) ) {
3243 return $string; // no need to truncate
3245 $stringOriginal = $string;
3246 # If ellipsis length is >= $length then we can't apply $adjustLength
3247 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3248 $string = $ellipsis; // this can be slightly unexpected
3249 # Otherwise, truncate and add ellipsis...
3251 $eLength = $adjustLength ?
strlen( $ellipsis ) : 0;
3252 if ( $length > 0 ) {
3253 $length -= $eLength;
3254 $string = substr( $string, 0, $length ); // xyz...
3255 $string = $this->removeBadCharLast( $string );
3256 $string = $string . $ellipsis;
3258 $length +
= $eLength;
3259 $string = substr( $string, $length ); // ...xyz
3260 $string = $this->removeBadCharFirst( $string );
3261 $string = $ellipsis . $string;
3264 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3265 # This check is *not* redundant if $adjustLength, due to the single case where
3266 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3267 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3270 return $stringOriginal;
3275 * Remove bytes that represent an incomplete Unicode character
3276 * at the end of string (e.g. bytes of the char are missing)
3278 * @param $string String
3281 protected function removeBadCharLast( $string ) {
3282 if ( $string != '' ) {
3283 $char = ord( $string[strlen( $string ) - 1] );
3285 if ( $char >= 0xc0 ) {
3286 # We got the first byte only of a multibyte char; remove it.
3287 $string = substr( $string, 0, -1 );
3288 } elseif ( $char >= 0x80 &&
3289 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3290 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) )
3292 # We chopped in the middle of a character; remove it
3300 * Remove bytes that represent an incomplete Unicode character
3301 * at the start of string (e.g. bytes of the char are missing)
3303 * @param $string String
3306 protected function removeBadCharFirst( $string ) {
3307 if ( $string != '' ) {
3308 $char = ord( $string[0] );
3309 if ( $char >= 0x80 && $char < 0xc0 ) {
3310 # We chopped in the middle of a character; remove the whole thing
3311 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3318 * Truncate a string of valid HTML to a specified length in bytes,
3319 * appending an optional string (e.g. for ellipses), and return valid HTML
3321 * This is only intended for styled/linked text, such as HTML with
3322 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3323 * Also, this will not detect things like "display:none" CSS.
3325 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3327 * @param string $text HTML string to truncate
3328 * @param int $length (zero/positive) Maximum length (including ellipses)
3329 * @param string $ellipsis String to append to the truncated text
3332 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3333 # Use the localized ellipsis character
3334 if ( $ellipsis == '...' ) {
3335 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3337 # Check if there is clearly no need to truncate
3338 if ( $length <= 0 ) {
3339 return $ellipsis; // no text shown, nothing to format (convention)
3340 } elseif ( strlen( $text ) <= $length ) {
3341 return $text; // string short enough even *with* HTML (short-circuit)
3344 $dispLen = 0; // innerHTML legth so far
3345 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3346 $tagType = 0; // 0-open, 1-close
3347 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3348 $entityState = 0; // 0-not entity, 1-entity
3349 $tag = $ret = ''; // accumulated tag name, accumulated result string
3350 $openTags = array(); // open tag stack
3351 $maybeState = null; // possible truncation state
3353 $textLen = strlen( $text );
3354 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3355 for ( $pos = 0; true; ++
$pos ) {
3356 # Consider truncation once the display length has reached the maximim.
3357 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3358 # Check that we're not in the middle of a bracket/entity...
3359 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3360 if ( !$testingEllipsis ) {
3361 $testingEllipsis = true;
3362 # Save where we are; we will truncate here unless there turn out to
3363 # be so few remaining characters that truncation is not necessary.
3364 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3365 $maybeState = array( $ret, $openTags ); // save state
3367 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3368 # String in fact does need truncation, the truncation point was OK.
3369 list( $ret, $openTags ) = $maybeState; // reload state
3370 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3371 $ret .= $ellipsis; // add ellipsis
3375 if ( $pos >= $textLen ) break; // extra iteration just for above checks
3377 # Read the next char...
3379 $lastCh = $pos ?
$text[$pos - 1] : '';
3380 $ret .= $ch; // add to result string
3382 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3383 $entityState = 0; // for bad HTML
3384 $bracketState = 1; // tag started (checking for backslash)
3385 } elseif ( $ch == '>' ) {
3386 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3387 $entityState = 0; // for bad HTML
3388 $bracketState = 0; // out of brackets
3389 } elseif ( $bracketState == 1 ) {
3391 $tagType = 1; // close tag (e.g. "</span>")
3393 $tagType = 0; // open tag (e.g. "<span>")
3396 $bracketState = 2; // building tag name
3397 } elseif ( $bracketState == 2 ) {
3401 // Name found (e.g. "<a href=..."), add on tag attributes...
3402 $pos +
= $this->truncate_skip( $ret, $text, "<>", $pos +
1 );
3404 } elseif ( $bracketState == 0 ) {
3405 if ( $entityState ) {
3408 $dispLen++
; // entity is one displayed char
3411 if ( $neLength == 0 && !$maybeState ) {
3412 // Save state without $ch. We want to *hit* the first
3413 // display char (to get tags) but not *use* it if truncating.
3414 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3417 $entityState = 1; // entity found, (e.g. " ")
3419 $dispLen++
; // this char is displayed
3420 // Add the next $max display text chars after this in one swoop...
3421 $max = ( $testingEllipsis ?
$length : $neLength ) - $dispLen;
3422 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos +
1, $max );
3423 $dispLen +
= $skipped;
3429 // Close the last tag if left unclosed by bad HTML
3430 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3431 while ( count( $openTags ) > 0 ) {
3432 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3438 * truncateHtml() helper function
3439 * like strcspn() but adds the skipped chars to $ret
3448 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3449 if ( $len === null ) {
3450 $len = -1; // -1 means "no limit" for strcspn
3451 } elseif ( $len < 0 ) {
3455 if ( $start < strlen( $text ) ) {
3456 $skipCount = strcspn( $text, $search, $start, $len );
3457 $ret .= substr( $text, $start, $skipCount );
3463 * truncateHtml() helper function
3464 * (a) push or pop $tag from $openTags as needed
3465 * (b) clear $tag value
3466 * @param &$tag string Current HTML tag name we are looking at
3467 * @param $tagType int (0-open tag, 1-close tag)
3468 * @param $lastCh string Character before the '>' that ended this tag
3469 * @param &$openTags array Open tag stack (not accounting for $tag)
3471 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3472 $tag = ltrim( $tag );
3474 if ( $tagType == 0 && $lastCh != '/' ) {
3475 $openTags[] = $tag; // tag opened (didn't close itself)
3476 } elseif ( $tagType == 1 ) {
3477 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3478 array_pop( $openTags ); // tag closed
3486 * Grammatical transformations, needed for inflected languages
3487 * Invoked by putting {{grammar:case|word}} in a message
3489 * @param $word string
3490 * @param $case string
3493 function convertGrammar( $word, $case ) {
3494 global $wgGrammarForms;
3495 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3496 return $wgGrammarForms[$this->getCode()][$case][$word];
3501 * Get the grammar forms for the content language
3502 * @return array of grammar forms
3505 function getGrammarForms() {
3506 global $wgGrammarForms;
3507 if ( isset( $wgGrammarForms[$this->getCode()] ) && is_array( $wgGrammarForms[$this->getCode()] ) ) {
3508 return $wgGrammarForms[$this->getCode()];
3513 * Provides an alternative text depending on specified gender.
3514 * Usage {{gender:username|masculine|feminine|neutral}}.
3515 * username is optional, in which case the gender of current user is used,
3516 * but only in (some) interface messages; otherwise default gender is used.
3518 * If no forms are given, an empty string is returned. If only one form is
3519 * given, it will be returned unconditionally. These details are implied by
3520 * the caller and cannot be overridden in subclasses.
3522 * If more than one form is given, the default is to use the neutral one
3523 * if it is specified, and to use the masculine one otherwise. These
3524 * details can be overridden in subclasses.
3526 * @param $gender string
3527 * @param $forms array
3531 function gender( $gender, $forms ) {
3532 if ( !count( $forms ) ) {
3535 $forms = $this->preConvertPlural( $forms, 2 );
3536 if ( $gender === 'male' ) {
3539 if ( $gender === 'female' ) {
3542 return isset( $forms[2] ) ?
$forms[2] : $forms[0];
3546 * Plural form transformations, needed for some languages.
3547 * For example, there are 3 form of plural in Russian and Polish,
3548 * depending on "count mod 10". See [[w:Plural]]
3549 * For English it is pretty simple.
3551 * Invoked by putting {{plural:count|wordform1|wordform2}}
3552 * or {{plural:count|wordform1|wordform2|wordform3}}
3554 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3556 * @param $count Integer: non-localized number
3557 * @param $forms Array: different plural forms
3558 * @return string Correct form of plural for $count in this language
3560 function convertPlural( $count, $forms ) {
3561 if ( !count( $forms ) ) {
3565 // Handle explicit 0= and 1= forms
3566 foreach ( $forms as $index => $form ) {
3567 if ( isset( $form[1] ) && $form[1] === '=' ) {
3568 if ( $form[0] === (string) $count ) {
3569 return substr( $form, 2 );
3571 unset( $forms[$index] );
3574 $forms = array_values( $forms );
3576 $pluralForm = $this->getPluralForm( $count );
3577 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3578 return $forms[$pluralForm];
3582 * Checks that convertPlural was given an array and pads it to requested
3583 * amount of forms by copying the last one.
3585 * @param $count Integer: How many forms should there be at least
3586 * @param $forms Array of forms given to convertPlural
3587 * @return array Padded array of forms or an exception if not an array
3589 protected function preConvertPlural( /* Array */ $forms, $count ) {
3590 while ( count( $forms ) < $count ) {
3591 $forms[] = $forms[count( $forms ) - 1];
3597 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
3598 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3599 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3600 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3603 * @param $str String: the validated block duration in English
3604 * @return string Somehow translated block duration
3605 * @see LanguageFi.php for example implementation
3607 function translateBlockExpiry( $str ) {
3608 $duration = SpecialBlock
::getSuggestedDurations( $this );
3609 foreach ( $duration as $show => $value ) {
3610 if ( strcmp( $str, $value ) == 0 ) {
3611 return htmlspecialchars( trim( $show ) );
3615 // Since usually only infinite or indefinite is only on list, so try
3616 // equivalents if still here.
3617 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3618 if ( in_array( $str, $indefs ) ) {
3619 foreach ( $indefs as $val ) {
3620 $show = array_search( $val, $duration, true );
3621 if ( $show !== false ) {
3622 return htmlspecialchars( trim( $show ) );
3626 // If all else fails, return the original string.
3631 * languages like Chinese need to be segmented in order for the diff
3634 * @param $text String
3637 public function segmentForDiff( $text ) {
3642 * and unsegment to show the result
3644 * @param $text String
3647 public function unsegmentForDiff( $text ) {
3652 * Return the LanguageConverter used in the Language
3655 * @return LanguageConverter
3657 public function getConverter() {
3658 return $this->mConverter
;
3662 * convert text to all supported variants
3664 * @param $text string
3667 public function autoConvertToAllVariants( $text ) {
3668 return $this->mConverter
->autoConvertToAllVariants( $text );
3672 * convert text to different variants of a language.
3674 * @param $text string
3677 public function convert( $text ) {
3678 return $this->mConverter
->convert( $text );
3682 * Convert a Title object to a string in the preferred variant
3684 * @param $title Title
3687 public function convertTitle( $title ) {
3688 return $this->mConverter
->convertTitle( $title );
3692 * Convert a namespace index to a string in the preferred variant
3697 public function convertNamespace( $ns ) {
3698 return $this->mConverter
->convertNamespace( $ns );
3702 * Check if this is a language with variants
3706 public function hasVariants() {
3707 return sizeof( $this->getVariants() ) > 1;
3711 * Check if the language has the specific variant
3714 * @param $variant string
3717 public function hasVariant( $variant ) {
3718 return (bool)$this->mConverter
->validateVariant( $variant );
3722 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3724 * @param $text string
3727 public function armourMath( $text ) {
3728 return $this->mConverter
->armourMath( $text );
3732 * Perform output conversion on a string, and encode for safe HTML output.
3733 * @param $text String text to be converted
3734 * @param $isTitle Bool whether this conversion is for the article title
3736 * @todo this should get integrated somewhere sane
3738 public function convertHtml( $text, $isTitle = false ) {
3739 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3743 * @param $key string
3746 public function convertCategoryKey( $key ) {
3747 return $this->mConverter
->convertCategoryKey( $key );
3751 * Get the list of variants supported by this language
3752 * see sample implementation in LanguageZh.php
3754 * @return array an array of language codes
3756 public function getVariants() {
3757 return $this->mConverter
->getVariants();
3763 public function getPreferredVariant() {
3764 return $this->mConverter
->getPreferredVariant();
3770 public function getDefaultVariant() {
3771 return $this->mConverter
->getDefaultVariant();
3777 public function getURLVariant() {
3778 return $this->mConverter
->getURLVariant();
3782 * If a language supports multiple variants, it is
3783 * possible that non-existing link in one variant
3784 * actually exists in another variant. this function
3785 * tries to find it. See e.g. LanguageZh.php
3787 * @param $link String: the name of the link
3788 * @param $nt Mixed: the title object of the link
3789 * @param $ignoreOtherCond Boolean: to disable other conditions when
3790 * we need to transclude a template or update a category's link
3791 * @return null the input parameters may be modified upon return
3793 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
3794 $this->mConverter
->findVariantLink( $link, $nt, $ignoreOtherCond );
3798 * If a language supports multiple variants, converts text
3799 * into an array of all possible variants of the text:
3800 * 'variant' => text in that variant
3802 * @deprecated since 1.17 Use autoConvertToAllVariants()
3804 * @param $text string
3808 public function convertLinkToAllVariants( $text ) {
3809 return $this->mConverter
->convertLinkToAllVariants( $text );
3813 * returns language specific options used by User::getPageRenderHash()
3814 * for example, the preferred language variant
3818 function getExtraHashOptions() {
3819 return $this->mConverter
->getExtraHashOptions();
3823 * For languages that support multiple variants, the title of an
3824 * article may be displayed differently in different variants. this
3825 * function returns the apporiate title defined in the body of the article.
3829 public function getParsedTitle() {
3830 return $this->mConverter
->getParsedTitle();
3834 * Enclose a string with the "no conversion" tag. This is used by
3835 * various functions in the Parser
3837 * @param $text String: text to be tagged for no conversion
3838 * @param $noParse bool
3839 * @return string the tagged text
3841 public function markNoConversion( $text, $noParse = false ) {
3842 return $this->mConverter
->markNoConversion( $text, $noParse );
3846 * A regular expression to match legal word-trailing characters
3847 * which should be merged onto a link of the form [[foo]]bar.
3851 public function linkTrail() {
3852 return self
::$dataCache->getItem( $this->mCode
, 'linkTrail' );
3858 function getLangObj() {
3863 * Get the RFC 3066 code for this language object
3865 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3866 * htmlspecialchars() or similar
3870 public function getCode() {
3871 return $this->mCode
;
3875 * Get the code in Bcp47 format which we can use
3876 * inside of html lang="" tags.
3878 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3879 * htmlspecialchars() or similar.
3884 public function getHtmlCode() {
3885 if ( is_null( $this->mHtmlCode
) ) {
3886 $this->mHtmlCode
= wfBCP47( $this->getCode() );
3888 return $this->mHtmlCode
;
3892 * @param $code string
3894 public function setCode( $code ) {
3895 $this->mCode
= $code;
3896 // Ensure we don't leave an incorrect html code lying around
3897 $this->mHtmlCode
= null;
3901 * Get the name of a file for a certain language code
3902 * @param $prefix string Prepend this to the filename
3903 * @param $code string Language code
3904 * @param $suffix string Append this to the filename
3905 * @throws MWException
3906 * @return string $prefix . $mangledCode . $suffix
3908 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
3909 // Protect against path traversal
3910 if ( !Language
::isValidCode( $code )
3911 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
3913 throw new MWException( "Invalid language code \"$code\"" );
3916 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
3920 * Get the language code from a file name. Inverse of getFileName()
3921 * @param $filename string $prefix . $languageCode . $suffix
3922 * @param $prefix string Prefix before the language code
3923 * @param $suffix string Suffix after the language code
3924 * @return string Language code, or false if $prefix or $suffix isn't found
3926 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
3928 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
3929 preg_quote( $suffix, '/' ) . '/', $filename, $m );
3930 if ( !count( $m ) ) {
3933 return str_replace( '_', '-', strtolower( $m[1] ) );
3937 * @param $code string
3940 public static function getMessagesFileName( $code ) {
3942 $file = self
::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
3943 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
3948 * @param $code string
3951 public static function getClassFileName( $code ) {
3953 return self
::getFileName( "$IP/languages/classes/Language", $code, '.php' );
3957 * Get the first fallback for a given language.
3959 * @param $code string
3961 * @return bool|string
3963 public static function getFallbackFor( $code ) {
3964 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3967 $fallbacks = self
::getFallbacksFor( $code );
3968 $first = array_shift( $fallbacks );
3974 * Get the ordered list of fallback languages.
3977 * @param $code string Language code
3980 public static function getFallbacksFor( $code ) {
3981 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3984 $v = self
::getLocalisationCache()->getItem( $code, 'fallback' );
3985 $v = array_map( 'trim', explode( ',', $v ) );
3986 if ( $v[count( $v ) - 1] !== 'en' ) {
3994 * Get all messages for a given language
3995 * WARNING: this may take a long time. If you just need all message *keys*
3996 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
3998 * @param $code string
4002 public static function getMessagesFor( $code ) {
4003 return self
::getLocalisationCache()->getItem( $code, 'messages' );
4007 * Get a message for a given language
4009 * @param $key string
4010 * @param $code string
4014 public static function getMessageFor( $key, $code ) {
4015 return self
::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4019 * Get all message keys for a given language. This is a faster alternative to
4020 * array_keys( Language::getMessagesFor( $code ) )
4023 * @param $code string Language code
4024 * @return array of message keys (strings)
4026 public static function getMessageKeysFor( $code ) {
4027 return self
::getLocalisationCache()->getSubItemList( $code, 'messages' );
4034 function fixVariableInNamespace( $talk ) {
4035 if ( strpos( $talk, '$1' ) === false ) {
4039 global $wgMetaNamespace;
4040 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4042 # Allow grammar transformations
4043 # Allowing full message-style parsing would make simple requests
4044 # such as action=raw much more expensive than they need to be.
4045 # This will hopefully cover most cases.
4046 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4047 array( &$this, 'replaceGrammarInNamespace' ), $talk );
4048 return str_replace( ' ', '_', $talk );
4055 function replaceGrammarInNamespace( $m ) {
4056 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4060 * @throws MWException
4063 static function getCaseMaps() {
4064 static $wikiUpperChars, $wikiLowerChars;
4065 if ( isset( $wikiUpperChars ) ) {
4066 return array( $wikiUpperChars, $wikiLowerChars );
4069 wfProfileIn( __METHOD__
);
4070 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4071 if ( $arr === false ) {
4072 throw new MWException(
4073 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4075 $wikiUpperChars = $arr['wikiUpperChars'];
4076 $wikiLowerChars = $arr['wikiLowerChars'];
4077 wfProfileOut( __METHOD__
);
4078 return array( $wikiUpperChars, $wikiLowerChars );
4082 * Decode an expiry (block, protection, etc) which has come from the DB
4084 * @todo FIXME: why are we returnings DBMS-dependent strings???
4086 * @param $expiry String: Database expiry String
4087 * @param $format Bool|Int true to process using language functions, or TS_ constant
4088 * to return the expiry in a given timestamp
4092 public function formatExpiry( $expiry, $format = true ) {
4093 static $infinity, $infinityMsg;
4094 if ( $infinity === null ) {
4095 $infinityMsg = wfMessage( 'infiniteblock' );
4096 $infinity = wfGetDB( DB_SLAVE
)->getInfinity();
4099 if ( $expiry == '' ||
$expiry == $infinity ) {
4100 return $format === true
4104 return $format === true
4105 ?
$this->timeanddate( $expiry, /* User preference timezone */ true )
4106 : wfTimestamp( $format, $expiry );
4112 * @param $seconds int|float
4113 * @param $format Array Optional
4114 * If $format['avoid'] == 'avoidseconds' - don't mention seconds if $seconds >= 1 hour
4115 * If $format['avoid'] == 'avoidminutes' - don't mention seconds/minutes if $seconds > 48 hours
4116 * If $format['noabbrevs'] is true - use 'seconds' and friends instead of 'seconds-abbrev' and friends
4117 * For backwards compatibility, $format may also be one of the strings 'avoidseconds' or 'avoidminutes'
4120 function formatTimePeriod( $seconds, $format = array() ) {
4121 if ( !is_array( $format ) ) {
4122 $format = array( 'avoid' => $format ); // For backwards compatibility
4124 if ( !isset( $format['avoid'] ) ) {
4125 $format['avoid'] = false;
4127 if ( !isset( $format['noabbrevs' ] ) ) {
4128 $format['noabbrevs'] = false;
4130 $secondsMsg = wfMessage(
4131 $format['noabbrevs'] ?
'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4132 $minutesMsg = wfMessage(
4133 $format['noabbrevs'] ?
'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4134 $hoursMsg = wfMessage(
4135 $format['noabbrevs'] ?
'hours' : 'hours-abbrev' )->inLanguage( $this );
4136 $daysMsg = wfMessage(
4137 $format['noabbrevs'] ?
'days' : 'days-abbrev' )->inLanguage( $this );
4139 if ( round( $seconds * 10 ) < 100 ) {
4140 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4141 $s = $secondsMsg->params( $s )->text();
4142 } elseif ( round( $seconds ) < 60 ) {
4143 $s = $this->formatNum( round( $seconds ) );
4144 $s = $secondsMsg->params( $s )->text();
4145 } elseif ( round( $seconds ) < 3600 ) {
4146 $minutes = floor( $seconds / 60 );
4147 $secondsPart = round( fmod( $seconds, 60 ) );
4148 if ( $secondsPart == 60 ) {
4152 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4154 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4155 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4156 $hours = floor( $seconds / 3600 );
4157 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4158 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4159 if ( $secondsPart == 60 ) {
4163 if ( $minutes == 60 ) {
4167 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4169 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4170 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4171 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4174 $days = floor( $seconds / 86400 );
4175 if ( $format['avoid'] === 'avoidminutes' ) {
4176 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4177 if ( $hours == 24 ) {
4181 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4183 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4184 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4185 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4186 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4187 if ( $minutes == 60 ) {
4191 if ( $hours == 24 ) {
4195 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4197 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4199 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4201 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4203 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4210 * Format a bitrate for output, using an appropriate
4211 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to the magnitude in question
4213 * This use base 1000. For base 1024 use formatSize(), for another base
4214 * see formatComputingNumbers()
4219 function formatBitrate( $bps ) {
4220 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4224 * @param $size int Size of the unit
4225 * @param $boundary int Size boundary (1000, or 1024 in most cases)
4226 * @param $messageKey string Message key to be uesd
4229 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4231 return str_replace( '$1', $this->formatNum( $size ),
4232 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4235 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4238 $maxIndex = count( $sizes ) - 1;
4239 while ( $size >= $boundary && $index < $maxIndex ) {
4244 // For small sizes no decimal places necessary
4247 // For MB and bigger two decimal places are smarter
4250 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4252 $size = round( $size, $round );
4253 $text = $this->getMessageFromDB( $msg );
4254 return str_replace( '$1', $this->formatNum( $size ), $text );
4258 * Format a size in bytes for output, using an appropriate
4259 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4261 * This method use base 1024. For base 1000 use formatBitrate(), for
4262 * another base see formatComputingNumbers()
4264 * @param $size int Size to format
4265 * @return string Plain text (not HTML)
4267 function formatSize( $size ) {
4268 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4272 * Make a list item, used by various special pages
4274 * @param $page String Page link
4275 * @param $details String Text between brackets
4276 * @param $oppositedm Boolean Add the direction mark opposite to your
4277 * language, to display text properly
4280 function specialList( $page, $details, $oppositedm = true ) {
4281 $dirmark = ( $oppositedm ?
$this->getDirMark( true ) : '' ) .
4282 $this->getDirMark();
4283 $details = $details ?
$dirmark . $this->getMessageFromDB( 'word-separator' ) .
4284 wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : '';
4285 return $page . $details;
4289 * Generate (prev x| next x) (20|50|100...) type links for paging
4291 * @param $title Title object to link
4292 * @param $offset Integer offset parameter
4293 * @param $limit Integer limit parameter
4294 * @param $query array|String optional URL query parameter string
4295 * @param $atend Bool optional param for specified if this is the last page
4298 public function viewPrevNext( Title
$title, $offset, $limit, array $query = array(), $atend = false ) {
4299 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4301 # Make 'previous' link
4302 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4303 if ( $offset > 0 ) {
4304 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4305 $query, $prev, 'prevn-title', 'mw-prevlink' );
4307 $plink = htmlspecialchars( $prev );
4311 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4313 $nlink = htmlspecialchars( $next );
4315 $nlink = $this->numLink( $title, $offset +
$limit, $limit,
4316 $query, $next, 'prevn-title', 'mw-nextlink' );
4319 # Make links to set number of items per page
4320 $numLinks = array();
4321 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4322 $numLinks[] = $this->numLink( $title, $offset, $num,
4323 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4326 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4327 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4331 * Helper function for viewPrevNext() that generates links
4333 * @param $title Title object to link
4334 * @param $offset Integer offset parameter
4335 * @param $limit Integer limit parameter
4336 * @param $query Array extra query parameters
4337 * @param $link String text to use for the link; will be escaped
4338 * @param $tooltipMsg String name of the message to use as tooltip
4339 * @param $class String value of the "class" attribute of the link
4340 * @return String HTML fragment
4342 private function numLink( Title
$title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) {
4343 $query = array( 'limit' => $limit, 'offset' => $offset ) +
$query;
4344 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4345 return Html
::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4346 'title' => $tooltip, 'class' => $class ), $link );
4350 * Get the conversion rule title, if any.
4354 public function getConvRuleTitle() {
4355 return $this->mConverter
->getConvRuleTitle();
4359 * Get the compiled plural rules for the language
4361 * @return array Associative array with plural form, and plural rule as key-value pairs
4363 public function getCompiledPluralRules() {
4364 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'compiledPluralRules' );
4365 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4366 if ( !$pluralRules ) {
4367 foreach ( $fallbacks as $fallbackCode ) {
4368 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4369 if ( $pluralRules ) {
4374 return $pluralRules;
4378 * Get the plural rules for the language
4380 * @return array Associative array with plural form, and plural rule as key-value pairs
4382 public function getPluralRules() {
4383 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'pluralRules' );
4384 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4385 if ( !$pluralRules ) {
4386 foreach ( $fallbacks as $fallbackCode ) {
4387 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4388 if ( $pluralRules ) {
4393 return $pluralRules;
4397 * Find the plural form matching to the given number
4398 * It return the form index.
4399 * @return int The index of the plural form
4401 private function getPluralForm( $number ) {
4402 $pluralRules = $this->getCompiledPluralRules();
4403 $form = CLDRPluralRuleEvaluator
::evaluateCompiled( $number, $pluralRules );