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