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