[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Internationalisation code. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Language 22 */ 23 24 /** 25 * @defgroup Language Language 26 */ 27 28 if ( !defined( 'MEDIAWIKI' ) ) { 29 echo "This file is part of MediaWiki, it is not a valid entry point.\n"; 30 exit( 1 ); 31 } 32 33 if ( function_exists( 'mb_strtoupper' ) ) { 34 mb_internal_encoding( 'UTF-8' ); 35 } 36 37 /** 38 * Internationalisation code 39 * @ingroup Language 40 */ 41 class Language { 42 /** 43 * @var LanguageConverter 44 */ 45 public $mConverter; 46 47 public $mVariants, $mCode, $mLoaded = false; 48 public $mMagicExtensions = array(), $mMagicHookDone = false; 49 private $mHtmlCode = null, $mParentLanguage = false; 50 51 public $dateFormatStrings = array(); 52 public $mExtendedSpecialPageAliases; 53 54 protected $namespaceNames, $mNamespaceIds, $namespaceAliases; 55 56 /** 57 * ReplacementArray object caches 58 */ 59 public $transformData = array(); 60 61 /** 62 * @var LocalisationCache 63 */ 64 static public $dataCache; 65 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 * @since 1.20 124 * @var array 125 */ 126 static public $durationIntervals = array( 127 'millennia' => 31556952000, 128 'centuries' => 3155695200, 129 'decades' => 315569520, 130 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 ) 131 'weeks' => 604800, 132 'days' => 86400, 133 'hours' => 3600, 134 'minutes' => 60, 135 'seconds' => 1, 136 ); 137 138 /** 139 * Cache for language fallbacks. 140 * @see Language::getFallbacksIncludingSiteLanguage 141 * @since 1.21 142 * @var array 143 */ 144 static private $fallbackLanguageCache = array(); 145 146 /** 147 * Get a cached or new language object for a given language code 148 * @param string $code 149 * @return Language 150 */ 151 static function factory( $code ) { 152 global $wgDummyLanguageCodes, $wgLangObjCacheSize; 153 154 if ( isset( $wgDummyLanguageCodes[$code] ) ) { 155 $code = $wgDummyLanguageCodes[$code]; 156 } 157 158 // get the language object to process 159 $langObj = isset( self::$mLangObjCache[$code] ) 160 ? self::$mLangObjCache[$code] 161 : self::newFromCode( $code ); 162 163 // merge the language object in to get it up front in the cache 164 self::$mLangObjCache = array_merge( array( $code => $langObj ), self::$mLangObjCache ); 165 // get rid of the oldest ones in case we have an overflow 166 self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true ); 167 168 return $langObj; 169 } 170 171 /** 172 * Create a language object for a given language code 173 * @param string $code 174 * @throws MWException 175 * @return Language 176 */ 177 protected static function newFromCode( $code ) { 178 // Protect against path traversal below 179 if ( !Language::isValidCode( $code ) 180 || strcspn( $code, ":/\\\000" ) !== strlen( $code ) 181 ) { 182 throw new MWException( "Invalid language code \"$code\"" ); 183 } 184 185 if ( !Language::isValidBuiltInCode( $code ) ) { 186 // It's not possible to customise this code with class files, so 187 // just return a Language object. This is to support uselang= hacks. 188 $lang = new Language; 189 $lang->setCode( $code ); 190 return $lang; 191 } 192 193 // Check if there is a language class for the code 194 $class = self::classFromCode( $code ); 195 self::preloadLanguageClass( $class ); 196 if ( class_exists( $class ) ) { 197 $lang = new $class; 198 return $lang; 199 } 200 201 // Keep trying the fallback list until we find an existing class 202 $fallbacks = Language::getFallbacksFor( $code ); 203 foreach ( $fallbacks as $fallbackCode ) { 204 if ( !Language::isValidBuiltInCode( $fallbackCode ) ) { 205 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" ); 206 } 207 208 $class = self::classFromCode( $fallbackCode ); 209 self::preloadLanguageClass( $class ); 210 if ( class_exists( $class ) ) { 211 $lang = Language::newFromCode( $fallbackCode ); 212 $lang->setCode( $code ); 213 return $lang; 214 } 215 } 216 217 throw new MWException( "Invalid fallback sequence for language '$code'" ); 218 } 219 220 /** 221 * Checks whether any localisation is available for that language tag 222 * in MediaWiki (MessagesXx.php exists). 223 * 224 * @param string $code Language tag (in lower case) 225 * @return bool Whether language is supported 226 * @since 1.21 227 */ 228 public static function isSupportedLanguage( $code ) { 229 return self::isValidBuiltInCode( $code ) 230 && ( is_readable( self::getMessagesFileName( $code ) ) 231 || is_readable( self::getJsonMessagesFileName( $code ) ) 232 ); 233 } 234 235 /** 236 * Returns true if a language code string is a well-formed language tag 237 * according to RFC 5646. 238 * This function only checks well-formedness; it doesn't check that 239 * language, script or variant codes actually exist in the repositories. 240 * 241 * Based on regexes by Mark Davis of the Unicode Consortium: 242 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt 243 * 244 * @param string $code 245 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'. 246 * 247 * @return bool 248 * @since 1.21 249 */ 250 public static function isWellFormedLanguageTag( $code, $lenient = false ) { 251 $alpha = '[a-z]'; 252 $digit = '[0-9]'; 253 $alphanum = '[a-z0-9]'; 254 $x = 'x'; # private use singleton 255 $singleton = '[a-wy-z]'; # other singleton 256 $s = $lenient ? '[-_]' : '-'; 257 258 $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}"; 259 $script = "$alpha{4}"; # ISO 15924 260 $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49 261 $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})"; 262 $extension = "$singleton(?:$s$alphanum{2,8})+"; 263 $privateUse = "$x(?:$s$alphanum{1,8})+"; 264 265 # Define certain grandfathered codes, since otherwise the regex is pretty useless. 266 # Since these are limited, this is safe even later changes to the registry -- 267 # the only oddity is that it might change the type of the tag, and thus 268 # the results from the capturing groups. 269 # http://www.iana.org/assignments/language-subtag-registry 270 271 $grandfathered = "en{$s}GB{$s}oed" 272 . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)" 273 . "|no{$s}(?:bok|nyn)" 274 . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)" 275 . "|zh{$s}min{$s}nan"; 276 277 $variantList = "$variant(?:$s$variant)*"; 278 $extensionList = "$extension(?:$s$extension)*"; 279 280 $langtag = "(?:($language)" 281 . "(?:$s$script)?" 282 . "(?:$s$region)?" 283 . "(?:$s$variantList)?" 284 . "(?:$s$extensionList)?" 285 . "(?:$s$privateUse)?)"; 286 287 # The final breakdown, with capturing groups for each of these components 288 # The variants, extensions, grandfathered, and private-use may have interior '-' 289 290 $root = "^(?:$langtag|$privateUse|$grandfathered)$"; 291 292 return (bool)preg_match( "/$root/", strtolower( $code ) ); 293 } 294 295 /** 296 * Returns true if a language code string is of a valid form, whether or 297 * not it exists. This includes codes which are used solely for 298 * customisation via the MediaWiki namespace. 299 * 300 * @param string $code 301 * 302 * @return bool 303 */ 304 public static function isValidCode( $code ) { 305 static $cache = array(); 306 if ( isset( $cache[$code] ) ) { 307 return $cache[$code]; 308 } 309 // People think language codes are html safe, so enforce it. 310 // Ideally we should only allow a-zA-Z0-9- 311 // but, .+ and other chars are often used for {{int:}} hacks 312 // see bugs 37564, 37587, 36938 313 $cache[$code] = 314 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code ) 315 && !preg_match( Title::getTitleInvalidRegex(), $code ); 316 317 return $cache[$code]; 318 } 319 320 /** 321 * Returns true if a language code is of a valid form for the purposes of 322 * internal customisation of MediaWiki, via Messages*.php or *.json. 323 * 324 * @param string $code 325 * 326 * @throws MWException 327 * @since 1.18 328 * @return bool 329 */ 330 public static function isValidBuiltInCode( $code ) { 331 332 if ( !is_string( $code ) ) { 333 if ( is_object( $code ) ) { 334 $addmsg = " of class " . get_class( $code ); 335 } else { 336 $addmsg = ''; 337 } 338 $type = gettype( $code ); 339 throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" ); 340 } 341 342 return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code ); 343 } 344 345 /** 346 * Returns true if a language code is an IETF tag known to MediaWiki. 347 * 348 * @param string $tag 349 * 350 * @since 1.21 351 * @return bool 352 */ 353 public static function isKnownLanguageTag( $tag ) { 354 static $coreLanguageNames; 355 356 // Quick escape for invalid input to avoid exceptions down the line 357 // when code tries to process tags which are not valid at all. 358 if ( !self::isValidBuiltInCode( $tag ) ) { 359 return false; 360 } 361 362 if ( $coreLanguageNames === null ) { 363 global $IP; 364 include "$IP/languages/Names.php"; 365 } 366 367 if ( isset( $coreLanguageNames[$tag] ) 368 || self::fetchLanguageName( $tag, $tag ) !== '' 369 ) { 370 return true; 371 } 372 373 return false; 374 } 375 376 /** 377 * @param string $code 378 * @return string Name of the language class 379 */ 380 public static function classFromCode( $code ) { 381 if ( $code == 'en' ) { 382 return 'Language'; 383 } else { 384 return 'Language' . str_replace( '-', '_', ucfirst( $code ) ); 385 } 386 } 387 388 /** 389 * Includes language class files 390 * 391 * @param string $class Name of the language class 392 */ 393 public static function preloadLanguageClass( $class ) { 394 global $IP; 395 396 if ( $class === 'Language' ) { 397 return; 398 } 399 400 if ( file_exists( "$IP/languages/classes/$class.php" ) ) { 401 include_once "$IP/languages/classes/$class.php"; 402 } 403 } 404 405 /** 406 * Get the LocalisationCache instance 407 * 408 * @return LocalisationCache 409 */ 410 public static function getLocalisationCache() { 411 if ( is_null( self::$dataCache ) ) { 412 global $wgLocalisationCacheConf; 413 $class = $wgLocalisationCacheConf['class']; 414 self::$dataCache = new $class( $wgLocalisationCacheConf ); 415 } 416 return self::$dataCache; 417 } 418 419 function __construct() { 420 $this->mConverter = new FakeConverter( $this ); 421 // Set the code to the name of the descendant 422 if ( get_class( $this ) == 'Language' ) { 423 $this->mCode = 'en'; 424 } else { 425 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) ); 426 } 427 self::getLocalisationCache(); 428 } 429 430 /** 431 * Reduce memory usage 432 */ 433 function __destruct() { 434 foreach ( $this as $name => $value ) { 435 unset( $this->$name ); 436 } 437 } 438 439 /** 440 * Hook which will be called if this is the content language. 441 * Descendants can use this to register hook functions or modify globals 442 */ 443 function initContLang() { 444 } 445 446 /** 447 * @return array 448 * @since 1.19 449 */ 450 function getFallbackLanguages() { 451 return self::getFallbacksFor( $this->mCode ); 452 } 453 454 /** 455 * Exports $wgBookstoreListEn 456 * @return array 457 */ 458 function getBookstoreList() { 459 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' ); 460 } 461 462 /** 463 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not 464 * available in localised form, it will be included in English. 465 * 466 * @return array 467 */ 468 public function getNamespaces() { 469 if ( is_null( $this->namespaceNames ) ) { 470 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces; 471 472 $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' ); 473 $validNamespaces = MWNamespace::getCanonicalNamespaces(); 474 475 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces; 476 477 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace; 478 if ( $wgMetaNamespaceTalk ) { 479 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk; 480 } else { 481 $talk = $this->namespaceNames[NS_PROJECT_TALK]; 482 $this->namespaceNames[NS_PROJECT_TALK] = 483 $this->fixVariableInNamespace( $talk ); 484 } 485 486 # Sometimes a language will be localised but not actually exist on this wiki. 487 foreach ( $this->namespaceNames as $key => $text ) { 488 if ( !isset( $validNamespaces[$key] ) ) { 489 unset( $this->namespaceNames[$key] ); 490 } 491 } 492 493 # The above mixing may leave namespaces out of canonical order. 494 # Re-order by namespace ID number... 495 ksort( $this->namespaceNames ); 496 497 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames ) ); 498 } 499 500 return $this->namespaceNames; 501 } 502 503 /** 504 * Arbitrarily set all of the namespace names at once. Mainly used for testing 505 * @param array $namespaces Array of namespaces (id => name) 506 */ 507 public function setNamespaces( array $namespaces ) { 508 $this->namespaceNames = $namespaces; 509 $this->mNamespaceIds = null; 510 } 511 512 /** 513 * Resets all of the namespace caches. Mainly used for testing 514 */ 515 public function resetNamespaces() { 516 $this->namespaceNames = null; 517 $this->mNamespaceIds = null; 518 $this->namespaceAliases = null; 519 } 520 521 /** 522 * A convenience function that returns the same thing as 523 * getNamespaces() except with the array values changed to ' ' 524 * where it found '_', useful for producing output to be displayed 525 * e.g. in <select> forms. 526 * 527 * @return array 528 */ 529 function getFormattedNamespaces() { 530 $ns = $this->getNamespaces(); 531 foreach ( $ns as $k => $v ) { 532 $ns[$k] = strtr( $v, '_', ' ' ); 533 } 534 return $ns; 535 } 536 537 /** 538 * Get a namespace value by key 539 * <code> 540 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI ); 541 * echo $mw_ns; // prints 'MediaWiki' 542 * </code> 543 * 544 * @param int $index The array key of the namespace to return 545 * @return string|bool String if the namespace value exists, otherwise false 546 */ 547 function getNsText( $index ) { 548 $ns = $this->getNamespaces(); 549 550 return isset( $ns[$index] ) ? $ns[$index] : false; 551 } 552 553 /** 554 * A convenience function that returns the same thing as 555 * getNsText() except with '_' changed to ' ', useful for 556 * producing output. 557 * 558 * <code> 559 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK ); 560 * echo $mw_ns; // prints 'MediaWiki talk' 561 * </code> 562 * 563 * @param int $index The array key of the namespace to return 564 * @return string Namespace name without underscores (empty string if namespace does not exist) 565 */ 566 function getFormattedNsText( $index ) { 567 $ns = $this->getNsText( $index ); 568 569 return strtr( $ns, '_', ' ' ); 570 } 571 572 /** 573 * Returns gender-dependent namespace alias if available. 574 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces 575 * @param int $index Namespace index 576 * @param string $gender Gender key (male, female... ) 577 * @return string 578 * @since 1.18 579 */ 580 function getGenderNsText( $index, $gender ) { 581 global $wgExtraGenderNamespaces; 582 583 $ns = $wgExtraGenderNamespaces + 584 self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' ); 585 586 return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index ); 587 } 588 589 /** 590 * Whether this language uses gender-dependent namespace aliases. 591 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces 592 * @return bool 593 * @since 1.18 594 */ 595 function needsGenderDistinction() { 596 global $wgExtraGenderNamespaces, $wgExtraNamespaces; 597 if ( count( $wgExtraGenderNamespaces ) > 0 ) { 598 // $wgExtraGenderNamespaces overrides everything 599 return true; 600 } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) { 601 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future 602 // $wgExtraNamespaces overrides any gender aliases specified in i18n files 603 return false; 604 } else { 605 // Check what is in i18n files 606 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' ); 607 return count( $aliases ) > 0; 608 } 609 } 610 611 /** 612 * Get a namespace key by value, case insensitive. 613 * Only matches namespace names for the current language, not the 614 * canonical ones defined in Namespace.php. 615 * 616 * @param string $text 617 * @return int|bool An integer if $text is a valid value otherwise false 618 */ 619 function getLocalNsIndex( $text ) { 620 $lctext = $this->lc( $text ); 621 $ids = $this->getNamespaceIds(); 622 return isset( $ids[$lctext] ) ? $ids[$lctext] : false; 623 } 624 625 /** 626 * @return array 627 */ 628 function getNamespaceAliases() { 629 if ( is_null( $this->namespaceAliases ) ) { 630 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' ); 631 if ( !$aliases ) { 632 $aliases = array(); 633 } else { 634 foreach ( $aliases as $name => $index ) { 635 if ( $index === NS_PROJECT_TALK ) { 636 unset( $aliases[$name] ); 637 $name = $this->fixVariableInNamespace( $name ); 638 $aliases[$name] = $index; 639 } 640 } 641 } 642 643 global $wgExtraGenderNamespaces; 644 $genders = $wgExtraGenderNamespaces + 645 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' ); 646 foreach ( $genders as $index => $forms ) { 647 foreach ( $forms as $alias ) { 648 $aliases[$alias] = $index; 649 } 650 } 651 652 # Also add converted namespace names as aliases, to avoid confusion. 653 $convertedNames = array(); 654 foreach ( $this->getVariants() as $variant ) { 655 if ( $variant === $this->mCode ) { 656 continue; 657 } 658 foreach ( $this->getNamespaces() as $ns => $_ ) { 659 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns; 660 } 661 } 662 663 $this->namespaceAliases = $aliases + $convertedNames; 664 } 665 666 return $this->namespaceAliases; 667 } 668 669 /** 670 * @return array 671 */ 672 function getNamespaceIds() { 673 if ( is_null( $this->mNamespaceIds ) ) { 674 global $wgNamespaceAliases; 675 # Put namespace names and aliases into a hashtable. 676 # If this is too slow, then we should arrange it so that it is done 677 # before caching. The catch is that at pre-cache time, the above 678 # class-specific fixup hasn't been done. 679 $this->mNamespaceIds = array(); 680 foreach ( $this->getNamespaces() as $index => $name ) { 681 $this->mNamespaceIds[$this->lc( $name )] = $index; 682 } 683 foreach ( $this->getNamespaceAliases() as $name => $index ) { 684 $this->mNamespaceIds[$this->lc( $name )] = $index; 685 } 686 if ( $wgNamespaceAliases ) { 687 foreach ( $wgNamespaceAliases as $name => $index ) { 688 $this->mNamespaceIds[$this->lc( $name )] = $index; 689 } 690 } 691 } 692 return $this->mNamespaceIds; 693 } 694 695 /** 696 * Get a namespace key by value, case insensitive. Canonical namespace 697 * names override custom ones defined for the current language. 698 * 699 * @param string $text 700 * @return int|bool An integer if $text is a valid value otherwise false 701 */ 702 function getNsIndex( $text ) { 703 $lctext = $this->lc( $text ); 704 $ns = MWNamespace::getCanonicalIndex( $lctext ); 705 if ( $ns !== null ) { 706 return $ns; 707 } 708 $ids = $this->getNamespaceIds(); 709 return isset( $ids[$lctext] ) ? $ids[$lctext] : false; 710 } 711 712 /** 713 * short names for language variants used for language conversion links. 714 * 715 * @param string $code 716 * @param bool $usemsg Use the "variantname-xyz" message if it exists 717 * @return string 718 */ 719 function getVariantname( $code, $usemsg = true ) { 720 $msg = "variantname-$code"; 721 if ( $usemsg && wfMessage( $msg )->exists() ) { 722 return $this->getMessageFromDB( $msg ); 723 } 724 $name = self::fetchLanguageName( $code ); 725 if ( $name ) { 726 return $name; # if it's defined as a language name, show that 727 } else { 728 # otherwise, output the language code 729 return $code; 730 } 731 } 732 733 /** 734 * @deprecated since 1.24, doesn't handle conflicting aliases. Use 735 * SpecialPageFactory::getLocalNameFor instead. 736 * @param string $name 737 * @return string 738 */ 739 function specialPage( $name ) { 740 $aliases = $this->getSpecialPageAliases(); 741 if ( isset( $aliases[$name][0] ) ) { 742 $name = $aliases[$name][0]; 743 } 744 return $this->getNsText( NS_SPECIAL ) . ':' . $name; 745 } 746 747 /** 748 * @return array 749 */ 750 function getDatePreferences() { 751 return self::$dataCache->getItem( $this->mCode, 'datePreferences' ); 752 } 753 754 /** 755 * @return array 756 */ 757 function getDateFormats() { 758 return self::$dataCache->getItem( $this->mCode, 'dateFormats' ); 759 } 760 761 /** 762 * @return array|string 763 */ 764 function getDefaultDateFormat() { 765 $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' ); 766 if ( $df === 'dmy or mdy' ) { 767 global $wgAmericanDates; 768 return $wgAmericanDates ? 'mdy' : 'dmy'; 769 } else { 770 return $df; 771 } 772 } 773 774 /** 775 * @return array 776 */ 777 function getDatePreferenceMigrationMap() { 778 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' ); 779 } 780 781 /** 782 * @param string $image 783 * @return array|null 784 */ 785 function getImageFile( $image ) { 786 return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image ); 787 } 788 789 /** 790 * @return array 791 * @since 1.24 792 */ 793 function getImageFiles() { 794 return self::$dataCache->getItem( $this->mCode, 'imageFiles' ); 795 } 796 797 /** 798 * @return array 799 */ 800 function getExtraUserToggles() { 801 return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' ); 802 } 803 804 /** 805 * @param string $tog 806 * @return string 807 */ 808 function getUserToggle( $tog ) { 809 return $this->getMessageFromDB( "tog-$tog" ); 810 } 811 812 /** 813 * Get native language names, indexed by code. 814 * Only those defined in MediaWiki, no other data like CLDR. 815 * If $customisedOnly is true, only returns codes with a messages file 816 * 817 * @param bool $customisedOnly 818 * 819 * @return array 820 * @deprecated since 1.20, use fetchLanguageNames() 821 */ 822 public static function getLanguageNames( $customisedOnly = false ) { 823 return self::fetchLanguageNames( null, $customisedOnly ? 'mwfile' : 'mw' ); 824 } 825 826 /** 827 * Get translated language names. This is done on best effort and 828 * by default this is exactly the same as Language::getLanguageNames. 829 * The CLDR extension provides translated names. 830 * @param string $code Language code. 831 * @return array Language code => language name 832 * @since 1.18.0 833 * @deprecated since 1.20, use fetchLanguageNames() 834 */ 835 public static function getTranslatedLanguageNames( $code ) { 836 return self::fetchLanguageNames( $code, 'all' ); 837 } 838 839 /** 840 * Get an array of language names, indexed by code. 841 * @param null|string $inLanguage Code of language in which to return the names 842 * Use null for autonyms (native names) 843 * @param string $include One of: 844 * 'all' all available languages 845 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default) 846 * 'mwfile' only if the language is in 'mw' *and* has a message file 847 * @return array Language code => language name 848 * @since 1.20 849 */ 850 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) { 851 global $wgExtraLanguageNames; 852 static $coreLanguageNames; 853 854 if ( $coreLanguageNames === null ) { 855 global $IP; 856 include "$IP/languages/Names.php"; 857 } 858 859 // If passed an invalid language code to use, fallback to en 860 if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) { 861 $inLanguage = 'en'; 862 } 863 864 $names = array(); 865 866 if ( $inLanguage ) { 867 # TODO: also include when $inLanguage is null, when this code is more efficient 868 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) ); 869 } 870 871 $mwNames = $wgExtraLanguageNames + $coreLanguageNames; 872 foreach ( $mwNames as $mwCode => $mwName ) { 873 # - Prefer own MediaWiki native name when not using the hook 874 # - For other names just add if not added through the hook 875 if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) { 876 $names[$mwCode] = $mwName; 877 } 878 } 879 880 if ( $include === 'all' ) { 881 return $names; 882 } 883 884 $returnMw = array(); 885 $coreCodes = array_keys( $mwNames ); 886 foreach ( $coreCodes as $coreCode ) { 887 $returnMw[$coreCode] = $names[$coreCode]; 888 } 889 890 if ( $include === 'mwfile' ) { 891 $namesMwFile = array(); 892 # We do this using a foreach over the codes instead of a directory 893 # loop so that messages files in extensions will work correctly. 894 foreach ( $returnMw as $code => $value ) { 895 if ( is_readable( self::getMessagesFileName( $code ) ) 896 || is_readable( self::getJsonMessagesFileName( $code ) ) 897 ) { 898 $namesMwFile[$code] = $names[$code]; 899 } 900 } 901 902 return $namesMwFile; 903 } 904 905 # 'mw' option; default if it's not one of the other two options (all/mwfile) 906 return $returnMw; 907 } 908 909 /** 910 * @param string $code The code of the language for which to get the name 911 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms) 912 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames() 913 * @return string Language name or empty 914 * @since 1.20 915 */ 916 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) { 917 $code = strtolower( $code ); 918 $array = self::fetchLanguageNames( $inLanguage, $include ); 919 return !array_key_exists( $code, $array ) ? '' : $array[$code]; 920 } 921 922 /** 923 * Get a message from the MediaWiki namespace. 924 * 925 * @param string $msg Message name 926 * @return string 927 */ 928 function getMessageFromDB( $msg ) { 929 return wfMessage( $msg )->inLanguage( $this )->text(); 930 } 931 932 /** 933 * Get the native language name of $code. 934 * Only if defined in MediaWiki, no other data like CLDR. 935 * @param string $code 936 * @return string 937 * @deprecated since 1.20, use fetchLanguageName() 938 */ 939 function getLanguageName( $code ) { 940 return self::fetchLanguageName( $code ); 941 } 942 943 /** 944 * @param string $key 945 * @return string 946 */ 947 function getMonthName( $key ) { 948 return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] ); 949 } 950 951 /** 952 * @return array 953 */ 954 function getMonthNamesArray() { 955 $monthNames = array( '' ); 956 for ( $i = 1; $i < 13; $i++ ) { 957 $monthNames[] = $this->getMonthName( $i ); 958 } 959 return $monthNames; 960 } 961 962 /** 963 * @param string $key 964 * @return string 965 */ 966 function getMonthNameGen( $key ) { 967 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] ); 968 } 969 970 /** 971 * @param string $key 972 * @return string 973 */ 974 function getMonthAbbreviation( $key ) { 975 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] ); 976 } 977 978 /** 979 * @return array 980 */ 981 function getMonthAbbreviationsArray() { 982 $monthNames = array( '' ); 983 for ( $i = 1; $i < 13; $i++ ) { 984 $monthNames[] = $this->getMonthAbbreviation( $i ); 985 } 986 return $monthNames; 987 } 988 989 /** 990 * @param string $key 991 * @return string 992 */ 993 function getWeekdayName( $key ) { 994 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] ); 995 } 996 997 /** 998 * @param string $key 999 * @return string 1000 */ 1001 function getWeekdayAbbreviation( $key ) { 1002 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] ); 1003 } 1004 1005 /** 1006 * @param string $key 1007 * @return string 1008 */ 1009 function getIranianCalendarMonthName( $key ) { 1010 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] ); 1011 } 1012 1013 /** 1014 * @param string $key 1015 * @return string 1016 */ 1017 function getHebrewCalendarMonthName( $key ) { 1018 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] ); 1019 } 1020 1021 /** 1022 * @param string $key 1023 * @return string 1024 */ 1025 function getHebrewCalendarMonthNameGen( $key ) { 1026 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] ); 1027 } 1028 1029 /** 1030 * @param string $key 1031 * @return string 1032 */ 1033 function getHijriCalendarMonthName( $key ) { 1034 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] ); 1035 } 1036 1037 /** 1038 * Pass through result from $dateTimeObj->format() 1039 * @param DateTime|bool|null &$dateTimeObj 1040 * @param string $ts 1041 * @param DateTimeZone|bool|null $zone 1042 * @param string $code 1043 * @return string 1044 */ 1045 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) { 1046 if ( !$dateTimeObj ) { 1047 $dateTimeObj = DateTime::createFromFormat( 1048 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' ) 1049 ); 1050 } 1051 return $dateTimeObj->format( $code ); 1052 } 1053 1054 /** 1055 * This is a workalike of PHP's date() function, but with better 1056 * internationalisation, a reduced set of format characters, and a better 1057 * escaping format. 1058 * 1059 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See 1060 * the PHP manual for definitions. There are a number of extensions, which 1061 * start with "x": 1062 * 1063 * xn Do not translate digits of the next numeric format character 1064 * xN Toggle raw digit (xn) flag, stays set until explicitly unset 1065 * xr Use roman numerals for the next numeric format character 1066 * xh Use hebrew numerals for the next numeric format character 1067 * xx Literal x 1068 * xg Genitive month name 1069 * 1070 * xij j (day number) in Iranian calendar 1071 * xiF F (month name) in Iranian calendar 1072 * xin n (month number) in Iranian calendar 1073 * xiy y (two digit year) in Iranian calendar 1074 * xiY Y (full year) in Iranian calendar 1075 * 1076 * xjj j (day number) in Hebrew calendar 1077 * xjF F (month name) in Hebrew calendar 1078 * xjt t (days in month) in Hebrew calendar 1079 * xjx xg (genitive month name) in Hebrew calendar 1080 * xjn n (month number) in Hebrew calendar 1081 * xjY Y (full year) in Hebrew calendar 1082 * 1083 * xmj j (day number) in Hijri calendar 1084 * xmF F (month name) in Hijri calendar 1085 * xmn n (month number) in Hijri calendar 1086 * xmY Y (full year) in Hijri calendar 1087 * 1088 * xkY Y (full year) in Thai solar calendar. Months and days are 1089 * identical to the Gregorian calendar 1090 * xoY Y (full year) in Minguo calendar or Juche year. 1091 * Months and days are identical to the 1092 * Gregorian calendar 1093 * xtY Y (full year) in Japanese nengo. Months and days are 1094 * identical to the Gregorian calendar 1095 * 1096 * Characters enclosed in double quotes will be considered literal (with 1097 * the quotes themselves removed). Unmatched quotes will be considered 1098 * literal quotes. Example: 1099 * 1100 * "The month is" F => The month is January 1101 * i's" => 20'11" 1102 * 1103 * Backslash escaping is also supported. 1104 * 1105 * Input timestamp is assumed to be pre-normalized to the desired local 1106 * time zone, if any. Note that the format characters crUeIOPTZ will assume 1107 * $ts is UTC if $zone is not given. 1108 * 1109 * @param string $format 1110 * @param string $ts 14-character timestamp 1111 * YYYYMMDDHHMMSS 1112 * 01234567890123 1113 * @param DateTimeZone $zone Timezone of $ts 1114 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for. 1115 * Only makes sense if $ts is the current time. 1116 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai? 1117 * 1118 * @throws MWException 1119 * @return string 1120 */ 1121 function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = null ) { 1122 $s = ''; 1123 $raw = false; 1124 $roman = false; 1125 $hebrewNum = false; 1126 $dateTimeObj = false; 1127 $rawToggle = false; 1128 $iranian = false; 1129 $hebrew = false; 1130 $hijri = false; 1131 $thai = false; 1132 $minguo = false; 1133 $tenno = false; 1134 1135 $usedSecond = false; 1136 $usedMinute = false; 1137 $usedHour = false; 1138 $usedAMPM = false; 1139 $usedDay = false; 1140 $usedWeek = false; 1141 $usedMonth = false; 1142 $usedYear = false; 1143 $usedISOYear = false; 1144 $usedIsLeapYear = false; 1145 1146 $usedHebrewMonth = false; 1147 $usedIranianMonth = false; 1148 $usedHijriMonth = false; 1149 $usedHebrewYear = false; 1150 $usedIranianYear = false; 1151 $usedHijriYear = false; 1152 $usedTennoYear = false; 1153 1154 if ( strlen( $ts ) !== 14 ) { 1155 throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" ); 1156 } 1157 1158 if ( !ctype_digit( $ts ) ) { 1159 throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" ); 1160 } 1161 1162 $formatLength = strlen( $format ); 1163 for ( $p = 0; $p < $formatLength; $p++ ) { 1164 $num = false; 1165 $code = $format[$p]; 1166 if ( $code == 'x' && $p < $formatLength - 1 ) { 1167 $code .= $format[++$p]; 1168 } 1169 1170 if ( ( $code === 'xi' 1171 || $code === 'xj' 1172 || $code === 'xk' 1173 || $code === 'xm' 1174 || $code === 'xo' 1175 || $code === 'xt' ) 1176 && $p < $formatLength - 1 ) { 1177 $code .= $format[++$p]; 1178 } 1179 1180 switch ( $code ) { 1181 case 'xx': 1182 $s .= 'x'; 1183 break; 1184 case 'xn': 1185 $raw = true; 1186 break; 1187 case 'xN': 1188 $rawToggle = !$rawToggle; 1189 break; 1190 case 'xr': 1191 $roman = true; 1192 break; 1193 case 'xh': 1194 $hebrewNum = true; 1195 break; 1196 case 'xg': 1197 $usedMonth = true; 1198 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) ); 1199 break; 1200 case 'xjx': 1201 $usedHebrewMonth = true; 1202 if ( !$hebrew ) { 1203 $hebrew = self::tsToHebrew( $ts ); 1204 } 1205 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] ); 1206 break; 1207 case 'd': 1208 $usedDay = true; 1209 $num = substr( $ts, 6, 2 ); 1210 break; 1211 case 'D': 1212 $usedDay = true; 1213 $s .= $this->getWeekdayAbbreviation( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1 ); 1214 break; 1215 case 'j': 1216 $usedDay = true; 1217 $num = intval( substr( $ts, 6, 2 ) ); 1218 break; 1219 case 'xij': 1220 $usedDay = true; 1221 if ( !$iranian ) { 1222 $iranian = self::tsToIranian( $ts ); 1223 } 1224 $num = $iranian[2]; 1225 break; 1226 case 'xmj': 1227 $usedDay = true; 1228 if ( !$hijri ) { 1229 $hijri = self::tsToHijri( $ts ); 1230 } 1231 $num = $hijri[2]; 1232 break; 1233 case 'xjj': 1234 $usedDay = true; 1235 if ( !$hebrew ) { 1236 $hebrew = self::tsToHebrew( $ts ); 1237 } 1238 $num = $hebrew[2]; 1239 break; 1240 case 'l': 1241 $usedDay = true; 1242 $s .= $this->getWeekdayName( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1 ); 1243 break; 1244 case 'F': 1245 $usedMonth = true; 1246 $s .= $this->getMonthName( substr( $ts, 4, 2 ) ); 1247 break; 1248 case 'xiF': 1249 $usedIranianMonth = true; 1250 if ( !$iranian ) { 1251 $iranian = self::tsToIranian( $ts ); 1252 } 1253 $s .= $this->getIranianCalendarMonthName( $iranian[1] ); 1254 break; 1255 case 'xmF': 1256 $usedHijriMonth = true; 1257 if ( !$hijri ) { 1258 $hijri = self::tsToHijri( $ts ); 1259 } 1260 $s .= $this->getHijriCalendarMonthName( $hijri[1] ); 1261 break; 1262 case 'xjF': 1263 $usedHebrewMonth = true; 1264 if ( !$hebrew ) { 1265 $hebrew = self::tsToHebrew( $ts ); 1266 } 1267 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] ); 1268 break; 1269 case 'm': 1270 $usedMonth = true; 1271 $num = substr( $ts, 4, 2 ); 1272 break; 1273 case 'M': 1274 $usedMonth = true; 1275 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) ); 1276 break; 1277 case 'n': 1278 $usedMonth = true; 1279 $num = intval( substr( $ts, 4, 2 ) ); 1280 break; 1281 case 'xin': 1282 $usedIranianMonth = true; 1283 if ( !$iranian ) { 1284 $iranian = self::tsToIranian( $ts ); 1285 } 1286 $num = $iranian[1]; 1287 break; 1288 case 'xmn': 1289 $usedHijriMonth = true; 1290 if ( !$hijri ) { 1291 $hijri = self::tsToHijri ( $ts ); 1292 } 1293 $num = $hijri[1]; 1294 break; 1295 case 'xjn': 1296 $usedHebrewMonth = true; 1297 if ( !$hebrew ) { 1298 $hebrew = self::tsToHebrew( $ts ); 1299 } 1300 $num = $hebrew[1]; 1301 break; 1302 case 'xjt': 1303 $usedHebrewMonth = true; 1304 if ( !$hebrew ) { 1305 $hebrew = self::tsToHebrew( $ts ); 1306 } 1307 $num = $hebrew[3]; 1308 break; 1309 case 'Y': 1310 $usedYear = true; 1311 $num = substr( $ts, 0, 4 ); 1312 break; 1313 case 'xiY': 1314 $usedIranianYear = true; 1315 if ( !$iranian ) { 1316 $iranian = self::tsToIranian( $ts ); 1317 } 1318 $num = $iranian[0]; 1319 break; 1320 case 'xmY': 1321 $usedHijriYear = true; 1322 if ( !$hijri ) { 1323 $hijri = self::tsToHijri( $ts ); 1324 } 1325 $num = $hijri[0]; 1326 break; 1327 case 'xjY': 1328 $usedHebrewYear = true; 1329 if ( !$hebrew ) { 1330 $hebrew = self::tsToHebrew( $ts ); 1331 } 1332 $num = $hebrew[0]; 1333 break; 1334 case 'xkY': 1335 $usedYear = true; 1336 if ( !$thai ) { 1337 $thai = self::tsToYear( $ts, 'thai' ); 1338 } 1339 $num = $thai[0]; 1340 break; 1341 case 'xoY': 1342 $usedYear = true; 1343 if ( !$minguo ) { 1344 $minguo = self::tsToYear( $ts, 'minguo' ); 1345 } 1346 $num = $minguo[0]; 1347 break; 1348 case 'xtY': 1349 $usedTennoYear = true; 1350 if ( !$tenno ) { 1351 $tenno = self::tsToYear( $ts, 'tenno' ); 1352 } 1353 $num = $tenno[0]; 1354 break; 1355 case 'y': 1356 $usedYear = true; 1357 $num = substr( $ts, 2, 2 ); 1358 break; 1359 case 'xiy': 1360 $usedIranianYear = true; 1361 if ( !$iranian ) { 1362 $iranian = self::tsToIranian( $ts ); 1363 } 1364 $num = substr( $iranian[0], -2 ); 1365 break; 1366 case 'a': 1367 $usedAMPM = true; 1368 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm'; 1369 break; 1370 case 'A': 1371 $usedAMPM = true; 1372 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM'; 1373 break; 1374 case 'g': 1375 $usedHour = true; 1376 $h = substr( $ts, 8, 2 ); 1377 $num = $h % 12 ? $h % 12 : 12; 1378 break; 1379 case 'G': 1380 $usedHour = true; 1381 $num = intval( substr( $ts, 8, 2 ) ); 1382 break; 1383 case 'h': 1384 $usedHour = true; 1385 $h = substr( $ts, 8, 2 ); 1386 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 ); 1387 break; 1388 case 'H': 1389 $usedHour = true; 1390 $num = substr( $ts, 8, 2 ); 1391 break; 1392 case 'i': 1393 $usedMinute = true; 1394 $num = substr( $ts, 10, 2 ); 1395 break; 1396 case 's': 1397 $usedSecond = true; 1398 $num = substr( $ts, 12, 2 ); 1399 break; 1400 case 'c': 1401 case 'r': 1402 $usedSecond = true; 1403 // fall through 1404 case 'e': 1405 case 'O': 1406 case 'P': 1407 case 'T': 1408 $s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code ); 1409 break; 1410 case 'w': 1411 case 'N': 1412 case 'z': 1413 $usedDay = true; 1414 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code ); 1415 break; 1416 case 'W': 1417 $usedWeek = true; 1418 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code ); 1419 break; 1420 case 't': 1421 $usedMonth = true; 1422 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code ); 1423 break; 1424 case 'L': 1425 $usedIsLeapYear = true; 1426 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code ); 1427 break; 1428 case 'o': 1429 $usedISOYear = true; 1430 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code ); 1431 break; 1432 case 'U': 1433 $usedSecond = true; 1434 // fall through 1435 case 'I': 1436 case 'Z': 1437 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code ); 1438 break; 1439 case '\\': 1440 # Backslash escaping 1441 if ( $p < $formatLength - 1 ) { 1442 $s .= $format[++$p]; 1443 } else { 1444 $s .= '\\'; 1445 } 1446 break; 1447 case '"': 1448 # Quoted literal 1449 if ( $p < $formatLength - 1 ) { 1450 $endQuote = strpos( $format, '"', $p + 1 ); 1451 if ( $endQuote === false ) { 1452 # No terminating quote, assume literal " 1453 $s .= '"'; 1454 } else { 1455 $s .= substr( $format, $p + 1, $endQuote - $p - 1 ); 1456 $p = $endQuote; 1457 } 1458 } else { 1459 # Quote at end of string, assume literal " 1460 $s .= '"'; 1461 } 1462 break; 1463 default: 1464 $s .= $format[$p]; 1465 } 1466 if ( $num !== false ) { 1467 if ( $rawToggle || $raw ) { 1468 $s .= $num; 1469 $raw = false; 1470 } elseif ( $roman ) { 1471 $s .= Language::romanNumeral( $num ); 1472 $roman = false; 1473 } elseif ( $hebrewNum ) { 1474 $s .= self::hebrewNumeral( $num ); 1475 $hebrewNum = false; 1476 } else { 1477 $s .= $this->formatNum( $num, true ); 1478 } 1479 } 1480 } 1481 1482 if ( $usedSecond ) { 1483 $ttl = 1; 1484 } elseif ( $usedMinute ) { 1485 $ttl = 60 - substr( $ts, 12, 2 ); 1486 } elseif ( $usedHour ) { 1487 $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 ); 1488 } elseif ( $usedAMPM ) { 1489 $ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 ); 1490 } elseif ( $usedDay || $usedHebrewMonth || $usedIranianMonth || $usedHijriMonth || $usedHebrewYear || $usedIranianYear || $usedHijriYear || $usedTennoYear ) { 1491 // @todo Someone who understands the non-Gregorian calendars should write proper logic for them 1492 // so that they don't need purged every day. 1493 $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 ); 1494 } else { 1495 $possibleTtls = array(); 1496 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 ); 1497 if ( $usedWeek ) { 1498 $possibleTtls[] = ( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 + $timeRemainingInDay; 1499 } elseif ( $usedISOYear ) { 1500 // December 28th falls on the last ISO week of the year, every year. 1501 // The last ISO week of a year can be 52 or 53. 1502 $lastWeekOfISOYear = DateTime::createFromFormat( 'Ymd', substr( $ts, 0, 4 ) . '1228', $zone ?: new DateTimeZone( 'UTC' ) )->format( 'W' ); 1503 $currentISOWeek = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' ); 1504 $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek; 1505 $timeRemainingInWeek = ( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 + $timeRemainingInDay; 1506 $possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek; 1507 } 1508 1509 if ( $usedMonth ) { 1510 $possibleTtls[] = ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) - substr( $ts, 6, 2 ) ) * 86400 + $timeRemainingInDay; 1511 } elseif ( $usedYear ) { 1512 $possibleTtls[] = ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400 1513 + $timeRemainingInDay; 1514 } elseif ( $usedIsLeapYear ) { 1515 $year = substr( $ts, 0, 4 ); 1516 $timeRemainingInYear = ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400 1517 + $timeRemainingInDay; 1518 $mod = $year % 4; 1519 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) { 1520 // this isn't a leap year. see when the next one starts 1521 $nextCandidate = $year - $mod + 4; 1522 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) { 1523 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 + $timeRemainingInYear; 1524 } else { 1525 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 + $timeRemainingInYear; 1526 } 1527 } else { 1528 // this is a leap year, so the next year isn't 1529 $possibleTtls[] = $timeRemainingInYear; 1530 } 1531 } 1532 1533 if ( $possibleTtls ) { 1534 $ttl = min( $possibleTtls ); 1535 } 1536 } 1537 1538 return $s; 1539 } 1540 1541 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ); 1542 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ); 1543 1544 /** 1545 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert 1546 * Gregorian dates to Iranian dates. Originally written in C, it 1547 * is released under the terms of GNU Lesser General Public 1548 * License. Conversion to PHP was performed by Niklas Laxström. 1549 * 1550 * Link: http://www.farsiweb.info/jalali/jalali.c 1551 * 1552 * @param string $ts 1553 * 1554 * @return string 1555 */ 1556 private static function tsToIranian( $ts ) { 1557 $gy = substr( $ts, 0, 4 ) -1600; 1558 $gm = substr( $ts, 4, 2 ) -1; 1559 $gd = substr( $ts, 6, 2 ) -1; 1560 1561 # Days passed from the beginning (including leap years) 1562 $gDayNo = 365 * $gy 1563 + floor( ( $gy + 3 ) / 4 ) 1564 - floor( ( $gy + 99 ) / 100 ) 1565 + floor( ( $gy + 399 ) / 400 ); 1566 1567 // Add days of the past months of this year 1568 for ( $i = 0; $i < $gm; $i++ ) { 1569 $gDayNo += self::$GREG_DAYS[$i]; 1570 } 1571 1572 // Leap years 1573 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) { 1574 $gDayNo++; 1575 } 1576 1577 // Days passed in current month 1578 $gDayNo += (int)$gd; 1579 1580 $jDayNo = $gDayNo - 79; 1581 1582 $jNp = floor( $jDayNo / 12053 ); 1583 $jDayNo %= 12053; 1584 1585 $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 ); 1586 $jDayNo %= 1461; 1587 1588 if ( $jDayNo >= 366 ) { 1589 $jy += floor( ( $jDayNo - 1 ) / 365 ); 1590 $jDayNo = floor( ( $jDayNo - 1 ) % 365 ); 1591 } 1592 1593 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) { 1594 $jDayNo -= self::$IRANIAN_DAYS[$i]; 1595 } 1596 1597 $jm = $i + 1; 1598 $jd = $jDayNo + 1; 1599 1600 return array( $jy, $jm, $jd ); 1601 } 1602 1603 /** 1604 * Converting Gregorian dates to Hijri dates. 1605 * 1606 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license 1607 * 1608 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0 1609 * 1610 * @param string $ts 1611 * 1612 * @return string 1613 */ 1614 private static function tsToHijri( $ts ) { 1615 $year = substr( $ts, 0, 4 ); 1616 $month = substr( $ts, 4, 2 ); 1617 $day = substr( $ts, 6, 2 ); 1618 1619 $zyr = $year; 1620 $zd = $day; 1621 $zm = $month; 1622 $zy = $zyr; 1623 1624 if ( 1625 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) || 1626 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) ) 1627 ) { 1628 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) + 1629 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) - 1630 (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) + 1631 $zd - 32075; 1632 } else { 1633 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) + 1634 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777; 1635 } 1636 1637 $zl = $zjd -1948440 + 10632; 1638 $zn = (int)( ( $zl - 1 ) / 10631 ); 1639 $zl = $zl - 10631 * $zn + 354; 1640 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) + 1641 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) ); 1642 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - 1643 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29; 1644 $zm = (int)( ( 24 * $zl ) / 709 ); 1645 $zd = $zl - (int)( ( 709 * $zm ) / 24 ); 1646 $zy = 30 * $zn + $zj - 30; 1647 1648 return array( $zy, $zm, $zd ); 1649 } 1650 1651 /** 1652 * Converting Gregorian dates to Hebrew dates. 1653 * 1654 * Based on a JavaScript code by Abu Mami and Yisrael Hersch 1655 * ([email protected], http://www.kaluach.net), who permitted 1656 * to translate the relevant functions into PHP and release them under 1657 * GNU GPL. 1658 * 1659 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13 1660 * and Adar II is 14. In a non-leap year, Adar is 6. 1661 * 1662 * @param string $ts 1663 * 1664 * @return string 1665 */ 1666 private static function tsToHebrew( $ts ) { 1667 # Parse date 1668 $year = substr( $ts, 0, 4 ); 1669 $month = substr( $ts, 4, 2 ); 1670 $day = substr( $ts, 6, 2 ); 1671 1672 # Calculate Hebrew year 1673 $hebrewYear = $year + 3760; 1674 1675 # Month number when September = 1, August = 12 1676 $month += 4; 1677 if ( $month > 12 ) { 1678 # Next year 1679 $month -= 12; 1680 $year++; 1681 $hebrewYear++; 1682 } 1683 1684 # Calculate day of year from 1 September 1685 $dayOfYear = $day; 1686 for ( $i = 1; $i < $month; $i++ ) { 1687 if ( $i == 6 ) { 1688 # February 1689 $dayOfYear += 28; 1690 # Check if the year is leap 1691 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) { 1692 $dayOfYear++; 1693 } 1694 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) { 1695 $dayOfYear += 30; 1696 } else { 1697 $dayOfYear += 31; 1698 } 1699 } 1700 1701 # Calculate the start of the Hebrew year 1702 $start = self::hebrewYearStart( $hebrewYear ); 1703 1704 # Calculate next year's start 1705 if ( $dayOfYear <= $start ) { 1706 # Day is before the start of the year - it is the previous year 1707 # Next year's start 1708 $nextStart = $start; 1709 # Previous year 1710 $year--; 1711 $hebrewYear--; 1712 # Add days since previous year's 1 September 1713 $dayOfYear += 365; 1714 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) { 1715 # Leap year 1716 $dayOfYear++; 1717 } 1718 # Start of the new (previous) year 1719 $start = self::hebrewYearStart( $hebrewYear ); 1720 } else { 1721 # Next year's start 1722 $nextStart = self::hebrewYearStart( $hebrewYear + 1 ); 1723 } 1724 1725 # Calculate Hebrew day of year 1726 $hebrewDayOfYear = $dayOfYear - $start; 1727 1728 # Difference between year's days 1729 $diff = $nextStart - $start; 1730 # Add 12 (or 13 for leap years) days to ignore the difference between 1731 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the 1732 # difference is only about the year type 1733 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) { 1734 $diff += 13; 1735 } else { 1736 $diff += 12; 1737 } 1738 1739 # Check the year pattern, and is leap year 1740 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year 1741 # This is mod 30, to work on both leap years (which add 30 days of Adar I) 1742 # and non-leap years 1743 $yearPattern = $diff % 30; 1744 # Check if leap year 1745 $isLeap = $diff >= 30; 1746 1747 # Calculate day in the month from number of day in the Hebrew year 1748 # Don't check Adar - if the day is not in Adar, we will stop before; 1749 # if it is in Adar, we will use it to check if it is Adar I or Adar II 1750 $hebrewDay = $hebrewDayOfYear; 1751 $hebrewMonth = 1; 1752 $days = 0; 1753 while ( $hebrewMonth <= 12 ) { 1754 # Calculate days in this month 1755 if ( $isLeap && $hebrewMonth == 6 ) { 1756 # Adar in a leap year 1757 if ( $isLeap ) { 1758 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days 1759 $days = 30; 1760 if ( $hebrewDay <= $days ) { 1761 # Day in Adar I 1762 $hebrewMonth = 13; 1763 } else { 1764 # Subtract the days of Adar I 1765 $hebrewDay -= $days; 1766 # Try Adar II 1767 $days = 29; 1768 if ( $hebrewDay <= $days ) { 1769 # Day in Adar II 1770 $hebrewMonth = 14; 1771 } 1772 } 1773 } 1774 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) { 1775 # Cheshvan in a complete year (otherwise as the rule below) 1776 $days = 30; 1777 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) { 1778 # Kislev in an incomplete year (otherwise as the rule below) 1779 $days = 29; 1780 } else { 1781 # Odd months have 30 days, even have 29 1782 $days = 30 - ( $hebrewMonth - 1 ) % 2; 1783 } 1784 if ( $hebrewDay <= $days ) { 1785 # In the current month 1786 break; 1787 } else { 1788 # Subtract the days of the current month 1789 $hebrewDay -= $days; 1790 # Try in the next month 1791 $hebrewMonth++; 1792 } 1793 } 1794 1795 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days ); 1796 } 1797 1798 /** 1799 * This calculates the Hebrew year start, as days since 1 September. 1800 * Based on Carl Friedrich Gauss algorithm for finding Easter date. 1801 * Used for Hebrew date. 1802 * 1803 * @param int $year 1804 * 1805 * @return string 1806 */ 1807 private static function hebrewYearStart( $year ) { 1808 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 ); 1809 $b = intval( ( $year - 1 ) % 4 ); 1810 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 ); 1811 if ( $m < 0 ) { 1812 $m--; 1813 } 1814 $Mar = intval( $m ); 1815 if ( $m < 0 ) { 1816 $m++; 1817 } 1818 $m -= $Mar; 1819 1820 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 ); 1821 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) { 1822 $Mar++; 1823 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) { 1824 $Mar += 2; 1825 } elseif ( $c == 2 || $c == 4 || $c == 6 ) { 1826 $Mar++; 1827 } 1828 1829 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24; 1830 return $Mar; 1831 } 1832 1833 /** 1834 * Algorithm to convert Gregorian dates to Thai solar dates, 1835 * Minguo dates or Minguo dates. 1836 * 1837 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar 1838 * http://en.wikipedia.org/wiki/Minguo_calendar 1839 * http://en.wikipedia.org/wiki/Japanese_era_name 1840 * 1841 * @param string $ts 14-character timestamp 1842 * @param string $cName Calender name 1843 * @return array Converted year, month, day 1844 */ 1845 private static function tsToYear( $ts, $cName ) { 1846 $gy = substr( $ts, 0, 4 ); 1847 $gm = substr( $ts, 4, 2 ); 1848 $gd = substr( $ts, 6, 2 ); 1849 1850 if ( !strcmp( $cName, 'thai' ) ) { 1851 # Thai solar dates 1852 # Add 543 years to the Gregorian calendar 1853 # Months and days are identical 1854 $gy_offset = $gy + 543; 1855 } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) { 1856 # Minguo dates 1857 # Deduct 1911 years from the Gregorian calendar 1858 # Months and days are identical 1859 $gy_offset = $gy - 1911; 1860 } elseif ( !strcmp( $cName, 'tenno' ) ) { 1861 # Nengō dates up to Meiji period 1862 # Deduct years from the Gregorian calendar 1863 # depending on the nengo periods 1864 # Months and days are identical 1865 if ( ( $gy < 1912 ) 1866 || ( ( $gy == 1912 ) && ( $gm < 7 ) ) 1867 || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) 1868 ) { 1869 # Meiji period 1870 $gy_gannen = $gy - 1868 + 1; 1871 $gy_offset = $gy_gannen; 1872 if ( $gy_gannen == 1 ) { 1873 $gy_offset = '元'; 1874 } 1875 $gy_offset = '明治' . $gy_offset; 1876 } elseif ( 1877 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) || 1878 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) || 1879 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) || 1880 ( ( $gy == 1926 ) && ( $gm < 12 ) ) || 1881 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) ) 1882 ) { 1883 # Taishō period 1884 $gy_gannen = $gy - 1912 + 1; 1885 $gy_offset = $gy_gannen; 1886 if ( $gy_gannen == 1 ) { 1887 $gy_offset = '元'; 1888 } 1889 $gy_offset = '大正' . $gy_offset; 1890 } elseif ( 1891 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) || 1892 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) || 1893 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) ) 1894 ) { 1895 # Shōwa period 1896 $gy_gannen = $gy - 1926 + 1; 1897 $gy_offset = $gy_gannen; 1898 if ( $gy_gannen == 1 ) { 1899 $gy_offset = '元'; 1900 } 1901 $gy_offset = '昭和' . $gy_offset; 1902 } else { 1903 # Heisei period 1904 $gy_gannen = $gy - 1989 + 1; 1905 $gy_offset = $gy_gannen; 1906 if ( $gy_gannen == 1 ) { 1907 $gy_offset = '元'; 1908 } 1909 $gy_offset = '平成' . $gy_offset; 1910 } 1911 } else { 1912 $gy_offset = $gy; 1913 } 1914 1915 return array( $gy_offset, $gm, $gd ); 1916 } 1917 1918 /** 1919 * Roman number formatting up to 10000 1920 * 1921 * @param int $num 1922 * 1923 * @return string 1924 */ 1925 static function romanNumeral( $num ) { 1926 static $table = array( 1927 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ), 1928 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ), 1929 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ), 1930 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM', 1931 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ) 1932 ); 1933 1934 $num = intval( $num ); 1935 if ( $num > 10000 || $num <= 0 ) { 1936 return $num; 1937 } 1938 1939 $s = ''; 1940 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) { 1941 if ( $num >= $pow10 ) { 1942 $s .= $table[$i][(int)floor( $num / $pow10 )]; 1943 } 1944 $num = $num % $pow10; 1945 } 1946 return $s; 1947 } 1948 1949 /** 1950 * Hebrew Gematria number formatting up to 9999 1951 * 1952 * @param int $num 1953 * 1954 * @return string 1955 */ 1956 static function hebrewNumeral( $num ) { 1957 static $table = array( 1958 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ), 1959 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ), 1960 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ), 1961 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ) 1962 ); 1963 1964 $num = intval( $num ); 1965 if ( $num > 9999 || $num <= 0 ) { 1966 return $num; 1967 } 1968 1969 $s = ''; 1970 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) { 1971 if ( $num >= $pow10 ) { 1972 if ( $num == 15 || $num == 16 ) { 1973 $s .= $table[0][9] . $table[0][$num - 9]; 1974 $num = 0; 1975 } else { 1976 $s .= $table[$i][intval( ( $num / $pow10 ) )]; 1977 if ( $pow10 == 1000 ) { 1978 $s .= "'"; 1979 } 1980 } 1981 } 1982 $num = $num % $pow10; 1983 } 1984 if ( strlen( $s ) == 2 ) { 1985 $str = $s . "'"; 1986 } else { 1987 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"'; 1988 $str .= substr( $s, strlen( $s ) - 2, 2 ); 1989 } 1990 $start = substr( $str, 0, strlen( $str ) - 2 ); 1991 $end = substr( $str, strlen( $str ) - 2 ); 1992 switch ( $end ) { 1993 case 'כ': 1994 $str = $start . 'ך'; 1995 break; 1996 case 'מ': 1997 $str = $start . 'ם'; 1998 break; 1999 case 'נ': 2000 $str = $start . 'ן'; 2001 break; 2002 case 'פ': 2003 $str = $start . 'ף'; 2004 break; 2005 case 'צ': 2006 $str = $start . 'ץ'; 2007 break; 2008 } 2009 return $str; 2010 } 2011 2012 /** 2013 * Used by date() and time() to adjust the time output. 2014 * 2015 * @param string $ts The time in date('YmdHis') format 2016 * @param mixed $tz Adjust the time by this amount (default false, mean we 2017 * get user timecorrection setting) 2018 * @return int 2019 */ 2020 function userAdjust( $ts, $tz = false ) { 2021 global $wgUser, $wgLocalTZoffset; 2022 2023 if ( $tz === false ) { 2024 $tz = $wgUser->getOption( 'timecorrection' ); 2025 } 2026 2027 $data = explode( '|', $tz, 3 ); 2028 2029 if ( $data[0] == 'ZoneInfo' ) { 2030 wfSuppressWarnings(); 2031 $userTZ = timezone_open( $data[2] ); 2032 wfRestoreWarnings(); 2033 if ( $userTZ !== false ) { 2034 $date = date_create( $ts, timezone_open( 'UTC' ) ); 2035 date_timezone_set( $date, $userTZ ); 2036 $date = date_format( $date, 'YmdHis' ); 2037 return $date; 2038 } 2039 # Unrecognized timezone, default to 'Offset' with the stored offset. 2040 $data[0] = 'Offset'; 2041 } 2042 2043 if ( $data[0] == 'System' || $tz == '' ) { 2044 # Global offset in minutes. 2045 $minDiff = $wgLocalTZoffset; 2046 } elseif ( $data[0] == 'Offset' ) { 2047 $minDiff = intval( $data[1] ); 2048 } else { 2049 $data = explode( ':', $tz ); 2050 if ( count( $data ) == 2 ) { 2051 $data[0] = intval( $data[0] ); 2052 $data[1] = intval( $data[1] ); 2053 $minDiff = abs( $data[0] ) * 60 + $data[1]; 2054 if ( $data[0] < 0 ) { 2055 $minDiff = -$minDiff; 2056 } 2057 } else { 2058 $minDiff = intval( $data[0] ) * 60; 2059 } 2060 } 2061 2062 # No difference ? Return time unchanged 2063 if ( 0 == $minDiff ) { 2064 return $ts; 2065 } 2066 2067 wfSuppressWarnings(); // E_STRICT system time bitching 2068 # Generate an adjusted date; take advantage of the fact that mktime 2069 # will normalize out-of-range values so we don't have to split $minDiff 2070 # into hours and minutes. 2071 $t = mktime( ( 2072 (int)substr( $ts, 8, 2 ) ), # Hours 2073 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes 2074 (int)substr( $ts, 12, 2 ), # Seconds 2075 (int)substr( $ts, 4, 2 ), # Month 2076 (int)substr( $ts, 6, 2 ), # Day 2077 (int)substr( $ts, 0, 4 ) ); # Year 2078 2079 $date = date( 'YmdHis', $t ); 2080 wfRestoreWarnings(); 2081 2082 return $date; 2083 } 2084 2085 /** 2086 * This is meant to be used by time(), date(), and timeanddate() to get 2087 * the date preference they're supposed to use, it should be used in 2088 * all children. 2089 * 2090 *<code> 2091 * function timeanddate([...], $format = true) { 2092 * $datePreference = $this->dateFormat($format); 2093 * [...] 2094 * } 2095 *</code> 2096 * 2097 * @param int|string|bool $usePrefs If true, the user's preference is used 2098 * if false, the site/language default is used 2099 * if int/string, assumed to be a format. 2100 * @return string 2101 */ 2102 function dateFormat( $usePrefs = true ) { 2103 global $wgUser; 2104 2105 if ( is_bool( $usePrefs ) ) { 2106 if ( $usePrefs ) { 2107 $datePreference = $wgUser->getDatePreference(); 2108 } else { 2109 $datePreference = (string)User::getDefaultOption( 'date' ); 2110 } 2111 } else { 2112 $datePreference = (string)$usePrefs; 2113 } 2114 2115 // return int 2116 if ( $datePreference == '' ) { 2117 return 'default'; 2118 } 2119 2120 return $datePreference; 2121 } 2122 2123 /** 2124 * Get a format string for a given type and preference 2125 * @param string $type May be date, time or both 2126 * @param string $pref The format name as it appears in Messages*.php 2127 * 2128 * @since 1.22 New type 'pretty' that provides a more readable timestamp format 2129 * 2130 * @return string 2131 */ 2132 function getDateFormatString( $type, $pref ) { 2133 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) { 2134 if ( $pref == 'default' ) { 2135 $pref = $this->getDefaultDateFormat(); 2136 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" ); 2137 } else { 2138 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" ); 2139 2140 if ( $type === 'pretty' && $df === null ) { 2141 $df = $this->getDateFormatString( 'date', $pref ); 2142 } 2143 2144 if ( $df === null ) { 2145 $pref = $this->getDefaultDateFormat(); 2146 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" ); 2147 } 2148 } 2149 $this->dateFormatStrings[$type][$pref] = $df; 2150 } 2151 return $this->dateFormatStrings[$type][$pref]; 2152 } 2153 2154 /** 2155 * @param string $ts The time format which needs to be turned into a 2156 * date('YmdHis') format with wfTimestamp(TS_MW,$ts) 2157 * @param bool $adj Whether to adjust the time output according to the 2158 * user configured offset ($timecorrection) 2159 * @param mixed $format True to use user's date format preference 2160 * @param string|bool $timecorrection The time offset as returned by 2161 * validateTimeZone() in Special:Preferences 2162 * @return string 2163 */ 2164 function date( $ts, $adj = false, $format = true, $timecorrection = false ) { 2165 $ts = wfTimestamp( TS_MW, $ts ); 2166 if ( $adj ) { 2167 $ts = $this->userAdjust( $ts, $timecorrection ); 2168 } 2169 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) ); 2170 return $this->sprintfDate( $df, $ts ); 2171 } 2172 2173 /** 2174 * @param string $ts The time format which needs to be turned into a 2175 * date('YmdHis') format with wfTimestamp(TS_MW,$ts) 2176 * @param bool $adj Whether to adjust the time output according to the 2177 * user configured offset ($timecorrection) 2178 * @param mixed $format True to use user's date format preference 2179 * @param string|bool $timecorrection The time offset as returned by 2180 * validateTimeZone() in Special:Preferences 2181 * @return string 2182 */ 2183 function time( $ts, $adj = false, $format = true, $timecorrection = false ) { 2184 $ts = wfTimestamp( TS_MW, $ts ); 2185 if ( $adj ) { 2186 $ts = $this->userAdjust( $ts, $timecorrection ); 2187 } 2188 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) ); 2189 return $this->sprintfDate( $df, $ts ); 2190 } 2191 2192 /** 2193 * @param string $ts The time format which needs to be turned into a 2194 * date('YmdHis') format with wfTimestamp(TS_MW,$ts) 2195 * @param bool $adj Whether to adjust the time output according to the 2196 * user configured offset ($timecorrection) 2197 * @param mixed $format What format to return, if it's false output the 2198 * default one (default true) 2199 * @param string|bool $timecorrection The time offset as returned by 2200 * validateTimeZone() in Special:Preferences 2201 * @return string 2202 */ 2203 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) { 2204 $ts = wfTimestamp( TS_MW, $ts ); 2205 if ( $adj ) { 2206 $ts = $this->userAdjust( $ts, $timecorrection ); 2207 } 2208 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) ); 2209 return $this->sprintfDate( $df, $ts ); 2210 } 2211 2212 /** 2213 * Takes a number of seconds and turns it into a text using values such as hours and minutes. 2214 * 2215 * @since 1.20 2216 * 2217 * @param int $seconds The amount of seconds. 2218 * @param array $chosenIntervals The intervals to enable. 2219 * 2220 * @return string 2221 */ 2222 public function formatDuration( $seconds, array $chosenIntervals = array() ) { 2223 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals ); 2224 2225 $segments = array(); 2226 2227 foreach ( $intervals as $intervalName => $intervalValue ) { 2228 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks, 2229 // duration-years, duration-decades, duration-centuries, duration-millennia 2230 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue ); 2231 $segments[] = $message->inLanguage( $this )->escaped(); 2232 } 2233 2234 return $this->listToText( $segments ); 2235 } 2236 2237 /** 2238 * Takes a number of seconds and returns an array with a set of corresponding intervals. 2239 * For example 65 will be turned into array( minutes => 1, seconds => 5 ). 2240 * 2241 * @since 1.20 2242 * 2243 * @param int $seconds The amount of seconds. 2244 * @param array $chosenIntervals The intervals to enable. 2245 * 2246 * @return array 2247 */ 2248 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) { 2249 if ( empty( $chosenIntervals ) ) { 2250 $chosenIntervals = array( 2251 'millennia', 2252 'centuries', 2253 'decades', 2254 'years', 2255 'days', 2256 'hours', 2257 'minutes', 2258 'seconds' 2259 ); 2260 } 2261 2262 $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) ); 2263 $sortedNames = array_keys( $intervals ); 2264 $smallestInterval = array_pop( $sortedNames ); 2265 2266 $segments = array(); 2267 2268 foreach ( $intervals as $name => $length ) { 2269 $value = floor( $seconds / $length ); 2270 2271 if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) { 2272 $seconds -= $value * $length; 2273 $segments[$name] = $value; 2274 } 2275 } 2276 2277 return $segments; 2278 } 2279 2280 /** 2281 * Internal helper function for userDate(), userTime() and userTimeAndDate() 2282 * 2283 * @param string $type Can be 'date', 'time' or 'both' 2284 * @param string $ts The time format which needs to be turned into a 2285 * date('YmdHis') format with wfTimestamp(TS_MW,$ts) 2286 * @param User $user User object used to get preferences for timezone and format 2287 * @param array $options Array, can contain the following keys: 2288 * - 'timecorrection': time correction, can have the following values: 2289 * - true: use user's preference 2290 * - false: don't use time correction 2291 * - int: value of time correction in minutes 2292 * - 'format': format to use, can have the following values: 2293 * - true: use user's preference 2294 * - false: use default preference 2295 * - string: format to use 2296 * @since 1.19 2297 * @return string 2298 */ 2299 private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) { 2300 $ts = wfTimestamp( TS_MW, $ts ); 2301 $options += array( 'timecorrection' => true, 'format' => true ); 2302 if ( $options['timecorrection'] !== false ) { 2303 if ( $options['timecorrection'] === true ) { 2304 $offset = $user->getOption( 'timecorrection' ); 2305 } else { 2306 $offset = $options['timecorrection']; 2307 } 2308 $ts = $this->userAdjust( $ts, $offset ); 2309 } 2310 if ( $options['format'] === true ) { 2311 $format = $user->getDatePreference(); 2312 } else { 2313 $format = $options['format']; 2314 } 2315 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) ); 2316 return $this->sprintfDate( $df, $ts ); 2317 } 2318 2319 /** 2320 * Get the formatted date for the given timestamp and formatted for 2321 * the given user. 2322 * 2323 * @param mixed $ts Mixed: the time format which needs to be turned into a 2324 * date('YmdHis') format with wfTimestamp(TS_MW,$ts) 2325 * @param User $user User object used to get preferences for timezone and format 2326 * @param array $options Array, can contain the following keys: 2327 * - 'timecorrection': time correction, can have the following values: 2328 * - true: use user's preference 2329 * - false: don't use time correction 2330 * - int: value of time correction in minutes 2331 * - 'format': format to use, can have the following values: 2332 * - true: use user's preference 2333 * - false: use default preference 2334 * - string: format to use 2335 * @since 1.19 2336 * @return string 2337 */ 2338 public function userDate( $ts, User $user, array $options = array() ) { 2339 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options ); 2340 } 2341 2342 /** 2343 * Get the formatted time for the given timestamp and formatted for 2344 * the given user. 2345 * 2346 * @param mixed $ts The time format which needs to be turned into a 2347 * date('YmdHis') format with wfTimestamp(TS_MW,$ts) 2348 * @param User $user User object used to get preferences for timezone and format 2349 * @param array $options Array, can contain the following keys: 2350 * - 'timecorrection': time correction, can have the following values: 2351 * - true: use user's preference 2352 * - false: don't use time correction 2353 * - int: value of time correction in minutes 2354 * - 'format': format to use, can have the following values: 2355 * - true: use user's preference 2356 * - false: use default preference 2357 * - string: format to use 2358 * @since 1.19 2359 * @return string 2360 */ 2361 public function userTime( $ts, User $user, array $options = array() ) { 2362 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options ); 2363 } 2364 2365 /** 2366 * Get the formatted date and time for the given timestamp and formatted for 2367 * the given user. 2368 * 2369 * @param mixed $ts The time format which needs to be turned into a 2370 * date('YmdHis') format with wfTimestamp(TS_MW,$ts) 2371 * @param User $user User object used to get preferences for timezone and format 2372 * @param array $options Array, can contain the following keys: 2373 * - 'timecorrection': time correction, can have the following values: 2374 * - true: use user's preference 2375 * - false: don't use time correction 2376 * - int: value of time correction in minutes 2377 * - 'format': format to use, can have the following values: 2378 * - true: use user's preference 2379 * - false: use default preference 2380 * - string: format to use 2381 * @since 1.19 2382 * @return string 2383 */ 2384 public function userTimeAndDate( $ts, User $user, array $options = array() ) { 2385 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options ); 2386 } 2387 2388 /** 2389 * Convert an MWTimestamp into a pretty human-readable timestamp using 2390 * the given user preferences and relative base time. 2391 * 2392 * DO NOT USE THIS FUNCTION DIRECTLY. Instead, call MWTimestamp::getHumanTimestamp 2393 * on your timestamp object, which will then call this function. Calling 2394 * this function directly will cause hooks to be skipped over. 2395 * 2396 * @see MWTimestamp::getHumanTimestamp 2397 * @param MWTimestamp $ts Timestamp to prettify 2398 * @param MWTimestamp $relativeTo Base timestamp 2399 * @param User $user User preferences to use 2400 * @return string Human timestamp 2401 * @since 1.22 2402 */ 2403 public function getHumanTimestamp( MWTimestamp $ts, MWTimestamp $relativeTo, User $user ) { 2404 $diff = $ts->diff( $relativeTo ); 2405 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) - 2406 (int)$relativeTo->timestamp->format( 'w' ) ); 2407 $days = $diff->days ?: (int)$diffDay; 2408 if ( $diff->invert || $days > 5 2409 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' ) 2410 ) { 2411 // Timestamps are in different years: use full timestamp 2412 // Also do full timestamp for future dates 2413 /** 2414 * @todo FIXME: Add better handling of future timestamps. 2415 */ 2416 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' ); 2417 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ); 2418 } elseif ( $days > 5 ) { 2419 // Timestamps are in same year, but more than 5 days ago: show day and month only. 2420 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' ); 2421 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ); 2422 } elseif ( $days > 1 ) { 2423 // Timestamp within the past week: show the day of the week and time 2424 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' ); 2425 $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )]; 2426 // Messages: 2427 // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at 2428 $ts = wfMessage( "$weekday-at" ) 2429 ->inLanguage( $this ) 2430 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) ) 2431 ->text(); 2432 } elseif ( $days == 1 ) { 2433 // Timestamp was yesterday: say 'yesterday' and the time. 2434 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' ); 2435 $ts = wfMessage( 'yesterday-at' ) 2436 ->inLanguage( $this ) 2437 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) ) 2438 ->text(); 2439 } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) { 2440 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time. 2441 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' ); 2442 $ts = wfMessage( 'today-at' ) 2443 ->inLanguage( $this ) 2444 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) ) 2445 ->text(); 2446 2447 // From here on in, the timestamp was soon enough ago so that we can simply say 2448 // XX units ago, e.g., "2 hours ago" or "5 minutes ago" 2449 } elseif ( $diff->h == 1 ) { 2450 // Less than 90 minutes, but more than an hour ago. 2451 $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text(); 2452 } elseif ( $diff->i >= 1 ) { 2453 // A few minutes ago. 2454 $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text(); 2455 } elseif ( $diff->s >= 30 ) { 2456 // Less than a minute, but more than 30 sec ago. 2457 $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text(); 2458 } else { 2459 // Less than 30 seconds ago. 2460 $ts = wfMessage( 'just-now' )->text(); 2461 } 2462 2463 return $ts; 2464 } 2465 2466 /** 2467 * @param string $key 2468 * @return array|null 2469 */ 2470 function getMessage( $key ) { 2471 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key ); 2472 } 2473 2474 /** 2475 * @return array 2476 */ 2477 function getAllMessages() { 2478 return self::$dataCache->getItem( $this->mCode, 'messages' ); 2479 } 2480 2481 /** 2482 * @param string $in 2483 * @param string $out 2484 * @param string $string 2485 * @return string 2486 */ 2487 function iconv( $in, $out, $string ) { 2488 # This is a wrapper for iconv in all languages except esperanto, 2489 # which does some nasty x-conversions beforehand 2490 2491 # Even with //IGNORE iconv can whine about illegal characters in 2492 # *input* string. We just ignore those too. 2493 # REF: http://bugs.php.net/bug.php?id=37166 2494 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885 2495 wfSuppressWarnings(); 2496 $text = iconv( $in, $out . '//IGNORE', $string ); 2497 wfRestoreWarnings(); 2498 return $text; 2499 } 2500 2501 // callback functions for uc(), lc(), ucwords(), ucwordbreaks() 2502 2503 /** 2504 * @param array $matches 2505 * @return mixed|string 2506 */ 2507 function ucwordbreaksCallbackAscii( $matches ) { 2508 return $this->ucfirst( $matches[1] ); 2509 } 2510 2511 /** 2512 * @param array $matches 2513 * @return string 2514 */ 2515 function ucwordbreaksCallbackMB( $matches ) { 2516 return mb_strtoupper( $matches[0] ); 2517 } 2518 2519 /** 2520 * @param array $matches 2521 * @return string 2522 */ 2523 function ucCallback( $matches ) { 2524 list( $wikiUpperChars ) = self::getCaseMaps(); 2525 return strtr( $matches[1], $wikiUpperChars ); 2526 } 2527 2528 /** 2529 * @param array $matches 2530 * @return string 2531 */ 2532 function lcCallback( $matches ) { 2533 list( , $wikiLowerChars ) = self::getCaseMaps(); 2534 return strtr( $matches[1], $wikiLowerChars ); 2535 } 2536 2537 /** 2538 * @param array $matches 2539 * @return string 2540 */ 2541 function ucwordsCallbackMB( $matches ) { 2542 return mb_strtoupper( $matches[0] ); 2543 } 2544 2545 /** 2546 * @param array $matches 2547 * @return string 2548 */ 2549 function ucwordsCallbackWiki( $matches ) { 2550 list( $wikiUpperChars ) = self::getCaseMaps(); 2551 return strtr( $matches[0], $wikiUpperChars ); 2552 } 2553 2554 /** 2555 * Make a string's first character uppercase 2556 * 2557 * @param string $str 2558 * 2559 * @return string 2560 */ 2561 function ucfirst( $str ) { 2562 $o = ord( $str ); 2563 if ( $o < 96 ) { // if already uppercase... 2564 return $str; 2565 } elseif ( $o < 128 ) { 2566 return ucfirst( $str ); // use PHP's ucfirst() 2567 } else { 2568 // fall back to more complex logic in case of multibyte strings 2569 return $this->uc( $str, true ); 2570 } 2571 } 2572 2573 /** 2574 * Convert a string to uppercase 2575 * 2576 * @param string $str 2577 * @param bool $first 2578 * 2579 * @return string 2580 */ 2581 function uc( $str, $first = false ) { 2582 if ( function_exists( 'mb_strtoupper' ) ) { 2583 if ( $first ) { 2584 if ( $this->isMultibyte( $str ) ) { 2585 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 ); 2586 } else { 2587 return ucfirst( $str ); 2588 } 2589 } else { 2590 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str ); 2591 } 2592 } else { 2593 if ( $this->isMultibyte( $str ) ) { 2594 $x = $first ? '^' : ''; 2595 return preg_replace_callback( 2596 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/", 2597 array( $this, 'ucCallback' ), 2598 $str 2599 ); 2600 } else { 2601 return $first ? ucfirst( $str ) : strtoupper( $str ); 2602 } 2603 } 2604 } 2605 2606 /** 2607 * @param string $str 2608 * @return mixed|string 2609 */ 2610 function lcfirst( $str ) { 2611 $o = ord( $str ); 2612 if ( !$o ) { 2613 return strval( $str ); 2614 } elseif ( $o >= 128 ) { 2615 return $this->lc( $str, true ); 2616 } elseif ( $o > 96 ) { 2617 return $str; 2618 } else { 2619 $str[0] = strtolower( $str[0] ); 2620 return $str; 2621 } 2622 } 2623 2624 /** 2625 * @param string $str 2626 * @param bool $first 2627 * @return mixed|string 2628 */ 2629 function lc( $str, $first = false ) { 2630 if ( function_exists( 'mb_strtolower' ) ) { 2631 if ( $first ) { 2632 if ( $this->isMultibyte( $str ) ) { 2633 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 ); 2634 } else { 2635 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ); 2636 } 2637 } else { 2638 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str ); 2639 } 2640 } else { 2641 if ( $this->isMultibyte( $str ) ) { 2642 $x = $first ? '^' : ''; 2643 return preg_replace_callback( 2644 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/", 2645 array( $this, 'lcCallback' ), 2646 $str 2647 ); 2648 } else { 2649 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str ); 2650 } 2651 } 2652 } 2653 2654 /** 2655 * @param string $str 2656 * @return bool 2657 */ 2658 function isMultibyte( $str ) { 2659 return (bool)preg_match( '/[\x80-\xff]/', $str ); 2660 } 2661 2662 /** 2663 * @param string $str 2664 * @return mixed|string 2665 */ 2666 function ucwords( $str ) { 2667 if ( $this->isMultibyte( $str ) ) { 2668 $str = $this->lc( $str ); 2669 2670 // regexp to find first letter in each word (i.e. after each space) 2671 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/"; 2672 2673 // function to use to capitalize a single char 2674 if ( function_exists( 'mb_strtoupper' ) ) { 2675 return preg_replace_callback( 2676 $replaceRegexp, 2677 array( $this, 'ucwordsCallbackMB' ), 2678 $str 2679 ); 2680 } else { 2681 return preg_replace_callback( 2682 $replaceRegexp, 2683 array( $this, 'ucwordsCallbackWiki' ), 2684 $str 2685 ); 2686 } 2687 } else { 2688 return ucwords( strtolower( $str ) ); 2689 } 2690 } 2691 2692 /** 2693 * capitalize words at word breaks 2694 * 2695 * @param string $str 2696 * @return mixed 2697 */ 2698 function ucwordbreaks( $str ) { 2699 if ( $this->isMultibyte( $str ) ) { 2700 $str = $this->lc( $str ); 2701 2702 // since \b doesn't work for UTF-8, we explicitely define word break chars 2703 $breaks = "[ \-\(\)\}\{\.,\?!]"; 2704 2705 // find first letter after word break 2706 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" . 2707 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/"; 2708 2709 if ( function_exists( 'mb_strtoupper' ) ) { 2710 return preg_replace_callback( 2711 $replaceRegexp, 2712 array( $this, 'ucwordbreaksCallbackMB' ), 2713 $str 2714 ); 2715 } else { 2716 return preg_replace_callback( 2717 $replaceRegexp, 2718 array( $this, 'ucwordsCallbackWiki' ), 2719 $str 2720 ); 2721 } 2722 } else { 2723 return preg_replace_callback( 2724 '/\b([\w\x80-\xff]+)\b/', 2725 array( $this, 'ucwordbreaksCallbackAscii' ), 2726 $str 2727 ); 2728 } 2729 } 2730 2731 /** 2732 * Return a case-folded representation of $s 2733 * 2734 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1 2735 * and $s2 are the same except for the case of their characters. It is not 2736 * necessary for the value returned to make sense when displayed. 2737 * 2738 * Do *not* perform any other normalisation in this function. If a caller 2739 * uses this function when it should be using a more general normalisation 2740 * function, then fix the caller. 2741 * 2742 * @param string $s 2743 * 2744 * @return string 2745 */ 2746 function caseFold( $s ) { 2747 return $this->uc( $s ); 2748 } 2749 2750 /** 2751 * @param string $s 2752 * @return string 2753 */ 2754 function checkTitleEncoding( $s ) { 2755 if ( is_array( $s ) ) { 2756 throw new MWException( 'Given array to checkTitleEncoding.' ); 2757 } 2758 if ( StringUtils::isUtf8( $s ) ) { 2759 return $s; 2760 } 2761 2762 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s ); 2763 } 2764 2765 /** 2766 * @return array 2767 */ 2768 function fallback8bitEncoding() { 2769 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' ); 2770 } 2771 2772 /** 2773 * Most writing systems use whitespace to break up words. 2774 * Some languages such as Chinese don't conventionally do this, 2775 * which requires special handling when breaking up words for 2776 * searching etc. 2777 * 2778 * @return bool 2779 */ 2780 function hasWordBreaks() { 2781 return true; 2782 } 2783 2784 /** 2785 * Some languages such as Chinese require word segmentation, 2786 * Specify such segmentation when overridden in derived class. 2787 * 2788 * @param string $string 2789 * @return string 2790 */ 2791 function segmentByWord( $string ) { 2792 return $string; 2793 } 2794 2795 /** 2796 * Some languages have special punctuation need to be normalized. 2797 * Make such changes here. 2798 * 2799 * @param string $string 2800 * @return string 2801 */ 2802 function normalizeForSearch( $string ) { 2803 return self::convertDoubleWidth( $string ); 2804 } 2805 2806 /** 2807 * convert double-width roman characters to single-width. 2808 * range: ff00-ff5f ~= 0020-007f 2809 * 2810 * @param string $string 2811 * 2812 * @return string 2813 */ 2814 protected static function convertDoubleWidth( $string ) { 2815 static $full = null; 2816 static $half = null; 2817 2818 if ( $full === null ) { 2819 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 2820 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 2821 $full = str_split( $fullWidth, 3 ); 2822 $half = str_split( $halfWidth ); 2823 } 2824 2825 $string = str_replace( $full, $half, $string ); 2826 return $string; 2827 } 2828 2829 /** 2830 * @param string $string 2831 * @param string $pattern 2832 * @return string 2833 */ 2834 protected static function insertSpace( $string, $pattern ) { 2835 $string = preg_replace( $pattern, " $1 ", $string ); 2836 $string = preg_replace( '/ +/', ' ', $string ); 2837 return $string; 2838 } 2839 2840 /** 2841 * @param array $termsArray 2842 * @return array 2843 */ 2844 function convertForSearchResult( $termsArray ) { 2845 # some languages, e.g. Chinese, need to do a conversion 2846 # in order for search results to be displayed correctly 2847 return $termsArray; 2848 } 2849 2850 /** 2851 * Get the first character of a string. 2852 * 2853 * @param string $s 2854 * @return string 2855 */ 2856 function firstChar( $s ) { 2857 $matches = array(); 2858 preg_match( 2859 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' . 2860 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/', 2861 $s, 2862 $matches 2863 ); 2864 2865 if ( isset( $matches[1] ) ) { 2866 if ( strlen( $matches[1] ) != 3 ) { 2867 return $matches[1]; 2868 } 2869 2870 // Break down Hangul syllables to grab the first jamo 2871 $code = utf8ToCodepoint( $matches[1] ); 2872 if ( $code < 0xac00 || 0xd7a4 <= $code ) { 2873 return $matches[1]; 2874 } elseif ( $code < 0xb098 ) { 2875 return "\xe3\x84\xb1"; 2876 } elseif ( $code < 0xb2e4 ) { 2877 return "\xe3\x84\xb4"; 2878 } elseif ( $code < 0xb77c ) { 2879 return "\xe3\x84\xb7"; 2880 } elseif ( $code < 0xb9c8 ) { 2881 return "\xe3\x84\xb9"; 2882 } elseif ( $code < 0xbc14 ) { 2883 return "\xe3\x85\x81"; 2884 } elseif ( $code < 0xc0ac ) { 2885 return "\xe3\x85\x82"; 2886 } elseif ( $code < 0xc544 ) { 2887 return "\xe3\x85\x85"; 2888 } elseif ( $code < 0xc790 ) { 2889 return "\xe3\x85\x87"; 2890 } elseif ( $code < 0xcc28 ) { 2891 return "\xe3\x85\x88"; 2892 } elseif ( $code < 0xce74 ) { 2893 return "\xe3\x85\x8a"; 2894 } elseif ( $code < 0xd0c0 ) { 2895 return "\xe3\x85\x8b"; 2896 } elseif ( $code < 0xd30c ) { 2897 return "\xe3\x85\x8c"; 2898 } elseif ( $code < 0xd558 ) { 2899 return "\xe3\x85\x8d"; 2900 } else { 2901 return "\xe3\x85\x8e"; 2902 } 2903 } else { 2904 return ''; 2905 } 2906 } 2907 2908 function initEncoding() { 2909 # Some languages may have an alternate char encoding option 2910 # (Esperanto X-coding, Japanese furigana conversion, etc) 2911 # If this language is used as the primary content language, 2912 # an override to the defaults can be set here on startup. 2913 } 2914 2915 /** 2916 * @param string $s 2917 * @return string 2918 */ 2919 function recodeForEdit( $s ) { 2920 # For some languages we'll want to explicitly specify 2921 # which characters make it into the edit box raw 2922 # or are converted in some way or another. 2923 global $wgEditEncoding; 2924 if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) { 2925 return $s; 2926 } else { 2927 return $this->iconv( 'UTF-8', $wgEditEncoding, $s ); 2928 } 2929 } 2930 2931 /** 2932 * @param string $s 2933 * @return string 2934 */ 2935 function recodeInput( $s ) { 2936 # Take the previous into account. 2937 global $wgEditEncoding; 2938 if ( $wgEditEncoding != '' ) { 2939 $enc = $wgEditEncoding; 2940 } else { 2941 $enc = 'UTF-8'; 2942 } 2943 if ( $enc == 'UTF-8' ) { 2944 return $s; 2945 } else { 2946 return $this->iconv( $enc, 'UTF-8', $s ); 2947 } 2948 } 2949 2950 /** 2951 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this 2952 * also cleans up certain backwards-compatible sequences, converting them 2953 * to the modern Unicode equivalent. 2954 * 2955 * This is language-specific for performance reasons only. 2956 * 2957 * @param string $s 2958 * 2959 * @return string 2960 */ 2961 function normalize( $s ) { 2962 global $wgAllUnicodeFixes; 2963 $s = UtfNormal::cleanUp( $s ); 2964 if ( $wgAllUnicodeFixes ) { 2965 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s ); 2966 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s ); 2967 } 2968 2969 return $s; 2970 } 2971 2972 /** 2973 * Transform a string using serialized data stored in the given file (which 2974 * must be in the serialized subdirectory of $IP). The file contains pairs 2975 * mapping source characters to destination characters. 2976 * 2977 * The data is cached in process memory. This will go faster if you have the 2978 * FastStringSearch extension. 2979 * 2980 * @param string $file 2981 * @param string $string 2982 * 2983 * @throws MWException 2984 * @return string 2985 */ 2986 function transformUsingPairFile( $file, $string ) { 2987 if ( !isset( $this->transformData[$file] ) ) { 2988 $data = wfGetPrecompiledData( $file ); 2989 if ( $data === false ) { 2990 throw new MWException( __METHOD__ . ": The transformation file $file is missing" ); 2991 } 2992 $this->transformData[$file] = new ReplacementArray( $data ); 2993 } 2994 return $this->transformData[$file]->replace( $string ); 2995 } 2996 2997 /** 2998 * For right-to-left language support 2999 * 3000 * @return bool 3001 */ 3002 function isRTL() { 3003 return self::$dataCache->getItem( $this->mCode, 'rtl' ); 3004 } 3005 3006 /** 3007 * Return the correct HTML 'dir' attribute value for this language. 3008 * @return string 3009 */ 3010 function getDir() { 3011 return $this->isRTL() ? 'rtl' : 'ltr'; 3012 } 3013 3014 /** 3015 * Return 'left' or 'right' as appropriate alignment for line-start 3016 * for this language's text direction. 3017 * 3018 * Should be equivalent to CSS3 'start' text-align value.... 3019 * 3020 * @return string 3021 */ 3022 function alignStart() { 3023 return $this->isRTL() ? 'right' : 'left'; 3024 } 3025 3026 /** 3027 * Return 'right' or 'left' as appropriate alignment for line-end 3028 * for this language's text direction. 3029 * 3030 * Should be equivalent to CSS3 'end' text-align value.... 3031 * 3032 * @return string 3033 */ 3034 function alignEnd() { 3035 return $this->isRTL() ? 'left' : 'right'; 3036 } 3037 3038 /** 3039 * A hidden direction mark (LRM or RLM), depending on the language direction. 3040 * Unlike getDirMark(), this function returns the character as an HTML entity. 3041 * This function should be used when the output is guaranteed to be HTML, 3042 * because it makes the output HTML source code more readable. When 3043 * the output is plain text or can be escaped, getDirMark() should be used. 3044 * 3045 * @param bool $opposite Get the direction mark opposite to your language 3046 * @return string 3047 * @since 1.20 3048 */ 3049 function getDirMarkEntity( $opposite = false ) { 3050 if ( $opposite ) { 3051 return $this->isRTL() ? '‎' : '‏'; 3052 } 3053 return $this->isRTL() ? '‏' : '‎'; 3054 } 3055 3056 /** 3057 * A hidden direction mark (LRM or RLM), depending on the language direction. 3058 * This function produces them as invisible Unicode characters and 3059 * the output may be hard to read and debug, so it should only be used 3060 * when the output is plain text or can be escaped. When the output is 3061 * HTML, use getDirMarkEntity() instead. 3062 * 3063 * @param bool $opposite Get the direction mark opposite to your language 3064 * @return string 3065 */ 3066 function getDirMark( $opposite = false ) { 3067 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM 3068 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM 3069 if ( $opposite ) { 3070 return $this->isRTL() ? $lrm : $rlm; 3071 } 3072 return $this->isRTL() ? $rlm : $lrm; 3073 } 3074 3075 /** 3076 * @return array 3077 */ 3078 function capitalizeAllNouns() { 3079 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' ); 3080 } 3081 3082 /** 3083 * An arrow, depending on the language direction. 3084 * 3085 * @param string $direction The direction of the arrow: forwards (default), 3086 * backwards, left, right, up, down. 3087 * @return string 3088 */ 3089 function getArrow( $direction = 'forwards' ) { 3090 switch ( $direction ) { 3091 case 'forwards': 3092 return $this->isRTL() ? '←' : '→'; 3093 case 'backwards': 3094 return $this->isRTL() ? '→' : '←'; 3095 case 'left': 3096 return '←'; 3097 case 'right': 3098 return '→'; 3099 case 'up': 3100 return '↑'; 3101 case 'down': 3102 return '↓'; 3103 } 3104 } 3105 3106 /** 3107 * To allow "foo[[bar]]" to extend the link over the whole word "foobar" 3108 * 3109 * @return bool 3110 */ 3111 function linkPrefixExtension() { 3112 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' ); 3113 } 3114 3115 /** 3116 * Get all magic words from cache. 3117 * @return array 3118 */ 3119 function getMagicWords() { 3120 return self::$dataCache->getItem( $this->mCode, 'magicWords' ); 3121 } 3122 3123 /** 3124 * Run the LanguageGetMagic hook once. 3125 */ 3126 protected function doMagicHook() { 3127 if ( $this->mMagicHookDone ) { 3128 return; 3129 } 3130 $this->mMagicHookDone = true; 3131 wfProfileIn( 'LanguageGetMagic' ); 3132 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) ); 3133 wfProfileOut( 'LanguageGetMagic' ); 3134 } 3135 3136 /** 3137 * Fill a MagicWord object with data from here 3138 * 3139 * @param MagicWord $mw 3140 */ 3141 function getMagic( $mw ) { 3142 // Saves a function call 3143 if ( !$this->mMagicHookDone ) { 3144 $this->doMagicHook(); 3145 } 3146 3147 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) { 3148 $rawEntry = $this->mMagicExtensions[$mw->mId]; 3149 } else { 3150 $rawEntry = self::$dataCache->getSubitem( 3151 $this->mCode, 'magicWords', $mw->mId ); 3152 } 3153 3154 if ( !is_array( $rawEntry ) ) { 3155 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" ); 3156 } else { 3157 $mw->mCaseSensitive = $rawEntry[0]; 3158 $mw->mSynonyms = array_slice( $rawEntry, 1 ); 3159 } 3160 } 3161 3162 /** 3163 * Add magic words to the extension array 3164 * 3165 * @param array $newWords 3166 */ 3167 function addMagicWordsByLang( $newWords ) { 3168 $fallbackChain = $this->getFallbackLanguages(); 3169 $fallbackChain = array_reverse( $fallbackChain ); 3170 foreach ( $fallbackChain as $code ) { 3171 if ( isset( $newWords[$code] ) ) { 3172 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions; 3173 } 3174 } 3175 } 3176 3177 /** 3178 * Get special page names, as an associative array 3179 * case folded alias => real name 3180 * @return array 3181 */ 3182 function getSpecialPageAliases() { 3183 // Cache aliases because it may be slow to load them 3184 if ( is_null( $this->mExtendedSpecialPageAliases ) ) { 3185 // Initialise array 3186 $this->mExtendedSpecialPageAliases = 3187 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' ); 3188 wfRunHooks( 'LanguageGetSpecialPageAliases', 3189 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) ); 3190 } 3191 3192 return $this->mExtendedSpecialPageAliases; 3193 } 3194 3195 /** 3196 * Italic is unsuitable for some languages 3197 * 3198 * @param string $text The text to be emphasized. 3199 * @return string 3200 */ 3201 function emphasize( $text ) { 3202 return "<em>$text</em>"; 3203 } 3204 3205 /** 3206 * Normally we output all numbers in plain en_US style, that is 3207 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone 3208 * point twohundredthirtyfive. However this is not suitable for all 3209 * languages, some such as Punjabi want ੨੯੩,੨੯੫.੨੩੫ and others such as 3210 * Icelandic just want to use commas instead of dots, and dots instead 3211 * of commas like "293.291,235". 3212 * 3213 * An example of this function being called: 3214 * <code> 3215 * wfMessage( 'message' )->numParams( $num )->text() 3216 * </code> 3217 * 3218 * See $separatorTransformTable on MessageIs.php for 3219 * the , => . and . => , implementation. 3220 * 3221 * @todo check if it's viable to use localeconv() for the decimal separator thing. 3222 * @param int|float $number The string to be formatted, should be an integer 3223 * or a floating point number. 3224 * @param bool $nocommafy Set to true for special numbers like dates 3225 * @return string 3226 */ 3227 public function formatNum( $number, $nocommafy = false ) { 3228 global $wgTranslateNumerals; 3229 if ( !$nocommafy ) { 3230 $number = $this->commafy( $number ); 3231 $s = $this->separatorTransformTable(); 3232 if ( $s ) { 3233 $number = strtr( $number, $s ); 3234 } 3235 } 3236 3237 if ( $wgTranslateNumerals ) { 3238 $s = $this->digitTransformTable(); 3239 if ( $s ) { 3240 $number = strtr( $number, $s ); 3241 } 3242 } 3243 3244 return $number; 3245 } 3246 3247 /** 3248 * Front-end for non-commafied formatNum 3249 * 3250 * @param int|float $number The string to be formatted, should be an integer 3251 * or a floating point number. 3252 * @since 1.21 3253 * @return string 3254 */ 3255 public function formatNumNoSeparators( $number ) { 3256 return $this->formatNum( $number, true ); 3257 } 3258 3259 /** 3260 * @param string $number 3261 * @return string 3262 */ 3263 public function parseFormattedNumber( $number ) { 3264 $s = $this->digitTransformTable(); 3265 if ( $s ) { 3266 // eliminate empty array values such as ''. (bug 64347) 3267 $s = array_filter( $s ); 3268 $number = strtr( $number, array_flip( $s ) ); 3269 } 3270 3271 $s = $this->separatorTransformTable(); 3272 if ( $s ) { 3273 // eliminate empty array values such as ''. (bug 64347) 3274 $s = array_filter( $s ); 3275 $number = strtr( $number, array_flip( $s ) ); 3276 } 3277 3278 $number = strtr( $number, array( ',' => '' ) ); 3279 return $number; 3280 } 3281 3282 /** 3283 * Adds commas to a given number 3284 * @since 1.19 3285 * @param mixed $number 3286 * @return string 3287 */ 3288 function commafy( $number ) { 3289 $digitGroupingPattern = $this->digitGroupingPattern(); 3290 if ( $number === null ) { 3291 return ''; 3292 } 3293 3294 if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) { 3295 // default grouping is at thousands, use the same for ###,###,### pattern too. 3296 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) ); 3297 } else { 3298 // Ref: http://cldr.unicode.org/translation/number-patterns 3299 $sign = ""; 3300 if ( intval( $number ) < 0 ) { 3301 // For negative numbers apply the algorithm like positive number and add sign. 3302 $sign = "-"; 3303 $number = substr( $number, 1 ); 3304 } 3305 $integerPart = array(); 3306 $decimalPart = array(); 3307 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches ); 3308 preg_match( "/\d+/", $number, $integerPart ); 3309 preg_match( "/\.\d*/", $number, $decimalPart ); 3310 $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : ""; 3311 if ( $groupedNumber === $number ) { 3312 // the string does not have any number part. Eg: .12345 3313 return $sign . $groupedNumber; 3314 } 3315 $start = $end = strlen( $integerPart[0] ); 3316 while ( $start > 0 ) { 3317 $match = $matches[0][$numMatches - 1]; 3318 $matchLen = strlen( $match ); 3319 $start = $end - $matchLen; 3320 if ( $start < 0 ) { 3321 $start = 0; 3322 } 3323 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber; 3324 $end = $start; 3325 if ( $numMatches > 1 ) { 3326 // use the last pattern for the rest of the number 3327 $numMatches--; 3328 } 3329 if ( $start > 0 ) { 3330 $groupedNumber = "," . $groupedNumber; 3331 } 3332 } 3333 return $sign . $groupedNumber; 3334 } 3335 } 3336 3337 /** 3338 * @return string 3339 */ 3340 function digitGroupingPattern() { 3341 return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' ); 3342 } 3343 3344 /** 3345 * @return array 3346 */ 3347 function digitTransformTable() { 3348 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' ); 3349 } 3350 3351 /** 3352 * @return array 3353 */ 3354 function separatorTransformTable() { 3355 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' ); 3356 } 3357 3358 /** 3359 * Take a list of strings and build a locale-friendly comma-separated 3360 * list, using the local comma-separator message. 3361 * The last two strings are chained with an "and". 3362 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…) 3363 * 3364 * @param string[] $l 3365 * @return string 3366 */ 3367 function listToText( array $l ) { 3368 $m = count( $l ) - 1; 3369 if ( $m < 0 ) { 3370 return ''; 3371 } 3372 if ( $m > 0 ) { 3373 $and = $this->getMessageFromDB( 'and' ); 3374 $space = $this->getMessageFromDB( 'word-separator' ); 3375 if ( $m > 1 ) { 3376 $comma = $this->getMessageFromDB( 'comma-separator' ); 3377 } 3378 } 3379 $s = $l[$m]; 3380 for ( $i = $m - 1; $i >= 0; $i-- ) { 3381 if ( $i == $m - 1 ) { 3382 $s = $l[$i] . $and . $space . $s; 3383 } else { 3384 $s = $l[$i] . $comma . $s; 3385 } 3386 } 3387 return $s; 3388 } 3389 3390 /** 3391 * Take a list of strings and build a locale-friendly comma-separated 3392 * list, using the local comma-separator message. 3393 * @param string[] $list Array of strings to put in a comma list 3394 * @return string 3395 */ 3396 function commaList( array $list ) { 3397 return implode( 3398 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(), 3399 $list 3400 ); 3401 } 3402 3403 /** 3404 * Take a list of strings and build a locale-friendly semicolon-separated 3405 * list, using the local semicolon-separator message. 3406 * @param string[] $list Array of strings to put in a semicolon list 3407 * @return string 3408 */ 3409 function semicolonList( array $list ) { 3410 return implode( 3411 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(), 3412 $list 3413 ); 3414 } 3415 3416 /** 3417 * Same as commaList, but separate it with the pipe instead. 3418 * @param string[] $list Array of strings to put in a pipe list 3419 * @return string 3420 */ 3421 function pipeList( array $list ) { 3422 return implode( 3423 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(), 3424 $list 3425 ); 3426 } 3427 3428 /** 3429 * Truncate a string to a specified length in bytes, appending an optional 3430 * string (e.g. for ellipses) 3431 * 3432 * The database offers limited byte lengths for some columns in the database; 3433 * multi-byte character sets mean we need to ensure that only whole characters 3434 * are included, otherwise broken characters can be passed to the user 3435 * 3436 * If $length is negative, the string will be truncated from the beginning 3437 * 3438 * @param string $string String to truncate 3439 * @param int $length Maximum length (including ellipses) 3440 * @param string $ellipsis String to append to the truncated text 3441 * @param bool $adjustLength Subtract length of ellipsis from $length. 3442 * $adjustLength was introduced in 1.18, before that behaved as if false. 3443 * @return string 3444 */ 3445 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) { 3446 # Use the localized ellipsis character 3447 if ( $ellipsis == '...' ) { 3448 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped(); 3449 } 3450 # Check if there is no need to truncate 3451 if ( $length == 0 ) { 3452 return $ellipsis; // convention 3453 } elseif ( strlen( $string ) <= abs( $length ) ) { 3454 return $string; // no need to truncate 3455 } 3456 $stringOriginal = $string; 3457 # If ellipsis length is >= $length then we can't apply $adjustLength 3458 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) { 3459 $string = $ellipsis; // this can be slightly unexpected 3460 # Otherwise, truncate and add ellipsis... 3461 } else { 3462 $eLength = $adjustLength ? strlen( $ellipsis ) : 0; 3463 if ( $length > 0 ) { 3464 $length -= $eLength; 3465 $string = substr( $string, 0, $length ); // xyz... 3466 $string = $this->removeBadCharLast( $string ); 3467 $string = rtrim( $string ); 3468 $string = $string . $ellipsis; 3469 } else { 3470 $length += $eLength; 3471 $string = substr( $string, $length ); // ...xyz 3472 $string = $this->removeBadCharFirst( $string ); 3473 $string = ltrim( $string ); 3474 $string = $ellipsis . $string; 3475 } 3476 } 3477 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181). 3478 # This check is *not* redundant if $adjustLength, due to the single case where 3479 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string. 3480 if ( strlen( $string ) < strlen( $stringOriginal ) ) { 3481 return $string; 3482 } else { 3483 return $stringOriginal; 3484 } 3485 } 3486 3487 /** 3488 * Remove bytes that represent an incomplete Unicode character 3489 * at the end of string (e.g. bytes of the char are missing) 3490 * 3491 * @param string $string 3492 * @return string 3493 */ 3494 protected function removeBadCharLast( $string ) { 3495 if ( $string != '' ) { 3496 $char = ord( $string[strlen( $string ) - 1] ); 3497 $m = array(); 3498 if ( $char >= 0xc0 ) { 3499 # We got the first byte only of a multibyte char; remove it. 3500 $string = substr( $string, 0, -1 ); 3501 } elseif ( $char >= 0x80 && 3502 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' . 3503 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) 3504 ) { 3505 # We chopped in the middle of a character; remove it 3506 $string = $m[1]; 3507 } 3508 } 3509 return $string; 3510 } 3511 3512 /** 3513 * Remove bytes that represent an incomplete Unicode character 3514 * at the start of string (e.g. bytes of the char are missing) 3515 * 3516 * @param string $string 3517 * @return string 3518 */ 3519 protected function removeBadCharFirst( $string ) { 3520 if ( $string != '' ) { 3521 $char = ord( $string[0] ); 3522 if ( $char >= 0x80 && $char < 0xc0 ) { 3523 # We chopped in the middle of a character; remove the whole thing 3524 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string ); 3525 } 3526 } 3527 return $string; 3528 } 3529 3530 /** 3531 * Truncate a string of valid HTML to a specified length in bytes, 3532 * appending an optional string (e.g. for ellipses), and return valid HTML 3533 * 3534 * This is only intended for styled/linked text, such as HTML with 3535 * tags like <span> and <a>, were the tags are self-contained (valid HTML). 3536 * Also, this will not detect things like "display:none" CSS. 3537 * 3538 * Note: since 1.18 you do not need to leave extra room in $length for ellipses. 3539 * 3540 * @param string $text HTML string to truncate 3541 * @param int $length (zero/positive) Maximum length (including ellipses) 3542 * @param string $ellipsis String to append to the truncated text 3543 * @return string 3544 */ 3545 function truncateHtml( $text, $length, $ellipsis = '...' ) { 3546 # Use the localized ellipsis character 3547 if ( $ellipsis == '...' ) { 3548 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped(); 3549 } 3550 # Check if there is clearly no need to truncate 3551 if ( $length <= 0 ) { 3552 return $ellipsis; // no text shown, nothing to format (convention) 3553 } elseif ( strlen( $text ) <= $length ) { 3554 return $text; // string short enough even *with* HTML (short-circuit) 3555 } 3556 3557 $dispLen = 0; // innerHTML legth so far 3558 $testingEllipsis = false; // checking if ellipses will make string longer/equal? 3559 $tagType = 0; // 0-open, 1-close 3560 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither 3561 $entityState = 0; // 0-not entity, 1-entity 3562 $tag = $ret = ''; // accumulated tag name, accumulated result string 3563 $openTags = array(); // open tag stack 3564 $maybeState = null; // possible truncation state 3565 3566 $textLen = strlen( $text ); 3567 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated 3568 for ( $pos = 0; true; ++$pos ) { 3569 # Consider truncation once the display length has reached the maximim. 3570 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case. 3571 # Check that we're not in the middle of a bracket/entity... 3572 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) { 3573 if ( !$testingEllipsis ) { 3574 $testingEllipsis = true; 3575 # Save where we are; we will truncate here unless there turn out to 3576 # be so few remaining characters that truncation is not necessary. 3577 if ( !$maybeState ) { // already saved? ($neLength = 0 case) 3578 $maybeState = array( $ret, $openTags ); // save state 3579 } 3580 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) { 3581 # String in fact does need truncation, the truncation point was OK. 3582 list( $ret, $openTags ) = $maybeState; // reload state 3583 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix 3584 $ret .= $ellipsis; // add ellipsis 3585 break; 3586 } 3587 } 3588 if ( $pos >= $textLen ) { 3589 break; // extra iteration just for above checks 3590 } 3591 3592 # Read the next char... 3593 $ch = $text[$pos]; 3594 $lastCh = $pos ? $text[$pos - 1] : ''; 3595 $ret .= $ch; // add to result string 3596 if ( $ch == '<' ) { 3597 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML 3598 $entityState = 0; // for bad HTML 3599 $bracketState = 1; // tag started (checking for backslash) 3600 } elseif ( $ch == '>' ) { 3601 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); 3602 $entityState = 0; // for bad HTML 3603 $bracketState = 0; // out of brackets 3604 } elseif ( $bracketState == 1 ) { 3605 if ( $ch == '/' ) { 3606 $tagType = 1; // close tag (e.g. "</span>") 3607 } else { 3608 $tagType = 0; // open tag (e.g. "<span>") 3609 $tag .= $ch; 3610 } 3611 $bracketState = 2; // building tag name 3612 } elseif ( $bracketState == 2 ) { 3613 if ( $ch != ' ' ) { 3614 $tag .= $ch; 3615 } else { 3616 // Name found (e.g. "<a href=..."), add on tag attributes... 3617 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 ); 3618 } 3619 } elseif ( $bracketState == 0 ) { 3620 if ( $entityState ) { 3621 if ( $ch == ';' ) { 3622 $entityState = 0; 3623 $dispLen++; // entity is one displayed char 3624 } 3625 } else { 3626 if ( $neLength == 0 && !$maybeState ) { 3627 // Save state without $ch. We want to *hit* the first 3628 // display char (to get tags) but not *use* it if truncating. 3629 $maybeState = array( substr( $ret, 0, -1 ), $openTags ); 3630 } 3631 if ( $ch == '&' ) { 3632 $entityState = 1; // entity found, (e.g. " ") 3633 } else { 3634 $dispLen++; // this char is displayed 3635 // Add the next $max display text chars after this in one swoop... 3636 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen; 3637 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max ); 3638 $dispLen += $skipped; 3639 $pos += $skipped; 3640 } 3641 } 3642 } 3643 } 3644 // Close the last tag if left unclosed by bad HTML 3645 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags ); 3646 while ( count( $openTags ) > 0 ) { 3647 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags 3648 } 3649 return $ret; 3650 } 3651 3652 /** 3653 * truncateHtml() helper function 3654 * like strcspn() but adds the skipped chars to $ret 3655 * 3656 * @param string $ret 3657 * @param string $text 3658 * @param string $search 3659 * @param int $start 3660 * @param null|int $len 3661 * @return int 3662 */ 3663 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) { 3664 if ( $len === null ) { 3665 $len = -1; // -1 means "no limit" for strcspn 3666 } elseif ( $len < 0 ) { 3667 $len = 0; // sanity 3668 } 3669 $skipCount = 0; 3670 if ( $start < strlen( $text ) ) { 3671 $skipCount = strcspn( $text, $search, $start, $len ); 3672 $ret .= substr( $text, $start, $skipCount ); 3673 } 3674 return $skipCount; 3675 } 3676 3677 /** 3678 * truncateHtml() helper function 3679 * (a) push or pop $tag from $openTags as needed 3680 * (b) clear $tag value 3681 * @param string &$tag Current HTML tag name we are looking at 3682 * @param int $tagType (0-open tag, 1-close tag) 3683 * @param string $lastCh Character before the '>' that ended this tag 3684 * @param array &$openTags Open tag stack (not accounting for $tag) 3685 */ 3686 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) { 3687 $tag = ltrim( $tag ); 3688 if ( $tag != '' ) { 3689 if ( $tagType == 0 && $lastCh != '/' ) { 3690 $openTags[] = $tag; // tag opened (didn't close itself) 3691 } elseif ( $tagType == 1 ) { 3692 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) { 3693 array_pop( $openTags ); // tag closed 3694 } 3695 } 3696 $tag = ''; 3697 } 3698 } 3699 3700 /** 3701 * Grammatical transformations, needed for inflected languages 3702 * Invoked by putting {{grammar:case|word}} in a message 3703 * 3704 * @param string $word 3705 * @param string $case 3706 * @return string 3707 */ 3708 function convertGrammar( $word, $case ) { 3709 global $wgGrammarForms; 3710 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) { 3711 return $wgGrammarForms[$this->getCode()][$case][$word]; 3712 } 3713 3714 return $word; 3715 } 3716 /** 3717 * Get the grammar forms for the content language 3718 * @return array Array of grammar forms 3719 * @since 1.20 3720 */ 3721 function getGrammarForms() { 3722 global $wgGrammarForms; 3723 if ( isset( $wgGrammarForms[$this->getCode()] ) 3724 && is_array( $wgGrammarForms[$this->getCode()] ) 3725 ) { 3726 return $wgGrammarForms[$this->getCode()]; 3727 } 3728 3729 return array(); 3730 } 3731 /** 3732 * Provides an alternative text depending on specified gender. 3733 * Usage {{gender:username|masculine|feminine|unknown}}. 3734 * username is optional, in which case the gender of current user is used, 3735 * but only in (some) interface messages; otherwise default gender is used. 3736 * 3737 * If no forms are given, an empty string is returned. If only one form is 3738 * given, it will be returned unconditionally. These details are implied by 3739 * the caller and cannot be overridden in subclasses. 3740 * 3741 * If three forms are given, the default is to use the third (unknown) form. 3742 * If fewer than three forms are given, the default is to use the first (masculine) form. 3743 * These details can be overridden in subclasses. 3744 * 3745 * @param string $gender 3746 * @param array $forms 3747 * 3748 * @return string 3749 */ 3750 function gender( $gender, $forms ) { 3751 if ( !count( $forms ) ) { 3752 return ''; 3753 } 3754 $forms = $this->preConvertPlural( $forms, 2 ); 3755 if ( $gender === 'male' ) { 3756 return $forms[0]; 3757 } 3758 if ( $gender === 'female' ) { 3759 return $forms[1]; 3760 } 3761 return isset( $forms[2] ) ? $forms[2] : $forms[0]; 3762 } 3763 3764 /** 3765 * Plural form transformations, needed for some languages. 3766 * For example, there are 3 form of plural in Russian and Polish, 3767 * depending on "count mod 10". See [[w:Plural]] 3768 * For English it is pretty simple. 3769 * 3770 * Invoked by putting {{plural:count|wordform1|wordform2}} 3771 * or {{plural:count|wordform1|wordform2|wordform3}} 3772 * 3773 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}} 3774 * 3775 * @param int $count Non-localized number 3776 * @param array $forms Different plural forms 3777 * @return string Correct form of plural for $count in this language 3778 */ 3779 function convertPlural( $count, $forms ) { 3780 // Handle explicit n=pluralform cases 3781 $forms = $this->handleExplicitPluralForms( $count, $forms ); 3782 if ( is_string( $forms ) ) { 3783 return $forms; 3784 } 3785 if ( !count( $forms ) ) { 3786 return ''; 3787 } 3788 3789 $pluralForm = $this->getPluralRuleIndexNumber( $count ); 3790 $pluralForm = min( $pluralForm, count( $forms ) - 1 ); 3791 return $forms[$pluralForm]; 3792 } 3793 3794 /** 3795 * Handles explicit plural forms for Language::convertPlural() 3796 * 3797 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero. 3798 * If an explicitly defined plural form matches the $count, then 3799 * string value returned, otherwise array returned for further consideration 3800 * by CLDR rules or overridden convertPlural(). 3801 * 3802 * @since 1.23 3803 * 3804 * @param int $count Non-localized number 3805 * @param array $forms Different plural forms 3806 * 3807 * @return array|string 3808 */ 3809 protected function handleExplicitPluralForms( $count, array $forms ) { 3810 foreach ( $forms as $index => $form ) { 3811 if ( preg_match( '/\d+=/i', $form ) ) { 3812 $pos = strpos( $form, '=' ); 3813 if ( substr( $form, 0, $pos ) === (string)$count ) { 3814 return substr( $form, $pos + 1 ); 3815 } 3816 unset( $forms[$index] ); 3817 } 3818 } 3819 return array_values( $forms ); 3820 } 3821 3822 /** 3823 * Checks that convertPlural was given an array and pads it to requested 3824 * amount of forms by copying the last one. 3825 * 3826 * @param array $forms Array of forms given to convertPlural 3827 * @param int $count How many forms should there be at least 3828 * @return array Padded array of forms or an exception if not an array 3829 */ 3830 protected function preConvertPlural( /* Array */ $forms, $count ) { 3831 while ( count( $forms ) < $count ) { 3832 $forms[] = $forms[count( $forms ) - 1]; 3833 } 3834 return $forms; 3835 } 3836 3837 /** 3838 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it 3839 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time 3840 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used 3841 * on old expiry lengths recorded in log entries. You'd need to provide the start date to 3842 * match up with it. 3843 * 3844 * @param string $str The validated block duration in English 3845 * @return string Somehow translated block duration 3846 * @see LanguageFi.php for example implementation 3847 */ 3848 function translateBlockExpiry( $str ) { 3849 $duration = SpecialBlock::getSuggestedDurations( $this ); 3850 foreach ( $duration as $show => $value ) { 3851 if ( strcmp( $str, $value ) == 0 ) { 3852 return htmlspecialchars( trim( $show ) ); 3853 } 3854 } 3855 3856 // Since usually only infinite or indefinite is only on list, so try 3857 // equivalents if still here. 3858 $indefs = array( 'infinite', 'infinity', 'indefinite' ); 3859 if ( in_array( $str, $indefs ) ) { 3860 foreach ( $indefs as $val ) { 3861 $show = array_search( $val, $duration, true ); 3862 if ( $show !== false ) { 3863 return htmlspecialchars( trim( $show ) ); 3864 } 3865 } 3866 } 3867 3868 // If all else fails, return a standard duration or timestamp description. 3869 $time = strtotime( $str, 0 ); 3870 if ( $time === false ) { // Unknown format. Return it as-is in case. 3871 return $str; 3872 } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp. 3873 // $time is relative to 0 so it's a duration length. 3874 return $this->formatDuration( $time ); 3875 } else { // It's an absolute timestamp. 3876 if ( $time === 0 ) { 3877 // wfTimestamp() handles 0 as current time instead of epoch. 3878 return $this->timeanddate( '19700101000000' ); 3879 } else { 3880 return $this->timeanddate( $time ); 3881 } 3882 } 3883 } 3884 3885 /** 3886 * languages like Chinese need to be segmented in order for the diff 3887 * to be of any use 3888 * 3889 * @param string $text 3890 * @return string 3891 */ 3892 public function segmentForDiff( $text ) { 3893 return $text; 3894 } 3895 3896 /** 3897 * and unsegment to show the result 3898 * 3899 * @param string $text 3900 * @return string 3901 */ 3902 public function unsegmentForDiff( $text ) { 3903 return $text; 3904 } 3905 3906 /** 3907 * Return the LanguageConverter used in the Language 3908 * 3909 * @since 1.19 3910 * @return LanguageConverter 3911 */ 3912 public function getConverter() { 3913 return $this->mConverter; 3914 } 3915 3916 /** 3917 * convert text to all supported variants 3918 * 3919 * @param string $text 3920 * @return array 3921 */ 3922 public function autoConvertToAllVariants( $text ) { 3923 return $this->mConverter->autoConvertToAllVariants( $text ); 3924 } 3925 3926 /** 3927 * convert text to different variants of a language. 3928 * 3929 * @param string $text 3930 * @return string 3931 */ 3932 public function convert( $text ) { 3933 return $this->mConverter->convert( $text ); 3934 } 3935 3936 /** 3937 * Convert a Title object to a string in the preferred variant 3938 * 3939 * @param Title $title 3940 * @return string 3941 */ 3942 public function convertTitle( $title ) { 3943 return $this->mConverter->convertTitle( $title ); 3944 } 3945 3946 /** 3947 * Convert a namespace index to a string in the preferred variant 3948 * 3949 * @param int $ns 3950 * @return string 3951 */ 3952 public function convertNamespace( $ns ) { 3953 return $this->mConverter->convertNamespace( $ns ); 3954 } 3955 3956 /** 3957 * Check if this is a language with variants 3958 * 3959 * @return bool 3960 */ 3961 public function hasVariants() { 3962 return count( $this->getVariants() ) > 1; 3963 } 3964 3965 /** 3966 * Check if the language has the specific variant 3967 * 3968 * @since 1.19 3969 * @param string $variant 3970 * @return bool 3971 */ 3972 public function hasVariant( $variant ) { 3973 return (bool)$this->mConverter->validateVariant( $variant ); 3974 } 3975 3976 /** 3977 * Put custom tags (e.g. -{ }-) around math to prevent conversion 3978 * 3979 * @param string $text 3980 * @return string 3981 * @deprecated since 1.22 is no longer used 3982 */ 3983 public function armourMath( $text ) { 3984 return $this->mConverter->armourMath( $text ); 3985 } 3986 3987 /** 3988 * Perform output conversion on a string, and encode for safe HTML output. 3989 * @param string $text Text to be converted 3990 * @param bool $isTitle Whether this conversion is for the article title 3991 * @return string 3992 * @todo this should get integrated somewhere sane 3993 */ 3994 public function convertHtml( $text, $isTitle = false ) { 3995 return htmlspecialchars( $this->convert( $text, $isTitle ) ); 3996 } 3997 3998 /** 3999 * @param string $key 4000 * @return string 4001 */ 4002 public function convertCategoryKey( $key ) { 4003 return $this->mConverter->convertCategoryKey( $key ); 4004 } 4005 4006 /** 4007 * Get the list of variants supported by this language 4008 * see sample implementation in LanguageZh.php 4009 * 4010 * @return array An array of language codes 4011 */ 4012 public function getVariants() { 4013 return $this->mConverter->getVariants(); 4014 } 4015 4016 /** 4017 * @return string 4018 */ 4019 public function getPreferredVariant() { 4020 return $this->mConverter->getPreferredVariant(); 4021 } 4022 4023 /** 4024 * @return string 4025 */ 4026 public function getDefaultVariant() { 4027 return $this->mConverter->getDefaultVariant(); 4028 } 4029 4030 /** 4031 * @return string 4032 */ 4033 public function getURLVariant() { 4034 return $this->mConverter->getURLVariant(); 4035 } 4036 4037 /** 4038 * If a language supports multiple variants, it is 4039 * possible that non-existing link in one variant 4040 * actually exists in another variant. this function 4041 * tries to find it. See e.g. LanguageZh.php 4042 * The input parameters may be modified upon return 4043 * 4044 * @param string &$link The name of the link 4045 * @param Title &$nt The title object of the link 4046 * @param bool $ignoreOtherCond To disable other conditions when 4047 * we need to transclude a template or update a category's link 4048 */ 4049 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) { 4050 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond ); 4051 } 4052 4053 /** 4054 * returns language specific options used by User::getPageRenderHash() 4055 * for example, the preferred language variant 4056 * 4057 * @return string 4058 */ 4059 function getExtraHashOptions() { 4060 return $this->mConverter->getExtraHashOptions(); 4061 } 4062 4063 /** 4064 * For languages that support multiple variants, the title of an 4065 * article may be displayed differently in different variants. this 4066 * function returns the apporiate title defined in the body of the article. 4067 * 4068 * @return string 4069 */ 4070 public function getParsedTitle() { 4071 return $this->mConverter->getParsedTitle(); 4072 } 4073 4074 /** 4075 * Prepare external link text for conversion. When the text is 4076 * a URL, it shouldn't be converted, and it'll be wrapped in 4077 * the "raw" tag (-{R| }-) to prevent conversion. 4078 * 4079 * This function is called "markNoConversion" for historical 4080 * reasons. 4081 * 4082 * @param string $text Text to be used for external link 4083 * @param bool $noParse Wrap it without confirming it's a real URL first 4084 * @return string The tagged text 4085 */ 4086 public function markNoConversion( $text, $noParse = false ) { 4087 // Excluding protocal-relative URLs may avoid many false positives. 4088 if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) { 4089 return $this->mConverter->markNoConversion( $text ); 4090 } else { 4091 return $text; 4092 } 4093 } 4094 4095 /** 4096 * A regular expression to match legal word-trailing characters 4097 * which should be merged onto a link of the form [[foo]]bar. 4098 * 4099 * @return string 4100 */ 4101 public function linkTrail() { 4102 return self::$dataCache->getItem( $this->mCode, 'linkTrail' ); 4103 } 4104 4105 /** 4106 * A regular expression character set to match legal word-prefixing 4107 * characters which should be merged onto a link of the form foo[[bar]]. 4108 * 4109 * @return string 4110 */ 4111 public function linkPrefixCharset() { 4112 return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' ); 4113 } 4114 4115 /** 4116 * @deprecated since 1.24, will be removed in 1.25 4117 * @return Language 4118 */ 4119 function getLangObj() { 4120 wfDeprecated( __METHOD__, '1.24' ); 4121 return $this; 4122 } 4123 4124 /** 4125 * Get the "parent" language which has a converter to convert a "compatible" language 4126 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb). 4127 * 4128 * @return Language|null 4129 * @since 1.22 4130 */ 4131 public function getParentLanguage() { 4132 if ( $this->mParentLanguage !== false ) { 4133 return $this->mParentLanguage; 4134 } 4135 4136 $pieces = explode( '-', $this->getCode() ); 4137 $code = $pieces[0]; 4138 if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) { 4139 $this->mParentLanguage = null; 4140 return null; 4141 } 4142 $lang = Language::factory( $code ); 4143 if ( !$lang->hasVariant( $this->getCode() ) ) { 4144 $this->mParentLanguage = null; 4145 return null; 4146 } 4147 4148 $this->mParentLanguage = $lang; 4149 return $lang; 4150 } 4151 4152 /** 4153 * Get the RFC 3066 code for this language object 4154 * 4155 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with 4156 * htmlspecialchars() or similar 4157 * 4158 * @return string 4159 */ 4160 public function getCode() { 4161 return $this->mCode; 4162 } 4163 4164 /** 4165 * Get the code in Bcp47 format which we can use 4166 * inside of html lang="" tags. 4167 * 4168 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with 4169 * htmlspecialchars() or similar. 4170 * 4171 * @since 1.19 4172 * @return string 4173 */ 4174 public function getHtmlCode() { 4175 if ( is_null( $this->mHtmlCode ) ) { 4176 $this->mHtmlCode = wfBCP47( $this->getCode() ); 4177 } 4178 return $this->mHtmlCode; 4179 } 4180 4181 /** 4182 * @param string $code 4183 */ 4184 public function setCode( $code ) { 4185 $this->mCode = $code; 4186 // Ensure we don't leave incorrect cached data lying around 4187 $this->mHtmlCode = null; 4188 $this->mParentLanguage = false; 4189 } 4190 4191 /** 4192 * Get the name of a file for a certain language code 4193 * @param string $prefix Prepend this to the filename 4194 * @param string $code Language code 4195 * @param string $suffix Append this to the filename 4196 * @throws MWException 4197 * @return string $prefix . $mangledCode . $suffix 4198 */ 4199 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) { 4200 if ( !self::isValidBuiltInCode( $code ) ) { 4201 throw new MWException( "Invalid language code \"$code\"" ); 4202 } 4203 4204 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix; 4205 } 4206 4207 /** 4208 * Get the language code from a file name. Inverse of getFileName() 4209 * @param string $filename $prefix . $languageCode . $suffix 4210 * @param string $prefix Prefix before the language code 4211 * @param string $suffix Suffix after the language code 4212 * @return string Language code, or false if $prefix or $suffix isn't found 4213 */ 4214 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) { 4215 $m = null; 4216 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' . 4217 preg_quote( $suffix, '/' ) . '/', $filename, $m ); 4218 if ( !count( $m ) ) { 4219 return false; 4220 } 4221 return str_replace( '_', '-', strtolower( $m[1] ) ); 4222 } 4223 4224 /** 4225 * @param string $code 4226 * @return string 4227 */ 4228 public static function getMessagesFileName( $code ) { 4229 global $IP; 4230 $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' ); 4231 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) ); 4232 return $file; 4233 } 4234 4235 /** 4236 * @param string $code 4237 * @return string 4238 * @since 1.23 4239 */ 4240 public static function getJsonMessagesFileName( $code ) { 4241 global $IP; 4242 4243 if ( !self::isValidBuiltInCode( $code ) ) { 4244 throw new MWException( "Invalid language code \"$code\"" ); 4245 } 4246 4247 return "$IP/languages/i18n/$code.json"; 4248 } 4249 4250 /** 4251 * @param string $code 4252 * @return string 4253 */ 4254 public static function getClassFileName( $code ) { 4255 global $IP; 4256 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' ); 4257 } 4258 4259 /** 4260 * Get the first fallback for a given language. 4261 * 4262 * @param string $code 4263 * 4264 * @return bool|string 4265 */ 4266 public static function getFallbackFor( $code ) { 4267 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) { 4268 return false; 4269 } else { 4270 $fallbacks = self::getFallbacksFor( $code ); 4271 $first = array_shift( $fallbacks ); 4272 return $first; 4273 } 4274 } 4275 4276 /** 4277 * Get the ordered list of fallback languages. 4278 * 4279 * @since 1.19 4280 * @param string $code Language code 4281 * @return array 4282 */ 4283 public static function getFallbacksFor( $code ) { 4284 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) { 4285 return array(); 4286 } else { 4287 $v = self::getLocalisationCache()->getItem( $code, 'fallback' ); 4288 $v = array_map( 'trim', explode( ',', $v ) ); 4289 if ( $v[count( $v ) - 1] !== 'en' ) { 4290 $v[] = 'en'; 4291 } 4292 return $v; 4293 } 4294 } 4295 4296 /** 4297 * Get the ordered list of fallback languages, ending with the fallback 4298 * language chain for the site language. 4299 * 4300 * @since 1.22 4301 * @param string $code Language code 4302 * @return array Array( fallbacks, site fallbacks ) 4303 */ 4304 public static function getFallbacksIncludingSiteLanguage( $code ) { 4305 global $wgLanguageCode; 4306 4307 // Usually, we will only store a tiny number of fallback chains, so we 4308 // keep them in static memory. 4309 $cacheKey = "{$code}-{$wgLanguageCode}"; 4310 4311 if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) { 4312 $fallbacks = self::getFallbacksFor( $code ); 4313 4314 // Append the site's fallback chain, including the site language itself 4315 $siteFallbacks = self::getFallbacksFor( $wgLanguageCode ); 4316 array_unshift( $siteFallbacks, $wgLanguageCode ); 4317 4318 // Eliminate any languages already included in the chain 4319 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks ); 4320 4321 self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks ); 4322 } 4323 return self::$fallbackLanguageCache[$cacheKey]; 4324 } 4325 4326 /** 4327 * Get all messages for a given language 4328 * WARNING: this may take a long time. If you just need all message *keys* 4329 * but need the *contents* of only a few messages, consider using getMessageKeysFor(). 4330 * 4331 * @param string $code 4332 * 4333 * @return array 4334 */ 4335 public static function getMessagesFor( $code ) { 4336 return self::getLocalisationCache()->getItem( $code, 'messages' ); 4337 } 4338 4339 /** 4340 * Get a message for a given language 4341 * 4342 * @param string $key 4343 * @param string $code 4344 * 4345 * @return string 4346 */ 4347 public static function getMessageFor( $key, $code ) { 4348 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key ); 4349 } 4350 4351 /** 4352 * Get all message keys for a given language. This is a faster alternative to 4353 * array_keys( Language::getMessagesFor( $code ) ) 4354 * 4355 * @since 1.19 4356 * @param string $code Language code 4357 * @return array Array of message keys (strings) 4358 */ 4359 public static function getMessageKeysFor( $code ) { 4360 return self::getLocalisationCache()->getSubItemList( $code, 'messages' ); 4361 } 4362 4363 /** 4364 * @param string $talk 4365 * @return mixed 4366 */ 4367 function fixVariableInNamespace( $talk ) { 4368 if ( strpos( $talk, '$1' ) === false ) { 4369 return $talk; 4370 } 4371 4372 global $wgMetaNamespace; 4373 $talk = str_replace( '$1', $wgMetaNamespace, $talk ); 4374 4375 # Allow grammar transformations 4376 # Allowing full message-style parsing would make simple requests 4377 # such as action=raw much more expensive than they need to be. 4378 # This will hopefully cover most cases. 4379 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i', 4380 array( &$this, 'replaceGrammarInNamespace' ), $talk ); 4381 return str_replace( ' ', '_', $talk ); 4382 } 4383 4384 /** 4385 * @param string $m 4386 * @return string 4387 */ 4388 function replaceGrammarInNamespace( $m ) { 4389 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) ); 4390 } 4391 4392 /** 4393 * @throws MWException 4394 * @return array 4395 */ 4396 static function getCaseMaps() { 4397 static $wikiUpperChars, $wikiLowerChars; 4398 if ( isset( $wikiUpperChars ) ) { 4399 return array( $wikiUpperChars, $wikiLowerChars ); 4400 } 4401 4402 wfProfileIn( __METHOD__ ); 4403 $arr = wfGetPrecompiledData( 'Utf8Case.ser' ); 4404 if ( $arr === false ) { 4405 throw new MWException( 4406 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" ); 4407 } 4408 $wikiUpperChars = $arr['wikiUpperChars']; 4409 $wikiLowerChars = $arr['wikiLowerChars']; 4410 wfProfileOut( __METHOD__ ); 4411 return array( $wikiUpperChars, $wikiLowerChars ); 4412 } 4413 4414 /** 4415 * Decode an expiry (block, protection, etc) which has come from the DB 4416 * 4417 * @todo FIXME: why are we returnings DBMS-dependent strings??? 4418 * 4419 * @param string $expiry Database expiry String 4420 * @param bool|int $format True to process using language functions, or TS_ constant 4421 * to return the expiry in a given timestamp 4422 * @return string 4423 * @since 1.18 4424 */ 4425 public function formatExpiry( $expiry, $format = true ) { 4426 static $infinity; 4427 if ( $infinity === null ) { 4428 $infinity = wfGetDB( DB_SLAVE )->getInfinity(); 4429 } 4430 4431 if ( $expiry == '' || $expiry == $infinity ) { 4432 return $format === true 4433 ? $this->getMessageFromDB( 'infiniteblock' ) 4434 : $infinity; 4435 } else { 4436 return $format === true 4437 ? $this->timeanddate( $expiry, /* User preference timezone */ true ) 4438 : wfTimestamp( $format, $expiry ); 4439 } 4440 } 4441 4442 /** 4443 * @todo Document 4444 * @param int|float $seconds 4445 * @param array $format Optional 4446 * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour. 4447 * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours. 4448 * If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev' 4449 * and friends. 4450 * For backwards compatibility, $format may also be one of the strings 'avoidseconds' 4451 * or 'avoidminutes'. 4452 * @return string 4453 */ 4454 function formatTimePeriod( $seconds, $format = array() ) { 4455 if ( !is_array( $format ) ) { 4456 $format = array( 'avoid' => $format ); // For backwards compatibility 4457 } 4458 if ( !isset( $format['avoid'] ) ) { 4459 $format['avoid'] = false; 4460 } 4461 if ( !isset( $format['noabbrevs'] ) ) { 4462 $format['noabbrevs'] = false; 4463 } 4464 $secondsMsg = wfMessage( 4465 $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this ); 4466 $minutesMsg = wfMessage( 4467 $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this ); 4468 $hoursMsg = wfMessage( 4469 $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this ); 4470 $daysMsg = wfMessage( 4471 $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this ); 4472 4473 if ( round( $seconds * 10 ) < 100 ) { 4474 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) ); 4475 $s = $secondsMsg->params( $s )->text(); 4476 } elseif ( round( $seconds ) < 60 ) { 4477 $s = $this->formatNum( round( $seconds ) ); 4478 $s = $secondsMsg->params( $s )->text(); 4479 } elseif ( round( $seconds ) < 3600 ) { 4480 $minutes = floor( $seconds / 60 ); 4481 $secondsPart = round( fmod( $seconds, 60 ) ); 4482 if ( $secondsPart == 60 ) { 4483 $secondsPart = 0; 4484 $minutes++; 4485 } 4486 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text(); 4487 $s .= ' '; 4488 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text(); 4489 } elseif ( round( $seconds ) <= 2 * 86400 ) { 4490 $hours = floor( $seconds / 3600 ); 4491 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 ); 4492 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 ); 4493 if ( $secondsPart == 60 ) { 4494 $secondsPart = 0; 4495 $minutes++; 4496 } 4497 if ( $minutes == 60 ) { 4498 $minutes = 0; 4499 $hours++; 4500 } 4501 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text(); 4502 $s .= ' '; 4503 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text(); 4504 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) { 4505 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text(); 4506 } 4507 } else { 4508 $days = floor( $seconds / 86400 ); 4509 if ( $format['avoid'] === 'avoidminutes' ) { 4510 $hours = round( ( $seconds - $days * 86400 ) / 3600 ); 4511 if ( $hours == 24 ) { 4512 $hours = 0; 4513 $days++; 4514 } 4515 $s = $daysMsg->params( $this->formatNum( $days ) )->text(); 4516 $s .= ' '; 4517 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text(); 4518 } elseif ( $format['avoid'] === 'avoidseconds' ) { 4519 $hours = floor( ( $seconds - $days * 86400 ) / 3600 ); 4520 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 ); 4521 if ( $minutes == 60 ) { 4522 $minutes = 0; 4523 $hours++; 4524 } 4525 if ( $hours == 24 ) { 4526 $hours = 0; 4527 $days++; 4528 } 4529 $s = $daysMsg->params( $this->formatNum( $days ) )->text(); 4530 $s .= ' '; 4531 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text(); 4532 $s .= ' '; 4533 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text(); 4534 } else { 4535 $s = $daysMsg->params( $this->formatNum( $days ) )->text(); 4536 $s .= ' '; 4537 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format ); 4538 } 4539 } 4540 return $s; 4541 } 4542 4543 /** 4544 * Format a bitrate for output, using an appropriate 4545 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to 4546 * the magnitude in question. 4547 * 4548 * This use base 1000. For base 1024 use formatSize(), for another base 4549 * see formatComputingNumbers(). 4550 * 4551 * @param int $bps 4552 * @return string 4553 */ 4554 function formatBitrate( $bps ) { 4555 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" ); 4556 } 4557 4558 /** 4559 * @param int $size Size of the unit 4560 * @param int $boundary Size boundary (1000, or 1024 in most cases) 4561 * @param string $messageKey Message key to be uesd 4562 * @return string 4563 */ 4564 function formatComputingNumbers( $size, $boundary, $messageKey ) { 4565 if ( $size <= 0 ) { 4566 return str_replace( '$1', $this->formatNum( $size ), 4567 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) ) 4568 ); 4569 } 4570 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ); 4571 $index = 0; 4572 4573 $maxIndex = count( $sizes ) - 1; 4574 while ( $size >= $boundary && $index < $maxIndex ) { 4575 $index++; 4576 $size /= $boundary; 4577 } 4578 4579 // For small sizes no decimal places necessary 4580 $round = 0; 4581 if ( $index > 1 ) { 4582 // For MB and bigger two decimal places are smarter 4583 $round = 2; 4584 } 4585 $msg = str_replace( '$1', $sizes[$index], $messageKey ); 4586 4587 $size = round( $size, $round ); 4588 $text = $this->getMessageFromDB( $msg ); 4589 return str_replace( '$1', $this->formatNum( $size ), $text ); 4590 } 4591 4592 /** 4593 * Format a size in bytes for output, using an appropriate 4594 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question 4595 * 4596 * This method use base 1024. For base 1000 use formatBitrate(), for 4597 * another base see formatComputingNumbers() 4598 * 4599 * @param int $size Size to format 4600 * @return string Plain text (not HTML) 4601 */ 4602 function formatSize( $size ) { 4603 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" ); 4604 } 4605 4606 /** 4607 * Make a list item, used by various special pages 4608 * 4609 * @param string $page Page link 4610 * @param string $details Text between brackets 4611 * @param bool $oppositedm Add the direction mark opposite to your 4612 * language, to display text properly 4613 * @return string 4614 */ 4615 function specialList( $page, $details, $oppositedm = true ) { 4616 $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . 4617 $this->getDirMark(); 4618 $details = $details ? $dirmark . $this->getMessageFromDB( 'word-separator' ) . 4619 wfMessage( 'parentheses' )->rawParams( $details )->inLanguage( $this )->escaped() : ''; 4620 return $page . $details; 4621 } 4622 4623 /** 4624 * Generate (prev x| next x) (20|50|100...) type links for paging 4625 * 4626 * @param Title $title Title object to link 4627 * @param int $offset 4628 * @param int $limit 4629 * @param array $query Optional URL query parameter string 4630 * @param bool $atend Optional param for specified if this is the last page 4631 * @return string 4632 */ 4633 public function viewPrevNext( Title $title, $offset, $limit, 4634 array $query = array(), $atend = false 4635 ) { 4636 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip? 4637 4638 # Make 'previous' link 4639 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text(); 4640 if ( $offset > 0 ) { 4641 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit, 4642 $query, $prev, 'prevn-title', 'mw-prevlink' ); 4643 } else { 4644 $plink = htmlspecialchars( $prev ); 4645 } 4646 4647 # Make 'next' link 4648 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text(); 4649 if ( $atend ) { 4650 $nlink = htmlspecialchars( $next ); 4651 } else { 4652 $nlink = $this->numLink( $title, $offset + $limit, $limit, 4653 $query, $next, 'nextn-title', 'mw-nextlink' ); 4654 } 4655 4656 # Make links to set number of items per page 4657 $numLinks = array(); 4658 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) { 4659 $numLinks[] = $this->numLink( $title, $offset, $num, 4660 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' ); 4661 } 4662 4663 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title 4664 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped(); 4665 } 4666 4667 /** 4668 * Helper function for viewPrevNext() that generates links 4669 * 4670 * @param Title $title Title object to link 4671 * @param int $offset 4672 * @param int $limit 4673 * @param array $query Extra query parameters 4674 * @param string $link Text to use for the link; will be escaped 4675 * @param string $tooltipMsg Name of the message to use as tooltip 4676 * @param string $class Value of the "class" attribute of the link 4677 * @return string HTML fragment 4678 */ 4679 private function numLink( Title $title, $offset, $limit, array $query, $link, 4680 $tooltipMsg, $class 4681 ) { 4682 $query = array( 'limit' => $limit, 'offset' => $offset ) + $query; 4683 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title ) 4684 ->numParams( $limit )->text(); 4685 4686 return Html::element( 'a', array( 'href' => $title->getLocalURL( $query ), 4687 'title' => $tooltip, 'class' => $class ), $link ); 4688 } 4689 4690 /** 4691 * Get the conversion rule title, if any. 4692 * 4693 * @return string 4694 */ 4695 public function getConvRuleTitle() { 4696 return $this->mConverter->getConvRuleTitle(); 4697 } 4698 4699 /** 4700 * Get the compiled plural rules for the language 4701 * @since 1.20 4702 * @return array Associative array with plural form, and plural rule as key-value pairs 4703 */ 4704 public function getCompiledPluralRules() { 4705 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' ); 4706 $fallbacks = Language::getFallbacksFor( $this->mCode ); 4707 if ( !$pluralRules ) { 4708 foreach ( $fallbacks as $fallbackCode ) { 4709 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' ); 4710 if ( $pluralRules ) { 4711 break; 4712 } 4713 } 4714 } 4715 return $pluralRules; 4716 } 4717 4718 /** 4719 * Get the plural rules for the language 4720 * @since 1.20 4721 * @return array Associative array with plural form number and plural rule as key-value pairs 4722 */ 4723 public function getPluralRules() { 4724 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' ); 4725 $fallbacks = Language::getFallbacksFor( $this->mCode ); 4726 if ( !$pluralRules ) { 4727 foreach ( $fallbacks as $fallbackCode ) { 4728 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' ); 4729 if ( $pluralRules ) { 4730 break; 4731 } 4732 } 4733 } 4734 return $pluralRules; 4735 } 4736 4737 /** 4738 * Get the plural rule types for the language 4739 * @since 1.22 4740 * @return array Associative array with plural form number and plural rule type as key-value pairs 4741 */ 4742 public function getPluralRuleTypes() { 4743 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' ); 4744 $fallbacks = Language::getFallbacksFor( $this->mCode ); 4745 if ( !$pluralRuleTypes ) { 4746 foreach ( $fallbacks as $fallbackCode ) { 4747 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' ); 4748 if ( $pluralRuleTypes ) { 4749 break; 4750 } 4751 } 4752 } 4753 return $pluralRuleTypes; 4754 } 4755 4756 /** 4757 * Find the index number of the plural rule appropriate for the given number 4758 * @param int $number 4759 * @return int The index number of the plural rule 4760 */ 4761 public function getPluralRuleIndexNumber( $number ) { 4762 $pluralRules = $this->getCompiledPluralRules(); 4763 $form = CLDRPluralRuleEvaluator::evaluateCompiled( $number, $pluralRules ); 4764 return $form; 4765 } 4766 4767 /** 4768 * Find the plural rule type appropriate for the given number 4769 * For example, if the language is set to Arabic, getPluralType(5) should 4770 * return 'few'. 4771 * @since 1.22 4772 * @param int $number 4773 * @return string The name of the plural rule type, e.g. one, two, few, many 4774 */ 4775 public function getPluralRuleType( $number ) { 4776 $index = $this->getPluralRuleIndexNumber( $number ); 4777 $pluralRuleTypes = $this->getPluralRuleTypes(); 4778 if ( isset( $pluralRuleTypes[$index] ) ) { 4779 return $pluralRuleTypes[$index]; 4780 } else { 4781 return 'other'; 4782 } 4783 } 4784 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |