Message order compatibility.
[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 # Default fallback, may be overridden when the messages file is included
1550 if ( $code != 'en' ) {
1551 $fallback = 'en';
1552 } else {
1553 $fallback = false;
1554 }
1555
1556 # Load the primary localisation from the source file
1557 $filename = self::getMessagesFileName( $code );
1558 if ( !file_exists( $filename ) ) {
1559 wfDebug( "No localisation file for $code, using implicit fallback to en\n" );
1560 $cache = array();
1561 $deps = array();
1562 } else {
1563 $deps = array( $filename => filemtime( $filename ) );
1564 require( $filename );
1565 $cache = compact( self::$mLocalisationKeys );
1566 wfDebug( "Got localisation for $code from source\n" );
1567 }
1568
1569 if ( !empty( $fallback ) ) {
1570 # Load the fallback localisation, with a circular reference guard
1571 if ( isset( $recursionGuard[$code] ) ) {
1572 throw new MWException( "Error: Circular fallback reference in language code $code" );
1573 }
1574 $recursionGuard[$code] = true;
1575 $newDeps = self::loadLocalisation( $fallback, $disableCache );
1576 unset( $recursionGuard[$code] );
1577
1578 $secondary = self::$mLocalisationCache[$fallback];
1579 $deps = array_merge( $deps, $newDeps );
1580
1581 # Merge the fallback localisation with the current localisation
1582 foreach ( self::$mLocalisationKeys as $key ) {
1583 if ( isset( $cache[$key] ) ) {
1584 if ( isset( $secondary[$key] ) ) {
1585 if ( in_array( $key, self::$mMergeableMapKeys ) ) {
1586 $cache[$key] = $cache[$key] + $secondary[$key];
1587 } elseif ( in_array( $key, self::$mMergeableListKeys ) ) {
1588 $cache[$key] = array_merge( $secondary[$key], $cache[$key] );
1589 } elseif ( in_array( $key, self::$mMergeableAliasListKeys ) ) {
1590 $cache[$key] = array_merge_recursive( $cache[$key], $secondary[$key] );
1591 }
1592 }
1593 } else {
1594 $cache[$key] = $secondary[$key];
1595 }
1596 }
1597
1598 # Merge bookstore lists if requested
1599 if ( !empty( $cache['bookstoreList']['inherit'] ) ) {
1600 $cache['bookstoreList'] = array_merge( $cache['bookstoreList'], $secondary['bookstoreList'] );
1601 }
1602 if ( isset( $cache['bookstoreList']['inherit'] ) ) {
1603 unset( $cache['bookstoreList']['inherit'] );
1604 }
1605 }
1606
1607 # Add dependencies to the cache entry
1608 $cache['deps'] = $deps;
1609
1610 # Replace spaces with underscores in namespace names
1611 $cache['namespaceNames'] = str_replace( ' ', '_', $cache['namespaceNames'] );
1612
1613 # Save to both caches
1614 self::$mLocalisationCache[$code] = $cache;
1615 if ( !$disableCache ) {
1616 $wgMemc->set( $memcKey, $cache );
1617 }
1618
1619 wfProfileOut( __METHOD__ );
1620 return $deps;
1621 }
1622
1623 /**
1624 * Test if a given localisation cache is out of date with respect to the
1625 * source Messages files. This is done automatically for the global cache
1626 * in $wgMemc, but is only done on certain occasions for the serialized
1627 * data file.
1628 *
1629 * @param $cache mixed Either a language code or a cache array
1630 */
1631 static function isLocalisationOutOfDate( $cache ) {
1632 if ( !is_array( $cache ) ) {
1633 self::loadLocalisation( $cache );
1634 $cache = self::$mLocalisationCache[$cache];
1635 }
1636 $expired = false;
1637 foreach ( $cache['deps'] as $file => $mtime ) {
1638 if ( !file_exists( $file ) || filemtime( $file ) > $mtime ) {
1639 $expired = true;
1640 break;
1641 }
1642 }
1643 return $expired;
1644 }
1645
1646 /**
1647 * Get the fallback for a given language
1648 */
1649 static function getFallbackFor( $code ) {
1650 self::loadLocalisation( $code );
1651 return self::$mLocalisationCache[$code]['fallback'];
1652 }
1653
1654 /**
1655 * Get all messages for a given language
1656 */
1657 static function getMessagesFor( $code ) {
1658 self::loadLocalisation( $code );
1659 return self::$mLocalisationCache[$code]['messages'];
1660 }
1661
1662 /**
1663 * Get a message for a given language
1664 */
1665 static function getMessageFor( $key, $code ) {
1666 self::loadLocalisation( $code );
1667 return @self::$mLocalisationCache[$code]['messages'][$key];
1668 }
1669
1670 /**
1671 * Load localisation data for this object
1672 */
1673 function load() {
1674 if ( !$this->mLoaded ) {
1675 self::loadLocalisation( $this->getCode() );
1676 $cache =& self::$mLocalisationCache[$this->getCode()];
1677 foreach ( self::$mLocalisationKeys as $key ) {
1678 $this->$key = $cache[$key];
1679 }
1680 $this->mLoaded = true;
1681
1682 $this->fixUpSettings();
1683 }
1684 }
1685
1686 /**
1687 * Do any necessary post-cache-load settings adjustment
1688 */
1689 function fixUpSettings() {
1690 global $wgExtraNamespaces, $wgMetaNamespace, $wgMetaNamespaceTalk, $wgMessageCache,
1691 $wgNamespaceAliases, $wgAmericanDates;
1692 wfProfileIn( __METHOD__ );
1693 if ( $wgExtraNamespaces ) {
1694 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames;
1695 }
1696
1697 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
1698 if ( $wgMetaNamespaceTalk ) {
1699 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
1700 } else {
1701 $talk = $this->namespaceNames[NS_PROJECT_TALK];
1702 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
1703
1704 # Allow grammar transformations
1705 # Allowing full message-style parsing would make simple requests
1706 # such as action=raw much more expensive than they need to be.
1707 # This will hopefully cover most cases.
1708 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
1709 array( &$this, 'replaceGrammarInNamespace' ), $talk );
1710 $talk = str_replace( ' ', '_', $talk );
1711 $this->namespaceNames[NS_PROJECT_TALK] = $talk;
1712 }
1713
1714 # The above mixing may leave namespaces out of canonical order.
1715 # Re-order by namespace ID number...
1716 ksort( $this->namespaceNames );
1717
1718 # Put namespace names and aliases into a hashtable.
1719 # If this is too slow, then we should arrange it so that it is done
1720 # before caching. The catch is that at pre-cache time, the above
1721 # class-specific fixup hasn't been done.
1722 $this->mNamespaceIds = array();
1723 foreach ( $this->namespaceNames as $index => $name ) {
1724 $this->mNamespaceIds[$this->lc($name)] = $index;
1725 }
1726 if ( $this->namespaceAliases ) {
1727 foreach ( $this->namespaceAliases as $name => $index ) {
1728 $this->mNamespaceIds[$this->lc($name)] = $index;
1729 }
1730 }
1731 if ( $wgNamespaceAliases ) {
1732 foreach ( $wgNamespaceAliases as $name => $index ) {
1733 $this->mNamespaceIds[$this->lc($name)] = $index;
1734 }
1735 }
1736
1737 if ( $this->defaultDateFormat == 'dmy or mdy' ) {
1738 $this->defaultDateFormat = $wgAmericanDates ? 'mdy' : 'dmy';
1739 }
1740 wfProfileOut( __METHOD__ );
1741 }
1742
1743 function replaceGrammarInNamespace( $m ) {
1744 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
1745 }
1746
1747 static function getCaseMaps() {
1748 static $wikiUpperChars, $wikiLowerChars;
1749 if ( isset( $wikiUpperChars ) ) {
1750 return array( $wikiUpperChars, $wikiLowerChars );
1751 }
1752
1753 wfProfileIn( __METHOD__ );
1754 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
1755 if ( $arr === false ) {
1756 throw new MWException(
1757 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
1758 }
1759 extract( $arr );
1760 wfProfileOut( __METHOD__ );
1761 return array( $wikiUpperChars, $wikiLowerChars );
1762 }
1763 }
1764
1765 ?>