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