MediaWiki REL1_34
LocalisationCache.php
Go to the documentation of this file.
1<?php
23use CLDRPluralRuleParser\Evaluator;
24use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
27use Psr\Log\LoggerInterface;
28
42 const VERSION = 4;
43
45 private $options;
46
52 private $manualRecache = false;
53
60 protected $data = [];
61
67 private $store;
68
72 private $logger;
73
76
79
88 private $loadedItems = [];
89
94 private $loadedSubitems = [];
95
101 private $initialisedLangs = [];
102
108 private $shallowFallbacks = [];
109
113 private $recachedLangs = [];
114
118 public static $allKeys = [
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',
129 ];
130
135 public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
136 'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
137 ];
138
142 public static $mergeableListKeys = [];
143
148 public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
149
155 public static $optionalMergeKeys = [ 'bookstoreList' ];
156
160 public static $magicWordKeys = [ 'magicWords' ];
161
165 public static $splitKeys = [ 'messages' ];
166
170 public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
171
176 private $pluralRules = null;
177
190 private $pluralRuleTypes = null;
191
192 private $mergeableKeys = null;
193
202 public static function getStoreFromConf( array $conf, $fallbackCacheDir ) : LCStore {
203 $storeArg = [];
204 $storeArg['directory'] =
205 $conf['storeDirectory'] ?: $fallbackCacheDir;
206
207 if ( !empty( $conf['storeClass'] ) ) {
208 $storeClass = $conf['storeClass'];
209 } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
210 ( $conf['store'] === 'detect' && $storeArg['directory'] )
211 ) {
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;
218 } else {
219 throw new MWException(
220 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
221 );
222 }
223
224 return new $storeClass( $storeArg );
225 }
226
231 public const CONSTRUCTOR_OPTIONS = [
232 // True to treat all files as expired until they are regenerated by this object.
233 'forceRecache',
234 'manualRecache',
235 'ExtensionMessagesFiles',
236 'MessagesDirs',
237 ];
238
254 function __construct(
255 ServiceOptions $options,
256 LCStore $store,
257 LoggerInterface $logger,
258 array $clearStoreCallbacks,
259 LanguageNameUtils $langNameUtils
260 ) {
261 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
262
263 $this->options = $options;
264 $this->store = $store;
265 $this->logger = $logger;
266 $this->clearStoreCallbacks = $clearStoreCallbacks;
267 $this->langNameUtils = $langNameUtils;
268
269 // Keep this separate from $this->options so it can be mutable
270 $this->manualRecache = $options->get( 'manualRecache' );
271 }
272
279 public function isMergeableKey( $key ) {
280 if ( $this->mergeableKeys === null ) {
281 $this->mergeableKeys = array_flip( array_merge(
282 self::$mergeableMapKeys,
283 self::$mergeableListKeys,
284 self::$mergeableAliasListKeys,
285 self::$optionalMergeKeys,
286 self::$magicWordKeys
287 ) );
288 }
289
290 return isset( $this->mergeableKeys[$key] );
291 }
292
302 public function getItem( $code, $key ) {
303 if ( !isset( $this->loadedItems[$code][$key] ) ) {
304 $this->loadItem( $code, $key );
305 }
306
307 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
308 return $this->shallowFallbacks[$code];
309 }
310
311 return $this->data[$code][$key];
312 }
313
321 public function getSubitem( $code, $key, $subkey ) {
322 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
323 !isset( $this->loadedItems[$code][$key] )
324 ) {
325 $this->loadSubitem( $code, $key, $subkey );
326 }
327
328 return $this->data[$code][$key][$subkey] ?? null;
329 }
330
343 public function getSubitemList( $code, $key ) {
344 if ( in_array( $key, self::$splitKeys ) ) {
345 return $this->getSubitem( $code, 'list', $key );
346 } else {
347 $item = $this->getItem( $code, $key );
348 if ( is_array( $item ) ) {
349 return array_keys( $item );
350 } else {
351 return false;
352 }
353 }
354 }
355
361 protected function loadItem( $code, $key ) {
362 if ( !isset( $this->initialisedLangs[$code] ) ) {
363 $this->initLanguage( $code );
364 }
365
366 // Check to see if initLanguage() loaded it for us
367 if ( isset( $this->loadedItems[$code][$key] ) ) {
368 return;
369 }
370
371 if ( isset( $this->shallowFallbacks[$code] ) ) {
372 $this->loadItem( $this->shallowFallbacks[$code], $key );
373
374 return;
375 }
376
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] ) ) {
381 continue;
382 }
383 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
384 }
385 } else {
386 $this->data[$code][$key] = $this->store->get( $code, $key );
387 }
388
389 $this->loadedItems[$code][$key] = true;
390 }
391
398 protected function loadSubitem( $code, $key, $subkey ) {
399 if ( !in_array( $key, self::$splitKeys ) ) {
400 $this->loadItem( $code, $key );
401
402 return;
403 }
404
405 if ( !isset( $this->initialisedLangs[$code] ) ) {
406 $this->initLanguage( $code );
407 }
408
409 // Check to see if initLanguage() loaded it for us
410 if ( isset( $this->loadedItems[$code][$key] ) ||
411 isset( $this->loadedSubitems[$code][$key][$subkey] )
412 ) {
413 return;
414 }
415
416 if ( isset( $this->shallowFallbacks[$code] ) ) {
417 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
418
419 return;
420 }
421
422 $value = $this->store->get( $code, "$key:$subkey" );
423 $this->data[$code][$key][$subkey] = $value;
424 $this->loadedSubitems[$code][$key][$subkey] = true;
425 }
426
434 public function isExpired( $code ) {
435 if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
436 $this->logger->debug( __METHOD__ . "($code): forced reload" );
437
438 return true;
439 }
440
441 $deps = $this->store->get( $code, 'deps' );
442 $keys = $this->store->get( $code, 'list' );
443 $preload = $this->store->get( $code, 'preload' );
444 // Different keys may expire separately for some stores
445 if ( $deps === null || $keys === null || $preload === null ) {
446 $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one" );
447
448 return true;
449 }
450
451 foreach ( $deps as $dep ) {
452 // Because we're unserializing stuff from cache, we
453 // could receive objects of classes that don't exist
454 // anymore (e.g. uninstalled extensions)
455 // When this happens, always expire the cache
456 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
457 $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
458 get_class( $dep ) );
459
460 return true;
461 }
462 }
463
464 return false;
465 }
466
472 protected function initLanguage( $code ) {
473 if ( isset( $this->initialisedLangs[$code] ) ) {
474 return;
475 }
476
477 $this->initialisedLangs[$code] = true;
478
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' );
482
483 return;
484 }
485
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.' );
492 } else {
493 $this->initShallowFallback( $code, 'en' );
494 }
495
496 return;
497 }
498
499 # Preload some stuff
500 $preload = $this->getItem( $code, 'preload' );
501 if ( $preload === null ) {
502 if ( $this->manualRecache ) {
503 // No Messages*.php file. Do shallow fallback to en.
504 if ( $code === 'en' ) {
505 throw new MWException( 'No localisation cache found for English. ' .
506 'Please run maintenance/rebuildLocalisationCache.php.' );
507 }
508 $this->initShallowFallback( $code, 'en' );
509
510 return;
511 } else {
512 throw new MWException( 'Invalid or missing localisation cache.' );
513 }
514 }
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;
520 }
521 } else {
522 $this->loadedItems[$code][$key] = true;
523 }
524 }
525 }
526
533 public function initShallowFallback( $primaryCode, $fallbackCode ) {
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;
538 }
539
547 protected function readPHPFile( $_fileName, $_fileType ) {
548 include $_fileName;
549
550 $data = [];
551 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
552 foreach ( self::$allKeys as $key ) {
553 // Not all keys are set in language files, so
554 // check they exist first
555 if ( isset( $$key ) ) {
556 $data[$key] = $$key;
557 }
558 }
559 } elseif ( $_fileType == 'aliases' ) {
560 if ( isset( $aliases ) ) {
561 $data['aliases'] = $aliases;
562 }
563 } else {
564 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
565 }
566
567 return $data;
568 }
569
576 public function readJSONFile( $fileName ) {
577 if ( !is_readable( $fileName ) ) {
578 return [];
579 }
580
581 $json = file_get_contents( $fileName );
582 if ( $json === false ) {
583 return [];
584 }
585
586 $data = FormatJson::decode( $json, true );
587 if ( $data === null ) {
588 throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
589 }
590
591 // Remove keys starting with '@', they're reserved for metadata and non-message data
592 foreach ( $data as $key => $unused ) {
593 if ( $key === '' || $key[0] === '@' ) {
594 unset( $data[$key] );
595 }
596 }
597
598 // The JSON format only supports messages, none of the other variables, so wrap the data
599 return [ 'messages' => $data ];
600 }
601
608 public function getCompiledPluralRules( $code ) {
609 $rules = $this->getPluralRules( $code );
610 if ( $rules === null ) {
611 return null;
612 }
613 try {
614 $compiledRules = Evaluator::compile( $rules );
615 } catch ( CLDRPluralRuleError $e ) {
616 $this->logger->debug( $e->getMessage() );
617
618 return [];
619 }
620
621 return $compiledRules;
622 }
623
631 public function getPluralRules( $code ) {
632 if ( $this->pluralRules === null ) {
633 $this->loadPluralFiles();
634 }
635 return $this->pluralRules[$code] ?? null;
636 }
637
645 public function getPluralRuleTypes( $code ) {
646 if ( $this->pluralRuleTypes === null ) {
647 $this->loadPluralFiles();
648 }
649 return $this->pluralRuleTypes[$code] ?? null;
650 }
651
655 protected function loadPluralFiles() {
656 global $IP;
657 $cldrPlural = "$IP/languages/data/plurals.xml";
658 $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
659 // Load CLDR plural rules
660 $this->loadPluralFile( $cldrPlural );
661 if ( file_exists( $mwPlural ) ) {
662 // Override or extend
663 $this->loadPluralFile( $mwPlural );
664 }
665 }
666
674 protected function loadPluralFile( $fileName ) {
675 // Use file_get_contents instead of DOMDocument::load (T58439)
676 $xml = file_get_contents( $fileName );
677 if ( !$xml ) {
678 throw new MWException( "Unable to read plurals file $fileName" );
679 }
680 $doc = new DOMDocument;
681 $doc->loadXML( $xml );
682 $rulesets = $doc->getElementsByTagName( "pluralRules" );
683 foreach ( $rulesets as $ruleset ) {
684 $codes = $ruleset->getAttribute( 'locales' );
685 $rules = [];
686 $ruleTypes = [];
687 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
688 foreach ( $ruleElements as $elt ) {
689 $ruleType = $elt->getAttribute( 'count' );
690 if ( $ruleType === 'other' ) {
691 // Don't record "other" rules, which have an empty condition
692 continue;
693 }
694 $rules[] = $elt->nodeValue;
695 $ruleTypes[] = $ruleType;
696 }
697 foreach ( explode( ' ', $codes ) as $code ) {
698 $this->pluralRules[$code] = $rules;
699 $this->pluralRuleTypes[$code] = $ruleTypes;
700 }
701 }
702 }
703
713 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
714 global $IP;
715
716 // This reads in the PHP i18n file with non-messages l10n data
717 $fileName = $this->langNameUtils->getMessagesFileName( $code );
718 if ( !file_exists( $fileName ) ) {
719 $data = [];
720 } else {
721 $deps[] = new FileDependency( $fileName );
722 $data = $this->readPHPFile( $fileName, 'core' );
723 }
724
725 # Load CLDR plural rules for JavaScript
726 $data['pluralRules'] = $this->getPluralRules( $code );
727 # And for PHP
728 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
729 # Load plural rule types
730 $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
731
732 $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
733 $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
734
735 return $data;
736 }
737
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 ) ) {
751 // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
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 );
758 }
759
760 if ( isset( $value['inherit'] ) ) {
761 unset( $value['inherit'] );
762 }
763 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
764 $this->mergeMagicWords( $value, $fallbackValue );
765 }
766 }
767 } else {
768 $value = $fallbackValue;
769 }
770 }
771
776 protected function mergeMagicWords( &$value, $fallbackValue ) {
777 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
778 if ( !isset( $value[$magicName] ) ) {
779 $value[$magicName] = $fallbackInfo;
780 } else {
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 );
786 }
787 }
788 }
789
803 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
804 $used = false;
805 foreach ( $codeSequence as $code ) {
806 if ( isset( $fallbackValue[$code] ) ) {
807 $this->mergeItem( $key, $value, $fallbackValue[$code] );
808 $used = true;
809 }
810 }
811
812 return $used;
813 }
814
822 public function getMessagesDirs() {
823 global $IP;
824
825 return [
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' );
831 }
832
839 public function recache( $code ) {
840 if ( !$code ) {
841 throw new MWException( "Invalid language code requested" );
842 }
843 $this->recachedLangs[ $code ] = true;
844
845 # Initial values
846 $initialData = array_fill_keys( self::$allKeys, null );
847 $coreData = $initialData;
848 $deps = [];
849
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" );
853
854 # Merge primary localisation
855 foreach ( $data as $key => $value ) {
856 $this->mergeItem( $key, $coreData[ $key ], $value );
857 }
858
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'] = [];
863 } else {
864 if ( !is_null( $coreData['fallback'] ) ) {
865 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
866 } else {
867 $coreData['fallbackSequence'] = [];
868 }
869 $len = count( $coreData['fallbackSequence'] );
870
871 # Before we add the 'en' fallback for messages, keep a copy of
872 # the original fallback sequence
873 $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
874
875 # Ensure that the sequence ends at 'en' for messages
876 if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
877 $coreData['fallbackSequence'][] = 'en';
878 }
879 }
880
881 $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
882 $messageDirs = $this->getMessagesDirs();
883
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
889 continue;
890 }
891
892 $data = $this->readPHPFile( $fileName, 'extension' );
893 $used = false;
894
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] );
899 $used = true;
900 }
901 }
902 }
903
904 if ( $used ) {
905 $deps[] = new FileDependency( $fileName );
906 }
907 }
908
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;
913
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 );
919
920 foreach ( $data as $key => $item ) {
921 $this->mergeItem( $key, $csData[$key], $item );
922 }
923
924 $deps[] = new FileDependency( $fileName );
925 }
926 }
927
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 );
932 }
933 }
934
935 if ( $csCode === $code ) {
936 # Merge core data into extension data
937 foreach ( $coreData as $key => $item ) {
938 $this->mergeItem( $key, $csData[$key], $item );
939 }
940 } else {
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 ] ) ) {
947 continue;
948 }
949
950 if ( is_null( $coreData[ $key ] ) || $this->isMergeableKey( $key ) ) {
951 $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
952 }
953 }
954 }
955
956 # Allow extensions an opportunity to adjust the data for this
957 # fallback
958 Hooks::run( 'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] );
959
960 # Merge the data for this fallback into the final array
961 if ( $csCode === $code ) {
962 $allData = $csData;
963 } else {
964 foreach ( self::$allKeys as $key ) {
965 if ( !isset( $csData[$key] ) ) {
966 continue;
967 }
968
969 if ( is_null( $allData[$key] ) || $this->isMergeableKey( $key ) ) {
970 $this->mergeItem( $key, $allData[$key], $csData[$key] );
971 }
972 }
973 }
974 }
975
976 # Add cache dependencies for any referenced globals
977 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
978 // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
979 // We use the key 'wgMessagesDirs' for historical reasons.
980 $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' );
981 $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
982
983 # Add dependencies to the cache entry
984 $allData['deps'] = $deps;
985
986 # Replace spaces with underscores in namespace names
987 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
988
989 # And do the same for special page aliases. $page is an array.
990 foreach ( $allData['specialPageAliases'] as &$page ) {
991 $page = str_replace( ' ', '_', $page );
992 }
993 # Decouple the reference to prevent accidental damage
994 unset( $page );
995
996 # If there were no plural rules, return an empty array
997 if ( $allData['pluralRules'] === null ) {
998 $allData['pluralRules'] = [];
999 }
1000 if ( $allData['compiledPluralRules'] === null ) {
1001 $allData['compiledPluralRules'] = [];
1002 }
1003 # If there were no plural rule types, return an empty array
1004 if ( $allData['pluralRuleTypes'] === null ) {
1005 $allData['pluralRuleTypes'] = [];
1006 }
1007
1008 # Set the list keys
1009 $allData['list'] = [];
1010 foreach ( self::$splitKeys as $key ) {
1011 $allData['list'][$key] = array_keys( $allData[$key] );
1012 }
1013 # Run hooks
1014 $unused = true; // Used to be $purgeBlobs, removed in 1.34
1015 Hooks::run( 'LocalisationCacheRecache', [ $this, $code, &$allData, &$unused ] );
1016
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.' );
1020 }
1021
1022 # Set the preload key
1023 $allData['preload'] = $this->buildPreload( $allData );
1024
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;
1029 }
1030
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 );
1037 }
1038 } else {
1039 $this->store->set( $key, $value );
1040 }
1041 }
1042 $this->store->finishWrite();
1043
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
1047 if ( !$this->store instanceof LCStoreNull ) {
1048 foreach ( $this->clearStoreCallbacks as $callback ) {
1049 $callback();
1050 }
1051 }
1052 }
1053
1062 protected function buildPreload( $data ) {
1063 $preload = [ 'messages' => [] ];
1064 foreach ( self::$preloadedKeys as $key ) {
1065 $preload[$key] = $data[$key];
1066 }
1067
1068 foreach ( $data['preloadedMessages'] as $subkey ) {
1069 $subitem = $data['messages'][$subkey] ?? null;
1070 $preload['messages'][$subkey] = $subitem;
1071 }
1072
1073 return $preload;
1074 }
1075
1081 public function unload( $code ) {
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] );
1087
1088 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1089 if ( $fbCode === $code ) {
1090 $this->unload( $shallowCode );
1091 }
1092 }
1093 }
1094
1098 public function unloadAll() {
1099 foreach ( $this->initialisedLangs as $lang => $unused ) {
1100 $this->unload( $lang );
1101 }
1102 }
1103
1107 public function disableBackend() {
1108 $this->store = new LCStoreNull;
1109 $this->manualRecache = false;
1110 }
1111}
$IP
Definition WebStart.php:41
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.
ServiceOptions $options
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
LoggerInterface $logger
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 *....
MediaWiki exception.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
A service that provides utilities to do with language names and codes.
Interface for the persistence layer of LocalisationCache.
Definition LCStore.php:38
if(!isset( $args[0])) $lang