23 use CLDRPluralRuleParser\Evaluator;
24 use CLDRPluralRuleParser\Error
as CLDRPluralRuleError;
110 'fallback',
'namespaceNames',
'bookstoreList',
111 'magicWords',
'messages',
'rtl',
'capitalizeAllNouns',
'digitTransformTable',
112 'separatorTransformTable',
'minimumGroupingDigits',
113 'fallback8bitEncoding',
'linkPrefixExtension',
114 'linkTrail',
'linkPrefixCharset',
'namespaceAliases',
115 'dateFormats',
'datePreferences',
'datePreferenceMigrationMap',
116 'defaultDateFormat',
'extraUserToggles',
'specialPageAliases',
117 'imageFiles',
'preloadedMessages',
'namespaceGenderAliases',
118 'digitGroupingPattern',
'pluralRules',
'pluralRuleTypes',
'compiledPluralRules',
126 'namespaceAliases',
'dateFormats',
'imageFiles',
'preloadedMessages'
196 if ( !empty(
$conf[
'storeClass'] ) ) {
197 $storeClass =
$conf[
'storeClass'];
199 switch (
$conf[
'store'] ) {
211 if ( !empty(
$conf[
'storeDirectory'] ) ) {
222 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
228 if ( !empty(
$conf[
'storeDirectory'] ) ) {
229 $storeConf[
'directory'] =
$conf[
'storeDirectory'];
232 $this->
store =
new $storeClass( $storeConf );
233 foreach ( [
'manualRecache',
'forceRecache' ]
as $var ) {
234 if ( isset(
$conf[$var] ) ) {
235 $this->$var =
$conf[$var];
247 if ( $this->mergeableKeys ===
null ) {
248 $this->mergeableKeys = array_flip( array_merge(
249 self::$mergeableMapKeys,
250 self::$mergeableListKeys,
251 self::$mergeableAliasListKeys,
252 self::$optionalMergeKeys,
257 return isset( $this->mergeableKeys[$key] );
270 if ( !isset( $this->loadedItems[
$code][$key] ) ) {
274 if ( $key ===
'fallback' && isset( $this->shallowFallbacks[
$code] ) ) {
275 return $this->shallowFallbacks[
$code];
289 if ( !isset( $this->loadedSubitems[
$code][$key][$subkey] ) &&
290 !isset( $this->loadedItems[
$code][$key] )
295 if ( isset( $this->
data[
$code][$key][$subkey] ) ) {
296 return $this->
data[
$code][$key][$subkey];
315 if ( in_array( $key, self::$splitKeys ) ) {
319 if ( is_array( $item ) ) {
320 return array_keys( $item );
333 if ( !isset( $this->initialisedLangs[
$code] ) ) {
338 if ( isset( $this->loadedItems[
$code][$key] ) ) {
342 if ( isset( $this->shallowFallbacks[
$code] ) ) {
348 if ( in_array( $key, self::$splitKeys ) ) {
349 $subkeyList = $this->
getSubitem( $code,
'list', $key );
350 foreach ( $subkeyList
as $subkey ) {
351 if ( isset( $this->
data[$code][$key][$subkey] ) ) {
360 $this->loadedItems[
$code][$key] =
true;
370 if ( !in_array( $key, self::$splitKeys ) ) {
376 if ( !isset( $this->initialisedLangs[
$code] ) ) {
381 if ( isset( $this->loadedItems[
$code][$key] ) ||
382 isset( $this->loadedSubitems[
$code][$key][$subkey] )
387 if ( isset( $this->shallowFallbacks[
$code] ) ) {
393 $value = $this->
store->get( $code,
"$key:$subkey" );
395 $this->loadedSubitems[
$code][$key][$subkey] =
true;
406 if ( $this->forceRecache && !isset( $this->recachedLangs[
$code] ) ) {
407 wfDebug( __METHOD__ .
"($code): forced reload\n" );
412 $deps = $this->
store->get( $code,
'deps' );
414 $preload = $this->
store->get( $code,
'preload' );
416 if ( $deps ===
null ||
$keys ===
null || $preload ===
null ) {
417 wfDebug( __METHOD__ .
"($code): cache missing, need to make one\n" );
422 foreach ( $deps
as $dep ) {
428 wfDebug( __METHOD__ .
"($code): cache for $code expired due to " .
429 get_class( $dep ) .
"\n" );
444 if ( isset( $this->initialisedLangs[
$code] ) ) {
448 $this->initialisedLangs[
$code] =
true;
450 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
457 # Recache the data if necessary
458 if ( !$this->manualRecache && $this->
isExpired( $code ) ) {
461 } elseif (
$code ===
'en' ) {
462 throw new MWException(
'MessagesEn.php is missing.' );
471 $preload = $this->
getItem( $code,
'preload' );
472 if ( $preload ===
null ) {
473 if ( $this->manualRecache ) {
475 if (
$code ===
'en' ) {
476 throw new MWException(
'No localisation cache found for English. ' .
477 'Please run maintenance/rebuildLocalisationCache.php.' );
483 throw new MWException(
'Invalid or missing localisation cache.' );
487 foreach ( $preload
as $key => $item ) {
488 if ( in_array( $key, self::$splitKeys ) ) {
489 foreach ( $item
as $subkey => $subitem ) {
490 $this->loadedSubitems[
$code][$key][$subkey] =
true;
493 $this->loadedItems[
$code][$key] =
true;
505 $this->
data[$primaryCode] =& $this->
data[$fallbackCode];
506 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
507 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
508 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
520 Wikimedia\suppressWarnings();
521 $_apcEnabled = ini_set(
'apc.cache_by_default',
'0' );
522 Wikimedia\restoreWarnings();
526 Wikimedia\suppressWarnings();
527 ini_set(
'apc.cache_by_default', $_apcEnabled );
528 Wikimedia\restoreWarnings();
530 if ( $_fileType ==
'core' || $_fileType ==
'extension' ) {
531 $data = compact( self::$allKeys );
532 } elseif ( $_fileType ==
'aliases' ) {
533 $data = compact(
'aliases' );
535 throw new MWException( __METHOD__ .
": Invalid file type: $_fileType" );
548 if ( !is_readable( $fileName ) ) {
552 $json = file_get_contents( $fileName );
553 if ( $json ===
false ) {
558 if (
$data ===
null ) {
559 throw new MWException( __METHOD__ .
": Invalid JSON file: $fileName" );
563 foreach (
$data as $key => $unused ) {
564 if ( $key ===
'' || $key[0] ===
'@' ) {
565 unset(
$data[$key] );
570 return [
'messages' =>
$data ];
581 if ( $rules ===
null ) {
585 $compiledRules = Evaluator::compile( $rules );
586 }
catch ( CLDRPluralRuleError
$e ) {
592 return $compiledRules;
603 if ( $this->pluralRules ===
null ) {
606 if ( !isset( $this->pluralRules[
$code] ) ) {
609 return $this->pluralRules[
$code];
621 if ( $this->pluralRuleTypes ===
null ) {
624 if ( !isset( $this->pluralRuleTypes[
$code] ) ) {
627 return $this->pluralRuleTypes[
$code];
636 $cldrPlural =
"$IP/languages/data/plurals.xml";
637 $mwPlural =
"$IP/languages/data/plurals-mediawiki.xml";
640 if ( file_exists( $mwPlural ) ) {
655 $xml = file_get_contents( $fileName );
657 throw new MWException(
"Unable to read plurals file $fileName" );
659 $doc =
new DOMDocument;
660 $doc->loadXML( $xml );
661 $rulesets = $doc->getElementsByTagName(
"pluralRules" );
662 foreach ( $rulesets
as $ruleset ) {
663 $codes = $ruleset->getAttribute(
'locales' );
666 $ruleElements = $ruleset->getElementsByTagName(
"pluralRule" );
667 foreach ( $ruleElements
as $elt ) {
668 $ruleType = $elt->getAttribute(
'count' );
669 if ( $ruleType ===
'other' ) {
673 $rules[] = $elt->nodeValue;
674 $ruleTypes[] = $ruleType;
676 foreach ( explode(
' ', $codes )
as $code ) {
677 $this->pluralRules[
$code] = $rules;
678 $this->pluralRuleTypes[
$code] = $ruleTypes;
697 if ( !file_exists( $fileName ) ) {
704 # Load CLDR plural rules for JavaScript
708 # Load plural rule types
711 $deps[
'plurals'] =
new FileDependency(
"$IP/languages/data/plurals.xml" );
712 $deps[
'plurals-mw'] =
new FileDependency(
"$IP/languages/data/plurals-mediawiki.xml" );
725 if ( !is_null(
$value ) ) {
726 if ( !is_null( $fallbackValue ) ) {
727 if ( in_array( $key, self::$mergeableMapKeys ) ) {
729 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
730 $value = array_unique( array_merge( $fallbackValue,
$value ) );
731 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
732 $value = array_merge_recursive(
$value, $fallbackValue );
733 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
734 if ( !empty(
$value[
'inherit'] ) ) {
738 if ( isset(
$value[
'inherit'] ) ) {
739 unset(
$value[
'inherit'] );
741 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
755 foreach ( $fallbackValue
as $magicName => $fallbackInfo ) {
756 if ( !isset(
$value[$magicName] ) ) {
757 $value[$magicName] = $fallbackInfo;
759 $oldSynonyms = array_slice( $fallbackInfo, 1 );
760 $newSynonyms = array_slice(
$value[$magicName], 1 );
761 $synonyms = array_values( array_unique( array_merge(
762 $newSynonyms, $oldSynonyms ) ) );
763 $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
783 foreach ( $codeSequence
as $code ) {
784 if ( isset( $fallbackValue[
$code] ) ) {
803 $config = MediaWikiServices::getInstance()->getMainConfig();
804 $messagesDirs = $config->get(
'MessagesDirs' );
806 'core' =>
"$IP/languages/i18n",
807 'api' =>
"$IP/includes/api/i18n",
808 'oojs-ui' =>
"$IP/resources/lib/oojs-ui/i18n",
822 throw new MWException(
"Invalid language code requested" );
824 $this->recachedLangs[
$code] =
true;
827 $initialData = array_fill_keys( self::$allKeys,
null );
828 $coreData = $initialData;
831 # Load the primary localisation from the source file
833 if (
$data ===
false ) {
834 wfDebug( __METHOD__ .
": no localisation file for $code, using fallback to en\n" );
835 $coreData[
'fallback'] =
'en';
837 wfDebug( __METHOD__ .
": got localisation for $code from source\n" );
839 # Merge primary localisation
845 # Fill in the fallback if it's not there already
846 if ( is_null( $coreData[
'fallback'] ) ) {
847 $coreData[
'fallback'] =
$code ===
'en' ?
false :
'en';
849 if ( $coreData[
'fallback'] ===
false ) {
850 $coreData[
'fallbackSequence'] = [];
852 $coreData[
'fallbackSequence'] = array_map(
'trim', explode(
',', $coreData[
'fallback'] ) );
853 $len =
count( $coreData[
'fallbackSequence'] );
855 # Ensure that the sequence ends at en
856 if ( $coreData[
'fallbackSequence'][$len - 1] !==
'en' ) {
857 $coreData[
'fallbackSequence'][] =
'en';
861 $codeSequence = array_merge( [
$code ], $coreData[
'fallbackSequence'] );
864 # Load non-JSON localisation data for extensions
865 $extensionData = array_fill_keys( $codeSequence, $initialData );
867 if ( isset( $messageDirs[$extension] ) ) {
868 # This extension has JSON message data; skip the PHP shim
875 foreach (
$data as $key => $item ) {
876 foreach ( $codeSequence
as $csCode ) {
877 if ( isset( $item[$csCode] ) ) {
878 $this->
mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
889 # Load the localisation data for each fallback, then merge it into the full array
890 $allData = $initialData;
891 foreach ( $codeSequence
as $csCode ) {
892 $csData = $initialData;
894 # Load core messages and the extension localisations.
895 foreach ( $messageDirs
as $dirs ) {
897 $fileName =
"$dir/$csCode.json";
900 foreach (
$data as $key => $item ) {
901 $this->
mergeItem( $key, $csData[$key], $item );
908 # Merge non-JSON extension data
909 if ( isset( $extensionData[$csCode] ) ) {
910 foreach ( $extensionData[$csCode]
as $key => $item ) {
911 $this->
mergeItem( $key, $csData[$key], $item );
915 if ( $csCode ===
$code ) {
916 # Merge core data into extension data
917 foreach ( $coreData
as $key => $item ) {
918 $this->
mergeItem( $key, $csData[$key], $item );
921 # Load the secondary localisation from the source file to
922 # avoid infinite cycles on cyclic fallbacks
924 if ( $fbData !==
false ) {
925 # Only merge the keys that make sense to merge
926 foreach ( self::$allKeys
as $key ) {
927 if ( !isset( $fbData[$key] ) ) {
931 if ( is_null( $coreData[$key] ) || $this->
isMergeableKey( $key ) ) {
932 $this->
mergeItem( $key, $csData[$key], $fbData[$key] );
938 # Allow extensions an opportunity to adjust the data for this
940 Hooks::run(
'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] );
942 # Merge the data for this fallback into the final array
943 if ( $csCode ===
$code ) {
946 foreach ( self::$allKeys
as $key ) {
947 if ( !isset( $csData[$key] ) ) {
951 if ( is_null( $allData[$key] ) || $this->
isMergeableKey( $key ) ) {
952 $this->
mergeItem( $key, $allData[$key], $csData[$key] );
958 # Add cache dependencies for any referenced globals
959 $deps[
'wgExtensionMessagesFiles'] =
new GlobalDependency(
'wgExtensionMessagesFiles' );
965 # Add dependencies to the cache entry
966 $allData[
'deps'] = $deps;
968 # Replace spaces with underscores in namespace names
969 $allData[
'namespaceNames'] = str_replace(
' ',
'_', $allData[
'namespaceNames'] );
971 # And do the same for special page aliases. $page is an array.
972 foreach ( $allData[
'specialPageAliases']
as &$page ) {
973 $page = str_replace(
' ',
'_', $page );
975 # Decouple the reference to prevent accidental damage
978 # If there were no plural rules, return an empty array
979 if ( $allData[
'pluralRules'] ===
null ) {
980 $allData[
'pluralRules'] = [];
982 if ( $allData[
'compiledPluralRules'] ===
null ) {
983 $allData[
'compiledPluralRules'] = [];
985 # If there were no plural rule types, return an empty array
986 if ( $allData[
'pluralRuleTypes'] ===
null ) {
987 $allData[
'pluralRuleTypes'] = [];
991 $allData[
'list'] = [];
992 foreach ( self::$splitKeys
as $key ) {
993 $allData[
'list'][$key] = array_keys( $allData[$key] );
997 Hooks::run(
'LocalisationCacheRecache', [ $this,
$code, &$allData, &$purgeBlobs ] );
999 if ( is_null( $allData[
'namespaceNames'] ) ) {
1000 throw new MWException( __METHOD__ .
': Localisation data failed sanity check! ' .
1001 'Check that your languages/messages/MessagesEn.php file is intact.' );
1004 # Set the preload key
1007 # Save to the process cache and register the items loaded
1009 foreach ( $allData
as $key => $item ) {
1010 $this->loadedItems[
$code][$key] =
true;
1013 # Save to the persistent cache
1015 foreach ( $allData
as $key =>
$value ) {
1016 if ( in_array( $key, self::$splitKeys ) ) {
1017 foreach (
$value as $subkey => $subvalue ) {
1018 $this->
store->set(
"$key:$subkey", $subvalue );
1024 $this->
store->finishWrite();
1026 # Clear out the MessageBlobStore
1027 # HACK: If using a null (i.e. disabled) storage backend, we
1028 # can't write to the MessageBlobStore either
1031 $blobStore->clear();
1044 $preload = [
'messages' => [] ];
1045 foreach ( self::$preloadedKeys
as $key ) {
1046 $preload[$key] =
$data[$key];
1049 foreach (
$data[
'preloadedMessages']
as $subkey ) {
1050 if ( isset(
$data[
'messages'][$subkey] ) ) {
1051 $subitem =
$data[
'messages'][$subkey];
1055 $preload[
'messages'][$subkey] = $subitem;
1068 unset( $this->loadedItems[
$code] );
1069 unset( $this->loadedSubitems[
$code] );
1070 unset( $this->initialisedLangs[
$code] );
1071 unset( $this->shallowFallbacks[
$code] );
1073 foreach ( $this->shallowFallbacks
as $shallowCode => $fbCode ) {
1074 if ( $fbCode ===
$code ) {
1075 $this->
unload( $shallowCode );
1084 foreach ( $this->initialisedLangs
as $lang => $unused ) {
1094 $this->manualRecache =
false;