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