Handle invalid language code gracefully in Language::fetchLanguageNames
[lhc/web/wiklou.git] / languages / Language.php
1 <?php
2 /**
3 * Internationalisation code.
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup Language
22 */
23
24 /**
25 * @defgroup Language Language
26 */
27
28 if ( !defined( 'MEDIAWIKI' ) ) {
29 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
30 exit( 1 );
31 }
32
33 if ( function_exists( 'mb_strtoupper' ) ) {
34 mb_internal_encoding( 'UTF-8' );
35 }
36
37 /**
38 * Internationalisation code
39 * @ingroup Language
40 */
41 class Language {
42 /**
43 * @var LanguageConverter
44 */
45 public $mConverter;
46
47 public $mVariants, $mCode, $mLoaded = false;
48 public $mMagicExtensions = array(), $mMagicHookDone = false;
49 private $mHtmlCode = null, $mParentLanguage = false;
50
51 public $dateFormatStrings = array();
52 public $mExtendedSpecialPageAliases;
53
54 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
55
56 /**
57 * ReplacementArray object caches
58 */
59 public $transformData = array();
60
61 /**
62 * @var LocalisationCache
63 */
64 static public $dataCache;
65
66 static public $mLangObjCache = array();
67
68 static public $mWeekdayMsgs = array(
69 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
70 'friday', 'saturday'
71 );
72
73 static public $mWeekdayAbbrevMsgs = array(
74 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
75 );
76
77 static public $mMonthMsgs = array(
78 'january', 'february', 'march', 'april', 'may_long', 'june',
79 'july', 'august', 'september', 'october', 'november',
80 'december'
81 );
82 static public $mMonthGenMsgs = array(
83 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
84 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
85 'december-gen'
86 );
87 static public $mMonthAbbrevMsgs = array(
88 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
89 'sep', 'oct', 'nov', 'dec'
90 );
91
92 static public $mIranianCalendarMonthMsgs = array(
93 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
94 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
95 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
96 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
97 );
98
99 static public $mHebrewCalendarMonthMsgs = array(
100 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
101 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
102 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
103 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
104 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
105 );
106
107 static public $mHebrewCalendarMonthGenMsgs = array(
108 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
109 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
110 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
111 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
112 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
113 );
114
115 static public $mHijriCalendarMonthMsgs = array(
116 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
117 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
118 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
119 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
120 );
121
122 /**
123 * @since 1.20
124 * @var array
125 */
126 static public $durationIntervals = array(
127 'millennia' => 31556952000,
128 'centuries' => 3155695200,
129 'decades' => 315569520,
130 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
131 'weeks' => 604800,
132 'days' => 86400,
133 'hours' => 3600,
134 'minutes' => 60,
135 'seconds' => 1,
136 );
137
138 /**
139 * Cache for language fallbacks.
140 * @see Language::getFallbacksIncludingSiteLanguage
141 * @since 1.21
142 * @var array
143 */
144 static private $fallbackLanguageCache = array();
145
146 /**
147 * Get a cached or new language object for a given language code
148 * @param string $code
149 * @return Language
150 */
151 static function factory( $code ) {
152 global $wgDummyLanguageCodes, $wgLangObjCacheSize;
153
154 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
155 $code = $wgDummyLanguageCodes[$code];
156 }
157
158 // get the language object to process
159 $langObj = isset( self::$mLangObjCache[$code] )
160 ? self::$mLangObjCache[$code]
161 : self::newFromCode( $code );
162
163 // merge the language object in to get it up front in the cache
164 self::$mLangObjCache = array_merge( array( $code => $langObj ), self::$mLangObjCache );
165 // get rid of the oldest ones in case we have an overflow
166 self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
167
168 return $langObj;
169 }
170
171 /**
172 * Create a language object for a given language code
173 * @param string $code
174 * @throws MWException
175 * @return Language
176 */
177 protected static function newFromCode( $code ) {
178 // Protect against path traversal below
179 if ( !Language::isValidCode( $code )
180 || strcspn( $code, ":/\\\000" ) !== strlen( $code )
181 ) {
182 throw new MWException( "Invalid language code \"$code\"" );
183 }
184
185 if ( !Language::isValidBuiltInCode( $code ) ) {
186 // It's not possible to customise this code with class files, so
187 // just return a Language object. This is to support uselang= hacks.
188 $lang = new Language;
189 $lang->setCode( $code );
190 return $lang;
191 }
192
193 // Check if there is a language class for the code
194 $class = self::classFromCode( $code );
195 self::preloadLanguageClass( $class );
196 if ( class_exists( $class ) ) {
197 $lang = new $class;
198 return $lang;
199 }
200
201 // Keep trying the fallback list until we find an existing class
202 $fallbacks = Language::getFallbacksFor( $code );
203 foreach ( $fallbacks as $fallbackCode ) {
204 if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
205 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
206 }
207
208 $class = self::classFromCode( $fallbackCode );
209 self::preloadLanguageClass( $class );
210 if ( class_exists( $class ) ) {
211 $lang = Language::newFromCode( $fallbackCode );
212 $lang->setCode( $code );
213 return $lang;
214 }
215 }
216
217 throw new MWException( "Invalid fallback sequence for language '$code'" );
218 }
219
220 /**
221 * Checks whether any localisation is available for that language tag
222 * in MediaWiki (MessagesXx.php exists).
223 *
224 * @param string $code Language tag (in lower case)
225 * @return bool Whether language is supported
226 * @since 1.21
227 */
228 public static function isSupportedLanguage( $code ) {
229 return self::isValidBuiltInCode( $code )
230 && ( is_readable( self::getMessagesFileName( $code ) )
231 || is_readable( self::getJsonMessagesFileName( $code ) )
232 );
233 }
234
235 /**
236 * Returns true if a language code string is a well-formed language tag
237 * according to RFC 5646.
238 * This function only checks well-formedness; it doesn't check that
239 * language, script or variant codes actually exist in the repositories.
240 *
241 * Based on regexes by Mark Davis of the Unicode Consortium:
242 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
243 *
244 * @param string $code
245 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
246 *
247 * @return bool
248 * @since 1.21
249 */
250 public static function isWellFormedLanguageTag( $code, $lenient = false ) {
251 $alpha = '[a-z]';
252 $digit = '[0-9]';
253 $alphanum = '[a-z0-9]';
254 $x = 'x'; # private use singleton
255 $singleton = '[a-wy-z]'; # other singleton
256 $s = $lenient ? '[-_]' : '-';
257
258 $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
259 $script = "$alpha{4}"; # ISO 15924
260 $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
261 $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
262 $extension = "$singleton(?:$s$alphanum{2,8})+";
263 $privateUse = "$x(?:$s$alphanum{1,8})+";
264
265 # Define certain grandfathered codes, since otherwise the regex is pretty useless.
266 # Since these are limited, this is safe even later changes to the registry --
267 # the only oddity is that it might change the type of the tag, and thus
268 # the results from the capturing groups.
269 # http://www.iana.org/assignments/language-subtag-registry
270
271 $grandfathered = "en{$s}GB{$s}oed"
272 . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
273 . "|no{$s}(?:bok|nyn)"
274 . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
275 . "|zh{$s}min{$s}nan";
276
277 $variantList = "$variant(?:$s$variant)*";
278 $extensionList = "$extension(?:$s$extension)*";
279
280 $langtag = "(?:($language)"
281 . "(?:$s$script)?"
282 . "(?:$s$region)?"
283 . "(?:$s$variantList)?"
284 . "(?:$s$extensionList)?"
285 . "(?:$s$privateUse)?)";
286
287 # The final breakdown, with capturing groups for each of these components
288 # The variants, extensions, grandfathered, and private-use may have interior '-'
289
290 $root = "^(?:$langtag|$privateUse|$grandfathered)$";
291
292 return (bool)preg_match( "/$root/", strtolower( $code ) );
293 }
294
295 /**
296 * Returns true if a language code string is of a valid form, whether or
297 * not it exists. This includes codes which are used solely for
298 * customisation via the MediaWiki namespace.
299 *
300 * @param string $code
301 *
302 * @return bool
303 */
304 public static function isValidCode( $code ) {
305 static $cache = array();
306 if ( isset( $cache[$code] ) ) {
307 return $cache[$code];
308 }
309 // People think language codes are html safe, so enforce it.
310 // Ideally we should only allow a-zA-Z0-9-
311 // but, .+ and other chars are often used for {{int:}} hacks
312 // see bugs 37564, 37587, 36938
313 $cache[$code] =
314 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
315 && !preg_match( Title::getTitleInvalidRegex(), $code );
316
317 return $cache[$code];
318 }
319
320 /**
321 * Returns true if a language code is of a valid form for the purposes of
322 * internal customisation of MediaWiki, via Messages*.php or *.json.
323 *
324 * @param string $code
325 *
326 * @throws MWException
327 * @since 1.18
328 * @return bool
329 */
330 public static function isValidBuiltInCode( $code ) {
331
332 if ( !is_string( $code ) ) {
333 if ( is_object( $code ) ) {
334 $addmsg = " of class " . get_class( $code );
335 } else {
336 $addmsg = '';
337 }
338 $type = gettype( $code );
339 throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
340 }
341
342 return (bool)preg_match( '/^[a-z0-9-]{2,}$/i', $code );
343 }
344
345 /**
346 * Returns true if a language code is an IETF tag known to MediaWiki.
347 *
348 * @param string $code
349 *
350 * @since 1.21
351 * @return bool
352 */
353 public static function isKnownLanguageTag( $tag ) {
354 static $coreLanguageNames;
355
356 // Quick escape for invalid input to avoid exceptions down the line
357 // when code tries to process tags which are not valid at all.
358 if ( !self::isValidBuiltInCode( $tag ) ) {
359 return false;
360 }
361
362 if ( $coreLanguageNames === null ) {
363 global $IP;
364 include "$IP/languages/Names.php";
365 }
366
367 if ( isset( $coreLanguageNames[$tag] )
368 || self::fetchLanguageName( $tag, $tag ) !== ''
369 ) {
370 return true;
371 }
372
373 return false;
374 }
375
376 /**
377 * @param string $code
378 * @return string Name of the language class
379 */
380 public static function classFromCode( $code ) {
381 if ( $code == 'en' ) {
382 return 'Language';
383 } else {
384 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
385 }
386 }
387
388 /**
389 * Includes language class files
390 *
391 * @param string $class Name of the language class
392 */
393 public static function preloadLanguageClass( $class ) {
394 global $IP;
395
396 if ( $class === 'Language' ) {
397 return;
398 }
399
400 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
401 include_once "$IP/languages/classes/$class.php";
402 }
403 }
404
405 /**
406 * Get the LocalisationCache instance
407 *
408 * @return LocalisationCache
409 */
410 public static function getLocalisationCache() {
411 if ( is_null( self::$dataCache ) ) {
412 global $wgLocalisationCacheConf;
413 $class = $wgLocalisationCacheConf['class'];
414 self::$dataCache = new $class( $wgLocalisationCacheConf );
415 }
416 return self::$dataCache;
417 }
418
419 function __construct() {
420 $this->mConverter = new FakeConverter( $this );
421 // Set the code to the name of the descendant
422 if ( get_class( $this ) == 'Language' ) {
423 $this->mCode = 'en';
424 } else {
425 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
426 }
427 self::getLocalisationCache();
428 }
429
430 /**
431 * Reduce memory usage
432 */
433 function __destruct() {
434 foreach ( $this as $name => $value ) {
435 unset( $this->$name );
436 }
437 }
438
439 /**
440 * Hook which will be called if this is the content language.
441 * Descendants can use this to register hook functions or modify globals
442 */
443 function initContLang() {
444 }
445
446 /**
447 * Same as getFallbacksFor for current language.
448 * @return array|bool
449 * @deprecated since 1.19
450 */
451 function getFallbackLanguageCode() {
452 wfDeprecated( __METHOD__, '1.19' );
453
454 return self::getFallbackFor( $this->mCode );
455 }
456
457 /**
458 * @return array
459 * @since 1.19
460 */
461 function getFallbackLanguages() {
462 return self::getFallbacksFor( $this->mCode );
463 }
464
465 /**
466 * Exports $wgBookstoreListEn
467 * @return array
468 */
469 function getBookstoreList() {
470 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
471 }
472
473 /**
474 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
475 * available in localised form, it will be included in English.
476 *
477 * @return array
478 */
479 public function getNamespaces() {
480 if ( is_null( $this->namespaceNames ) ) {
481 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
482
483 $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
484 $validNamespaces = MWNamespace::getCanonicalNamespaces();
485
486 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
487
488 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
489 if ( $wgMetaNamespaceTalk ) {
490 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
491 } else {
492 $talk = $this->namespaceNames[NS_PROJECT_TALK];
493 $this->namespaceNames[NS_PROJECT_TALK] =
494 $this->fixVariableInNamespace( $talk );
495 }
496
497 # Sometimes a language will be localised but not actually exist on this wiki.
498 foreach ( $this->namespaceNames as $key => $text ) {
499 if ( !isset( $validNamespaces[$key] ) ) {
500 unset( $this->namespaceNames[$key] );
501 }
502 }
503
504 # The above mixing may leave namespaces out of canonical order.
505 # Re-order by namespace ID number...
506 ksort( $this->namespaceNames );
507
508 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames ) );
509 }
510
511 return $this->namespaceNames;
512 }
513
514 /**
515 * Arbitrarily set all of the namespace names at once. Mainly used for testing
516 * @param array $namespaces Array of namespaces (id => name)
517 */
518 public function setNamespaces( array $namespaces ) {
519 $this->namespaceNames = $namespaces;
520 $this->mNamespaceIds = null;
521 }
522
523 /**
524 * Resets all of the namespace caches. Mainly used for testing
525 */
526 public function resetNamespaces() {
527 $this->namespaceNames = null;
528 $this->mNamespaceIds = null;
529 $this->namespaceAliases = null;
530 }
531
532 /**
533 * A convenience function that returns the same thing as
534 * getNamespaces() except with the array values changed to ' '
535 * where it found '_', useful for producing output to be displayed
536 * e.g. in <select> forms.
537 *
538 * @return array
539 */
540 function getFormattedNamespaces() {
541 $ns = $this->getNamespaces();
542 foreach ( $ns as $k => $v ) {
543 $ns[$k] = strtr( $v, '_', ' ' );
544 }
545 return $ns;
546 }
547
548 /**
549 * Get a namespace value by key
550 * <code>
551 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
552 * echo $mw_ns; // prints 'MediaWiki'
553 * </code>
554 *
555 * @param int $index The array key of the namespace to return
556 * @return string|bool String if the namespace value exists, otherwise false
557 */
558 function getNsText( $index ) {
559 $ns = $this->getNamespaces();
560
561 return isset( $ns[$index] ) ? $ns[$index] : false;
562 }
563
564 /**
565 * A convenience function that returns the same thing as
566 * getNsText() except with '_' changed to ' ', useful for
567 * producing output.
568 *
569 * <code>
570 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
571 * echo $mw_ns; // prints 'MediaWiki talk'
572 * </code>
573 *
574 * @param int $index The array key of the namespace to return
575 * @return string Namespace name without underscores (empty string if namespace does not exist)
576 */
577 function getFormattedNsText( $index ) {
578 $ns = $this->getNsText( $index );
579
580 return strtr( $ns, '_', ' ' );
581 }
582
583 /**
584 * Returns gender-dependent namespace alias if available.
585 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
586 * @param int $index Namespace index
587 * @param string $gender Gender key (male, female... )
588 * @return string
589 * @since 1.18
590 */
591 function getGenderNsText( $index, $gender ) {
592 global $wgExtraGenderNamespaces;
593
594 $ns = $wgExtraGenderNamespaces +
595 self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
596
597 return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
598 }
599
600 /**
601 * Whether this language uses gender-dependent namespace aliases.
602 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
603 * @return bool
604 * @since 1.18
605 */
606 function needsGenderDistinction() {
607 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
608 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
609 // $wgExtraGenderNamespaces overrides everything
610 return true;
611 } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
612 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
613 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
614 return false;
615 } else {
616 // Check what is in i18n files
617 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
618 return count( $aliases ) > 0;
619 }
620 }
621
622 /**
623 * Get a namespace key by value, case insensitive.
624 * Only matches namespace names for the current language, not the
625 * canonical ones defined in Namespace.php.
626 *
627 * @param string $text
628 * @return int|bool An integer if $text is a valid value otherwise false
629 */
630 function getLocalNsIndex( $text ) {
631 $lctext = $this->lc( $text );
632 $ids = $this->getNamespaceIds();
633 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
634 }
635
636 /**
637 * @return array
638 */
639 function getNamespaceAliases() {
640 if ( is_null( $this->namespaceAliases ) ) {
641 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
642 if ( !$aliases ) {
643 $aliases = array();
644 } else {
645 foreach ( $aliases as $name => $index ) {
646 if ( $index === NS_PROJECT_TALK ) {
647 unset( $aliases[$name] );
648 $name = $this->fixVariableInNamespace( $name );
649 $aliases[$name] = $index;
650 }
651 }
652 }
653
654 global $wgExtraGenderNamespaces;
655 $genders = $wgExtraGenderNamespaces +
656 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
657 foreach ( $genders as $index => $forms ) {
658 foreach ( $forms as $alias ) {
659 $aliases[$alias] = $index;
660 }
661 }
662
663 # Also add converted namespace names as aliases, to avoid confusion.
664 $convertedNames = array();
665 foreach ( $this->getVariants() as $variant ) {
666 if ( $variant === $this->mCode ) {
667 continue;
668 }
669 foreach ( $this->getNamespaces() as $ns => $_ ) {
670 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
671 }
672 }
673
674 $this->namespaceAliases = $aliases + $convertedNames;
675 }
676
677 return $this->namespaceAliases;
678 }
679
680 /**
681 * @return array
682 */
683 function getNamespaceIds() {
684 if ( is_null( $this->mNamespaceIds ) ) {
685 global $wgNamespaceAliases;
686 # Put namespace names and aliases into a hashtable.
687 # If this is too slow, then we should arrange it so that it is done
688 # before caching. The catch is that at pre-cache time, the above
689 # class-specific fixup hasn't been done.
690 $this->mNamespaceIds = array();
691 foreach ( $this->getNamespaces() as $index => $name ) {
692 $this->mNamespaceIds[$this->lc( $name )] = $index;
693 }
694 foreach ( $this->getNamespaceAliases() as $name => $index ) {
695 $this->mNamespaceIds[$this->lc( $name )] = $index;
696 }
697 if ( $wgNamespaceAliases ) {
698 foreach ( $wgNamespaceAliases as $name => $index ) {
699 $this->mNamespaceIds[$this->lc( $name )] = $index;
700 }
701 }
702 }
703 return $this->mNamespaceIds;
704 }
705
706 /**
707 * Get a namespace key by value, case insensitive. Canonical namespace
708 * names override custom ones defined for the current language.
709 *
710 * @param string $text
711 * @return int|bool An integer if $text is a valid value otherwise false
712 */
713 function getNsIndex( $text ) {
714 $lctext = $this->lc( $text );
715 $ns = MWNamespace::getCanonicalIndex( $lctext );
716 if ( $ns !== null ) {
717 return $ns;
718 }
719 $ids = $this->getNamespaceIds();
720 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
721 }
722
723 /**
724 * short names for language variants used for language conversion links.
725 *
726 * @param string $code
727 * @param bool $usemsg Use the "variantname-xyz" message if it exists
728 * @return string
729 */
730 function getVariantname( $code, $usemsg = true ) {
731 $msg = "variantname-$code";
732 if ( $usemsg && wfMessage( $msg )->exists() ) {
733 return $this->getMessageFromDB( $msg );
734 }
735 $name = self::fetchLanguageName( $code );
736 if ( $name ) {
737 return $name; # if it's defined as a language name, show that
738 } else {
739 # otherwise, output the language code
740 return $code;
741 }
742 }
743
744 /**
745 * @param string $name
746 * @return string
747 */
748 function specialPage( $name ) {
749 $aliases = $this->getSpecialPageAliases();
750 if ( isset( $aliases[$name][0] ) ) {
751 $name = $aliases[$name][0];
752 }
753 return $this->getNsText( NS_SPECIAL ) . ':' . $name;
754 }
755
756 /**
757 * @return array
758 */
759 function getDatePreferences() {
760 return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
761 }
762
763 /**
764 * @return array
765 */
766 function getDateFormats() {
767 return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
768 }
769
770 /**
771 * @return array|string
772 */
773 function getDefaultDateFormat() {
774 $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
775 if ( $df === 'dmy or mdy' ) {
776 global $wgAmericanDates;
777 return $wgAmericanDates ? 'mdy' : 'dmy';
778 } else {
779 return $df;
780 }
781 }
782
783 /**
784 * @return array
785 */
786 function getDatePreferenceMigrationMap() {
787 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
788 }
789
790 /**
791 * @param string $image
792 * @return array|null
793 */
794 function getImageFile( $image ) {
795 return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
796 }
797
798 /**
799 * @return array
800 */
801 function getExtraUserToggles() {
802 return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
803 }
804
805 /**
806 * @param string $tog
807 * @return string
808 */
809 function getUserToggle( $tog ) {
810 return $this->getMessageFromDB( "tog-$tog" );
811 }
812
813 /**
814 * Get native language names, indexed by code.
815 * Only those defined in MediaWiki, no other data like CLDR.
816 * If $customisedOnly is true, only returns codes with a messages file
817 *
818 * @param bool $customisedOnly
819 *
820 * @return array
821 * @deprecated since 1.20, use fetchLanguageNames()
822 */
823 public static function getLanguageNames( $customisedOnly = false ) {
824 return self::fetchLanguageNames( null, $customisedOnly ? 'mwfile' : 'mw' );
825 }
826
827 /**
828 * Get translated language names. This is done on best effort and
829 * by default this is exactly the same as Language::getLanguageNames.
830 * The CLDR extension provides translated names.
831 * @param string $code Language code.
832 * @return array Language code => language name
833 * @since 1.18.0
834 * @deprecated since 1.20, use fetchLanguageNames()
835 */
836 public static function getTranslatedLanguageNames( $code ) {
837 return self::fetchLanguageNames( $code, 'all' );
838 }
839
840 /**
841 * Get an array of language names, indexed by code.
842 * @param null|string $inLanguage Code of language in which to return the names
843 * Use null for autonyms (native names)
844 * @param string $include One of:
845 * 'all' all available languages
846 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
847 * 'mwfile' only if the language is in 'mw' *and* has a message file
848 * @return array Language code => language name
849 * @since 1.20
850 */
851 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
852 global $wgExtraLanguageNames;
853 static $coreLanguageNames;
854
855 if ( $coreLanguageNames === null ) {
856 global $IP;
857 include "$IP/languages/Names.php";
858 }
859
860 // If passed an invalid language code to use, fallback to en
861 if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
862 $inLanguage = 'en';
863 }
864
865 $names = array();
866
867 if ( $inLanguage ) {
868 # TODO: also include when $inLanguage is null, when this code is more efficient
869 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
870 }
871
872 $mwNames = $wgExtraLanguageNames + $coreLanguageNames;
873 foreach ( $mwNames as $mwCode => $mwName ) {
874 # - Prefer own MediaWiki native name when not using the hook
875 # - For other names just add if not added through the hook
876 if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
877 $names[$mwCode] = $mwName;
878 }
879 }
880
881 if ( $include === 'all' ) {
882 return $names;
883 }
884
885 $returnMw = array();
886 $coreCodes = array_keys( $mwNames );
887 foreach ( $coreCodes as $coreCode ) {
888 $returnMw[$coreCode] = $names[$coreCode];
889 }
890
891 if ( $include === 'mwfile' ) {
892 $namesMwFile = array();
893 # We do this using a foreach over the codes instead of a directory
894 # loop so that messages files in extensions will work correctly.
895 foreach ( $returnMw as $code => $value ) {
896 if ( is_readable( self::getMessagesFileName( $code ) )
897 || is_readable( self::getJsonMessagesFileName( $code ) )
898 ) {
899 $namesMwFile[$code] = $names[$code];
900 }
901 }
902
903 return $namesMwFile;
904 }
905
906 # 'mw' option; default if it's not one of the other two options (all/mwfile)
907 return $returnMw;
908 }
909
910 /**
911 * @param string $code The code of the language for which to get the name
912 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
913 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
914 * @return string Language name or empty
915 * @since 1.20
916 */
917 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
918 $code = strtolower( $code );
919 $array = self::fetchLanguageNames( $inLanguage, $include );
920 return !array_key_exists( $code, $array ) ? '' : $array[$code];
921 }
922
923 /**
924 * Get a message from the MediaWiki namespace.
925 *
926 * @param string $msg Message name
927 * @return string
928 */
929 function getMessageFromDB( $msg ) {
930 return wfMessage( $msg )->inLanguage( $this )->text();
931 }
932
933 /**
934 * Get the native language name of $code.
935 * Only if defined in MediaWiki, no other data like CLDR.
936 * @param string $code
937 * @return string
938 * @deprecated since 1.20, use fetchLanguageName()
939 */
940 function getLanguageName( $code ) {
941 return self::fetchLanguageName( $code );
942 }
943
944 /**
945 * @param string $key
946 * @return string
947 */
948 function getMonthName( $key ) {
949 return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
950 }
951
952 /**
953 * @return array
954 */
955 function getMonthNamesArray() {
956 $monthNames = array( '' );
957 for ( $i = 1; $i < 13; $i++ ) {
958 $monthNames[] = $this->getMonthName( $i );
959 }
960 return $monthNames;
961 }
962
963 /**
964 * @param string $key
965 * @return string
966 */
967 function getMonthNameGen( $key ) {
968 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
969 }
970
971 /**
972 * @param string $key
973 * @return string
974 */
975 function getMonthAbbreviation( $key ) {
976 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
977 }
978
979 /**
980 * @return array
981 */
982 function getMonthAbbreviationsArray() {
983 $monthNames = array( '' );
984 for ( $i = 1; $i < 13; $i++ ) {
985 $monthNames[] = $this->getMonthAbbreviation( $i );
986 }
987 return $monthNames;
988 }
989
990 /**
991 * @param string $key
992 * @return string
993 */
994 function getWeekdayName( $key ) {
995 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
996 }
997
998 /**
999 * @param string $key
1000 * @return string
1001 */
1002 function getWeekdayAbbreviation( $key ) {
1003 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
1004 }
1005
1006 /**
1007 * @param string $key
1008 * @return string
1009 */
1010 function getIranianCalendarMonthName( $key ) {
1011 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
1012 }
1013
1014 /**
1015 * @param string $key
1016 * @return string
1017 */
1018 function getHebrewCalendarMonthName( $key ) {
1019 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
1020 }
1021
1022 /**
1023 * @param string $key
1024 * @return string
1025 */
1026 function getHebrewCalendarMonthNameGen( $key ) {
1027 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1028 }
1029
1030 /**
1031 * @param string $key
1032 * @return string
1033 */
1034 function getHijriCalendarMonthName( $key ) {
1035 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1036 }
1037
1038 /**
1039 * Pass through result from $dateTimeObj->format()
1040 */
1041 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1042 if ( !$dateTimeObj ) {
1043 $dateTimeObj = DateTime::createFromFormat(
1044 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1045 );
1046 }
1047 return $dateTimeObj->format( $code );
1048 }
1049
1050 /**
1051 * This is a workalike of PHP's date() function, but with better
1052 * internationalisation, a reduced set of format characters, and a better
1053 * escaping format.
1054 *
1055 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1056 * the PHP manual for definitions. There are a number of extensions, which
1057 * start with "x":
1058 *
1059 * xn Do not translate digits of the next numeric format character
1060 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
1061 * xr Use roman numerals for the next numeric format character
1062 * xh Use hebrew numerals for the next numeric format character
1063 * xx Literal x
1064 * xg Genitive month name
1065 *
1066 * xij j (day number) in Iranian calendar
1067 * xiF F (month name) in Iranian calendar
1068 * xin n (month number) in Iranian calendar
1069 * xiy y (two digit year) in Iranian calendar
1070 * xiY Y (full year) in Iranian calendar
1071 *
1072 * xjj j (day number) in Hebrew calendar
1073 * xjF F (month name) in Hebrew calendar
1074 * xjt t (days in month) in Hebrew calendar
1075 * xjx xg (genitive month name) in Hebrew calendar
1076 * xjn n (month number) in Hebrew calendar
1077 * xjY Y (full year) in Hebrew calendar
1078 *
1079 * xmj j (day number) in Hijri calendar
1080 * xmF F (month name) in Hijri calendar
1081 * xmn n (month number) in Hijri calendar
1082 * xmY Y (full year) in Hijri calendar
1083 *
1084 * xkY Y (full year) in Thai solar calendar. Months and days are
1085 * identical to the Gregorian calendar
1086 * xoY Y (full year) in Minguo calendar or Juche year.
1087 * Months and days are identical to the
1088 * Gregorian calendar
1089 * xtY Y (full year) in Japanese nengo. Months and days are
1090 * identical to the Gregorian calendar
1091 *
1092 * Characters enclosed in double quotes will be considered literal (with
1093 * the quotes themselves removed). Unmatched quotes will be considered
1094 * literal quotes. Example:
1095 *
1096 * "The month is" F => The month is January
1097 * i's" => 20'11"
1098 *
1099 * Backslash escaping is also supported.
1100 *
1101 * Input timestamp is assumed to be pre-normalized to the desired local
1102 * time zone, if any. Note that the format characters crUeIOPTZ will assume
1103 * $ts is UTC if $zone is not given.
1104 *
1105 * @param string $format
1106 * @param string $ts 14-character timestamp
1107 * YYYYMMDDHHMMSS
1108 * 01234567890123
1109 * @param DateTimeZone $zone Timezone of $ts
1110 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
1111 * Only makes sense if $ts is the current time.
1112 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1113 *
1114 * @throws MWException
1115 * @return string
1116 */
1117 function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = null ) {
1118 $s = '';
1119 $raw = false;
1120 $roman = false;
1121 $hebrewNum = false;
1122 $dateTimeObj = false;
1123 $rawToggle = false;
1124 $iranian = false;
1125 $hebrew = false;
1126 $hijri = false;
1127 $thai = false;
1128 $minguo = false;
1129 $tenno = false;
1130
1131 $usedSecond = false;
1132 $usedMinute = false;
1133 $usedHour = false;
1134 $usedAMPM = false;
1135 $usedDay = false;
1136 $usedWeek = false;
1137 $usedMonth = false;
1138 $usedYear = false;
1139 $usedISOYear = false;
1140 $usedIsLeapYear = false;
1141
1142 $usedHebrewMonth = false;
1143 $usedIranianMonth = false;
1144 $usedHijriMonth = false;
1145 $usedHebrewYear = false;
1146 $usedIranianYear = false;
1147 $usedHijriYear = false;
1148 $usedTennoYear = false;
1149
1150 if ( strlen( $ts ) !== 14 ) {
1151 throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1152 }
1153
1154 if ( !ctype_digit( $ts ) ) {
1155 throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1156 }
1157
1158 $formatLength = strlen( $format );
1159 for ( $p = 0; $p < $formatLength; $p++ ) {
1160 $num = false;
1161 $code = $format[$p];
1162 if ( $code == 'x' && $p < $formatLength - 1 ) {
1163 $code .= $format[++$p];
1164 }
1165
1166 if ( ( $code === 'xi'
1167 || $code === 'xj'
1168 || $code === 'xk'
1169 || $code === 'xm'
1170 || $code === 'xo'
1171 || $code === 'xt' )
1172 && $p < $formatLength - 1 ) {
1173 $code .= $format[++$p];
1174 }
1175
1176 switch ( $code ) {
1177 case 'xx':
1178 $s .= 'x';
1179 break;
1180 case 'xn':
1181 $raw = true;
1182 break;
1183 case 'xN':
1184 $rawToggle = !$rawToggle;
1185 break;
1186 case 'xr':
1187 $roman = true;
1188 break;
1189 case 'xh':
1190 $hebrewNum = true;
1191 break;
1192 case 'xg':
1193 $usedMonth = true;
1194 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1195 break;
1196 case 'xjx':
1197 $usedHebrewMonth = true;
1198 if ( !$hebrew ) {
1199 $hebrew = self::tsToHebrew( $ts );
1200 }
1201 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1202 break;
1203 case 'd':
1204 $usedDay = true;
1205 $num = substr( $ts, 6, 2 );
1206 break;
1207 case 'D':
1208 $usedDay = true;
1209 $s .= $this->getWeekdayAbbreviation( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1 );
1210 break;
1211 case 'j':
1212 $usedDay = true;
1213 $num = intval( substr( $ts, 6, 2 ) );
1214 break;
1215 case 'xij':
1216 $usedDay = true;
1217 if ( !$iranian ) {
1218 $iranian = self::tsToIranian( $ts );
1219 }
1220 $num = $iranian[2];
1221 break;
1222 case 'xmj':
1223 $usedDay = true;
1224 if ( !$hijri ) {
1225 $hijri = self::tsToHijri( $ts );
1226 }
1227 $num = $hijri[2];
1228 break;
1229 case 'xjj':
1230 $usedDay = true;
1231 if ( !$hebrew ) {
1232 $hebrew = self::tsToHebrew( $ts );
1233 }
1234 $num = $hebrew[2];
1235 break;
1236 case 'l':
1237 $usedDay = true;
1238 $s .= $this->getWeekdayName( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1 );
1239 break;
1240 case 'F':
1241 $usedMonth = true;
1242 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1243 break;
1244 case 'xiF':
1245 $usedIranianMonth = true;
1246 if ( !$iranian ) {
1247 $iranian = self::tsToIranian( $ts );
1248 }
1249 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1250 break;
1251 case 'xmF':
1252 $usedHijriMonth = true;
1253 if ( !$hijri ) {
1254 $hijri = self::tsToHijri( $ts );
1255 }
1256 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1257 break;
1258 case 'xjF':
1259 $usedHebrewMonth = true;
1260 if ( !$hebrew ) {
1261 $hebrew = self::tsToHebrew( $ts );
1262 }
1263 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1264 break;
1265 case 'm':
1266 $usedMonth = true;
1267 $num = substr( $ts, 4, 2 );
1268 break;
1269 case 'M':
1270 $usedMonth = true;
1271 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1272 break;
1273 case 'n':
1274 $usedMonth = true;
1275 $num = intval( substr( $ts, 4, 2 ) );
1276 break;
1277 case 'xin':
1278 $usedIranianMonth = true;
1279 if ( !$iranian ) {
1280 $iranian = self::tsToIranian( $ts );
1281 }
1282 $num = $iranian[1];
1283 break;
1284 case 'xmn':
1285 $usedHijriMonth = true;
1286 if ( !$hijri ) {
1287 $hijri = self::tsToHijri ( $ts );
1288 }
1289 $num = $hijri[1];
1290 break;
1291 case 'xjn':
1292 $usedHebrewMonth = true;
1293 if ( !$hebrew ) {
1294 $hebrew = self::tsToHebrew( $ts );
1295 }
1296 $num = $hebrew[1];
1297 break;
1298 case 'xjt':
1299 $usedHebrewMonth = true;
1300 if ( !$hebrew ) {
1301 $hebrew = self::tsToHebrew( $ts );
1302 }
1303 $num = $hebrew[3];
1304 break;
1305 case 'Y':
1306 $usedYear = true;
1307 $num = substr( $ts, 0, 4 );
1308 break;
1309 case 'xiY':
1310 $usedIranianYear = true;
1311 if ( !$iranian ) {
1312 $iranian = self::tsToIranian( $ts );
1313 }
1314 $num = $iranian[0];
1315 break;
1316 case 'xmY':
1317 $usedHijriYear = true;
1318 if ( !$hijri ) {
1319 $hijri = self::tsToHijri( $ts );
1320 }
1321 $num = $hijri[0];
1322 break;
1323 case 'xjY':
1324 $usedHebrewYear = true;
1325 if ( !$hebrew ) {
1326 $hebrew = self::tsToHebrew( $ts );
1327 }
1328 $num = $hebrew[0];
1329 break;
1330 case 'xkY':
1331 $usedYear = true;
1332 if ( !$thai ) {
1333 $thai = self::tsToYear( $ts, 'thai' );
1334 }
1335 $num = $thai[0];
1336 break;
1337 case 'xoY':
1338 $usedYear = true;
1339 if ( !$minguo ) {
1340 $minguo = self::tsToYear( $ts, 'minguo' );
1341 }
1342 $num = $minguo[0];
1343 break;
1344 case 'xtY':
1345 $usedTennoYear = true;
1346 if ( !$tenno ) {
1347 $tenno = self::tsToYear( $ts, 'tenno' );
1348 }
1349 $num = $tenno[0];
1350 break;
1351 case 'y':
1352 $usedYear = true;
1353 $num = substr( $ts, 2, 2 );
1354 break;
1355 case 'xiy':
1356 $usedIranianYear = true;
1357 if ( !$iranian ) {
1358 $iranian = self::tsToIranian( $ts );
1359 }
1360 $num = substr( $iranian[0], -2 );
1361 break;
1362 case 'a':
1363 $usedAMPM = true;
1364 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1365 break;
1366 case 'A':
1367 $usedAMPM = true;
1368 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1369 break;
1370 case 'g':
1371 $usedHour = true;
1372 $h = substr( $ts, 8, 2 );
1373 $num = $h % 12 ? $h % 12 : 12;
1374 break;
1375 case 'G':
1376 $usedHour = true;
1377 $num = intval( substr( $ts, 8, 2 ) );
1378 break;
1379 case 'h':
1380 $usedHour = true;
1381 $h = substr( $ts, 8, 2 );
1382 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1383 break;
1384 case 'H':
1385 $usedHour = true;
1386 $num = substr( $ts, 8, 2 );
1387 break;
1388 case 'i':
1389 $usedMinute = true;
1390 $num = substr( $ts, 10, 2 );
1391 break;
1392 case 's':
1393 $usedSecond = true;
1394 $num = substr( $ts, 12, 2 );
1395 break;
1396 case 'c':
1397 case 'r':
1398 $usedSecond = true;
1399 // fall through
1400 case 'e':
1401 case 'O':
1402 case 'P':
1403 case 'T':
1404 $s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1405 break;
1406 case 'w':
1407 case 'N':
1408 case 'z':
1409 $usedDay = true;
1410 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1411 break;
1412 case 'W':
1413 $usedWeek = true;
1414 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1415 break;
1416 case 't':
1417 $usedMonth = true;
1418 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1419 break;
1420 case 'L':
1421 $usedIsLeapYear = true;
1422 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1423 break;
1424 case 'o':
1425 $usedISOYear = true;
1426 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1427 break;
1428 case 'U':
1429 $usedSecond = true;
1430 // fall through
1431 case 'I':
1432 case 'Z':
1433 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1434 break;
1435 case '\\':
1436 # Backslash escaping
1437 if ( $p < $formatLength - 1 ) {
1438 $s .= $format[++$p];
1439 } else {
1440 $s .= '\\';
1441 }
1442 break;
1443 case '"':
1444 # Quoted literal
1445 if ( $p < $formatLength - 1 ) {
1446 $endQuote = strpos( $format, '"', $p + 1 );
1447 if ( $endQuote === false ) {
1448 # No terminating quote, assume literal "
1449 $s .= '"';
1450 } else {
1451 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1452 $p = $endQuote;
1453 }
1454 } else {
1455 # Quote at end of string, assume literal "
1456 $s .= '"';
1457 }
1458 break;
1459 default:
1460 $s .= $format[$p];
1461 }
1462 if ( $num !== false ) {
1463 if ( $rawToggle || $raw ) {
1464 $s .= $num;
1465 $raw = false;
1466 } elseif ( $roman ) {
1467 $s .= Language::romanNumeral( $num );
1468 $roman = false;
1469 } elseif ( $hebrewNum ) {
1470 $s .= self::hebrewNumeral( $num );
1471 $hebrewNum = false;
1472 } else {
1473 $s .= $this->formatNum( $num, true );
1474 }
1475 }
1476 }
1477
1478 if ( $usedSecond ) {
1479 $ttl = 1;
1480 } elseif ( $usedMinute ) {
1481 $ttl = 60 - substr( $ts, 12, 2 );
1482 } elseif ( $usedHour ) {
1483 $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1484 } elseif ( $usedAMPM ) {
1485 $ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1486 } elseif ( $usedDay || $usedHebrewMonth || $usedIranianMonth || $usedHijriMonth || $usedHebrewYear || $usedIranianYear || $usedHijriYear || $usedTennoYear ) {
1487 // @todo Someone who understands the non-Gregorian calendars should write proper logic for them
1488 // so that they don't need purged every day.
1489 $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1490 } else {
1491 $possibleTtls = array();
1492 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1493 if ( $usedWeek ) {
1494 $possibleTtls[] = ( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 + $timeRemainingInDay;
1495 } elseif ( $usedISOYear ) {
1496 // December 28th falls on the last ISO week of the year, every year.
1497 // The last ISO week of a year can be 52 or 53.
1498 $lastWeekOfISOYear = DateTime::createFromFormat( 'Ymd', substr( $ts, 0, 4 ) . '1228', $zone ?: new DateTimeZone( 'UTC' ) )->format( 'W' );
1499 $currentISOWeek = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1500 $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1501 $timeRemainingInWeek = ( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 + $timeRemainingInDay;
1502 $possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1503 }
1504
1505 if ( $usedMonth ) {
1506 $possibleTtls[] = ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) - substr( $ts, 6, 2 ) ) * 86400 + $timeRemainingInDay;
1507 } elseif ( $usedYear ) {
1508 $possibleTtls[] = ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1509 + $timeRemainingInDay;
1510 } elseif ( $usedIsLeapYear ) {
1511 $year = substr( $ts, 0, 4 );
1512 $timeRemainingInYear = ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1513 + $timeRemainingInDay;
1514 $mod = $year % 4;
1515 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1516 // this isn't a leap year. see when the next one starts
1517 $nextCandidate = $year - $mod + 4;
1518 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1519 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 + $timeRemainingInYear;
1520 } else {
1521 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 + $timeRemainingInYear;
1522 }
1523 } else {
1524 // this is a leap year, so the next year isn't
1525 $possibleTtls[] = $timeRemainingInYear;
1526 }
1527 }
1528
1529 if ( $possibleTtls ) {
1530 $ttl = min( $possibleTtls );
1531 }
1532 }
1533
1534 return $s;
1535 }
1536
1537 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1538 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1539
1540 /**
1541 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1542 * Gregorian dates to Iranian dates. Originally written in C, it
1543 * is released under the terms of GNU Lesser General Public
1544 * License. Conversion to PHP was performed by Niklas Laxström.
1545 *
1546 * Link: http://www.farsiweb.info/jalali/jalali.c
1547 *
1548 * @param string $ts
1549 *
1550 * @return string
1551 */
1552 private static function tsToIranian( $ts ) {
1553 $gy = substr( $ts, 0, 4 ) -1600;
1554 $gm = substr( $ts, 4, 2 ) -1;
1555 $gd = substr( $ts, 6, 2 ) -1;
1556
1557 # Days passed from the beginning (including leap years)
1558 $gDayNo = 365 * $gy
1559 + floor( ( $gy + 3 ) / 4 )
1560 - floor( ( $gy + 99 ) / 100 )
1561 + floor( ( $gy + 399 ) / 400 );
1562
1563 // Add days of the past months of this year
1564 for ( $i = 0; $i < $gm; $i++ ) {
1565 $gDayNo += self::$GREG_DAYS[$i];
1566 }
1567
1568 // Leap years
1569 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1570 $gDayNo++;
1571 }
1572
1573 // Days passed in current month
1574 $gDayNo += (int)$gd;
1575
1576 $jDayNo = $gDayNo - 79;
1577
1578 $jNp = floor( $jDayNo / 12053 );
1579 $jDayNo %= 12053;
1580
1581 $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1582 $jDayNo %= 1461;
1583
1584 if ( $jDayNo >= 366 ) {
1585 $jy += floor( ( $jDayNo - 1 ) / 365 );
1586 $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1587 }
1588
1589 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1590 $jDayNo -= self::$IRANIAN_DAYS[$i];
1591 }
1592
1593 $jm = $i + 1;
1594 $jd = $jDayNo + 1;
1595
1596 return array( $jy, $jm, $jd );
1597 }
1598
1599 /**
1600 * Converting Gregorian dates to Hijri dates.
1601 *
1602 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1603 *
1604 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1605 *
1606 * @param string $ts
1607 *
1608 * @return string
1609 */
1610 private static function tsToHijri( $ts ) {
1611 $year = substr( $ts, 0, 4 );
1612 $month = substr( $ts, 4, 2 );
1613 $day = substr( $ts, 6, 2 );
1614
1615 $zyr = $year;
1616 $zd = $day;
1617 $zm = $month;
1618 $zy = $zyr;
1619
1620 if (
1621 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1622 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1623 ) {
1624 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1625 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1626 (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1627 $zd - 32075;
1628 } else {
1629 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1630 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1631 }
1632
1633 $zl = $zjd -1948440 + 10632;
1634 $zn = (int)( ( $zl - 1 ) / 10631 );
1635 $zl = $zl - 10631 * $zn + 354;
1636 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1637 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1638 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1639 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1640 $zm = (int)( ( 24 * $zl ) / 709 );
1641 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1642 $zy = 30 * $zn + $zj - 30;
1643
1644 return array( $zy, $zm, $zd );
1645 }
1646
1647 /**
1648 * Converting Gregorian dates to Hebrew dates.
1649 *
1650 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1651 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1652 * to translate the relevant functions into PHP and release them under
1653 * GNU GPL.
1654 *
1655 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1656 * and Adar II is 14. In a non-leap year, Adar is 6.
1657 *
1658 * @param string $ts
1659 *
1660 * @return string
1661 */
1662 private static function tsToHebrew( $ts ) {
1663 # Parse date
1664 $year = substr( $ts, 0, 4 );
1665 $month = substr( $ts, 4, 2 );
1666 $day = substr( $ts, 6, 2 );
1667
1668 # Calculate Hebrew year
1669 $hebrewYear = $year + 3760;
1670
1671 # Month number when September = 1, August = 12
1672 $month += 4;
1673 if ( $month > 12 ) {
1674 # Next year
1675 $month -= 12;
1676 $year++;
1677 $hebrewYear++;
1678 }
1679
1680 # Calculate day of year from 1 September
1681 $dayOfYear = $day;
1682 for ( $i = 1; $i < $month; $i++ ) {
1683 if ( $i == 6 ) {
1684 # February
1685 $dayOfYear += 28;
1686 # Check if the year is leap
1687 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1688 $dayOfYear++;
1689 }
1690 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1691 $dayOfYear += 30;
1692 } else {
1693 $dayOfYear += 31;
1694 }
1695 }
1696
1697 # Calculate the start of the Hebrew year
1698 $start = self::hebrewYearStart( $hebrewYear );
1699
1700 # Calculate next year's start
1701 if ( $dayOfYear <= $start ) {
1702 # Day is before the start of the year - it is the previous year
1703 # Next year's start
1704 $nextStart = $start;
1705 # Previous year
1706 $year--;
1707 $hebrewYear--;
1708 # Add days since previous year's 1 September
1709 $dayOfYear += 365;
1710 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1711 # Leap year
1712 $dayOfYear++;
1713 }
1714 # Start of the new (previous) year
1715 $start = self::hebrewYearStart( $hebrewYear );
1716 } else {
1717 # Next year's start
1718 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1719 }
1720
1721 # Calculate Hebrew day of year
1722 $hebrewDayOfYear = $dayOfYear - $start;
1723
1724 # Difference between year's days
1725 $diff = $nextStart - $start;
1726 # Add 12 (or 13 for leap years) days to ignore the difference between
1727 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1728 # difference is only about the year type
1729 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1730 $diff += 13;
1731 } else {
1732 $diff += 12;
1733 }
1734
1735 # Check the year pattern, and is leap year
1736 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1737 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1738 # and non-leap years
1739 $yearPattern = $diff % 30;
1740 # Check if leap year
1741 $isLeap = $diff >= 30;
1742
1743 # Calculate day in the month from number of day in the Hebrew year
1744 # Don't check Adar - if the day is not in Adar, we will stop before;
1745 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1746 $hebrewDay = $hebrewDayOfYear;
1747 $hebrewMonth = 1;
1748 $days = 0;
1749 while ( $hebrewMonth <= 12 ) {
1750 # Calculate days in this month
1751 if ( $isLeap && $hebrewMonth == 6 ) {
1752 # Adar in a leap year
1753 if ( $isLeap ) {
1754 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1755 $days = 30;
1756 if ( $hebrewDay <= $days ) {
1757 # Day in Adar I
1758 $hebrewMonth = 13;
1759 } else {
1760 # Subtract the days of Adar I
1761 $hebrewDay -= $days;
1762 # Try Adar II
1763 $days = 29;
1764 if ( $hebrewDay <= $days ) {
1765 # Day in Adar II
1766 $hebrewMonth = 14;
1767 }
1768 }
1769 }
1770 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1771 # Cheshvan in a complete year (otherwise as the rule below)
1772 $days = 30;
1773 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1774 # Kislev in an incomplete year (otherwise as the rule below)
1775 $days = 29;
1776 } else {
1777 # Odd months have 30 days, even have 29
1778 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1779 }
1780 if ( $hebrewDay <= $days ) {
1781 # In the current month
1782 break;
1783 } else {
1784 # Subtract the days of the current month
1785 $hebrewDay -= $days;
1786 # Try in the next month
1787 $hebrewMonth++;
1788 }
1789 }
1790
1791 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1792 }
1793
1794 /**
1795 * This calculates the Hebrew year start, as days since 1 September.
1796 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1797 * Used for Hebrew date.
1798 *
1799 * @param int $year
1800 *
1801 * @return string
1802 */
1803 private static function hebrewYearStart( $year ) {
1804 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1805 $b = intval( ( $year - 1 ) % 4 );
1806 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1807 if ( $m < 0 ) {
1808 $m--;
1809 }
1810 $Mar = intval( $m );
1811 if ( $m < 0 ) {
1812 $m++;
1813 }
1814 $m -= $Mar;
1815
1816 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1817 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1818 $Mar++;
1819 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1820 $Mar += 2;
1821 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1822 $Mar++;
1823 }
1824
1825 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1826 return $Mar;
1827 }
1828
1829 /**
1830 * Algorithm to convert Gregorian dates to Thai solar dates,
1831 * Minguo dates or Minguo dates.
1832 *
1833 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1834 * http://en.wikipedia.org/wiki/Minguo_calendar
1835 * http://en.wikipedia.org/wiki/Japanese_era_name
1836 *
1837 * @param string $ts 14-character timestamp
1838 * @param string $cName Calender name
1839 * @return array Converted year, month, day
1840 */
1841 private static function tsToYear( $ts, $cName ) {
1842 $gy = substr( $ts, 0, 4 );
1843 $gm = substr( $ts, 4, 2 );
1844 $gd = substr( $ts, 6, 2 );
1845
1846 if ( !strcmp( $cName, 'thai' ) ) {
1847 # Thai solar dates
1848 # Add 543 years to the Gregorian calendar
1849 # Months and days are identical
1850 $gy_offset = $gy + 543;
1851 } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1852 # Minguo dates
1853 # Deduct 1911 years from the Gregorian calendar
1854 # Months and days are identical
1855 $gy_offset = $gy - 1911;
1856 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1857 # Nengō dates up to Meiji period
1858 # Deduct years from the Gregorian calendar
1859 # depending on the nengo periods
1860 # Months and days are identical
1861 if ( ( $gy < 1912 )
1862 || ( ( $gy == 1912 ) && ( $gm < 7 ) )
1863 || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1864 ) {
1865 # Meiji period
1866 $gy_gannen = $gy - 1868 + 1;
1867 $gy_offset = $gy_gannen;
1868 if ( $gy_gannen == 1 ) {
1869 $gy_offset = '元';
1870 }
1871 $gy_offset = '明治' . $gy_offset;
1872 } elseif (
1873 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1874 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1875 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1876 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1877 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1878 ) {
1879 # Taishō period
1880 $gy_gannen = $gy - 1912 + 1;
1881 $gy_offset = $gy_gannen;
1882 if ( $gy_gannen == 1 ) {
1883 $gy_offset = '元';
1884 }
1885 $gy_offset = '大正' . $gy_offset;
1886 } elseif (
1887 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1888 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1889 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1890 ) {
1891 # Shōwa period
1892 $gy_gannen = $gy - 1926 + 1;
1893 $gy_offset = $gy_gannen;
1894 if ( $gy_gannen == 1 ) {
1895 $gy_offset = '元';
1896 }
1897 $gy_offset = '昭和' . $gy_offset;
1898 } else {
1899 # Heisei period
1900 $gy_gannen = $gy - 1989 + 1;
1901 $gy_offset = $gy_gannen;
1902 if ( $gy_gannen == 1 ) {
1903 $gy_offset = '元';
1904 }
1905 $gy_offset = '平成' . $gy_offset;
1906 }
1907 } else {
1908 $gy_offset = $gy;
1909 }
1910
1911 return array( $gy_offset, $gm, $gd );
1912 }
1913
1914 /**
1915 * Roman number formatting up to 10000
1916 *
1917 * @param int $num
1918 *
1919 * @return string
1920 */
1921 static function romanNumeral( $num ) {
1922 static $table = array(
1923 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1924 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1925 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1926 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1927 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
1928 );
1929
1930 $num = intval( $num );
1931 if ( $num > 10000 || $num <= 0 ) {
1932 return $num;
1933 }
1934
1935 $s = '';
1936 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1937 if ( $num >= $pow10 ) {
1938 $s .= $table[$i][(int)floor( $num / $pow10 )];
1939 }
1940 $num = $num % $pow10;
1941 }
1942 return $s;
1943 }
1944
1945 /**
1946 * Hebrew Gematria number formatting up to 9999
1947 *
1948 * @param int $num
1949 *
1950 * @return string
1951 */
1952 static function hebrewNumeral( $num ) {
1953 static $table = array(
1954 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1955 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1956 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1957 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1958 );
1959
1960 $num = intval( $num );
1961 if ( $num > 9999 || $num <= 0 ) {
1962 return $num;
1963 }
1964
1965 $s = '';
1966 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1967 if ( $num >= $pow10 ) {
1968 if ( $num == 15 || $num == 16 ) {
1969 $s .= $table[0][9] . $table[0][$num - 9];
1970 $num = 0;
1971 } else {
1972 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1973 if ( $pow10 == 1000 ) {
1974 $s .= "'";
1975 }
1976 }
1977 }
1978 $num = $num % $pow10;
1979 }
1980 if ( strlen( $s ) == 2 ) {
1981 $str = $s . "'";
1982 } else {
1983 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1984 $str .= substr( $s, strlen( $s ) - 2, 2 );
1985 }
1986 $start = substr( $str, 0, strlen( $str ) - 2 );
1987 $end = substr( $str, strlen( $str ) - 2 );
1988 switch ( $end ) {
1989 case 'כ':
1990 $str = $start . 'ך';
1991 break;
1992 case 'מ':
1993 $str = $start . 'ם';
1994 break;
1995 case 'נ':
1996 $str = $start . 'ן';
1997 break;
1998 case 'פ':
1999 $str = $start . 'ף';
2000 break;
2001 case 'צ':
2002 $str = $start . 'ץ';
2003 break;
2004 }
2005 return $str;
2006 }
2007
2008 /**
2009 * Used by date() and time() to adjust the time output.
2010 *
2011 * @param string $ts The time in date('YmdHis') format
2012 * @param mixed $tz Adjust the time by this amount (default false, mean we
2013 * get user timecorrection setting)
2014 * @return int
2015 */
2016 function userAdjust( $ts, $tz = false ) {
2017 global $wgUser, $wgLocalTZoffset;
2018
2019 if ( $tz === false ) {
2020 $tz = $wgUser->getOption( 'timecorrection' );
2021 }
2022
2023 $data = explode( '|', $tz, 3 );
2024
2025 if ( $data[0] == 'ZoneInfo' ) {
2026 wfSuppressWarnings();
2027 $userTZ = timezone_open( $data[2] );
2028 wfRestoreWarnings();
2029 if ( $userTZ !== false ) {
2030 $date = date_create( $ts, timezone_open( 'UTC' ) );
2031 date_timezone_set( $date, $userTZ );
2032 $date = date_format( $date, 'YmdHis' );
2033 return $date;
2034 }
2035 # Unrecognized timezone, default to 'Offset' with the stored offset.
2036 $data[0] = 'Offset';
2037 }
2038
2039 if ( $data[0] == 'System' || $tz == '' ) {
2040 # Global offset in minutes.
2041 $minDiff = $wgLocalTZoffset;
2042 } elseif ( $data[0] == 'Offset' ) {
2043 $minDiff = intval( $data[1] );
2044 } else {
2045 $data = explode( ':', $tz );
2046 if ( count( $data ) == 2 ) {
2047 $data[0] = intval( $data[0] );
2048 $data[1] = intval( $data[1] );
2049 $minDiff = abs( $data[0] ) * 60 + $data[1];
2050 if ( $data[0] < 0 ) {
2051 $minDiff = -$minDiff;
2052 }
2053 } else {
2054 $minDiff = intval( $data[0] ) * 60;
2055 }
2056 }
2057
2058 # No difference ? Return time unchanged
2059 if ( 0 == $minDiff ) {
2060 return $ts;
2061 }
2062
2063 wfSuppressWarnings(); // E_STRICT system time bitching
2064 # Generate an adjusted date; take advantage of the fact that mktime
2065 # will normalize out-of-range values so we don't have to split $minDiff
2066 # into hours and minutes.
2067 $t = mktime( (
2068 (int)substr( $ts, 8, 2 ) ), # Hours
2069 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2070 (int)substr( $ts, 12, 2 ), # Seconds
2071 (int)substr( $ts, 4, 2 ), # Month
2072 (int)substr( $ts, 6, 2 ), # Day
2073 (int)substr( $ts, 0, 4 ) ); # Year
2074
2075 $date = date( 'YmdHis', $t );
2076 wfRestoreWarnings();
2077
2078 return $date;
2079 }
2080
2081 /**
2082 * This is meant to be used by time(), date(), and timeanddate() to get
2083 * the date preference they're supposed to use, it should be used in
2084 * all children.
2085 *
2086 *<code>
2087 * function timeanddate([...], $format = true) {
2088 * $datePreference = $this->dateFormat($format);
2089 * [...]
2090 * }
2091 *</code>
2092 *
2093 * @param int|string|bool $usePrefs If true, the user's preference is used
2094 * if false, the site/language default is used
2095 * if int/string, assumed to be a format.
2096 * @return string
2097 */
2098 function dateFormat( $usePrefs = true ) {
2099 global $wgUser;
2100
2101 if ( is_bool( $usePrefs ) ) {
2102 if ( $usePrefs ) {
2103 $datePreference = $wgUser->getDatePreference();
2104 } else {
2105 $datePreference = (string)User::getDefaultOption( 'date' );
2106 }
2107 } else {
2108 $datePreference = (string)$usePrefs;
2109 }
2110
2111 // return int
2112 if ( $datePreference == '' ) {
2113 return 'default';
2114 }
2115
2116 return $datePreference;
2117 }
2118
2119 /**
2120 * Get a format string for a given type and preference
2121 * @param string $type May be date, time or both
2122 * @param string $pref The format name as it appears in Messages*.php
2123 *
2124 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2125 *
2126 * @return string
2127 */
2128 function getDateFormatString( $type, $pref ) {
2129 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2130 if ( $pref == 'default' ) {
2131 $pref = $this->getDefaultDateFormat();
2132 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2133 } else {
2134 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2135
2136 if ( $type === 'pretty' && $df === null ) {
2137 $df = $this->getDateFormatString( 'date', $pref );
2138 }
2139
2140 if ( $df === null ) {
2141 $pref = $this->getDefaultDateFormat();
2142 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2143 }
2144 }
2145 $this->dateFormatStrings[$type][$pref] = $df;
2146 }
2147 return $this->dateFormatStrings[$type][$pref];
2148 }
2149
2150 /**
2151 * @param string $ts The time format which needs to be turned into a
2152 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2153 * @param bool $adj Whether to adjust the time output according to the
2154 * user configured offset ($timecorrection)
2155 * @param mixed $format True to use user's date format preference
2156 * @param string|bool $timecorrection The time offset as returned by
2157 * validateTimeZone() in Special:Preferences
2158 * @return string
2159 */
2160 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2161 $ts = wfTimestamp( TS_MW, $ts );
2162 if ( $adj ) {
2163 $ts = $this->userAdjust( $ts, $timecorrection );
2164 }
2165 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2166 return $this->sprintfDate( $df, $ts );
2167 }
2168
2169 /**
2170 * @param string $ts The time format which needs to be turned into a
2171 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2172 * @param bool $adj Whether to adjust the time output according to the
2173 * user configured offset ($timecorrection)
2174 * @param mixed $format True to use user's date format preference
2175 * @param string|bool $timecorrection The time offset as returned by
2176 * validateTimeZone() in Special:Preferences
2177 * @return string
2178 */
2179 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2180 $ts = wfTimestamp( TS_MW, $ts );
2181 if ( $adj ) {
2182 $ts = $this->userAdjust( $ts, $timecorrection );
2183 }
2184 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2185 return $this->sprintfDate( $df, $ts );
2186 }
2187
2188 /**
2189 * @param string $ts The time format which needs to be turned into a
2190 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2191 * @param bool $adj Whether to adjust the time output according to the
2192 * user configured offset ($timecorrection)
2193 * @param mixed $format What format to return, if it's false output the
2194 * default one (default true)
2195 * @param string|bool $timecorrection The time offset as returned by
2196 * validateTimeZone() in Special:Preferences
2197 * @return string
2198 */
2199 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2200 $ts = wfTimestamp( TS_MW, $ts );
2201 if ( $adj ) {
2202 $ts = $this->userAdjust( $ts, $timecorrection );
2203 }
2204 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2205 return $this->sprintfDate( $df, $ts );
2206 }
2207
2208 /**
2209 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2210 *
2211 * @since 1.20
2212 *
2213 * @param int $seconds The amount of seconds.
2214 * @param array $chosenIntervals The intervals to enable.
2215 *
2216 * @return string
2217 */
2218 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
2219 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2220
2221 $segments = array();
2222
2223 foreach ( $intervals as $intervalName => $intervalValue ) {
2224 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2225 // duration-years, duration-decades, duration-centuries, duration-millennia
2226 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2227 $segments[] = $message->inLanguage( $this )->escaped();
2228 }
2229
2230 return $this->listToText( $segments );
2231 }
2232
2233 /**
2234 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2235 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2236 *
2237 * @since 1.20
2238 *
2239 * @param int $seconds The amount of seconds.
2240 * @param array $chosenIntervals The intervals to enable.
2241 *
2242 * @return array
2243 */
2244 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2245 if ( empty( $chosenIntervals ) ) {
2246 $chosenIntervals = array(
2247 'millennia',
2248 'centuries',
2249 'decades',
2250 'years',
2251 'days',
2252 'hours',
2253 'minutes',
2254 'seconds'
2255 );
2256 }
2257
2258 $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2259 $sortedNames = array_keys( $intervals );
2260 $smallestInterval = array_pop( $sortedNames );
2261
2262 $segments = array();
2263
2264 foreach ( $intervals as $name => $length ) {
2265 $value = floor( $seconds / $length );
2266
2267 if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2268 $seconds -= $value * $length;
2269 $segments[$name] = $value;
2270 }
2271 }
2272
2273 return $segments;
2274 }
2275
2276 /**
2277 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2278 *
2279 * @param string $type Can be 'date', 'time' or 'both'
2280 * @param string $ts The time format which needs to be turned into a
2281 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2282 * @param User $user User object used to get preferences for timezone and format
2283 * @param array $options Array, can contain the following keys:
2284 * - 'timecorrection': time correction, can have the following values:
2285 * - true: use user's preference
2286 * - false: don't use time correction
2287 * - int: value of time correction in minutes
2288 * - 'format': format to use, can have the following values:
2289 * - true: use user's preference
2290 * - false: use default preference
2291 * - string: format to use
2292 * @since 1.19
2293 * @return string
2294 */
2295 private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2296 $ts = wfTimestamp( TS_MW, $ts );
2297 $options += array( 'timecorrection' => true, 'format' => true );
2298 if ( $options['timecorrection'] !== false ) {
2299 if ( $options['timecorrection'] === true ) {
2300 $offset = $user->getOption( 'timecorrection' );
2301 } else {
2302 $offset = $options['timecorrection'];
2303 }
2304 $ts = $this->userAdjust( $ts, $offset );
2305 }
2306 if ( $options['format'] === true ) {
2307 $format = $user->getDatePreference();
2308 } else {
2309 $format = $options['format'];
2310 }
2311 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2312 return $this->sprintfDate( $df, $ts );
2313 }
2314
2315 /**
2316 * Get the formatted date for the given timestamp and formatted for
2317 * the given user.
2318 *
2319 * @param mixed $ts Mixed: the time format which needs to be turned into a
2320 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2321 * @param User $user User object used to get preferences for timezone and format
2322 * @param array $options Array, can contain the following keys:
2323 * - 'timecorrection': time correction, can have the following values:
2324 * - true: use user's preference
2325 * - false: don't use time correction
2326 * - int: value of time correction in minutes
2327 * - 'format': format to use, can have the following values:
2328 * - true: use user's preference
2329 * - false: use default preference
2330 * - string: format to use
2331 * @since 1.19
2332 * @return string
2333 */
2334 public function userDate( $ts, User $user, array $options = array() ) {
2335 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2336 }
2337
2338 /**
2339 * Get the formatted time for the given timestamp and formatted for
2340 * the given user.
2341 *
2342 * @param mixed $ts The time format which needs to be turned into a
2343 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2344 * @param User $user User object used to get preferences for timezone and format
2345 * @param array $options Array, can contain the following keys:
2346 * - 'timecorrection': time correction, can have the following values:
2347 * - true: use user's preference
2348 * - false: don't use time correction
2349 * - int: value of time correction in minutes
2350 * - 'format': format to use, can have the following values:
2351 * - true: use user's preference
2352 * - false: use default preference
2353 * - string: format to use
2354 * @since 1.19
2355 * @return string
2356 */
2357 public function userTime( $ts, User $user, array $options = array() ) {
2358 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2359 }
2360
2361 /**
2362 * Get the formatted date and time for the given timestamp and formatted for
2363 * the given user.
2364 *
2365 * @param mixed $ts the time format which needs to be turned into a
2366 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2367 * @param User $user User object used to get preferences for timezone and format
2368 * @param array $options Array, can contain the following keys:
2369 * - 'timecorrection': time correction, can have the following values:
2370 * - true: use user's preference
2371 * - false: don't use time correction
2372 * - int: value of time correction in minutes
2373 * - 'format': format to use, can have the following values:
2374 * - true: use user's preference
2375 * - false: use default preference
2376 * - string: format to use
2377 * @since 1.19
2378 * @return string
2379 */
2380 public function userTimeAndDate( $ts, User $user, array $options = array() ) {
2381 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2382 }
2383
2384 /**
2385 * Convert an MWTimestamp into a pretty human-readable timestamp using
2386 * the given user preferences and relative base time.
2387 *
2388 * DO NOT USE THIS FUNCTION DIRECTLY. Instead, call MWTimestamp::getHumanTimestamp
2389 * on your timestamp object, which will then call this function. Calling
2390 * this function directly will cause hooks to be skipped over.
2391 *
2392 * @see MWTimestamp::getHumanTimestamp
2393 * @param MWTimestamp $ts Timestamp to prettify
2394 * @param MWTimestamp $relativeTo Base timestamp
2395 * @param User $user User preferences to use
2396 * @return string Human timestamp
2397 * @since 1.22
2398 */
2399 public function getHumanTimestamp( MWTimestamp $ts, MWTimestamp $relativeTo, User $user ) {
2400 $diff = $ts->diff( $relativeTo );
2401 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2402 (int)$relativeTo->timestamp->format( 'w' ) );
2403 $days = $diff->days ?: (int)$diffDay;
2404 if ( $diff->invert || $days > 5
2405 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2406 ) {
2407 // Timestamps are in different years: use full timestamp
2408 // Also do full timestamp for future dates
2409 /**
2410 * @FIXME Add better handling of future timestamps.
2411 */
2412 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2413 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2414 } elseif ( $days > 5 ) {
2415 // Timestamps are in same year, but more than 5 days ago: show day and month only.
2416 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2417 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2418 } elseif ( $days > 1 ) {
2419 // Timestamp within the past week: show the day of the week and time
2420 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2421 $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2422 // Messages:
2423 // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2424 $ts = wfMessage( "$weekday-at" )
2425 ->inLanguage( $this )
2426 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2427 ->text();
2428 } elseif ( $days == 1 ) {
2429 // Timestamp was yesterday: say 'yesterday' and the time.
2430 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2431 $ts = wfMessage( 'yesterday-at' )
2432 ->inLanguage( $this )
2433 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2434 ->text();
2435 } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2436 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2437 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2438 $ts = wfMessage( 'today-at' )
2439 ->inLanguage( $this )
2440 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2441 ->text();
2442
2443 // From here on in, the timestamp was soon enough ago so that we can simply say
2444 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2445 } elseif ( $diff->h == 1 ) {
2446 // Less than 90 minutes, but more than an hour ago.
2447 $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2448 } elseif ( $diff->i >= 1 ) {
2449 // A few minutes ago.
2450 $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2451 } elseif ( $diff->s >= 30 ) {
2452 // Less than a minute, but more than 30 sec ago.
2453 $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2454 } else {
2455 // Less than 30 seconds ago.
2456 $ts = wfMessage( 'just-now' )->text();
2457 }
2458
2459 return $ts;
2460 }
2461
2462 /**
2463 * @param string $key
2464 * @return array|null
2465 */
2466 function getMessage( $key ) {
2467 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2468 }
2469
2470 /**
2471 * @return array
2472 */
2473 function getAllMessages() {
2474 return self::$dataCache->getItem( $this->mCode, 'messages' );
2475 }
2476
2477 /**
2478 * @param string $in
2479 * @param string $out
2480 * @param string $string
2481 * @return string
2482 */
2483 function iconv( $in, $out, $string ) {
2484 # This is a wrapper for iconv in all languages except esperanto,
2485 # which does some nasty x-conversions beforehand
2486
2487 # Even with //IGNORE iconv can whine about illegal characters in
2488 # *input* string. We just ignore those too.
2489 # REF: http://bugs.php.net/bug.php?id=37166
2490 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2491 wfSuppressWarnings();
2492 $text = iconv( $in, $out . '//IGNORE', $string );
2493 wfRestoreWarnings();
2494 return $text;
2495 }
2496
2497 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2498
2499 /**
2500 * @param array $matches
2501 * @return mixed|string
2502 */
2503 function ucwordbreaksCallbackAscii( $matches ) {
2504 return $this->ucfirst( $matches[1] );
2505 }
2506
2507 /**
2508 * @param array $matches
2509 * @return string
2510 */
2511 function ucwordbreaksCallbackMB( $matches ) {
2512 return mb_strtoupper( $matches[0] );
2513 }
2514
2515 /**
2516 * @param array $matches
2517 * @return string
2518 */
2519 function ucCallback( $matches ) {
2520 list( $wikiUpperChars ) = self::getCaseMaps();
2521 return strtr( $matches[1], $wikiUpperChars );
2522 }
2523
2524 /**
2525 * @param array $matches
2526 * @return string
2527 */
2528 function lcCallback( $matches ) {
2529 list( , $wikiLowerChars ) = self::getCaseMaps();
2530 return strtr( $matches[1], $wikiLowerChars );
2531 }
2532
2533 /**
2534 * @param array $matches
2535 * @return string
2536 */
2537 function ucwordsCallbackMB( $matches ) {
2538 return mb_strtoupper( $matches[0] );
2539 }
2540
2541 /**
2542 * @param array $matches
2543 * @return string
2544 */
2545 function ucwordsCallbackWiki( $matches ) {
2546 list( $wikiUpperChars ) = self::getCaseMaps();
2547 return strtr( $matches[0], $wikiUpperChars );
2548 }
2549
2550 /**
2551 * Make a string's first character uppercase
2552 *
2553 * @param string $str
2554 *
2555 * @return string
2556 */
2557 function ucfirst( $str ) {
2558 $o = ord( $str );
2559 if ( $o < 96 ) { // if already uppercase...
2560 return $str;
2561 } elseif ( $o < 128 ) {
2562 return ucfirst( $str ); // use PHP's ucfirst()
2563 } else {
2564 // fall back to more complex logic in case of multibyte strings
2565 return $this->uc( $str, true );
2566 }
2567 }
2568
2569 /**
2570 * Convert a string to uppercase
2571 *
2572 * @param string $str
2573 * @param bool $first
2574 *
2575 * @return string
2576 */
2577 function uc( $str, $first = false ) {
2578 if ( function_exists( 'mb_strtoupper' ) ) {
2579 if ( $first ) {
2580 if ( $this->isMultibyte( $str ) ) {
2581 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2582 } else {
2583 return ucfirst( $str );
2584 }
2585 } else {
2586 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2587 }
2588 } else {
2589 if ( $this->isMultibyte( $str ) ) {
2590 $x = $first ? '^' : '';
2591 return preg_replace_callback(
2592 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2593 array( $this, 'ucCallback' ),
2594 $str
2595 );
2596 } else {
2597 return $first ? ucfirst( $str ) : strtoupper( $str );
2598 }
2599 }
2600 }
2601
2602 /**
2603 * @param string $str
2604 * @return mixed|string
2605 */
2606 function lcfirst( $str ) {
2607 $o = ord( $str );
2608 if ( !$o ) {
2609 return strval( $str );
2610 } elseif ( $o >= 128 ) {
2611 return $this->lc( $str, true );
2612 } elseif ( $o > 96 ) {
2613 return $str;
2614 } else {
2615 $str[0] = strtolower( $str[0] );
2616 return $str;
2617 }
2618 }
2619
2620 /**
2621 * @param string $str
2622 * @param bool $first
2623 * @return mixed|string
2624 */
2625 function lc( $str, $first = false ) {
2626 if ( function_exists( 'mb_strtolower' ) ) {
2627 if ( $first ) {
2628 if ( $this->isMultibyte( $str ) ) {
2629 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2630 } else {
2631 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2632 }
2633 } else {
2634 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2635 }
2636 } else {
2637 if ( $this->isMultibyte( $str ) ) {
2638 $x = $first ? '^' : '';
2639 return preg_replace_callback(
2640 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2641 array( $this, 'lcCallback' ),
2642 $str
2643 );
2644 } else {
2645 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2646 }
2647 }
2648 }
2649
2650 /**
2651 * @param string $str
2652 * @return bool
2653 */
2654 function isMultibyte( $str ) {
2655 return (bool)preg_match( '/[\x80-\xff]/', $str );
2656 }
2657
2658 /**
2659 * @param string $str
2660 * @return mixed|string
2661 */
2662 function ucwords( $str ) {
2663 if ( $this->isMultibyte( $str ) ) {
2664 $str = $this->lc( $str );
2665
2666 // regexp to find first letter in each word (i.e. after each space)
2667 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2668
2669 // function to use to capitalize a single char
2670 if ( function_exists( 'mb_strtoupper' ) ) {
2671 return preg_replace_callback(
2672 $replaceRegexp,
2673 array( $this, 'ucwordsCallbackMB' ),
2674 $str
2675 );
2676 } else {
2677 return preg_replace_callback(
2678 $replaceRegexp,
2679 array( $this, 'ucwordsCallbackWiki' ),
2680 $str
2681 );
2682 }
2683 } else {
2684 return ucwords( strtolower( $str ) );
2685 }
2686 }
2687
2688 /**
2689 * capitalize words at word breaks
2690 *
2691 * @param string $str
2692 * @return mixed
2693 */
2694 function ucwordbreaks( $str ) {
2695 if ( $this->isMultibyte( $str ) ) {
2696 $str = $this->lc( $str );
2697
2698 // since \b doesn't work for UTF-8, we explicitely define word break chars
2699 $breaks = "[ \-\(\)\}\{\.,\?!]";
2700
2701 // find first letter after word break
2702 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2703 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2704
2705 if ( function_exists( 'mb_strtoupper' ) ) {
2706 return preg_replace_callback(
2707 $replaceRegexp,
2708 array( $this, 'ucwordbreaksCallbackMB' ),
2709 $str
2710 );
2711 } else {
2712 return preg_replace_callback(
2713 $replaceRegexp,
2714 array( $this, 'ucwordsCallbackWiki' ),
2715 $str
2716 );
2717 }
2718 } else {
2719 return preg_replace_callback(
2720 '/\b([\w\x80-\xff]+)\b/',
2721 array( $this, 'ucwordbreaksCallbackAscii' ),
2722 $str
2723 );
2724 }
2725 }
2726
2727 /**
2728 * Return a case-folded representation of $s
2729 *
2730 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2731 * and $s2 are the same except for the case of their characters. It is not
2732 * necessary for the value returned to make sense when displayed.
2733 *
2734 * Do *not* perform any other normalisation in this function. If a caller
2735 * uses this function when it should be using a more general normalisation
2736 * function, then fix the caller.
2737 *
2738 * @param string $s
2739 *
2740 * @return string
2741 */
2742 function caseFold( $s ) {
2743 return $this->uc( $s );
2744 }
2745
2746 /**
2747 * @param string $s
2748 * @return string
2749 */
2750 function checkTitleEncoding( $s ) {
2751 if ( is_array( $s ) ) {
2752 throw new MWException( 'Given array to checkTitleEncoding.' );
2753 }
2754 if ( StringUtils::isUtf8( $s ) ) {
2755 return $s;
2756 }
2757
2758 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2759 }
2760
2761 /**
2762 * @return array
2763 */
2764 function fallback8bitEncoding() {
2765 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2766 }
2767
2768 /**
2769 * Most writing systems use whitespace to break up words.
2770 * Some languages such as Chinese don't conventionally do this,
2771 * which requires special handling when breaking up words for
2772 * searching etc.
2773 *
2774 * @return bool
2775 */
2776 function hasWordBreaks() {
2777 return true;
2778 }
2779
2780 /**
2781 * Some languages such as Chinese require word segmentation,
2782 * Specify such segmentation when overridden in derived class.
2783 *
2784 * @param string $string
2785 * @return string
2786 */
2787 function segmentByWord( $string ) {
2788 return $string;
2789 }
2790
2791 /**
2792 * Some languages have special punctuation need to be normalized.
2793 * Make such changes here.
2794 *
2795 * @param string $string
2796 * @return string
2797 */
2798 function normalizeForSearch( $string ) {
2799 return self::convertDoubleWidth( $string );
2800 }
2801
2802 /**
2803 * convert double-width roman characters to single-width.
2804 * range: ff00-ff5f ~= 0020-007f
2805 *
2806 * @param string $string
2807 *
2808 * @return string
2809 */
2810 protected static function convertDoubleWidth( $string ) {
2811 static $full = null;
2812 static $half = null;
2813
2814 if ( $full === null ) {
2815 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2816 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2817 $full = str_split( $fullWidth, 3 );
2818 $half = str_split( $halfWidth );
2819 }
2820
2821 $string = str_replace( $full, $half, $string );
2822 return $string;
2823 }
2824
2825 /**
2826 * @param string $string
2827 * @param string $pattern
2828 * @return string
2829 */
2830 protected static function insertSpace( $string, $pattern ) {
2831 $string = preg_replace( $pattern, " $1 ", $string );
2832 $string = preg_replace( '/ +/', ' ', $string );
2833 return $string;
2834 }
2835
2836 /**
2837 * @param array $termsArray
2838 * @return array
2839 */
2840 function convertForSearchResult( $termsArray ) {
2841 # some languages, e.g. Chinese, need to do a conversion
2842 # in order for search results to be displayed correctly
2843 return $termsArray;
2844 }
2845
2846 /**
2847 * Get the first character of a string.
2848 *
2849 * @param string $s
2850 * @return string
2851 */
2852 function firstChar( $s ) {
2853 $matches = array();
2854 preg_match(
2855 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2856 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2857 $s,
2858 $matches
2859 );
2860
2861 if ( isset( $matches[1] ) ) {
2862 if ( strlen( $matches[1] ) != 3 ) {
2863 return $matches[1];
2864 }
2865
2866 // Break down Hangul syllables to grab the first jamo
2867 $code = utf8ToCodepoint( $matches[1] );
2868 if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2869 return $matches[1];
2870 } elseif ( $code < 0xb098 ) {
2871 return "\xe3\x84\xb1";
2872 } elseif ( $code < 0xb2e4 ) {
2873 return "\xe3\x84\xb4";
2874 } elseif ( $code < 0xb77c ) {
2875 return "\xe3\x84\xb7";
2876 } elseif ( $code < 0xb9c8 ) {
2877 return "\xe3\x84\xb9";
2878 } elseif ( $code < 0xbc14 ) {
2879 return "\xe3\x85\x81";
2880 } elseif ( $code < 0xc0ac ) {
2881 return "\xe3\x85\x82";
2882 } elseif ( $code < 0xc544 ) {
2883 return "\xe3\x85\x85";
2884 } elseif ( $code < 0xc790 ) {
2885 return "\xe3\x85\x87";
2886 } elseif ( $code < 0xcc28 ) {
2887 return "\xe3\x85\x88";
2888 } elseif ( $code < 0xce74 ) {
2889 return "\xe3\x85\x8a";
2890 } elseif ( $code < 0xd0c0 ) {
2891 return "\xe3\x85\x8b";
2892 } elseif ( $code < 0xd30c ) {
2893 return "\xe3\x85\x8c";
2894 } elseif ( $code < 0xd558 ) {
2895 return "\xe3\x85\x8d";
2896 } else {
2897 return "\xe3\x85\x8e";
2898 }
2899 } else {
2900 return '';
2901 }
2902 }
2903
2904 function initEncoding() {
2905 # Some languages may have an alternate char encoding option
2906 # (Esperanto X-coding, Japanese furigana conversion, etc)
2907 # If this language is used as the primary content language,
2908 # an override to the defaults can be set here on startup.
2909 }
2910
2911 /**
2912 * @param string $s
2913 * @return string
2914 */
2915 function recodeForEdit( $s ) {
2916 # For some languages we'll want to explicitly specify
2917 # which characters make it into the edit box raw
2918 # or are converted in some way or another.
2919 global $wgEditEncoding;
2920 if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
2921 return $s;
2922 } else {
2923 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2924 }
2925 }
2926
2927 /**
2928 * @param string $s
2929 * @return string
2930 */
2931 function recodeInput( $s ) {
2932 # Take the previous into account.
2933 global $wgEditEncoding;
2934 if ( $wgEditEncoding != '' ) {
2935 $enc = $wgEditEncoding;
2936 } else {
2937 $enc = 'UTF-8';
2938 }
2939 if ( $enc == 'UTF-8' ) {
2940 return $s;
2941 } else {
2942 return $this->iconv( $enc, 'UTF-8', $s );
2943 }
2944 }
2945
2946 /**
2947 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2948 * also cleans up certain backwards-compatible sequences, converting them
2949 * to the modern Unicode equivalent.
2950 *
2951 * This is language-specific for performance reasons only.
2952 *
2953 * @param string $s
2954 *
2955 * @return string
2956 */
2957 function normalize( $s ) {
2958 global $wgAllUnicodeFixes;
2959 $s = UtfNormal::cleanUp( $s );
2960 if ( $wgAllUnicodeFixes ) {
2961 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2962 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2963 }
2964
2965 return $s;
2966 }
2967
2968 /**
2969 * Transform a string using serialized data stored in the given file (which
2970 * must be in the serialized subdirectory of $IP). The file contains pairs
2971 * mapping source characters to destination characters.
2972 *
2973 * The data is cached in process memory. This will go faster if you have the
2974 * FastStringSearch extension.
2975 *
2976 * @param string $file
2977 * @param string $string
2978 *
2979 * @throws MWException
2980 * @return string
2981 */
2982 function transformUsingPairFile( $file, $string ) {
2983 if ( !isset( $this->transformData[$file] ) ) {
2984 $data = wfGetPrecompiledData( $file );
2985 if ( $data === false ) {
2986 throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
2987 }
2988 $this->transformData[$file] = new ReplacementArray( $data );
2989 }
2990 return $this->transformData[$file]->replace( $string );
2991 }
2992
2993 /**
2994 * For right-to-left language support
2995 *
2996 * @return bool
2997 */
2998 function isRTL() {
2999 return self::$dataCache->getItem( $this->mCode, 'rtl' );
3000 }
3001
3002 /**
3003 * Return the correct HTML 'dir' attribute value for this language.
3004 * @return string
3005 */
3006 function getDir() {
3007 return $this->isRTL() ? 'rtl' : 'ltr';
3008 }
3009
3010 /**
3011 * Return 'left' or 'right' as appropriate alignment for line-start
3012 * for this language's text direction.
3013 *
3014 * Should be equivalent to CSS3 'start' text-align value....
3015 *
3016 * @return string
3017 */
3018 function alignStart() {
3019 return $this->isRTL() ? 'right' : 'left';
3020 }
3021
3022 /**
3023 * Return 'right' or 'left' as appropriate alignment for line-end
3024 * for this language's text direction.
3025 *
3026 * Should be equivalent to CSS3 'end' text-align value....
3027 *
3028 * @return string
3029 */
3030 function alignEnd() {
3031 return $this->isRTL() ? 'left' : 'right';
3032 }
3033
3034 /**
3035 * A hidden direction mark (LRM or RLM), depending on the language direction.
3036 * Unlike getDirMark(), this function returns the character as an HTML entity.
3037 * This function should be used when the output is guaranteed to be HTML,
3038 * because it makes the output HTML source code more readable. When
3039 * the output is plain text or can be escaped, getDirMark() should be used.
3040 *
3041 * @param bool $opposite Get the direction mark opposite to your language
3042 * @return string
3043 * @since 1.20
3044 */
3045 function getDirMarkEntity( $opposite = false ) {
3046 if ( $opposite ) {
3047 return $this->isRTL() ? '&lrm;' : '&rlm;';
3048 }
3049 return $this->isRTL() ? '&rlm;' : '&lrm;';
3050 }
3051
3052 /**
3053 * A hidden direction mark (LRM or RLM), depending on the language direction.
3054 * This function produces them as invisible Unicode characters and
3055 * the output may be hard to read and debug, so it should only be used
3056 * when the output is plain text or can be escaped. When the output is
3057 * HTML, use getDirMarkEntity() instead.
3058 *
3059 * @param bool $opposite Get the direction mark opposite to your language
3060 * @return string
3061 */
3062 function getDirMark( $opposite = false ) {
3063 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3064 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3065 if ( $opposite ) {
3066 return $this->isRTL() ? $lrm : $rlm;
3067 }
3068 return $this->isRTL() ? $rlm : $lrm;
3069 }
3070
3071 /**
3072 * @return array
3073 */
3074 function capitalizeAllNouns() {
3075 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3076 }
3077
3078 /**
3079 * An arrow, depending on the language direction.
3080 *
3081 * @param string $direction The direction of the arrow: forwards (default),
3082 * backwards, left, right, up, down.
3083 * @return string
3084 */
3085 function getArrow( $direction = 'forwards' ) {
3086 switch ( $direction ) {
3087 case 'forwards':
3088 return $this->isRTL() ? '←' : '→';
3089 case 'backwards':
3090 return $this->isRTL() ? '→' : '←';
3091 case 'left':
3092 return '←';
3093 case 'right':
3094 return '→';
3095 case 'up':
3096 return '↑';
3097 case 'down':
3098 return '↓';
3099 }
3100 }
3101
3102 /**
3103 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3104 *
3105 * @return bool
3106 */
3107 function linkPrefixExtension() {
3108 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3109 }
3110
3111 /**
3112 * Get all magic words from cache.
3113 * @return array
3114 */
3115 function getMagicWords() {
3116 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3117 }
3118
3119 /**
3120 * Run the LanguageGetMagic hook once.
3121 */
3122 protected function doMagicHook() {
3123 if ( $this->mMagicHookDone ) {
3124 return;
3125 }
3126 $this->mMagicHookDone = true;
3127 wfProfileIn( 'LanguageGetMagic' );
3128 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
3129 wfProfileOut( 'LanguageGetMagic' );
3130 }
3131
3132 /**
3133 * Fill a MagicWord object with data from here
3134 *
3135 * @param MagicWord $mw
3136 */
3137 function getMagic( $mw ) {
3138 // Saves a function call
3139 if ( ! $this->mMagicHookDone ) {
3140 $this->doMagicHook();
3141 }
3142
3143 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
3144 $rawEntry = $this->mMagicExtensions[$mw->mId];
3145 } else {
3146 $rawEntry = self::$dataCache->getSubitem(
3147 $this->mCode, 'magicWords', $mw->mId );
3148 }
3149
3150 if ( !is_array( $rawEntry ) ) {
3151 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3152 } else {
3153 $mw->mCaseSensitive = $rawEntry[0];
3154 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3155 }
3156 }
3157
3158 /**
3159 * Add magic words to the extension array
3160 *
3161 * @param array $newWords
3162 */
3163 function addMagicWordsByLang( $newWords ) {
3164 $fallbackChain = $this->getFallbackLanguages();
3165 $fallbackChain = array_reverse( $fallbackChain );
3166 foreach ( $fallbackChain as $code ) {
3167 if ( isset( $newWords[$code] ) ) {
3168 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3169 }
3170 }
3171 }
3172
3173 /**
3174 * Get special page names, as an associative array
3175 * case folded alias => real name
3176 * @return array
3177 */
3178 function getSpecialPageAliases() {
3179 // Cache aliases because it may be slow to load them
3180 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3181 // Initialise array
3182 $this->mExtendedSpecialPageAliases =
3183 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3184 wfRunHooks( 'LanguageGetSpecialPageAliases',
3185 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
3186 }
3187
3188 return $this->mExtendedSpecialPageAliases;
3189 }
3190
3191 /**
3192 * Italic is unsuitable for some languages
3193 *
3194 * @param string $text The text to be emphasized.
3195 * @return string
3196 */
3197 function emphasize( $text ) {
3198 return "<em>$text</em>";
3199 }
3200
3201 /**
3202 * Normally we output all numbers in plain en_US style, that is
3203 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3204 * point twohundredthirtyfive. However this is not suitable for all
3205 * languages, some such as Punjabi want ੨੯੩,੨੯੫.੨੩੫ and others such as
3206 * Icelandic just want to use commas instead of dots, and dots instead
3207 * of commas like "293.291,235".
3208 *
3209 * An example of this function being called:
3210 * <code>
3211 * wfMessage( 'message' )->numParams( $num )->text()
3212 * </code>
3213 *
3214 * See $separatorTransformTable on MessageIs.php for
3215 * the , => . and . => , implementation.
3216 *
3217 * @todo check if it's viable to use localeconv() for the decimal separator thing.
3218 * @param int|float $number The string to be formatted, should be an integer
3219 * or a floating point number.
3220 * @param bool $nocommafy Set to true for special numbers like dates
3221 * @return string
3222 */
3223 public function formatNum( $number, $nocommafy = false ) {
3224 global $wgTranslateNumerals;
3225 if ( !$nocommafy ) {
3226 $number = $this->commafy( $number );
3227 $s = $this->separatorTransformTable();
3228 if ( $s ) {
3229 $number = strtr( $number, $s );
3230 }
3231 }
3232
3233 if ( $wgTranslateNumerals ) {
3234 $s = $this->digitTransformTable();
3235 if ( $s ) {
3236 $number = strtr( $number, $s );
3237 }
3238 }
3239
3240 return $number;
3241 }
3242
3243 /**
3244 * Front-end for non-commafied formatNum
3245 *
3246 * @param int|float $number The string to be formatted, should be an integer
3247 * or a floating point number.
3248 * @since 1.21
3249 * @return string
3250 */
3251 public function formatNumNoSeparators( $number ) {
3252 return $this->formatNum( $number, true );
3253 }
3254
3255 /**
3256 * @param string $number
3257 * @return string
3258 */
3259 public function parseFormattedNumber( $number ) {
3260 $s = $this->digitTransformTable();
3261 if ( $s ) {
3262 // eliminate empty array values such as ''. (bug 64347)
3263 $s = array_filter( $s );
3264 $number = strtr( $number, array_flip( $s ) );
3265 }
3266
3267 $s = $this->separatorTransformTable();
3268 if ( $s ) {
3269 // eliminate empty array values such as ''. (bug 64347)
3270 $s = array_filter( $s );
3271 $number = strtr( $number, array_flip( $s ) );
3272 }
3273
3274 $number = strtr( $number, array( ',' => '' ) );
3275 return $number;
3276 }
3277
3278 /**
3279 * Adds commas to a given number
3280 * @since 1.19
3281 * @param mixed $number
3282 * @return string
3283 */
3284 function commafy( $number ) {
3285 $digitGroupingPattern = $this->digitGroupingPattern();
3286 if ( $number === null ) {
3287 return '';
3288 }
3289
3290 if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3291 // default grouping is at thousands, use the same for ###,###,### pattern too.
3292 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3293 } else {
3294 // Ref: http://cldr.unicode.org/translation/number-patterns
3295 $sign = "";
3296 if ( intval( $number ) < 0 ) {
3297 // For negative numbers apply the algorithm like positive number and add sign.
3298 $sign = "-";
3299 $number = substr( $number, 1 );
3300 }
3301 $integerPart = array();
3302 $decimalPart = array();
3303 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3304 preg_match( "/\d+/", $number, $integerPart );
3305 preg_match( "/\.\d*/", $number, $decimalPart );
3306 $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3307 if ( $groupedNumber === $number ) {
3308 // the string does not have any number part. Eg: .12345
3309 return $sign . $groupedNumber;
3310 }
3311 $start = $end = strlen( $integerPart[0] );
3312 while ( $start > 0 ) {
3313 $match = $matches[0][$numMatches - 1];
3314 $matchLen = strlen( $match );
3315 $start = $end - $matchLen;
3316 if ( $start < 0 ) {
3317 $start = 0;
3318 }
3319 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3320 $end = $start;
3321 if ( $numMatches > 1 ) {
3322 // use the last pattern for the rest of the number
3323 $numMatches--;
3324 }
3325 if ( $start > 0 ) {
3326 $groupedNumber = "," . $groupedNumber;
3327 }
3328 }
3329 return $sign . $groupedNumber;
3330 }
3331 }
3332
3333 /**
3334 * @return string
3335 */
3336 function digitGroupingPattern() {
3337 return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3338 }
3339
3340 /**
3341 * @return array
3342 */
3343 function digitTransformTable() {
3344 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3345 }
3346
3347 /**
3348 * @return array
3349 */
3350 function separatorTransformTable() {
3351 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3352 }
3353
3354 /**
3355 * Take a list of strings and build a locale-friendly comma-separated
3356 * list, using the local comma-separator message.
3357 * The last two strings are chained with an "and".
3358 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3359 *
3360 * @param string[] $l
3361 * @return string
3362 */
3363 function listToText( array $l ) {
3364 $m = count( $l ) - 1;
3365 if ( $m < 0 ) {
3366 return '';
3367 }
3368 if ( $m > 0 ) {
3369 $and = $this->getMessageFromDB( 'and' );
3370 $space = $this->getMessageFromDB( 'word-separator' );
3371 if ( $m > 1 ) {
3372 $comma = $this->getMessageFromDB( 'comma-separator' );
3373 }
3374 }
3375 $s = $l[$m];
3376 for ( $i = $m - 1; $i >= 0; $i-- ) {
3377 if ( $i == $m - 1 ) {
3378 $s = $l[$i] . $and . $space . $s;
3379 } else {
3380 $s = $l[$i] . $comma . $s;
3381 }
3382 }
3383 return $s;
3384 }
3385
3386 /**
3387 * Take a list of strings and build a locale-friendly comma-separated
3388 * list, using the local comma-separator message.
3389 * @param string[] $list Array of strings to put in a comma list
3390 * @return string
3391 */
3392 function commaList( array $list ) {
3393 return implode(
3394 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3395 $list
3396 );
3397 }
3398
3399 /**
3400 * Take a list of strings and build a locale-friendly semicolon-separated
3401 * list, using the local semicolon-separator message.
3402 * @param string[] $list Array of strings to put in a semicolon list
3403 * @return string
3404 */
3405 function semicolonList( array $list ) {
3406 return implode(
3407 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3408 $list
3409 );
3410 }
3411
3412 /**
3413 * Same as commaList, but separate it with the pipe instead.
3414 * @param string[] $list Array of strings to put in a pipe list
3415 * @return string
3416 */
3417 function pipeList( array $list ) {
3418 return implode(
3419 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3420 $list
3421 );
3422 }
3423
3424 /**
3425 * Truncate a string to a specified length in bytes, appending an optional
3426 * string (e.g. for ellipses)
3427 *
3428 * The database offers limited byte lengths for some columns in the database;
3429 * multi-byte character sets mean we need to ensure that only whole characters
3430 * are included, otherwise broken characters can be passed to the user
3431 *
3432 * If $length is negative, the string will be truncated from the beginning
3433 *
3434 * @param string $string String to truncate
3435 * @param int $length Maximum length (including ellipses)
3436 * @param string $ellipsis String to append to the truncated text
3437 * @param bool $adjustLength Subtract length of ellipsis from $length.
3438 * $adjustLength was introduced in 1.18, before that behaved as if false.
3439 * @return string
3440 */
3441 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3442 # Use the localized ellipsis character
3443 if ( $ellipsis == '...' ) {
3444 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3445 }
3446 # Check if there is no need to truncate
3447 if ( $length == 0 ) {
3448 return $ellipsis; // convention
3449 } elseif ( strlen( $string ) <= abs( $length ) ) {
3450 return $string; // no need to truncate
3451 }
3452 $stringOriginal = $string;
3453 # If ellipsis length is >= $length then we can't apply $adjustLength
3454 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3455 $string = $ellipsis; // this can be slightly unexpected
3456 # Otherwise, truncate and add ellipsis...
3457 } else {
3458 $eLength = $adjustLength ? strlen( $ellipsis ) : 0;
3459 if ( $length > 0 ) {
3460 $length -= $eLength;
3461 $string = substr( $string, 0, $length ); // xyz...
3462 $string = $this->removeBadCharLast( $string );
3463 $string = rtrim( $string );
3464 $string = $string . $ellipsis;
3465 } else {
3466 $length += $eLength;
3467 $string = substr( $string, $length ); // ...xyz
3468 $string = $this->removeBadCharFirst( $string );
3469 $string = ltrim( $string );
3470 $string = $ellipsis . $string;
3471 }
3472 }
3473 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3474 # This check is *not* redundant if $adjustLength, due to the single case where
3475 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3476 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3477 return $string;
3478 } else {
3479 return $stringOriginal;
3480 }
3481 }
3482
3483 /**
3484 * Remove bytes that represent an incomplete Unicode character
3485 * at the end of string (e.g. bytes of the char are missing)
3486 *
3487 * @param string $string
3488 * @return string
3489 */
3490 protected function removeBadCharLast( $string ) {
3491 if ( $string != '' ) {
3492 $char = ord( $string[strlen( $string ) - 1] );
3493 $m = array();
3494 if ( $char >= 0xc0 ) {
3495 # We got the first byte only of a multibyte char; remove it.
3496 $string = substr( $string, 0, -1 );
3497 } elseif ( $char >= 0x80 &&
3498 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3499 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m )
3500 ) {
3501 # We chopped in the middle of a character; remove it
3502 $string = $m[1];
3503 }
3504 }
3505 return $string;
3506 }
3507
3508 /**
3509 * Remove bytes that represent an incomplete Unicode character
3510 * at the start of string (e.g. bytes of the char are missing)
3511 *
3512 * @param string $string
3513 * @return string
3514 */
3515 protected function removeBadCharFirst( $string ) {
3516 if ( $string != '' ) {
3517 $char = ord( $string[0] );
3518 if ( $char >= 0x80 && $char < 0xc0 ) {
3519 # We chopped in the middle of a character; remove the whole thing
3520 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3521 }
3522 }
3523 return $string;
3524 }
3525
3526 /**
3527 * Truncate a string of valid HTML to a specified length in bytes,
3528 * appending an optional string (e.g. for ellipses), and return valid HTML
3529 *
3530 * This is only intended for styled/linked text, such as HTML with
3531 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3532 * Also, this will not detect things like "display:none" CSS.
3533 *
3534 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3535 *
3536 * @param string $text HTML string to truncate
3537 * @param int $length (zero/positive) Maximum length (including ellipses)
3538 * @param string $ellipsis String to append to the truncated text
3539 * @return string
3540 */
3541 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3542 # Use the localized ellipsis character
3543 if ( $ellipsis == '...' ) {
3544 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3545 }
3546 # Check if there is clearly no need to truncate
3547 if ( $length <= 0 ) {
3548 return $ellipsis; // no text shown, nothing to format (convention)
3549 } elseif ( strlen( $text ) <= $length ) {
3550 return $text; // string short enough even *with* HTML (short-circuit)
3551 }
3552
3553 $dispLen = 0; // innerHTML legth so far
3554 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3555 $tagType = 0; // 0-open, 1-close
3556 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3557 $entityState = 0; // 0-not entity, 1-entity
3558 $tag = $ret = ''; // accumulated tag name, accumulated result string
3559 $openTags = array(); // open tag stack
3560 $maybeState = null; // possible truncation state
3561
3562 $textLen = strlen( $text );
3563 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3564 for ( $pos = 0; true; ++$pos ) {
3565 # Consider truncation once the display length has reached the maximim.
3566 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3567 # Check that we're not in the middle of a bracket/entity...
3568 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3569 if ( !$testingEllipsis ) {
3570 $testingEllipsis = true;
3571 # Save where we are; we will truncate here unless there turn out to
3572 # be so few remaining characters that truncation is not necessary.
3573 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3574 $maybeState = array( $ret, $openTags ); // save state
3575 }
3576 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3577 # String in fact does need truncation, the truncation point was OK.
3578 list( $ret, $openTags ) = $maybeState; // reload state
3579 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3580 $ret .= $ellipsis; // add ellipsis
3581 break;
3582 }
3583 }
3584 if ( $pos >= $textLen ) {
3585 break; // extra iteration just for above checks
3586 }
3587
3588 # Read the next char...
3589 $ch = $text[$pos];
3590 $lastCh = $pos ? $text[$pos - 1] : '';
3591 $ret .= $ch; // add to result string
3592 if ( $ch == '<' ) {
3593 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3594 $entityState = 0; // for bad HTML
3595 $bracketState = 1; // tag started (checking for backslash)
3596 } elseif ( $ch == '>' ) {
3597 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3598 $entityState = 0; // for bad HTML
3599 $bracketState = 0; // out of brackets
3600 } elseif ( $bracketState == 1 ) {
3601 if ( $ch == '/' ) {
3602 $tagType = 1; // close tag (e.g. "</span>")
3603 } else {
3604 $tagType = 0; // open tag (e.g. "<span>")
3605 $tag .= $ch;
3606 }
3607 $bracketState = 2; // building tag name
3608 } elseif ( $bracketState == 2 ) {
3609 if ( $ch != ' ' ) {
3610 $tag .= $ch;
3611 } else {
3612 // Name found (e.g. "<a href=..."), add on tag attributes...
3613 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3614 }
3615 } elseif ( $bracketState == 0 ) {
3616 if ( $entityState ) {
3617 if ( $ch == ';' ) {
3618 $entityState = 0;
3619 $dispLen++; // entity is one displayed char
3620 }
3621 } else {
3622 if ( $neLength == 0 && !$maybeState ) {
3623 // Save state without $ch. We want to *hit* the first
3624 // display char (to get tags) but not *use* it if truncating.
3625 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3626 }
3627 if ( $ch == '&' ) {
3628 $entityState = 1; // entity found, (e.g. "&#160;")
3629 } else {
3630 $dispLen++; // this char is displayed
3631 // Add the next $max display text chars after this in one swoop...
3632 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3633 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3634 $dispLen += $skipped;
3635 $pos += $skipped;
3636 }
3637 }
3638 }
3639 }
3640 // Close the last tag if left unclosed by bad HTML
3641 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3642 while ( count( $openTags ) > 0 ) {
3643 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3644 }
3645 return $ret;
3646 }
3647
3648 /**
3649 * truncateHtml() helper function
3650 * like strcspn() but adds the skipped chars to $ret
3651 *
3652 * @param string $ret
3653 * @param string $text
3654 * @param string $search
3655 * @param int $start
3656 * @param null|int $len
3657 * @return int
3658 */
3659 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3660 if ( $len === null ) {
3661 $len = -1; // -1 means "no limit" for strcspn
3662 } elseif ( $len < 0 ) {
3663 $len = 0; // sanity
3664 }
3665 $skipCount = 0;
3666 if ( $start < strlen( $text ) ) {
3667 $skipCount = strcspn( $text, $search, $start, $len );
3668 $ret .= substr( $text, $start, $skipCount );
3669 }
3670 return $skipCount;
3671 }
3672
3673 /**
3674 * truncateHtml() helper function
3675 * (a) push or pop $tag from $openTags as needed
3676 * (b) clear $tag value
3677 * @param string &$tag Current HTML tag name we are looking at
3678 * @param int $tagType (0-open tag, 1-close tag)
3679 * @param string $lastCh Character before the '>' that ended this tag
3680 * @param array &$openTags Open tag stack (not accounting for $tag)
3681 */
3682 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3683 $tag = ltrim( $tag );
3684 if ( $tag != '' ) {
3685 if ( $tagType == 0 && $lastCh != '/' ) {
3686 $openTags[] = $tag; // tag opened (didn't close itself)
3687 } elseif ( $tagType == 1 ) {
3688 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3689 array_pop( $openTags ); // tag closed
3690 }
3691 }
3692 $tag = '';
3693 }
3694 }
3695
3696 /**
3697 * Grammatical transformations, needed for inflected languages
3698 * Invoked by putting {{grammar:case|word}} in a message
3699 *
3700 * @param string $word
3701 * @param string $case
3702 * @return string
3703 */
3704 function convertGrammar( $word, $case ) {
3705 global $wgGrammarForms;
3706 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3707 return $wgGrammarForms[$this->getCode()][$case][$word];
3708 }
3709
3710 return $word;
3711 }
3712 /**
3713 * Get the grammar forms for the content language
3714 * @return array Array of grammar forms
3715 * @since 1.20
3716 */
3717 function getGrammarForms() {
3718 global $wgGrammarForms;
3719 if ( isset( $wgGrammarForms[$this->getCode()] )
3720 && is_array( $wgGrammarForms[$this->getCode()] )
3721 ) {
3722 return $wgGrammarForms[$this->getCode()];
3723 }
3724
3725 return array();
3726 }
3727 /**
3728 * Provides an alternative text depending on specified gender.
3729 * Usage {{gender:username|masculine|feminine|unknown}}.
3730 * username is optional, in which case the gender of current user is used,
3731 * but only in (some) interface messages; otherwise default gender is used.
3732 *
3733 * If no forms are given, an empty string is returned. If only one form is
3734 * given, it will be returned unconditionally. These details are implied by
3735 * the caller and cannot be overridden in subclasses.
3736 *
3737 * If three forms are given, the default is to use the third (unknown) form.
3738 * If fewer than three forms are given, the default is to use the first (masculine) form.
3739 * These details can be overridden in subclasses.
3740 *
3741 * @param string $gender
3742 * @param array $forms
3743 *
3744 * @return string
3745 */
3746 function gender( $gender, $forms ) {
3747 if ( !count( $forms ) ) {
3748 return '';
3749 }
3750 $forms = $this->preConvertPlural( $forms, 2 );
3751 if ( $gender === 'male' ) {
3752 return $forms[0];
3753 }
3754 if ( $gender === 'female' ) {
3755 return $forms[1];
3756 }
3757 return isset( $forms[2] ) ? $forms[2] : $forms[0];
3758 }
3759
3760 /**
3761 * Plural form transformations, needed for some languages.
3762 * For example, there are 3 form of plural in Russian and Polish,
3763 * depending on "count mod 10". See [[w:Plural]]
3764 * For English it is pretty simple.
3765 *
3766 * Invoked by putting {{plural:count|wordform1|wordform2}}
3767 * or {{plural:count|wordform1|wordform2|wordform3}}
3768 *
3769 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3770 *
3771 * @param int $count Non-localized number
3772 * @param array $forms Different plural forms
3773 * @return string Correct form of plural for $count in this language
3774 */
3775 function convertPlural( $count, $forms ) {
3776 // Handle explicit n=pluralform cases
3777 $forms = $this->handleExplicitPluralForms( $count, $forms );
3778 if ( is_string( $forms ) ) {
3779 return $forms;
3780 }
3781 if ( !count( $forms ) ) {
3782 return '';
3783 }
3784
3785 $pluralForm = $this->getPluralRuleIndexNumber( $count );
3786 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3787 return $forms[$pluralForm];
3788 }
3789
3790 /**
3791 * Handles explicit plural forms for Language::convertPlural()
3792 *
3793 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3794 * If an explicitly defined plural form matches the $count, then
3795 * string value returned, otherwise array returned for further consideration
3796 * by CLDR rules or overridden convertPlural().
3797 *
3798 * @since 1.23
3799 *
3800 * @param int $count non-localized number
3801 * @param array $forms different plural forms
3802 *
3803 * @return array|string
3804 */
3805 protected function handleExplicitPluralForms( $count, array $forms ) {
3806 foreach ( $forms as $index => $form ) {
3807 if ( preg_match( '/\d+=/i', $form ) ) {
3808 $pos = strpos( $form, '=' );
3809 if ( substr( $form, 0, $pos ) === (string) $count ) {
3810 return substr( $form, $pos + 1 );
3811 }
3812 unset( $forms[$index] );
3813 }
3814 }
3815 return array_values( $forms );
3816 }
3817
3818 /**
3819 * Checks that convertPlural was given an array and pads it to requested
3820 * amount of forms by copying the last one.
3821 *
3822 * @param int $count How many forms should there be at least
3823 * @param array $forms Array of forms given to convertPlural
3824 * @return array Padded array of forms or an exception if not an array
3825 */
3826 protected function preConvertPlural( /* Array */ $forms, $count ) {
3827 while ( count( $forms ) < $count ) {
3828 $forms[] = $forms[count( $forms ) - 1];
3829 }
3830 return $forms;
3831 }
3832
3833 /**
3834 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
3835 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3836 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3837 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3838 * match up with it.
3839 *
3840 * @param string $str The validated block duration in English
3841 * @return string Somehow translated block duration
3842 * @see LanguageFi.php for example implementation
3843 */
3844 function translateBlockExpiry( $str ) {
3845 $duration = SpecialBlock::getSuggestedDurations( $this );
3846 foreach ( $duration as $show => $value ) {
3847 if ( strcmp( $str, $value ) == 0 ) {
3848 return htmlspecialchars( trim( $show ) );
3849 }
3850 }
3851
3852 // Since usually only infinite or indefinite is only on list, so try
3853 // equivalents if still here.
3854 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3855 if ( in_array( $str, $indefs ) ) {
3856 foreach ( $indefs as $val ) {
3857 $show = array_search( $val, $duration, true );
3858 if ( $show !== false ) {
3859 return htmlspecialchars( trim( $show ) );
3860 }
3861 }
3862 }
3863
3864 // If all else fails, return a standard duration or timestamp description.
3865 $time = strtotime( $str, 0 );
3866 if ( $time === false ) { // Unknown format. Return it as-is in case.
3867 return $str;
3868 } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
3869 // $time is relative to 0 so it's a duration length.
3870 return $this->formatDuration( $time );
3871 } else { // It's an absolute timestamp.
3872 if ( $time === 0 ) {
3873 // wfTimestamp() handles 0 as current time instead of epoch.
3874 return $this->timeanddate( '19700101000000' );
3875 } else {
3876 return $this->timeanddate( $time );
3877 }
3878 }
3879 }
3880
3881 /**
3882 * languages like Chinese need to be segmented in order for the diff
3883 * to be of any use
3884 *
3885 * @param string $text
3886 * @return string
3887 */
3888 public function segmentForDiff( $text ) {
3889 return $text;
3890 }
3891
3892 /**
3893 * and unsegment to show the result
3894 *
3895 * @param string $text
3896 * @return string
3897 */
3898 public function unsegmentForDiff( $text ) {
3899 return $text;
3900 }
3901
3902 /**
3903 * Return the LanguageConverter used in the Language
3904 *
3905 * @since 1.19
3906 * @return LanguageConverter
3907 */
3908 public function getConverter() {
3909 return $this->mConverter;
3910 }
3911
3912 /**
3913 * convert text to all supported variants
3914 *
3915 * @param string $text
3916 * @return array
3917 */
3918 public function autoConvertToAllVariants( $text ) {
3919 return $this->mConverter->autoConvertToAllVariants( $text );
3920 }
3921
3922 /**
3923 * convert text to different variants of a language.
3924 *
3925 * @param string $text
3926 * @return string
3927 */
3928 public function convert( $text ) {
3929 return $this->mConverter->convert( $text );
3930 }
3931
3932 /**
3933 * Convert a Title object to a string in the preferred variant
3934 *
3935 * @param Title $title
3936 * @return string
3937 */
3938 public function convertTitle( $title ) {
3939 return $this->mConverter->convertTitle( $title );
3940 }
3941
3942 /**
3943 * Convert a namespace index to a string in the preferred variant
3944 *
3945 * @param int $ns
3946 * @return string
3947 */
3948 public function convertNamespace( $ns ) {
3949 return $this->mConverter->convertNamespace( $ns );
3950 }
3951
3952 /**
3953 * Check if this is a language with variants
3954 *
3955 * @return bool
3956 */
3957 public function hasVariants() {
3958 return count( $this->getVariants() ) > 1;
3959 }
3960
3961 /**
3962 * Check if the language has the specific variant
3963 *
3964 * @since 1.19
3965 * @param string $variant
3966 * @return bool
3967 */
3968 public function hasVariant( $variant ) {
3969 return (bool)$this->mConverter->validateVariant( $variant );
3970 }
3971
3972 /**
3973 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3974 *
3975 * @param string $text
3976 * @return string
3977 * @deprecated since 1.22 is no longer used
3978 */
3979 public function armourMath( $text ) {
3980 return $this->mConverter->armourMath( $text );
3981 }
3982
3983 /**
3984 * Perform output conversion on a string, and encode for safe HTML output.
3985 * @param string $text Text to be converted
3986 * @param bool $isTitle Whether this conversion is for the article title
3987 * @return string
3988 * @todo this should get integrated somewhere sane
3989 */
3990 public function convertHtml( $text, $isTitle = false ) {
3991 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3992 }
3993
3994 /**
3995 * @param string $key
3996 * @return string
3997 */
3998 public function convertCategoryKey( $key ) {
3999 return $this->mConverter->convertCategoryKey( $key );
4000 }
4001
4002 /**
4003 * Get the list of variants supported by this language
4004 * see sample implementation in LanguageZh.php
4005 *
4006 * @return array an array of language codes
4007 */
4008 public function getVariants() {
4009 return $this->mConverter->getVariants();
4010 }
4011
4012 /**
4013 * @return string
4014 */
4015 public function getPreferredVariant() {
4016 return $this->mConverter->getPreferredVariant();
4017 }
4018
4019 /**
4020 * @return string
4021 */
4022 public function getDefaultVariant() {
4023 return $this->mConverter->getDefaultVariant();
4024 }
4025
4026 /**
4027 * @return string
4028 */
4029 public function getURLVariant() {
4030 return $this->mConverter->getURLVariant();
4031 }
4032
4033 /**
4034 * If a language supports multiple variants, it is
4035 * possible that non-existing link in one variant
4036 * actually exists in another variant. this function
4037 * tries to find it. See e.g. LanguageZh.php
4038 *
4039 * @param string $link The name of the link
4040 * @param Title $nt The title object of the link
4041 * @param bool $ignoreOtherCond To disable other conditions when
4042 * we need to transclude a template or update a category's link
4043 * @return null the input parameters may be modified upon return
4044 */
4045 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4046 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4047 }
4048
4049 /**
4050 * returns language specific options used by User::getPageRenderHash()
4051 * for example, the preferred language variant
4052 *
4053 * @return string
4054 */
4055 function getExtraHashOptions() {
4056 return $this->mConverter->getExtraHashOptions();
4057 }
4058
4059 /**
4060 * For languages that support multiple variants, the title of an
4061 * article may be displayed differently in different variants. this
4062 * function returns the apporiate title defined in the body of the article.
4063 *
4064 * @return string
4065 */
4066 public function getParsedTitle() {
4067 return $this->mConverter->getParsedTitle();
4068 }
4069
4070 /**
4071 * Prepare external link text for conversion. When the text is
4072 * a URL, it shouldn't be converted, and it'll be wrapped in
4073 * the "raw" tag (-{R| }-) to prevent conversion.
4074 *
4075 * This function is called "markNoConversion" for historical
4076 * reasons.
4077 *
4078 * @param string $text Text to be used for external link
4079 * @param bool $noParse Wrap it without confirming it's a real URL first
4080 * @return string The tagged text
4081 */
4082 public function markNoConversion( $text, $noParse = false ) {
4083 // Excluding protocal-relative URLs may avoid many false positives.
4084 if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4085 return $this->mConverter->markNoConversion( $text );
4086 } else {
4087 return $text;
4088 }
4089 }
4090
4091 /**
4092 * A regular expression to match legal word-trailing characters
4093 * which should be merged onto a link of the form [[foo]]bar.
4094 *
4095 * @return string
4096 */
4097 public function linkTrail() {
4098 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4099 }
4100
4101 /**
4102 * A regular expression character set to match legal word-prefixing
4103 * characters which should be merged onto a link of the form foo[[bar]].
4104 *
4105 * @return string
4106 */
4107 public function linkPrefixCharset() {
4108 return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4109 }
4110
4111 /**
4112 * @deprecated since 1.24, will be removed in 1.25
4113 * @return Language
4114 */
4115 function getLangObj() {
4116 wfDeprecated( __METHOD__, '1.24' );
4117 return $this;
4118 }
4119
4120 /**
4121 * Get the "parent" language which has a converter to convert a "compatible" language
4122 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4123 *
4124 * @return Language|null
4125 * @since 1.22
4126 */
4127 public function getParentLanguage() {
4128 if ( $this->mParentLanguage !== false ) {
4129 return $this->mParentLanguage;
4130 }
4131
4132 $pieces = explode( '-', $this->getCode() );
4133 $code = $pieces[0];
4134 if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4135 $this->mParentLanguage = null;
4136 return null;
4137 }
4138 $lang = Language::factory( $code );
4139 if ( !$lang->hasVariant( $this->getCode() ) ) {
4140 $this->mParentLanguage = null;
4141 return null;
4142 }
4143
4144 $this->mParentLanguage = $lang;
4145 return $lang;
4146 }
4147
4148 /**
4149 * Get the RFC 3066 code for this language object
4150 *
4151 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4152 * htmlspecialchars() or similar
4153 *
4154 * @return string
4155 */
4156 public function getCode() {
4157 return $this->mCode;
4158 }
4159
4160 /**
4161 * Get the code in Bcp47 format which we can use
4162 * inside of html lang="" tags.
4163 *
4164 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4165 * htmlspecialchars() or similar.
4166 *
4167 * @since 1.19
4168 * @return string
4169 */
4170 public function getHtmlCode() {
4171 if ( is_null( $this->mHtmlCode ) ) {
4172 $this->mHtmlCode = wfBCP47( $this->getCode() );
4173 }
4174 return $this->mHtmlCode;
4175 }
4176
4177 /**
4178 * @param string $code
4179 */
4180 public function setCode( $code ) {
4181 $this->mCode = $code;
4182 // Ensure we don't leave incorrect cached data lying around
4183 $this->mHtmlCode = null;
4184 $this->mParentLanguage = false;
4185 }
4186
4187 /**
4188 * Get the name of a file for a certain language code
4189 * @param string $prefix Prepend this to the filename
4190 * @param string $code Language code
4191 * @param string $suffix Append this to the filename
4192 * @throws MWException
4193 * @return string $prefix . $mangledCode . $suffix
4194 */
4195 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4196 if ( !self::isValidBuiltInCode( $code ) ) {
4197 throw new MWException( "Invalid language code \"$code\"" );
4198 }
4199
4200 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4201 }
4202
4203 /**
4204 * Get the language code from a file name. Inverse of getFileName()
4205 * @param string $filename $prefix . $languageCode . $suffix
4206 * @param string $prefix Prefix before the language code
4207 * @param string $suffix Suffix after the language code
4208 * @return string Language code, or false if $prefix or $suffix isn't found
4209 */
4210 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4211 $m = null;
4212 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4213 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4214 if ( !count( $m ) ) {
4215 return false;
4216 }
4217 return str_replace( '_', '-', strtolower( $m[1] ) );
4218 }
4219
4220 /**
4221 * @param string $code
4222 * @return string
4223 */
4224 public static function getMessagesFileName( $code ) {
4225 global $IP;
4226 $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4227 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
4228 return $file;
4229 }
4230
4231 /**
4232 * @param string $code
4233 * @return string
4234 * @since 1.23
4235 */
4236 public static function getJsonMessagesFileName( $code ) {
4237 global $IP;
4238
4239 if ( !self::isValidBuiltInCode( $code ) ) {
4240 throw new MWException( "Invalid language code \"$code\"" );
4241 }
4242
4243 return "$IP/languages/i18n/$code.json";
4244 }
4245
4246 /**
4247 * @param string $code
4248 * @return string
4249 */
4250 public static function getClassFileName( $code ) {
4251 global $IP;
4252 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
4253 }
4254
4255 /**
4256 * Get the first fallback for a given language.
4257 *
4258 * @param string $code
4259 *
4260 * @return bool|string
4261 */
4262 public static function getFallbackFor( $code ) {
4263 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4264 return false;
4265 } else {
4266 $fallbacks = self::getFallbacksFor( $code );
4267 $first = array_shift( $fallbacks );
4268 return $first;
4269 }
4270 }
4271
4272 /**
4273 * Get the ordered list of fallback languages.
4274 *
4275 * @since 1.19
4276 * @param string $code Language code
4277 * @return array
4278 */
4279 public static function getFallbacksFor( $code ) {
4280 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4281 return array();
4282 } else {
4283 $v = self::getLocalisationCache()->getItem( $code, 'fallback' );
4284 $v = array_map( 'trim', explode( ',', $v ) );
4285 if ( $v[count( $v ) - 1] !== 'en' ) {
4286 $v[] = 'en';
4287 }
4288 return $v;
4289 }
4290 }
4291
4292 /**
4293 * Get the ordered list of fallback languages, ending with the fallback
4294 * language chain for the site language.
4295 *
4296 * @since 1.22
4297 * @param string $code Language code
4298 * @return array array( fallbacks, site fallbacks )
4299 */
4300 public static function getFallbacksIncludingSiteLanguage( $code ) {
4301 global $wgLanguageCode;
4302
4303 // Usually, we will only store a tiny number of fallback chains, so we
4304 // keep them in static memory.
4305 $cacheKey = "{$code}-{$wgLanguageCode}";
4306
4307 if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4308 $fallbacks = self::getFallbacksFor( $code );
4309
4310 // Append the site's fallback chain, including the site language itself
4311 $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4312 array_unshift( $siteFallbacks, $wgLanguageCode );
4313
4314 // Eliminate any languages already included in the chain
4315 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4316
4317 self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
4318 }
4319 return self::$fallbackLanguageCache[$cacheKey];
4320 }
4321
4322 /**
4323 * Get all messages for a given language
4324 * WARNING: this may take a long time. If you just need all message *keys*
4325 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4326 *
4327 * @param string $code
4328 *
4329 * @return array
4330 */
4331 public static function getMessagesFor( $code ) {
4332 return self::getLocalisationCache()->getItem( $code, 'messages' );
4333 }
4334
4335 /**
4336 * Get a message for a given language
4337 *
4338 * @param string $key
4339 * @param string $code
4340 *
4341 * @return string
4342 */
4343 public static function getMessageFor( $key, $code ) {
4344 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4345 }
4346
4347 /**
4348 * Get all message keys for a given language. This is a faster alternative to
4349 * array_keys( Language::getMessagesFor( $code ) )
4350 *
4351 * @since 1.19
4352 * @param string $code Language code
4353 * @return array of message keys (strings)
4354 */
4355 public static function getMessageKeysFor( $code ) {
4356 return self::getLocalisationCache()->getSubItemList( $code, 'messages' );
4357 }
4358
4359 /**
4360 * @param string $talk
4361 * @return mixed
4362 */
4363 function fixVariableInNamespace( $talk ) {
4364 if ( strpos( $talk, '$1' ) === false ) {
4365 return $talk;
4366 }
4367
4368 global $wgMetaNamespace;
4369 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4370
4371 # Allow grammar transformations
4372 # Allowing full message-style parsing would make simple requests
4373 # such as action=raw much more expensive than they need to be.
4374 # This will hopefully cover most cases.
4375 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4376 array( &$this, 'replaceGrammarInNamespace' ), $talk );
4377 return str_replace( ' ', '_', $talk );
4378 }
4379
4380 /**
4381 * @param string $m
4382 * @return string
4383 */
4384 function replaceGrammarInNamespace( $m ) {
4385 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4386 }
4387
4388 /**
4389 * @throws MWException
4390 * @return array
4391 */
4392 static function getCaseMaps() {
4393 static $wikiUpperChars, $wikiLowerChars;
4394 if ( isset( $wikiUpperChars ) ) {
4395 return array( $wikiUpperChars, $wikiLowerChars );
4396 }
4397
4398 wfProfileIn( __METHOD__ );
4399 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4400 if ( $arr === false ) {
4401 throw new MWException(
4402 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4403 }
4404 $wikiUpperChars = $arr['wikiUpperChars'];
4405 $wikiLowerChars = $arr['wikiLowerChars'];
4406 wfProfileOut( __METHOD__ );
4407 return array( $wikiUpperChars, $wikiLowerChars );
4408 }
4409
4410 /**
4411 * Decode an expiry (block, protection, etc) which has come from the DB
4412 *
4413 * @todo FIXME: why are we returnings DBMS-dependent strings???
4414 *
4415 * @param string $expiry Database expiry String
4416 * @param bool|int $format True to process using language functions, or TS_ constant
4417 * to return the expiry in a given timestamp
4418 * @return string
4419 * @since 1.18
4420 */
4421 public function formatExpiry( $expiry, $format = true ) {
4422 static $infinity;
4423 if ( $infinity === null ) {
4424 $infinity = wfGetDB( DB_SLAVE )->getInfinity();
4425 }
4426
4427 if ( $expiry == '' || $expiry == $infinity ) {
4428 return $format === true
4429 ? $this->getMessageFromDB( 'infiniteblock' )
4430 : $infinity;
4431 } else {
4432 return $format === true
4433 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
4434 : wfTimestamp( $format, $expiry );
4435 }
4436 }
4437
4438 /**
4439 * @todo Document
4440 * @param int|float $seconds
4441 * @param array $format Optional
4442 * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4443 * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4444 * If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4445 * and friends.
4446 * For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4447 * or 'avoidminutes'.
4448 * @return string
4449 */
4450 function formatTimePeriod( $seconds, $format = array() ) {
4451 if ( !is_array( $format ) ) {
4452 $format = array( 'avoid' => $format ); // For backwards compatibility
4453 }
4454 if ( !isset( $format['avoid'] ) ) {
4455 $format['avoid'] = false;
4456 }
4457 if ( !isset( $format['noabbrevs' ] ) ) {
4458 $format['noabbrevs'] = false;
4459 }
4460 $secondsMsg = wfMessage(
4461 $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4462 $minutesMsg = wfMessage(
4463 $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4464 $hoursMsg = wfMessage(
4465 $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4466 $daysMsg = wfMessage(
4467 $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4468
4469 if ( round( $seconds * 10 ) < 100 ) {
4470 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4471 $s = $secondsMsg->params( $s )->text();
4472 } elseif ( round( $seconds ) < 60 ) {
4473 $s = $this->formatNum( round( $seconds ) );
4474 $s = $secondsMsg->params( $s )->text();
4475 } elseif ( round( $seconds ) < 3600 ) {
4476 $minutes = floor( $seconds / 60 );
4477 $secondsPart = round( fmod( $seconds, 60 ) );
4478 if ( $secondsPart == 60 ) {
4479 $secondsPart = 0;
4480 $minutes++;
4481 }
4482 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4483 $s .= ' ';
4484 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4485 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4486 $hours = floor( $seconds / 3600 );
4487 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4488 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4489 if ( $secondsPart == 60 ) {
4490 $secondsPart = 0;
4491 $minutes++;
4492 }
4493 if ( $minutes == 60 ) {
4494 $minutes = 0;
4495 $hours++;
4496 }
4497 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4498 $s .= ' ';
4499 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4500 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4501 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4502 }
4503 } else {
4504 $days = floor( $seconds / 86400 );
4505 if ( $format['avoid'] === 'avoidminutes' ) {
4506 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4507 if ( $hours == 24 ) {
4508 $hours = 0;
4509 $days++;
4510 }
4511 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4512 $s .= ' ';
4513 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4514 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4515 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4516 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4517 if ( $minutes == 60 ) {
4518 $minutes = 0;
4519 $hours++;
4520 }
4521 if ( $hours == 24 ) {
4522 $hours = 0;
4523 $days++;
4524 }
4525 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4526 $s .= ' ';
4527 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4528 $s .= ' ';
4529 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4530 } else {
4531 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4532 $s .= ' ';
4533 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4534 }
4535 }
4536 return $s;
4537 }
4538
4539 /**
4540 * Format a bitrate for output, using an appropriate
4541 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4542 * the magnitude in question.
4543 *
4544 * This use base 1000. For base 1024 use formatSize(), for another base
4545 * see formatComputingNumbers().
4546 *
4547 * @param int $bps
4548 * @return string
4549 */
4550 function formatBitrate( $bps ) {
4551 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4552 }
4553
4554 /**
4555 * @param int $size Size of the unit
4556 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4557 * @param string $messageKey Message key to be uesd
4558 * @return string
4559 */
4560 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4561 if ( $size <= 0 ) {
4562 return str_replace( '$1', $this->formatNum( $size ),
4563 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4564 );
4565 }
4566 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4567 $index = 0;
4568
4569 $maxIndex = count( $sizes ) - 1;
4570 while ( $size >= $boundary && $index < $maxIndex ) {
4571 $index++;
4572 $size /= $boundary;
4573 }
4574
4575 // For small sizes no decimal places necessary
4576 $round = 0;
4577 if ( $index > 1 ) {
4578 // For MB and bigger two decimal places are smarter
4579 $round = 2;
4580 }
4581 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4582
4583 $size = round( $size, $round );
4584 $text = $this->getMessageFromDB( $msg );
4585 return str_replace( '$1', $this->formatNum( $size ), $text );
4586 }
4587
4588 /**
4589 * Format a size in bytes for output, using an appropriate
4590 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4591 *
4592 * This method use base 1024. For base 1000 use formatBitrate(), for
4593 * another base see formatComputingNumbers()
4594 *
4595 * @param int $size Size to format
4596 * @return string Plain text (not HTML)
4597 */
4598 function formatSize( $size ) {
4599 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4600 }
4601
4602 /**
4603 * Make a list item, used by various special pages
4604 *
4605 * @param string $page Page link
4606 * @param string $details Text between brackets
4607 * @param bool $oppositedm Add the direction mark opposite to your
4608 * language, to display text properly
4609 * @return string
4610 */
4611 function specialList( $page, $details, $oppositedm = true ) {
4612 $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) .
4613 $this->getDirMark();
4614 $details = $details ? $dirmark . $this->getMessageFromDB( 'word-separator' ) .
4615 wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : '';
4616 return $page . $details;
4617 }
4618
4619 /**
4620 * Generate (prev x| next x) (20|50|100...) type links for paging
4621 *
4622 * @param Title $title Title object to link
4623 * @param int $offset
4624 * @param int $limit
4625 * @param array|string $query Optional URL query parameter string
4626 * @param bool $atend Optional param for specified if this is the last page
4627 * @return string
4628 */
4629 public function viewPrevNext( Title $title, $offset, $limit,
4630 array $query = array(), $atend = false
4631 ) {
4632 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4633
4634 # Make 'previous' link
4635 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4636 if ( $offset > 0 ) {
4637 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4638 $query, $prev, 'prevn-title', 'mw-prevlink' );
4639 } else {
4640 $plink = htmlspecialchars( $prev );
4641 }
4642
4643 # Make 'next' link
4644 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4645 if ( $atend ) {
4646 $nlink = htmlspecialchars( $next );
4647 } else {
4648 $nlink = $this->numLink( $title, $offset + $limit, $limit,
4649 $query, $next, 'nextn-title', 'mw-nextlink' );
4650 }
4651
4652 # Make links to set number of items per page
4653 $numLinks = array();
4654 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4655 $numLinks[] = $this->numLink( $title, $offset, $num,
4656 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4657 }
4658
4659 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4660 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4661 }
4662
4663 /**
4664 * Helper function for viewPrevNext() that generates links
4665 *
4666 * @param Title $title Title object to link
4667 * @param int $offset
4668 * @param int $limit
4669 * @param array $query Extra query parameters
4670 * @param string $link Text to use for the link; will be escaped
4671 * @param string $tooltipMsg Name of the message to use as tooltip
4672 * @param string $class Value of the "class" attribute of the link
4673 * @return string HTML fragment
4674 */
4675 private function numLink( Title $title, $offset, $limit, array $query, $link,
4676 $tooltipMsg, $class
4677 ) {
4678 $query = array( 'limit' => $limit, 'offset' => $offset ) + $query;
4679 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4680 ->numParams( $limit )->text();
4681
4682 return Html::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4683 'title' => $tooltip, 'class' => $class ), $link );
4684 }
4685
4686 /**
4687 * Get the conversion rule title, if any.
4688 *
4689 * @return string
4690 */
4691 public function getConvRuleTitle() {
4692 return $this->mConverter->getConvRuleTitle();
4693 }
4694
4695 /**
4696 * Get the compiled plural rules for the language
4697 * @since 1.20
4698 * @return array Associative array with plural form, and plural rule as key-value pairs
4699 */
4700 public function getCompiledPluralRules() {
4701 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4702 $fallbacks = Language::getFallbacksFor( $this->mCode );
4703 if ( !$pluralRules ) {
4704 foreach ( $fallbacks as $fallbackCode ) {
4705 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4706 if ( $pluralRules ) {
4707 break;
4708 }
4709 }
4710 }
4711 return $pluralRules;
4712 }
4713
4714 /**
4715 * Get the plural rules for the language
4716 * @since 1.20
4717 * @return array Associative array with plural form number and plural rule as key-value pairs
4718 */
4719 public function getPluralRules() {
4720 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4721 $fallbacks = Language::getFallbacksFor( $this->mCode );
4722 if ( !$pluralRules ) {
4723 foreach ( $fallbacks as $fallbackCode ) {
4724 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4725 if ( $pluralRules ) {
4726 break;
4727 }
4728 }
4729 }
4730 return $pluralRules;
4731 }
4732
4733 /**
4734 * Get the plural rule types for the language
4735 * @since 1.22
4736 * @return array Associative array with plural form number and plural rule type as key-value pairs
4737 */
4738 public function getPluralRuleTypes() {
4739 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4740 $fallbacks = Language::getFallbacksFor( $this->mCode );
4741 if ( !$pluralRuleTypes ) {
4742 foreach ( $fallbacks as $fallbackCode ) {
4743 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4744 if ( $pluralRuleTypes ) {
4745 break;
4746 }
4747 }
4748 }
4749 return $pluralRuleTypes;
4750 }
4751
4752 /**
4753 * Find the index number of the plural rule appropriate for the given number
4754 * @return int The index number of the plural rule
4755 */
4756 public function getPluralRuleIndexNumber( $number ) {
4757 $pluralRules = $this->getCompiledPluralRules();
4758 $form = CLDRPluralRuleEvaluator::evaluateCompiled( $number, $pluralRules );
4759 return $form;
4760 }
4761
4762 /**
4763 * Find the plural rule type appropriate for the given number
4764 * For example, if the language is set to Arabic, getPluralType(5) should
4765 * return 'few'.
4766 * @since 1.22
4767 * @return string The name of the plural rule type, e.g. one, two, few, many
4768 */
4769 public function getPluralRuleType( $number ) {
4770 $index = $this->getPluralRuleIndexNumber( $number );
4771 $pluralRuleTypes = $this->getPluralRuleTypes();
4772 if ( isset( $pluralRuleTypes[$index] ) ) {
4773 return $pluralRuleTypes[$index];
4774 } else {
4775 return 'other';
4776 }
4777 }
4778 }