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