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