25 use CLDRPluralRuleParser\Evaluator;
26 use CLDRPluralRuleParser\Error
as CLDRPluralRuleError;
112 'fallback',
'namespaceNames',
'bookstoreList',
113 'magicWords',
'messages',
'rtl',
'capitalizeAllNouns',
'digitTransformTable',
114 'separatorTransformTable',
'fallback8bitEncoding',
'linkPrefixExtension',
115 'linkTrail',
'linkPrefixCharset',
'namespaceAliases',
116 'dateFormats',
'datePreferences',
'datePreferenceMigrationMap',
117 'defaultDateFormat',
'extraUserToggles',
'specialPageAliases',
118 'imageFiles',
'preloadedMessages',
'namespaceGenderAliases',
119 'digitGroupingPattern',
'pluralRules',
'pluralRuleTypes',
'compiledPluralRules',
127 'namespaceAliases',
'dateFormats',
'imageFiles',
'preloadedMessages'
198 if ( !empty(
$conf[
'storeClass'] ) ) {
199 $storeClass =
$conf[
'storeClass'];
201 switch (
$conf[
'store'] ) {
204 $storeClass =
'LCStoreCDB';
207 $storeClass =
'LCStoreDB';
210 $storeClass =
'LCStoreStaticArray';
213 if ( !empty(
$conf[
'storeDirectory'] ) ) {
214 $storeClass =
'LCStoreCDB';
217 $storeClass =
'LCStoreCDB';
219 $storeClass =
'LCStoreDB';
224 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
230 if ( !empty(
$conf[
'storeDirectory'] ) ) {
231 $storeConf[
'directory'] =
$conf[
'storeDirectory'];
234 $this->
store =
new $storeClass( $storeConf );
235 foreach ( [
'manualRecache',
'forceRecache' ]
as $var ) {
236 if ( isset(
$conf[$var] ) ) {
237 $this->$var =
$conf[$var];
249 if ( $this->mergeableKeys ===
null ) {
250 $this->mergeableKeys = array_flip( array_merge(
251 self::$mergeableMapKeys,
252 self::$mergeableListKeys,
253 self::$mergeableAliasListKeys,
254 self::$optionalMergeKeys,
259 return isset( $this->mergeableKeys[$key] );
272 if ( !isset( $this->loadedItems[
$code][$key] ) ) {
276 if ( $key ===
'fallback' && isset( $this->shallowFallbacks[
$code] ) ) {
277 return $this->shallowFallbacks[
$code];
291 if ( !isset( $this->loadedSubitems[
$code][$key][$subkey] ) &&
292 !isset( $this->loadedItems[
$code][$key] )
297 if ( isset( $this->
data[
$code][$key][$subkey] ) ) {
298 return $this->
data[
$code][$key][$subkey];
317 if ( in_array( $key, self::$splitKeys ) ) {
321 if ( is_array( $item ) ) {
322 return array_keys( $item );
335 if ( !isset( $this->initialisedLangs[
$code] ) ) {
340 if ( isset( $this->loadedItems[
$code][$key] ) ) {
344 if ( isset( $this->shallowFallbacks[
$code] ) ) {
350 if ( in_array( $key, self::$splitKeys ) ) {
351 $subkeyList = $this->
getSubitem( $code,
'list', $key );
352 foreach ( $subkeyList
as $subkey ) {
353 if ( isset( $this->
data[$code][$key][$subkey] ) ) {
362 $this->loadedItems[
$code][$key] =
true;
372 if ( !in_array( $key, self::$splitKeys ) ) {
378 if ( !isset( $this->initialisedLangs[
$code] ) ) {
383 if ( isset( $this->loadedItems[
$code][$key] ) ||
384 isset( $this->loadedSubitems[
$code][$key][$subkey] )
389 if ( isset( $this->shallowFallbacks[
$code] ) ) {
395 $value = $this->
store->get( $code,
"$key:$subkey" );
397 $this->loadedSubitems[
$code][$key][$subkey] =
true;
408 if ( $this->forceRecache && !isset( $this->recachedLangs[
$code] ) ) {
409 wfDebug( __METHOD__ .
"($code): forced reload\n" );
414 $deps = $this->
store->get( $code,
'deps' );
416 $preload = $this->
store->get( $code,
'preload' );
418 if ( $deps ===
null ||
$keys ===
null || $preload ===
null ) {
419 wfDebug( __METHOD__ .
"($code): cache missing, need to make one\n" );
424 foreach ( $deps
as $dep ) {
430 wfDebug( __METHOD__ .
"($code): cache for $code expired due to " .
431 get_class( $dep ) .
"\n" );
446 if ( isset( $this->initialisedLangs[
$code] ) ) {
450 $this->initialisedLangs[
$code] =
true;
452 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
459 # Recache the data if necessary
460 if ( !$this->manualRecache && $this->
isExpired( $code ) ) {
463 } elseif (
$code ===
'en' ) {
464 throw new MWException(
'MessagesEn.php is missing.' );
473 $preload = $this->
getItem( $code,
'preload' );
474 if ( $preload ===
null ) {
475 if ( $this->manualRecache ) {
477 if (
$code ===
'en' ) {
478 throw new MWException(
'No localisation cache found for English. ' .
479 'Please run maintenance/rebuildLocalisationCache.php.' );
485 throw new MWException(
'Invalid or missing localisation cache.' );
489 foreach ( $preload
as $key => $item ) {
490 if ( in_array( $key, self::$splitKeys ) ) {
491 foreach ( $item
as $subkey => $subitem ) {
492 $this->loadedSubitems[
$code][$key][$subkey] =
true;
495 $this->loadedItems[
$code][$key] =
true;
507 $this->
data[$primaryCode] =& $this->
data[$fallbackCode];
508 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
509 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
510 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
522 MediaWiki\suppressWarnings();
523 $_apcEnabled = ini_set(
'apc.cache_by_default',
'0' );
524 MediaWiki\restoreWarnings();
528 MediaWiki\suppressWarnings();
529 ini_set(
'apc.cache_by_default', $_apcEnabled );
530 MediaWiki\restoreWarnings();
532 if ( $_fileType ==
'core' || $_fileType ==
'extension' ) {
533 $data = compact( self::$allKeys );
534 } elseif ( $_fileType ==
'aliases' ) {
535 $data = compact(
'aliases' );
537 throw new MWException( __METHOD__ .
": Invalid file type: $_fileType" );
550 if ( !is_readable( $fileName ) ) {
554 $json = file_get_contents( $fileName );
555 if ( $json ===
false ) {
560 if (
$data ===
null ) {
561 throw new MWException( __METHOD__ .
": Invalid JSON file: $fileName" );
565 foreach (
$data as $key => $unused ) {
566 if ( $key ===
'' || $key[0] ===
'@' ) {
567 unset(
$data[$key] );
572 return [
'messages' =>
$data ];
583 if ( $rules ===
null ) {
587 $compiledRules = Evaluator::compile( $rules );
588 }
catch ( CLDRPluralRuleError
$e ) {
594 return $compiledRules;
605 if ( $this->pluralRules ===
null ) {
608 if ( !isset( $this->pluralRules[
$code] ) ) {
611 return $this->pluralRules[
$code];
623 if ( $this->pluralRuleTypes ===
null ) {
626 if ( !isset( $this->pluralRuleTypes[
$code] ) ) {
629 return $this->pluralRuleTypes[
$code];
638 $cldrPlural =
"$IP/languages/data/plurals.xml";
639 $mwPlural =
"$IP/languages/data/plurals-mediawiki.xml";
642 if ( file_exists( $mwPlural ) ) {
657 $xml = file_get_contents( $fileName );
659 throw new MWException(
"Unable to read plurals file $fileName" );
661 $doc =
new DOMDocument;
662 $doc->loadXML( $xml );
663 $rulesets = $doc->getElementsByTagName(
"pluralRules" );
664 foreach ( $rulesets
as $ruleset ) {
665 $codes = $ruleset->getAttribute(
'locales' );
668 $ruleElements = $ruleset->getElementsByTagName(
"pluralRule" );
669 foreach ( $ruleElements
as $elt ) {
670 $ruleType = $elt->getAttribute(
'count' );
671 if ( $ruleType ===
'other' ) {
675 $rules[] = $elt->nodeValue;
676 $ruleTypes[] = $ruleType;
678 foreach ( explode(
' ', $codes )
as $code ) {
679 $this->pluralRules[
$code] = $rules;
680 $this->pluralRuleTypes[
$code] = $ruleTypes;
699 if ( !file_exists( $fileName ) ) {
706 # Load CLDR plural rules for JavaScript
710 # Load plural rule types
713 $deps[
'plurals'] =
new FileDependency(
"$IP/languages/data/plurals.xml" );
714 $deps[
'plurals-mw'] =
new FileDependency(
"$IP/languages/data/plurals-mediawiki.xml" );
727 if ( !is_null(
$value ) ) {
728 if ( !is_null( $fallbackValue ) ) {
729 if ( in_array( $key, self::$mergeableMapKeys ) ) {
731 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
732 $value = array_unique( array_merge( $fallbackValue,
$value ) );
733 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
734 $value = array_merge_recursive(
$value, $fallbackValue );
735 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
736 if ( !empty(
$value[
'inherit'] ) ) {
740 if ( isset(
$value[
'inherit'] ) ) {
741 unset(
$value[
'inherit'] );
743 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
757 foreach ( $fallbackValue
as $magicName => $fallbackInfo ) {
758 if ( !isset(
$value[$magicName] ) ) {
759 $value[$magicName] = $fallbackInfo;
761 $oldSynonyms = array_slice( $fallbackInfo, 1 );
762 $newSynonyms = array_slice(
$value[$magicName], 1 );
763 $synonyms = array_values( array_unique( array_merge(
764 $newSynonyms, $oldSynonyms ) ) );
765 $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
785 foreach ( $codeSequence
as $code ) {
786 if ( isset( $fallbackValue[
$code] ) ) {
805 $config = MediaWikiServices::getInstance()->getMainConfig();
806 $messagesDirs = $config->get(
'MessagesDirs' );
808 'core' =>
"$IP/languages/i18n",
809 'api' =>
"$IP/includes/api/i18n",
810 'oojs-ui' =>
"$IP/resources/lib/oojs-ui/i18n",
824 throw new MWException(
"Invalid language code requested" );
826 $this->recachedLangs[
$code] =
true;
829 $initialData = array_fill_keys( self::$allKeys,
null );
830 $coreData = $initialData;
833 # Load the primary localisation from the source file
835 if (
$data ===
false ) {
836 wfDebug( __METHOD__ .
": no localisation file for $code, using fallback to en\n" );
837 $coreData[
'fallback'] =
'en';
839 wfDebug( __METHOD__ .
": got localisation for $code from source\n" );
841 # Merge primary localisation
847 # Fill in the fallback if it's not there already
848 if ( is_null( $coreData[
'fallback'] ) ) {
849 $coreData[
'fallback'] =
$code ===
'en' ?
false :
'en';
851 if ( $coreData[
'fallback'] ===
false ) {
852 $coreData[
'fallbackSequence'] = [];
854 $coreData[
'fallbackSequence'] = array_map(
'trim', explode(
',', $coreData[
'fallback'] ) );
855 $len =
count( $coreData[
'fallbackSequence'] );
857 # Ensure that the sequence ends at en
858 if ( $coreData[
'fallbackSequence'][$len - 1] !==
'en' ) {
859 $coreData[
'fallbackSequence'][] =
'en';
863 $codeSequence = array_merge( [
$code ], $coreData[
'fallbackSequence'] );
866 # Load non-JSON localisation data for extensions
867 $extensionData = array_fill_keys( $codeSequence, $initialData );
869 if ( isset( $messageDirs[$extension] ) ) {
870 # This extension has JSON message data; skip the PHP shim
877 foreach (
$data as $key => $item ) {
878 foreach ( $codeSequence
as $csCode ) {
879 if ( isset( $item[$csCode] ) ) {
880 $this->
mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
891 # Load the localisation data for each fallback, then merge it into the full array
892 $allData = $initialData;
893 foreach ( $codeSequence
as $csCode ) {
894 $csData = $initialData;
896 # Load core messages and the extension localisations.
897 foreach ( $messageDirs
as $dirs ) {
899 $fileName =
"$dir/$csCode.json";
902 foreach (
$data as $key => $item ) {
903 $this->
mergeItem( $key, $csData[$key], $item );
910 # Merge non-JSON extension data
911 if ( isset( $extensionData[$csCode] ) ) {
912 foreach ( $extensionData[$csCode]
as $key => $item ) {
913 $this->
mergeItem( $key, $csData[$key], $item );
917 if ( $csCode ===
$code ) {
918 # Merge core data into extension data
919 foreach ( $coreData
as $key => $item ) {
920 $this->
mergeItem( $key, $csData[$key], $item );
923 # Load the secondary localisation from the source file to
924 # avoid infinite cycles on cyclic fallbacks
926 if ( $fbData !==
false ) {
927 # Only merge the keys that make sense to merge
928 foreach ( self::$allKeys
as $key ) {
929 if ( !isset( $fbData[$key] ) ) {
933 if ( is_null( $coreData[$key] ) || $this->
isMergeableKey( $key ) ) {
934 $this->
mergeItem( $key, $csData[$key], $fbData[$key] );
940 # Allow extensions an opportunity to adjust the data for this
942 Hooks::run(
'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] );
944 # Merge the data for this fallback into the final array
945 if ( $csCode ===
$code ) {
948 foreach ( self::$allKeys
as $key ) {
949 if ( !isset( $csData[$key] ) ) {
953 if ( is_null( $allData[$key] ) || $this->
isMergeableKey( $key ) ) {
954 $this->
mergeItem( $key, $allData[$key], $csData[$key] );
960 # Add cache dependencies for any referenced globals
961 $deps[
'wgExtensionMessagesFiles'] =
new GlobalDependency(
'wgExtensionMessagesFiles' );
967 # Add dependencies to the cache entry
968 $allData[
'deps'] = $deps;
970 # Replace spaces with underscores in namespace names
971 $allData[
'namespaceNames'] = str_replace(
' ',
'_', $allData[
'namespaceNames'] );
973 # And do the same for special page aliases. $page is an array.
974 foreach ( $allData[
'specialPageAliases']
as &
$page ) {
977 # Decouple the reference to prevent accidental damage
980 # If there were no plural rules, return an empty array
981 if ( $allData[
'pluralRules'] ===
null ) {
982 $allData[
'pluralRules'] = [];
984 if ( $allData[
'compiledPluralRules'] ===
null ) {
985 $allData[
'compiledPluralRules'] = [];
987 # If there were no plural rule types, return an empty array
988 if ( $allData[
'pluralRuleTypes'] ===
null ) {
989 $allData[
'pluralRuleTypes'] = [];
993 $allData[
'list'] = [];
994 foreach ( self::$splitKeys
as $key ) {
995 $allData[
'list'][$key] = array_keys( $allData[$key] );
999 Hooks::run(
'LocalisationCacheRecache', [ $this,
$code, &$allData, &$purgeBlobs ] );
1001 if ( is_null( $allData[
'namespaceNames'] ) ) {
1002 throw new MWException( __METHOD__ .
': Localisation data failed sanity check! ' .
1003 'Check that your languages/messages/MessagesEn.php file is intact.' );
1006 # Set the preload key
1009 # Save to the process cache and register the items loaded
1011 foreach ( $allData
as $key => $item ) {
1012 $this->loadedItems[
$code][$key] =
true;
1015 # Save to the persistent cache
1017 foreach ( $allData
as $key =>
$value ) {
1018 if ( in_array( $key, self::$splitKeys ) ) {
1019 foreach (
$value as $subkey => $subvalue ) {
1020 $this->
store->set(
"$key:$subkey", $subvalue );
1026 $this->
store->finishWrite();
1028 # Clear out the MessageBlobStore
1029 # HACK: If using a null (i.e. disabled) storage backend, we
1030 # can't write to the MessageBlobStore either
1033 $blobStore->clear();
1046 $preload = [
'messages' => [] ];
1047 foreach ( self::$preloadedKeys
as $key ) {
1048 $preload[$key] =
$data[$key];
1051 foreach (
$data[
'preloadedMessages']
as $subkey ) {
1052 if ( isset(
$data[
'messages'][$subkey] ) ) {
1053 $subitem =
$data[
'messages'][$subkey];
1057 $preload[
'messages'][$subkey] = $subitem;
1070 unset( $this->loadedItems[
$code] );
1071 unset( $this->loadedSubitems[
$code] );
1072 unset( $this->initialisedLangs[
$code] );
1073 unset( $this->shallowFallbacks[
$code] );
1075 foreach ( $this->shallowFallbacks
as $shallowCode => $fbCode ) {
1076 if ( $fbCode ===
$code ) {
1077 $this->
unload( $shallowCode );
1086 foreach ( $this->initialisedLangs
as $lang => $unused ) {
1096 $this->manualRecache =
false;