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