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