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