Change or to ||.
[lhc/web/wiklou.git] / languages / Language.php
1 <?php
2 /**
3 * @defgroup Language Language
4 *
5 * @file
6 * @ingroup Language
7 */
8
9 if( !defined( 'MEDIAWIKI' ) ) {
10 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
11 exit( 1 );
12 }
13
14 # Read language names
15 global $wgLanguageNames;
16 require_once( dirname(__FILE__) . '/Names.php' ) ;
17
18 global $wgInputEncoding, $wgOutputEncoding;
19
20 /**
21 * These are always UTF-8, they exist only for backwards compatibility
22 */
23 $wgInputEncoding = "UTF-8";
24 $wgOutputEncoding = "UTF-8";
25
26 if( function_exists( 'mb_strtoupper' ) ) {
27 mb_internal_encoding('UTF-8');
28 }
29
30 /**
31 * a fake language converter
32 *
33 * @ingroup Language
34 */
35 class FakeConverter {
36 var $mLang;
37 function FakeConverter($langobj) {$this->mLang = $langobj;}
38 function autoConvertToAllVariants($text) {return $text;}
39 function convert($t, $i) {return $t;}
40 function parserConvert($t, $p) {return $t;}
41 function getVariants() { return array( $this->mLang->getCode() ); }
42 function getPreferredVariant() {return $this->mLang->getCode(); }
43 function findVariantLink(&$l, &$n, $ignoreOtherCond = false) {}
44 function getExtraHashOptions() {return '';}
45 function getParsedTitle() {return '';}
46 function markNoConversion($text, $noParse=false) {return $text;}
47 function convertCategoryKey( $key ) {return $key; }
48 function convertLinkToAllVariants($text){ return array( $this->mLang->getCode() => $text); }
49 function armourMath($text){ return $text; }
50 }
51
52 /**
53 * Internationalisation code
54 * @ingroup Language
55 */
56 class Language {
57 var $mConverter, $mVariants, $mCode, $mLoaded = false;
58 var $mMagicExtensions = array(), $mMagicHookDone = false;
59
60 var $mNamespaceIds, $namespaceNames, $namespaceAliases;
61 var $dateFormatStrings = array();
62 var $minSearchLength;
63 var $mExtendedSpecialPageAliases;
64
65 /**
66 * ReplacementArray object caches
67 */
68 var $transformData = array();
69
70 static public $dataCache;
71 static public $mLangObjCache = array();
72
73 static public $mWeekdayMsgs = array(
74 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
75 'friday', 'saturday'
76 );
77
78 static public $mWeekdayAbbrevMsgs = array(
79 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
80 );
81
82 static public $mMonthMsgs = array(
83 'january', 'february', 'march', 'april', 'may_long', 'june',
84 'july', 'august', 'september', 'october', 'november',
85 'december'
86 );
87 static public $mMonthGenMsgs = array(
88 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
89 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
90 'december-gen'
91 );
92 static public $mMonthAbbrevMsgs = array(
93 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
94 'sep', 'oct', 'nov', 'dec'
95 );
96
97 static public $mIranianCalendarMonthMsgs = array(
98 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
99 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
100 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
101 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
102 );
103
104 static public $mHebrewCalendarMonthMsgs = array(
105 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
106 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
107 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
108 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
109 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
110 );
111
112 static public $mHebrewCalendarMonthGenMsgs = array(
113 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
114 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
115 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
116 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
117 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
118 );
119
120 static public $mHijriCalendarMonthMsgs = array(
121 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
122 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
123 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
124 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
125 );
126
127 /**
128 * Get a cached language object for a given language code
129 */
130 static function factory( $code ) {
131 if ( !isset( self::$mLangObjCache[$code] ) ) {
132 if( count( self::$mLangObjCache ) > 10 ) {
133 // Don't keep a billion objects around, that's stupid.
134 self::$mLangObjCache = array();
135 }
136 self::$mLangObjCache[$code] = self::newFromCode( $code );
137 }
138 return self::$mLangObjCache[$code];
139 }
140
141 /**
142 * Create a language object for a given language code
143 */
144 protected static function newFromCode( $code ) {
145 global $IP;
146 static $recursionLevel = 0;
147 if ( $code == 'en' ) {
148 $class = 'Language';
149 } else {
150 $class = 'Language' . str_replace( '-', '_', ucfirst( $code ) );
151 // Preload base classes to work around APC/PHP5 bug
152 if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
153 include_once("$IP/languages/classes/$class.deps.php");
154 }
155 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
156 include_once("$IP/languages/classes/$class.php");
157 }
158 }
159
160 if ( $recursionLevel > 5 ) {
161 throw new MWException( "Language fallback loop detected when creating class $class\n" );
162 }
163
164 if( ! class_exists( $class ) ) {
165 $fallback = Language::getFallbackFor( $code );
166 ++$recursionLevel;
167 $lang = Language::newFromCode( $fallback );
168 --$recursionLevel;
169 $lang->setCode( $code );
170 } else {
171 $lang = new $class;
172 }
173 return $lang;
174 }
175
176 /**
177 * Get the LocalisationCache instance
178 */
179 public static function getLocalisationCache() {
180 if ( is_null( self::$dataCache ) ) {
181 global $wgLocalisationCacheConf;
182 $class = $wgLocalisationCacheConf['class'];
183 self::$dataCache = new $class( $wgLocalisationCacheConf );
184 }
185 return self::$dataCache;
186 }
187
188 function __construct() {
189 $this->mConverter = new FakeConverter($this);
190 // Set the code to the name of the descendant
191 if ( get_class( $this ) == 'Language' ) {
192 $this->mCode = 'en';
193 } else {
194 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
195 }
196 self::getLocalisationCache();
197 }
198
199 /**
200 * Reduce memory usage
201 */
202 function __destruct() {
203 foreach ( $this as $name => $value ) {
204 unset( $this->$name );
205 }
206 }
207
208 /**
209 * Hook which will be called if this is the content language.
210 * Descendants can use this to register hook functions or modify globals
211 */
212 function initContLang() {}
213
214 /**
215 * @deprecated Use User::getDefaultOptions()
216 * @return array
217 */
218 function getDefaultUserOptions() {
219 wfDeprecated( __METHOD__ );
220 return User::getDefaultOptions();
221 }
222
223 function getFallbackLanguageCode() {
224 if ( $this->mCode === 'en' ) {
225 return false;
226 } else {
227 return self::$dataCache->getItem( $this->mCode, 'fallback' );
228 }
229 }
230
231 /**
232 * Exports $wgBookstoreListEn
233 * @return array
234 */
235 function getBookstoreList() {
236 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
237 }
238
239 /**
240 * @return array
241 */
242 function getNamespaces() {
243 if ( is_null( $this->namespaceNames ) ) {
244 global $wgExtraNamespaces, $wgMetaNamespace, $wgMetaNamespaceTalk;
245
246 $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
247 if ( $wgExtraNamespaces ) {
248 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames;
249 }
250
251 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
252 if ( $wgMetaNamespaceTalk ) {
253 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
254 } else {
255 $talk = $this->namespaceNames[NS_PROJECT_TALK];
256 $this->namespaceNames[NS_PROJECT_TALK] =
257 $this->fixVariableInNamespace( $talk );
258 }
259
260 # The above mixing may leave namespaces out of canonical order.
261 # Re-order by namespace ID number...
262 ksort( $this->namespaceNames );
263 }
264 return $this->namespaceNames;
265 }
266
267 /**
268 * A convenience function that returns the same thing as
269 * getNamespaces() except with the array values changed to ' '
270 * where it found '_', useful for producing output to be displayed
271 * e.g. in <select> forms.
272 *
273 * @return array
274 */
275 function getFormattedNamespaces() {
276 $ns = $this->getNamespaces();
277 foreach($ns as $k => $v) {
278 $ns[$k] = strtr($v, '_', ' ');
279 }
280 return $ns;
281 }
282
283 /**
284 * Get a namespace value by key
285 * <code>
286 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
287 * echo $mw_ns; // prints 'MediaWiki'
288 * </code>
289 *
290 * @param $index Int: the array key of the namespace to return
291 * @return mixed, string if the namespace value exists, otherwise false
292 */
293 function getNsText( $index ) {
294 $ns = $this->getNamespaces();
295 return isset( $ns[$index] ) ? $ns[$index] : false;
296 }
297
298 /**
299 * A convenience function that returns the same thing as
300 * getNsText() except with '_' changed to ' ', useful for
301 * producing output.
302 *
303 * @return array
304 */
305 function getFormattedNsText( $index ) {
306 $ns = $this->getNsText( $index );
307 return strtr($ns, '_', ' ');
308 }
309
310 /**
311 * Get a namespace key by value, case insensitive.
312 * Only matches namespace names for the current language, not the
313 * canonical ones defined in Namespace.php.
314 *
315 * @param $text String
316 * @return mixed An integer if $text is a valid value otherwise false
317 */
318 function getLocalNsIndex( $text ) {
319 $lctext = $this->lc($text);
320 $ids = $this->getNamespaceIds();
321 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
322 }
323
324 function getNamespaceAliases() {
325 if ( is_null( $this->namespaceAliases ) ) {
326 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
327 if ( !$aliases ) {
328 $aliases = array();
329 } else {
330 foreach ( $aliases as $name => $index ) {
331 if ( $index === NS_PROJECT_TALK ) {
332 unset( $aliases[$name] );
333 $name = $this->fixVariableInNamespace( $name );
334 $aliases[$name] = $index;
335 }
336 }
337 }
338 $this->namespaceAliases = $aliases;
339 }
340 return $this->namespaceAliases;
341 }
342
343 function getNamespaceIds() {
344 if ( is_null( $this->mNamespaceIds ) ) {
345 global $wgNamespaceAliases;
346 # Put namespace names and aliases into a hashtable.
347 # If this is too slow, then we should arrange it so that it is done
348 # before caching. The catch is that at pre-cache time, the above
349 # class-specific fixup hasn't been done.
350 $this->mNamespaceIds = array();
351 foreach ( $this->getNamespaces() as $index => $name ) {
352 $this->mNamespaceIds[$this->lc($name)] = $index;
353 }
354 foreach ( $this->getNamespaceAliases() as $name => $index ) {
355 $this->mNamespaceIds[$this->lc($name)] = $index;
356 }
357 if ( $wgNamespaceAliases ) {
358 foreach ( $wgNamespaceAliases as $name => $index ) {
359 $this->mNamespaceIds[$this->lc($name)] = $index;
360 }
361 }
362 }
363 return $this->mNamespaceIds;
364 }
365
366
367 /**
368 * Get a namespace key by value, case insensitive. Canonical namespace
369 * names override custom ones defined for the current language.
370 *
371 * @param $text String
372 * @return mixed An integer if $text is a valid value otherwise false
373 */
374 function getNsIndex( $text ) {
375 $lctext = $this->lc($text);
376 if ( ( $ns = MWNamespace::getCanonicalIndex( $lctext ) ) !== null ) {
377 return $ns;
378 }
379 $ids = $this->getNamespaceIds();
380 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
381 }
382
383 /**
384 * short names for language variants used for language conversion links.
385 *
386 * @param $code String
387 * @return string
388 */
389 function getVariantname( $code ) {
390 return $this->getMessageFromDB( "variantname-$code" );
391 }
392
393 function specialPage( $name ) {
394 $aliases = $this->getSpecialPageAliases();
395 if ( isset( $aliases[$name][0] ) ) {
396 $name = $aliases[$name][0];
397 }
398 return $this->getNsText( NS_SPECIAL ) . ':' . $name;
399 }
400
401 function getQuickbarSettings() {
402 return array(
403 $this->getMessage( 'qbsettings-none' ),
404 $this->getMessage( 'qbsettings-fixedleft' ),
405 $this->getMessage( 'qbsettings-fixedright' ),
406 $this->getMessage( 'qbsettings-floatingleft' ),
407 $this->getMessage( 'qbsettings-floatingright' )
408 );
409 }
410
411 function getMathNames() {
412 return self::$dataCache->getItem( $this->mCode, 'mathNames' );
413 }
414
415 function getDatePreferences() {
416 return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
417 }
418
419 function getDateFormats() {
420 return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
421 }
422
423 function getDefaultDateFormat() {
424 $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
425 if ( $df === 'dmy or mdy' ) {
426 global $wgAmericanDates;
427 return $wgAmericanDates ? 'mdy' : 'dmy';
428 } else {
429 return $df;
430 }
431 }
432
433 function getDatePreferenceMigrationMap() {
434 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
435 }
436
437 function getImageFile( $image ) {
438 return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
439 }
440
441 function getDefaultUserOptionOverrides() {
442 return self::$dataCache->getItem( $this->mCode, 'defaultUserOptionOverrides' );
443 }
444
445 function getExtraUserToggles() {
446 return self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
447 }
448
449 function getUserToggle( $tog ) {
450 return $this->getMessageFromDB( "tog-$tog" );
451 }
452
453 /**
454 * Get language names, indexed by code.
455 * If $customisedOnly is true, only returns codes with a messages file
456 */
457 public static function getLanguageNames( $customisedOnly = false ) {
458 global $wgLanguageNames, $wgExtraLanguageNames;
459 $allNames = $wgExtraLanguageNames + $wgLanguageNames;
460 if ( !$customisedOnly ) {
461 return $allNames;
462 }
463
464 global $IP;
465 $names = array();
466 $dir = opendir( "$IP/languages/messages" );
467 while( false !== ( $file = readdir( $dir ) ) ) {
468 $code = self::getCodeFromFileName( $file, 'Messages' );
469 if ( $code && isset( $allNames[$code] ) ) {
470 $names[$code] = $allNames[$code];
471 }
472 }
473 closedir( $dir );
474 return $names;
475 }
476
477 /**
478 * Get a message from the MediaWiki namespace.
479 *
480 * @param $msg String: message name
481 * @return string
482 */
483 function getMessageFromDB( $msg ) {
484 return wfMsgExt( $msg, array( 'parsemag', 'language' => $this ) );
485 }
486
487 function getLanguageName( $code ) {
488 $names = self::getLanguageNames();
489 if ( !array_key_exists( $code, $names ) ) {
490 return '';
491 }
492 return $names[$code];
493 }
494
495 function getMonthName( $key ) {
496 return $this->getMessageFromDB( self::$mMonthMsgs[$key-1] );
497 }
498
499 function getMonthNameGen( $key ) {
500 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key-1] );
501 }
502
503 function getMonthAbbreviation( $key ) {
504 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key-1] );
505 }
506
507 function getWeekdayName( $key ) {
508 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key-1] );
509 }
510
511 function getWeekdayAbbreviation( $key ) {
512 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key-1] );
513 }
514
515 function getIranianCalendarMonthName( $key ) {
516 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key-1] );
517 }
518
519 function getHebrewCalendarMonthName( $key ) {
520 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key-1] );
521 }
522
523 function getHebrewCalendarMonthNameGen( $key ) {
524 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key-1] );
525 }
526
527 function getHijriCalendarMonthName( $key ) {
528 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key-1] );
529 }
530
531 /**
532 * Used by date() and time() to adjust the time output.
533 *
534 * @param $ts Int the time in date('YmdHis') format
535 * @param $tz Mixed: adjust the time by this amount (default false, mean we
536 * get user timecorrection setting)
537 * @return int
538 */
539 function userAdjust( $ts, $tz = false ) {
540 global $wgUser, $wgLocalTZoffset;
541
542 if ( $tz === false ) {
543 $tz = $wgUser->getOption( 'timecorrection' );
544 }
545
546 $data = explode( '|', $tz, 3 );
547
548 if ( $data[0] == 'ZoneInfo' ) {
549 if ( function_exists( 'timezone_open' ) && @timezone_open( $data[2] ) !== false ) {
550 $date = date_create( $ts, timezone_open( 'UTC' ) );
551 date_timezone_set( $date, timezone_open( $data[2] ) );
552 $date = date_format( $date, 'YmdHis' );
553 return $date;
554 }
555 # Unrecognized timezone, default to 'Offset' with the stored offset.
556 $data[0] = 'Offset';
557 }
558
559 $minDiff = 0;
560 if ( $data[0] == 'System' || $tz == '' ) {
561 # Global offset in minutes.
562 if( isset($wgLocalTZoffset) ) $minDiff = $wgLocalTZoffset;
563 } else if ( $data[0] == 'Offset' ) {
564 $minDiff = intval( $data[1] );
565 } else {
566 $data = explode( ':', $tz );
567 if( count( $data ) == 2 ) {
568 $data[0] = intval( $data[0] );
569 $data[1] = intval( $data[1] );
570 $minDiff = abs( $data[0] ) * 60 + $data[1];
571 if ( $data[0] < 0 ) $minDiff = -$minDiff;
572 } else {
573 $minDiff = intval( $data[0] ) * 60;
574 }
575 }
576
577 # No difference ? Return time unchanged
578 if ( 0 == $minDiff ) return $ts;
579
580 wfSuppressWarnings(); // E_STRICT system time bitching
581 # Generate an adjusted date; take advantage of the fact that mktime
582 # will normalize out-of-range values so we don't have to split $minDiff
583 # into hours and minutes.
584 $t = mktime( (
585 (int)substr( $ts, 8, 2) ), # Hours
586 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
587 (int)substr( $ts, 12, 2 ), # Seconds
588 (int)substr( $ts, 4, 2 ), # Month
589 (int)substr( $ts, 6, 2 ), # Day
590 (int)substr( $ts, 0, 4 ) ); #Year
591
592 $date = date( 'YmdHis', $t );
593 wfRestoreWarnings();
594
595 return $date;
596 }
597
598 /**
599 * This is a workalike of PHP's date() function, but with better
600 * internationalisation, a reduced set of format characters, and a better
601 * escaping format.
602 *
603 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
604 * PHP manual for definitions. "o" format character is supported since
605 * PHP 5.1.0, previous versions return literal o.
606 * There are a number of extensions, which start with "x":
607 *
608 * xn Do not translate digits of the next numeric format character
609 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
610 * xr Use roman numerals for the next numeric format character
611 * xh Use hebrew numerals for the next numeric format character
612 * xx Literal x
613 * xg Genitive month name
614 *
615 * xij j (day number) in Iranian calendar
616 * xiF F (month name) in Iranian calendar
617 * xin n (month number) in Iranian calendar
618 * xiY Y (full year) in Iranian calendar
619 *
620 * xjj j (day number) in Hebrew calendar
621 * xjF F (month name) in Hebrew calendar
622 * xjt t (days in month) in Hebrew calendar
623 * xjx xg (genitive month name) in Hebrew calendar
624 * xjn n (month number) in Hebrew calendar
625 * xjY Y (full year) in Hebrew calendar
626 *
627 * xmj j (day number) in Hijri calendar
628 * xmF F (month name) in Hijri calendar
629 * xmn n (month number) in Hijri calendar
630 * xmY Y (full year) in Hijri calendar
631 *
632 * xkY Y (full year) in Thai solar calendar. Months and days are
633 * identical to the Gregorian calendar
634 * xoY Y (full year) in Minguo calendar or Juche year.
635 * Months and days are identical to the
636 * Gregorian calendar
637 * xtY Y (full year) in Japanese nengo. Months and days are
638 * identical to the Gregorian calendar
639 *
640 * Characters enclosed in double quotes will be considered literal (with
641 * the quotes themselves removed). Unmatched quotes will be considered
642 * literal quotes. Example:
643 *
644 * "The month is" F => The month is January
645 * i's" => 20'11"
646 *
647 * Backslash escaping is also supported.
648 *
649 * Input timestamp is assumed to be pre-normalized to the desired local
650 * time zone, if any.
651 *
652 * @param $format String
653 * @param $ts String: 14-character timestamp
654 * YYYYMMDDHHMMSS
655 * 01234567890123
656 * @todo emulation of "o" format character for PHP pre 5.1.0
657 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
658 */
659 function sprintfDate( $format, $ts ) {
660 $s = '';
661 $raw = false;
662 $roman = false;
663 $hebrewNum = false;
664 $unix = false;
665 $rawToggle = false;
666 $iranian = false;
667 $hebrew = false;
668 $hijri = false;
669 $thai = false;
670 $minguo = false;
671 $tenno = false;
672 for ( $p = 0; $p < strlen( $format ); $p++ ) {
673 $num = false;
674 $code = $format[$p];
675 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
676 $code .= $format[++$p];
677 }
678
679 if ( ( $code === 'xi' || $code == 'xj' || $code == 'xk' || $code == 'xm' || $code == 'xo' || $code == 'xt' ) && $p < strlen( $format ) - 1 ) {
680 $code .= $format[++$p];
681 }
682
683 switch ( $code ) {
684 case 'xx':
685 $s .= 'x';
686 break;
687 case 'xn':
688 $raw = true;
689 break;
690 case 'xN':
691 $rawToggle = !$rawToggle;
692 break;
693 case 'xr':
694 $roman = true;
695 break;
696 case 'xh':
697 $hebrewNum = true;
698 break;
699 case 'xg':
700 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
701 break;
702 case 'xjx':
703 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
704 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
705 break;
706 case 'd':
707 $num = substr( $ts, 6, 2 );
708 break;
709 case 'D':
710 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
711 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) + 1 );
712 break;
713 case 'j':
714 $num = intval( substr( $ts, 6, 2 ) );
715 break;
716 case 'xij':
717 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
718 $num = $iranian[2];
719 break;
720 case 'xmj':
721 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
722 $num = $hijri[2];
723 break;
724 case 'xjj':
725 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
726 $num = $hebrew[2];
727 break;
728 case 'l':
729 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
730 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) + 1 );
731 break;
732 case 'N':
733 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
734 $w = gmdate( 'w', $unix );
735 $num = $w ? $w : 7;
736 break;
737 case 'w':
738 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
739 $num = gmdate( 'w', $unix );
740 break;
741 case 'z':
742 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
743 $num = gmdate( 'z', $unix );
744 break;
745 case 'W':
746 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
747 $num = gmdate( 'W', $unix );
748 break;
749 case 'F':
750 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
751 break;
752 case 'xiF':
753 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
754 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
755 break;
756 case 'xmF':
757 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
758 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
759 break;
760 case 'xjF':
761 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
762 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
763 break;
764 case 'm':
765 $num = substr( $ts, 4, 2 );
766 break;
767 case 'M':
768 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
769 break;
770 case 'n':
771 $num = intval( substr( $ts, 4, 2 ) );
772 break;
773 case 'xin':
774 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
775 $num = $iranian[1];
776 break;
777 case 'xmn':
778 if ( !$hijri ) $hijri = self::tsToHijri ( $ts );
779 $num = $hijri[1];
780 break;
781 case 'xjn':
782 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
783 $num = $hebrew[1];
784 break;
785 case 't':
786 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
787 $num = gmdate( 't', $unix );
788 break;
789 case 'xjt':
790 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
791 $num = $hebrew[3];
792 break;
793 case 'L':
794 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
795 $num = gmdate( 'L', $unix );
796 break;
797 # 'o' is supported since PHP 5.1.0
798 # return literal if not supported
799 # TODO: emulation for pre 5.1.0 versions
800 case 'o':
801 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
802 if ( version_compare(PHP_VERSION, '5.1.0') === 1 )
803 $num = date( 'o', $unix );
804 else
805 $s .= 'o';
806 break;
807 case 'Y':
808 $num = substr( $ts, 0, 4 );
809 break;
810 case 'xiY':
811 if ( !$iranian ) $iranian = self::tsToIranian( $ts );
812 $num = $iranian[0];
813 break;
814 case 'xmY':
815 if ( !$hijri ) $hijri = self::tsToHijri( $ts );
816 $num = $hijri[0];
817 break;
818 case 'xjY':
819 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
820 $num = $hebrew[0];
821 break;
822 case 'xkY':
823 if ( !$thai ) $thai = self::tsToYear( $ts, 'thai' );
824 $num = $thai[0];
825 break;
826 case 'xoY':
827 if ( !$minguo ) $minguo = self::tsToYear( $ts, 'minguo' );
828 $num = $minguo[0];
829 break;
830 case 'xtY':
831 if ( !$tenno ) $tenno = self::tsToYear( $ts, 'tenno' );
832 $num = $tenno[0];
833 break;
834 case 'y':
835 $num = substr( $ts, 2, 2 );
836 break;
837 case 'a':
838 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
839 break;
840 case 'A':
841 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
842 break;
843 case 'g':
844 $h = substr( $ts, 8, 2 );
845 $num = $h % 12 ? $h % 12 : 12;
846 break;
847 case 'G':
848 $num = intval( substr( $ts, 8, 2 ) );
849 break;
850 case 'h':
851 $h = substr( $ts, 8, 2 );
852 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
853 break;
854 case 'H':
855 $num = substr( $ts, 8, 2 );
856 break;
857 case 'i':
858 $num = substr( $ts, 10, 2 );
859 break;
860 case 's':
861 $num = substr( $ts, 12, 2 );
862 break;
863 case 'c':
864 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
865 $s .= gmdate( 'c', $unix );
866 break;
867 case 'r':
868 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
869 $s .= gmdate( 'r', $unix );
870 break;
871 case 'U':
872 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
873 $num = $unix;
874 break;
875 case '\\':
876 # Backslash escaping
877 if ( $p < strlen( $format ) - 1 ) {
878 $s .= $format[++$p];
879 } else {
880 $s .= '\\';
881 }
882 break;
883 case '"':
884 # Quoted literal
885 if ( $p < strlen( $format ) - 1 ) {
886 $endQuote = strpos( $format, '"', $p + 1 );
887 if ( $endQuote === false ) {
888 # No terminating quote, assume literal "
889 $s .= '"';
890 } else {
891 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
892 $p = $endQuote;
893 }
894 } else {
895 # Quote at end of string, assume literal "
896 $s .= '"';
897 }
898 break;
899 default:
900 $s .= $format[$p];
901 }
902 if ( $num !== false ) {
903 if ( $rawToggle || $raw ) {
904 $s .= $num;
905 $raw = false;
906 } elseif ( $roman ) {
907 $s .= self::romanNumeral( $num );
908 $roman = false;
909 } elseif( $hebrewNum ) {
910 $s .= self::hebrewNumeral( $num );
911 $hebrewNum = false;
912 } else {
913 $s .= $this->formatNum( $num, true );
914 }
915 $num = false;
916 }
917 }
918 return $s;
919 }
920
921 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
922 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
923 /**
924 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
925 * Gregorian dates to Iranian dates. Originally written in C, it
926 * is released under the terms of GNU Lesser General Public
927 * License. Conversion to PHP was performed by Niklas Laxström.
928 *
929 * Link: http://www.farsiweb.info/jalali/jalali.c
930 */
931 private static function tsToIranian( $ts ) {
932 $gy = substr( $ts, 0, 4 ) -1600;
933 $gm = substr( $ts, 4, 2 ) -1;
934 $gd = substr( $ts, 6, 2 ) -1;
935
936 # Days passed from the beginning (including leap years)
937 $gDayNo = 365*$gy
938 + floor(($gy+3) / 4)
939 - floor(($gy+99) / 100)
940 + floor(($gy+399) / 400);
941
942
943 // Add days of the past months of this year
944 for( $i = 0; $i < $gm; $i++ ) {
945 $gDayNo += self::$GREG_DAYS[$i];
946 }
947
948 // Leap years
949 if ( $gm > 1 && (($gy%4===0 && $gy%100!==0 || ($gy%400==0)))) {
950 $gDayNo++;
951 }
952
953 // Days passed in current month
954 $gDayNo += $gd;
955
956 $jDayNo = $gDayNo - 79;
957
958 $jNp = floor($jDayNo / 12053);
959 $jDayNo %= 12053;
960
961 $jy = 979 + 33*$jNp + 4*floor($jDayNo/1461);
962 $jDayNo %= 1461;
963
964 if ( $jDayNo >= 366 ) {
965 $jy += floor(($jDayNo-1)/365);
966 $jDayNo = floor(($jDayNo-1)%365);
967 }
968
969 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
970 $jDayNo -= self::$IRANIAN_DAYS[$i];
971 }
972
973 $jm= $i+1;
974 $jd= $jDayNo+1;
975
976 return array($jy, $jm, $jd);
977 }
978 /**
979 * Converting Gregorian dates to Hijri dates.
980 *
981 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
982 *
983 * @link http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
984 */
985 private static function tsToHijri ( $ts ) {
986 $year = substr( $ts, 0, 4 );
987 $month = substr( $ts, 4, 2 );
988 $day = substr( $ts, 6, 2 );
989
990 $zyr = $year;
991 $zd=$day;
992 $zm=$month;
993 $zy=$zyr;
994
995
996
997 if (($zy>1582)||(($zy==1582)&&($zm>10))||(($zy==1582)&&($zm==10)&&($zd>14)))
998 {
999
1000
1001 $zjd=(int)((1461*($zy + 4800 + (int)( ($zm-14) /12) ))/4) + (int)((367*($zm-2-12*((int)(($zm-14)/12))))/12)-(int)((3*(int)(( ($zy+4900+(int)(($zm-14)/12))/100)))/4)+$zd-32075;
1002 }
1003 else
1004 {
1005 $zjd = 367*$zy-(int)((7*($zy+5001+(int)(($zm-9)/7)))/4)+(int)((275*$zm)/9)+$zd+1729777;
1006 }
1007
1008 $zl=$zjd-1948440+10632;
1009 $zn=(int)(($zl-1)/10631);
1010 $zl=$zl-10631*$zn+354;
1011 $zj=((int)((10985-$zl)/5316))*((int)((50*$zl)/17719))+((int)($zl/5670))*((int)((43*$zl)/15238));
1012 $zl=$zl-((int)((30-$zj)/15))*((int)((17719*$zj)/50))-((int)($zj/16))*((int)((15238*$zj)/43))+29;
1013 $zm=(int)((24*$zl)/709);
1014 $zd=$zl-(int)((709*$zm)/24);
1015 $zy=30*$zn+$zj-30;
1016
1017 return array ($zy, $zm, $zd);
1018 }
1019
1020 /**
1021 * Converting Gregorian dates to Hebrew dates.
1022 *
1023 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1024 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1025 * to translate the relevant functions into PHP and release them under
1026 * GNU GPL.
1027 *
1028 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1029 * and Adar II is 14. In a non-leap year, Adar is 6.
1030 */
1031 private static function tsToHebrew( $ts ) {
1032 # Parse date
1033 $year = substr( $ts, 0, 4 );
1034 $month = substr( $ts, 4, 2 );
1035 $day = substr( $ts, 6, 2 );
1036
1037 # Calculate Hebrew year
1038 $hebrewYear = $year + 3760;
1039
1040 # Month number when September = 1, August = 12
1041 $month += 4;
1042 if( $month > 12 ) {
1043 # Next year
1044 $month -= 12;
1045 $year++;
1046 $hebrewYear++;
1047 }
1048
1049 # Calculate day of year from 1 September
1050 $dayOfYear = $day;
1051 for( $i = 1; $i < $month; $i++ ) {
1052 if( $i == 6 ) {
1053 # February
1054 $dayOfYear += 28;
1055 # Check if the year is leap
1056 if( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1057 $dayOfYear++;
1058 }
1059 } elseif( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1060 $dayOfYear += 30;
1061 } else {
1062 $dayOfYear += 31;
1063 }
1064 }
1065
1066 # Calculate the start of the Hebrew year
1067 $start = self::hebrewYearStart( $hebrewYear );
1068
1069 # Calculate next year's start
1070 if( $dayOfYear <= $start ) {
1071 # Day is before the start of the year - it is the previous year
1072 # Next year's start
1073 $nextStart = $start;
1074 # Previous year
1075 $year--;
1076 $hebrewYear--;
1077 # Add days since previous year's 1 September
1078 $dayOfYear += 365;
1079 if( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1080 # Leap year
1081 $dayOfYear++;
1082 }
1083 # Start of the new (previous) year
1084 $start = self::hebrewYearStart( $hebrewYear );
1085 } else {
1086 # Next year's start
1087 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1088 }
1089
1090 # Calculate Hebrew day of year
1091 $hebrewDayOfYear = $dayOfYear - $start;
1092
1093 # Difference between year's days
1094 $diff = $nextStart - $start;
1095 # Add 12 (or 13 for leap years) days to ignore the difference between
1096 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1097 # difference is only about the year type
1098 if( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1099 $diff += 13;
1100 } else {
1101 $diff += 12;
1102 }
1103
1104 # Check the year pattern, and is leap year
1105 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1106 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1107 # and non-leap years
1108 $yearPattern = $diff % 30;
1109 # Check if leap year
1110 $isLeap = $diff >= 30;
1111
1112 # Calculate day in the month from number of day in the Hebrew year
1113 # Don't check Adar - if the day is not in Adar, we will stop before;
1114 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1115 $hebrewDay = $hebrewDayOfYear;
1116 $hebrewMonth = 1;
1117 $days = 0;
1118 while( $hebrewMonth <= 12 ) {
1119 # Calculate days in this month
1120 if( $isLeap && $hebrewMonth == 6 ) {
1121 # Adar in a leap year
1122 if( $isLeap ) {
1123 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1124 $days = 30;
1125 if( $hebrewDay <= $days ) {
1126 # Day in Adar I
1127 $hebrewMonth = 13;
1128 } else {
1129 # Subtract the days of Adar I
1130 $hebrewDay -= $days;
1131 # Try Adar II
1132 $days = 29;
1133 if( $hebrewDay <= $days ) {
1134 # Day in Adar II
1135 $hebrewMonth = 14;
1136 }
1137 }
1138 }
1139 } elseif( $hebrewMonth == 2 && $yearPattern == 2 ) {
1140 # Cheshvan in a complete year (otherwise as the rule below)
1141 $days = 30;
1142 } elseif( $hebrewMonth == 3 && $yearPattern == 0 ) {
1143 # Kislev in an incomplete year (otherwise as the rule below)
1144 $days = 29;
1145 } else {
1146 # Odd months have 30 days, even have 29
1147 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1148 }
1149 if( $hebrewDay <= $days ) {
1150 # In the current month
1151 break;
1152 } else {
1153 # Subtract the days of the current month
1154 $hebrewDay -= $days;
1155 # Try in the next month
1156 $hebrewMonth++;
1157 }
1158 }
1159
1160 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1161 }
1162
1163 /**
1164 * This calculates the Hebrew year start, as days since 1 September.
1165 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1166 * Used for Hebrew date.
1167 */
1168 private static function hebrewYearStart( $year ) {
1169 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1170 $b = intval( ( $year - 1 ) % 4 );
1171 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1172 if( $m < 0 ) {
1173 $m--;
1174 }
1175 $Mar = intval( $m );
1176 if( $m < 0 ) {
1177 $m++;
1178 }
1179 $m -= $Mar;
1180
1181 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7);
1182 if( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1183 $Mar++;
1184 } else if( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1185 $Mar += 2;
1186 } else if( $c == 2 || $c == 4 || $c == 6 ) {
1187 $Mar++;
1188 }
1189
1190 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1191 return $Mar;
1192 }
1193
1194 /**
1195 * Algorithm to convert Gregorian dates to Thai solar dates,
1196 * Minguo dates or Minguo dates.
1197 *
1198 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1199 * http://en.wikipedia.org/wiki/Minguo_calendar
1200 * http://en.wikipedia.org/wiki/Japanese_era_name
1201 *
1202 * @param $ts String: 14-character timestamp, calender name
1203 * @return array converted year, month, day
1204 */
1205 private static function tsToYear( $ts, $cName ) {
1206 $gy = substr( $ts, 0, 4 );
1207 $gm = substr( $ts, 4, 2 );
1208 $gd = substr( $ts, 6, 2 );
1209
1210 if (!strcmp($cName,'thai')) {
1211 # Thai solar dates
1212 # Add 543 years to the Gregorian calendar
1213 # Months and days are identical
1214 $gy_offset = $gy + 543;
1215 } else if ((!strcmp($cName,'minguo')) || !strcmp($cName,'juche')) {
1216 # Minguo dates
1217 # Deduct 1911 years from the Gregorian calendar
1218 # Months and days are identical
1219 $gy_offset = $gy - 1911;
1220 } else if (!strcmp($cName,'tenno')) {
1221 # Nengō dates up to Meiji period
1222 # Deduct years from the Gregorian calendar
1223 # depending on the nengo periods
1224 # Months and days are identical
1225 if (($gy < 1912) || (($gy == 1912) && ($gm < 7)) || (($gy == 1912) && ($gm == 7) && ($gd < 31))) {
1226 # Meiji period
1227 $gy_gannen = $gy - 1868 + 1;
1228 $gy_offset = $gy_gannen;
1229 if ($gy_gannen == 1)
1230 $gy_offset = '元';
1231 $gy_offset = '明治'.$gy_offset;
1232 } else if ((($gy == 1912) && ($gm == 7) && ($gd == 31)) || (($gy == 1912) && ($gm >= 8)) || (($gy > 1912) && ($gy < 1926)) || (($gy == 1926) && ($gm < 12)) || (($gy == 1926) && ($gm == 12) && ($gd < 26))) {
1233 # Taishō period
1234 $gy_gannen = $gy - 1912 + 1;
1235 $gy_offset = $gy_gannen;
1236 if ($gy_gannen == 1)
1237 $gy_offset = '元';
1238 $gy_offset = '大正'.$gy_offset;
1239 } else if ((($gy == 1926) && ($gm == 12) && ($gd >= 26)) || (($gy > 1926) && ($gy < 1989)) || (($gy == 1989) && ($gm == 1) && ($gd < 8))) {
1240 # Shōwa period
1241 $gy_gannen = $gy - 1926 + 1;
1242 $gy_offset = $gy_gannen;
1243 if ($gy_gannen == 1)
1244 $gy_offset = '元';
1245 $gy_offset = '昭和'.$gy_offset;
1246 } else {
1247 # Heisei period
1248 $gy_gannen = $gy - 1989 + 1;
1249 $gy_offset = $gy_gannen;
1250 if ($gy_gannen == 1)
1251 $gy_offset = '元';
1252 $gy_offset = '平成'.$gy_offset;
1253 }
1254 } else {
1255 $gy_offset = $gy;
1256 }
1257
1258 return array( $gy_offset, $gm, $gd );
1259 }
1260
1261 /**
1262 * Roman number formatting up to 3000
1263 */
1264 static function romanNumeral( $num ) {
1265 static $table = array(
1266 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1267 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1268 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1269 array( '', 'M', 'MM', 'MMM' )
1270 );
1271
1272 $num = intval( $num );
1273 if ( $num > 3000 || $num <= 0 ) {
1274 return $num;
1275 }
1276
1277 $s = '';
1278 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1279 if ( $num >= $pow10 ) {
1280 $s .= $table[$i][floor($num / $pow10)];
1281 }
1282 $num = $num % $pow10;
1283 }
1284 return $s;
1285 }
1286
1287 /**
1288 * Hebrew Gematria number formatting up to 9999
1289 */
1290 static function hebrewNumeral( $num ) {
1291 static $table = array(
1292 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1293 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1294 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1295 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1296 );
1297
1298 $num = intval( $num );
1299 if ( $num > 9999 || $num <= 0 ) {
1300 return $num;
1301 }
1302
1303 $s = '';
1304 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1305 if ( $num >= $pow10 ) {
1306 if ( $num == 15 || $num == 16 ) {
1307 $s .= $table[0][9] . $table[0][$num - 9];
1308 $num = 0;
1309 } else {
1310 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1311 if( $pow10 == 1000 ) {
1312 $s .= "'";
1313 }
1314 }
1315 }
1316 $num = $num % $pow10;
1317 }
1318 if( strlen( $s ) == 2 ) {
1319 $str = $s . "'";
1320 } else {
1321 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1322 $str .= substr( $s, strlen( $s ) - 2, 2 );
1323 }
1324 $start = substr( $str, 0, strlen( $str ) - 2 );
1325 $end = substr( $str, strlen( $str ) - 2 );
1326 switch( $end ) {
1327 case 'כ':
1328 $str = $start . 'ך';
1329 break;
1330 case 'מ':
1331 $str = $start . 'ם';
1332 break;
1333 case 'נ':
1334 $str = $start . 'ן';
1335 break;
1336 case 'פ':
1337 $str = $start . 'ף';
1338 break;
1339 case 'צ':
1340 $str = $start . 'ץ';
1341 break;
1342 }
1343 return $str;
1344 }
1345
1346 /**
1347 * This is meant to be used by time(), date(), and timeanddate() to get
1348 * the date preference they're supposed to use, it should be used in
1349 * all children.
1350 *
1351 *<code>
1352 * function timeanddate([...], $format = true) {
1353 * $datePreference = $this->dateFormat($format);
1354 * [...]
1355 * }
1356 *</code>
1357 *
1358 * @param $usePrefs Mixed: if true, the user's preference is used
1359 * if false, the site/language default is used
1360 * if int/string, assumed to be a format.
1361 * @return string
1362 */
1363 function dateFormat( $usePrefs = true ) {
1364 global $wgUser;
1365
1366 if( is_bool( $usePrefs ) ) {
1367 if( $usePrefs ) {
1368 $datePreference = $wgUser->getDatePreference();
1369 } else {
1370 $options = User::getDefaultOptions();
1371 $datePreference = (string)$options['date'];
1372 }
1373 } else {
1374 $datePreference = (string)$usePrefs;
1375 }
1376
1377 // return int
1378 if( $datePreference == '' ) {
1379 return 'default';
1380 }
1381
1382 return $datePreference;
1383 }
1384
1385 /**
1386 * Get a format string for a given type and preference
1387 * @param $type May be date, time or both
1388 * @param $pref The format name as it appears in Messages*.php
1389 */
1390 function getDateFormatString( $type, $pref ) {
1391 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
1392 if ( $pref == 'default' ) {
1393 $pref = $this->getDefaultDateFormat();
1394 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
1395 } else {
1396 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
1397 if ( is_null( $df ) ) {
1398 $pref = $this->getDefaultDateFormat();
1399 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
1400 }
1401 }
1402 $this->dateFormatStrings[$type][$pref] = $df;
1403 }
1404 return $this->dateFormatStrings[$type][$pref];
1405 }
1406
1407 /**
1408 * @param $ts Mixed: the time format which needs to be turned into a
1409 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1410 * @param $adj Bool: whether to adjust the time output according to the
1411 * user configured offset ($timecorrection)
1412 * @param $format Mixed: true to use user's date format preference
1413 * @param $timecorrection String: the time offset as returned by
1414 * validateTimeZone() in Special:Preferences
1415 * @return string
1416 */
1417 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1418 if ( $adj ) {
1419 $ts = $this->userAdjust( $ts, $timecorrection );
1420 }
1421 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
1422 return $this->sprintfDate( $df, $ts );
1423 }
1424
1425 /**
1426 * @param $ts Mixed: the time format which needs to be turned into a
1427 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1428 * @param $adj Bool: whether to adjust the time output according to the
1429 * user configured offset ($timecorrection)
1430 * @param $format Mixed: true to use user's date format preference
1431 * @param $timecorrection String: the time offset as returned by
1432 * validateTimeZone() in Special:Preferences
1433 * @return string
1434 */
1435 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1436 if ( $adj ) {
1437 $ts = $this->userAdjust( $ts, $timecorrection );
1438 }
1439 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
1440 return $this->sprintfDate( $df, $ts );
1441 }
1442
1443 /**
1444 * @param $ts Mixed: the time format which needs to be turned into a
1445 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1446 * @param $adj Bool: whether to adjust the time output according to the
1447 * user configured offset ($timecorrection)
1448 * @param $format Mixed: what format to return, if it's false output the
1449 * default one (default true)
1450 * @param $timecorrection String: the time offset as returned by
1451 * validateTimeZone() in Special:Preferences
1452 * @return string
1453 */
1454 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false) {
1455 $ts = wfTimestamp( TS_MW, $ts );
1456 if ( $adj ) {
1457 $ts = $this->userAdjust( $ts, $timecorrection );
1458 }
1459 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
1460 return $this->sprintfDate( $df, $ts );
1461 }
1462
1463 function getMessage( $key ) {
1464 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
1465 }
1466
1467 function getAllMessages() {
1468 return self::$dataCache->getItem( $this->mCode, 'messages' );
1469 }
1470
1471 function iconv( $in, $out, $string ) {
1472 # This is a wrapper for iconv in all languages except esperanto,
1473 # which does some nasty x-conversions beforehand
1474
1475 # Even with //IGNORE iconv can whine about illegal characters in
1476 # *input* string. We just ignore those too.
1477 # REF: http://bugs.php.net/bug.php?id=37166
1478 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
1479 wfSuppressWarnings();
1480 $text = iconv( $in, $out . '//IGNORE', $string );
1481 wfRestoreWarnings();
1482 return $text;
1483 }
1484
1485 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
1486 function ucwordbreaksCallbackAscii($matches){
1487 return $this->ucfirst($matches[1]);
1488 }
1489
1490 function ucwordbreaksCallbackMB($matches){
1491 return mb_strtoupper($matches[0]);
1492 }
1493
1494 function ucCallback($matches){
1495 list( $wikiUpperChars ) = self::getCaseMaps();
1496 return strtr( $matches[1], $wikiUpperChars );
1497 }
1498
1499 function lcCallback($matches){
1500 list( , $wikiLowerChars ) = self::getCaseMaps();
1501 return strtr( $matches[1], $wikiLowerChars );
1502 }
1503
1504 function ucwordsCallbackMB($matches){
1505 return mb_strtoupper($matches[0]);
1506 }
1507
1508 function ucwordsCallbackWiki($matches){
1509 list( $wikiUpperChars ) = self::getCaseMaps();
1510 return strtr( $matches[0], $wikiUpperChars );
1511 }
1512
1513 function ucfirst( $str ) {
1514 $o = ord( $str );
1515 if ( $o < 96 ) {
1516 return $str;
1517 } elseif ( $o < 128 ) {
1518 return ucfirst($str);
1519 } else {
1520 // fall back to more complex logic in case of multibyte strings
1521 return self::uc($str,true);
1522 }
1523 }
1524
1525 function uc( $str, $first = false ) {
1526 if ( function_exists( 'mb_strtoupper' ) ) {
1527 if ( $first ) {
1528 if ( self::isMultibyte( $str ) ) {
1529 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1530 } else {
1531 return ucfirst( $str );
1532 }
1533 } else {
1534 return self::isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
1535 }
1536 } else {
1537 if ( self::isMultibyte( $str ) ) {
1538 list( $wikiUpperChars ) = $this->getCaseMaps();
1539 $x = $first ? '^' : '';
1540 return preg_replace_callback(
1541 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1542 array($this,"ucCallback"),
1543 $str
1544 );
1545 } else {
1546 return $first ? ucfirst( $str ) : strtoupper( $str );
1547 }
1548 }
1549 }
1550
1551 function lcfirst( $str ) {
1552 $o = ord( $str );
1553 if ( !$o ) {
1554 return strval( $str );
1555 } elseif ( $o >= 128 ) {
1556 return self::lc( $str, true );
1557 } elseif ( $o > 96 ) {
1558 return $str;
1559 } else {
1560 $str[0] = strtolower( $str[0] );
1561 return $str;
1562 }
1563 }
1564
1565 function lc( $str, $first = false ) {
1566 if ( function_exists( 'mb_strtolower' ) )
1567 if ( $first )
1568 if ( self::isMultibyte( $str ) )
1569 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1570 else
1571 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
1572 else
1573 return self::isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
1574 else
1575 if ( self::isMultibyte( $str ) ) {
1576 list( , $wikiLowerChars ) = self::getCaseMaps();
1577 $x = $first ? '^' : '';
1578 return preg_replace_callback(
1579 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1580 array($this,"lcCallback"),
1581 $str
1582 );
1583 } else
1584 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
1585 }
1586
1587 function isMultibyte( $str ) {
1588 return (bool)preg_match( '/[\x80-\xff]/', $str );
1589 }
1590
1591 function ucwords($str) {
1592 if ( self::isMultibyte( $str ) ) {
1593 $str = self::lc($str);
1594
1595 // regexp to find first letter in each word (i.e. after each space)
1596 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
1597
1598 // function to use to capitalize a single char
1599 if ( function_exists( 'mb_strtoupper' ) )
1600 return preg_replace_callback(
1601 $replaceRegexp,
1602 array($this,"ucwordsCallbackMB"),
1603 $str
1604 );
1605 else
1606 return preg_replace_callback(
1607 $replaceRegexp,
1608 array($this,"ucwordsCallbackWiki"),
1609 $str
1610 );
1611 }
1612 else
1613 return ucwords( strtolower( $str ) );
1614 }
1615
1616 # capitalize words at word breaks
1617 function ucwordbreaks($str){
1618 if (self::isMultibyte( $str ) ) {
1619 $str = self::lc($str);
1620
1621 // since \b doesn't work for UTF-8, we explicitely define word break chars
1622 $breaks= "[ \-\(\)\}\{\.,\?!]";
1623
1624 // find first letter after word break
1625 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
1626
1627 if ( function_exists( 'mb_strtoupper' ) )
1628 return preg_replace_callback(
1629 $replaceRegexp,
1630 array($this,"ucwordbreaksCallbackMB"),
1631 $str
1632 );
1633 else
1634 return preg_replace_callback(
1635 $replaceRegexp,
1636 array($this,"ucwordsCallbackWiki"),
1637 $str
1638 );
1639 }
1640 else
1641 return preg_replace_callback(
1642 '/\b([\w\x80-\xff]+)\b/',
1643 array($this,"ucwordbreaksCallbackAscii"),
1644 $str );
1645 }
1646
1647 /**
1648 * Return a case-folded representation of $s
1649 *
1650 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
1651 * and $s2 are the same except for the case of their characters. It is not
1652 * necessary for the value returned to make sense when displayed.
1653 *
1654 * Do *not* perform any other normalisation in this function. If a caller
1655 * uses this function when it should be using a more general normalisation
1656 * function, then fix the caller.
1657 */
1658 function caseFold( $s ) {
1659 return $this->uc( $s );
1660 }
1661
1662 function checkTitleEncoding( $s ) {
1663 if( is_array( $s ) ) {
1664 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
1665 }
1666 # Check for non-UTF-8 URLs
1667 $ishigh = preg_match( '/[\x80-\xff]/', $s);
1668 if(!$ishigh) return $s;
1669
1670 $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1671 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
1672 if( $isutf8 ) return $s;
1673
1674 return $this->iconv( $this->fallback8bitEncoding(), "utf-8", $s );
1675 }
1676
1677 function fallback8bitEncoding() {
1678 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
1679 }
1680
1681 /**
1682 * Most writing systems use whitespace to break up words.
1683 * Some languages such as Chinese don't conventionally do this,
1684 * which requires special handling when breaking up words for
1685 * searching etc.
1686 */
1687 function hasWordBreaks() {
1688 return true;
1689 }
1690
1691 /**
1692 * Some languages have special punctuation to strip out
1693 * or characters which need to be converted for MySQL's
1694 * indexing to grok it correctly. Make such changes here.
1695 *
1696 * @param $string String
1697 * @return String
1698 */
1699 function stripForSearch( $string ) {
1700 global $wgDBtype, $wgSearchType;
1701 if ( $wgDBtype != 'mysql' || $wgSearchType == 'LuceneSearch' ) {
1702 return $string;
1703 }
1704
1705 wfProfileIn( __METHOD__ );
1706
1707 // MySQL fulltext index doesn't grok utf-8, so we
1708 // need to fold cases and convert to hex
1709 $out = preg_replace_callback(
1710 "/([\\xc0-\\xff][\\x80-\\xbf]*)/",
1711 array( $this, 'stripForSearchCallback' ),
1712 $this->lc( $string ) );
1713
1714 // And to add insult to injury, the default indexing
1715 // ignores short words... Pad them so we can pass them
1716 // through without reconfiguring the server...
1717 $minLength = $this->minSearchLength();
1718 if( $minLength > 1 ) {
1719 $n = $minLength-1;
1720 $out = preg_replace(
1721 "/\b(\w{1,$n})\b/",
1722 "$1u800",
1723 $out );
1724 }
1725
1726 // Periods within things like hostnames and IP addresses
1727 // are also important -- we want a search for "example.com"
1728 // or "192.168.1.1" to work sanely.
1729 //
1730 // MySQL's search seems to ignore them, so you'd match on
1731 // "example.wikipedia.com" and "192.168.83.1" as well.
1732 $out = preg_replace(
1733 "/(\w)\.(\w|\*)/u",
1734 "$1u82e$2",
1735 $out );
1736
1737 wfProfileOut( __METHOD__ );
1738 return $out;
1739 }
1740
1741 /**
1742 * Armor a case-folded UTF-8 string to get through MySQL's
1743 * fulltext search without being mucked up by funny charset
1744 * settings or anything else of the sort.
1745 */
1746 protected function stripForSearchCallback( $matches ) {
1747 return 'u8' . bin2hex( $matches[1] );
1748 }
1749
1750 /**
1751 * Check MySQL server's ft_min_word_len setting so we know
1752 * if we need to pad short words...
1753 */
1754 protected function minSearchLength() {
1755 if( is_null( $this->minSearchLength ) ) {
1756 $sql = "show global variables like 'ft\\_min\\_word\\_len'";
1757 $dbr = wfGetDB( DB_SLAVE );
1758 $result = $dbr->query( $sql );
1759 $row = $result->fetchObject();
1760 $result->free();
1761
1762 if( $row && $row->Variable_name == 'ft_min_word_len' ) {
1763 $this->minSearchLength = intval( $row->Value );
1764 } else {
1765 $this->minSearchLength = 0;
1766 }
1767 }
1768 return $this->minSearchLength;
1769 }
1770
1771 function convertForSearchResult( $termsArray ) {
1772 # some languages, e.g. Chinese, need to do a conversion
1773 # in order for search results to be displayed correctly
1774 return $termsArray;
1775 }
1776
1777 /**
1778 * Get the first character of a string.
1779 *
1780 * @param $s string
1781 * @return string
1782 */
1783 function firstChar( $s ) {
1784 $matches = array();
1785 preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1786 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/', $s, $matches);
1787
1788 if ( isset( $matches[1] ) ) {
1789 if ( strlen( $matches[1] ) != 3 ) {
1790 return $matches[1];
1791 }
1792
1793 // Break down Hangul syllables to grab the first jamo
1794 $code = utf8ToCodepoint( $matches[1] );
1795 if ( $code < 0xac00 || 0xd7a4 <= $code) {
1796 return $matches[1];
1797 } elseif ( $code < 0xb098 ) {
1798 return "\xe3\x84\xb1";
1799 } elseif ( $code < 0xb2e4 ) {
1800 return "\xe3\x84\xb4";
1801 } elseif ( $code < 0xb77c ) {
1802 return "\xe3\x84\xb7";
1803 } elseif ( $code < 0xb9c8 ) {
1804 return "\xe3\x84\xb9";
1805 } elseif ( $code < 0xbc14 ) {
1806 return "\xe3\x85\x81";
1807 } elseif ( $code < 0xc0ac ) {
1808 return "\xe3\x85\x82";
1809 } elseif ( $code < 0xc544 ) {
1810 return "\xe3\x85\x85";
1811 } elseif ( $code < 0xc790 ) {
1812 return "\xe3\x85\x87";
1813 } elseif ( $code < 0xcc28 ) {
1814 return "\xe3\x85\x88";
1815 } elseif ( $code < 0xce74 ) {
1816 return "\xe3\x85\x8a";
1817 } elseif ( $code < 0xd0c0 ) {
1818 return "\xe3\x85\x8b";
1819 } elseif ( $code < 0xd30c ) {
1820 return "\xe3\x85\x8c";
1821 } elseif ( $code < 0xd558 ) {
1822 return "\xe3\x85\x8d";
1823 } else {
1824 return "\xe3\x85\x8e";
1825 }
1826 } else {
1827 return "";
1828 }
1829 }
1830
1831 function initEncoding() {
1832 # Some languages may have an alternate char encoding option
1833 # (Esperanto X-coding, Japanese furigana conversion, etc)
1834 # If this language is used as the primary content language,
1835 # an override to the defaults can be set here on startup.
1836 }
1837
1838 function recodeForEdit( $s ) {
1839 # For some languages we'll want to explicitly specify
1840 # which characters make it into the edit box raw
1841 # or are converted in some way or another.
1842 # Note that if wgOutputEncoding is different from
1843 # wgInputEncoding, this text will be further converted
1844 # to wgOutputEncoding.
1845 global $wgEditEncoding;
1846 if( $wgEditEncoding == '' or
1847 $wgEditEncoding == 'UTF-8' ) {
1848 return $s;
1849 } else {
1850 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
1851 }
1852 }
1853
1854 function recodeInput( $s ) {
1855 # Take the previous into account.
1856 global $wgEditEncoding;
1857 if($wgEditEncoding != "") {
1858 $enc = $wgEditEncoding;
1859 } else {
1860 $enc = 'UTF-8';
1861 }
1862 if( $enc == 'UTF-8' ) {
1863 return $s;
1864 } else {
1865 return $this->iconv( $enc, 'UTF-8', $s );
1866 }
1867 }
1868
1869 /**
1870 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
1871 * also cleans up certain backwards-compatible sequences, converting them
1872 * to the modern Unicode equivalent.
1873 *
1874 * This is language-specific for performance reasons only.
1875 */
1876 function normalize( $s ) {
1877 return UtfNormal::cleanUp( $s );
1878 }
1879
1880 /**
1881 * Transform a string using serialized data stored in the given file (which
1882 * must be in the serialized subdirectory of $IP). The file contains pairs
1883 * mapping source characters to destination characters.
1884 *
1885 * The data is cached in process memory. This will go faster if you have the
1886 * FastStringSearch extension.
1887 */
1888 function transformUsingPairFile( $file, $string ) {
1889 if ( !isset( $this->transformData[$file] ) ) {
1890 $data = wfGetPrecompiledData( $file );
1891 if ( $data === false ) {
1892 throw new MWException( __METHOD__.": The transformation file $file is missing" );
1893 }
1894 $this->transformData[$file] = new ReplacementArray( $data );
1895 }
1896 return $this->transformData[$file]->replace( $string );
1897 }
1898
1899 /**
1900 * For right-to-left language support
1901 *
1902 * @return bool
1903 */
1904 function isRTL() {
1905 return self::$dataCache->getItem( $this->mCode, 'rtl' );
1906 }
1907
1908 /**
1909 * Return the correct HTML 'dir' attribute value for this language.
1910 * @return String
1911 */
1912 function getDir() {
1913 return $this->isRTL() ? 'rtl' : 'ltr';
1914 }
1915
1916 /**
1917 * Return 'left' or 'right' as appropriate alignment for line-start
1918 * for this language's text direction.
1919 *
1920 * Should be equivalent to CSS3 'start' text-align value....
1921 *
1922 * @return String
1923 */
1924 function alignStart() {
1925 return $this->isRTL() ? 'right' : 'left';
1926 }
1927
1928 /**
1929 * Return 'right' or 'left' as appropriate alignment for line-end
1930 * for this language's text direction.
1931 *
1932 * Should be equivalent to CSS3 'end' text-align value....
1933 *
1934 * @return String
1935 */
1936 function alignEnd() {
1937 return $this->isRTL() ? 'left' : 'right';
1938 }
1939
1940 /**
1941 * A hidden direction mark (LRM or RLM), depending on the language direction
1942 *
1943 * @return string
1944 */
1945 function getDirMark() {
1946 return $this->isRTL() ? "\xE2\x80\x8F" : "\xE2\x80\x8E";
1947 }
1948
1949 function capitalizeAllNouns() {
1950 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
1951 }
1952
1953 /**
1954 * An arrow, depending on the language direction
1955 *
1956 * @return string
1957 */
1958 function getArrow() {
1959 return $this->isRTL() ? '←' : '→';
1960 }
1961
1962 /**
1963 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
1964 *
1965 * @return bool
1966 */
1967 function linkPrefixExtension() {
1968 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
1969 }
1970
1971 function getMagicWords() {
1972 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
1973 }
1974
1975 # Fill a MagicWord object with data from here
1976 function getMagic( $mw ) {
1977 if ( !$this->mMagicHookDone ) {
1978 $this->mMagicHookDone = true;
1979 wfProfileIn( 'LanguageGetMagic' );
1980 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
1981 wfProfileOut( 'LanguageGetMagic' );
1982 }
1983 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
1984 $rawEntry = $this->mMagicExtensions[$mw->mId];
1985 } else {
1986 $magicWords = $this->getMagicWords();
1987 if ( isset( $magicWords[$mw->mId] ) ) {
1988 $rawEntry = $magicWords[$mw->mId];
1989 } else {
1990 $rawEntry = false;
1991 }
1992 }
1993
1994 if( !is_array( $rawEntry ) ) {
1995 error_log( "\"$rawEntry\" is not a valid magic thingie for \"$mw->mId\"" );
1996 } else {
1997 $mw->mCaseSensitive = $rawEntry[0];
1998 $mw->mSynonyms = array_slice( $rawEntry, 1 );
1999 }
2000 }
2001
2002 /**
2003 * Add magic words to the extension array
2004 */
2005 function addMagicWordsByLang( $newWords ) {
2006 $code = $this->getCode();
2007 $fallbackChain = array();
2008 while ( $code && !in_array( $code, $fallbackChain ) ) {
2009 $fallbackChain[] = $code;
2010 $code = self::getFallbackFor( $code );
2011 }
2012 if ( !in_array( 'en', $fallbackChain ) ) {
2013 $fallbackChain[] = 'en';
2014 }
2015 $fallbackChain = array_reverse( $fallbackChain );
2016 foreach ( $fallbackChain as $code ) {
2017 if ( isset( $newWords[$code] ) ) {
2018 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
2019 }
2020 }
2021 }
2022
2023 /**
2024 * Get special page names, as an associative array
2025 * case folded alias => real name
2026 */
2027 function getSpecialPageAliases() {
2028 // Cache aliases because it may be slow to load them
2029 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
2030 // Initialise array
2031 $this->mExtendedSpecialPageAliases =
2032 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
2033 wfRunHooks( 'LanguageGetSpecialPageAliases',
2034 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
2035 }
2036
2037 return $this->mExtendedSpecialPageAliases;
2038 }
2039
2040 /**
2041 * Italic is unsuitable for some languages
2042 *
2043 * @param $text String: the text to be emphasized.
2044 * @return string
2045 */
2046 function emphasize( $text ) {
2047 return "<em>$text</em>";
2048 }
2049
2050 /**
2051 * Normally we output all numbers in plain en_US style, that is
2052 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
2053 * point twohundredthirtyfive. However this is not sutable for all
2054 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
2055 * Icelandic just want to use commas instead of dots, and dots instead
2056 * of commas like "293.291,235".
2057 *
2058 * An example of this function being called:
2059 * <code>
2060 * wfMsg( 'message', $wgLang->formatNum( $num ) )
2061 * </code>
2062 *
2063 * See LanguageGu.php for the Gujarati implementation and
2064 * $separatorTransformTable on MessageIs.php for
2065 * the , => . and . => , implementation.
2066 *
2067 * @todo check if it's viable to use localeconv() for the decimal
2068 * separator thing.
2069 * @param $number Mixed: the string to be formatted, should be an integer
2070 * or a floating point number.
2071 * @param $nocommafy Bool: set to true for special numbers like dates
2072 * @return string
2073 */
2074 function formatNum( $number, $nocommafy = false ) {
2075 global $wgTranslateNumerals;
2076 if (!$nocommafy) {
2077 $number = $this->commafy($number);
2078 $s = $this->separatorTransformTable();
2079 if ($s) { $number = strtr($number, $s); }
2080 }
2081
2082 if ($wgTranslateNumerals) {
2083 $s = $this->digitTransformTable();
2084 if ($s) { $number = strtr($number, $s); }
2085 }
2086
2087 return $number;
2088 }
2089
2090 function parseFormattedNumber( $number ) {
2091 $s = $this->digitTransformTable();
2092 if ($s) { $number = strtr($number, array_flip($s)); }
2093
2094 $s = $this->separatorTransformTable();
2095 if ($s) { $number = strtr($number, array_flip($s)); }
2096
2097 $number = strtr( $number, array (',' => '') );
2098 return $number;
2099 }
2100
2101 /**
2102 * Adds commas to a given number
2103 *
2104 * @param $_ mixed
2105 * @return string
2106 */
2107 function commafy($_) {
2108 return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_)));
2109 }
2110
2111 function digitTransformTable() {
2112 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
2113 }
2114
2115 function separatorTransformTable() {
2116 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
2117 }
2118
2119
2120 /**
2121 * Take a list of strings and build a locale-friendly comma-separated
2122 * list, using the local comma-separator message.
2123 * The last two strings are chained with an "and".
2124 *
2125 * @param $l Array
2126 * @return string
2127 */
2128 function listToText( $l ) {
2129 $s = '';
2130 $m = count( $l ) - 1;
2131 if( $m == 1 ) {
2132 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
2133 }
2134 else {
2135 for ( $i = $m; $i >= 0; $i-- ) {
2136 if ( $i == $m ) {
2137 $s = $l[$i];
2138 } else if( $i == $m - 1 ) {
2139 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
2140 } else {
2141 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
2142 }
2143 }
2144 return $s;
2145 }
2146 }
2147
2148 /**
2149 * Take a list of strings and build a locale-friendly comma-separated
2150 * list, using the local comma-separator message.
2151 * @param $list array of strings to put in a comma list
2152 * @return string
2153 */
2154 function commaList( $list ) {
2155 return implode(
2156 $list,
2157 wfMsgExt( 'comma-separator', array( 'parsemag', 'escapenoentities', 'language' => $this ) ) );
2158 }
2159
2160 /**
2161 * Take a list of strings and build a locale-friendly semicolon-separated
2162 * list, using the local semicolon-separator message.
2163 * @param $list array of strings to put in a semicolon list
2164 * @return string
2165 */
2166 function semicolonList( $list ) {
2167 return implode(
2168 $list,
2169 wfMsgExt( 'semicolon-separator', array( 'parsemag', 'escapenoentities', 'language' => $this ) ) );
2170 }
2171
2172 /**
2173 * Same as commaList, but separate it with the pipe instead.
2174 * @param $list array of strings to put in a pipe list
2175 * @return string
2176 */
2177 function pipeList( $list ) {
2178 return implode(
2179 $list,
2180 wfMsgExt( 'pipe-separator', array( 'escapenoentities', 'language' => $this ) ) );
2181 }
2182
2183 /**
2184 * Truncate a string to a specified length in bytes, appending an optional
2185 * string (e.g. for ellipses)
2186 *
2187 * The database offers limited byte lengths for some columns in the database;
2188 * multi-byte character sets mean we need to ensure that only whole characters
2189 * are included, otherwise broken characters can be passed to the user
2190 *
2191 * If $length is negative, the string will be truncated from the beginning
2192 *
2193 * @param $string String to truncate
2194 * @param $length Int: maximum length (excluding ellipses)
2195 * @param $ellipsis String to append to the truncated text
2196 * @return string
2197 */
2198 function truncate( $string, $length, $ellipsis = '...' ) {
2199 # Use the localized ellipsis character
2200 if( $ellipsis == '...' ) {
2201 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
2202 }
2203
2204 if( $length == 0 ) {
2205 return $ellipsis;
2206 }
2207 if ( strlen( $string ) <= abs( $length ) ) {
2208 return $string;
2209 }
2210 if( $length > 0 ) {
2211 $string = substr( $string, 0, $length );
2212 $char = ord( $string[strlen( $string ) - 1] );
2213 $m = array();
2214 if ($char >= 0xc0) {
2215 # We got the first byte only of a multibyte char; remove it.
2216 $string = substr( $string, 0, -1 );
2217 } elseif( $char >= 0x80 &&
2218 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
2219 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) ) {
2220 # We chopped in the middle of a character; remove it
2221 $string = $m[1];
2222 }
2223 return $string . $ellipsis;
2224 } else {
2225 $string = substr( $string, $length );
2226 $char = ord( $string[0] );
2227 if( $char >= 0x80 && $char < 0xc0 ) {
2228 # We chopped in the middle of a character; remove the whole thing
2229 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
2230 }
2231 return $ellipsis . $string;
2232 }
2233 }
2234
2235 /**
2236 * Grammatical transformations, needed for inflected languages
2237 * Invoked by putting {{grammar:case|word}} in a message
2238 *
2239 * @param $word string
2240 * @param $case string
2241 * @return string
2242 */
2243 function convertGrammar( $word, $case ) {
2244 global $wgGrammarForms;
2245 if ( isset($wgGrammarForms[$this->getCode()][$case][$word]) ) {
2246 return $wgGrammarForms[$this->getCode()][$case][$word];
2247 }
2248 return $word;
2249 }
2250
2251 /**
2252 * Provides an alternative text depending on specified gender.
2253 * Usage {{gender:username|masculine|feminine|neutral}}.
2254 * username is optional, in which case the gender of current user is used,
2255 * but only in (some) interface messages; otherwise default gender is used.
2256 * If second or third parameter are not specified, masculine is used.
2257 * These details may be overriden per language.
2258 */
2259 function gender( $gender, $forms ) {
2260 if ( !count($forms) ) { return ''; }
2261 $forms = $this->preConvertPlural( $forms, 2 );
2262 if ( $gender === 'male' ) return $forms[0];
2263 if ( $gender === 'female' ) return $forms[1];
2264 return isset($forms[2]) ? $forms[2] : $forms[0];
2265 }
2266
2267 /**
2268 * Plural form transformations, needed for some languages.
2269 * For example, there are 3 form of plural in Russian and Polish,
2270 * depending on "count mod 10". See [[w:Plural]]
2271 * For English it is pretty simple.
2272 *
2273 * Invoked by putting {{plural:count|wordform1|wordform2}}
2274 * or {{plural:count|wordform1|wordform2|wordform3}}
2275 *
2276 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
2277 *
2278 * @param $count Integer: non-localized number
2279 * @param $forms Array: different plural forms
2280 * @return string Correct form of plural for $count in this language
2281 */
2282 function convertPlural( $count, $forms ) {
2283 if ( !count($forms) ) { return ''; }
2284 $forms = $this->preConvertPlural( $forms, 2 );
2285
2286 return ( $count == 1 ) ? $forms[0] : $forms[1];
2287 }
2288
2289 /**
2290 * Checks that convertPlural was given an array and pads it to requested
2291 * amound of forms by copying the last one.
2292 *
2293 * @param $count Integer: How many forms should there be at least
2294 * @param $forms Array of forms given to convertPlural
2295 * @return array Padded array of forms or an exception if not an array
2296 */
2297 protected function preConvertPlural( /* Array */ $forms, $count ) {
2298 while ( count($forms) < $count ) {
2299 $forms[] = $forms[count($forms)-1];
2300 }
2301 return $forms;
2302 }
2303
2304 /**
2305 * For translaing of expiry times
2306 * @param $str String: the validated block time in English
2307 * @return Somehow translated block time
2308 * @see LanguageFi.php for example implementation
2309 */
2310 function translateBlockExpiry( $str ) {
2311
2312 $scBlockExpiryOptions = $this->getMessageFromDB( 'ipboptions' );
2313
2314 if ( $scBlockExpiryOptions == '-') {
2315 return $str;
2316 }
2317
2318 foreach (explode(',', $scBlockExpiryOptions) as $option) {
2319 if ( strpos($option, ":") === false )
2320 continue;
2321 list($show, $value) = explode(":", $option);
2322 if ( strcmp ( $str, $value) == 0 ) {
2323 return htmlspecialchars( trim( $show ) );
2324 }
2325 }
2326
2327 return $str;
2328 }
2329
2330 /**
2331 * languages like Chinese need to be segmented in order for the diff
2332 * to be of any use
2333 *
2334 * @param $text String
2335 * @return String
2336 */
2337 function segmentForDiff( $text ) {
2338 return $text;
2339 }
2340
2341 /**
2342 * and unsegment to show the result
2343 *
2344 * @param $text String
2345 * @return String
2346 */
2347 function unsegmentForDiff( $text ) {
2348 return $text;
2349 }
2350
2351 # convert text to all supported variants
2352 function autoConvertToAllVariants($text) {
2353 return $this->mConverter->autoConvertToAllVariants($text);
2354 }
2355
2356 # convert text to different variants of a language.
2357 function convert( $text, $isTitle = false) {
2358 return $this->mConverter->convert($text, $isTitle);
2359 }
2360
2361 # Convert text from within Parser
2362 function parserConvert( $text, &$parser ) {
2363 return $this->mConverter->parserConvert( $text, $parser );
2364 }
2365
2366 # Check if this is a language with variants
2367 function hasVariants(){
2368 return sizeof($this->getVariants())>1;
2369 }
2370
2371 # Put custom tags (e.g. -{ }-) around math to prevent conversion
2372 function armourMath($text){
2373 return $this->mConverter->armourMath($text);
2374 }
2375
2376
2377 /**
2378 * Perform output conversion on a string, and encode for safe HTML output.
2379 * @param $text String text to be converted
2380 * @param $isTitle Bool whether this conversion is for the article title
2381 * @return string
2382 * @todo this should get integrated somewhere sane
2383 */
2384 function convertHtml( $text, $isTitle = false ) {
2385 return htmlspecialchars( $this->convert( $text, $isTitle ) );
2386 }
2387
2388 function convertCategoryKey( $key ) {
2389 return $this->mConverter->convertCategoryKey( $key );
2390 }
2391
2392 /**
2393 * get the list of variants supported by this langauge
2394 * see sample implementation in LanguageZh.php
2395 *
2396 * @return array an array of language codes
2397 */
2398 function getVariants() {
2399 return $this->mConverter->getVariants();
2400 }
2401
2402
2403 function getPreferredVariant( $fromUser = true, $fromHeader = false ) {
2404 return $this->mConverter->getPreferredVariant( $fromUser, $fromHeader );
2405 }
2406
2407 /**
2408 * if a language supports multiple variants, it is
2409 * possible that non-existing link in one variant
2410 * actually exists in another variant. this function
2411 * tries to find it. See e.g. LanguageZh.php
2412 *
2413 * @param $link String: the name of the link
2414 * @param $nt Mixed: the title object of the link
2415 * @param boolean $ignoreOtherCond: to disable other conditions when
2416 * we need to transclude a template or update a category's link
2417 * @return null the input parameters may be modified upon return
2418 */
2419 function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
2420 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
2421 }
2422
2423 /**
2424 * If a language supports multiple variants, converts text
2425 * into an array of all possible variants of the text:
2426 * 'variant' => text in that variant
2427 */
2428 function convertLinkToAllVariants($text){
2429 return $this->mConverter->convertLinkToAllVariants($text);
2430 }
2431
2432
2433 /**
2434 * returns language specific options used by User::getPageRenderHash()
2435 * for example, the preferred language variant
2436 *
2437 * @return string
2438 */
2439 function getExtraHashOptions() {
2440 return $this->mConverter->getExtraHashOptions();
2441 }
2442
2443 /**
2444 * for languages that support multiple variants, the title of an
2445 * article may be displayed differently in different variants. this
2446 * function returns the apporiate title defined in the body of the article.
2447 *
2448 * @return string
2449 */
2450 function getParsedTitle() {
2451 return $this->mConverter->getParsedTitle();
2452 }
2453
2454 /**
2455 * Enclose a string with the "no conversion" tag. This is used by
2456 * various functions in the Parser
2457 *
2458 * @param $text String: text to be tagged for no conversion
2459 * @param $noParse
2460 * @return string the tagged text
2461 */
2462 function markNoConversion( $text, $noParse=false ) {
2463 return $this->mConverter->markNoConversion( $text, $noParse );
2464 }
2465
2466 /**
2467 * A regular expression to match legal word-trailing characters
2468 * which should be merged onto a link of the form [[foo]]bar.
2469 *
2470 * @return string
2471 */
2472 function linkTrail() {
2473 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
2474 }
2475
2476 function getLangObj() {
2477 return $this;
2478 }
2479
2480 /**
2481 * Get the RFC 3066 code for this language object
2482 */
2483 function getCode() {
2484 return $this->mCode;
2485 }
2486
2487 function setCode( $code ) {
2488 $this->mCode = $code;
2489 }
2490
2491 /**
2492 * Get the name of a file for a certain language code
2493 * @param $prefix string Prepend this to the filename
2494 * @param $code string Language code
2495 * @param $suffix string Append this to the filename
2496 * @return string $prefix . $mangledCode . $suffix
2497 */
2498 static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
2499 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
2500 }
2501
2502 /**
2503 * Get the language code from a file name. Inverse of getFileName()
2504 * @param $filename string $prefix . $languageCode . $suffix
2505 * @param $prefix string Prefix before the language code
2506 * @param $suffix string Suffix after the language code
2507 * @return Language code, or false if $prefix or $suffix isn't found
2508 */
2509 static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
2510 $m = null;
2511 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
2512 preg_quote( $suffix, '/' ) . '/', $filename, $m );
2513 if ( !count( $m ) ) {
2514 return false;
2515 }
2516 return str_replace( '_', '-', strtolower( $m[1] ) );
2517 }
2518
2519 static function getMessagesFileName( $code ) {
2520 global $IP;
2521 return self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
2522 }
2523
2524 static function getClassFileName( $code ) {
2525 global $IP;
2526 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
2527 }
2528
2529 /**
2530 * Get the fallback for a given language
2531 */
2532 static function getFallbackFor( $code ) {
2533 if ( $code === 'en' ) {
2534 // Shortcut
2535 return false;
2536 } else {
2537 return self::getLocalisationCache()->getItem( $code, 'fallback' );
2538 }
2539 }
2540
2541 /**
2542 * Get all messages for a given language
2543 * WARNING: this may take a long time
2544 */
2545 static function getMessagesFor( $code ) {
2546 return self::getLocalisationCache()->getItem( $code, 'messages' );
2547 }
2548
2549 /**
2550 * Get a message for a given language
2551 */
2552 static function getMessageFor( $key, $code ) {
2553 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
2554 }
2555
2556 function fixVariableInNamespace( $talk ) {
2557 if ( strpos( $talk, '$1' ) === false ) return $talk;
2558
2559 global $wgMetaNamespace;
2560 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
2561
2562 # Allow grammar transformations
2563 # Allowing full message-style parsing would make simple requests
2564 # such as action=raw much more expensive than they need to be.
2565 # This will hopefully cover most cases.
2566 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
2567 array( &$this, 'replaceGrammarInNamespace' ), $talk );
2568 return str_replace( ' ', '_', $talk );
2569 }
2570
2571 function replaceGrammarInNamespace( $m ) {
2572 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
2573 }
2574
2575 static function getCaseMaps() {
2576 static $wikiUpperChars, $wikiLowerChars;
2577 if ( isset( $wikiUpperChars ) ) {
2578 return array( $wikiUpperChars, $wikiLowerChars );
2579 }
2580
2581 wfProfileIn( __METHOD__ );
2582 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
2583 if ( $arr === false ) {
2584 throw new MWException(
2585 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
2586 }
2587 extract( $arr );
2588 wfProfileOut( __METHOD__ );
2589 return array( $wikiUpperChars, $wikiLowerChars );
2590 }
2591
2592 function formatTimePeriod( $seconds ) {
2593 if ( $seconds < 10 ) {
2594 return $this->formatNum( sprintf( "%.1f", $seconds ) ) . wfMsg( 'seconds-abbrev' );
2595 } elseif ( $seconds < 60 ) {
2596 return $this->formatNum( round( $seconds ) ) . wfMsg( 'seconds-abbrev' );
2597 } elseif ( $seconds < 3600 ) {
2598 return $this->formatNum( floor( $seconds / 60 ) ) . wfMsg( 'minutes-abbrev' ) .
2599 $this->formatNum( round( fmod( $seconds, 60 ) ) ) . wfMsg( 'seconds-abbrev' );
2600 } else {
2601 $hours = floor( $seconds / 3600 );
2602 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
2603 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
2604 return $this->formatNum( $hours ) . wfMsg( 'hours-abbrev' ) .
2605 $this->formatNum( $minutes ) . wfMsg( 'minutes-abbrev' ) .
2606 $this->formatNum( $secondsPart ) . wfMsg( 'seconds-abbrev' );
2607 }
2608 }
2609
2610 function formatBitrate( $bps ) {
2611 $units = array( 'bps', 'kbps', 'Mbps', 'Gbps' );
2612 if ( $bps <= 0 ) {
2613 return $this->formatNum( $bps ) . $units[0];
2614 }
2615 $unitIndex = floor( log10( $bps ) / 3 );
2616 $mantissa = $bps / pow( 1000, $unitIndex );
2617 if ( $mantissa < 10 ) {
2618 $mantissa = round( $mantissa, 1 );
2619 } else {
2620 $mantissa = round( $mantissa );
2621 }
2622 return $this->formatNum( $mantissa ) . $units[$unitIndex];
2623 }
2624
2625 /**
2626 * Format a size in bytes for output, using an appropriate
2627 * unit (B, KB, MB or GB) according to the magnitude in question
2628 *
2629 * @param $size Size to format
2630 * @return string Plain text (not HTML)
2631 */
2632 function formatSize( $size ) {
2633 // For small sizes no decimal places necessary
2634 $round = 0;
2635 if( $size > 1024 ) {
2636 $size = $size / 1024;
2637 if( $size > 1024 ) {
2638 $size = $size / 1024;
2639 // For MB and bigger two decimal places are smarter
2640 $round = 2;
2641 if( $size > 1024 ) {
2642 $size = $size / 1024;
2643 $msg = 'size-gigabytes';
2644 } else {
2645 $msg = 'size-megabytes';
2646 }
2647 } else {
2648 $msg = 'size-kilobytes';
2649 }
2650 } else {
2651 $msg = 'size-bytes';
2652 }
2653 $size = round( $size, $round );
2654 $text = $this->getMessageFromDB( $msg );
2655 return str_replace( '$1', $this->formatNum( $size ), $text );
2656 }
2657 }