23 use CLDRPluralRuleParser\Evaluator;
24 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
27 use Psr\Log\LoggerInterface;
119 'fallback',
'namespaceNames',
'bookstoreList',
120 'magicWords',
'messages',
'rtl',
'capitalizeAllNouns',
121 'digitTransformTable',
'separatorTransformTable',
122 'minimumGroupingDigits',
'fallback8bitEncoding',
123 'linkPrefixExtension',
'linkTrail',
'linkPrefixCharset',
124 'namespaceAliases',
'dateFormats',
'datePreferences',
125 'datePreferenceMigrationMap',
'defaultDateFormat',
126 'specialPageAliases',
'imageFiles',
'preloadedMessages',
127 'namespaceGenderAliases',
'digitGroupingPattern',
'pluralRules',
128 'pluralRuleTypes',
'compiledPluralRules',
136 'namespaceAliases',
'dateFormats',
'imageFiles',
'preloadedMessages'
204 $storeArg[
'directory'] =
205 $conf[
'storeDirectory'] ?: $fallbackCacheDir;
207 if ( !empty( $conf[
'storeClass'] ) ) {
208 $storeClass = $conf[
'storeClass'];
209 } elseif ( $conf[
'store'] ===
'files' || $conf[
'store'] ===
'file' ||
210 ( $conf[
'store'] ===
'detect' && $storeArg[
'directory'] )
212 $storeClass = LCStoreCDB::class;
213 } elseif ( $conf[
'store'] ===
'db' || $conf[
'store'] ===
'detect' ) {
214 $storeClass = LCStoreDB::class;
215 $storeArg[
'server'] = $conf[
'storeServer'] ?? [];
216 } elseif ( $conf[
'store'] ===
'array' ) {
217 $storeClass = LCStoreStaticArray::class;
220 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
224 return new $storeClass( $storeArg );
231 public const CONSTRUCTOR_OPTIONS = [
235 'ExtensionMessagesFiles',
270 $this->manualRecache =
$options->
get(
'manualRecache' );
280 if ( $this->mergeableKeys ===
null ) {
281 $this->mergeableKeys = array_flip( array_merge(
282 self::$mergeableMapKeys,
283 self::$mergeableListKeys,
284 self::$mergeableAliasListKeys,
285 self::$optionalMergeKeys,
290 return isset( $this->mergeableKeys[$key] );
303 if ( !isset( $this->loadedItems[$code][$key] ) ) {
307 if ( $key ===
'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
308 return $this->shallowFallbacks[$code];
311 return $this->data[$code][$key];
322 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
323 !isset( $this->loadedItems[$code][$key] )
328 return $this->data[$code][$key][$subkey] ??
null;
344 if ( in_array( $key, self::$splitKeys ) ) {
345 return $this->
getSubitem( $code,
'list', $key );
347 $item = $this->
getItem( $code, $key );
348 if ( is_array( $item ) ) {
349 return array_keys( $item );
362 if ( !isset( $this->initialisedLangs[$code] ) ) {
367 if ( isset( $this->loadedItems[$code][$key] ) ) {
371 if ( isset( $this->shallowFallbacks[$code] ) ) {
372 $this->
loadItem( $this->shallowFallbacks[$code], $key );
377 if ( in_array( $key, self::$splitKeys ) ) {
378 $subkeyList = $this->
getSubitem( $code,
'list', $key );
379 foreach ( $subkeyList as $subkey ) {
380 if ( isset( $this->data[$code][$key][$subkey] ) ) {
383 $this->data[$code][$key][$subkey] = $this->
getSubitem( $code, $key, $subkey );
386 $this->data[$code][$key] = $this->store->get( $code, $key );
389 $this->loadedItems[$code][$key] =
true;
399 if ( !in_array( $key, self::$splitKeys ) ) {
405 if ( !isset( $this->initialisedLangs[$code] ) ) {
410 if ( isset( $this->loadedItems[$code][$key] ) ||
411 isset( $this->loadedSubitems[$code][$key][$subkey] )
416 if ( isset( $this->shallowFallbacks[$code] ) ) {
417 $this->
loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
422 $value = $this->store->get( $code,
"$key:$subkey" );
423 $this->data[$code][$key][$subkey] = $value;
424 $this->loadedSubitems[$code][$key][$subkey] =
true;
435 if ( $this->options->get(
'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
436 $this->logger->debug( __METHOD__ .
"($code): forced reload" );
441 $deps = $this->store->get( $code,
'deps' );
442 $keys = $this->store->get( $code,
'list' );
443 $preload = $this->store->get( $code,
'preload' );
445 if ( $deps ===
null ||
$keys ===
null || $preload ===
null ) {
446 $this->logger->debug( __METHOD__ .
"($code): cache missing, need to make one" );
451 foreach ( $deps as $dep ) {
457 $this->logger->debug( __METHOD__ .
"($code): cache for $code expired due to " .
473 if ( isset( $this->initialisedLangs[$code] ) ) {
477 $this->initialisedLangs[$code] =
true;
479 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
480 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
486 # Recache the data if necessary
487 if ( !$this->manualRecache && $this->
isExpired( $code ) ) {
488 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
490 } elseif ( $code ===
'en' ) {
491 throw new MWException(
'MessagesEn.php is missing.' );
500 $preload = $this->
getItem( $code,
'preload' );
501 if ( $preload ===
null ) {
502 if ( $this->manualRecache ) {
504 if ( $code ===
'en' ) {
505 throw new MWException(
'No localisation cache found for English. ' .
506 'Please run maintenance/rebuildLocalisationCache.php.' );
512 throw new MWException(
'Invalid or missing localisation cache.' );
515 $this->data[$code] = $preload;
516 foreach ( $preload as $key => $item ) {
517 if ( in_array( $key, self::$splitKeys ) ) {
518 foreach ( $item as $subkey => $subitem ) {
519 $this->loadedSubitems[$code][$key][$subkey] =
true;
522 $this->loadedItems[$code][$key] =
true;
534 $this->data[$primaryCode] =& $this->data[$fallbackCode];
535 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
536 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
537 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
551 if ( $_fileType ==
'core' || $_fileType ==
'extension' ) {
552 foreach ( self::$allKeys as $key ) {
555 if ( isset( $$key ) ) {
559 } elseif ( $_fileType ==
'aliases' ) {
560 if ( isset( $aliases ) ) {
561 $data[
'aliases'] = $aliases;
564 throw new MWException( __METHOD__ .
": Invalid file type: $_fileType" );
577 if ( !is_readable( $fileName ) ) {
581 $json = file_get_contents( $fileName );
582 if ( $json ===
false ) {
587 if (
$data ===
null ) {
588 throw new MWException( __METHOD__ .
": Invalid JSON file: $fileName" );
592 foreach (
$data as $key => $unused ) {
593 if ( $key ===
'' || $key[0] ===
'@' ) {
594 unset(
$data[$key] );
599 return [
'messages' =>
$data ];
610 if ( $rules ===
null ) {
614 $compiledRules = Evaluator::compile( $rules );
615 }
catch ( CLDRPluralRuleError $e ) {
616 $this->logger->debug( $e->getMessage() );
621 return $compiledRules;
632 if ( $this->pluralRules ===
null ) {
635 return $this->pluralRules[$code] ??
null;
646 if ( $this->pluralRuleTypes ===
null ) {
649 return $this->pluralRuleTypes[$code] ??
null;
657 $cldrPlural =
"$IP/languages/data/plurals.xml";
658 $mwPlural =
"$IP/languages/data/plurals-mediawiki.xml";
661 if ( file_exists( $mwPlural ) ) {
676 $xml = file_get_contents( $fileName );
678 throw new MWException(
"Unable to read plurals file $fileName" );
680 $doc =
new DOMDocument;
681 $doc->loadXML( $xml );
682 $rulesets = $doc->getElementsByTagName(
"pluralRules" );
683 foreach ( $rulesets as $ruleset ) {
684 $codes = $ruleset->getAttribute(
'locales' );
687 $ruleElements = $ruleset->getElementsByTagName(
"pluralRule" );
688 foreach ( $ruleElements as $elt ) {
689 $ruleType = $elt->getAttribute(
'count' );
690 if ( $ruleType ===
'other' ) {
694 $rules[] = $elt->nodeValue;
695 $ruleTypes[] = $ruleType;
697 foreach ( explode(
' ', $codes ) as $code ) {
698 $this->pluralRules[$code] = $rules;
699 $this->pluralRuleTypes[$code] = $ruleTypes;
717 $fileName = $this->langNameUtils->getMessagesFileName( $code );
718 if ( !file_exists( $fileName ) ) {
725 # Load CLDR plural rules for JavaScript
729 # Load plural rule types
732 $deps[
'plurals'] =
new FileDependency(
"$IP/languages/data/plurals.xml" );
733 $deps[
'plurals-mw'] =
new FileDependency(
"$IP/languages/data/plurals-mediawiki.xml" );
745 protected function mergeItem( $key, &$value, $fallbackValue ) {
746 if ( !is_null( $value ) ) {
747 if ( !is_null( $fallbackValue ) ) {
748 if ( in_array( $key, self::$mergeableMapKeys ) ) {
749 $value = $value + $fallbackValue;
750 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
752 $value = array_unique( array_merge( $fallbackValue, $value ) );
753 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
754 $value = array_merge_recursive( $value, $fallbackValue );
755 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
756 if ( !empty( $value[
'inherit'] ) ) {
757 $value = array_merge( $fallbackValue, $value );
760 if ( isset( $value[
'inherit'] ) ) {
761 unset( $value[
'inherit'] );
763 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
768 $value = $fallbackValue;
777 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
778 if ( !isset( $value[$magicName] ) ) {
779 $value[$magicName] = $fallbackInfo;
781 $oldSynonyms = array_slice( $fallbackInfo, 1 );
782 $newSynonyms = array_slice( $value[$magicName], 1 );
783 $synonyms = array_values( array_unique( array_merge(
784 $newSynonyms, $oldSynonyms ) ) );
785 $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
805 foreach ( $codeSequence as $code ) {
806 if ( isset( $fallbackValue[$code] ) ) {
807 $this->
mergeItem( $key, $value, $fallbackValue[$code] );
826 'core' =>
"$IP/languages/i18n",
827 'exif' =>
"$IP/languages/i18n/exif",
828 'api' =>
"$IP/includes/api/i18n",
829 'oojs-ui' =>
"$IP/resources/lib/ooui/i18n",
830 ] + $this->options->get(
'MessagesDirs' );
841 throw new MWException(
"Invalid language code requested" );
843 $this->recachedLangs[ $code ] =
true;
846 $initialData = array_fill_keys( self::$allKeys,
null );
847 $coreData = $initialData;
850 # Load the primary localisation from the source file
852 $this->logger->debug( __METHOD__ .
": got localisation for $code from source" );
854 # Merge primary localisation
855 foreach (
$data as $key => $value ) {
856 $this->
mergeItem( $key, $coreData[ $key ], $value );
859 # Fill in the fallback if it's not there already
860 if ( ( is_null( $coreData[
'fallback'] ) || $coreData[
'fallback'] ===
false ) && $code ===
'en' ) {
861 $coreData[
'fallback'] =
false;
862 $coreData[
'originalFallbackSequence'] = $coreData[
'fallbackSequence'] = [];
864 if ( !is_null( $coreData[
'fallback'] ) ) {
865 $coreData[
'fallbackSequence'] = array_map(
'trim', explode(
',', $coreData[
'fallback'] ) );
867 $coreData[
'fallbackSequence'] = [];
869 $len = count( $coreData[
'fallbackSequence'] );
871 # Before we add the 'en' fallback for messages, keep a copy of
872 # the original fallback sequence
873 $coreData[
'originalFallbackSequence'] = $coreData[
'fallbackSequence'];
875 # Ensure that the sequence ends at 'en' for messages
876 if ( !$len || $coreData[
'fallbackSequence'][$len - 1] !==
'en' ) {
877 $coreData[
'fallbackSequence'][] =
'en';
881 $codeSequence = array_merge( [ $code ], $coreData[
'fallbackSequence'] );
884 # Load non-JSON localisation data for extensions
885 $extensionData = array_fill_keys( $codeSequence, $initialData );
886 foreach ( $this->options->get(
'ExtensionMessagesFiles' ) as $extension => $fileName ) {
887 if ( isset( $messageDirs[$extension] ) ) {
888 # This extension has JSON message data; skip the PHP shim
895 foreach (
$data as $key => $item ) {
896 foreach ( $codeSequence as $csCode ) {
897 if ( isset( $item[$csCode] ) ) {
898 $this->
mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
909 # Load the localisation data for each fallback, then merge it into the full array
910 $allData = $initialData;
911 foreach ( $codeSequence as $csCode ) {
912 $csData = $initialData;
914 # Load core messages and the extension localisations.
915 foreach ( $messageDirs as
$dirs ) {
916 foreach ( (array)
$dirs as $dir ) {
917 $fileName =
"$dir/$csCode.json";
920 foreach (
$data as $key => $item ) {
921 $this->
mergeItem( $key, $csData[$key], $item );
928 # Merge non-JSON extension data
929 if ( isset( $extensionData[$csCode] ) ) {
930 foreach ( $extensionData[$csCode] as $key => $item ) {
931 $this->
mergeItem( $key, $csData[$key], $item );
935 if ( $csCode === $code ) {
936 # Merge core data into extension data
937 foreach ( $coreData as $key => $item ) {
938 $this->
mergeItem( $key, $csData[$key], $item );
941 # Load the secondary localisation from the source file to
942 # avoid infinite cycles on cyclic fallbacks
944 # Only merge the keys that make sense to merge
945 foreach ( self::$allKeys as $key ) {
946 if ( !isset( $fbData[ $key ] ) ) {
950 if ( is_null( $coreData[ $key ] ) || $this->
isMergeableKey( $key ) ) {
951 $this->
mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
956 # Allow extensions an opportunity to adjust the data for this
958 Hooks::run(
'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] );
960 # Merge the data for this fallback into the final array
961 if ( $csCode === $code ) {
964 foreach ( self::$allKeys as $key ) {
965 if ( !isset( $csData[$key] ) ) {
969 if ( is_null( $allData[$key] ) || $this->
isMergeableKey( $key ) ) {
970 $this->
mergeItem( $key, $allData[$key], $csData[$key] );
976 # Add cache dependencies for any referenced globals
977 $deps[
'wgExtensionMessagesFiles'] =
new GlobalDependency(
'wgExtensionMessagesFiles' );
983 # Add dependencies to the cache entry
984 $allData[
'deps'] = $deps;
986 # Replace spaces with underscores in namespace names
987 $allData[
'namespaceNames'] = str_replace(
' ',
'_', $allData[
'namespaceNames'] );
989 # And do the same for special page aliases. $page is an array.
990 foreach ( $allData[
'specialPageAliases'] as &$page ) {
991 $page = str_replace(
' ',
'_', $page );
993 # Decouple the reference to prevent accidental damage
996 # If there were no plural rules, return an empty array
997 if ( $allData[
'pluralRules'] ===
null ) {
998 $allData[
'pluralRules'] = [];
1000 if ( $allData[
'compiledPluralRules'] ===
null ) {
1001 $allData[
'compiledPluralRules'] = [];
1003 # If there were no plural rule types, return an empty array
1004 if ( $allData[
'pluralRuleTypes'] ===
null ) {
1005 $allData[
'pluralRuleTypes'] = [];
1009 $allData[
'list'] = [];
1010 foreach ( self::$splitKeys as $key ) {
1011 $allData[
'list'][$key] = array_keys( $allData[$key] );
1015 Hooks::run(
'LocalisationCacheRecache', [ $this, $code, &$allData, &$unused ] );
1017 if ( is_null( $allData[
'namespaceNames'] ) ) {
1018 throw new MWException( __METHOD__ .
': Localisation data failed sanity check! ' .
1019 'Check that your languages/messages/MessagesEn.php file is intact.' );
1022 # Set the preload key
1025 # Save to the process cache and register the items loaded
1026 $this->data[$code] = $allData;
1027 foreach ( $allData as $key => $item ) {
1028 $this->loadedItems[$code][$key] =
true;
1031 # Save to the persistent cache
1032 $this->store->startWrite( $code );
1033 foreach ( $allData as $key => $value ) {
1034 if ( in_array( $key, self::$splitKeys ) ) {
1035 foreach ( $value as $subkey => $subvalue ) {
1036 $this->store->set(
"$key:$subkey", $subvalue );
1039 $this->store->set( $key, $value );
1042 $this->store->finishWrite();
1044 # Clear out the MessageBlobStore
1045 # HACK: If using a null (i.e. disabled) storage backend, we
1046 # can't write to the MessageBlobStore either
1048 foreach ( $this->clearStoreCallbacks as $callback ) {
1063 $preload = [
'messages' => [] ];
1064 foreach ( self::$preloadedKeys as $key ) {
1065 $preload[$key] =
$data[$key];
1068 foreach (
$data[
'preloadedMessages'] as $subkey ) {
1069 $subitem =
$data[
'messages'][$subkey] ??
null;
1070 $preload[
'messages'][$subkey] = $subitem;
1082 unset( $this->data[$code] );
1083 unset( $this->loadedItems[$code] );
1084 unset( $this->loadedSubitems[$code] );
1085 unset( $this->initialisedLangs[$code] );
1086 unset( $this->shallowFallbacks[$code] );
1088 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1089 if ( $fbCode === $code ) {
1090 $this->
unload( $shallowCode );
1099 foreach ( $this->initialisedLangs as
$lang => $unused ) {
1109 $this->manualRecache =
false;