Remove translations of optional 'listgrouprights-right-display' that are the same...
[mediawiki.git] / languages / Language.php
blobe0fb27382772bdbbf775d7f85e813c1e06caf2a4
1 <?php
2 /**
3 * @defgroup Language Language
5 * @file
6 * @ingroup Language
7 */
9 if( !defined( 'MEDIAWIKI' ) ) {
10 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
11 exit( 1 );
14 # Read language names
15 global $wgLanguageNames;
16 require_once( dirname(__FILE__) . '/Names.php' ) ;
18 global $wgInputEncoding, $wgOutputEncoding;
20 /**
21 * These are always UTF-8, they exist only for backwards compatibility
23 $wgInputEncoding = "UTF-8";
24 $wgOutputEncoding = "UTF-8";
26 if( function_exists( 'mb_strtoupper' ) ) {
27 mb_internal_encoding('UTF-8');
30 /**
31 * a fake language converter
33 * @ingroup Language
35 class FakeConverter {
36 var $mLang;
37 function FakeConverter($langobj) {$this->mLang = $langobj;}
38 function autoConvertToAllVariants($text) {return $text;}
39 function convert($t, $i) {return $t;}
40 function parserConvert($t, $p) {return $t;}
41 function getVariants() { return array( $this->mLang->getCode() ); }
42 function getPreferredVariant() {return $this->mLang->getCode(); }
43 function findVariantLink(&$l, &$n, $ignoreOtherCond = false) {}
44 function getExtraHashOptions() {return '';}
45 function getParsedTitle() {return '';}
46 function markNoConversion($text, $noParse=false) {return $text;}
47 function convertCategoryKey( $key ) {return $key; }
48 function convertLinkToAllVariants($text){ return array( $this->mLang->getCode() => $text); }
49 function armourMath($text){ return $text; }
52 /**
53 * Internationalisation code
54 * @ingroup Language
56 class Language {
57 var $mConverter, $mVariants, $mCode, $mLoaded = false;
58 var $mMagicExtensions = array(), $mMagicHookDone = false;
60 static public $mLocalisationKeys = array(
61 'fallback', 'namespaceNames', 'mathNames', 'bookstoreList',
62 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
63 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
64 'defaultUserOptionOverrides', 'linkTrail', 'namespaceAliases',
65 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
66 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
67 'imageFiles'
70 static public $mMergeableMapKeys = array( 'messages', 'namespaceNames', 'mathNames',
71 'dateFormats', 'defaultUserOptionOverrides', 'magicWords', 'imageFiles' );
73 static public $mMergeableListKeys = array( 'extraUserToggles' );
75 static public $mMergeableAliasListKeys = array( 'specialPageAliases' );
77 static public $mLocalisationCache = array();
78 static public $mLangObjCache = array();
80 static public $mWeekdayMsgs = array(
81 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
82 'friday', 'saturday'
85 static public $mWeekdayAbbrevMsgs = array(
86 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
89 static public $mMonthMsgs = array(
90 'january', 'february', 'march', 'april', 'may_long', 'june',
91 'july', 'august', 'september', 'october', 'november',
92 'december'
94 static public $mMonthGenMsgs = array(
95 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
96 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
97 'december-gen'
99 static public $mMonthAbbrevMsgs = array(
100 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
101 'sep', 'oct', 'nov', 'dec'
104 static public $mIranianCalendarMonthMsgs = array(
105 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
106 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
107 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
108 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
111 static public $mHebrewCalendarMonthMsgs = array(
112 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
113 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
114 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
115 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
116 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
119 static public $mHebrewCalendarMonthGenMsgs = array(
120 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
121 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
122 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
123 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
124 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
127 static public $mHijriCalendarMonthMsgs = array(
128 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
129 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
130 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
131 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
135 * Get a cached language object for a given language code
137 static function factory( $code ) {
138 if ( !isset( self::$mLangObjCache[$code] ) ) {
139 if( count( self::$mLangObjCache ) > 10 ) {
140 // Don't keep a billion objects around, that's stupid.
141 self::$mLangObjCache = array();
143 self::$mLangObjCache[$code] = self::newFromCode( $code );
145 return self::$mLangObjCache[$code];
149 * Create a language object for a given language code
151 protected static function newFromCode( $code ) {
152 global $IP;
153 static $recursionLevel = 0;
154 if ( $code == 'en' ) {
155 $class = 'Language';
156 } else {
157 $class = 'Language' . str_replace( '-', '_', ucfirst( $code ) );
158 // Preload base classes to work around APC/PHP5 bug
159 if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
160 include_once("$IP/languages/classes/$class.deps.php");
162 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
163 include_once("$IP/languages/classes/$class.php");
167 if ( $recursionLevel > 5 ) {
168 throw new MWException( "Language fallback loop detected when creating class $class\n" );
171 if( ! class_exists( $class ) ) {
172 $fallback = Language::getFallbackFor( $code );
173 ++$recursionLevel;
174 $lang = Language::newFromCode( $fallback );
175 --$recursionLevel;
176 $lang->setCode( $code );
177 } else {
178 $lang = new $class;
180 return $lang;
183 function __construct() {
184 $this->mConverter = new FakeConverter($this);
185 // Set the code to the name of the descendant
186 if ( get_class( $this ) == 'Language' ) {
187 $this->mCode = 'en';
188 } else {
189 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
194 * Reduce memory usage
196 function __destruct() {
197 foreach ( $this as $name => $value ) {
198 unset( $this->$name );
203 * Hook which will be called if this is the content language.
204 * Descendants can use this to register hook functions or modify globals
206 function initContLang() {}
209 * @deprecated Use User::getDefaultOptions()
210 * @return array
212 function getDefaultUserOptions() {
213 wfDeprecated( __METHOD__ );
214 return User::getDefaultOptions();
217 function getFallbackLanguageCode() {
218 return self::getFallbackFor( $this->mCode );
222 * Exports $wgBookstoreListEn
223 * @return array
225 function getBookstoreList() {
226 $this->load();
227 return $this->bookstoreList;
231 * @return array
233 function getNamespaces() {
234 $this->load();
235 return $this->namespaceNames;
239 * A convenience function that returns the same thing as
240 * getNamespaces() except with the array values changed to ' '
241 * where it found '_', useful for producing output to be displayed
242 * e.g. in <select> forms.
244 * @return array
246 function getFormattedNamespaces() {
247 $ns = $this->getNamespaces();
248 foreach($ns as $k => $v) {
249 $ns[$k] = strtr($v, '_', ' ');
251 return $ns;
255 * Get a namespace value by key
256 * <code>
257 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
258 * echo $mw_ns; // prints 'MediaWiki'
259 * </code>
261 * @param $index Int: the array key of the namespace to return
262 * @return mixed, string if the namespace value exists, otherwise false
264 function getNsText( $index ) {
265 $ns = $this->getNamespaces();
266 return isset( $ns[$index] ) ? $ns[$index] : false;
270 * A convenience function that returns the same thing as
271 * getNsText() except with '_' changed to ' ', useful for
272 * producing output.
274 * @return array
276 function getFormattedNsText( $index ) {
277 $ns = $this->getNsText( $index );
278 return strtr($ns, '_', ' ');
282 * Get a namespace key by value, case insensitive.
283 * Only matches namespace names for the current language, not the
284 * canonical ones defined in Namespace.php.
286 * @param $text String
287 * @return mixed An integer if $text is a valid value otherwise false
289 function getLocalNsIndex( $text ) {
290 $this->load();
291 $lctext = $this->lc($text);
292 return isset( $this->mNamespaceIds[$lctext] ) ? $this->mNamespaceIds[$lctext] : false;
296 * Get a namespace key by value, case insensitive. Canonical namespace
297 * names override custom ones defined for the current language.
299 * @param $text String
300 * @return mixed An integer if $text is a valid value otherwise false
302 function getNsIndex( $text ) {
303 $this->load();
304 $lctext = $this->lc($text);
305 if( ( $ns = MWNamespace::getCanonicalIndex( $lctext ) ) !== null ) return $ns;
306 return isset( $this->mNamespaceIds[$lctext] ) ? $this->mNamespaceIds[$lctext] : false;
310 * short names for language variants used for language conversion links.
312 * @param $code String
313 * @return string
315 function getVariantname( $code ) {
316 return $this->getMessageFromDB( "variantname-$code" );
319 function specialPage( $name ) {
320 $aliases = $this->getSpecialPageAliases();
321 if ( isset( $aliases[$name][0] ) ) {
322 $name = $aliases[$name][0];
324 return $this->getNsText( NS_SPECIAL ) . ':' . $name;
327 function getQuickbarSettings() {
328 return array(
329 $this->getMessage( 'qbsettings-none' ),
330 $this->getMessage( 'qbsettings-fixedleft' ),
331 $this->getMessage( 'qbsettings-fixedright' ),
332 $this->getMessage( 'qbsettings-floatingleft' ),
333 $this->getMessage( 'qbsettings-floatingright' )
337 function getMathNames() {
338 $this->load();
339 return $this->mathNames;
342 function getDatePreferences() {
343 $this->load();
344 return $this->datePreferences;
347 function getDateFormats() {
348 $this->load();
349 return $this->dateFormats;
352 function getDefaultDateFormat() {
353 $this->load();
354 return $this->defaultDateFormat;
357 function getDatePreferenceMigrationMap() {
358 $this->load();
359 return $this->datePreferenceMigrationMap;
362 function getImageFile( $image ) {
363 $this->load();
364 return $this->imageFiles[$image];
367 function getDefaultUserOptionOverrides() {
368 $this->load();
369 # XXX - apparently some languageas get empty arrays, didn't get to it yet -- midom
370 if (is_array($this->defaultUserOptionOverrides)) {
371 return $this->defaultUserOptionOverrides;
372 } else {
373 return array();
377 function getExtraUserToggles() {
378 $this->load();
379 return $this->extraUserToggles;
382 function getUserToggle( $tog ) {
383 return $this->getMessageFromDB( "tog-$tog" );
387 * Get language names, indexed by code.
388 * If $customisedOnly is true, only returns codes with a messages file
390 public static function getLanguageNames( $customisedOnly = false ) {
391 global $wgLanguageNames, $wgExtraLanguageNames;
392 $allNames = $wgExtraLanguageNames + $wgLanguageNames;
393 if ( !$customisedOnly ) {
394 return $allNames;
397 global $IP;
398 $names = array();
399 $dir = opendir( "$IP/languages/messages" );
400 while( false !== ( $file = readdir( $dir ) ) ) {
401 $m = array();
402 if( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $file, $m ) ) {
403 $code = str_replace( '_', '-', strtolower( $m[1] ) );
404 if ( isset( $allNames[$code] ) ) {
405 $names[$code] = $allNames[$code];
409 closedir( $dir );
410 return $names;
414 * Get a message from the MediaWiki namespace.
416 * @param $msg String: message name
417 * @return string
419 function getMessageFromDB( $msg ) {
420 return wfMsgExt( $msg, array( 'parsemag', 'language' => $this ) );
423 function getLanguageName( $code ) {
424 $names = self::getLanguageNames();
425 if ( !array_key_exists( $code, $names ) ) {
426 return '';
428 return $names[$code];
431 function getMonthName( $key ) {
432 return $this->getMessageFromDB( self::$mMonthMsgs[$key-1] );
435 function getMonthNameGen( $key ) {
436 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key-1] );
439 function getMonthAbbreviation( $key ) {
440 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key-1] );
443 function getWeekdayName( $key ) {
444 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key-1] );
447 function getWeekdayAbbreviation( $key ) {
448 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key-1] );
451 function getIranianCalendarMonthName( $key ) {
452 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key-1] );
455 function getHebrewCalendarMonthName( $key ) {
456 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key-1] );
459 function getHebrewCalendarMonthNameGen( $key ) {
460 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key-1] );
463 function getHijriCalendarMonthName( $key ) {
464 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key-1] );
468 * Used by date() and time() to adjust the time output.
470 * @param $ts Int the time in date('YmdHis') format
471 * @param $tz Mixed: adjust the time by this amount (default false, mean we
472 * get user timecorrection setting)
473 * @return int
475 function userAdjust( $ts, $tz = false ) {
476 global $wgUser, $wgLocalTZoffset;
478 if ( $tz === false ) {
479 $tz = $wgUser->getOption( 'timecorrection' );
482 $data = explode( '|', $tz, 3 );
484 if ( $data[0] == 'ZoneInfo' ) {
485 if ( function_exists( 'timezone_open' ) && @timezone_open( $data[2] ) !== false ) {
486 $date = date_create( $ts, timezone_open( 'UTC' ) );
487 date_timezone_set( $date, timezone_open( $data[2] ) );
488 $date = date_format( $date, 'YmdHis' );
489 return $date;
491 # Unrecognized timezone, default to 'Offset' with the stored offset.
492 $data[0] = 'Offset';
495 $minDiff = 0;
496 if ( $data[0] == 'System' || $tz == '' ) {
497 # Global offset in minutes.
498 if( isset($wgLocalTZoffset) ) $minDiff = $wgLocalTZoffset;
499 } else if ( $data[0] == 'Offset' ) {
500 $minDiff = intval( $data[1] );
501 } else {
502 $data = explode( ':', $tz );
503 if( count( $data ) == 2 ) {
504 $data[0] = intval( $data[0] );
505 $data[1] = intval( $data[1] );
506 $minDiff = abs( $data[0] ) * 60 + $data[1];
507 if ( $data[0] < 0 ) $minDiff = -$minDiff;
508 } else {
509 $minDiff = intval( $data[0] ) * 60;
513 # No difference ? Return time unchanged
514 if ( 0 == $minDiff ) return $ts;
516 wfSuppressWarnings(); // E_STRICT system time bitching
517 # Generate an adjusted date; take advantage of the fact that mktime
518 # will normalize out-of-range values so we don't have to split $minDiff
519 # into hours and minutes.
520 $t = mktime( (
521 (int)substr( $ts, 8, 2) ), # Hours
522 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
523 (int)substr( $ts, 12, 2 ), # Seconds
524 (int)substr( $ts, 4, 2 ), # Month
525 (int)substr( $ts, 6, 2 ), # Day
526 (int)substr( $ts, 0, 4 ) ); #Year
528 $date = date( 'YmdHis', $t );
529 wfRestoreWarnings();
531 return $date;
535 * This is a workalike of PHP's date() function, but with better
536 * internationalisation, a reduced set of format characters, and a better
537 * escaping format.
539 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
540 * PHP manual for definitions. "o" format character is supported since
541 * PHP 5.1.0, previous versions return literal o.
542 * There are a number of extensions, which start with "x":
544 * xn Do not translate digits of the next numeric format character
545 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
546 * xr Use roman numerals for the next numeric format character
547 * xh Use hebrew numerals for the next numeric format character
548 * xx Literal x
549 * xg Genitive month name
551 * xij j (day number) in Iranian calendar
552 * xiF F (month name) in Iranian calendar
553 * xin n (month number) in Iranian calendar
554 * xiY Y (full year) in Iranian calendar
556 * xjj j (day number) in Hebrew calendar
557 * xjF F (month name) in Hebrew calendar
558 * xjt t (days in month) in Hebrew calendar
559 * xjx xg (genitive month name) in Hebrew calendar
560 * xjn n (month number) in Hebrew calendar
561 * xjY Y (full year) in Hebrew calendar
563 * xmj j (day number) in Hijri calendar
564 * xmF F (month name) in Hijri calendar
565 * xmn n (month number) in Hijri calendar
566 * xmY Y (full year) in Hijri calendar
568 * xkY Y (full year) in Thai solar calendar. Months and days are
569 * identical to the Gregorian calendar
570 * xoY Y (full year) in Minguo calendar or Juche year.
571 * Months and days are identical to the
572 * Gregorian calendar
573 * xtY Y (full year) in Japanese nengo. Months and days are
574 * identical to the Gregorian calendar
576 * Characters enclosed in double quotes will be considered literal (with
577 * the quotes themselves removed). Unmatched quotes will be considered
578 * literal quotes. Example:
580 * "The month is" F => The month is January
581 * i's" => 20'11"
583 * Backslash escaping is also supported.
585 * Input timestamp is assumed to be pre-normalized to the desired local
586 * time zone, if any.
588 * @param $format String
589 * @param $ts String: 14-character timestamp
590 * YYYYMMDDHHMMSS
591 * 01234567890123
592 * @todo emulation of "o" format character for PHP pre 5.1.0
593 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
595 function sprintfDate( $format, $ts ) {
596 $s = '';
597 $raw = false;
598 $roman = false;
599 $hebrewNum = false;
600 $unix = false;
601 $rawToggle = false;
602 $iranian = false;
603 $hebrew = false;
604 $hijri = false;
605 $thai = false;
606 $minguo = false;
607 $tenno = false;
608 for ( $p = 0; $p < strlen( $format ); $p++ ) {
609 $num = false;
610 $code = $format[$p];
611 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
612 $code .= $format[++$p];
615 if ( ( $code === 'xi' || $code == 'xj' || $code == 'xk' || $code == 'xm' || $code == 'xo' || $code == 'xt' ) && $p < strlen( $format ) - 1 ) {
616 $code .= $format[++$p];
619 switch ( $code ) {
620 case 'xx':
621 $s .= 'x';
622 break;
623 case 'xn':
624 $raw = true;
625 break;
626 case 'xN':
627 $rawToggle = !$rawToggle;
628 break;
629 case 'xr':
630 $roman = true;
631 break;
632 case 'xh':
633 $hebrewNum = true;
634 break;
635 case 'xg':
636 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
637 break;
638 case 'xjx':
639 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
640 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
641 break;
642 case 'd':
643 $num = substr( $ts, 6, 2 );
644 break;
645 case 'D':
646 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
647 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) + 1 );
648 break;
649 case 'j':
650 $num = intval( substr( $ts, 6, 2 ) );
651 break;
652 case 'xij':
653 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
654 $num = $iranian[2];
655 break;
656 case 'xmj':
657 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
658 $num = $hijri[2];
659 break;
660 case 'xjj':
661 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
662 $num = $hebrew[2];
663 break;
664 case 'l':
665 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
666 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) + 1 );
667 break;
668 case 'N':
669 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
670 $w = gmdate( 'w', $unix );
671 $num = $w ? $w : 7;
672 break;
673 case 'w':
674 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
675 $num = gmdate( 'w', $unix );
676 break;
677 case 'z':
678 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
679 $num = gmdate( 'z', $unix );
680 break;
681 case 'W':
682 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
683 $num = gmdate( 'W', $unix );
684 break;
685 case 'F':
686 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
687 break;
688 case 'xiF':
689 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
690 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
691 break;
692 case 'xmF':
693 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
694 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
695 break;
696 case 'xjF':
697 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
698 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
699 break;
700 case 'm':
701 $num = substr( $ts, 4, 2 );
702 break;
703 case 'M':
704 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
705 break;
706 case 'n':
707 $num = intval( substr( $ts, 4, 2 ) );
708 break;
709 case 'xin':
710 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
711 $num = $iranian[1];
712 break;
713 case 'xmn':
714 if ( !$hijri ) $hijri = self::tsToHijri ( $ts );
715 $num = $hijri[1];
716 break;
717 case 'xjn':
718 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
719 $num = $hebrew[1];
720 break;
721 case 't':
722 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
723 $num = gmdate( 't', $unix );
724 break;
725 case 'xjt':
726 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
727 $num = $hebrew[3];
728 break;
729 case 'L':
730 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
731 $num = gmdate( 'L', $unix );
732 break;
733 # 'o' is supported since PHP 5.1.0
734 # return literal if not supported
735 # TODO: emulation for pre 5.1.0 versions
736 case 'o':
737 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
738 if ( version_compare(PHP_VERSION, '5.1.0') === 1 )
739 $num = date( 'o', $unix );
740 else
741 $s .= 'o';
742 break;
743 case 'Y':
744 $num = substr( $ts, 0, 4 );
745 break;
746 case 'xiY':
747 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
748 $num = $iranian[0];
749 break;
750 case 'xmY':
751 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
752 $num = $hijri[0];
753 break;
754 case 'xjY':
755 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
756 $num = $hebrew[0];
757 break;
758 case 'xkY':
759 if ( !$thai ) $thai = self::tsToYear( $ts, 'thai' );
760 $num = $thai[0];
761 break;
762 case 'xoY':
763 if ( !$minguo ) $minguo = self::tsToYear( $ts, 'minguo' );
764 $num = $minguo[0];
765 break;
766 case 'xtY':
767 if ( !$tenno ) $tenno = self::tsToYear( $ts, 'tenno' );
768 $num = $tenno[0];
769 break;
770 case 'y':
771 $num = substr( $ts, 2, 2 );
772 break;
773 case 'a':
774 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
775 break;
776 case 'A':
777 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
778 break;
779 case 'g':
780 $h = substr( $ts, 8, 2 );
781 $num = $h % 12 ? $h % 12 : 12;
782 break;
783 case 'G':
784 $num = intval( substr( $ts, 8, 2 ) );
785 break;
786 case 'h':
787 $h = substr( $ts, 8, 2 );
788 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
789 break;
790 case 'H':
791 $num = substr( $ts, 8, 2 );
792 break;
793 case 'i':
794 $num = substr( $ts, 10, 2 );
795 break;
796 case 's':
797 $num = substr( $ts, 12, 2 );
798 break;
799 case 'c':
800 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
801 $s .= gmdate( 'c', $unix );
802 break;
803 case 'r':
804 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
805 $s .= gmdate( 'r', $unix );
806 break;
807 case 'U':
808 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
809 $num = $unix;
810 break;
811 case '\\':
812 # Backslash escaping
813 if ( $p < strlen( $format ) - 1 ) {
814 $s .= $format[++$p];
815 } else {
816 $s .= '\\';
818 break;
819 case '"':
820 # Quoted literal
821 if ( $p < strlen( $format ) - 1 ) {
822 $endQuote = strpos( $format, '"', $p + 1 );
823 if ( $endQuote === false ) {
824 # No terminating quote, assume literal "
825 $s .= '"';
826 } else {
827 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
828 $p = $endQuote;
830 } else {
831 # Quote at end of string, assume literal "
832 $s .= '"';
834 break;
835 default:
836 $s .= $format[$p];
838 if ( $num !== false ) {
839 if ( $rawToggle || $raw ) {
840 $s .= $num;
841 $raw = false;
842 } elseif ( $roman ) {
843 $s .= self::romanNumeral( $num );
844 $roman = false;
845 } elseif( $hebrewNum ) {
846 $s .= self::hebrewNumeral( $num );
847 $hebrewNum = false;
848 } else {
849 $s .= $this->formatNum( $num, true );
851 $num = false;
854 return $s;
857 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
858 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
860 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
861 * Gregorian dates to Iranian dates. Originally written in C, it
862 * is released under the terms of GNU Lesser General Public
863 * License. Conversion to PHP was performed by Niklas Laxström.
865 * Link: http://www.farsiweb.info/jalali/jalali.c
867 private static function tsToIranian( $ts ) {
868 $gy = substr( $ts, 0, 4 ) -1600;
869 $gm = substr( $ts, 4, 2 ) -1;
870 $gd = substr( $ts, 6, 2 ) -1;
872 # Days passed from the beginning (including leap years)
873 $gDayNo = 365*$gy
874 + floor(($gy+3) / 4)
875 - floor(($gy+99) / 100)
876 + floor(($gy+399) / 400);
879 // Add days of the past months of this year
880 for( $i = 0; $i < $gm; $i++ ) {
881 $gDayNo += self::$GREG_DAYS[$i];
884 // Leap years
885 if ( $gm > 1 && (($gy%4===0 && $gy%100!==0 || ($gy%400==0)))) {
886 $gDayNo++;
889 // Days passed in current month
890 $gDayNo += $gd;
892 $jDayNo = $gDayNo - 79;
894 $jNp = floor($jDayNo / 12053);
895 $jDayNo %= 12053;
897 $jy = 979 + 33*$jNp + 4*floor($jDayNo/1461);
898 $jDayNo %= 1461;
900 if ( $jDayNo >= 366 ) {
901 $jy += floor(($jDayNo-1)/365);
902 $jDayNo = floor(($jDayNo-1)%365);
905 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
906 $jDayNo -= self::$IRANIAN_DAYS[$i];
909 $jm= $i+1;
910 $jd= $jDayNo+1;
912 return array($jy, $jm, $jd);
915 * Converting Gregorian dates to Hijri dates.
917 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
919 * @link http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
921 private static function tsToHijri ( $ts ) {
922 $year = substr( $ts, 0, 4 );
923 $month = substr( $ts, 4, 2 );
924 $day = substr( $ts, 6, 2 );
926 $zyr = $year;
927 $zd=$day;
928 $zm=$month;
929 $zy=$zyr;
933 if (($zy>1582)||(($zy==1582)&&($zm>10))||(($zy==1582)&&($zm==10)&&($zd>14)))
937 $zjd=(int)((1461*($zy + 4800 + (int)( ($zm-14) /12) ))/4) + (int)((367*($zm-2-12*((int)(($zm-14)/12))))/12)-(int)((3*(int)(( ($zy+4900+(int)(($zm-14)/12))/100)))/4)+$zd-32075;
939 else
941 $zjd = 367*$zy-(int)((7*($zy+5001+(int)(($zm-9)/7)))/4)+(int)((275*$zm)/9)+$zd+1729777;
944 $zl=$zjd-1948440+10632;
945 $zn=(int)(($zl-1)/10631);
946 $zl=$zl-10631*$zn+354;
947 $zj=((int)((10985-$zl)/5316))*((int)((50*$zl)/17719))+((int)($zl/5670))*((int)((43*$zl)/15238));
948 $zl=$zl-((int)((30-$zj)/15))*((int)((17719*$zj)/50))-((int)($zj/16))*((int)((15238*$zj)/43))+29;
949 $zm=(int)((24*$zl)/709);
950 $zd=$zl-(int)((709*$zm)/24);
951 $zy=30*$zn+$zj-30;
953 return array ($zy, $zm, $zd);
957 * Converting Gregorian dates to Hebrew dates.
959 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
960 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
961 * to translate the relevant functions into PHP and release them under
962 * GNU GPL.
964 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
965 * and Adar II is 14. In a non-leap year, Adar is 6.
967 private static function tsToHebrew( $ts ) {
968 # Parse date
969 $year = substr( $ts, 0, 4 );
970 $month = substr( $ts, 4, 2 );
971 $day = substr( $ts, 6, 2 );
973 # Calculate Hebrew year
974 $hebrewYear = $year + 3760;
976 # Month number when September = 1, August = 12
977 $month += 4;
978 if( $month > 12 ) {
979 # Next year
980 $month -= 12;
981 $year++;
982 $hebrewYear++;
985 # Calculate day of year from 1 September
986 $dayOfYear = $day;
987 for( $i = 1; $i < $month; $i++ ) {
988 if( $i == 6 ) {
989 # February
990 $dayOfYear += 28;
991 # Check if the year is leap
992 if( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
993 $dayOfYear++;
995 } elseif( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
996 $dayOfYear += 30;
997 } else {
998 $dayOfYear += 31;
1002 # Calculate the start of the Hebrew year
1003 $start = self::hebrewYearStart( $hebrewYear );
1005 # Calculate next year's start
1006 if( $dayOfYear <= $start ) {
1007 # Day is before the start of the year - it is the previous year
1008 # Next year's start
1009 $nextStart = $start;
1010 # Previous year
1011 $year--;
1012 $hebrewYear--;
1013 # Add days since previous year's 1 September
1014 $dayOfYear += 365;
1015 if( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1016 # Leap year
1017 $dayOfYear++;
1019 # Start of the new (previous) year
1020 $start = self::hebrewYearStart( $hebrewYear );
1021 } else {
1022 # Next year's start
1023 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1026 # Calculate Hebrew day of year
1027 $hebrewDayOfYear = $dayOfYear - $start;
1029 # Difference between year's days
1030 $diff = $nextStart - $start;
1031 # Add 12 (or 13 for leap years) days to ignore the difference between
1032 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1033 # difference is only about the year type
1034 if( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1035 $diff += 13;
1036 } else {
1037 $diff += 12;
1040 # Check the year pattern, and is leap year
1041 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1042 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1043 # and non-leap years
1044 $yearPattern = $diff % 30;
1045 # Check if leap year
1046 $isLeap = $diff >= 30;
1048 # Calculate day in the month from number of day in the Hebrew year
1049 # Don't check Adar - if the day is not in Adar, we will stop before;
1050 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1051 $hebrewDay = $hebrewDayOfYear;
1052 $hebrewMonth = 1;
1053 $days = 0;
1054 while( $hebrewMonth <= 12 ) {
1055 # Calculate days in this month
1056 if( $isLeap && $hebrewMonth == 6 ) {
1057 # Adar in a leap year
1058 if( $isLeap ) {
1059 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1060 $days = 30;
1061 if( $hebrewDay <= $days ) {
1062 # Day in Adar I
1063 $hebrewMonth = 13;
1064 } else {
1065 # Subtract the days of Adar I
1066 $hebrewDay -= $days;
1067 # Try Adar II
1068 $days = 29;
1069 if( $hebrewDay <= $days ) {
1070 # Day in Adar II
1071 $hebrewMonth = 14;
1075 } elseif( $hebrewMonth == 2 && $yearPattern == 2 ) {
1076 # Cheshvan in a complete year (otherwise as the rule below)
1077 $days = 30;
1078 } elseif( $hebrewMonth == 3 && $yearPattern == 0 ) {
1079 # Kislev in an incomplete year (otherwise as the rule below)
1080 $days = 29;
1081 } else {
1082 # Odd months have 30 days, even have 29
1083 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1085 if( $hebrewDay <= $days ) {
1086 # In the current month
1087 break;
1088 } else {
1089 # Subtract the days of the current month
1090 $hebrewDay -= $days;
1091 # Try in the next month
1092 $hebrewMonth++;
1096 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1100 * This calculates the Hebrew year start, as days since 1 September.
1101 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1102 * Used for Hebrew date.
1104 private static function hebrewYearStart( $year ) {
1105 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1106 $b = intval( ( $year - 1 ) % 4 );
1107 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1108 if( $m < 0 ) {
1109 $m--;
1111 $Mar = intval( $m );
1112 if( $m < 0 ) {
1113 $m++;
1115 $m -= $Mar;
1117 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7);
1118 if( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1119 $Mar++;
1120 } else if( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1121 $Mar += 2;
1122 } else if( $c == 2 || $c == 4 || $c == 6 ) {
1123 $Mar++;
1126 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1127 return $Mar;
1131 * Algorithm to convert Gregorian dates to Thai solar dates,
1132 * Minguo dates or Minguo dates.
1134 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1135 * http://en.wikipedia.org/wiki/Minguo_calendar
1136 * http://en.wikipedia.org/wiki/Japanese_era_name
1138 * @param $ts String: 14-character timestamp, calender name
1139 * @return array converted year, month, day
1141 private static function tsToYear( $ts, $cName ) {
1142 $gy = substr( $ts, 0, 4 );
1143 $gm = substr( $ts, 4, 2 );
1144 $gd = substr( $ts, 6, 2 );
1146 if (!strcmp($cName,'thai')) {
1147 # Thai solar dates
1148 # Add 543 years to the Gregorian calendar
1149 # Months and days are identical
1150 $gy_offset = $gy + 543;
1151 } else if ((!strcmp($cName,'minguo')) || !strcmp($cName,'juche')) {
1152 # Minguo dates
1153 # Deduct 1911 years from the Gregorian calendar
1154 # Months and days are identical
1155 $gy_offset = $gy - 1911;
1156 } else if (!strcmp($cName,'tenno')) {
1157 # Nengō dates up to Meiji period
1158 # Deduct years from the Gregorian calendar
1159 # depending on the nengo periods
1160 # Months and days are identical
1161 if (($gy < 1912) || (($gy == 1912) && ($gm < 7)) || (($gy == 1912) && ($gm == 7) && ($gd < 31))) {
1162 # Meiji period
1163 $gy_gannen = $gy - 1868 + 1;
1164 $gy_offset = $gy_gannen;
1165 if ($gy_gannen == 1)
1166 $gy_offset = '元';
1167 $gy_offset = '明治'.$gy_offset;
1168 } else if ((($gy == 1912) && ($gm == 7) && ($gd == 31)) || (($gy == 1912) && ($gm >= 8)) || (($gy > 1912) && ($gy < 1926)) || (($gy == 1926) && ($gm < 12)) || (($gy == 1926) && ($gm == 12) && ($gd < 26))) {
1169 # Taishō period
1170 $gy_gannen = $gy - 1912 + 1;
1171 $gy_offset = $gy_gannen;
1172 if ($gy_gannen == 1)
1173 $gy_offset = '元';
1174 $gy_offset = '大正'.$gy_offset;
1175 } else if ((($gy == 1926) && ($gm == 12) && ($gd >= 26)) || (($gy > 1926) && ($gy < 1989)) || (($gy == 1989) && ($gm == 1) && ($gd < 8))) {
1176 # Shōwa period
1177 $gy_gannen = $gy - 1926 + 1;
1178 $gy_offset = $gy_gannen;
1179 if ($gy_gannen == 1)
1180 $gy_offset = '元';
1181 $gy_offset = '昭和'.$gy_offset;
1182 } else {
1183 # Heisei period
1184 $gy_gannen = $gy - 1989 + 1;
1185 $gy_offset = $gy_gannen;
1186 if ($gy_gannen == 1)
1187 $gy_offset = '元';
1188 $gy_offset = '平成'.$gy_offset;
1190 } else {
1191 $gy_offset = $gy;
1194 return array( $gy_offset, $gm, $gd );
1198 * Roman number formatting up to 3000
1200 static function romanNumeral( $num ) {
1201 static $table = array(
1202 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1203 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1204 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1205 array( '', 'M', 'MM', 'MMM' )
1208 $num = intval( $num );
1209 if ( $num > 3000 || $num <= 0 ) {
1210 return $num;
1213 $s = '';
1214 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1215 if ( $num >= $pow10 ) {
1216 $s .= $table[$i][floor($num / $pow10)];
1218 $num = $num % $pow10;
1220 return $s;
1224 * Hebrew Gematria number formatting up to 9999
1226 static function hebrewNumeral( $num ) {
1227 static $table = array(
1228 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1229 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1230 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1231 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1234 $num = intval( $num );
1235 if ( $num > 9999 || $num <= 0 ) {
1236 return $num;
1239 $s = '';
1240 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1241 if ( $num >= $pow10 ) {
1242 if ( $num == 15 || $num == 16 ) {
1243 $s .= $table[0][9] . $table[0][$num - 9];
1244 $num = 0;
1245 } else {
1246 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1247 if( $pow10 == 1000 ) {
1248 $s .= "'";
1252 $num = $num % $pow10;
1254 if( strlen( $s ) == 2 ) {
1255 $str = $s . "'";
1256 } else {
1257 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1258 $str .= substr( $s, strlen( $s ) - 2, 2 );
1260 $start = substr( $str, 0, strlen( $str ) - 2 );
1261 $end = substr( $str, strlen( $str ) - 2 );
1262 switch( $end ) {
1263 case 'כ':
1264 $str = $start . 'ך';
1265 break;
1266 case 'מ':
1267 $str = $start . 'ם';
1268 break;
1269 case 'נ':
1270 $str = $start . 'ן';
1271 break;
1272 case 'פ':
1273 $str = $start . 'ף';
1274 break;
1275 case 'צ':
1276 $str = $start . 'ץ';
1277 break;
1279 return $str;
1283 * This is meant to be used by time(), date(), and timeanddate() to get
1284 * the date preference they're supposed to use, it should be used in
1285 * all children.
1287 *<code>
1288 * function timeanddate([...], $format = true) {
1289 * $datePreference = $this->dateFormat($format);
1290 * [...]
1292 *</code>
1294 * @param $usePrefs Mixed: if true, the user's preference is used
1295 * if false, the site/language default is used
1296 * if int/string, assumed to be a format.
1297 * @return string
1299 function dateFormat( $usePrefs = true ) {
1300 global $wgUser;
1302 if( is_bool( $usePrefs ) ) {
1303 if( $usePrefs ) {
1304 $datePreference = $wgUser->getDatePreference();
1305 } else {
1306 $options = User::getDefaultOptions();
1307 $datePreference = (string)$options['date'];
1309 } else {
1310 $datePreference = (string)$usePrefs;
1313 // return int
1314 if( $datePreference == '' ) {
1315 return 'default';
1318 return $datePreference;
1322 * @param $ts Mixed: the time format which needs to be turned into a
1323 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1324 * @param $adj Bool: whether to adjust the time output according to the
1325 * user configured offset ($timecorrection)
1326 * @param $format Mixed: true to use user's date format preference
1327 * @param $timecorrection String: the time offset as returned by
1328 * validateTimeZone() in Special:Preferences
1329 * @return string
1331 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1332 $this->load();
1333 if ( $adj ) {
1334 $ts = $this->userAdjust( $ts, $timecorrection );
1337 $pref = $this->dateFormat( $format );
1338 if( $pref == 'default' || !isset( $this->dateFormats["$pref date"] ) ) {
1339 $pref = $this->defaultDateFormat;
1341 return $this->sprintfDate( $this->dateFormats["$pref date"], $ts );
1345 * @param $ts Mixed: the time format which needs to be turned into a
1346 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1347 * @param $adj Bool: whether to adjust the time output according to the
1348 * user configured offset ($timecorrection)
1349 * @param $format Mixed: true to use user's date format preference
1350 * @param $timecorrection String: the time offset as returned by
1351 * validateTimeZone() in Special:Preferences
1352 * @return string
1354 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1355 $this->load();
1356 if ( $adj ) {
1357 $ts = $this->userAdjust( $ts, $timecorrection );
1360 $pref = $this->dateFormat( $format );
1361 if( $pref == 'default' || !isset( $this->dateFormats["$pref time"] ) ) {
1362 $pref = $this->defaultDateFormat;
1364 return $this->sprintfDate( $this->dateFormats["$pref time"], $ts );
1368 * @param $ts Mixed: the time format which needs to be turned into a
1369 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1370 * @param $adj Bool: whether to adjust the time output according to the
1371 * user configured offset ($timecorrection)
1372 * @param $format Mixed: what format to return, if it's false output the
1373 * default one (default true)
1374 * @param $timecorrection String: the time offset as returned by
1375 * validateTimeZone() in Special:Preferences
1376 * @return string
1378 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false) {
1379 $this->load();
1381 $ts = wfTimestamp( TS_MW, $ts );
1383 if ( $adj ) {
1384 $ts = $this->userAdjust( $ts, $timecorrection );
1387 $pref = $this->dateFormat( $format );
1388 if( $pref == 'default' || !isset( $this->dateFormats["$pref both"] ) ) {
1389 $pref = $this->defaultDateFormat;
1392 return $this->sprintfDate( $this->dateFormats["$pref both"], $ts );
1395 function getMessage( $key ) {
1396 $this->load();
1397 return isset( $this->messages[$key] ) ? $this->messages[$key] : null;
1400 function getAllMessages() {
1401 $this->load();
1402 return $this->messages;
1405 function iconv( $in, $out, $string ) {
1406 # For most languages, this is a wrapper for iconv
1407 return iconv( $in, $out . '//IGNORE', $string );
1410 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
1411 function ucwordbreaksCallbackAscii($matches){
1412 return $this->ucfirst($matches[1]);
1415 function ucwordbreaksCallbackMB($matches){
1416 return mb_strtoupper($matches[0]);
1419 function ucCallback($matches){
1420 list( $wikiUpperChars ) = self::getCaseMaps();
1421 return strtr( $matches[1], $wikiUpperChars );
1424 function lcCallback($matches){
1425 list( , $wikiLowerChars ) = self::getCaseMaps();
1426 return strtr( $matches[1], $wikiLowerChars );
1429 function ucwordsCallbackMB($matches){
1430 return mb_strtoupper($matches[0]);
1433 function ucwordsCallbackWiki($matches){
1434 list( $wikiUpperChars ) = self::getCaseMaps();
1435 return strtr( $matches[0], $wikiUpperChars );
1438 function ucfirst( $str ) {
1439 if ( empty($str) ) return $str;
1440 if ( ord($str[0]) < 128 ) return ucfirst($str);
1441 else return self::uc($str,true); // fall back to more complex logic in case of multibyte strings
1444 function uc( $str, $first = false ) {
1445 if ( function_exists( 'mb_strtoupper' ) ) {
1446 if ( $first ) {
1447 if ( self::isMultibyte( $str ) ) {
1448 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1449 } else {
1450 return ucfirst( $str );
1452 } else {
1453 return self::isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
1455 } else {
1456 if ( self::isMultibyte( $str ) ) {
1457 list( $wikiUpperChars ) = $this->getCaseMaps();
1458 $x = $first ? '^' : '';
1459 return preg_replace_callback(
1460 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1461 array($this,"ucCallback"),
1462 $str
1464 } else {
1465 return $first ? ucfirst( $str ) : strtoupper( $str );
1470 function lcfirst( $str ) {
1471 if ( empty($str) ) return $str;
1472 if ( is_string( $str ) && ord($str[0]) < 128 ) {
1473 // editing string in place = cool
1474 $str[0]=strtolower($str[0]);
1475 return $str;
1477 else return self::lc( $str, true );
1480 function lc( $str, $first = false ) {
1481 if ( function_exists( 'mb_strtolower' ) )
1482 if ( $first )
1483 if ( self::isMultibyte( $str ) )
1484 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1485 else
1486 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
1487 else
1488 return self::isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
1489 else
1490 if ( self::isMultibyte( $str ) ) {
1491 list( , $wikiLowerChars ) = self::getCaseMaps();
1492 $x = $first ? '^' : '';
1493 return preg_replace_callback(
1494 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1495 array($this,"lcCallback"),
1496 $str
1498 } else
1499 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
1502 function isMultibyte( $str ) {
1503 return (bool)preg_match( '/[\x80-\xff]/', $str );
1506 function ucwords($str) {
1507 if ( self::isMultibyte( $str ) ) {
1508 $str = self::lc($str);
1510 // regexp to find first letter in each word (i.e. after each space)
1511 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
1513 // function to use to capitalize a single char
1514 if ( function_exists( 'mb_strtoupper' ) )
1515 return preg_replace_callback(
1516 $replaceRegexp,
1517 array($this,"ucwordsCallbackMB"),
1518 $str
1520 else
1521 return preg_replace_callback(
1522 $replaceRegexp,
1523 array($this,"ucwordsCallbackWiki"),
1524 $str
1527 else
1528 return ucwords( strtolower( $str ) );
1531 # capitalize words at word breaks
1532 function ucwordbreaks($str){
1533 if (self::isMultibyte( $str ) ) {
1534 $str = self::lc($str);
1536 // since \b doesn't work for UTF-8, we explicitely define word break chars
1537 $breaks= "[ \-\(\)\}\{\.,\?!]";
1539 // find first letter after word break
1540 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
1542 if ( function_exists( 'mb_strtoupper' ) )
1543 return preg_replace_callback(
1544 $replaceRegexp,
1545 array($this,"ucwordbreaksCallbackMB"),
1546 $str
1548 else
1549 return preg_replace_callback(
1550 $replaceRegexp,
1551 array($this,"ucwordsCallbackWiki"),
1552 $str
1555 else
1556 return preg_replace_callback(
1557 '/\b([\w\x80-\xff]+)\b/',
1558 array($this,"ucwordbreaksCallbackAscii"),
1559 $str );
1563 * Return a case-folded representation of $s
1565 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
1566 * and $s2 are the same except for the case of their characters. It is not
1567 * necessary for the value returned to make sense when displayed.
1569 * Do *not* perform any other normalisation in this function. If a caller
1570 * uses this function when it should be using a more general normalisation
1571 * function, then fix the caller.
1573 function caseFold( $s ) {
1574 return $this->uc( $s );
1577 function checkTitleEncoding( $s ) {
1578 if( is_array( $s ) ) {
1579 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
1581 # Check for non-UTF-8 URLs
1582 $ishigh = preg_match( '/[\x80-\xff]/', $s);
1583 if(!$ishigh) return $s;
1585 $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1586 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
1587 if( $isutf8 ) return $s;
1589 return $this->iconv( $this->fallback8bitEncoding(), "utf-8", $s );
1592 function fallback8bitEncoding() {
1593 $this->load();
1594 return $this->fallback8bitEncoding;
1598 * Some languages have special punctuation to strip out
1599 * or characters which need to be converted for MySQL's
1600 * indexing to grok it correctly. Make such changes here.
1602 * @param $string String
1603 * @return String
1605 function stripForSearch( $string ) {
1606 global $wgDBtype;
1607 if ( $wgDBtype != 'mysql' ) {
1608 return $string;
1612 wfProfileIn( __METHOD__ );
1614 // MySQL fulltext index doesn't grok utf-8, so we
1615 // need to fold cases and convert to hex
1616 $out = preg_replace_callback(
1617 "/([\\xc0-\\xff][\\x80-\\xbf]*)/",
1618 array( $this, 'stripForSearchCallback' ),
1619 $this->lc( $string ) );
1621 // And to add insult to injury, the default indexing
1622 // ignores short words... Pad them so we can pass them
1623 // through without reconfiguring the server...
1624 $minLength = $this->minSearchLength();
1625 if( $minLength > 1 ) {
1626 $n = $minLength-1;
1627 $out = preg_replace(
1628 "/\b(\w{1,$n})\b/",
1629 "$1u800",
1630 $out );
1633 // Periods within things like hostnames and IP addresses
1634 // are also important -- we want a search for "example.com"
1635 // or "192.168.1.1" to work sanely.
1637 // MySQL's search seems to ignore them, so you'd match on
1638 // "example.wikipedia.com" and "192.168.83.1" as well.
1639 $out = preg_replace(
1640 "/(\w)\.(\w|\*)/u",
1641 "$1u82e$2",
1642 $out );
1644 wfProfileOut( __METHOD__ );
1645 return $out;
1649 * Armor a case-folded UTF-8 string to get through MySQL's
1650 * fulltext search without being mucked up by funny charset
1651 * settings or anything else of the sort.
1653 protected function stripForSearchCallback( $matches ) {
1654 return 'u8' . bin2hex( $matches[1] );
1658 * Check MySQL server's ft_min_word_len setting so we know
1659 * if we need to pad short words...
1661 protected function minSearchLength() {
1662 if( !isset( $this->minSearchLength ) ) {
1663 $sql = "show global variables like 'ft\\_min\\_word\\_len'";
1664 $dbr = wfGetDB( DB_SLAVE );
1665 $result = $dbr->query( $sql );
1666 $row = $result->fetchObject();
1667 $result->free();
1669 if( $row && $row->Variable_name == 'ft_min_word_len' ) {
1670 $this->minSearchLength = intval( $row->Value );
1671 } else {
1672 $this->minSearchLength = 0;
1675 return $this->minSearchLength;
1678 function convertForSearchResult( $termsArray ) {
1679 # some languages, e.g. Chinese, need to do a conversion
1680 # in order for search results to be displayed correctly
1681 return $termsArray;
1685 * Get the first character of a string.
1687 * @param $s string
1688 * @return string
1690 function firstChar( $s ) {
1691 $matches = array();
1692 preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1693 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/', $s, $matches);
1695 if ( isset( $matches[1] ) ) {
1696 if ( strlen( $matches[1] ) != 3 ) {
1697 return $matches[1];
1700 // Break down Hangul syllables to grab the first jamo
1701 $code = utf8ToCodepoint( $matches[1] );
1702 if ( $code < 0xac00 || 0xd7a4 <= $code) {
1703 return $matches[1];
1704 } elseif ( $code < 0xb098 ) {
1705 return "\xe3\x84\xb1";
1706 } elseif ( $code < 0xb2e4 ) {
1707 return "\xe3\x84\xb4";
1708 } elseif ( $code < 0xb77c ) {
1709 return "\xe3\x84\xb7";
1710 } elseif ( $code < 0xb9c8 ) {
1711 return "\xe3\x84\xb9";
1712 } elseif ( $code < 0xbc14 ) {
1713 return "\xe3\x85\x81";
1714 } elseif ( $code < 0xc0ac ) {
1715 return "\xe3\x85\x82";
1716 } elseif ( $code < 0xc544 ) {
1717 return "\xe3\x85\x85";
1718 } elseif ( $code < 0xc790 ) {
1719 return "\xe3\x85\x87";
1720 } elseif ( $code < 0xcc28 ) {
1721 return "\xe3\x85\x88";
1722 } elseif ( $code < 0xce74 ) {
1723 return "\xe3\x85\x8a";
1724 } elseif ( $code < 0xd0c0 ) {
1725 return "\xe3\x85\x8b";
1726 } elseif ( $code < 0xd30c ) {
1727 return "\xe3\x85\x8c";
1728 } elseif ( $code < 0xd558 ) {
1729 return "\xe3\x85\x8d";
1730 } else {
1731 return "\xe3\x85\x8e";
1733 } else {
1734 return "";
1738 function initEncoding() {
1739 # Some languages may have an alternate char encoding option
1740 # (Esperanto X-coding, Japanese furigana conversion, etc)
1741 # If this language is used as the primary content language,
1742 # an override to the defaults can be set here on startup.
1745 function recodeForEdit( $s ) {
1746 # For some languages we'll want to explicitly specify
1747 # which characters make it into the edit box raw
1748 # or are converted in some way or another.
1749 # Note that if wgOutputEncoding is different from
1750 # wgInputEncoding, this text will be further converted
1751 # to wgOutputEncoding.
1752 global $wgEditEncoding;
1753 if( $wgEditEncoding == '' or
1754 $wgEditEncoding == 'UTF-8' ) {
1755 return $s;
1756 } else {
1757 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
1761 function recodeInput( $s ) {
1762 # Take the previous into account.
1763 global $wgEditEncoding;
1764 if($wgEditEncoding != "") {
1765 $enc = $wgEditEncoding;
1766 } else {
1767 $enc = 'UTF-8';
1769 if( $enc == 'UTF-8' ) {
1770 return $s;
1771 } else {
1772 return $this->iconv( $enc, 'UTF-8', $s );
1777 * For right-to-left language support
1779 * @return bool
1781 function isRTL() {
1782 $this->load();
1783 return $this->rtl;
1787 * A hidden direction mark (LRM or RLM), depending on the language direction
1789 * @return string
1791 function getDirMark() {
1792 return $this->isRTL() ? "\xE2\x80\x8F" : "\xE2\x80\x8E";
1795 function capitalizeAllNouns() {
1796 $this->load();
1797 return $this->capitalizeAllNouns;
1801 * An arrow, depending on the language direction
1803 * @return string
1805 function getArrow() {
1806 return $this->isRTL() ? '←' : '→';
1810 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
1812 * @return bool
1814 function linkPrefixExtension() {
1815 $this->load();
1816 return $this->linkPrefixExtension;
1819 function &getMagicWords() {
1820 $this->load();
1821 return $this->magicWords;
1824 # Fill a MagicWord object with data from here
1825 function getMagic( &$mw ) {
1826 if ( !$this->mMagicHookDone ) {
1827 $this->mMagicHookDone = true;
1828 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
1830 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
1831 $rawEntry = $this->mMagicExtensions[$mw->mId];
1832 } else {
1833 $magicWords =& $this->getMagicWords();
1834 if ( isset( $magicWords[$mw->mId] ) ) {
1835 $rawEntry = $magicWords[$mw->mId];
1836 } else {
1837 # Fall back to English if local list is incomplete
1838 $magicWords =& Language::getMagicWords();
1839 if ( !isset($magicWords[$mw->mId]) ) {
1840 throw new MWException("Magic word '{$mw->mId}' not found" );
1842 $rawEntry = $magicWords[$mw->mId];
1846 if( !is_array( $rawEntry ) ) {
1847 error_log( "\"$rawEntry\" is not a valid magic thingie for \"$mw->mId\"" );
1848 } else {
1849 $mw->mCaseSensitive = $rawEntry[0];
1850 $mw->mSynonyms = array_slice( $rawEntry, 1 );
1855 * Add magic words to the extension array
1857 function addMagicWordsByLang( $newWords ) {
1858 $code = $this->getCode();
1859 $fallbackChain = array();
1860 while ( $code && !in_array( $code, $fallbackChain ) ) {
1861 $fallbackChain[] = $code;
1862 $code = self::getFallbackFor( $code );
1864 if ( !in_array( 'en', $fallbackChain ) ) {
1865 $fallbackChain[] = 'en';
1867 $fallbackChain = array_reverse( $fallbackChain );
1868 foreach ( $fallbackChain as $code ) {
1869 if ( isset( $newWords[$code] ) ) {
1870 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
1876 * Get special page names, as an associative array
1877 * case folded alias => real name
1879 function getSpecialPageAliases() {
1880 $this->load();
1882 // Cache aliases because it may be slow to load them
1883 if ( !isset( $this->mExtendedSpecialPageAliases ) ) {
1885 // Initialise array
1886 $this->mExtendedSpecialPageAliases = $this->specialPageAliases;
1888 global $wgExtensionAliasesFiles;
1889 foreach ( $wgExtensionAliasesFiles as $file ) {
1891 // Fail fast
1892 if ( !file_exists($file) )
1893 throw new MWException( "Aliases file does not exist: $file" );
1895 $aliases = array();
1896 require($file);
1898 // Check the availability of aliases
1899 if ( !isset($aliases['en']) )
1900 throw new MWException( "Malformed aliases file: $file" );
1902 // Merge all aliases in fallback chain
1903 $code = $this->getCode();
1904 do {
1905 if ( !isset($aliases[$code]) ) continue;
1907 $aliases[$code] = $this->fixSpecialPageAliases( $aliases[$code] );
1908 /* Merge the aliases, THIS will break if there is special page name
1909 * which looks like a numerical key, thanks to PHP...
1910 * See the array_merge_recursive manual entry */
1911 $this->mExtendedSpecialPageAliases = array_merge_recursive(
1912 $this->mExtendedSpecialPageAliases, $aliases[$code] );
1914 } while ( $code = self::getFallbackFor( $code ) );
1917 wfRunHooks( 'LanguageGetSpecialPageAliases',
1918 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
1921 return $this->mExtendedSpecialPageAliases;
1925 * Function to fix special page aliases. Will convert the first letter to
1926 * upper case and spaces to underscores. Can be given a full aliases array,
1927 * in which case it will recursively fix all aliases.
1929 public function fixSpecialPageAliases( $mixed ) {
1930 // Work recursively until in string level
1931 if ( is_array($mixed) ) {
1932 $callback = array( $this, 'fixSpecialPageAliases' );
1933 return array_map( $callback, $mixed );
1935 return str_replace( ' ', '_', $this->ucfirst( $mixed ) );
1939 * Italic is unsuitable for some languages
1941 * @param $text String: the text to be emphasized.
1942 * @return string
1944 function emphasize( $text ) {
1945 return "<em>$text</em>";
1949 * Normally we output all numbers in plain en_US style, that is
1950 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
1951 * point twohundredthirtyfive. However this is not sutable for all
1952 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
1953 * Icelandic just want to use commas instead of dots, and dots instead
1954 * of commas like "293.291,235".
1956 * An example of this function being called:
1957 * <code>
1958 * wfMsg( 'message', $wgLang->formatNum( $num ) )
1959 * </code>
1961 * See LanguageGu.php for the Gujarati implementation and
1962 * $separatorTransformTable on MessageIs.php for
1963 * the , => . and . => , implementation.
1965 * @todo check if it's viable to use localeconv() for the decimal
1966 * separator thing.
1967 * @param $number Mixed: the string to be formatted, should be an integer
1968 * or a floating point number.
1969 * @param $nocommafy Bool: set to true for special numbers like dates
1970 * @return string
1972 function formatNum( $number, $nocommafy = false ) {
1973 global $wgTranslateNumerals;
1974 if (!$nocommafy) {
1975 $number = $this->commafy($number);
1976 $s = $this->separatorTransformTable();
1977 if ($s) { $number = strtr($number, $s); }
1980 if ($wgTranslateNumerals) {
1981 $s = $this->digitTransformTable();
1982 if ($s) { $number = strtr($number, $s); }
1985 return $number;
1988 function parseFormattedNumber( $number ) {
1989 $s = $this->digitTransformTable();
1990 if ($s) { $number = strtr($number, array_flip($s)); }
1992 $s = $this->separatorTransformTable();
1993 if ($s) { $number = strtr($number, array_flip($s)); }
1995 $number = strtr( $number, array (',' => '') );
1996 return $number;
2000 * Adds commas to a given number
2002 * @param $_ mixed
2003 * @return string
2005 function commafy($_) {
2006 return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_)));
2009 function digitTransformTable() {
2010 $this->load();
2011 return $this->digitTransformTable;
2014 function separatorTransformTable() {
2015 $this->load();
2016 return $this->separatorTransformTable;
2021 * Take a list of strings and build a locale-friendly comma-separated
2022 * list, using the local comma-separator message.
2023 * The last two strings are chained with an "and".
2025 * @param $l Array
2026 * @return string
2028 function listToText( $l ) {
2029 $s = '';
2030 $m = count( $l ) - 1;
2031 if( $m == 1 ) {
2032 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
2034 else {
2035 for ( $i = $m; $i >= 0; $i-- ) {
2036 if ( $i == $m ) {
2037 $s = $l[$i];
2038 } else if( $i == $m - 1 ) {
2039 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
2040 } else {
2041 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
2044 return $s;
2049 * Take a list of strings and build a locale-friendly comma-separated
2050 * list, using the local comma-separator message.
2051 * @param $list array of strings to put in a comma list
2052 * @return string
2054 function commaList( $list ) {
2055 return implode(
2056 $list,
2057 wfMsgExt( 'comma-separator', array( 'parsemag', 'escapenoentities', 'language' => $this ) ) );
2061 * Take a list of strings and build a locale-friendly semicolon-separated
2062 * list, using the local semicolon-separator message.
2063 * @param $list array of strings to put in a semicolon list
2064 * @return string
2066 function semicolonList( $list ) {
2067 return implode(
2068 $list,
2069 wfMsgExt( 'semicolon-separator', array( 'parsemag', 'escapenoentities', 'language' => $this ) ) );
2073 * Same as commaList, but separate it with the pipe instead.
2074 * @param $list array of strings to put in a pipe list
2075 * @return string
2077 function pipeList( $list ) {
2078 return implode(
2079 $list,
2080 wfMsgExt( 'pipe-separator', array( 'escapenoentities', 'language' => $this ) ) );
2084 * Truncate a string to a specified length in bytes, appending an optional
2085 * string (e.g. for ellipses)
2087 * The database offers limited byte lengths for some columns in the database;
2088 * multi-byte character sets mean we need to ensure that only whole characters
2089 * are included, otherwise broken characters can be passed to the user
2091 * If $length is negative, the string will be truncated from the beginning
2093 * @param $string String to truncate
2094 * @param $length Int: maximum length (excluding ellipses)
2095 * @param $ellipsis String to append to the truncated text
2096 * @return string
2098 function truncate( $string, $length, $ellipsis = '...' ) {
2099 # Use the localized ellipsis character
2100 if( $ellipsis == '...' ) {
2101 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
2104 if( $length == 0 ) {
2105 return $ellipsis;
2107 if ( strlen( $string ) <= abs( $length ) ) {
2108 return $string;
2110 if( $length > 0 ) {
2111 $string = substr( $string, 0, $length );
2112 $char = ord( $string[strlen( $string ) - 1] );
2113 $m = array();
2114 if ($char >= 0xc0) {
2115 # We got the first byte only of a multibyte char; remove it.
2116 $string = substr( $string, 0, -1 );
2117 } elseif( $char >= 0x80 &&
2118 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
2119 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) ) {
2120 # We chopped in the middle of a character; remove it
2121 $string = $m[1];
2123 return $string . $ellipsis;
2124 } else {
2125 $string = substr( $string, $length );
2126 $char = ord( $string[0] );
2127 if( $char >= 0x80 && $char < 0xc0 ) {
2128 # We chopped in the middle of a character; remove the whole thing
2129 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
2131 return $ellipsis . $string;
2136 * Grammatical transformations, needed for inflected languages
2137 * Invoked by putting {{grammar:case|word}} in a message
2139 * @param $word string
2140 * @param $case string
2141 * @return string
2143 function convertGrammar( $word, $case ) {
2144 global $wgGrammarForms;
2145 if ( isset($wgGrammarForms[$this->getCode()][$case][$word]) ) {
2146 return $wgGrammarForms[$this->getCode()][$case][$word];
2148 return $word;
2152 * Provides an alternative text depending on specified gender.
2153 * Usage {{gender:username|masculine|feminine|neutral}}.
2154 * username is optional, in which case the gender of current user is used,
2155 * but only in (some) interface messages; otherwise default gender is used.
2156 * If second or third parameter are not specified, masculine is used.
2157 * These details may be overriden per language.
2159 function gender( $gender, $forms ) {
2160 if ( !count($forms) ) { return ''; }
2161 $forms = $this->preConvertPlural( $forms, 2 );
2162 if ( $gender === 'male' ) return $forms[0];
2163 if ( $gender === 'female' ) return $forms[1];
2164 return isset($forms[2]) ? $forms[2] : $forms[0];
2168 * Plural form transformations, needed for some languages.
2169 * For example, there are 3 form of plural in Russian and Polish,
2170 * depending on "count mod 10". See [[w:Plural]]
2171 * For English it is pretty simple.
2173 * Invoked by putting {{plural:count|wordform1|wordform2}}
2174 * or {{plural:count|wordform1|wordform2|wordform3}}
2176 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
2178 * @param $count Integer: non-localized number
2179 * @param $forms Array: different plural forms
2180 * @return string Correct form of plural for $count in this language
2182 function convertPlural( $count, $forms ) {
2183 if ( !count($forms) ) { return ''; }
2184 $forms = $this->preConvertPlural( $forms, 2 );
2186 return ( $count == 1 ) ? $forms[0] : $forms[1];
2190 * Checks that convertPlural was given an array and pads it to requested
2191 * amound of forms by copying the last one.
2193 * @param $count Integer: How many forms should there be at least
2194 * @param $forms Array of forms given to convertPlural
2195 * @return array Padded array of forms or an exception if not an array
2197 protected function preConvertPlural( /* Array */ $forms, $count ) {
2198 while ( count($forms) < $count ) {
2199 $forms[] = $forms[count($forms)-1];
2201 return $forms;
2205 * For translaing of expiry times
2206 * @param $str String: the validated block time in English
2207 * @return Somehow translated block time
2208 * @see LanguageFi.php for example implementation
2210 function translateBlockExpiry( $str ) {
2212 $scBlockExpiryOptions = $this->getMessageFromDB( 'ipboptions' );
2214 if ( $scBlockExpiryOptions == '-') {
2215 return $str;
2218 foreach (explode(',', $scBlockExpiryOptions) as $option) {
2219 if ( strpos($option, ":") === false )
2220 continue;
2221 list($show, $value) = explode(":", $option);
2222 if ( strcmp ( $str, $value) == 0 ) {
2223 return htmlspecialchars( trim( $show ) );
2227 return $str;
2231 * languages like Chinese need to be segmented in order for the diff
2232 * to be of any use
2234 * @param $text String
2235 * @return String
2237 function segmentForDiff( $text ) {
2238 return $text;
2242 * and unsegment to show the result
2244 * @param $text String
2245 * @return String
2247 function unsegmentForDiff( $text ) {
2248 return $text;
2251 # convert text to all supported variants
2252 function autoConvertToAllVariants($text) {
2253 return $this->mConverter->autoConvertToAllVariants($text);
2256 # convert text to different variants of a language.
2257 function convert( $text, $isTitle = false) {
2258 return $this->mConverter->convert($text, $isTitle);
2261 # Convert text from within Parser
2262 function parserConvert( $text, &$parser ) {
2263 return $this->mConverter->parserConvert( $text, $parser );
2266 # Check if this is a language with variants
2267 function hasVariants(){
2268 return sizeof($this->getVariants())>1;
2271 # Put custom tags (e.g. -{ }-) around math to prevent conversion
2272 function armourMath($text){
2273 return $this->mConverter->armourMath($text);
2278 * Perform output conversion on a string, and encode for safe HTML output.
2279 * @param $text String
2280 * @param $isTitle Bool -- wtf?
2281 * @return string
2282 * @todo this should get integrated somewhere sane
2284 function convertHtml( $text, $isTitle = false ) {
2285 return htmlspecialchars( $this->convert( $text, $isTitle ) );
2288 function convertCategoryKey( $key ) {
2289 return $this->mConverter->convertCategoryKey( $key );
2293 * get the list of variants supported by this langauge
2294 * see sample implementation in LanguageZh.php
2296 * @return array an array of language codes
2298 function getVariants() {
2299 return $this->mConverter->getVariants();
2303 function getPreferredVariant( $fromUser = true ) {
2304 return $this->mConverter->getPreferredVariant( $fromUser );
2308 * if a language supports multiple variants, it is
2309 * possible that non-existing link in one variant
2310 * actually exists in another variant. this function
2311 * tries to find it. See e.g. LanguageZh.php
2313 * @param $link String: the name of the link
2314 * @param $nt Mixed: the title object of the link
2315 * @param boolean $ignoreOtherCond: to disable other conditions when
2316 * we need to transclude a template or update a category's link
2317 * @return null the input parameters may be modified upon return
2319 function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
2320 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
2324 * If a language supports multiple variants, converts text
2325 * into an array of all possible variants of the text:
2326 * 'variant' => text in that variant
2328 function convertLinkToAllVariants($text){
2329 return $this->mConverter->convertLinkToAllVariants($text);
2334 * returns language specific options used by User::getPageRenderHash()
2335 * for example, the preferred language variant
2337 * @return string
2339 function getExtraHashOptions() {
2340 return $this->mConverter->getExtraHashOptions();
2344 * for languages that support multiple variants, the title of an
2345 * article may be displayed differently in different variants. this
2346 * function returns the apporiate title defined in the body of the article.
2348 * @return string
2350 function getParsedTitle() {
2351 return $this->mConverter->getParsedTitle();
2355 * Enclose a string with the "no conversion" tag. This is used by
2356 * various functions in the Parser
2358 * @param $text String: text to be tagged for no conversion
2359 * @param $noParse
2360 * @return string the tagged text
2362 function markNoConversion( $text, $noParse=false ) {
2363 return $this->mConverter->markNoConversion( $text, $noParse );
2367 * A regular expression to match legal word-trailing characters
2368 * which should be merged onto a link of the form [[foo]]bar.
2370 * @return string
2372 function linkTrail() {
2373 $this->load();
2374 return $this->linkTrail;
2377 function getLangObj() {
2378 return $this;
2382 * Get the RFC 3066 code for this language object
2384 function getCode() {
2385 return $this->mCode;
2388 function setCode( $code ) {
2389 $this->mCode = $code;
2392 static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
2393 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
2396 static function getMessagesFileName( $code ) {
2397 global $IP;
2398 return self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
2401 static function getClassFileName( $code ) {
2402 global $IP;
2403 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
2406 static function getLocalisationArray( $code, $disableCache = false ) {
2407 self::loadLocalisation( $code, $disableCache );
2408 return self::$mLocalisationCache[$code];
2412 * Load localisation data for a given code into the static cache
2414 * @return array Dependencies, map of filenames to mtimes
2416 static function loadLocalisation( $code, $disableCache = false ) {
2417 static $recursionGuard = array();
2418 global $wgMemc, $wgEnableSerializedMessages, $wgCheckSerialized;
2420 if ( !$code ) {
2421 throw new MWException( "Invalid language code requested" );
2424 if ( !$disableCache ) {
2425 # Try the per-process cache
2426 if ( isset( self::$mLocalisationCache[$code] ) ) {
2427 return self::$mLocalisationCache[$code]['deps'];
2430 wfProfileIn( __METHOD__ );
2432 # Try the serialized directory
2433 if( $wgEnableSerializedMessages ) {
2434 $cache = wfGetPrecompiledData( self::getFileName( "Messages", $code, '.ser' ) );
2435 if ( $cache ) {
2436 if ( $wgCheckSerialized && self::isLocalisationOutOfDate( $cache ) ) {
2437 $cache = false;
2438 wfDebug( "Language::loadLocalisation(): precompiled data file for $code is out of date\n" );
2439 } else {
2440 self::$mLocalisationCache[$code] = $cache;
2441 wfDebug( "Language::loadLocalisation(): got localisation for $code from precompiled data file\n" );
2442 wfProfileOut( __METHOD__ );
2443 return self::$mLocalisationCache[$code]['deps'];
2446 } else {
2447 $cache = false;
2450 # Try the global cache
2451 $memcKey = wfMemcKey('localisation', $code );
2452 $fbMemcKey = wfMemcKey('fallback', $cache['fallback'] );
2453 $cache = $wgMemc->get( $memcKey );
2454 if ( $cache ) {
2455 if ( self::isLocalisationOutOfDate( $cache ) ) {
2456 $wgMemc->delete( $memcKey );
2457 $wgMemc->delete( $fbMemcKey );
2458 $cache = false;
2459 wfDebug( "Language::loadLocalisation(): localisation cache for $code had expired\n" );
2460 } else {
2461 self::$mLocalisationCache[$code] = $cache;
2462 wfDebug( "Language::loadLocalisation(): got localisation for $code from cache\n" );
2463 wfProfileOut( __METHOD__ );
2464 return $cache['deps'];
2467 } else {
2468 wfProfileIn( __METHOD__ );
2471 # Default fallback, may be overridden when the messages file is included
2472 if ( $code != 'en' ) {
2473 $fallback = 'en';
2474 } else {
2475 $fallback = false;
2478 # Load the primary localisation from the source file
2479 $filename = self::getMessagesFileName( $code );
2480 if ( !file_exists( $filename ) ) {
2481 wfDebug( "Language::loadLocalisation(): no localisation file for $code, using implicit fallback to en\n" );
2482 $cache = compact( self::$mLocalisationKeys ); // Set correct fallback
2483 $deps = array();
2484 } else {
2485 $deps = array( $filename => filemtime( $filename ) );
2486 require( $filename );
2487 $cache = compact( self::$mLocalisationKeys );
2488 wfDebug( "Language::loadLocalisation(): got localisation for $code from source\n" );
2491 # Load magic word source file
2492 global $IP;
2493 $filename = "$IP/includes/MagicWord.php";
2494 $newDeps = array( $filename => filemtime( $filename ) );
2495 $deps = array_merge( $deps, $newDeps );
2497 if ( !empty( $fallback ) ) {
2498 # Load the fallback localisation, with a circular reference guard
2499 if ( isset( $recursionGuard[$code] ) ) {
2500 throw new MWException( "Error: Circular fallback reference in language code $code" );
2502 $recursionGuard[$code] = true;
2503 $newDeps = self::loadLocalisation( $fallback, $disableCache );
2504 unset( $recursionGuard[$code] );
2506 $secondary = self::$mLocalisationCache[$fallback];
2507 $deps = array_merge( $deps, $newDeps );
2509 # Merge the fallback localisation with the current localisation
2510 foreach ( self::$mLocalisationKeys as $key ) {
2511 if ( isset( $cache[$key] ) ) {
2512 if ( isset( $secondary[$key] ) ) {
2513 if ( in_array( $key, self::$mMergeableMapKeys ) ) {
2514 $cache[$key] = $cache[$key] + $secondary[$key];
2515 } elseif ( in_array( $key, self::$mMergeableListKeys ) ) {
2516 $cache[$key] = array_merge( $secondary[$key], $cache[$key] );
2517 } elseif ( in_array( $key, self::$mMergeableAliasListKeys ) ) {
2518 $cache[$key] = array_merge_recursive( $cache[$key], $secondary[$key] );
2521 } else {
2522 $cache[$key] = $secondary[$key];
2526 # Merge bookstore lists if requested
2527 if ( !empty( $cache['bookstoreList']['inherit'] ) ) {
2528 $cache['bookstoreList'] = array_merge( $cache['bookstoreList'], $secondary['bookstoreList'] );
2530 if ( isset( $cache['bookstoreList']['inherit'] ) ) {
2531 unset( $cache['bookstoreList']['inherit'] );
2535 # Add dependencies to the cache entry
2536 $cache['deps'] = $deps;
2538 # Replace spaces with underscores in namespace names
2539 $cache['namespaceNames'] = str_replace( ' ', '_', $cache['namespaceNames'] );
2541 # And do the same for specialpage aliases. $page is an array.
2542 foreach ( $cache['specialPageAliases'] as &$page ) {
2543 $page = str_replace( ' ', '_', $page );
2545 # Decouple the reference to prevent accidental damage
2546 unset($page);
2548 # Save to both caches
2549 self::$mLocalisationCache[$code] = $cache;
2550 if ( !$disableCache ) {
2551 $wgMemc->set( $memcKey, $cache );
2552 $wgMemc->set( $fbMemcKey, (string) $cache['fallback'] );
2555 wfProfileOut( __METHOD__ );
2556 return $deps;
2560 * Test if a given localisation cache is out of date with respect to the
2561 * source Messages files. This is done automatically for the global cache
2562 * in $wgMemc, but is only done on certain occasions for the serialized
2563 * data file.
2565 * @param $cache mixed Either a language code or a cache array
2567 static function isLocalisationOutOfDate( $cache ) {
2568 if ( !is_array( $cache ) ) {
2569 self::loadLocalisation( $cache );
2570 $cache = self::$mLocalisationCache[$cache];
2572 // At least one language file and the MagicWord file needed
2573 if( count($cache['deps']) < 2 ) {
2574 return true;
2576 $expired = false;
2577 foreach ( $cache['deps'] as $file => $mtime ) {
2578 if ( !file_exists( $file ) || filemtime( $file ) > $mtime ) {
2579 $expired = true;
2580 break;
2583 return $expired;
2587 * Get the fallback for a given language
2589 static function getFallbackFor( $code ) {
2590 // Shortcut
2591 if ( $code === 'en' ) return false;
2593 // Local cache
2594 static $cache = array();
2595 // Quick return
2596 if ( isset($cache[$code]) ) return $cache[$code];
2598 // Try memcache
2599 global $wgMemc;
2600 $memcKey = wfMemcKey( 'fallback', $code );
2601 $fbcode = $wgMemc->get( $memcKey );
2603 if ( is_string($fbcode) ) {
2604 // False is stored as a string to detect failures in memcache properly
2605 if ( $fbcode === '' ) $fbcode = false;
2607 // Update local cache and return
2608 $cache[$code] = $fbcode;
2609 return $fbcode;
2612 // Nothing in caches, load and and update both caches
2613 self::loadLocalisation( $code );
2614 $fbcode = self::$mLocalisationCache[$code]['fallback'];
2616 $cache[$code] = $fbcode;
2617 $wgMemc->set( $memcKey, (string) $fbcode );
2619 return $fbcode;
2622 /**
2623 * Get all messages for a given language
2625 static function getMessagesFor( $code ) {
2626 self::loadLocalisation( $code );
2627 return self::$mLocalisationCache[$code]['messages'];
2630 /**
2631 * Get a message for a given language
2633 static function getMessageFor( $key, $code ) {
2634 self::loadLocalisation( $code );
2635 return isset( self::$mLocalisationCache[$code]['messages'][$key] ) ? self::$mLocalisationCache[$code]['messages'][$key] : null;
2639 * Load localisation data for this object
2641 function load() {
2642 if ( !$this->mLoaded ) {
2643 self::loadLocalisation( $this->getCode() );
2644 $cache =& self::$mLocalisationCache[$this->getCode()];
2645 foreach ( self::$mLocalisationKeys as $key ) {
2646 $this->$key = $cache[$key];
2648 $this->mLoaded = true;
2650 $this->fixUpSettings();
2655 * Do any necessary post-cache-load settings adjustment
2657 function fixUpSettings() {
2658 global $wgExtraNamespaces, $wgMetaNamespace, $wgMetaNamespaceTalk,
2659 $wgNamespaceAliases, $wgAmericanDates;
2660 wfProfileIn( __METHOD__ );
2661 if ( $wgExtraNamespaces ) {
2662 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames;
2665 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
2666 if ( $wgMetaNamespaceTalk ) {
2667 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
2668 } else {
2669 $talk = $this->namespaceNames[NS_PROJECT_TALK];
2670 $this->namespaceNames[NS_PROJECT_TALK] =
2671 $this->fixVariableInNamespace( $talk );
2674 # The above mixing may leave namespaces out of canonical order.
2675 # Re-order by namespace ID number...
2676 ksort( $this->namespaceNames );
2678 # Put namespace names and aliases into a hashtable.
2679 # If this is too slow, then we should arrange it so that it is done
2680 # before caching. The catch is that at pre-cache time, the above
2681 # class-specific fixup hasn't been done.
2682 $this->mNamespaceIds = array();
2683 foreach ( $this->namespaceNames as $index => $name ) {
2684 $this->mNamespaceIds[$this->lc($name)] = $index;
2686 if ( $this->namespaceAliases ) {
2687 foreach ( $this->namespaceAliases as $name => $index ) {
2688 if ( $index === NS_PROJECT_TALK ) {
2689 unset( $this->namespaceAliases[$name] );
2690 $name = $this->fixVariableInNamespace( $name );
2691 $this->namespaceAliases[$name] = $index;
2693 $this->mNamespaceIds[$this->lc($name)] = $index;
2696 if ( $wgNamespaceAliases ) {
2697 foreach ( $wgNamespaceAliases as $name => $index ) {
2698 $this->mNamespaceIds[$this->lc($name)] = $index;
2702 if ( $this->defaultDateFormat == 'dmy or mdy' ) {
2703 $this->defaultDateFormat = $wgAmericanDates ? 'mdy' : 'dmy';
2705 wfProfileOut( __METHOD__ );
2708 function fixVariableInNamespace( $talk ) {
2709 if ( strpos( $talk, '$1' ) === false ) return $talk;
2711 global $wgMetaNamespace;
2712 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
2714 # Allow grammar transformations
2715 # Allowing full message-style parsing would make simple requests
2716 # such as action=raw much more expensive than they need to be.
2717 # This will hopefully cover most cases.
2718 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
2719 array( &$this, 'replaceGrammarInNamespace' ), $talk );
2720 return str_replace( ' ', '_', $talk );
2723 function replaceGrammarInNamespace( $m ) {
2724 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
2727 static function getCaseMaps() {
2728 static $wikiUpperChars, $wikiLowerChars;
2729 if ( isset( $wikiUpperChars ) ) {
2730 return array( $wikiUpperChars, $wikiLowerChars );
2733 wfProfileIn( __METHOD__ );
2734 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
2735 if ( $arr === false ) {
2736 throw new MWException(
2737 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
2739 extract( $arr );
2740 wfProfileOut( __METHOD__ );
2741 return array( $wikiUpperChars, $wikiLowerChars );
2744 function formatTimePeriod( $seconds ) {
2745 if ( $seconds < 10 ) {
2746 return $this->formatNum( sprintf( "%.1f", $seconds ) ) . wfMsg( 'seconds-abbrev' );
2747 } elseif ( $seconds < 60 ) {
2748 return $this->formatNum( round( $seconds ) ) . wfMsg( 'seconds-abbrev' );
2749 } elseif ( $seconds < 3600 ) {
2750 return $this->formatNum( floor( $seconds / 60 ) ) . wfMsg( 'minutes-abbrev' ) .
2751 $this->formatNum( round( fmod( $seconds, 60 ) ) ) . wfMsg( 'seconds-abbrev' );
2752 } else {
2753 $hours = floor( $seconds / 3600 );
2754 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
2755 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
2756 return $this->formatNum( $hours ) . wfMsg( 'hours-abbrev' ) .
2757 $this->formatNum( $minutes ) . wfMsg( 'minutes-abbrev' ) .
2758 $this->formatNum( $secondsPart ) . wfMsg( 'seconds-abbrev' );
2762 function formatBitrate( $bps ) {
2763 $units = array( 'bps', 'kbps', 'Mbps', 'Gbps' );
2764 if ( $bps <= 0 ) {
2765 return $this->formatNum( $bps ) . $units[0];
2767 $unitIndex = floor( log10( $bps ) / 3 );
2768 $mantissa = $bps / pow( 1000, $unitIndex );
2769 if ( $mantissa < 10 ) {
2770 $mantissa = round( $mantissa, 1 );
2771 } else {
2772 $mantissa = round( $mantissa );
2774 return $this->formatNum( $mantissa ) . $units[$unitIndex];
2778 * Format a size in bytes for output, using an appropriate
2779 * unit (B, KB, MB or GB) according to the magnitude in question
2781 * @param $size Size to format
2782 * @return string Plain text (not HTML)
2784 function formatSize( $size ) {
2785 // For small sizes no decimal places necessary
2786 $round = 0;
2787 if( $size > 1024 ) {
2788 $size = $size / 1024;
2789 if( $size > 1024 ) {
2790 $size = $size / 1024;
2791 // For MB and bigger two decimal places are smarter
2792 $round = 2;
2793 if( $size > 1024 ) {
2794 $size = $size / 1024;
2795 $msg = 'size-gigabytes';
2796 } else {
2797 $msg = 'size-megabytes';
2799 } else {
2800 $msg = 'size-kilobytes';
2802 } else {
2803 $msg = 'size-bytes';
2805 $size = round( $size, $round );
2806 $text = $this->getMessageFromDB( $msg );
2807 return str_replace( '$1', $this->formatNum( $size ), $text );