23 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
24 use CLDRPluralRuleParser\Evaluator;
29 use Psr\Log\LoggerInterface;
124 'fallback',
'namespaceNames',
'bookstoreList',
125 'magicWords',
'messages',
'rtl',
'capitalizeAllNouns',
126 'digitTransformTable',
'separatorTransformTable',
127 'minimumGroupingDigits',
'fallback8bitEncoding',
128 'linkPrefixExtension',
'linkTrail',
'linkPrefixCharset',
129 'namespaceAliases',
'dateFormats',
'datePreferences',
130 'datePreferenceMigrationMap',
'defaultDateFormat',
131 'specialPageAliases',
'imageFiles',
'preloadedMessages',
132 'namespaceGenderAliases',
'digitGroupingPattern',
'pluralRules',
133 'pluralRuleTypes',
'compiledPluralRules',
141 'namespaceAliases',
'dateFormats',
'imageFiles',
'preloadedMessages'
209 $storeArg[
'directory'] =
210 $conf[
'storeDirectory'] ?: $fallbackCacheDir;
212 if ( !empty( $conf[
'storeClass'] ) ) {
213 $storeClass = $conf[
'storeClass'];
214 } elseif ( $conf[
'store'] ===
'files' || $conf[
'store'] ===
'file' ||
215 ( $conf[
'store'] ===
'detect' && $storeArg[
'directory'] )
217 $storeClass = LCStoreCDB::class;
218 } elseif ( $conf[
'store'] ===
'db' || $conf[
'store'] ===
'detect' ) {
219 $storeClass = LCStoreDB::class;
220 $storeArg[
'server'] = $conf[
'storeServer'] ?? [];
221 } elseif ( $conf[
'store'] ===
'array' ) {
222 $storeClass = LCStoreStaticArray::class;
225 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
229 return new $storeClass( $storeArg );
239 'ExtensionMessagesFiles',
274 $this->hookRunner =
new HookRunner( $hookContainer );
277 $this->manualRecache =
$options->
get(
'manualRecache' );
287 if ( $this->mergeableKeys ===
null ) {
288 $this->mergeableKeys = array_flip( array_merge(
289 self::$mergeableMapKeys,
290 self::$mergeableListKeys,
291 self::$mergeableAliasListKeys,
292 self::$optionalMergeKeys,
297 return isset( $this->mergeableKeys[$key] );
310 if ( !isset( $this->loadedItems[$code][$key] ) ) {
314 if ( $key ===
'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
315 return $this->shallowFallbacks[$code];
318 return $this->data[$code][$key];
329 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
330 !isset( $this->loadedItems[$code][$key] )
335 return $this->data[$code][$key][$subkey] ??
null;
351 if ( in_array( $key, self::$splitKeys ) ) {
352 return $this->
getSubitem( $code,
'list', $key );
354 $item = $this->
getItem( $code, $key );
355 if ( is_array( $item ) ) {
356 return array_keys( $item );
369 if ( !isset( $this->initialisedLangs[$code] ) ) {
374 if ( isset( $this->loadedItems[$code][$key] ) ) {
378 if ( isset( $this->shallowFallbacks[$code] ) ) {
379 $this->
loadItem( $this->shallowFallbacks[$code], $key );
384 if ( in_array( $key, self::$splitKeys ) ) {
385 $subkeyList = $this->
getSubitem( $code,
'list', $key );
386 foreach ( $subkeyList as $subkey ) {
387 if ( isset( $this->data[$code][$key][$subkey] ) ) {
390 $this->data[$code][$key][$subkey] = $this->
getSubitem( $code, $key, $subkey );
393 $this->data[$code][$key] = $this->store->get( $code, $key );
396 $this->loadedItems[$code][$key] =
true;
406 if ( !in_array( $key, self::$splitKeys ) ) {
412 if ( !isset( $this->initialisedLangs[$code] ) ) {
417 if ( isset( $this->loadedItems[$code][$key] ) ||
418 isset( $this->loadedSubitems[$code][$key][$subkey] )
423 if ( isset( $this->shallowFallbacks[$code] ) ) {
424 $this->
loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
429 $value = $this->store->get( $code,
"$key:$subkey" );
430 $this->data[$code][$key][$subkey] = $value;
431 $this->loadedSubitems[$code][$key][$subkey] =
true;
442 if ( $this->options->get(
'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
443 $this->logger->debug( __METHOD__ .
"($code): forced reload" );
448 $deps = $this->store->get( $code,
'deps' );
449 $keys = $this->store->get( $code,
'list' );
450 $preload = $this->store->get( $code,
'preload' );
452 if ( $deps ===
null ||
$keys ===
null || $preload ===
null ) {
453 $this->logger->debug( __METHOD__ .
"($code): cache missing, need to make one" );
458 foreach ( $deps as $dep ) {
464 $this->logger->debug( __METHOD__ .
"($code): cache for $code expired due to " .
480 if ( isset( $this->initialisedLangs[$code] ) ) {
484 $this->initialisedLangs[$code] =
true;
486 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
487 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
493 # Recache the data if necessary
494 if ( !$this->manualRecache && $this->
isExpired( $code ) ) {
495 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
497 } elseif ( $code ===
'en' ) {
498 throw new MWException(
'MessagesEn.php is missing.' );
507 $preload = $this->
getItem( $code,
'preload' );
508 if ( $preload ===
null ) {
509 if ( $this->manualRecache ) {
511 if ( $code ===
'en' ) {
512 throw new MWException(
'No localisation cache found for English. ' .
513 'Please run maintenance/rebuildLocalisationCache.php.' );
519 throw new MWException(
'Invalid or missing localisation cache.' );
522 $this->data[$code] = $preload;
523 foreach ( $preload as $key => $item ) {
524 if ( in_array( $key, self::$splitKeys ) ) {
525 foreach ( $item as $subkey => $subitem ) {
526 $this->loadedSubitems[$code][$key][$subkey] =
true;
529 $this->loadedItems[$code][$key] =
true;
541 $this->data[$primaryCode] =& $this->data[$fallbackCode];
542 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
543 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
544 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
558 if ( $_fileType ==
'core' || $_fileType ==
'extension' ) {
559 foreach ( self::$allKeys as $key ) {
562 if ( isset( $$key ) ) {
566 } elseif ( $_fileType ==
'aliases' ) {
568 if ( isset( $aliases ) ) {
569 $data[
'aliases'] = $aliases;
572 throw new MWException( __METHOD__ .
": Invalid file type: $_fileType" );
585 if ( !is_readable( $fileName ) ) {
589 $json = file_get_contents( $fileName );
590 if ( $json ===
false ) {
595 if (
$data ===
null ) {
596 throw new MWException( __METHOD__ .
": Invalid JSON file: $fileName" );
600 foreach (
$data as $key => $unused ) {
601 if ( $key ===
'' || $key[0] ===
'@' ) {
602 unset(
$data[$key] );
607 return [
'messages' =>
$data ];
618 if ( $rules ===
null ) {
622 $compiledRules = Evaluator::compile( $rules );
623 }
catch ( CLDRPluralRuleError $e ) {
624 $this->logger->debug( $e->getMessage() );
629 return $compiledRules;
640 if ( $this->pluralRules ===
null ) {
643 return $this->pluralRules[$code] ??
null;
654 if ( $this->pluralRuleTypes ===
null ) {
657 return $this->pluralRuleTypes[$code] ??
null;
665 $cldrPlural =
"$IP/languages/data/plurals.xml";
666 $mwPlural =
"$IP/languages/data/plurals-mediawiki.xml";
669 if ( file_exists( $mwPlural ) ) {
684 $xml = file_get_contents( $fileName );
686 throw new MWException(
"Unable to read plurals file $fileName" );
688 $doc =
new DOMDocument;
689 $doc->loadXML( $xml );
690 $rulesets = $doc->getElementsByTagName(
"pluralRules" );
691 foreach ( $rulesets as $ruleset ) {
692 $codes = $ruleset->getAttribute(
'locales' );
695 $ruleElements = $ruleset->getElementsByTagName(
"pluralRule" );
696 foreach ( $ruleElements as $elt ) {
697 $ruleType = $elt->getAttribute(
'count' );
698 if ( $ruleType ===
'other' ) {
702 $rules[] = $elt->nodeValue;
703 $ruleTypes[] = $ruleType;
705 foreach ( explode(
' ', $codes ) as $code ) {
706 $this->pluralRules[$code] = $rules;
707 $this->pluralRuleTypes[$code] = $ruleTypes;
725 $fileName = $this->langNameUtils->getMessagesFileName( $code );
726 if ( !file_exists( $fileName ) ) {
733 # Load CLDR plural rules for JavaScript
737 # Load plural rule types
740 $deps[
'plurals'] =
new FileDependency(
"$IP/languages/data/plurals.xml" );
741 $deps[
'plurals-mw'] =
new FileDependency(
"$IP/languages/data/plurals-mediawiki.xml" );
753 protected function mergeItem( $key, &$value, $fallbackValue ) {
754 if ( $value !==
null ) {
755 if ( $fallbackValue !==
null ) {
756 if ( in_array( $key, self::$mergeableMapKeys ) ) {
757 $value += $fallbackValue;
758 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
759 $value = array_unique( array_merge( $fallbackValue, $value ) );
760 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
761 $value = array_merge_recursive( $value, $fallbackValue );
762 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
763 if ( !empty( $value[
'inherit'] ) ) {
764 $value = array_merge( $fallbackValue, $value );
767 unset( $value[
'inherit'] );
768 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
773 $value = $fallbackValue;
782 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
783 if ( !isset( $value[$magicName] ) ) {
784 $value[$magicName] = $fallbackInfo;
786 $oldSynonyms = array_slice( $fallbackInfo, 1 );
787 $newSynonyms = array_slice( $value[$magicName], 1 );
788 $synonyms = array_values( array_unique( array_merge(
789 $newSynonyms, $oldSynonyms ) ) );
790 $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
810 foreach ( $codeSequence as $code ) {
811 if ( isset( $fallbackValue[$code] ) ) {
812 $this->
mergeItem( $key, $value, $fallbackValue[$code] );
831 'core' =>
"$IP/languages/i18n",
832 'exif' =>
"$IP/languages/i18n/exif",
833 'api' =>
"$IP/includes/api/i18n",
834 'rest' =>
"$IP/includes/Rest/i18n",
835 'oojs-ui' =>
"$IP/resources/lib/ooui/i18n",
836 'paramvalidator' =>
"$IP/includes/libs/ParamValidator/i18n",
837 ] + $this->options->get(
'MessagesDirs' );
848 throw new MWException(
"Invalid language code requested" );
850 $this->recachedLangs[ $code ] =
true;
853 $initialData = array_fill_keys( self::$allKeys,
null );
854 $coreData = $initialData;
857 # Load the primary localisation from the source file
859 $this->logger->debug( __METHOD__ .
": got localisation for $code from source" );
861 # Merge primary localisation
862 foreach (
$data as $key => $value ) {
863 $this->
mergeItem( $key, $coreData[ $key ], $value );
866 # Fill in the fallback if it's not there already
868 if ( ( $coreData[
'fallback'] ===
null || $coreData[
'fallback'] ===
false ) && $code ===
'en' ) {
869 $coreData[
'fallback'] =
false;
870 $coreData[
'originalFallbackSequence'] = $coreData[
'fallbackSequence'] = [];
872 if ( $coreData[
'fallback'] !==
null ) {
873 $coreData[
'fallbackSequence'] = array_map(
'trim', explode(
',', $coreData[
'fallback'] ) );
875 $coreData[
'fallbackSequence'] = [];
877 $len = count( $coreData[
'fallbackSequence'] );
879 # Before we add the 'en' fallback for messages, keep a copy of
880 # the original fallback sequence
881 $coreData[
'originalFallbackSequence'] = $coreData[
'fallbackSequence'];
883 # Ensure that the sequence ends at 'en' for messages
884 if ( !$len || $coreData[
'fallbackSequence'][$len - 1] !==
'en' ) {
885 $coreData[
'fallbackSequence'][] =
'en';
889 $codeSequence = array_merge( [ $code ], $coreData[
'fallbackSequence'] );
892 # Load non-JSON localisation data for extensions
893 $extensionData = array_fill_keys( $codeSequence, $initialData );
894 foreach ( $this->options->get(
'ExtensionMessagesFiles' ) as $extension => $fileName ) {
895 if ( isset( $messageDirs[$extension] ) ) {
896 # This extension has JSON message data; skip the PHP shim
903 foreach (
$data as $key => $item ) {
904 foreach ( $codeSequence as $csCode ) {
905 if ( isset( $item[$csCode] ) ) {
906 $this->
mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
917 # Load the localisation data for each fallback, then merge it into the full array
918 $allData = $initialData;
919 foreach ( $codeSequence as $csCode ) {
920 $csData = $initialData;
922 # Load core messages and the extension localisations.
923 foreach ( $messageDirs as
$dirs ) {
924 foreach ( (array)
$dirs as $dir ) {
925 $fileName =
"$dir/$csCode.json";
928 foreach (
$data as $key => $item ) {
929 $this->
mergeItem( $key, $csData[$key], $item );
936 # Merge non-JSON extension data
937 if ( isset( $extensionData[$csCode] ) ) {
938 foreach ( $extensionData[$csCode] as $key => $item ) {
939 $this->
mergeItem( $key, $csData[$key], $item );
943 if ( $csCode === $code ) {
944 # Merge core data into extension data
945 foreach ( $coreData as $key => $item ) {
946 $this->
mergeItem( $key, $csData[$key], $item );
949 # Load the secondary localisation from the source file to
950 # avoid infinite cycles on cyclic fallbacks
952 # Only merge the keys that make sense to merge
953 foreach ( self::$allKeys as $key ) {
954 if ( !isset( $fbData[ $key ] ) ) {
958 if ( ( $coreData[ $key ] ) ===
null || $this->
isMergeableKey( $key ) ) {
959 $this->
mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
964 # Allow extensions an opportunity to adjust the data for this
966 $this->hookRunner->onLocalisationCacheRecacheFallback( $this, $csCode, $csData );
968 # Merge the data for this fallback into the final array
969 if ( $csCode === $code ) {
972 foreach ( self::$allKeys as $key ) {
973 if ( !isset( $csData[$key] ) ) {
979 $this->
mergeItem( $key, $allData[$key], $csData[$key] );
985 # Add cache dependencies for any referenced globals
986 $deps[
'wgExtensionMessagesFiles'] =
new GlobalDependency(
'wgExtensionMessagesFiles' );
992 # Add dependencies to the cache entry
993 $allData[
'deps'] = $deps;
995 # Replace spaces with underscores in namespace names
996 $allData[
'namespaceNames'] = str_replace(
' ',
'_', $allData[
'namespaceNames'] );
998 # And do the same for special page aliases. $page is an array.
999 foreach ( $allData[
'specialPageAliases'] as &$page ) {
1000 $page = str_replace(
' ',
'_', $page );
1002 # Decouple the reference to prevent accidental damage
1005 # If there were no plural rules, return an empty array
1006 if ( $allData[
'pluralRules'] ===
null ) {
1007 $allData[
'pluralRules'] = [];
1009 if ( $allData[
'compiledPluralRules'] ===
null ) {
1010 $allData[
'compiledPluralRules'] = [];
1012 # If there were no plural rule types, return an empty array
1013 if ( $allData[
'pluralRuleTypes'] ===
null ) {
1014 $allData[
'pluralRuleTypes'] = [];
1018 $allData[
'list'] = [];
1019 foreach ( self::$splitKeys as $key ) {
1020 $allData[
'list'][$key] = array_keys( $allData[$key] );
1024 $this->hookRunner->onLocalisationCacheRecache( $this, $code, $allData, $unused );
1026 if ( $allData[
'namespaceNames'] ===
null ) {
1027 throw new MWException( __METHOD__ .
': Localisation data failed sanity check! ' .
1028 'Check that your languages/messages/MessagesEn.php file is intact.' );
1031 # Set the preload key
1034 # Save to the process cache and register the items loaded
1035 $this->data[$code] = $allData;
1036 foreach ( $allData as $key => $item ) {
1037 $this->loadedItems[$code][$key] =
true;
1040 # Save to the persistent cache
1041 $this->store->startWrite( $code );
1042 foreach ( $allData as $key => $value ) {
1043 if ( in_array( $key, self::$splitKeys ) ) {
1044 foreach ( $value as $subkey => $subvalue ) {
1045 $this->store->set(
"$key:$subkey", $subvalue );
1048 $this->store->set( $key, $value );
1051 $this->store->finishWrite();
1053 # Clear out the MessageBlobStore
1054 # HACK: If using a null (i.e. disabled) storage backend, we
1055 # can't write to the MessageBlobStore either
1057 foreach ( $this->clearStoreCallbacks as $callback ) {
1072 $preload = [
'messages' => [] ];
1073 foreach ( self::$preloadedKeys as $key ) {
1074 $preload[$key] =
$data[$key];
1077 foreach (
$data[
'preloadedMessages'] as $subkey ) {
1078 $subitem =
$data[
'messages'][$subkey] ??
null;
1079 $preload[
'messages'][$subkey] = $subitem;
1091 unset( $this->data[$code] );
1092 unset( $this->loadedItems[$code] );
1093 unset( $this->loadedSubitems[$code] );
1094 unset( $this->initialisedLangs[$code] );
1095 unset( $this->shallowFallbacks[$code] );
1097 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1098 if ( $fbCode === $code ) {
1099 $this->
unload( $shallowCode );
1108 foreach ( $this->initialisedLangs as
$lang => $unused ) {
1118 $this->manualRecache =
false;