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