Adds support for Iranian calendar
[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 return self::uc( $str, true );
935 }
936
937 function uc( $str, $first = false ) {
938 if ( function_exists( 'mb_strtoupper' ) ) {
939 if ( $first ) {
940 if ( self::isMultibyte( $str ) ) {
941 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
942 } else {
943 return ucfirst( $str );
944 }
945 } else {
946 return self::isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
947 }
948 } else {
949 if ( self::isMultibyte( $str ) ) {
950 list( $wikiUpperChars ) = $this->getCaseMaps();
951 $x = $first ? '^' : '';
952 return preg_replace_callback(
953 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
954 array($this,"ucCallback"),
955 $str
956 );
957 } else {
958 return $first ? ucfirst( $str ) : strtoupper( $str );
959 }
960 }
961 }
962
963 function lcfirst( $str ) {
964 return self::lc( $str, true );
965 }
966
967 function lc( $str, $first = false ) {
968 if ( function_exists( 'mb_strtolower' ) )
969 if ( $first )
970 if ( self::isMultibyte( $str ) )
971 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
972 else
973 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
974 else
975 return self::isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
976 else
977 if ( self::isMultibyte( $str ) ) {
978 list( , $wikiLowerChars ) = self::getCaseMaps();
979 $x = $first ? '^' : '';
980 return preg_replace_callback(
981 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
982 array($this,"lcCallback"),
983 $str
984 );
985 } else
986 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
987 }
988
989 function isMultibyte( $str ) {
990 return (bool)preg_match( '/[\x80-\xff]/', $str );
991 }
992
993 function ucwords($str) {
994 if ( self::isMultibyte( $str ) ) {
995 $str = self::lc($str);
996
997 // regexp to find first letter in each word (i.e. after each space)
998 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
999
1000 // function to use to capitalize a single char
1001 if ( function_exists( 'mb_strtoupper' ) )
1002 return preg_replace_callback(
1003 $replaceRegexp,
1004 array($this,"ucwordsCallbackMB"),
1005 $str
1006 );
1007 else
1008 return preg_replace_callback(
1009 $replaceRegexp,
1010 array($this,"ucwordsCallbackWiki"),
1011 $str
1012 );
1013 }
1014 else
1015 return ucwords( strtolower( $str ) );
1016 }
1017
1018 # capitalize words at word breaks
1019 function ucwordbreaks($str){
1020 if (self::isMultibyte( $str ) ) {
1021 $str = self::lc($str);
1022
1023 // since \b doesn't work for UTF-8, we explicitely define word break chars
1024 $breaks= "[ \-\(\)\}\{\.,\?!]";
1025
1026 // find first letter after word break
1027 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
1028
1029 if ( function_exists( 'mb_strtoupper' ) )
1030 return preg_replace_callback(
1031 $replaceRegexp,
1032 array($this,"ucwordbreaksCallbackMB"),
1033 $str
1034 );
1035 else
1036 return preg_replace_callback(
1037 $replaceRegexp,
1038 array($this,"ucwordsCallbackWiki"),
1039 $str
1040 );
1041 }
1042 else
1043 return preg_replace_callback(
1044 '/\b([\w\x80-\xff]+)\b/',
1045 array($this,"ucwordbreaksCallbackAscii"),
1046 $str );
1047 }
1048
1049 /**
1050 * Return a case-folded representation of $s
1051 *
1052 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
1053 * and $s2 are the same except for the case of their characters. It is not
1054 * necessary for the value returned to make sense when displayed.
1055 *
1056 * Do *not* perform any other normalisation in this function. If a caller
1057 * uses this function when it should be using a more general normalisation
1058 * function, then fix the caller.
1059 */
1060 function caseFold( $s ) {
1061 return $this->uc( $s );
1062 }
1063
1064 function checkTitleEncoding( $s ) {
1065 if( is_array( $s ) ) {
1066 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
1067 }
1068 # Check for non-UTF-8 URLs
1069 $ishigh = preg_match( '/[\x80-\xff]/', $s);
1070 if(!$ishigh) return $s;
1071
1072 $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1073 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
1074 if( $isutf8 ) return $s;
1075
1076 return $this->iconv( $this->fallback8bitEncoding(), "utf-8", $s );
1077 }
1078
1079 function fallback8bitEncoding() {
1080 $this->load();
1081 return $this->fallback8bitEncoding;
1082 }
1083
1084 /**
1085 * Some languages have special punctuation to strip out
1086 * or characters which need to be converted for MySQL's
1087 * indexing to grok it correctly. Make such changes here.
1088 *
1089 * @param string $in
1090 * @return string
1091 */
1092 function stripForSearch( $string ) {
1093 global $wgDBtype;
1094 if ( $wgDBtype != 'mysql' ) {
1095 return $string;
1096 }
1097
1098 # MySQL fulltext index doesn't grok utf-8, so we
1099 # need to fold cases and convert to hex
1100
1101 wfProfileIn( __METHOD__ );
1102 if( function_exists( 'mb_strtolower' ) ) {
1103 $out = preg_replace(
1104 "/([\\xc0-\\xff][\\x80-\\xbf]*)/e",
1105 "'U8' . bin2hex( \"$1\" )",
1106 mb_strtolower( $string ) );
1107 } else {
1108 list( , $wikiLowerChars ) = self::getCaseMaps();
1109 $out = preg_replace(
1110 "/([\\xc0-\\xff][\\x80-\\xbf]*)/e",
1111 "'U8' . bin2hex( strtr( \"\$1\", \$wikiLowerChars ) )",
1112 $string );
1113 }
1114 wfProfileOut( __METHOD__ );
1115 return $out;
1116 }
1117
1118 function convertForSearchResult( $termsArray ) {
1119 # some languages, e.g. Chinese, need to do a conversion
1120 # in order for search results to be displayed correctly
1121 return $termsArray;
1122 }
1123
1124 /**
1125 * Get the first character of a string.
1126 *
1127 * @param string $s
1128 * @return string
1129 */
1130 function firstChar( $s ) {
1131 $matches = array();
1132 preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1133 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/', $s, $matches);
1134
1135 return isset( $matches[1] ) ? $matches[1] : "";
1136 }
1137
1138 function initEncoding() {
1139 # Some languages may have an alternate char encoding option
1140 # (Esperanto X-coding, Japanese furigana conversion, etc)
1141 # If this language is used as the primary content language,
1142 # an override to the defaults can be set here on startup.
1143 }
1144
1145 function recodeForEdit( $s ) {
1146 # For some languages we'll want to explicitly specify
1147 # which characters make it into the edit box raw
1148 # or are converted in some way or another.
1149 # Note that if wgOutputEncoding is different from
1150 # wgInputEncoding, this text will be further converted
1151 # to wgOutputEncoding.
1152 global $wgEditEncoding;
1153 if( $wgEditEncoding == '' or
1154 $wgEditEncoding == 'UTF-8' ) {
1155 return $s;
1156 } else {
1157 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
1158 }
1159 }
1160
1161 function recodeInput( $s ) {
1162 # Take the previous into account.
1163 global $wgEditEncoding;
1164 if($wgEditEncoding != "") {
1165 $enc = $wgEditEncoding;
1166 } else {
1167 $enc = 'UTF-8';
1168 }
1169 if( $enc == 'UTF-8' ) {
1170 return $s;
1171 } else {
1172 return $this->iconv( $enc, 'UTF-8', $s );
1173 }
1174 }
1175
1176 /**
1177 * For right-to-left language support
1178 *
1179 * @return bool
1180 */
1181 function isRTL() {
1182 $this->load();
1183 return $this->rtl;
1184 }
1185
1186 /**
1187 * A hidden direction mark (LRM or RLM), depending on the language direction
1188 *
1189 * @return string
1190 */
1191 function getDirMark() {
1192 return $this->isRTL() ? "\xE2\x80\x8F" : "\xE2\x80\x8E";
1193 }
1194
1195 /**
1196 * An arrow, depending on the language direction
1197 *
1198 * @return string
1199 */
1200 function getArrow() {
1201 return $this->isRTL() ? '←' : '→';
1202 }
1203
1204 /**
1205 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
1206 *
1207 * @return bool
1208 */
1209 function linkPrefixExtension() {
1210 $this->load();
1211 return $this->linkPrefixExtension;
1212 }
1213
1214 function &getMagicWords() {
1215 $this->load();
1216 return $this->magicWords;
1217 }
1218
1219 # Fill a MagicWord object with data from here
1220 function getMagic( &$mw ) {
1221 if ( !$this->mMagicHookDone ) {
1222 $this->mMagicHookDone = true;
1223 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
1224 }
1225 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
1226 $rawEntry = $this->mMagicExtensions[$mw->mId];
1227 } else {
1228 $magicWords =& $this->getMagicWords();
1229 if ( isset( $magicWords[$mw->mId] ) ) {
1230 $rawEntry = $magicWords[$mw->mId];
1231 } else {
1232 # Fall back to English if local list is incomplete
1233 $magicWords =& Language::getMagicWords();
1234 $rawEntry = $magicWords[$mw->mId];
1235 }
1236 }
1237
1238 if( !is_array( $rawEntry ) ) {
1239 error_log( "\"$rawEntry\" is not a valid magic thingie for \"$mw->mId\"" );
1240 }
1241 $mw->mCaseSensitive = $rawEntry[0];
1242 $mw->mSynonyms = array_slice( $rawEntry, 1 );
1243 }
1244
1245 /**
1246 * Add magic words to the extension array
1247 */
1248 function addMagicWordsByLang( $newWords ) {
1249 $code = $this->getCode();
1250 $fallbackChain = array();
1251 while ( $code && !in_array( $code, $fallbackChain ) ) {
1252 $fallbackChain[] = $code;
1253 $code = self::getFallbackFor( $code );
1254 }
1255 if ( !in_array( 'en', $fallbackChain ) ) {
1256 $fallbackChain[] = 'en';
1257 }
1258 $fallbackChain = array_reverse( $fallbackChain );
1259 foreach ( $fallbackChain as $code ) {
1260 if ( isset( $newWords[$code] ) ) {
1261 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
1262 }
1263 }
1264 }
1265
1266 /**
1267 * Get special page names, as an associative array
1268 * case folded alias => real name
1269 */
1270 function getSpecialPageAliases() {
1271 $this->load();
1272 if ( !isset( $this->mExtendedSpecialPageAliases ) ) {
1273 $this->mExtendedSpecialPageAliases = $this->specialPageAliases;
1274 wfRunHooks( 'LanguageGetSpecialPageAliases',
1275 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
1276 }
1277 return $this->mExtendedSpecialPageAliases;
1278 }
1279
1280 /**
1281 * Italic is unsuitable for some languages
1282 *
1283 * @public
1284 *
1285 * @param string $text The text to be emphasized.
1286 * @return string
1287 */
1288 function emphasize( $text ) {
1289 return "<em>$text</em>";
1290 }
1291
1292 /**
1293 * Normally we output all numbers in plain en_US style, that is
1294 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
1295 * point twohundredthirtyfive. However this is not sutable for all
1296 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
1297 * Icelandic just want to use commas instead of dots, and dots instead
1298 * of commas like "293.291,235".
1299 *
1300 * An example of this function being called:
1301 * <code>
1302 * wfMsg( 'message', $wgLang->formatNum( $num ) )
1303 * </code>
1304 *
1305 * See LanguageGu.php for the Gujarati implementation and
1306 * LanguageIs.php for the , => . and . => , implementation.
1307 *
1308 * @todo check if it's viable to use localeconv() for the decimal
1309 * seperator thing.
1310 * @public
1311 * @param mixed $number the string to be formatted, should be an integer or
1312 * a floating point number.
1313 * @param bool $nocommafy Set to true for special numbers like dates
1314 * @return string
1315 */
1316 function formatNum( $number, $nocommafy = false ) {
1317 global $wgTranslateNumerals;
1318 if (!$nocommafy) {
1319 $number = $this->commafy($number);
1320 $s = $this->separatorTransformTable();
1321 if (!is_null($s)) { $number = strtr($number, $s); }
1322 }
1323
1324 if ($wgTranslateNumerals) {
1325 $s = $this->digitTransformTable();
1326 if (!is_null($s)) { $number = strtr($number, $s); }
1327 }
1328
1329 return $number;
1330 }
1331
1332 function parseFormattedNumber( $number ) {
1333 $s = $this->digitTransformTable();
1334 if (!is_null($s)) { $number = strtr($number, array_flip($s)); }
1335
1336 $s = $this->separatorTransformTable();
1337 if (!is_null($s)) { $number = strtr($number, array_flip($s)); }
1338
1339 $number = strtr( $number, array (',' => '') );
1340 return $number;
1341 }
1342
1343 /**
1344 * Adds commas to a given number
1345 *
1346 * @param mixed $_
1347 * @return string
1348 */
1349 function commafy($_) {
1350 return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_)));
1351 }
1352
1353 function digitTransformTable() {
1354 $this->load();
1355 return $this->digitTransformTable;
1356 }
1357
1358 function separatorTransformTable() {
1359 $this->load();
1360 return $this->separatorTransformTable;
1361 }
1362
1363
1364 /**
1365 * For the credit list in includes/Credits.php (action=credits)
1366 *
1367 * @param array $l
1368 * @return string
1369 */
1370 function listToText( $l ) {
1371 $s = '';
1372 $m = count($l) - 1;
1373 for ($i = $m; $i >= 0; $i--) {
1374 if ($i == $m) {
1375 $s = $l[$i];
1376 } else if ($i == $m - 1) {
1377 $s = $l[$i] . ' ' . $this->getMessageFromDB( 'and' ) . ' ' . $s;
1378 } else {
1379 $s = $l[$i] . ', ' . $s;
1380 }
1381 }
1382 return $s;
1383 }
1384
1385 /**
1386 * Truncate a string to a specified length in bytes, appending an optional
1387 * string (e.g. for ellipses)
1388 *
1389 * The database offers limited byte lengths for some columns in the database;
1390 * multi-byte character sets mean we need to ensure that only whole characters
1391 * are included, otherwise broken characters can be passed to the user
1392 *
1393 * If $length is negative, the string will be truncated from the beginning
1394 *
1395 * @param string $string String to truncate
1396 * @param int $length Maximum length (excluding ellipses)
1397 * @param string $ellipses String to append to the truncated text
1398 * @return string
1399 */
1400 function truncate( $string, $length, $ellipsis = "" ) {
1401 if( $length == 0 ) {
1402 return $ellipsis;
1403 }
1404 if ( strlen( $string ) <= abs( $length ) ) {
1405 return $string;
1406 }
1407 if( $length > 0 ) {
1408 $string = substr( $string, 0, $length );
1409 $char = ord( $string[strlen( $string ) - 1] );
1410 $m = array();
1411 if ($char >= 0xc0) {
1412 # We got the first byte only of a multibyte char; remove it.
1413 $string = substr( $string, 0, -1 );
1414 } elseif( $char >= 0x80 &&
1415 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
1416 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) ) {
1417 # We chopped in the middle of a character; remove it
1418 $string = $m[1];
1419 }
1420 return $string . $ellipsis;
1421 } else {
1422 $string = substr( $string, $length );
1423 $char = ord( $string[0] );
1424 if( $char >= 0x80 && $char < 0xc0 ) {
1425 # We chopped in the middle of a character; remove the whole thing
1426 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
1427 }
1428 return $ellipsis . $string;
1429 }
1430 }
1431
1432 /**
1433 * Grammatical transformations, needed for inflected languages
1434 * Invoked by putting {{grammar:case|word}} in a message
1435 *
1436 * @param string $word
1437 * @param string $case
1438 * @return string
1439 */
1440 function convertGrammar( $word, $case ) {
1441 global $wgGrammarForms;
1442 if ( isset($wgGrammarForms['en'][$case][$word]) ) {
1443 return $wgGrammarForms['en'][$case][$word];
1444 }
1445 return $word;
1446 }
1447
1448 /**
1449 * Plural form transformations, needed for some languages.
1450 * For example, there are 3 form of plural in Russian and Polish,
1451 * depending on "count mod 10". See [[w:Plural]]
1452 * For English it is pretty simple.
1453 *
1454 * Invoked by putting {{plural:count|wordform1|wordform2}}
1455 * or {{plural:count|wordform1|wordform2|wordform3}}
1456 *
1457 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
1458 *
1459 * @param integer $count
1460 * @param string $wordform1
1461 * @param string $wordform2
1462 * @param string $wordform3 (optional)
1463 * @param string $wordform4 (optional)
1464 * @param string $wordform5 (optional)
1465 * @return string
1466 */
1467 function convertPlural( $count, $w1, $w2, $w3, $w4, $w5) {
1468 return ( $count == '1' || $count == '-1' ) ? $w1 : $w2;
1469 }
1470
1471 /**
1472 * For translaing of expiry times
1473 * @param string The validated block time in English
1474 * @return Somehow translated block time
1475 * @see LanguageFi.php for example implementation
1476 */
1477 function translateBlockExpiry( $str ) {
1478
1479 $scBlockExpiryOptions = $this->getMessageFromDB( 'ipboptions' );
1480
1481 if ( $scBlockExpiryOptions == '-') {
1482 return $str;
1483 }
1484
1485 foreach (explode(',', $scBlockExpiryOptions) as $option) {
1486 if ( strpos($option, ":") === false )
1487 continue;
1488 list($show, $value) = explode(":", $option);
1489 if ( strcmp ( $str, $value) == 0 ) {
1490 return htmlspecialchars( trim( $show ) );
1491 }
1492 }
1493
1494 return $str;
1495 }
1496
1497 /**
1498 * languages like Chinese need to be segmented in order for the diff
1499 * to be of any use
1500 *
1501 * @param string $text
1502 * @return string
1503 */
1504 function segmentForDiff( $text ) {
1505 return $text;
1506 }
1507
1508 /**
1509 * and unsegment to show the result
1510 *
1511 * @param string $text
1512 * @return string
1513 */
1514 function unsegmentForDiff( $text ) {
1515 return $text;
1516 }
1517
1518 # convert text to different variants of a language.
1519 function convert( $text, $isTitle = false) {
1520 return $this->mConverter->convert($text, $isTitle);
1521 }
1522
1523 # Convert text from within Parser
1524 function parserConvert( $text, &$parser ) {
1525 return $this->mConverter->parserConvert( $text, $parser );
1526 }
1527
1528 # Check if this is a language with variants
1529 function hasVariants(){
1530 return sizeof($this->getVariants())>1;
1531 }
1532
1533 # Put custom tags (e.g. -{ }-) around math to prevent conversion
1534 function armourMath($text){
1535 return $this->mConverter->armourMath($text);
1536 }
1537
1538
1539 /**
1540 * Perform output conversion on a string, and encode for safe HTML output.
1541 * @param string $text
1542 * @param bool $isTitle -- wtf?
1543 * @return string
1544 * @todo this should get integrated somewhere sane
1545 */
1546 function convertHtml( $text, $isTitle = false ) {
1547 return htmlspecialchars( $this->convert( $text, $isTitle ) );
1548 }
1549
1550 function convertCategoryKey( $key ) {
1551 return $this->mConverter->convertCategoryKey( $key );
1552 }
1553
1554 /**
1555 * get the list of variants supported by this langauge
1556 * see sample implementation in LanguageZh.php
1557 *
1558 * @return array an array of language codes
1559 */
1560 function getVariants() {
1561 return $this->mConverter->getVariants();
1562 }
1563
1564
1565 function getPreferredVariant( $fromUser = true ) {
1566 return $this->mConverter->getPreferredVariant( $fromUser );
1567 }
1568
1569 /**
1570 * if a language supports multiple variants, it is
1571 * possible that non-existing link in one variant
1572 * actually exists in another variant. this function
1573 * tries to find it. See e.g. LanguageZh.php
1574 *
1575 * @param string $link the name of the link
1576 * @param mixed $nt the title object of the link
1577 * @return null the input parameters may be modified upon return
1578 */
1579 function findVariantLink( &$link, &$nt ) {
1580 $this->mConverter->findVariantLink($link, $nt);
1581 }
1582
1583 /**
1584 * If a language supports multiple variants, converts text
1585 * into an array of all possible variants of the text:
1586 * 'variant' => text in that variant
1587 */
1588
1589 function convertLinkToAllVariants($text){
1590 return $this->mConverter->convertLinkToAllVariants($text);
1591 }
1592
1593
1594 /**
1595 * returns language specific options used by User::getPageRenderHash()
1596 * for example, the preferred language variant
1597 *
1598 * @return string
1599 * @public
1600 */
1601 function getExtraHashOptions() {
1602 return $this->mConverter->getExtraHashOptions();
1603 }
1604
1605 /**
1606 * for languages that support multiple variants, the title of an
1607 * article may be displayed differently in different variants. this
1608 * function returns the apporiate title defined in the body of the article.
1609 *
1610 * @return string
1611 */
1612 function getParsedTitle() {
1613 return $this->mConverter->getParsedTitle();
1614 }
1615
1616 /**
1617 * Enclose a string with the "no conversion" tag. This is used by
1618 * various functions in the Parser
1619 *
1620 * @param string $text text to be tagged for no conversion
1621 * @return string the tagged text
1622 */
1623 function markNoConversion( $text, $noParse=false ) {
1624 return $this->mConverter->markNoConversion( $text, $noParse );
1625 }
1626
1627 /**
1628 * A regular expression to match legal word-trailing characters
1629 * which should be merged onto a link of the form [[foo]]bar.
1630 *
1631 * @return string
1632 * @public
1633 */
1634 function linkTrail() {
1635 $this->load();
1636 return $this->linkTrail;
1637 }
1638
1639 function getLangObj() {
1640 return $this;
1641 }
1642
1643 /**
1644 * Get the RFC 3066 code for this language object
1645 */
1646 function getCode() {
1647 return $this->mCode;
1648 }
1649
1650 function setCode( $code ) {
1651 $this->mCode = $code;
1652 }
1653
1654 static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
1655 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
1656 }
1657
1658 static function getMessagesFileName( $code ) {
1659 global $IP;
1660 return self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
1661 }
1662
1663 static function getClassFileName( $code ) {
1664 global $IP;
1665 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
1666 }
1667
1668 static function getLocalisationArray( $code, $disableCache = false ) {
1669 self::loadLocalisation( $code, $disableCache );
1670 return self::$mLocalisationCache[$code];
1671 }
1672
1673 /**
1674 * Load localisation data for a given code into the static cache
1675 *
1676 * @return array Dependencies, map of filenames to mtimes
1677 */
1678 static function loadLocalisation( $code, $disableCache = false ) {
1679 static $recursionGuard = array();
1680 global $wgMemc;
1681
1682 if ( !$code ) {
1683 throw new MWException( "Invalid language code requested" );
1684 }
1685
1686 if ( !$disableCache ) {
1687 # Try the per-process cache
1688 if ( isset( self::$mLocalisationCache[$code] ) ) {
1689 return self::$mLocalisationCache[$code]['deps'];
1690 }
1691
1692 wfProfileIn( __METHOD__ );
1693
1694 # Try the serialized directory
1695 $cache = wfGetPrecompiledData( self::getFileName( "Messages", $code, '.ser' ) );
1696 if ( $cache ) {
1697 self::$mLocalisationCache[$code] = $cache;
1698 wfDebug( "Language::loadLocalisation(): got localisation for $code from precompiled data file\n" );
1699 wfProfileOut( __METHOD__ );
1700 return self::$mLocalisationCache[$code]['deps'];
1701 }
1702
1703 # Try the global cache
1704 $memcKey = wfMemcKey('localisation', $code );
1705 $cache = $wgMemc->get( $memcKey );
1706 if ( $cache ) {
1707 # Check file modification times
1708 foreach ( $cache['deps'] as $file => $mtime ) {
1709 if ( !file_exists( $file ) || filemtime( $file ) > $mtime ) {
1710 break;
1711 }
1712 }
1713 if ( self::isLocalisationOutOfDate( $cache ) ) {
1714 $wgMemc->delete( $memcKey );
1715 $cache = false;
1716 wfDebug( "Language::loadLocalisation(): localisation cache for $code had expired due to update of $file\n" );
1717 } else {
1718 self::$mLocalisationCache[$code] = $cache;
1719 wfDebug( "Language::loadLocalisation(): got localisation for $code from cache\n" );
1720 wfProfileOut( __METHOD__ );
1721 return $cache['deps'];
1722 }
1723 }
1724 } else {
1725 wfProfileIn( __METHOD__ );
1726 }
1727
1728 # Default fallback, may be overridden when the messages file is included
1729 if ( $code != 'en' ) {
1730 $fallback = 'en';
1731 } else {
1732 $fallback = false;
1733 }
1734
1735 # Load the primary localisation from the source file
1736 $filename = self::getMessagesFileName( $code );
1737 if ( !file_exists( $filename ) ) {
1738 wfDebug( "Language::loadLocalisation(): no localisation file for $code, using implicit fallback to en\n" );
1739 $cache = array();
1740 $deps = array();
1741 } else {
1742 $deps = array( $filename => filemtime( $filename ) );
1743 require( $filename );
1744 $cache = compact( self::$mLocalisationKeys );
1745 wfDebug( "Language::loadLocalisation(): got localisation for $code from source\n" );
1746 }
1747
1748 if ( !empty( $fallback ) ) {
1749 # Load the fallback localisation, with a circular reference guard
1750 if ( isset( $recursionGuard[$code] ) ) {
1751 throw new MWException( "Error: Circular fallback reference in language code $code" );
1752 }
1753 $recursionGuard[$code] = true;
1754 $newDeps = self::loadLocalisation( $fallback, $disableCache );
1755 unset( $recursionGuard[$code] );
1756
1757 $secondary = self::$mLocalisationCache[$fallback];
1758 $deps = array_merge( $deps, $newDeps );
1759
1760 # Merge the fallback localisation with the current localisation
1761 foreach ( self::$mLocalisationKeys as $key ) {
1762 if ( isset( $cache[$key] ) ) {
1763 if ( isset( $secondary[$key] ) ) {
1764 if ( in_array( $key, self::$mMergeableMapKeys ) ) {
1765 $cache[$key] = $cache[$key] + $secondary[$key];
1766 } elseif ( in_array( $key, self::$mMergeableListKeys ) ) {
1767 $cache[$key] = array_merge( $secondary[$key], $cache[$key] );
1768 } elseif ( in_array( $key, self::$mMergeableAliasListKeys ) ) {
1769 $cache[$key] = array_merge_recursive( $cache[$key], $secondary[$key] );
1770 }
1771 }
1772 } else {
1773 $cache[$key] = $secondary[$key];
1774 }
1775 }
1776
1777 # Merge bookstore lists if requested
1778 if ( !empty( $cache['bookstoreList']['inherit'] ) ) {
1779 $cache['bookstoreList'] = array_merge( $cache['bookstoreList'], $secondary['bookstoreList'] );
1780 }
1781 if ( isset( $cache['bookstoreList']['inherit'] ) ) {
1782 unset( $cache['bookstoreList']['inherit'] );
1783 }
1784 }
1785
1786 # Add dependencies to the cache entry
1787 $cache['deps'] = $deps;
1788
1789 # Replace spaces with underscores in namespace names
1790 $cache['namespaceNames'] = str_replace( ' ', '_', $cache['namespaceNames'] );
1791
1792 # Save to both caches
1793 self::$mLocalisationCache[$code] = $cache;
1794 if ( !$disableCache ) {
1795 $wgMemc->set( $memcKey, $cache );
1796 }
1797
1798 wfProfileOut( __METHOD__ );
1799 return $deps;
1800 }
1801
1802 /**
1803 * Test if a given localisation cache is out of date with respect to the
1804 * source Messages files. This is done automatically for the global cache
1805 * in $wgMemc, but is only done on certain occasions for the serialized
1806 * data file.
1807 *
1808 * @param $cache mixed Either a language code or a cache array
1809 */
1810 static function isLocalisationOutOfDate( $cache ) {
1811 if ( !is_array( $cache ) ) {
1812 self::loadLocalisation( $cache );
1813 $cache = self::$mLocalisationCache[$cache];
1814 }
1815 $expired = false;
1816 foreach ( $cache['deps'] as $file => $mtime ) {
1817 if ( !file_exists( $file ) || filemtime( $file ) > $mtime ) {
1818 $expired = true;
1819 break;
1820 }
1821 }
1822 return $expired;
1823 }
1824
1825 /**
1826 * Get the fallback for a given language
1827 */
1828 static function getFallbackFor( $code ) {
1829 self::loadLocalisation( $code );
1830 return self::$mLocalisationCache[$code]['fallback'];
1831 }
1832
1833 /**
1834 * Get all messages for a given language
1835 */
1836 static function getMessagesFor( $code ) {
1837 self::loadLocalisation( $code );
1838 return self::$mLocalisationCache[$code]['messages'];
1839 }
1840
1841 /**
1842 * Get a message for a given language
1843 */
1844 static function getMessageFor( $key, $code ) {
1845 self::loadLocalisation( $code );
1846 return isset( self::$mLocalisationCache[$code]['messages'][$key] ) ? self::$mLocalisationCache[$code]['messages'][$key] : null;
1847 }
1848
1849 /**
1850 * Load localisation data for this object
1851 */
1852 function load() {
1853 if ( !$this->mLoaded ) {
1854 self::loadLocalisation( $this->getCode() );
1855 $cache =& self::$mLocalisationCache[$this->getCode()];
1856 foreach ( self::$mLocalisationKeys as $key ) {
1857 $this->$key = $cache[$key];
1858 }
1859 $this->mLoaded = true;
1860
1861 $this->fixUpSettings();
1862 }
1863 }
1864
1865 /**
1866 * Do any necessary post-cache-load settings adjustment
1867 */
1868 function fixUpSettings() {
1869 global $wgExtraNamespaces, $wgMetaNamespace, $wgMetaNamespaceTalk,
1870 $wgNamespaceAliases, $wgAmericanDates;
1871 wfProfileIn( __METHOD__ );
1872 if ( $wgExtraNamespaces ) {
1873 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames;
1874 }
1875
1876 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
1877 if ( $wgMetaNamespaceTalk ) {
1878 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
1879 } else {
1880 $talk = $this->namespaceNames[NS_PROJECT_TALK];
1881 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
1882
1883 # Allow grammar transformations
1884 # Allowing full message-style parsing would make simple requests
1885 # such as action=raw much more expensive than they need to be.
1886 # This will hopefully cover most cases.
1887 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
1888 array( &$this, 'replaceGrammarInNamespace' ), $talk );
1889 $talk = str_replace( ' ', '_', $talk );
1890 $this->namespaceNames[NS_PROJECT_TALK] = $talk;
1891 }
1892
1893 # The above mixing may leave namespaces out of canonical order.
1894 # Re-order by namespace ID number...
1895 ksort( $this->namespaceNames );
1896
1897 # Put namespace names and aliases into a hashtable.
1898 # If this is too slow, then we should arrange it so that it is done
1899 # before caching. The catch is that at pre-cache time, the above
1900 # class-specific fixup hasn't been done.
1901 $this->mNamespaceIds = array();
1902 foreach ( $this->namespaceNames as $index => $name ) {
1903 $this->mNamespaceIds[$this->lc($name)] = $index;
1904 }
1905 if ( $this->namespaceAliases ) {
1906 foreach ( $this->namespaceAliases as $name => $index ) {
1907 $this->mNamespaceIds[$this->lc($name)] = $index;
1908 }
1909 }
1910 if ( $wgNamespaceAliases ) {
1911 foreach ( $wgNamespaceAliases as $name => $index ) {
1912 $this->mNamespaceIds[$this->lc($name)] = $index;
1913 }
1914 }
1915
1916 if ( $this->defaultDateFormat == 'dmy or mdy' ) {
1917 $this->defaultDateFormat = $wgAmericanDates ? 'mdy' : 'dmy';
1918 }
1919 wfProfileOut( __METHOD__ );
1920 }
1921
1922 function replaceGrammarInNamespace( $m ) {
1923 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
1924 }
1925
1926 static function getCaseMaps() {
1927 static $wikiUpperChars, $wikiLowerChars;
1928 if ( isset( $wikiUpperChars ) ) {
1929 return array( $wikiUpperChars, $wikiLowerChars );
1930 }
1931
1932 wfProfileIn( __METHOD__ );
1933 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
1934 if ( $arr === false ) {
1935 throw new MWException(
1936 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
1937 }
1938 extract( $arr );
1939 wfProfileOut( __METHOD__ );
1940 return array( $wikiUpperChars, $wikiLowerChars );
1941 }
1942
1943 function formatTimePeriod( $seconds ) {
1944 if ( $seconds < 10 ) {
1945 return $this->formatNum( sprintf( "%.1f", $seconds ) ) . wfMsg( 'seconds-abbrev' );
1946 } elseif ( $seconds < 60 ) {
1947 return $this->formatNum( round( $seconds ) ) . wfMsg( 'seconds-abbrev' );
1948 } elseif ( $seconds < 3600 ) {
1949 return $this->formatNum( floor( $seconds / 60 ) ) . wfMsg( 'minutes-abbrev' ) .
1950 $this->formatNum( round( fmod( $seconds, 60 ) ) ) . wfMsg( 'seconds-abbrev' );
1951 } else {
1952 $hours = floor( $seconds / 3600 );
1953 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
1954 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
1955 return $this->formatNum( $hours ) . wfMsg( 'hours-abbrev' ) .
1956 $this->formatNum( $minutes ) . wfMsg( 'minutes-abbrev' ) .
1957 $this->formatNum( $secondsPart ) . wfMsg( 'seconds-abbrev' );
1958 }
1959 }
1960
1961 function formatBitrate( $bps ) {
1962 $units = array( 'bps', 'kbps', 'Mbps', 'Gbps' );
1963 if ( $bps <= 0 ) {
1964 return $this->formatNum( $bps ) . $units[0];
1965 }
1966 $unitIndex = floor( log10( $bps ) / 3 );
1967 $mantissa = $bps / pow( 1000, $unitIndex );
1968 if ( $mantissa < 10 ) {
1969 $mantissa = round( $mantissa, 1 );
1970 } else {
1971 $mantissa = round( $mantissa );
1972 }
1973 return $this->formatNum( $mantissa ) . $units[$unitIndex];
1974 }
1975
1976 /**
1977 * Format a size in bytes for output, using an appropriate
1978 * unit (B, KB, MB or GB) according to the magnitude in question
1979 *
1980 * @param $size Size to format
1981 * @return string Plain text (not HTML)
1982 */
1983 function formatSize( $size ) {
1984 // For small sizes no decimal places necessary
1985 $round = 0;
1986 if( $size > 1024 ) {
1987 $size = $size / 1024;
1988 if( $size > 1024 ) {
1989 $size = $size / 1024;
1990 // For MB and bigger two decimal places are smarter
1991 $round = 2;
1992 if( $size > 1024 ) {
1993 $size = $size / 1024;
1994 $msg = 'size-gigabytes';
1995 } else {
1996 $msg = 'size-megabytes';
1997 }
1998 } else {
1999 $msg = 'size-kilobytes';
2000 }
2001 } else {
2002 $msg = 'size-bytes';
2003 }
2004 $size = round( $size, $round );
2005 $text = $this->getMessageFromDB( $msg );
2006 return str_replace( '$1', $this->formatNum( $size ), $text );
2007 }
2008 }
2009
2010
2011
2012
2013