=Update.
[lhc/web/wiklou.git] / languages / Language.php
1 <?php
2 /**
3 * @package MediaWiki
4 * @subpackage Language
5 */
6
7 if( !defined( 'MEDIAWIKI' ) ) {
8 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
9 exit( 1 );
10 }
11
12 #
13 # In general you should not make customizations in these language files
14 # directly, but should use the MediaWiki: special namespace to customize
15 # user interface messages through the wiki.
16 # See http://meta.wikipedia.org/wiki/MediaWiki_namespace
17 #
18 # NOTE TO TRANSLATORS: Do not copy this whole file when making translations!
19 # A lot of common constants and a base class with inheritable methods are
20 # defined here, which should not be redefined. See the other LanguageXx.php
21 # files for examples.
22 #
23
24 # Read language names
25 global $wgLanguageNames;
26 require_once( 'Names.php' );
27
28 global $wgInputEncoding, $wgOutputEncoding;
29
30 /**
31 * These are always UTF-8, they exist only for backwards compatibility
32 */
33 $wgInputEncoding = "UTF-8";
34 $wgOutputEncoding = "UTF-8";
35
36 if( function_exists( 'mb_strtoupper' ) ) {
37 mb_internal_encoding('UTF-8');
38 }
39
40 /* a fake language converter */
41 class FakeConverter {
42 var $mLang;
43 function FakeConverter($langobj) {$this->mLang = $langobj;}
44 function convert($t, $i) {return $t;}
45 function parserConvert($t, $p) {return $t;}
46 function getVariants() { return array( $this->mLang->getCode() ); }
47 function getPreferredVariant() {return $this->mLang->getCode(); }
48 function findVariantLink(&$l, &$n) {}
49 function getExtraHashOptions() {return '';}
50 function getParsedTitle() {return '';}
51 function markNoConversion($text, $noParse=false) {return $text;}
52 function convertCategoryKey( $key ) {return $key; }
53 function convertLinkToAllVariants($text){ return array( $this->mLang->getCode() => $text); }
54 function setNoTitleConvert(){}
55 }
56
57 #--------------------------------------------------------------------------
58 # Internationalisation code
59 #--------------------------------------------------------------------------
60
61 class Language {
62 var $mConverter, $mVariants, $mCode, $mLoaded = false;
63
64 static public $mLocalisationKeys = array( 'fallback', 'namespaceNames',
65 'quickbarSettings', 'skinNames', 'mathNames',
66 'bookstoreList', 'magicWords', 'messages', 'rtl', 'digitTransformTable',
67 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
68 'defaultUserOptionOverrides', 'linkTrail', 'namespaceAliases',
69 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
70 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases' );
71
72 static public $mMergeableMapKeys = array( 'messages', 'namespaceNames', 'mathNames',
73 'dateFormats', 'defaultUserOptionOverrides', 'magicWords' );
74
75 static public $mMergeableListKeys = array( 'extraUserToggles' );
76
77 static public $mMergeableAliasListKeys = array( 'specialPageAliases' );
78
79 static public $mLocalisationCache = array();
80
81 static public $mWeekdayMsgs = array(
82 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
83 'friday', 'saturday'
84 );
85
86 static public $mWeekdayAbbrevMsgs = array(
87 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
88 );
89
90 static public $mMonthMsgs = array(
91 'january', 'february', 'march', 'april', 'may_long', 'june',
92 'july', 'august', 'september', 'october', 'november',
93 'december'
94 );
95 static public $mMonthGenMsgs = array(
96 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
97 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
98 'december-gen'
99 );
100 static public $mMonthAbbrevMsgs = array(
101 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
102 'sep', 'oct', 'nov', 'dec'
103 );
104
105 /**
106 * Create a language object for a given language code
107 */
108 static function factory( $code ) {
109 global $IP;
110 static $recursionLevel = 0;
111
112 if ( $code == 'en' ) {
113 $class = 'Language';
114 } else {
115 $class = 'Language' . str_replace( '-', '_', ucfirst( $code ) );
116 // Preload base classes to work around APC/PHP5 bug
117 if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
118 include_once("$IP/languages/classes/$class.deps.php");
119 }
120 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
121 include_once("$IP/languages/classes/$class.php");
122 }
123 }
124
125 if ( $recursionLevel > 5 ) {
126 throw new MWException( "Language fallback loop detected when creating class $class\n" );
127 }
128
129 if( ! class_exists( $class ) ) {
130 $fallback = Language::getFallbackFor( $code );
131 ++$recursionLevel;
132 $lang = Language::factory( $fallback );
133 --$recursionLevel;
134 $lang->setCode( $code );
135 } else {
136 $lang = new $class;
137 }
138
139 return $lang;
140 }
141
142 function __construct() {
143 $this->mConverter = new FakeConverter($this);
144 // Set the code to the name of the descendant
145 if ( get_class( $this ) == 'Language' ) {
146 $this->mCode = 'en';
147 } else {
148 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
149 }
150 }
151
152 /**
153 * Hook which will be called if this is the content language.
154 * Descendants can use this to register hook functions or modify globals
155 */
156 function initContLang() {}
157
158 /**
159 * @deprecated
160 * @return array
161 */
162 function getDefaultUserOptions() {
163 return User::getDefaultOptions();
164 }
165
166 /**
167 * Exports $wgBookstoreListEn
168 * @return array
169 */
170 function getBookstoreList() {
171 $this->load();
172 return $this->bookstoreList;
173 }
174
175 /**
176 * @return array
177 */
178 function getNamespaces() {
179 $this->load();
180 return $this->namespaceNames;
181 }
182
183 /**
184 * A convenience function that returns the same thing as
185 * getNamespaces() except with the array values changed to ' '
186 * where it found '_', useful for producing output to be displayed
187 * e.g. in <select> forms.
188 *
189 * @return array
190 */
191 function getFormattedNamespaces() {
192 $ns = $this->getNamespaces();
193 foreach($ns as $k => $v) {
194 $ns[$k] = strtr($v, '_', ' ');
195 }
196 return $ns;
197 }
198
199 /**
200 * Get a namespace value by key
201 * <code>
202 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
203 * echo $mw_ns; // prints 'MediaWiki'
204 * </code>
205 *
206 * @param int $index the array key of the namespace to return
207 * @return mixed, string if the namespace value exists, otherwise false
208 */
209 function getNsText( $index ) {
210 $ns = $this->getNamespaces();
211 return isset( $ns[$index] ) ? $ns[$index] : false;
212 }
213
214 /**
215 * A convenience function that returns the same thing as
216 * getNsText() except with '_' changed to ' ', useful for
217 * producing output.
218 *
219 * @return array
220 */
221 function getFormattedNsText( $index ) {
222 $ns = $this->getNsText( $index );
223 return strtr($ns, '_', ' ');
224 }
225
226 /**
227 * Get a namespace key by value, case insensetive.
228 *
229 * @param string $text
230 * @return mixed An integer if $text is a valid value otherwise false
231 */
232 function getNsIndex( $text ) {
233 $this->load();
234 $index = @$this->mNamespaceIds[$this->lc($text)];
235 if ( is_null( $index ) ) {
236 return false;
237 } else {
238 return $index;
239 }
240 }
241
242 /**
243 * short names for language variants used for language conversion links.
244 *
245 * @param string $code
246 * @return string
247 */
248 function getVariantname( $code ) {
249 return $this->getMessageFromDB( "variantname-$code" );
250 }
251
252 function specialPage( $name ) {
253 $aliases = $this->getSpecialPageAliases();
254 if ( isset( $aliases[$name][0] ) ) {
255 $name = $aliases[$name][0];
256 }
257 return $this->getNsText(NS_SPECIAL) . ':' . $name;
258 }
259
260 function getQuickbarSettings() {
261 $this->load();
262 return $this->quickbarSettings;
263 }
264
265 function getSkinNames() {
266 $this->load();
267 return $this->skinNames;
268 }
269
270 function getMathNames() {
271 $this->load();
272 return $this->mathNames;
273 }
274
275 function getDatePreferences() {
276 $this->load();
277 return $this->datePreferences;
278 }
279
280 function getDateFormats() {
281 $this->load();
282 return $this->dateFormats;
283 }
284
285 function getDefaultDateFormat() {
286 $this->load();
287 return $this->defaultDateFormat;
288 }
289
290 function getDatePreferenceMigrationMap() {
291 $this->load();
292 return $this->datePreferenceMigrationMap;
293 }
294
295 function getDefaultUserOptionOverrides() {
296 $this->load();
297 return $this->defaultUserOptionOverrides;
298 }
299
300 function getExtraUserToggles() {
301 $this->load();
302 return $this->extraUserToggles;
303 }
304
305 function getUserToggle( $tog ) {
306 return $this->getMessageFromDB( "tog-$tog" );
307 }
308
309 /**
310 * Get language names, indexed by code.
311 * If $customisedOnly is true, only returns codes with a messages file
312 */
313 function getLanguageNames( $customisedOnly = false ) {
314 global $wgLanguageNames;
315 if ( !$customisedOnly ) {
316 return $wgLanguageNames;
317 }
318
319 global $IP;
320 $messageFiles = glob( "$IP/languages/messages/Messages*.php" );
321 $names = array();
322 foreach ( $messageFiles as $file ) {
323 if( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $file, $m ) ) {
324 $code = str_replace( '_', '-', strtolower( $m[1] ) );
325 if ( isset( $wgLanguageNames[$code] ) ) {
326 $names[$code] = $wgLanguageNames[$code];
327 }
328 }
329 }
330 return $names;
331 }
332
333 /**
334 * Ugly hack to get a message maybe from the MediaWiki namespace, if this
335 * language object is the content or user language.
336 */
337 function getMessageFromDB( $msg ) {
338 global $wgContLang, $wgLang;
339 if ( $wgContLang->getCode() == $this->getCode() ) {
340 # Content language
341 return wfMsgForContent( $msg );
342 } elseif ( $wgLang->getCode() == $this->getCode() ) {
343 # User language
344 return wfMsg( $msg );
345 } else {
346 # Neither, get from localisation
347 return $this->getMessage( $msg );
348 }
349 }
350
351 function getLanguageName( $code ) {
352 global $wgLanguageNames;
353 if ( ! array_key_exists( $code, $wgLanguageNames ) ) {
354 return '';
355 }
356 return $wgLanguageNames[$code];
357 }
358
359 function getMonthName( $key ) {
360 return $this->getMessageFromDB( self::$mMonthMsgs[$key-1] );
361 }
362
363 function getMonthNameGen( $key ) {
364 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key-1] );
365 }
366
367 function getMonthAbbreviation( $key ) {
368 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key-1] );
369 }
370
371 function getWeekdayName( $key ) {
372 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key-1] );
373 }
374
375 function getWeekdayAbbreviation( $key ) {
376 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key-1] );
377 }
378
379 /**
380 * Used by date() and time() to adjust the time output.
381 * @public
382 * @param int $ts the time in date('YmdHis') format
383 * @param mixed $tz adjust the time by this amount (default false,
384 * mean we get user timecorrection setting)
385 * @return int
386 */
387 function userAdjust( $ts, $tz = false ) {
388 global $wgUser, $wgLocalTZoffset;
389
390 if (!$tz) {
391 $tz = $wgUser->getOption( 'timecorrection' );
392 }
393
394 # minutes and hours differences:
395 $minDiff = 0;
396 $hrDiff = 0;
397
398 if ( $tz === '' ) {
399 # Global offset in minutes.
400 if( isset($wgLocalTZoffset) ) {
401 $hrDiff = $wgLocalTZoffset % 60;
402 $minDiff = $wgLocalTZoffset - ($hrDiff * 60);
403 }
404 } elseif ( strpos( $tz, ':' ) !== false ) {
405 $tzArray = explode( ':', $tz );
406 $hrDiff = intval($tzArray[0]);
407 $minDiff = intval($hrDiff < 0 ? -$tzArray[1] : $tzArray[1]);
408 } else {
409 $hrDiff = intval( $tz );
410 }
411
412 # No difference ? Return time unchanged
413 if ( 0 == $hrDiff && 0 == $minDiff ) { return $ts; }
414
415 # Generate an adjusted date
416 $t = mktime( (
417 (int)substr( $ts, 8, 2) ) + $hrDiff, # Hours
418 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
419 (int)substr( $ts, 12, 2 ), # Seconds
420 (int)substr( $ts, 4, 2 ), # Month
421 (int)substr( $ts, 6, 2 ), # Day
422 (int)substr( $ts, 0, 4 ) ); #Year
423 return date( 'YmdHis', $t );
424 }
425
426 /**
427 * This is a workalike of PHP's date() function, but with better
428 * internationalisation, a reduced set of format characters, and a better
429 * escaping format.
430 *
431 * Supported format characters are dDjlNwzWFmMntLYyaAgGhHiscrU. See the
432 * PHP manual for definitions. There are a number of extensions, which
433 * start with "x":
434 *
435 * xn Do not translate digits of the next numeric format character
436 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
437 * xr Use roman numerals for the next numeric format character
438 * xx Literal x
439 * xg Genitive month name
440 *
441 * Characters enclosed in double quotes will be considered literal (with
442 * the quotes themselves removed). Unmatched quotes will be considered
443 * literal quotes. Example:
444 *
445 * "The month is" F => The month is January
446 * i's" => 20'11"
447 *
448 * Backslash escaping is also supported.
449 *
450 * @param string $format
451 * @param string $ts 14-character timestamp
452 * YYYYMMDDHHMMSS
453 * 01234567890123
454 */
455 function sprintfDate( $format, $ts ) {
456 $s = '';
457 $raw = false;
458 $roman = false;
459 $unix = false;
460 $rawToggle = false;
461 for ( $p = 0; $p < strlen( $format ); $p++ ) {
462 $num = false;
463 $code = $format[$p];
464 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
465 $code .= $format[++$p];
466 }
467
468 switch ( $code ) {
469 case 'xx':
470 $s .= 'x';
471 break;
472 case 'xn':
473 $raw = true;
474 break;
475 case 'xN':
476 $rawToggle = !$rawToggle;
477 break;
478 case 'xr':
479 $roman = true;
480 break;
481 case 'xg':
482 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
483 break;
484 case 'd':
485 $num = substr( $ts, 6, 2 );
486 break;
487 case 'D':
488 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
489 $s .= $this->getWeekdayAbbreviation( date( 'w', $unix ) + 1 );
490 break;
491 case 'j':
492 $num = intval( substr( $ts, 6, 2 ) );
493 break;
494 case 'l':
495 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
496 $s .= $this->getWeekdayName( date( 'w', $unix ) + 1 );
497 break;
498 case 'N':
499 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
500 $w = date( 'w', $unix );
501 $num = $w ? $w : 7;
502 break;
503 case 'w':
504 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
505 $num = date( 'w', $unix );
506 break;
507 case 'z':
508 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
509 $num = date( 'z', $unix );
510 break;
511 case 'W':
512 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
513 $num = date( 'W', $unix );
514 break;
515 case 'F':
516 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
517 break;
518 case 'm':
519 $num = substr( $ts, 4, 2 );
520 break;
521 case 'M':
522 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
523 break;
524 case 'n':
525 $num = intval( substr( $ts, 4, 2 ) );
526 break;
527 case 't':
528 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
529 $num = date( 't', $unix );
530 break;
531 case 'L':
532 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
533 $num = date( 'L', $unix );
534 break;
535 case 'Y':
536 $num = substr( $ts, 0, 4 );
537 break;
538 case 'y':
539 $num = substr( $ts, 2, 2 );
540 break;
541 case 'a':
542 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
543 break;
544 case 'A':
545 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
546 break;
547 case 'g':
548 $h = substr( $ts, 8, 2 );
549 $num = $h % 12 ? $h % 12 : 12;
550 break;
551 case 'G':
552 $num = intval( substr( $ts, 8, 2 ) );
553 break;
554 case 'h':
555 $h = substr( $ts, 8, 2 );
556 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
557 break;
558 case 'H':
559 $num = substr( $ts, 8, 2 );
560 break;
561 case 'i':
562 $num = substr( $ts, 10, 2 );
563 break;
564 case 's':
565 $num = substr( $ts, 12, 2 );
566 break;
567 case 'c':
568 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
569 $s .= date( 'c', $unix );
570 break;
571 case 'r':
572 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
573 $s .= date( 'r', $unix );
574 break;
575 case 'U':
576 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
577 $num = $unix;
578 break;
579 case '\\':
580 # Backslash escaping
581 if ( $p < strlen( $format ) - 1 ) {
582 $s .= $format[++$p];
583 } else {
584 $s .= '\\';
585 }
586 break;
587 case '"':
588 # Quoted literal
589 if ( $p < strlen( $format ) - 1 ) {
590 $endQuote = strpos( $format, '"', $p + 1 );
591 if ( $endQuote === false ) {
592 # No terminating quote, assume literal "
593 $s .= '"';
594 } else {
595 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
596 $p = $endQuote;
597 }
598 } else {
599 # Quote at end of string, assume literal "
600 $s .= '"';
601 }
602 break;
603 default:
604 $s .= $format[$p];
605 }
606 if ( $num !== false ) {
607 if ( $rawToggle || $raw ) {
608 $s .= $num;
609 $raw = false;
610 } elseif ( $roman ) {
611 $s .= self::romanNumeral( $num );
612 $roman = false;
613 } else {
614 $s .= $this->formatNum( $num, true );
615 }
616 $num = false;
617 }
618 }
619 return $s;
620 }
621
622 /**
623 * Roman number formatting up to 3000
624 */
625 static function romanNumeral( $num ) {
626 static $table = array(
627 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
628 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
629 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
630 array( '', 'M', 'MM', 'MMM' )
631 );
632
633 $num = intval( $num );
634 if ( $num > 3000 || $num <= 0 ) {
635 return $num;
636 }
637
638 $s = '';
639 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
640 if ( $num >= $pow10 ) {
641 $s .= $table[$i][floor($num / $pow10)];
642 }
643 $num = $num % $pow10;
644 }
645 return $s;
646 }
647
648 /**
649 * This is meant to be used by time(), date(), and timeanddate() to get
650 * the date preference they're supposed to use, it should be used in
651 * all children.
652 *
653 *<code>
654 * function timeanddate([...], $format = true) {
655 * $datePreference = $this->dateFormat($format);
656 * [...]
657 * }
658 *</code>
659 *
660 * @param mixed $usePrefs: if true, the user's preference is used
661 * if false, the site/language default is used
662 * if int/string, assumed to be a format.
663 * @return string
664 */
665 function dateFormat( $usePrefs = true ) {
666 global $wgUser;
667
668 if( is_bool( $usePrefs ) ) {
669 if( $usePrefs ) {
670 $datePreference = $wgUser->getDatePreference();
671 } else {
672 $options = User::getDefaultOptions();
673 $datePreference = (string)$options['date'];
674 }
675 } else {
676 $datePreference = (string)$usePrefs;
677 }
678
679 // return int
680 if( $datePreference == '' ) {
681 return 'default';
682 }
683
684 return $datePreference;
685 }
686
687 /**
688 * @public
689 * @param mixed $ts the time format which needs to be turned into a
690 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
691 * @param bool $adj whether to adjust the time output according to the
692 * user configured offset ($timecorrection)
693 * @param mixed $format true to use user's date format preference
694 * @param string $timecorrection the time offset as returned by
695 * validateTimeZone() in Special:Preferences
696 * @return string
697 */
698 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
699 $this->load();
700 if ( $adj ) {
701 $ts = $this->userAdjust( $ts, $timecorrection );
702 }
703
704 $pref = $this->dateFormat( $format );
705 if( $pref == 'default' || !isset( $this->dateFormats["$pref date"] ) ) {
706 $pref = $this->defaultDateFormat;
707 }
708 return $this->sprintfDate( $this->dateFormats["$pref date"], $ts );
709 }
710
711 /**
712 * @public
713 * @param mixed $ts the time format which needs to be turned into a
714 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
715 * @param bool $adj whether to adjust the time output according to the
716 * user configured offset ($timecorrection)
717 * @param mixed $format true to use user's date format preference
718 * @param string $timecorrection the time offset as returned by
719 * validateTimeZone() in Special:Preferences
720 * @return string
721 */
722 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
723 $this->load();
724 if ( $adj ) {
725 $ts = $this->userAdjust( $ts, $timecorrection );
726 }
727
728 $pref = $this->dateFormat( $format );
729 if( $pref == 'default' || !isset( $this->dateFormats["$pref time"] ) ) {
730 $pref = $this->defaultDateFormat;
731 }
732 return $this->sprintfDate( $this->dateFormats["$pref time"], $ts );
733 }
734
735 /**
736 * @public
737 * @param mixed $ts the time format which needs to be turned into a
738 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
739 * @param bool $adj whether to adjust the time output according to the
740 * user configured offset ($timecorrection)
741
742 * @param mixed $format what format to return, if it's false output the
743 * default one (default true)
744 * @param string $timecorrection the time offset as returned by
745 * validateTimeZone() in Special:Preferences
746 * @return string
747 */
748 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false) {
749 $this->load();
750 if ( $adj ) {
751 $ts = $this->userAdjust( $ts, $timecorrection );
752 }
753
754 $pref = $this->dateFormat( $format );
755 if( $pref == 'default' || !isset( $this->dateFormats["$pref both"] ) ) {
756 $pref = $this->defaultDateFormat;
757 }
758
759 return $this->sprintfDate( $this->dateFormats["$pref both"], $ts );
760 }
761
762 function getMessage( $key ) {
763 $this->load();
764 return @$this->messages[$key];
765 }
766
767 function getAllMessages() {
768 $this->load();
769 return $this->messages;
770 }
771
772 function iconv( $in, $out, $string ) {
773 # For most languages, this is a wrapper for iconv
774 return iconv( $in, $out, $string );
775 }
776
777 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
778 function ucwordbreaksCallbackAscii($matches){
779 return $this->ucfirst($matches[1]);
780 }
781
782 function ucwordbreaksCallbackMB($matches){
783 return mb_strtoupper($matches[0]);
784 }
785
786 function ucCallback($matches){
787 list( $wikiUpperChars ) = self::getCaseMaps();
788 return strtr( $matches[1], $wikiUpperChars );
789 }
790
791 function lcCallback($matches){
792 list( , $wikiLowerChars ) = self::getCaseMaps();
793 return strtr( $matches[1], $wikiLowerChars );
794 }
795
796 function ucwordsCallbackMB($matches){
797 return mb_strtoupper($matches[0]);
798 }
799
800 function ucwordsCallbackWiki($matches){
801 list( $wikiUpperChars ) = self::getCaseMaps();
802 return strtr( $matches[0], $wikiUpperChars );
803 }
804
805 function ucfirst( $str ) {
806 return self::uc( $str, true );
807 }
808
809 function uc( $str, $first = false ) {
810 if ( function_exists( 'mb_strtoupper' ) )
811 if ( $first )
812 if ( self::isMultibyte( $str ) )
813 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
814 else
815 return ucfirst( $str );
816 else
817 return self::isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
818 else
819 if ( self::isMultibyte( $str ) ) {
820 list( $wikiUpperChars ) = $this->getCaseMaps();
821 $x = $first ? '^' : '';
822 return preg_replace_callback(
823 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
824 array($this,"ucCallback"),
825 $str
826 );
827 } else
828 return $first ? ucfirst( $str ) : strtoupper( $str );
829 }
830
831 function lcfirst( $str ) {
832 return self::lc( $str, true );
833 }
834
835 function lc( $str, $first = false ) {
836 if ( function_exists( 'mb_strtolower' ) )
837 if ( $first )
838 if ( self::isMultibyte( $str ) )
839 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
840 else
841 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
842 else
843 return self::isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
844 else
845 if ( self::isMultibyte( $str ) ) {
846 list( , $wikiLowerChars ) = self::getCaseMaps();
847 $x = $first ? '^' : '';
848 return preg_replace_callback(
849 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
850 array($this,"lcCallback"),
851 $str
852 );
853 } else
854 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
855 }
856
857 function isMultibyte( $str ) {
858 return (bool)preg_match( '/[\x80-\xff]/', $str );
859 }
860
861 function ucwords($str) {
862 if ( self::isMultibyte( $str ) ) {
863 $str = self::lc($str);
864
865 // regexp to find first letter in each word (i.e. after each space)
866 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
867
868 // function to use to capitalize a single char
869 if ( function_exists( 'mb_strtoupper' ) )
870 return preg_replace_callback(
871 $replaceRegexp,
872 array($this,"ucwordsCallbackMB"),
873 $str
874 );
875 else
876 return preg_replace_callback(
877 $replaceRegexp,
878 array($this,"ucwordsCallbackWiki"),
879 $str
880 );
881 }
882 else
883 return ucwords( strtolower( $str ) );
884 }
885
886 # capitalize words at word breaks
887 function ucwordbreaks($str){
888 if (self::isMultibyte( $str ) ) {
889 $str = self::lc($str);
890
891 // since \b doesn't work for UTF-8, we explicitely define word break chars
892 $breaks= "[ \-\(\)\}\{\.,\?!]";
893
894 // find first letter after word break
895 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
896
897 if ( function_exists( 'mb_strtoupper' ) )
898 return preg_replace_callback(
899 $replaceRegexp,
900 array($this,"ucwordbreaksCallbackMB"),
901 $str
902 );
903 else
904 return preg_replace_callback(
905 $replaceRegexp,
906 array($this,"ucwordsCallbackWiki"),
907 $str
908 );
909 }
910 else
911 return preg_replace_callback(
912 '/\b([\w\x80-\xff]+)\b/',
913 array($this,"ucwordbreaksCallbackAscii"),
914 $str );
915 }
916
917 /**
918 * Return a case-folded representation of $s
919 *
920 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
921 * and $s2 are the same except for the case of their characters. It is not
922 * necessary for the value returned to make sense when displayed.
923 *
924 * Do *not* perform any other normalisation in this function. If a caller
925 * uses this function when it should be using a more general normalisation
926 * function, then fix the caller.
927 */
928 function caseFold( $s ) {
929 return $this->uc( $s );
930 }
931
932 function checkTitleEncoding( $s ) {
933 if( is_array( $s ) ) {
934 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
935 }
936 # Check for non-UTF-8 URLs
937 $ishigh = preg_match( '/[\x80-\xff]/', $s);
938 if(!$ishigh) return $s;
939
940 $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
941 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
942 if( $isutf8 ) return $s;
943
944 return $this->iconv( $this->fallback8bitEncoding(), "utf-8", $s );
945 }
946
947 function fallback8bitEncoding() {
948 $this->load();
949 return $this->fallback8bitEncoding;
950 }
951
952 /**
953 * Some languages have special punctuation to strip out
954 * or characters which need to be converted for MySQL's
955 * indexing to grok it correctly. Make such changes here.
956 *
957 * @param string $in
958 * @return string
959 */
960 function stripForSearch( $string ) {
961 # MySQL fulltext index doesn't grok utf-8, so we
962 # need to fold cases and convert to hex
963
964 wfProfileIn( __METHOD__ );
965 if( function_exists( 'mb_strtolower' ) ) {
966 $out = preg_replace(
967 "/([\\xc0-\\xff][\\x80-\\xbf]*)/e",
968 "'U8' . bin2hex( \"$1\" )",
969 mb_strtolower( $string ) );
970 } else {
971 list( , $wikiLowerChars ) = self::getCaseMaps();
972 $out = preg_replace(
973 "/([\\xc0-\\xff][\\x80-\\xbf]*)/e",
974 "'U8' . bin2hex( strtr( \"\$1\", \$wikiLowerChars ) )",
975 $string );
976 }
977 wfProfileOut( __METHOD__ );
978 return $out;
979 }
980
981 function convertForSearchResult( $termsArray ) {
982 # some languages, e.g. Chinese, need to do a conversion
983 # in order for search results to be displayed correctly
984 return $termsArray;
985 }
986
987 /**
988 * Get the first character of a string.
989 *
990 * @param string $s
991 * @return string
992 */
993 function firstChar( $s ) {
994 preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
995 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/', $s, $matches);
996
997 return isset( $matches[1] ) ? $matches[1] : "";
998 }
999
1000 function initEncoding() {
1001 # Some languages may have an alternate char encoding option
1002 # (Esperanto X-coding, Japanese furigana conversion, etc)
1003 # If this language is used as the primary content language,
1004 # an override to the defaults can be set here on startup.
1005 }
1006
1007 function recodeForEdit( $s ) {
1008 # For some languages we'll want to explicitly specify
1009 # which characters make it into the edit box raw
1010 # or are converted in some way or another.
1011 # Note that if wgOutputEncoding is different from
1012 # wgInputEncoding, this text will be further converted
1013 # to wgOutputEncoding.
1014 global $wgEditEncoding;
1015 if( $wgEditEncoding == '' or
1016 $wgEditEncoding == 'UTF-8' ) {
1017 return $s;
1018 } else {
1019 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
1020 }
1021 }
1022
1023 function recodeInput( $s ) {
1024 # Take the previous into account.
1025 global $wgEditEncoding;
1026 if($wgEditEncoding != "") {
1027 $enc = $wgEditEncoding;
1028 } else {
1029 $enc = 'UTF-8';
1030 }
1031 if( $enc == 'UTF-8' ) {
1032 return $s;
1033 } else {
1034 return $this->iconv( $enc, 'UTF-8', $s );
1035 }
1036 }
1037
1038 /**
1039 * For right-to-left language support
1040 *
1041 * @return bool
1042 */
1043 function isRTL() {
1044 $this->load();
1045 return $this->rtl;
1046 }
1047
1048 /**
1049 * A hidden direction mark (LRM or RLM), depending on the language direction
1050 *
1051 * @return string
1052 */
1053 function getDirMark() {
1054 return $this->isRTL() ? "\xE2\x80\x8F" : "\xE2\x80\x8E";
1055 }
1056
1057 /**
1058 * An arrow, depending on the language direction
1059 *
1060 * @return string
1061 */
1062 function getArrow() {
1063 return $this->isRTL() ? '←' : '→';
1064 }
1065
1066 /**
1067 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
1068 *
1069 * @return bool
1070 */
1071 function linkPrefixExtension() {
1072 $this->load();
1073 return $this->linkPrefixExtension;
1074 }
1075
1076 function &getMagicWords() {
1077 $this->load();
1078 return $this->magicWords;
1079 }
1080
1081 # Fill a MagicWord object with data from here
1082 function getMagic( &$mw ) {
1083 if ( !isset( $this->mMagicExtensions ) ) {
1084 $this->mMagicExtensions = array();
1085 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
1086 }
1087 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
1088 $rawEntry = $this->mMagicExtensions[$mw->mId];
1089 } else {
1090 $magicWords =& $this->getMagicWords();
1091 if ( isset( $magicWords[$mw->mId] ) ) {
1092 $rawEntry = $magicWords[$mw->mId];
1093 } else {
1094 # Fall back to English if local list is incomplete
1095 $magicWords =& Language::getMagicWords();
1096 $rawEntry = $magicWords[$mw->mId];
1097 }
1098 }
1099
1100 if( !is_array( $rawEntry ) ) {
1101 error_log( "\"$rawEntry\" is not a valid magic thingie for \"$mw->mId\"" );
1102 }
1103 $mw->mCaseSensitive = $rawEntry[0];
1104 $mw->mSynonyms = array_slice( $rawEntry, 1 );
1105 }
1106
1107 /**
1108 * Get special page names, as an associative array
1109 * case folded alias => real name
1110 */
1111 function getSpecialPageAliases() {
1112 $this->load();
1113 if ( !isset( $this->mExtendedSpecialPageAliases ) ) {
1114 $this->mExtendedSpecialPageAliases = $this->specialPageAliases;
1115 wfRunHooks( 'LangugeGetSpecialPageAliases',
1116 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
1117 }
1118 return $this->mExtendedSpecialPageAliases;
1119 }
1120
1121 /**
1122 * Italic is unsuitable for some languages
1123 *
1124 * @public
1125 *
1126 * @param string $text The text to be emphasized.
1127 * @return string
1128 */
1129 function emphasize( $text ) {
1130 return "<em>$text</em>";
1131 }
1132
1133 /**
1134 * Normally we output all numbers in plain en_US style, that is
1135 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
1136 * point twohundredthirtyfive. However this is not sutable for all
1137 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
1138 * Icelandic just want to use commas instead of dots, and dots instead
1139 * of commas like "293.291,235".
1140 *
1141 * An example of this function being called:
1142 * <code>
1143 * wfMsg( 'message', $wgLang->formatNum( $num ) )
1144 * </code>
1145 *
1146 * See LanguageGu.php for the Gujarati implementation and
1147 * LanguageIs.php for the , => . and . => , implementation.
1148 *
1149 * @todo check if it's viable to use localeconv() for the decimal
1150 * seperator thing.
1151 * @public
1152 * @param mixed $number the string to be formatted, should be an integer or
1153 * a floating point number.
1154 * @param bool $nocommafy Set to true for special numbers like dates
1155 * @return string
1156 */
1157 function formatNum( $number, $nocommafy = false ) {
1158 global $wgTranslateNumerals;
1159 if (!$nocommafy) {
1160 $number = $this->commafy($number);
1161 $s = $this->separatorTransformTable();
1162 if (!is_null($s)) { $number = strtr($number, $s); }
1163 }
1164
1165 if ($wgTranslateNumerals) {
1166 $s = $this->digitTransformTable();
1167 if (!is_null($s)) { $number = strtr($number, $s); }
1168 }
1169
1170 return $number;
1171 }
1172
1173 /**
1174 * Adds commas to a given number
1175 *
1176 * @param mixed $_
1177 * @return string
1178 */
1179 function commafy($_) {
1180 return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_)));
1181 }
1182
1183 function digitTransformTable() {
1184 $this->load();
1185 return $this->digitTransformTable;
1186 }
1187
1188 function separatorTransformTable() {
1189 $this->load();
1190 return $this->separatorTransformTable;
1191 }
1192
1193
1194 /**
1195 * For the credit list in includes/Credits.php (action=credits)
1196 *
1197 * @param array $l
1198 * @return string
1199 */
1200 function listToText( $l ) {
1201 $s = '';
1202 $m = count($l) - 1;
1203 for ($i = $m; $i >= 0; $i--) {
1204 if ($i == $m) {
1205 $s = $l[$i];
1206 } else if ($i == $m - 1) {
1207 $s = $l[$i] . ' ' . $this->getMessageFromDB( 'and' ) . ' ' . $s;
1208 } else {
1209 $s = $l[$i] . ', ' . $s;
1210 }
1211 }
1212 return $s;
1213 }
1214
1215 # Crop a string from the beginning or end to a certain number of bytes.
1216 # (Bytes are used because our storage has limited byte lengths for some
1217 # columns in the database.) Multibyte charsets will need to make sure that
1218 # only whole characters are included!
1219 #
1220 # $length does not include the optional ellipsis.
1221 # If $length is negative, snip from the beginning
1222 function truncate( $string, $length, $ellipsis = "" ) {
1223 if( $length == 0 ) {
1224 return $ellipsis;
1225 }
1226 if ( strlen( $string ) <= abs( $length ) ) {
1227 return $string;
1228 }
1229 if( $length > 0 ) {
1230 $string = substr( $string, 0, $length );
1231 $char = ord( $string[strlen( $string ) - 1] );
1232 if ($char >= 0xc0) {
1233 # We got the first byte only of a multibyte char; remove it.
1234 $string = substr( $string, 0, -1 );
1235 } elseif( $char >= 0x80 &&
1236 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
1237 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) ) {
1238 # We chopped in the middle of a character; remove it
1239 $string = $m[1];
1240 }
1241 return $string . $ellipsis;
1242 } else {
1243 $string = substr( $string, $length );
1244 $char = ord( $string[0] );
1245 if( $char >= 0x80 && $char < 0xc0 ) {
1246 # We chopped in the middle of a character; remove the whole thing
1247 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
1248 }
1249 return $ellipsis . $string;
1250 }
1251 }
1252
1253 /**
1254 * Grammatical transformations, needed for inflected languages
1255 * Invoked by putting {{grammar:case|word}} in a message
1256 *
1257 * @param string $word
1258 * @param string $case
1259 * @return string
1260 */
1261 function convertGrammar( $word, $case ) {
1262 global $wgGrammarForms;
1263 if ( isset($wgGrammarForms['en'][$case][$word]) ) {
1264 return $wgGrammarForms['en'][$case][$word];
1265 }
1266 return $word;
1267 }
1268
1269 /**
1270 * Plural form transformations, needed for some languages.
1271 * For example, where are 3 form of plural in Russian and Polish,
1272 * depending on "count mod 10". See [[w:Plural]]
1273 * For English it is pretty simple.
1274 *
1275 * Invoked by putting {{plural:count|wordform1|wordform2}}
1276 * or {{plural:count|wordform1|wordform2|wordform3}}
1277 *
1278 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
1279 *
1280 * @param integer $count
1281 * @param string $wordform1
1282 * @param string $wordform2
1283 * @param string $wordform3 (optional)
1284 * @return string
1285 */
1286 function convertPlural( $count, $w1, $w2, $w3) {
1287 return $count == '1' ? $w1 : $w2;
1288 }
1289
1290 /**
1291 * For translaing of expiry times
1292 * @param string The validated block time in English
1293 * @return Somehow translated block time
1294 * @see LanguageFi.php for example implementation
1295 */
1296 function translateBlockExpiry( $str ) {
1297
1298 $scBlockExpiryOptions = $this->getMessageFromDB( 'ipboptions' );
1299
1300 if ( $scBlockExpiryOptions == '-') {
1301 return $str;
1302 }
1303
1304 foreach (explode(',', $scBlockExpiryOptions) as $option) {
1305 if ( strpos($option, ":") === false )
1306 continue;
1307 list($show, $value) = explode(":", $option);
1308 if ( strcmp ( $str, $value) == 0 )
1309 return '<span title="' . htmlspecialchars($str). '">' .
1310 htmlspecialchars( trim( $show ) ) . '</span>';
1311 }
1312
1313 return $str;
1314 }
1315
1316 /**
1317 * languages like Chinese need to be segmented in order for the diff
1318 * to be of any use
1319 *
1320 * @param string $text
1321 * @return string
1322 */
1323 function segmentForDiff( $text ) {
1324 return $text;
1325 }
1326
1327 /**
1328 * and unsegment to show the result
1329 *
1330 * @param string $text
1331 * @return string
1332 */
1333 function unsegmentForDiff( $text ) {
1334 return $text;
1335 }
1336
1337 # convert text to different variants of a language.
1338 function convert( $text, $isTitle = false) {
1339 return $this->mConverter->convert($text, $isTitle);
1340 }
1341
1342 # Convert text from within Parser
1343 function parserConvert( $text, &$parser ) {
1344 return $this->mConverter->parserConvert( $text, $parser );
1345 }
1346
1347 # Tell the converter that it shouldn't convert titles
1348 function setNoTitleConvert(){
1349 $this->mConverter->setNotitleConvert();
1350 }
1351
1352 # Check if this is a language with variants
1353 function hasVariants(){
1354 return sizeof($this->getVariants())>1;
1355 }
1356
1357
1358 /**
1359 * Perform output conversion on a string, and encode for safe HTML output.
1360 * @param string $text
1361 * @param bool $isTitle -- wtf?
1362 * @return string
1363 * @todo this should get integrated somewhere sane
1364 */
1365 function convertHtml( $text, $isTitle = false ) {
1366 return htmlspecialchars( $this->convert( $text, $isTitle ) );
1367 }
1368
1369 function convertCategoryKey( $key ) {
1370 return $this->mConverter->convertCategoryKey( $key );
1371 }
1372
1373 /**
1374 * get the list of variants supported by this langauge
1375 * see sample implementation in LanguageZh.php
1376 *
1377 * @return array an array of language codes
1378 */
1379 function getVariants() {
1380 return $this->mConverter->getVariants();
1381 }
1382
1383
1384 function getPreferredVariant( $fromUser = true ) {
1385 return $this->mConverter->getPreferredVariant( $fromUser );
1386 }
1387
1388 /**
1389 * if a language supports multiple variants, it is
1390 * possible that non-existing link in one variant
1391 * actually exists in another variant. this function
1392 * tries to find it. See e.g. LanguageZh.php
1393 *
1394 * @param string $link the name of the link
1395 * @param mixed $nt the title object of the link
1396 * @return null the input parameters may be modified upon return
1397 */
1398 function findVariantLink( &$link, &$nt ) {
1399 $this->mConverter->findVariantLink($link, $nt);
1400 }
1401
1402 /**
1403 * If a language supports multiple variants, converts text
1404 * into an array of all possible variants of the text:
1405 * 'variant' => text in that variant
1406 */
1407
1408 function convertLinkToAllVariants($text){
1409 return $this->mConverter->convertLinkToAllVariants($text);
1410 }
1411
1412
1413 /**
1414 * returns language specific options used by User::getPageRenderHash()
1415 * for example, the preferred language variant
1416 *
1417 * @return string
1418 * @public
1419 */
1420 function getExtraHashOptions() {
1421 return $this->mConverter->getExtraHashOptions();
1422 }
1423
1424 /**
1425 * for languages that support multiple variants, the title of an
1426 * article may be displayed differently in different variants. this
1427 * function returns the apporiate title defined in the body of the article.
1428 *
1429 * @return string
1430 */
1431 function getParsedTitle() {
1432 return $this->mConverter->getParsedTitle();
1433 }
1434
1435 /**
1436 * Enclose a string with the "no conversion" tag. This is used by
1437 * various functions in the Parser
1438 *
1439 * @param string $text text to be tagged for no conversion
1440 * @return string the tagged text
1441 */
1442 function markNoConversion( $text, $noParse=false ) {
1443 return $this->mConverter->markNoConversion( $text, $noParse );
1444 }
1445
1446 /**
1447 * A regular expression to match legal word-trailing characters
1448 * which should be merged onto a link of the form [[foo]]bar.
1449 *
1450 * @return string
1451 * @public
1452 */
1453 function linkTrail() {
1454 $this->load();
1455 return $this->linkTrail;
1456 }
1457
1458 function getLangObj() {
1459 return $this;
1460 }
1461
1462 /**
1463 * Get the RFC 3066 code for this language object
1464 */
1465 function getCode() {
1466 return $this->mCode;
1467 }
1468
1469 function setCode( $code ) {
1470 $this->mCode = $code;
1471 }
1472
1473 static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
1474 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
1475 }
1476
1477 static function getMessagesFileName( $code ) {
1478 global $IP;
1479 return self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
1480 }
1481
1482 static function getClassFileName( $code ) {
1483 global $IP;
1484 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
1485 }
1486
1487 static function getLocalisationArray( $code, $disableCache = false ) {
1488 self::loadLocalisation( $code, $disableCache );
1489 return self::$mLocalisationCache[$code];
1490 }
1491
1492 /**
1493 * Load localisation data for a given code into the static cache
1494 *
1495 * @return array Dependencies, map of filenames to mtimes
1496 */
1497 static function loadLocalisation( $code, $disableCache = false ) {
1498 static $recursionGuard = array();
1499 global $wgMemc;
1500
1501 if ( !$code ) {
1502 throw new MWException( "Invalid language code requested" );
1503 }
1504
1505 if ( !$disableCache ) {
1506 # Try the per-process cache
1507 if ( isset( self::$mLocalisationCache[$code] ) ) {
1508 return self::$mLocalisationCache[$code]['deps'];
1509 }
1510
1511 wfProfileIn( __METHOD__ );
1512
1513 # Try the serialized directory
1514 $cache = wfGetPrecompiledData( self::getFileName( "Messages", $code, '.ser' ) );
1515 if ( $cache ) {
1516 self::$mLocalisationCache[$code] = $cache;
1517 wfDebug( "Got localisation for $code from precompiled data file\n" );
1518 wfProfileOut( __METHOD__ );
1519 return self::$mLocalisationCache[$code]['deps'];
1520 }
1521
1522 # Try the global cache
1523 $memcKey = wfMemcKey('localisation', $code );
1524 $cache = $wgMemc->get( $memcKey );
1525 if ( $cache ) {
1526 $expired = false;
1527 # Check file modification times
1528 foreach ( $cache['deps'] as $file => $mtime ) {
1529 if ( !file_exists( $file ) || filemtime( $file ) > $mtime ) {
1530 $expired = true;
1531 break;
1532 }
1533 }
1534 if ( self::isLocalisationOutOfDate( $cache ) ) {
1535 $wgMemc->delete( $memcKey );
1536 $cache = false;
1537 wfDebug( "Localisation cache for $code had expired due to update of $file\n" );
1538 } else {
1539 self::$mLocalisationCache[$code] = $cache;
1540 wfDebug( "Got localisation for $code from cache\n" );
1541 wfProfileOut( __METHOD__ );
1542 return $cache['deps'];
1543 }
1544 }
1545 } else {
1546 wfProfileIn( __METHOD__ );
1547 }
1548
1549 if ( $code != 'en' ) {
1550 $fallback = 'en';
1551 } else {
1552 $fallback = false;
1553 }
1554
1555 # Load the primary localisation from the source file
1556 $filename = self::getMessagesFileName( $code );
1557 if ( !file_exists( $filename ) ) {
1558 wfDebug( "No localisation file for $code, using implicit fallback to en\n" );
1559 $cache = array();
1560 $deps = array();
1561 } else {
1562 $deps = array( $filename => filemtime( $filename ) );
1563 require( $filename );
1564 $cache = compact( self::$mLocalisationKeys );
1565 wfDebug( "Got localisation for $code from source\n" );
1566 }
1567
1568 if ( !empty( $fallback ) ) {
1569 # Load the fallback localisation, with a circular reference guard
1570 if ( isset( $recursionGuard[$code] ) ) {
1571 throw new MWException( "Error: Circular fallback reference in language code $code" );
1572 }
1573 $recursionGuard[$code] = true;
1574 $newDeps = self::loadLocalisation( $fallback, $disableCache );
1575 unset( $recursionGuard[$code] );
1576
1577 $secondary = self::$mLocalisationCache[$fallback];
1578 $deps = array_merge( $deps, $newDeps );
1579
1580 # Merge the fallback localisation with the current localisation
1581 foreach ( self::$mLocalisationKeys as $key ) {
1582 if ( isset( $cache[$key] ) ) {
1583 if ( isset( $secondary[$key] ) ) {
1584 if ( in_array( $key, self::$mMergeableMapKeys ) ) {
1585 $cache[$key] = $cache[$key] + $secondary[$key];
1586 } elseif ( in_array( $key, self::$mMergeableListKeys ) ) {
1587 $cache[$key] = array_merge( $secondary[$key], $cache[$key] );
1588 } elseif ( in_array( $key, self::$mMergeableAliasListKeys ) ) {
1589 $cache[$key] = array_merge_recursive( $cache[$key], $secondary[$key] );
1590 }
1591 }
1592 } else {
1593 $cache[$key] = $secondary[$key];
1594 }
1595 }
1596
1597 # Merge bookstore lists if requested
1598 if ( !empty( $cache['bookstoreList']['inherit'] ) ) {
1599 $cache['bookstoreList'] = array_merge( $cache['bookstoreList'], $secondary['bookstoreList'] );
1600 }
1601 if ( isset( $cache['bookstoreList']['inherit'] ) ) {
1602 unset( $cache['bookstoreList']['inherit'] );
1603 }
1604 }
1605
1606 # Add dependencies to the cache entry
1607 $cache['deps'] = $deps;
1608
1609 # Replace spaces with underscores in namespace names
1610 $cache['namespaceNames'] = str_replace( ' ', '_', $cache['namespaceNames'] );
1611
1612 # Save to both caches
1613 self::$mLocalisationCache[$code] = $cache;
1614 if ( !$disableCache ) {
1615 $wgMemc->set( $memcKey, $cache );
1616 }
1617
1618 wfProfileOut( __METHOD__ );
1619 return $deps;
1620 }
1621
1622 /**
1623 * Test if a given localisation cache is out of date with respect to the
1624 * source Messages files. This is done automatically for the global cache
1625 * in $wgMemc, but is only done on certain occasions for the serialized
1626 * data file.
1627 *
1628 * @param $cache mixed Either a language code or a cache array
1629 */
1630 static function isLocalisationOutOfDate( $cache ) {
1631 if ( !is_array( $cache ) ) {
1632 self::loadLocalisation( $cache );
1633 $cache = self::$mLocalisationCache[$cache];
1634 }
1635 $expired = false;
1636 foreach ( $cache['deps'] as $file => $mtime ) {
1637 if ( !file_exists( $file ) || filemtime( $file ) > $mtime ) {
1638 $expired = true;
1639 break;
1640 }
1641 }
1642 return $expired;
1643 }
1644
1645 /**
1646 * Get the fallback for a given language
1647 */
1648 static function getFallbackFor( $code ) {
1649 self::loadLocalisation( $code );
1650 return self::$mLocalisationCache[$code]['fallback'];
1651 }
1652
1653 /**
1654 * Get all messages for a given language
1655 */
1656 static function getMessagesFor( $code ) {
1657 self::loadLocalisation( $code );
1658 return self::$mLocalisationCache[$code]['messages'];
1659 }
1660
1661 /**
1662 * Get a message for a given language
1663 */
1664 static function getMessageFor( $key, $code ) {
1665 self::loadLocalisation( $code );
1666 return @self::$mLocalisationCache[$code]['messages'][$key];
1667 }
1668
1669 /**
1670 * Load localisation data for this object
1671 */
1672 function load() {
1673 if ( !$this->mLoaded ) {
1674 self::loadLocalisation( $this->getCode() );
1675 $cache =& self::$mLocalisationCache[$this->getCode()];
1676 foreach ( self::$mLocalisationKeys as $key ) {
1677 $this->$key = $cache[$key];
1678 }
1679 $this->mLoaded = true;
1680
1681 $this->fixUpSettings();
1682 }
1683 }
1684
1685 /**
1686 * Do any necessary post-cache-load settings adjustment
1687 */
1688 function fixUpSettings() {
1689 global $wgExtraNamespaces, $wgMetaNamespace, $wgMetaNamespaceTalk, $wgMessageCache,
1690 $wgNamespaceAliases, $wgAmericanDates;
1691 wfProfileIn( __METHOD__ );
1692 if ( $wgExtraNamespaces ) {
1693 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames;
1694 }
1695
1696 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
1697 if ( $wgMetaNamespaceTalk ) {
1698 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
1699 } else {
1700 $talk = $this->namespaceNames[NS_PROJECT_TALK];
1701 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
1702
1703 # Allow grammar transformations
1704 # Allowing full message-style parsing would make simple requests
1705 # such as action=raw much more expensive than they need to be.
1706 # This will hopefully cover most cases.
1707 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
1708 array( &$this, 'replaceGrammarInNamespace' ), $talk );
1709 $talk = str_replace( ' ', '_', $talk );
1710 $this->namespaceNames[NS_PROJECT_TALK] = $talk;
1711 }
1712
1713 # The above mixing may leave namespaces out of canonical order.
1714 # Re-order by namespace ID number...
1715 ksort( $this->namespaceNames );
1716
1717 # Put namespace names and aliases into a hashtable.
1718 # If this is too slow, then we should arrange it so that it is done
1719 # before caching. The catch is that at pre-cache time, the above
1720 # class-specific fixup hasn't been done.
1721 $this->mNamespaceIds = array();
1722 foreach ( $this->namespaceNames as $index => $name ) {
1723 $this->mNamespaceIds[$this->lc($name)] = $index;
1724 }
1725 if ( $this->namespaceAliases ) {
1726 foreach ( $this->namespaceAliases as $name => $index ) {
1727 $this->mNamespaceIds[$this->lc($name)] = $index;
1728 }
1729 }
1730 if ( $wgNamespaceAliases ) {
1731 foreach ( $wgNamespaceAliases as $name => $index ) {
1732 $this->mNamespaceIds[$this->lc($name)] = $index;
1733 }
1734 }
1735
1736 if ( $this->defaultDateFormat == 'dmy or mdy' ) {
1737 $this->defaultDateFormat = $wgAmericanDates ? 'mdy' : 'dmy';
1738 }
1739 wfProfileOut( __METHOD__ );
1740 }
1741
1742 function replaceGrammarInNamespace( $m ) {
1743 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
1744 }
1745
1746 static function getCaseMaps() {
1747 static $wikiUpperChars, $wikiLowerChars;
1748 if ( isset( $wikiUpperChars ) ) {
1749 return array( $wikiUpperChars, $wikiLowerChars );
1750 }
1751
1752 wfProfileIn( __METHOD__ );
1753 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
1754 if ( $arr === false ) {
1755 throw new MWException(
1756 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
1757 }
1758 extract( $arr );
1759 wfProfileOut( __METHOD__ );
1760 return array( $wikiUpperChars, $wikiLowerChars );
1761 }
1762 }
1763
1764 ?>