Followup to r60271: pass the delimeter to preg_quote() per CR comment
[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;
1701 if ( $wgDBtype != 'mysql' ) {
1702 return $string;
1703 }
1704
1705
1706 wfProfileIn( __METHOD__ );
1707
1708 // MySQL fulltext index doesn't grok utf-8, so we
1709 // need to fold cases and convert to hex
1710 $out = preg_replace_callback(
1711 "/([\\xc0-\\xff][\\x80-\\xbf]*)/",
1712 array( $this, 'stripForSearchCallback' ),
1713 $this->lc( $string ) );
1714
1715 // And to add insult to injury, the default indexing
1716 // ignores short words... Pad them so we can pass them
1717 // through without reconfiguring the server...
1718 $minLength = $this->minSearchLength();
1719 if( $minLength > 1 ) {
1720 $n = $minLength-1;
1721 $out = preg_replace(
1722 "/\b(\w{1,$n})\b/",
1723 "$1u800",
1724 $out );
1725 }
1726
1727 // Periods within things like hostnames and IP addresses
1728 // are also important -- we want a search for "example.com"
1729 // or "192.168.1.1" to work sanely.
1730 //
1731 // MySQL's search seems to ignore them, so you'd match on
1732 // "example.wikipedia.com" and "192.168.83.1" as well.
1733 $out = preg_replace(
1734 "/(\w)\.(\w|\*)/u",
1735 "$1u82e$2",
1736 $out );
1737
1738 wfProfileOut( __METHOD__ );
1739 return $out;
1740 }
1741
1742 /**
1743 * Armor a case-folded UTF-8 string to get through MySQL's
1744 * fulltext search without being mucked up by funny charset
1745 * settings or anything else of the sort.
1746 */
1747 protected function stripForSearchCallback( $matches ) {
1748 return 'u8' . bin2hex( $matches[1] );
1749 }
1750
1751 /**
1752 * Check MySQL server's ft_min_word_len setting so we know
1753 * if we need to pad short words...
1754 */
1755 protected function minSearchLength() {
1756 if( is_null( $this->minSearchLength ) ) {
1757 $sql = "show global variables like 'ft\\_min\\_word\\_len'";
1758 $dbr = wfGetDB( DB_SLAVE );
1759 $result = $dbr->query( $sql );
1760 $row = $result->fetchObject();
1761 $result->free();
1762
1763 if( $row && $row->Variable_name == 'ft_min_word_len' ) {
1764 $this->minSearchLength = intval( $row->Value );
1765 } else {
1766 $this->minSearchLength = 0;
1767 }
1768 }
1769 return $this->minSearchLength;
1770 }
1771
1772 function convertForSearchResult( $termsArray ) {
1773 # some languages, e.g. Chinese, need to do a conversion
1774 # in order for search results to be displayed correctly
1775 return $termsArray;
1776 }
1777
1778 /**
1779 * Get the first character of a string.
1780 *
1781 * @param $s string
1782 * @return string
1783 */
1784 function firstChar( $s ) {
1785 $matches = array();
1786 preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
1787 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/', $s, $matches);
1788
1789 if ( isset( $matches[1] ) ) {
1790 if ( strlen( $matches[1] ) != 3 ) {
1791 return $matches[1];
1792 }
1793
1794 // Break down Hangul syllables to grab the first jamo
1795 $code = utf8ToCodepoint( $matches[1] );
1796 if ( $code < 0xac00 || 0xd7a4 <= $code) {
1797 return $matches[1];
1798 } elseif ( $code < 0xb098 ) {
1799 return "\xe3\x84\xb1";
1800 } elseif ( $code < 0xb2e4 ) {
1801 return "\xe3\x84\xb4";
1802 } elseif ( $code < 0xb77c ) {
1803 return "\xe3\x84\xb7";
1804 } elseif ( $code < 0xb9c8 ) {
1805 return "\xe3\x84\xb9";
1806 } elseif ( $code < 0xbc14 ) {
1807 return "\xe3\x85\x81";
1808 } elseif ( $code < 0xc0ac ) {
1809 return "\xe3\x85\x82";
1810 } elseif ( $code < 0xc544 ) {
1811 return "\xe3\x85\x85";
1812 } elseif ( $code < 0xc790 ) {
1813 return "\xe3\x85\x87";
1814 } elseif ( $code < 0xcc28 ) {
1815 return "\xe3\x85\x88";
1816 } elseif ( $code < 0xce74 ) {
1817 return "\xe3\x85\x8a";
1818 } elseif ( $code < 0xd0c0 ) {
1819 return "\xe3\x85\x8b";
1820 } elseif ( $code < 0xd30c ) {
1821 return "\xe3\x85\x8c";
1822 } elseif ( $code < 0xd558 ) {
1823 return "\xe3\x85\x8d";
1824 } else {
1825 return "\xe3\x85\x8e";
1826 }
1827 } else {
1828 return "";
1829 }
1830 }
1831
1832 function initEncoding() {
1833 # Some languages may have an alternate char encoding option
1834 # (Esperanto X-coding, Japanese furigana conversion, etc)
1835 # If this language is used as the primary content language,
1836 # an override to the defaults can be set here on startup.
1837 }
1838
1839 function recodeForEdit( $s ) {
1840 # For some languages we'll want to explicitly specify
1841 # which characters make it into the edit box raw
1842 # or are converted in some way or another.
1843 # Note that if wgOutputEncoding is different from
1844 # wgInputEncoding, this text will be further converted
1845 # to wgOutputEncoding.
1846 global $wgEditEncoding;
1847 if( $wgEditEncoding == '' or
1848 $wgEditEncoding == 'UTF-8' ) {
1849 return $s;
1850 } else {
1851 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
1852 }
1853 }
1854
1855 function recodeInput( $s ) {
1856 # Take the previous into account.
1857 global $wgEditEncoding;
1858 if($wgEditEncoding != "") {
1859 $enc = $wgEditEncoding;
1860 } else {
1861 $enc = 'UTF-8';
1862 }
1863 if( $enc == 'UTF-8' ) {
1864 return $s;
1865 } else {
1866 return $this->iconv( $enc, 'UTF-8', $s );
1867 }
1868 }
1869
1870 /**
1871 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
1872 * also cleans up certain backwards-compatible sequences, converting them
1873 * to the modern Unicode equivalent.
1874 *
1875 * This is language-specific for performance reasons only.
1876 */
1877 function normalize( $s ) {
1878 return UtfNormal::cleanUp( $s );
1879 }
1880
1881 /**
1882 * Transform a string using serialized data stored in the given file (which
1883 * must be in the serialized subdirectory of $IP). The file contains pairs
1884 * mapping source characters to destination characters.
1885 *
1886 * The data is cached in process memory. This will go faster if you have the
1887 * FastStringSearch extension.
1888 */
1889 function transformUsingPairFile( $file, $string ) {
1890 if ( !isset( $this->transformData[$file] ) ) {
1891 $data = wfGetPrecompiledData( $file );
1892 if ( $data === false ) {
1893 throw new MWException( __METHOD__.": The transformation file $file is missing" );
1894 }
1895 $this->transformData[$file] = new ReplacementArray( $data );
1896 }
1897 return $this->transformData[$file]->replace( $string );
1898 }
1899
1900 /**
1901 * For right-to-left language support
1902 *
1903 * @return bool
1904 */
1905 function isRTL() {
1906 return self::$dataCache->getItem( $this->mCode, 'rtl' );
1907 }
1908
1909 /**
1910 * Return the correct HTML 'dir' attribute value for this language.
1911 * @return String
1912 */
1913 function getDir() {
1914 return $this->isRTL() ? 'rtl' : 'ltr';
1915 }
1916
1917 /**
1918 * Return 'left' or 'right' as appropriate alignment for line-start
1919 * for this language's text direction.
1920 *
1921 * Should be equivalent to CSS3 'start' text-align value....
1922 *
1923 * @return String
1924 */
1925 function alignStart() {
1926 return $this->isRTL() ? 'right' : 'left';
1927 }
1928
1929 /**
1930 * Return 'right' or 'left' as appropriate alignment for line-end
1931 * for this language's text direction.
1932 *
1933 * Should be equivalent to CSS3 'end' text-align value....
1934 *
1935 * @return String
1936 */
1937 function alignEnd() {
1938 return $this->isRTL() ? 'left' : 'right';
1939 }
1940
1941 /**
1942 * A hidden direction mark (LRM or RLM), depending on the language direction
1943 *
1944 * @return string
1945 */
1946 function getDirMark() {
1947 return $this->isRTL() ? "\xE2\x80\x8F" : "\xE2\x80\x8E";
1948 }
1949
1950 function capitalizeAllNouns() {
1951 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
1952 }
1953
1954 /**
1955 * An arrow, depending on the language direction
1956 *
1957 * @return string
1958 */
1959 function getArrow() {
1960 return $this->isRTL() ? '←' : '→';
1961 }
1962
1963 /**
1964 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
1965 *
1966 * @return bool
1967 */
1968 function linkPrefixExtension() {
1969 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
1970 }
1971
1972 function getMagicWords() {
1973 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
1974 }
1975
1976 # Fill a MagicWord object with data from here
1977 function getMagic( $mw ) {
1978 if ( !$this->mMagicHookDone ) {
1979 $this->mMagicHookDone = true;
1980 wfProfileIn( 'LanguageGetMagic' );
1981 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
1982 wfProfileOut( 'LanguageGetMagic' );
1983 }
1984 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
1985 $rawEntry = $this->mMagicExtensions[$mw->mId];
1986 } else {
1987 $magicWords = $this->getMagicWords();
1988 if ( isset( $magicWords[$mw->mId] ) ) {
1989 $rawEntry = $magicWords[$mw->mId];
1990 } else {
1991 $rawEntry = false;
1992 }
1993 }
1994
1995 if( !is_array( $rawEntry ) ) {
1996 error_log( "\"$rawEntry\" is not a valid magic thingie for \"$mw->mId\"" );
1997 } else {
1998 $mw->mCaseSensitive = $rawEntry[0];
1999 $mw->mSynonyms = array_slice( $rawEntry, 1 );
2000 }
2001 }
2002
2003 /**
2004 * Add magic words to the extension array
2005 */
2006 function addMagicWordsByLang( $newWords ) {
2007 $code = $this->getCode();
2008 $fallbackChain = array();
2009 while ( $code && !in_array( $code, $fallbackChain ) ) {
2010 $fallbackChain[] = $code;
2011 $code = self::getFallbackFor( $code );
2012 }
2013 if ( !in_array( 'en', $fallbackChain ) ) {
2014 $fallbackChain[] = 'en';
2015 }
2016 $fallbackChain = array_reverse( $fallbackChain );
2017 foreach ( $fallbackChain as $code ) {
2018 if ( isset( $newWords[$code] ) ) {
2019 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
2020 }
2021 }
2022 }
2023
2024 /**
2025 * Get special page names, as an associative array
2026 * case folded alias => real name
2027 */
2028 function getSpecialPageAliases() {
2029 // Cache aliases because it may be slow to load them
2030 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
2031 // Initialise array
2032 $this->mExtendedSpecialPageAliases =
2033 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
2034 wfRunHooks( 'LanguageGetSpecialPageAliases',
2035 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
2036 }
2037
2038 return $this->mExtendedSpecialPageAliases;
2039 }
2040
2041 /**
2042 * Italic is unsuitable for some languages
2043 *
2044 * @param $text String: the text to be emphasized.
2045 * @return string
2046 */
2047 function emphasize( $text ) {
2048 return "<em>$text</em>";
2049 }
2050
2051 /**
2052 * Normally we output all numbers in plain en_US style, that is
2053 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
2054 * point twohundredthirtyfive. However this is not sutable for all
2055 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
2056 * Icelandic just want to use commas instead of dots, and dots instead
2057 * of commas like "293.291,235".
2058 *
2059 * An example of this function being called:
2060 * <code>
2061 * wfMsg( 'message', $wgLang->formatNum( $num ) )
2062 * </code>
2063 *
2064 * See LanguageGu.php for the Gujarati implementation and
2065 * $separatorTransformTable on MessageIs.php for
2066 * the , => . and . => , implementation.
2067 *
2068 * @todo check if it's viable to use localeconv() for the decimal
2069 * separator thing.
2070 * @param $number Mixed: the string to be formatted, should be an integer
2071 * or a floating point number.
2072 * @param $nocommafy Bool: set to true for special numbers like dates
2073 * @return string
2074 */
2075 function formatNum( $number, $nocommafy = false ) {
2076 global $wgTranslateNumerals;
2077 if (!$nocommafy) {
2078 $number = $this->commafy($number);
2079 $s = $this->separatorTransformTable();
2080 if ($s) { $number = strtr($number, $s); }
2081 }
2082
2083 if ($wgTranslateNumerals) {
2084 $s = $this->digitTransformTable();
2085 if ($s) { $number = strtr($number, $s); }
2086 }
2087
2088 return $number;
2089 }
2090
2091 function parseFormattedNumber( $number ) {
2092 $s = $this->digitTransformTable();
2093 if ($s) { $number = strtr($number, array_flip($s)); }
2094
2095 $s = $this->separatorTransformTable();
2096 if ($s) { $number = strtr($number, array_flip($s)); }
2097
2098 $number = strtr( $number, array (',' => '') );
2099 return $number;
2100 }
2101
2102 /**
2103 * Adds commas to a given number
2104 *
2105 * @param $_ mixed
2106 * @return string
2107 */
2108 function commafy($_) {
2109 return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_)));
2110 }
2111
2112 function digitTransformTable() {
2113 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
2114 }
2115
2116 function separatorTransformTable() {
2117 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
2118 }
2119
2120
2121 /**
2122 * Take a list of strings and build a locale-friendly comma-separated
2123 * list, using the local comma-separator message.
2124 * The last two strings are chained with an "and".
2125 *
2126 * @param $l Array
2127 * @return string
2128 */
2129 function listToText( $l ) {
2130 $s = '';
2131 $m = count( $l ) - 1;
2132 if( $m == 1 ) {
2133 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
2134 }
2135 else {
2136 for ( $i = $m; $i >= 0; $i-- ) {
2137 if ( $i == $m ) {
2138 $s = $l[$i];
2139 } else if( $i == $m - 1 ) {
2140 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
2141 } else {
2142 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
2143 }
2144 }
2145 return $s;
2146 }
2147 }
2148
2149 /**
2150 * Take a list of strings and build a locale-friendly comma-separated
2151 * list, using the local comma-separator message.
2152 * @param $list array of strings to put in a comma list
2153 * @return string
2154 */
2155 function commaList( $list ) {
2156 return implode(
2157 $list,
2158 wfMsgExt( 'comma-separator', array( 'parsemag', 'escapenoentities', 'language' => $this ) ) );
2159 }
2160
2161 /**
2162 * Take a list of strings and build a locale-friendly semicolon-separated
2163 * list, using the local semicolon-separator message.
2164 * @param $list array of strings to put in a semicolon list
2165 * @return string
2166 */
2167 function semicolonList( $list ) {
2168 return implode(
2169 $list,
2170 wfMsgExt( 'semicolon-separator', array( 'parsemag', 'escapenoentities', 'language' => $this ) ) );
2171 }
2172
2173 /**
2174 * Same as commaList, but separate it with the pipe instead.
2175 * @param $list array of strings to put in a pipe list
2176 * @return string
2177 */
2178 function pipeList( $list ) {
2179 return implode(
2180 $list,
2181 wfMsgExt( 'pipe-separator', array( 'escapenoentities', 'language' => $this ) ) );
2182 }
2183
2184 /**
2185 * Truncate a string to a specified length in bytes, appending an optional
2186 * string (e.g. for ellipses)
2187 *
2188 * The database offers limited byte lengths for some columns in the database;
2189 * multi-byte character sets mean we need to ensure that only whole characters
2190 * are included, otherwise broken characters can be passed to the user
2191 *
2192 * If $length is negative, the string will be truncated from the beginning
2193 *
2194 * @param $string String to truncate
2195 * @param $length Int: maximum length (excluding ellipses)
2196 * @param $ellipsis String to append to the truncated text
2197 * @return string
2198 */
2199 function truncate( $string, $length, $ellipsis = '...' ) {
2200 # Use the localized ellipsis character
2201 if( $ellipsis == '...' ) {
2202 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
2203 }
2204
2205 if( $length == 0 ) {
2206 return $ellipsis;
2207 }
2208 if ( strlen( $string ) <= abs( $length ) ) {
2209 return $string;
2210 }
2211 if( $length > 0 ) {
2212 $string = substr( $string, 0, $length );
2213 $char = ord( $string[strlen( $string ) - 1] );
2214 $m = array();
2215 if ($char >= 0xc0) {
2216 # We got the first byte only of a multibyte char; remove it.
2217 $string = substr( $string, 0, -1 );
2218 } elseif( $char >= 0x80 &&
2219 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
2220 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) ) {
2221 # We chopped in the middle of a character; remove it
2222 $string = $m[1];
2223 }
2224 return $string . $ellipsis;
2225 } else {
2226 $string = substr( $string, $length );
2227 $char = ord( $string[0] );
2228 if( $char >= 0x80 && $char < 0xc0 ) {
2229 # We chopped in the middle of a character; remove the whole thing
2230 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
2231 }
2232 return $ellipsis . $string;
2233 }
2234 }
2235
2236 /**
2237 * Grammatical transformations, needed for inflected languages
2238 * Invoked by putting {{grammar:case|word}} in a message
2239 *
2240 * @param $word string
2241 * @param $case string
2242 * @return string
2243 */
2244 function convertGrammar( $word, $case ) {
2245 global $wgGrammarForms;
2246 if ( isset($wgGrammarForms[$this->getCode()][$case][$word]) ) {
2247 return $wgGrammarForms[$this->getCode()][$case][$word];
2248 }
2249 return $word;
2250 }
2251
2252 /**
2253 * Provides an alternative text depending on specified gender.
2254 * Usage {{gender:username|masculine|feminine|neutral}}.
2255 * username is optional, in which case the gender of current user is used,
2256 * but only in (some) interface messages; otherwise default gender is used.
2257 * If second or third parameter are not specified, masculine is used.
2258 * These details may be overriden per language.
2259 */
2260 function gender( $gender, $forms ) {
2261 if ( !count($forms) ) { return ''; }
2262 $forms = $this->preConvertPlural( $forms, 2 );
2263 if ( $gender === 'male' ) return $forms[0];
2264 if ( $gender === 'female' ) return $forms[1];
2265 return isset($forms[2]) ? $forms[2] : $forms[0];
2266 }
2267
2268 /**
2269 * Plural form transformations, needed for some languages.
2270 * For example, there are 3 form of plural in Russian and Polish,
2271 * depending on "count mod 10". See [[w:Plural]]
2272 * For English it is pretty simple.
2273 *
2274 * Invoked by putting {{plural:count|wordform1|wordform2}}
2275 * or {{plural:count|wordform1|wordform2|wordform3}}
2276 *
2277 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
2278 *
2279 * @param $count Integer: non-localized number
2280 * @param $forms Array: different plural forms
2281 * @return string Correct form of plural for $count in this language
2282 */
2283 function convertPlural( $count, $forms ) {
2284 if ( !count($forms) ) { return ''; }
2285 $forms = $this->preConvertPlural( $forms, 2 );
2286
2287 return ( $count == 1 ) ? $forms[0] : $forms[1];
2288 }
2289
2290 /**
2291 * Checks that convertPlural was given an array and pads it to requested
2292 * amound of forms by copying the last one.
2293 *
2294 * @param $count Integer: How many forms should there be at least
2295 * @param $forms Array of forms given to convertPlural
2296 * @return array Padded array of forms or an exception if not an array
2297 */
2298 protected function preConvertPlural( /* Array */ $forms, $count ) {
2299 while ( count($forms) < $count ) {
2300 $forms[] = $forms[count($forms)-1];
2301 }
2302 return $forms;
2303 }
2304
2305 /**
2306 * For translaing of expiry times
2307 * @param $str String: the validated block time in English
2308 * @return Somehow translated block time
2309 * @see LanguageFi.php for example implementation
2310 */
2311 function translateBlockExpiry( $str ) {
2312
2313 $scBlockExpiryOptions = $this->getMessageFromDB( 'ipboptions' );
2314
2315 if ( $scBlockExpiryOptions == '-') {
2316 return $str;
2317 }
2318
2319 foreach (explode(',', $scBlockExpiryOptions) as $option) {
2320 if ( strpos($option, ":") === false )
2321 continue;
2322 list($show, $value) = explode(":", $option);
2323 if ( strcmp ( $str, $value) == 0 ) {
2324 return htmlspecialchars( trim( $show ) );
2325 }
2326 }
2327
2328 return $str;
2329 }
2330
2331 /**
2332 * languages like Chinese need to be segmented in order for the diff
2333 * to be of any use
2334 *
2335 * @param $text String
2336 * @return String
2337 */
2338 function segmentForDiff( $text ) {
2339 return $text;
2340 }
2341
2342 /**
2343 * and unsegment to show the result
2344 *
2345 * @param $text String
2346 * @return String
2347 */
2348 function unsegmentForDiff( $text ) {
2349 return $text;
2350 }
2351
2352 # convert text to all supported variants
2353 function autoConvertToAllVariants($text) {
2354 return $this->mConverter->autoConvertToAllVariants($text);
2355 }
2356
2357 # convert text to different variants of a language.
2358 function convert( $text, $isTitle = false) {
2359 return $this->mConverter->convert($text, $isTitle);
2360 }
2361
2362 # Convert text from within Parser
2363 function parserConvert( $text, &$parser ) {
2364 return $this->mConverter->parserConvert( $text, $parser );
2365 }
2366
2367 # Check if this is a language with variants
2368 function hasVariants(){
2369 return sizeof($this->getVariants())>1;
2370 }
2371
2372 # Put custom tags (e.g. -{ }-) around math to prevent conversion
2373 function armourMath($text){
2374 return $this->mConverter->armourMath($text);
2375 }
2376
2377
2378 /**
2379 * Perform output conversion on a string, and encode for safe HTML output.
2380 * @param $text String text to be converted
2381 * @param $isTitle Bool whether this conversion is for the article title
2382 * @return string
2383 * @todo this should get integrated somewhere sane
2384 */
2385 function convertHtml( $text, $isTitle = false ) {
2386 return htmlspecialchars( $this->convert( $text, $isTitle ) );
2387 }
2388
2389 function convertCategoryKey( $key ) {
2390 return $this->mConverter->convertCategoryKey( $key );
2391 }
2392
2393 /**
2394 * get the list of variants supported by this langauge
2395 * see sample implementation in LanguageZh.php
2396 *
2397 * @return array an array of language codes
2398 */
2399 function getVariants() {
2400 return $this->mConverter->getVariants();
2401 }
2402
2403
2404 function getPreferredVariant( $fromUser = true, $fromHeader = false ) {
2405 return $this->mConverter->getPreferredVariant( $fromUser, $fromHeader );
2406 }
2407
2408 /**
2409 * if a language supports multiple variants, it is
2410 * possible that non-existing link in one variant
2411 * actually exists in another variant. this function
2412 * tries to find it. See e.g. LanguageZh.php
2413 *
2414 * @param $link String: the name of the link
2415 * @param $nt Mixed: the title object of the link
2416 * @param boolean $ignoreOtherCond: to disable other conditions when
2417 * we need to transclude a template or update a category's link
2418 * @return null the input parameters may be modified upon return
2419 */
2420 function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
2421 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
2422 }
2423
2424 /**
2425 * If a language supports multiple variants, converts text
2426 * into an array of all possible variants of the text:
2427 * 'variant' => text in that variant
2428 */
2429 function convertLinkToAllVariants($text){
2430 return $this->mConverter->convertLinkToAllVariants($text);
2431 }
2432
2433
2434 /**
2435 * returns language specific options used by User::getPageRenderHash()
2436 * for example, the preferred language variant
2437 *
2438 * @return string
2439 */
2440 function getExtraHashOptions() {
2441 return $this->mConverter->getExtraHashOptions();
2442 }
2443
2444 /**
2445 * for languages that support multiple variants, the title of an
2446 * article may be displayed differently in different variants. this
2447 * function returns the apporiate title defined in the body of the article.
2448 *
2449 * @return string
2450 */
2451 function getParsedTitle() {
2452 return $this->mConverter->getParsedTitle();
2453 }
2454
2455 /**
2456 * Enclose a string with the "no conversion" tag. This is used by
2457 * various functions in the Parser
2458 *
2459 * @param $text String: text to be tagged for no conversion
2460 * @param $noParse
2461 * @return string the tagged text
2462 */
2463 function markNoConversion( $text, $noParse=false ) {
2464 return $this->mConverter->markNoConversion( $text, $noParse );
2465 }
2466
2467 /**
2468 * A regular expression to match legal word-trailing characters
2469 * which should be merged onto a link of the form [[foo]]bar.
2470 *
2471 * @return string
2472 */
2473 function linkTrail() {
2474 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
2475 }
2476
2477 function getLangObj() {
2478 return $this;
2479 }
2480
2481 /**
2482 * Get the RFC 3066 code for this language object
2483 */
2484 function getCode() {
2485 return $this->mCode;
2486 }
2487
2488 function setCode( $code ) {
2489 $this->mCode = $code;
2490 }
2491
2492 /**
2493 * Get the name of a file for a certain language code
2494 * @param $prefix string Prepend this to the filename
2495 * @param $code string Language code
2496 * @param $suffix string Append this to the filename
2497 * @return string $prefix . $mangledCode . $suffix
2498 */
2499 static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
2500 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
2501 }
2502
2503 /**
2504 * Get the language code from a file name. Inverse of getFileName()
2505 * @param $filename string $prefix . $languageCode . $suffix
2506 * @param $prefix string Prefix before the language code
2507 * @param $suffix string Suffix after the language code
2508 * @return Language code, or false if $prefix or $suffix isn't found
2509 */
2510 static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
2511 $m = null;
2512 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
2513 preg_quote( $suffix, '/' ) . '/', $filename, $m );
2514 if ( !count( $m ) ) {
2515 return false;
2516 }
2517 return str_replace( '_', '-', strtolower( $m[1] ) );
2518 }
2519
2520 static function getMessagesFileName( $code ) {
2521 global $IP;
2522 return self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
2523 }
2524
2525 static function getClassFileName( $code ) {
2526 global $IP;
2527 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
2528 }
2529
2530 /**
2531 * Get the fallback for a given language
2532 */
2533 static function getFallbackFor( $code ) {
2534 if ( $code === 'en' ) {
2535 // Shortcut
2536 return false;
2537 } else {
2538 return self::getLocalisationCache()->getItem( $code, 'fallback' );
2539 }
2540 }
2541
2542 /**
2543 * Get all messages for a given language
2544 * WARNING: this may take a long time
2545 */
2546 static function getMessagesFor( $code ) {
2547 return self::getLocalisationCache()->getItem( $code, 'messages' );
2548 }
2549
2550 /**
2551 * Get a message for a given language
2552 */
2553 static function getMessageFor( $key, $code ) {
2554 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
2555 }
2556
2557 function fixVariableInNamespace( $talk ) {
2558 if ( strpos( $talk, '$1' ) === false ) return $talk;
2559
2560 global $wgMetaNamespace;
2561 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
2562
2563 # Allow grammar transformations
2564 # Allowing full message-style parsing would make simple requests
2565 # such as action=raw much more expensive than they need to be.
2566 # This will hopefully cover most cases.
2567 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
2568 array( &$this, 'replaceGrammarInNamespace' ), $talk );
2569 return str_replace( ' ', '_', $talk );
2570 }
2571
2572 function replaceGrammarInNamespace( $m ) {
2573 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
2574 }
2575
2576 static function getCaseMaps() {
2577 static $wikiUpperChars, $wikiLowerChars;
2578 if ( isset( $wikiUpperChars ) ) {
2579 return array( $wikiUpperChars, $wikiLowerChars );
2580 }
2581
2582 wfProfileIn( __METHOD__ );
2583 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
2584 if ( $arr === false ) {
2585 throw new MWException(
2586 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
2587 }
2588 extract( $arr );
2589 wfProfileOut( __METHOD__ );
2590 return array( $wikiUpperChars, $wikiLowerChars );
2591 }
2592
2593 function formatTimePeriod( $seconds ) {
2594 if ( $seconds < 10 ) {
2595 return $this->formatNum( sprintf( "%.1f", $seconds ) ) . wfMsg( 'seconds-abbrev' );
2596 } elseif ( $seconds < 60 ) {
2597 return $this->formatNum( round( $seconds ) ) . wfMsg( 'seconds-abbrev' );
2598 } elseif ( $seconds < 3600 ) {
2599 return $this->formatNum( floor( $seconds / 60 ) ) . wfMsg( 'minutes-abbrev' ) .
2600 $this->formatNum( round( fmod( $seconds, 60 ) ) ) . wfMsg( 'seconds-abbrev' );
2601 } else {
2602 $hours = floor( $seconds / 3600 );
2603 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
2604 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
2605 return $this->formatNum( $hours ) . wfMsg( 'hours-abbrev' ) .
2606 $this->formatNum( $minutes ) . wfMsg( 'minutes-abbrev' ) .
2607 $this->formatNum( $secondsPart ) . wfMsg( 'seconds-abbrev' );
2608 }
2609 }
2610
2611 function formatBitrate( $bps ) {
2612 $units = array( 'bps', 'kbps', 'Mbps', 'Gbps' );
2613 if ( $bps <= 0 ) {
2614 return $this->formatNum( $bps ) . $units[0];
2615 }
2616 $unitIndex = floor( log10( $bps ) / 3 );
2617 $mantissa = $bps / pow( 1000, $unitIndex );
2618 if ( $mantissa < 10 ) {
2619 $mantissa = round( $mantissa, 1 );
2620 } else {
2621 $mantissa = round( $mantissa );
2622 }
2623 return $this->formatNum( $mantissa ) . $units[$unitIndex];
2624 }
2625
2626 /**
2627 * Format a size in bytes for output, using an appropriate
2628 * unit (B, KB, MB or GB) according to the magnitude in question
2629 *
2630 * @param $size Size to format
2631 * @return string Plain text (not HTML)
2632 */
2633 function formatSize( $size ) {
2634 // For small sizes no decimal places necessary
2635 $round = 0;
2636 if( $size > 1024 ) {
2637 $size = $size / 1024;
2638 if( $size > 1024 ) {
2639 $size = $size / 1024;
2640 // For MB and bigger two decimal places are smarter
2641 $round = 2;
2642 if( $size > 1024 ) {
2643 $size = $size / 1024;
2644 $msg = 'size-gigabytes';
2645 } else {
2646 $msg = 'size-megabytes';
2647 }
2648 } else {
2649 $msg = 'size-kilobytes';
2650 }
2651 } else {
2652 $msg = 'size-bytes';
2653 }
2654 $size = round( $size, $round );
2655 $text = $this->getMessageFromDB( $msg );
2656 return str_replace( '$1', $this->formatNum( $size ), $text );
2657 }
2658 }