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