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