47 private $manualRecache;
64 protected $sourceLanguage = [];
73 private $clearStoreCallbacks;
75 private $langNameUtils;
86 private $loadedItems = [];
94 private $loadedSubitems = [];
103 private $initialisedLangs = [];
112 private $shallowFallbacks = [];
119 private $recachedLangs = [];
131 private $coreDataLoaded = [];
137 'fallback',
'namespaceNames',
'bookstoreList',
138 'magicWords',
'messages',
'rtl',
139 'digitTransformTable',
'separatorTransformTable',
140 'minimumGroupingDigits',
'numberingSystem',
'fallback8bitEncoding',
141 'linkPrefixExtension',
'linkTrail',
'linkPrefixCharset',
142 'namespaceAliases',
'dateFormats',
'jsDateFormats',
'datePreferences',
143 'datePreferenceMigrationMap',
'defaultDateFormat',
144 'specialPageAliases',
'imageFiles',
'preloadedMessages',
145 'namespaceGenderAliases',
'digitGroupingPattern',
'pluralRules',
146 'pluralRuleTypes',
'compiledPluralRules',
'formalityIndex'
156 private const CORE_ONLY_KEYS = [
157 'fallback',
'rtl',
'digitTransformTable',
'separatorTransformTable',
158 'minimumGroupingDigits',
'numberingSystem',
159 'fallback8bitEncoding',
'linkPrefixExtension',
160 'linkTrail',
'linkPrefixCharset',
'datePreferences',
161 'datePreferenceMigrationMap',
'defaultDateFormat',
'digitGroupingPattern',
173 private const ALL_EXCEPT_CORE_ONLY_KEYS = [
174 'namespaceNames',
'bookstoreList',
'magicWords',
'messages',
175 'namespaceAliases',
'dateFormats',
'jsDateFormats',
'specialPageAliases',
176 'imageFiles',
'preloadedMessages',
'namespaceGenderAliases',
177 'pluralRules',
'pluralRuleTypes',
'compiledPluralRules',
187 private const MERGEABLE_MAP_KEYS = [
'messages',
'namespaceNames',
188 'namespaceAliases',
'dateFormats',
'jsDateFormats',
'imageFiles',
'preloadedMessages'
195 private const MERGEABLE_ALIAS_LIST_KEYS = [
'specialPageAliases' ];
202 private const OPTIONAL_MERGE_KEYS = [
'bookstoreList' ];
207 private const MAGIC_WORD_KEYS = [
'magicWords' ];
212 private const SPLIT_KEYS = [
'messages' ];
218 private const SOURCE_PREFIX_KEYS = [
'messages' ];
223 private const SOURCEPREFIX_SEPARATOR =
':';
228 private const PRELOADED_KEYS = [
'dateFormats',
'namespaceNames' ];
230 private const PLURAL_FILES = [
232 MW_INSTALL_PATH .
'/languages/data/plurals.xml',
234 MW_INSTALL_PATH .
'/languages/data/plurals-mediawiki.xml',
243 private static $pluralRules =
null;
259 private static $pluralRuleTypes =
null;
271 $storeArg[
'directory'] =
272 $conf[
'storeDirectory'] ?: $fallbackCacheDir;
274 if ( !empty( $conf[
'storeClass'] ) ) {
275 $storeClass = $conf[
'storeClass'];
276 } elseif ( $conf[
'store'] ===
'files' || $conf[
'store'] ===
'file' ||
277 ( $conf[
'store'] ===
'detect' && $storeArg[
'directory'] )
279 $storeClass = LCStoreCDB::class;
280 } elseif ( $conf[
'store'] ===
'db' || $conf[
'store'] ===
'detect' ) {
281 $storeClass = LCStoreDB::class;
282 $storeArg[
'server'] = $conf[
'storeServer'] ?? [];
283 } elseif ( $conf[
'store'] ===
'array' ) {
284 $storeClass = LCStoreStaticArray::class;
287 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
291 return new $storeClass( $storeArg );
297 public const CONSTRUCTOR_OPTIONS = [
301 MainConfigNames::ExtensionMessagesFiles,
302 MainConfigNames::MessagesDirs,
303 MainConfigNames::TranslationAliasesDirs,
322 LoggerInterface $logger,
323 array $clearStoreCallbacks,
324 LanguageNameUtils $langNameUtils,
329 $this->options = $options;
330 $this->store = $store;
331 $this->logger = $logger;
332 $this->clearStoreCallbacks = $clearStoreCallbacks;
333 $this->langNameUtils = $langNameUtils;
334 $this->hookRunner =
new HookRunner( $hookContainer );
337 $this->manualRecache = $options->
get(
'manualRecache' );
346 private static function isMergeableKey(
string $key ): bool {
347 static $mergeableKeys;
348 $mergeableKeys ??= array_fill_keys( [
349 ...self::MERGEABLE_MAP_KEYS,
350 ...self::MERGEABLE_ALIAS_LIST_KEYS,
351 ...self::OPTIONAL_MERGE_KEYS,
352 ...self::MAGIC_WORD_KEYS,
354 return isset( $mergeableKeys[$key] );
367 if ( !isset( $this->loadedItems[$code][$key] ) ) {
368 $this->loadItem( $code, $key );
371 if ( $key ===
'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
372 return $this->shallowFallbacks[$code];
376 return $this->data[$code][$key];
387 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
388 !isset( $this->loadedItems[$code][$key] )
390 $this->loadSubitem( $code, $key, $subkey );
393 return $this->data[$code][$key][$subkey] ??
null;
406 $subitem = $this->getSubitem( $code, $key, $subkey );
408 if ( $subitem ===
null ) {
413 return [ $subitem, $this->sourceLanguage[$code][$key][$subkey] ?? $code ];
430 if ( in_array( $key, self::SPLIT_KEYS ) ) {
431 return $this->getSubitem( $code,
'list', $key );
433 $item = $this->getItem( $code, $key );
434 if ( is_array( $item ) ) {
435 return array_keys( $item );
448 private function loadItem( $code, $key ) {
449 if ( isset( $this->loadedItems[$code][$key] ) ) {
454 in_array( $key, self::CORE_ONLY_KEYS,
true ) ||
456 $key ===
'fallbackSequence' ||
457 $key ===
'originalFallbackSequence'
459 if ( $this->langNameUtils->isValidBuiltInCode( $code ) ) {
460 $this->loadCoreData( $code );
465 if ( !isset( $this->initialisedLangs[$code] ) ) {
466 $this->initLanguage( $code );
469 if ( isset( $this->loadedItems[$code][$key] ) ) {
474 if ( isset( $this->shallowFallbacks[$code] ) ) {
475 $this->loadItem( $this->shallowFallbacks[$code], $key );
480 if ( in_array( $key, self::SPLIT_KEYS ) ) {
481 $subkeyList = $this->getSubitem( $code,
'list', $key );
482 foreach ( $subkeyList as $subkey ) {
483 if ( isset( $this->data[$code][$key][$subkey] ) ) {
486 $this->loadSubitem( $code, $key, $subkey );
489 $this->data[$code][$key] = $this->store->get( $code, $key );
492 $this->loadedItems[$code][$key] =
true;
502 private function loadSubitem( $code, $key, $subkey ) {
503 if ( !in_array( $key, self::SPLIT_KEYS ) ) {
504 $this->loadItem( $code, $key );
509 if ( !isset( $this->initialisedLangs[$code] ) ) {
510 $this->initLanguage( $code );
514 if ( isset( $this->loadedItems[$code][$key] ) ||
515 isset( $this->loadedSubitems[$code][$key][$subkey] )
520 if ( isset( $this->shallowFallbacks[$code] ) ) {
521 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
526 $value = $this->store->get( $code,
"$key:$subkey" );
527 if ( $value !==
null && in_array( $key, self::SOURCE_PREFIX_KEYS ) ) {
529 $this->sourceLanguage[$code][$key][$subkey],
530 $this->data[$code][$key][$subkey]
531 ] = explode( self::SOURCEPREFIX_SEPARATOR, $value, 2 );
533 $this->data[$code][$key][$subkey] = $value;
536 $this->loadedSubitems[$code][$key][$subkey] =
true;
547 if ( $this->options->get(
'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
548 $this->logger->debug( __METHOD__ .
"($code): forced reload" );
553 $deps = $this->store->get( $code,
'deps' );
554 $keys = $this->store->get( $code,
'list' );
555 $preload = $this->store->get( $code,
'preload' );
557 if ( $deps ===
null || $keys ===
null || $preload ===
null ) {
558 $this->logger->debug( __METHOD__ .
"($code): cache missing, need to make one" );
563 foreach ( $deps as $dep ) {
569 $this->logger->debug( __METHOD__ .
"($code): cache for $code expired due to " .
584 private function initLanguage( $code ) {
585 if ( isset( $this->initialisedLangs[$code] ) ) {
589 $this->initialisedLangs[$code] =
true;
591 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
592 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
593 $this->initShallowFallback( $code,
'en' );
598 # Re-cache the data if necessary
599 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
600 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
601 $this->recache( $code );
602 } elseif ( $code ===
'en' ) {
603 throw new RuntimeException(
'MessagesEn.php is missing.' );
605 $this->initShallowFallback( $code,
'en' );
612 $preload = $this->getItem( $code,
'preload' );
613 if ( $preload ===
null ) {
614 if ( $this->manualRecache ) {
616 if ( $code ===
'en' ) {
617 throw new RuntimeException(
'No localisation cache found for English. ' .
618 'Please run maintenance/rebuildLocalisationCache.php.' );
620 $this->initShallowFallback( $code,
'en' );
624 throw new RuntimeException(
'Invalid or missing localisation cache.' );
628 foreach ( self::SOURCE_PREFIX_KEYS as $key ) {
629 if ( !isset( $preload[$key] ) ) {
632 foreach ( $preload[$key] as $subkey => $value ) {
633 if ( $value !==
null ) {
635 $this->sourceLanguage[$code][$key][$subkey],
636 $preload[$key][$subkey]
637 ] = explode( self::SOURCEPREFIX_SEPARATOR, $value, 2 );
639 $preload[$key][$subkey] =
null;
644 if ( isset( $this->data[$code] ) ) {
645 foreach ( $preload as $key => $value ) {
647 $this->mergeItem( $key, $this->data[$code][$key], $value );
650 $this->data[$code] = $preload;
652 foreach ( $preload as $key => $item ) {
653 if ( in_array( $key, self::SPLIT_KEYS ) ) {
654 foreach ( $item as $subkey => $subitem ) {
655 $this->loadedSubitems[$code][$key][$subkey] =
true;
658 $this->loadedItems[$code][$key] =
true;
670 private function initShallowFallback( $primaryCode, $fallbackCode ) {
671 $this->data[$primaryCode] =& $this->data[$fallbackCode];
672 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
673 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
674 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
675 $this->coreDataLoaded[$primaryCode] =& $this->coreDataLoaded[$fallbackCode];
689 if ( $_fileType ==
'core' ) {
690 foreach ( self::ALL_KEYS as $key ) {
694 if ( isset( $$key ) ) {
698 } elseif ( $_fileType ==
'extension' ) {
699 foreach ( self::ALL_EXCEPT_CORE_ONLY_KEYS as $key ) {
701 if ( isset( $$key ) ) {
705 } elseif ( $_fileType ==
'aliases' ) {
707 if ( isset( $aliases ) ) {
708 $data[
'aliases'] = $aliases;
711 throw new InvalidArgumentException( __METHOD__ .
": Invalid file type: $_fileType" );
723 private function readJSONFile( $fileName ) {
724 if ( !is_readable( $fileName ) ) {
728 $json = file_get_contents( $fileName );
729 if ( $json ===
false ) {
733 $data = FormatJson::decode( $json,
true );
734 if ( $data ===
null ) {
735 throw new RuntimeException( __METHOD__ .
": Invalid JSON file: $fileName" );
739 foreach ( $data as $key => $unused ) {
740 if ( $key ===
'' || $key[0] ===
'@' ) {
741 unset( $data[$key] );
755 private function getCompiledPluralRules( $code ) {
756 $rules = $this->getPluralRules( $code );
757 if ( $rules ===
null ) {
761 $compiledRules = Evaluator::compile( $rules );
762 }
catch ( CLDRPluralRuleError $e ) {
763 $this->logger->debug( $e->getMessage() );
768 return $compiledRules;
780 private function getPluralRules( $code ) {
781 if ( self::$pluralRules ===
null ) {
782 self::loadPluralFiles();
784 return self::$pluralRules[$code] ??
null;
796 private function getPluralRuleTypes( $code ) {
797 if ( self::$pluralRuleTypes ===
null ) {
798 self::loadPluralFiles();
800 return self::$pluralRuleTypes[$code] ??
null;
806 private static function loadPluralFiles() {
807 foreach ( self::PLURAL_FILES as $fileName ) {
808 self::loadPluralFile( $fileName );
818 private static function loadPluralFile( $fileName ) {
820 $xml = file_get_contents( $fileName );
822 throw new RuntimeException(
"Unable to read plurals file $fileName" );
824 $doc =
new DOMDocument;
825 $doc->loadXML( $xml );
826 $rulesets = $doc->getElementsByTagName(
"pluralRules" );
827 foreach ( $rulesets as $ruleset ) {
828 $codes = $ruleset->getAttribute(
'locales' );
831 $ruleElements = $ruleset->getElementsByTagName(
"pluralRule" );
832 foreach ( $ruleElements as $elt ) {
833 $ruleType = $elt->getAttribute(
'count' );
834 if ( $ruleType ===
'other' ) {
838 $rules[] = $elt->nodeValue;
839 $ruleTypes[] = $ruleType;
841 foreach ( explode(
' ', $codes ) as $code ) {
842 self::$pluralRules[$code] = $rules;
843 self::$pluralRuleTypes[$code] = $ruleTypes;
856 private function readSourceFilesAndRegisterDeps( $code, &$deps ) {
858 $fileName = $this->langNameUtils->getMessagesFileName( $code );
859 if ( !is_file( $fileName ) ) {
863 $data = $this->readPHPFile( $fileName,
'core' );
877 private function readPluralFilesAndRegisterDeps( $code, &$deps ) {
880 'pluralRules' => $this->getPluralRules( $code ),
882 'compiledPluralRules' => $this->getCompiledPluralRules( $code ),
884 'pluralRuleTypes' => $this->getPluralRuleTypes( $code ),
887 foreach ( self::PLURAL_FILES as $fileName ) {
902 private function mergeItem( $key, &$value, $fallbackValue ) {
903 if ( $value !==
null ) {
904 if ( $fallbackValue !==
null ) {
905 if ( in_array( $key, self::MERGEABLE_MAP_KEYS ) ) {
906 $value += $fallbackValue;
907 } elseif ( in_array( $key, self::MERGEABLE_ALIAS_LIST_KEYS ) ) {
908 $value = array_merge_recursive( $value, $fallbackValue );
909 } elseif ( in_array( $key, self::OPTIONAL_MERGE_KEYS ) ) {
910 if ( !empty( $value[
'inherit'] ) ) {
911 $value = array_merge( $fallbackValue, $value );
914 unset( $value[
'inherit'] );
915 } elseif ( in_array( $key, self::MAGIC_WORD_KEYS ) ) {
916 $this->mergeMagicWords( $value, $fallbackValue );
920 $value = $fallbackValue;
924 private function mergeMagicWords( array &$value, array $fallbackValue ): void {
925 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
926 if ( !isset( $value[$magicName] ) ) {
927 $value[$magicName] = $fallbackInfo;
929 $value[$magicName] = [
933 ...array_slice( $value[$magicName], 1 ),
934 ...array_slice( $fallbackInfo, 1 ),
952 'core' =>
"$IP/languages/i18n",
953 'botpasswords' =>
"$IP/languages/i18n/botpasswords",
954 'codex' =>
"$IP/languages/i18n/codex",
955 'datetime' =>
"$IP/languages/i18n/datetime",
956 'exif' =>
"$IP/languages/i18n/exif",
957 'languageconverter' =>
"$IP/languages/i18n/languageconverter",
958 'interwiki' =>
"$IP/languages/i18n/interwiki",
959 'preferences' =>
"$IP/languages/i18n/preferences",
961 'nontranslatable' =>
"$IP/languages/i18n/nontranslatable",
963 'api' =>
"$IP/includes/Api/i18n",
964 'rest' =>
"$IP/includes/Rest/i18n",
965 'oojs-ui' =>
"$IP/resources/lib/ooui/i18n",
966 'paramvalidator' =>
"$IP/includes/libs/ParamValidator/i18n",
967 'installer' =>
"$IP/includes/Installer/i18n",
968 ] + $this->options->get( MainConfigNames::MessagesDirs );
981 private function loadCoreData(
string $code ) {
983 throw new InvalidArgumentException(
"Invalid language code requested" );
985 if ( $this->coreDataLoaded[$code] ??
false ) {
989 $coreData = array_fill_keys( self::CORE_ONLY_KEYS,
null );
992 # Load the primary localisation from the source file
993 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
994 $this->logger->debug( __METHOD__ .
": got localisation for $code from source" );
996 # Merge primary localisation
997 foreach ( $data as $key => $value ) {
998 $this->mergeItem( $key, $coreData[ $key ], $value );
1001 # Fill in the fallback if it's not there already
1003 if ( ( $coreData[
'fallback'] ===
null || $coreData[
'fallback'] ===
false ) && $code ===
'en' ) {
1004 $coreData[
'fallback'] =
false;
1005 $coreData[
'originalFallbackSequence'] = $coreData[
'fallbackSequence'] = [];
1007 if ( $coreData[
'fallback'] !==
null ) {
1008 $coreData[
'fallbackSequence'] = array_map(
'trim', explode(
',', $coreData[
'fallback'] ) );
1010 $coreData[
'fallbackSequence'] = [];
1012 $len = count( $coreData[
'fallbackSequence'] );
1014 # Before we add the 'en' fallback for messages, keep a copy of
1015 # the original fallback sequence
1016 $coreData[
'originalFallbackSequence'] = $coreData[
'fallbackSequence'];
1018 # Ensure that the sequence ends at 'en' for messages
1019 if ( !$len || $coreData[
'fallbackSequence'][$len - 1] !==
'en' ) {
1020 $coreData[
'fallbackSequence'][] =
'en';
1024 foreach ( $coreData[
'fallbackSequence'] as $fbCode ) {
1026 $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps );
1027 foreach ( self::CORE_ONLY_KEYS as $key ) {
1029 if ( isset( $fbData[$key] ) && !isset( $coreData[$key] ) ) {
1030 $coreData[$key] = $fbData[$key];
1035 $coreData[
'deps'] = $deps;
1036 foreach ( $coreData as $key => $item ) {
1037 $this->data[$code][$key] ??=
null;
1039 $this->mergeItem( $key, $this->data[$code][$key], $item );
1041 in_array( $key, self::CORE_ONLY_KEYS,
true ) ||
1043 $key ===
'fallbackSequence' ||
1044 $key ===
'originalFallbackSequence'
1050 $this->loadedItems[$code][$key] =
true;
1054 $this->coreDataLoaded[$code] =
true;
1065 throw new InvalidArgumentException(
"Invalid language code requested" );
1067 $this->recachedLangs[ $code ] =
true;
1070 $initialData = array_fill_keys( self::ALL_KEYS,
null );
1071 $this->data[$code] = [];
1072 $this->loadedItems[$code] = [];
1073 $this->loadedSubitems[$code] = [];
1074 $this->coreDataLoaded[$code] =
false;
1075 $this->loadCoreData( $code );
1076 $coreData = $this->data[$code];
1078 $deps = $coreData[
'deps'];
1079 $coreData += $this->readPluralFilesAndRegisterDeps( $code, $deps );
1081 $codeSequence = [ $code, ...$coreData[
'fallbackSequence'] ];
1082 $messageDirs = $this->getMessagesDirs();
1083 $translationAliasesDirs = $this->options->get( MainConfigNames::TranslationAliasesDirs );
1085 # Load non-JSON localisation data for extensions
1086 $extensionData = array_fill_keys( $codeSequence, $initialData );
1087 foreach ( $this->options->get( MainConfigNames::ExtensionMessagesFiles ) as $extension => $fileName ) {
1088 if ( isset( $messageDirs[$extension] ) || isset( $translationAliasesDirs[$extension] ) ) {
1089 # This extension has JSON message data; skip the PHP shim
1093 $data = $this->readPHPFile( $fileName,
'extension' );
1096 foreach ( $data as $key => $item ) {
1097 foreach ( $codeSequence as $csCode ) {
1098 if ( isset( $item[$csCode] ) ) {
1101 if ( in_array( $key, self::SOURCE_PREFIX_KEYS ) ) {
1102 foreach ( $item[$csCode] as $subkey => $_ ) {
1103 $this->sourceLanguage[$code][$key][$subkey] ??= $csCode;
1106 $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
1117 # Load the localisation data for each fallback, then merge it into the full array
1118 $allData = $initialData;
1119 foreach ( $codeSequence as $csCode ) {
1120 $csData = $initialData;
1122 # Load core messages and the extension localisations.
1123 foreach ( $messageDirs as $dirs ) {
1124 foreach ( (array)$dirs as $dir ) {
1125 $fileName =
"$dir/$csCode.json";
1126 $messages = $this->readJSONFile( $fileName );
1128 foreach ( $messages as $subkey => $_ ) {
1129 $this->sourceLanguage[$code][
'messages'][$subkey] ??= $csCode;
1131 $this->mergeItem(
'messages', $csData[
'messages'], $messages );
1137 foreach ( $translationAliasesDirs as $dirs ) {
1138 foreach ( (array)$dirs as $dir ) {
1139 $fileName =
"$dir/$csCode.json";
1140 $data = $this->readJSONFile( $fileName );
1142 foreach ( $data as $key => $item ) {
1145 $normalizedKey = lcfirst( $key );
1147 if ( $normalizedKey ===
'@metadata' ) {
1152 if ( !in_array( $normalizedKey, self::ALL_ALIAS_KEYS ) ) {
1153 throw new UnexpectedValueException(
1154 "Invalid key: \"$key\" for " . MainConfigNames::TranslationAliasesDirs .
". " .
1155 'Valid keys: ' . implode(
', ', self::ALL_ALIAS_KEYS )
1159 $this->mergeItem( $normalizedKey, $extensionData[$csCode][$normalizedKey], $item );
1166 # Merge non-JSON extension data
1167 if ( isset( $extensionData[$csCode] ) ) {
1168 foreach ( $extensionData[$csCode] as $key => $item ) {
1169 $this->mergeItem( $key, $csData[$key], $item );
1173 if ( $csCode === $code ) {
1174 # Merge core data into extension data
1175 foreach ( $coreData as $key => $item ) {
1176 $this->mergeItem( $key, $csData[$key], $item );
1179 # Load the secondary localisation from the source file to
1180 # avoid infinite cycles on cyclic fallbacks
1181 $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
1182 $fbData += $this->readPluralFilesAndRegisterDeps( $csCode, $deps );
1183 # Only merge the keys that make sense to merge
1184 foreach ( self::ALL_KEYS as $key ) {
1185 if ( !isset( $fbData[ $key ] ) ) {
1189 if ( !isset( $coreData[ $key ] ) || self::isMergeableKey( $key ) ) {
1190 $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
1195 # Allow extensions an opportunity to adjust the data for this fallback
1196 $this->hookRunner->onLocalisationCacheRecacheFallback( $this, $csCode, $csData );
1198 # Merge the data for this fallback into the final array
1199 if ( $csCode === $code ) {
1202 foreach ( self::ALL_KEYS as $key ) {
1203 if ( !isset( $csData[$key] ) ) {
1208 if ( $allData[$key] ===
null || self::isMergeableKey( $key ) ) {
1209 $this->mergeItem( $key, $allData[$key], $csData[$key] );
1215 if ( !isset( $allData[
'rtl'] ) ) {
1216 throw new RuntimeException( __METHOD__ .
': Localisation data failed validation check! ' .
1217 'Check that your languages/messages/MessagesEn.php file is intact.' );
1222 $deps[
'wgExtensionMessagesFiles'] =
1224 $deps[
'wgMessagesDirs'] =
1228 # Add dependencies to the cache entry
1229 $allData[
'deps'] = $deps;
1231 # Replace spaces with underscores in namespace names
1232 $allData[
'namespaceNames'] = str_replace(
' ',
'_', $allData[
'namespaceNames'] );
1234 # And do the same for special page aliases. $page is an array.
1235 foreach ( $allData[
'specialPageAliases'] as &$page ) {
1236 $page = str_replace(
' ',
'_', $page );
1238 # Decouple the reference to prevent accidental damage
1241 # If there were no plural rules, return an empty array
1242 $allData[
'pluralRules'] ??= [];
1243 $allData[
'compiledPluralRules'] ??= [];
1244 # If there were no plural rule types, return an empty array
1245 $allData[
'pluralRuleTypes'] ??= [];
1248 $allData[
'list'] = [];
1249 foreach ( self::SPLIT_KEYS as $key ) {
1250 $allData[
'list'][$key] = array_keys( $allData[$key] );
1254 $this->hookRunner->onLocalisationCacheRecache( $this, $code, $allData, $unused );
1256 # Save to the process cache and register the items loaded
1257 $this->data[$code] = $allData;
1258 $this->loadedItems[$code] = [];
1259 $this->loadedSubitems[$code] = [];
1260 foreach ( $allData as $key => $item ) {
1261 $this->loadedItems[$code][$key] =
true;
1264 # Prefix each item with its source language code before save
1265 foreach ( self::SOURCE_PREFIX_KEYS as $key ) {
1267 foreach ( $allData[$key] as $subKey => $value ) {
1269 $allData[$key][$subKey] = ( $this->sourceLanguage[$code][$key][$subKey] ?? $code ) .
1270 self::SOURCEPREFIX_SEPARATOR . $value;
1274 # Set the preload key
1275 $allData[
'preload'] = $this->buildPreload( $allData );
1277 # Save to the persistent cache
1278 $this->store->startWrite( $code );
1279 foreach ( $allData as $key => $value ) {
1280 if ( in_array( $key, self::SPLIT_KEYS ) ) {
1281 foreach ( $value as $subkey => $subvalue ) {
1282 $this->store->set(
"$key:$subkey", $subvalue );
1285 $this->store->set( $key, $value );
1288 $this->store->finishWrite();
1290 # Clear out the MessageBlobStore
1291 # HACK: If using a null (i.e., disabled) storage backend, we
1292 # can't write to the MessageBlobStore either
1294 foreach ( $this->clearStoreCallbacks as $callback ) {
1309 private function buildPreload( $data ) {
1310 $preload = [
'messages' => [] ];
1311 foreach ( self::PRELOADED_KEYS as $key ) {
1312 $preload[$key] = $data[$key];
1315 foreach ( $data[
'preloadedMessages'] as $subkey ) {
1316 $subitem = $data[
'messages'][$subkey] ??
null;
1317 $preload[
'messages'][$subkey] = $subitem;
1331 unset( $this->data[$code] );
1332 unset( $this->loadedItems[$code] );
1333 unset( $this->loadedSubitems[$code] );
1334 unset( $this->initialisedLangs[$code] );
1335 unset( $this->shallowFallbacks[$code] );
1336 unset( $this->sourceLanguage[$code] );
1337 unset( $this->coreDataLoaded[$code] );
1339 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1340 if ( $fbCode === $code ) {
1341 $this->unload( $shallowCode );
1350 foreach ( $this->initialisedLangs as $lang => $unused ) {
1351 $this->unload( $lang );
1360 $this->manualRecache =
false;