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