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