23use CLDRPluralRuleParser\Evaluator;
24use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
27use 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',
257 LoggerInterface $logger,
258 array $clearStoreCallbacks,
263 $this->options = $options;
264 $this->store = $store;
265 $this->logger = $logger;
266 $this->clearStoreCallbacks = $clearStoreCallbacks;
267 $this->langNameUtils = $langNameUtils;
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] ) ) {
304 $this->loadItem( $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] )
325 $this->loadSubitem( $code, $key, $subkey );
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] ) ) {
363 $this->initLanguage( $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 ) ) {
400 $this->loadItem( $code, $key );
405 if ( !isset( $this->initialisedLangs[$code] ) ) {
406 $this->initLanguage( $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 ) ) {
481 $this->initShallowFallback( $code,
'en' );
486 # Recache the data if necessary
487 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
488 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
489 $this->recache( $code );
490 } elseif ( $code ===
'en' ) {
491 throw new MWException(
'MessagesEn.php is missing.' );
493 $this->initShallowFallback( $code,
'en' );
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.' );
508 $this->initShallowFallback( $code,
'en' );
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 ) {
586 $data = FormatJson::decode( $json,
true );
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 ];
609 $rules = $this->getPluralRules( $code );
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 ) {
633 $this->loadPluralFiles();
635 return $this->pluralRules[$code] ??
null;
646 if ( $this->pluralRuleTypes ===
null ) {
647 $this->loadPluralFiles();
649 return $this->pluralRuleTypes[$code] ??
null;
657 $cldrPlural =
"$IP/languages/data/plurals.xml";
658 $mwPlural =
"$IP/languages/data/plurals-mediawiki.xml";
660 $this->loadPluralFile( $cldrPlural );
661 if ( file_exists( $mwPlural ) ) {
663 $this->loadPluralFile( $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 ) ) {
722 $data = $this->readPHPFile( $fileName,
'core' );
725 # Load CLDR plural rules for JavaScript
726 $data[
'pluralRules'] = $this->getPluralRules( $code );
728 $data[
'compiledPluralRules'] = $this->getCompiledPluralRules( $code );
729 # Load plural rule types
730 $data[
'pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
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 ) ) {
764 $this->mergeMagicWords( $value, $fallbackValue );
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
851 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
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'] );
882 $messageDirs = $this->getMessagesDirs();
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
892 $data = $this->readPHPFile( $fileName,
'extension' );
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";
918 $data = $this->readJSONFile( $fileName );
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
943 $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
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
1023 $allData[
'preload'] = $this->buildPreload( $allData );
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 ) {
1100 $this->unload(
$lang );
1109 $this->manualRecache =
false;
isExpired()
Returns true if the dependency is expired, false otherwise.
Null store backend, used to avoid DB errors during install.
Class for caching the contents of localisation files, Messages*.php and *.i18n.php.
$manualRecache
True if recaching should only be done on an explicit call to recache().
static $magicWordKeys
Keys for items that are formatted like $magicWords.
buildPreload( $data)
Build the preload item from the given pre-cache data.
initLanguage( $code)
Initialise a language in this object.
static $mergeableMapKeys
Keys for items which consist of associative arrays, which may be merged by a fallback sequence.
readSourceFilesAndRegisterDeps( $code, &$deps)
Read the data from the source files for a given language, and register the relevant dependencies in t...
isMergeableKey( $key)
Returns true if the given key is mergeable, that is, if it is an associative array which can be merge...
loadPluralFile( $fileName)
Load a plural XML file with the given filename, compile the relevant rules, and save the compiled rul...
getPluralRules( $code)
Get the plural rules for a given language from the XML files.
$loadedItems
A 2-d associative array, code/key, where presence indicates that the item is loaded.
readPHPFile( $_fileName, $_fileType)
Read a PHP file containing localisation data.
$initialisedLangs
An array where presence of a key indicates that that language has been initialised.
readJSONFile( $fileName)
Read a JSON file containing localisation messages.
loadPluralFiles()
Load the plural XML files.
unload( $code)
Unload the data for a given language from the object cache.
unloadAll()
Unload all data.
disableBackend()
Disable the storage backend.
static $mergeableAliasListKeys
Keys for items which contain an array of arrays of equivalent aliases for each subitem.
getSubitemList( $code, $key)
Get the list of subitem keys for a given item.
$recachedLangs
An array where the keys are codes that have been recached by this instance.
$loadedSubitems
A 3-d associative array, code/key/subkey, where presence indicates that the subitem is loaded.
static $mergeableListKeys
Keys for items which are a numbered array.
recache( $code)
Load localisation data for a given language for both core and extensions and save it to the persisten...
isExpired( $code)
Returns true if the cache identified by $code is missing or expired.
mergeItem( $key, &$value, $fallbackValue)
Merge two localisation values, a primary and a fallback, overwriting the primary value in place.
getSubitem( $code, $key, $subkey)
Get a subitem, for instance a single message for a given language.
mergeMagicWords(&$value, $fallbackValue)
$shallowFallbacks
An array mapping non-existent pseudo-languages to fallback languages.
initShallowFallback( $primaryCode, $fallbackCode)
Create a fallback from one language to another, without creating a complete persistent cache.
static getStoreFromConf(array $conf, $fallbackCacheDir)
Return a suitable LCStore as specified by the given configuration.
LanguageNameUtils $langNameUtils
static $allKeys
All item keys.
getCompiledPluralRules( $code)
Get the compiled plural rules for a given language from the XML files.
static $optionalMergeKeys
Keys for items which contain an associative array, and may be merged if the primary value contains th...
getMessagesDirs()
Gets the combined list of messages dirs from core and extensions.
callable[] $clearStoreCallbacks
See comment for parameter in constructor.
static $preloadedKeys
Keys which are loaded automatically by initLanguage()
getItem( $code, $key)
Get a cache item.
static $splitKeys
Keys for items where the subitems are stored in the backend separately.
loadItem( $code, $key)
Load an item into the cache.
$pluralRuleTypes
Associative array of cached plural rule types.
getPluralRuleTypes( $code)
Get the plural rule types for a given language from the XML files.
__construct(ServiceOptions $options, LCStore $store, LoggerInterface $logger, array $clearStoreCallbacks, LanguageNameUtils $langNameUtils)
For constructor parameters, see the documentation in DefaultSettings.php for $wgLocalisationCacheConf...
LCStore $store
The persistent store object.
loadSubitem( $code, $key, $subkey)
Load a subitem into the cache.
$pluralRules
Associative array of cached plural rules.
mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue)
Given an array mapping language code to localisation value, such as is found in extension *....
Interface for the persistence layer of LocalisationCache.
if(!isset( $args[0])) $lang