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