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