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