Follow-up to r50869:
[mediawiki.git] / languages / Language.php
blob20daeb8b88d4abb563df53fcfe04321166831bfe
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 convert($t, $i, $v) {return $t;}
39 function parserConvert($t, $p) {return $t;}
40 function getVariants() { return array( $this->mLang->getCode() ); }
41 function getPreferredVariant() {return $this->mLang->getCode(); }
42 function findVariantLink(&$l, &$n, $ignoreOtherCond = false) {}
43 function getExtraHashOptions() {return '';}
44 function getParsedTitle() {return '';}
45 function markNoConversion($text, $noParse=false) {return $text;}
46 function convertCategoryKey( $key ) {return $key; }
47 function convertLinkToAllVariants($text){ return array( $this->mLang->getCode() => $text); }
48 function armourMath($text){ return $text; }
49 function groupConvert($group) {return '';}
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', '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";
1796 * An arrow, depending on the language direction
1798 * @return string
1800 function getArrow() {
1801 return $this->isRTL() ? '←' : '→';
1805 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
1807 * @return bool
1809 function linkPrefixExtension() {
1810 $this->load();
1811 return $this->linkPrefixExtension;
1814 function &getMagicWords() {
1815 $this->load();
1816 return $this->magicWords;
1819 # Fill a MagicWord object with data from here
1820 function getMagic( &$mw ) {
1821 if ( !$this->mMagicHookDone ) {
1822 $this->mMagicHookDone = true;
1823 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
1825 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
1826 $rawEntry = $this->mMagicExtensions[$mw->mId];
1827 } else {
1828 $magicWords =& $this->getMagicWords();
1829 if ( isset( $magicWords[$mw->mId] ) ) {
1830 $rawEntry = $magicWords[$mw->mId];
1831 } else {
1832 # Fall back to English if local list is incomplete
1833 $magicWords =& Language::getMagicWords();
1834 if ( !isset($magicWords[$mw->mId]) ) {
1835 throw new MWException("Magic word '{$mw->mId}' not found" );
1837 $rawEntry = $magicWords[$mw->mId];
1841 if( !is_array( $rawEntry ) ) {
1842 error_log( "\"$rawEntry\" is not a valid magic thingie for \"$mw->mId\"" );
1843 } else {
1844 $mw->mCaseSensitive = $rawEntry[0];
1845 $mw->mSynonyms = array_slice( $rawEntry, 1 );
1850 * Add magic words to the extension array
1852 function addMagicWordsByLang( $newWords ) {
1853 $code = $this->getCode();
1854 $fallbackChain = array();
1855 while ( $code && !in_array( $code, $fallbackChain ) ) {
1856 $fallbackChain[] = $code;
1857 $code = self::getFallbackFor( $code );
1859 if ( !in_array( 'en', $fallbackChain ) ) {
1860 $fallbackChain[] = 'en';
1862 $fallbackChain = array_reverse( $fallbackChain );
1863 foreach ( $fallbackChain as $code ) {
1864 if ( isset( $newWords[$code] ) ) {
1865 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
1871 * Get special page names, as an associative array
1872 * case folded alias => real name
1874 function getSpecialPageAliases() {
1875 $this->load();
1877 // Cache aliases because it may be slow to load them
1878 if ( !isset( $this->mExtendedSpecialPageAliases ) ) {
1880 // Initialise array
1881 $this->mExtendedSpecialPageAliases = $this->specialPageAliases;
1883 global $wgExtensionAliasesFiles;
1884 foreach ( $wgExtensionAliasesFiles as $file ) {
1886 // Fail fast
1887 if ( !file_exists($file) )
1888 throw new MWException( "Aliases file does not exist: $file" );
1890 $aliases = array();
1891 require($file);
1893 // Check the availability of aliases
1894 if ( !isset($aliases['en']) )
1895 throw new MWException( "Malformed aliases file: $file" );
1897 // Merge all aliases in fallback chain
1898 $code = $this->getCode();
1899 do {
1900 if ( !isset($aliases[$code]) ) continue;
1902 $aliases[$code] = $this->fixSpecialPageAliases( $aliases[$code] );
1903 /* Merge the aliases, THIS will break if there is special page name
1904 * which looks like a numerical key, thanks to PHP...
1905 * See the array_merge_recursive manual entry */
1906 $this->mExtendedSpecialPageAliases = array_merge_recursive(
1907 $this->mExtendedSpecialPageAliases, $aliases[$code] );
1909 } while ( $code = self::getFallbackFor( $code ) );
1912 wfRunHooks( 'LanguageGetSpecialPageAliases',
1913 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
1916 return $this->mExtendedSpecialPageAliases;
1920 * Function to fix special page aliases. Will convert the first letter to
1921 * upper case and spaces to underscores. Can be given a full aliases array,
1922 * in which case it will recursively fix all aliases.
1924 public function fixSpecialPageAliases( $mixed ) {
1925 // Work recursively until in string level
1926 if ( is_array($mixed) ) {
1927 $callback = array( $this, 'fixSpecialPageAliases' );
1928 return array_map( $callback, $mixed );
1930 return str_replace( ' ', '_', $this->ucfirst( $mixed ) );
1934 * Italic is unsuitable for some languages
1936 * @param $text String: the text to be emphasized.
1937 * @return string
1939 function emphasize( $text ) {
1940 return "<em>$text</em>";
1944 * Normally we output all numbers in plain en_US style, that is
1945 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
1946 * point twohundredthirtyfive. However this is not sutable for all
1947 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
1948 * Icelandic just want to use commas instead of dots, and dots instead
1949 * of commas like "293.291,235".
1951 * An example of this function being called:
1952 * <code>
1953 * wfMsg( 'message', $wgLang->formatNum( $num ) )
1954 * </code>
1956 * See LanguageGu.php for the Gujarati implementation and
1957 * $separatorTransformTable on MessageIs.php for
1958 * the , => . and . => , implementation.
1960 * @todo check if it's viable to use localeconv() for the decimal
1961 * separator thing.
1962 * @param $number Mixed: the string to be formatted, should be an integer
1963 * or a floating point number.
1964 * @param $nocommafy Bool: set to true for special numbers like dates
1965 * @return string
1967 function formatNum( $number, $nocommafy = false ) {
1968 global $wgTranslateNumerals;
1969 if (!$nocommafy) {
1970 $number = $this->commafy($number);
1971 $s = $this->separatorTransformTable();
1972 if ($s) { $number = strtr($number, $s); }
1975 if ($wgTranslateNumerals) {
1976 $s = $this->digitTransformTable();
1977 if ($s) { $number = strtr($number, $s); }
1980 return $number;
1983 function parseFormattedNumber( $number ) {
1984 $s = $this->digitTransformTable();
1985 if ($s) { $number = strtr($number, array_flip($s)); }
1987 $s = $this->separatorTransformTable();
1988 if ($s) { $number = strtr($number, array_flip($s)); }
1990 $number = strtr( $number, array (',' => '') );
1991 return $number;
1995 * Adds commas to a given number
1997 * @param $_ mixed
1998 * @return string
2000 function commafy($_) {
2001 return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_)));
2004 function digitTransformTable() {
2005 $this->load();
2006 return $this->digitTransformTable;
2009 function separatorTransformTable() {
2010 $this->load();
2011 return $this->separatorTransformTable;
2016 * Take a list of strings and build a locale-friendly comma-separated
2017 * list, using the local comma-separator message.
2018 * The last two strings are chained with an "and".
2020 * @param $l Array
2021 * @return string
2023 function listToText( $l ) {
2024 $s = '';
2025 $m = count( $l ) - 1;
2026 if( $m == 1 ) {
2027 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
2029 else {
2030 for ( $i = $m; $i >= 0; $i-- ) {
2031 if ( $i == $m ) {
2032 $s = $l[$i];
2033 } else if( $i == $m - 1 ) {
2034 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
2035 } else {
2036 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
2039 return $s;
2044 * Take a list of strings and build a locale-friendly comma-separated
2045 * list, using the local comma-separator message.
2046 * @param $list array of strings to put in a comma list
2047 * @return string
2049 function commaList( $list ) {
2050 return implode(
2051 $list,
2052 wfMsgExt( 'comma-separator', array( 'escapenoentities', 'language' => $this ) ) );
2056 * Take a list of strings and build a locale-friendly semicolon-separated
2057 * list, using the local semicolon-separator message.
2058 * @param $list array of strings to put in a semicolon list
2059 * @return string
2061 function semicolonList( $list ) {
2062 return implode(
2063 $list,
2064 wfMsgExt( 'semicolon-separator', array( 'escapenoentities', 'language' => $this ) ) );
2068 * Same as commaList, but separate it with the pipe instead.
2069 * @param $list array of strings to put in a pipe list
2070 * @return string
2072 function pipeList( $list ) {
2073 return implode(
2074 $list,
2075 wfMsgExt( 'pipe-separator', array( 'escapenoentities', 'language' => $this ) ) );
2079 * Truncate a string to a specified length in bytes, appending an optional
2080 * string (e.g. for ellipses)
2082 * The database offers limited byte lengths for some columns in the database;
2083 * multi-byte character sets mean we need to ensure that only whole characters
2084 * are included, otherwise broken characters can be passed to the user
2086 * If $length is negative, the string will be truncated from the beginning
2088 * @param $string String to truncate
2089 * @param $length Int: maximum length (excluding ellipses)
2090 * @param $ellipsis String to append to the truncated text
2091 * @return string
2093 function truncate( $string, $length, $ellipsis = '...' ) {
2094 # Use the localized ellipsis character
2095 if( $ellipsis == '...' ) {
2096 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
2099 if( $length == 0 ) {
2100 return $ellipsis;
2102 if ( strlen( $string ) <= abs( $length ) ) {
2103 return $string;
2105 if( $length > 0 ) {
2106 $string = substr( $string, 0, $length );
2107 $char = ord( $string[strlen( $string ) - 1] );
2108 $m = array();
2109 if ($char >= 0xc0) {
2110 # We got the first byte only of a multibyte char; remove it.
2111 $string = substr( $string, 0, -1 );
2112 } elseif( $char >= 0x80 &&
2113 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
2114 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) ) {
2115 # We chopped in the middle of a character; remove it
2116 $string = $m[1];
2118 return $string . $ellipsis;
2119 } else {
2120 $string = substr( $string, $length );
2121 $char = ord( $string[0] );
2122 if( $char >= 0x80 && $char < 0xc0 ) {
2123 # We chopped in the middle of a character; remove the whole thing
2124 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
2126 return $ellipsis . $string;
2131 * Grammatical transformations, needed for inflected languages
2132 * Invoked by putting {{grammar:case|word}} in a message
2134 * @param $word string
2135 * @param $case string
2136 * @return string
2138 function convertGrammar( $word, $case ) {
2139 global $wgGrammarForms;
2140 if ( isset($wgGrammarForms[$this->getCode()][$case][$word]) ) {
2141 return $wgGrammarForms[$this->getCode()][$case][$word];
2143 return $word;
2147 * Provides an alternative text depending on specified gender.
2148 * Usage {{gender:username|masculine|feminine|neutral}}.
2149 * username is optional, in which case the gender of current user is used,
2150 * but only in (some) interface messages; otherwise default gender is used.
2151 * If second or third parameter are not specified, masculine is used.
2152 * These details may be overriden per language.
2154 function gender( $gender, $forms ) {
2155 if ( !count($forms) ) { return ''; }
2156 $forms = $this->preConvertPlural( $forms, 2 );
2157 if ( $gender === 'male' ) return $forms[0];
2158 if ( $gender === 'female' ) return $forms[1];
2159 return isset($forms[2]) ? $forms[2] : $forms[0];
2163 * Plural form transformations, needed for some languages.
2164 * For example, there are 3 form of plural in Russian and Polish,
2165 * depending on "count mod 10". See [[w:Plural]]
2166 * For English it is pretty simple.
2168 * Invoked by putting {{plural:count|wordform1|wordform2}}
2169 * or {{plural:count|wordform1|wordform2|wordform3}}
2171 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
2173 * @param $count Integer: non-localized number
2174 * @param $forms Array: different plural forms
2175 * @return string Correct form of plural for $count in this language
2177 function convertPlural( $count, $forms ) {
2178 if ( !count($forms) ) { return ''; }
2179 $forms = $this->preConvertPlural( $forms, 2 );
2181 return ( $count == 1 ) ? $forms[0] : $forms[1];
2185 * Checks that convertPlural was given an array and pads it to requested
2186 * amound of forms by copying the last one.
2188 * @param $count Integer: How many forms should there be at least
2189 * @param $forms Array of forms given to convertPlural
2190 * @return array Padded array of forms or an exception if not an array
2192 protected function preConvertPlural( /* Array */ $forms, $count ) {
2193 while ( count($forms) < $count ) {
2194 $forms[] = $forms[count($forms)-1];
2196 return $forms;
2200 * For translaing of expiry times
2201 * @param $str String: the validated block time in English
2202 * @return Somehow translated block time
2203 * @see LanguageFi.php for example implementation
2205 function translateBlockExpiry( $str ) {
2207 $scBlockExpiryOptions = $this->getMessageFromDB( 'ipboptions' );
2209 if ( $scBlockExpiryOptions == '-') {
2210 return $str;
2213 foreach (explode(',', $scBlockExpiryOptions) as $option) {
2214 if ( strpos($option, ":") === false )
2215 continue;
2216 list($show, $value) = explode(":", $option);
2217 if ( strcmp ( $str, $value) == 0 ) {
2218 return htmlspecialchars( trim( $show ) );
2222 return $str;
2226 * languages like Chinese need to be segmented in order for the diff
2227 * to be of any use
2229 * @param $text String
2230 * @return String
2232 function segmentForDiff( $text ) {
2233 return $text;
2237 * and unsegment to show the result
2239 * @param $text String
2240 * @return String
2242 function unsegmentForDiff( $text ) {
2243 return $text;
2246 # convert text to different variants of a language.
2247 function convert( $text, $isTitle = false, $variant = null ) {
2248 return $this->mConverter->convert($text, $isTitle, $variant);
2251 # Convert text from within Parser
2252 function parserConvert( $text, &$parser ) {
2253 return $this->mConverter->parserConvert( $text, $parser );
2256 # Check if this is a language with variants
2257 function hasVariants(){
2258 return sizeof($this->getVariants())>1;
2261 # Put custom tags (e.g. -{ }-) around math to prevent conversion
2262 function armourMath($text){
2263 return $this->mConverter->armourMath($text);
2268 * Perform output conversion on a string, and encode for safe HTML output.
2269 * @param $text String
2270 * @param $isTitle Bool -- wtf?
2271 * @return string
2272 * @todo this should get integrated somewhere sane
2274 function convertHtml( $text, $isTitle = false ) {
2275 return htmlspecialchars( $this->convert( $text, $isTitle ) );
2278 function convertCategoryKey( $key ) {
2279 return $this->mConverter->convertCategoryKey( $key );
2283 * get the list of variants supported by this langauge
2284 * see sample implementation in LanguageZh.php
2286 * @return array an array of language codes
2288 function getVariants() {
2289 return $this->mConverter->getVariants();
2293 function getPreferredVariant( $fromUser = true ) {
2294 return $this->mConverter->getPreferredVariant( $fromUser );
2298 * if a language supports multiple variants, it is
2299 * possible that non-existing link in one variant
2300 * actually exists in another variant. this function
2301 * tries to find it. See e.g. LanguageZh.php
2303 * @param $link String: the name of the link
2304 * @param $nt Mixed: the title object of the link
2305 * @param boolean $ignoreOtherCond: to disable other conditions when
2306 * we need to transclude a template or update a category's link
2307 * @return null the input parameters may be modified upon return
2309 function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
2310 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
2314 * If a language supports multiple variants, converts text
2315 * into an array of all possible variants of the text:
2316 * 'variant' => text in that variant
2318 function convertLinkToAllVariants($text){
2319 return $this->mConverter->convertLinkToAllVariants($text);
2324 * returns language specific options used by User::getPageRenderHash()
2325 * for example, the preferred language variant
2327 * @return string
2329 function getExtraHashOptions() {
2330 return $this->mConverter->getExtraHashOptions();
2334 * for languages that support multiple variants, the title of an
2335 * article may be displayed differently in different variants. this
2336 * function returns the apporiate title defined in the body of the article.
2338 * @return string
2340 function getParsedTitle() {
2341 return $this->mConverter->getParsedTitle();
2345 * Enclose a string with the "no conversion" tag. This is used by
2346 * various functions in the Parser
2348 * @param $text String: text to be tagged for no conversion
2349 * @param $noParse
2350 * @return string the tagged text
2352 function markNoConversion( $text, $noParse=false ) {
2353 return $this->mConverter->markNoConversion( $text, $noParse );
2357 * Callback function for magicword 'groupconvert'
2359 * @param string $group: the group name called for
2360 * @return blank string
2362 function groupConvert( $group ) {
2363 return $this->mConverter->groupConvert( $group );
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 );