MediaWiki REL1_37
LocalisationCache.php
Go to the documentation of this file.
1<?php
23use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
24use CLDRPluralRuleParser\Evaluator;
29use Psr\Log\LoggerInterface;
30
44 public const VERSION = 4;
45
47 private $options;
48
55
62 protected $data = [];
63
69 private $store;
70
74 private $logger;
75
77 private $hookRunner;
78
81
84
93 private $loadedItems = [];
94
99 private $loadedSubitems = [];
100
106 private $initialisedLangs = [];
107
113 private $shallowFallbacks = [];
114
118 private $recachedLangs = [];
119
123 public static $allKeys = [
124 'fallback', 'namespaceNames', 'bookstoreList',
125 'magicWords', 'messages', 'rtl',
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',
134 ];
135
140 public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
141 'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
142 ];
143
147 public static $mergeableListKeys = [];
148
153 public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
154
160 public static $optionalMergeKeys = [ 'bookstoreList' ];
161
165 public static $magicWordKeys = [ 'magicWords' ];
166
170 public static $splitKeys = [ 'messages' ];
171
175 public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
176
181 private $pluralRules = null;
182
195 private $pluralRuleTypes = null;
196
197 private $mergeableKeys = null;
198
207 public static function getStoreFromConf( array $conf, $fallbackCacheDir ): LCStore {
208 $storeArg = [];
209 $storeArg['directory'] =
210 $conf['storeDirectory'] ?: $fallbackCacheDir;
211
212 if ( !empty( $conf['storeClass'] ) ) {
213 $storeClass = $conf['storeClass'];
214 } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
215 ( $conf['store'] === 'detect' && $storeArg['directory'] )
216 ) {
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;
223 } else {
224 throw new MWException(
225 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
226 );
227 }
228
229 return new $storeClass( $storeArg );
230 }
231
235 public const CONSTRUCTOR_OPTIONS = [
236 // True to treat all files as expired until they are regenerated by this object.
237 'forceRecache',
238 'manualRecache',
239 'ExtensionMessagesFiles',
240 'MessagesDirs',
241 ];
242
259 public function __construct(
260 ServiceOptions $options,
261 LCStore $store,
262 LoggerInterface $logger,
263 array $clearStoreCallbacks,
264 LanguageNameUtils $langNameUtils,
265 HookContainer $hookContainer
266 ) {
267 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
268
269 $this->options = $options;
270 $this->store = $store;
271 $this->logger = $logger;
272 $this->clearStoreCallbacks = $clearStoreCallbacks;
273 $this->langNameUtils = $langNameUtils;
274 $this->hookRunner = new HookRunner( $hookContainer );
275
276 // Keep this separate from $this->options so it can be mutable
277 $this->manualRecache = $options->get( 'manualRecache' );
278 }
279
286 public function isMergeableKey( $key ) {
287 if ( $this->mergeableKeys === null ) {
288 $this->mergeableKeys = array_fill_keys( array_merge(
289 self::$mergeableMapKeys,
290 self::$mergeableListKeys,
291 self::$mergeableAliasListKeys,
292 self::$optionalMergeKeys,
293 self::$magicWordKeys
294 ), true );
295 }
296
297 return isset( $this->mergeableKeys[$key] );
298 }
299
309 public function getItem( $code, $key ) {
310 if ( !isset( $this->loadedItems[$code][$key] ) ) {
311 $this->loadItem( $code, $key );
312 }
313
314 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
315 return $this->shallowFallbacks[$code];
316 }
317
318 return $this->data[$code][$key];
319 }
320
328 public function getSubitem( $code, $key, $subkey ) {
329 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
330 !isset( $this->loadedItems[$code][$key] )
331 ) {
332 $this->loadSubitem( $code, $key, $subkey );
333 }
334
335 return $this->data[$code][$key][$subkey] ?? null;
336 }
337
350 public function getSubitemList( $code, $key ) {
351 if ( in_array( $key, self::$splitKeys ) ) {
352 return $this->getSubitem( $code, 'list', $key );
353 } else {
354 $item = $this->getItem( $code, $key );
355 if ( is_array( $item ) ) {
356 return array_keys( $item );
357 } else {
358 return false;
359 }
360 }
361 }
362
368 protected function loadItem( $code, $key ) {
369 if ( !isset( $this->initialisedLangs[$code] ) ) {
370 $this->initLanguage( $code );
371 }
372
373 // Check to see if initLanguage() loaded it for us
374 if ( isset( $this->loadedItems[$code][$key] ) ) {
375 return;
376 }
377
378 if ( isset( $this->shallowFallbacks[$code] ) ) {
379 $this->loadItem( $this->shallowFallbacks[$code], $key );
380
381 return;
382 }
383
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] ) ) {
388 continue;
389 }
390 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
391 }
392 } else {
393 $this->data[$code][$key] = $this->store->get( $code, $key );
394 }
395
396 $this->loadedItems[$code][$key] = true;
397 }
398
405 protected function loadSubitem( $code, $key, $subkey ) {
406 if ( !in_array( $key, self::$splitKeys ) ) {
407 $this->loadItem( $code, $key );
408
409 return;
410 }
411
412 if ( !isset( $this->initialisedLangs[$code] ) ) {
413 $this->initLanguage( $code );
414 }
415
416 // Check to see if initLanguage() loaded it for us
417 if ( isset( $this->loadedItems[$code][$key] ) ||
418 isset( $this->loadedSubitems[$code][$key][$subkey] )
419 ) {
420 return;
421 }
422
423 if ( isset( $this->shallowFallbacks[$code] ) ) {
424 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
425
426 return;
427 }
428
429 $value = $this->store->get( $code, "$key:$subkey" );
430 $this->data[$code][$key][$subkey] = $value;
431 $this->loadedSubitems[$code][$key][$subkey] = true;
432 }
433
441 public function isExpired( $code ) {
442 if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
443 $this->logger->debug( __METHOD__ . "($code): forced reload" );
444
445 return true;
446 }
447
448 $deps = $this->store->get( $code, 'deps' );
449 $keys = $this->store->get( $code, 'list' );
450 $preload = $this->store->get( $code, 'preload' );
451 // Different keys may expire separately for some stores
452 if ( $deps === null || $keys === null || $preload === null ) {
453 $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one" );
454
455 return true;
456 }
457
458 foreach ( $deps as $dep ) {
459 // Because we're unserializing stuff from cache, we
460 // could receive objects of classes that don't exist
461 // anymore (e.g. uninstalled extensions)
462 // When this happens, always expire the cache
463 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
464 $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
465 get_class( $dep ) );
466
467 return true;
468 }
469 }
470
471 return false;
472 }
473
479 protected function initLanguage( $code ) {
480 if ( isset( $this->initialisedLangs[$code] ) ) {
481 return;
482 }
483
484 $this->initialisedLangs[$code] = true;
485
486 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
487 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
488 $this->initShallowFallback( $code, 'en' );
489
490 return;
491 }
492
493 # Recache the data if necessary
494 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
495 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
496 $this->recache( $code );
497 } elseif ( $code === 'en' ) {
498 throw new MWException( 'MessagesEn.php is missing.' );
499 } else {
500 $this->initShallowFallback( $code, 'en' );
501 }
502
503 return;
504 }
505
506 # Preload some stuff
507 $preload = $this->getItem( $code, 'preload' );
508 if ( $preload === null ) {
509 if ( $this->manualRecache ) {
510 // No Messages*.php file. Do shallow fallback to en.
511 if ( $code === 'en' ) {
512 throw new MWException( 'No localisation cache found for English. ' .
513 'Please run maintenance/rebuildLocalisationCache.php.' );
514 }
515 $this->initShallowFallback( $code, 'en' );
516
517 return;
518 } else {
519 throw new MWException( 'Invalid or missing localisation cache.' );
520 }
521 }
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;
527 }
528 } else {
529 $this->loadedItems[$code][$key] = true;
530 }
531 }
532 }
533
540 public function initShallowFallback( $primaryCode, $fallbackCode ) {
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;
545 }
546
554 protected function readPHPFile( $_fileName, $_fileType ) {
555 include $_fileName;
556
557 $data = [];
558 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
559 foreach ( self::$allKeys as $key ) {
560 // Not all keys are set in language files, so
561 // check they exist first
562 if ( isset( $$key ) ) {
563 $data[$key] = $$key;
564 }
565 }
566 } elseif ( $_fileType == 'aliases' ) {
567 // @phan-suppress-next-line PhanImpossibleCondition May be set in included file
568 if ( isset( $aliases ) ) {
569 $data['aliases'] = $aliases;
570 }
571 } else {
572 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
573 }
574
575 return $data;
576 }
577
584 public function readJSONFile( $fileName ) {
585 if ( !is_readable( $fileName ) ) {
586 return [];
587 }
588
589 $json = file_get_contents( $fileName );
590 if ( $json === false ) {
591 return [];
592 }
593
594 $data = FormatJson::decode( $json, true );
595 if ( $data === null ) {
596 throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
597 }
598
599 // Remove keys starting with '@', they're reserved for metadata and non-message data
600 foreach ( $data as $key => $unused ) {
601 if ( $key === '' || $key[0] === '@' ) {
602 unset( $data[$key] );
603 }
604 }
605
606 // The JSON format only supports messages, none of the other variables, so wrap the data
607 return [ 'messages' => $data ];
608 }
609
616 public function getCompiledPluralRules( $code ) {
617 $rules = $this->getPluralRules( $code );
618 if ( $rules === null ) {
619 return null;
620 }
621 try {
622 $compiledRules = Evaluator::compile( $rules );
623 } catch ( CLDRPluralRuleError $e ) {
624 $this->logger->debug( $e->getMessage() );
625
626 return [];
627 }
628
629 return $compiledRules;
630 }
631
639 public function getPluralRules( $code ) {
640 if ( $this->pluralRules === null ) {
641 $this->loadPluralFiles();
642 }
643 return $this->pluralRules[$code] ?? null;
644 }
645
653 public function getPluralRuleTypes( $code ) {
654 if ( $this->pluralRuleTypes === null ) {
655 $this->loadPluralFiles();
656 }
657 return $this->pluralRuleTypes[$code] ?? null;
658 }
659
663 protected function loadPluralFiles() {
664 foreach ( $this->getPluralFiles() as $fileName ) {
665 $this->loadPluralFile( $fileName );
666 }
667 }
668
669 private function getPluralFiles(): array {
670 global $IP;
671 return [
672 // Load CLDR plural rules
673 "$IP/languages/data/plurals.xml",
674 // Override or extend with MW-specific rules
675 "$IP/languages/data/plurals-mediawiki.xml",
676 ];
677 }
678
686 protected function loadPluralFile( $fileName ) {
687 // Use file_get_contents instead of DOMDocument::load (T58439)
688 $xml = file_get_contents( $fileName );
689 if ( !$xml ) {
690 throw new MWException( "Unable to read plurals file $fileName" );
691 }
692 $doc = new DOMDocument;
693 $doc->loadXML( $xml );
694 $rulesets = $doc->getElementsByTagName( "pluralRules" );
695 foreach ( $rulesets as $ruleset ) {
696 $codes = $ruleset->getAttribute( 'locales' );
697 $rules = [];
698 $ruleTypes = [];
699 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
700 foreach ( $ruleElements as $elt ) {
701 $ruleType = $elt->getAttribute( 'count' );
702 if ( $ruleType === 'other' ) {
703 // Don't record "other" rules, which have an empty condition
704 continue;
705 }
706 $rules[] = $elt->nodeValue;
707 $ruleTypes[] = $ruleType;
708 }
709 foreach ( explode( ' ', $codes ) as $code ) {
710 $this->pluralRules[$code] = $rules;
711 $this->pluralRuleTypes[$code] = $ruleTypes;
712 }
713 }
714 }
715
725 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
726 // This reads in the PHP i18n file with non-messages l10n data
727 $fileName = $this->langNameUtils->getMessagesFileName( $code );
728 if ( !file_exists( $fileName ) ) {
729 $data = [];
730 } else {
731 $deps[] = new FileDependency( $fileName );
732 $data = $this->readPHPFile( $fileName, 'core' );
733 }
734
735 // Load CLDR plural rules for JavaScript
736 $data['pluralRules'] = $this->getPluralRules( $code );
737 // And for PHP
738 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
739 // Load plural rule types
740 $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
741
742 foreach ( $this->getPluralFiles() as $fileName ) {
743 $deps[] = new FileDependency( $fileName );
744 }
745
746 return $data;
747 }
748
756 protected function mergeItem( $key, &$value, $fallbackValue ) {
757 if ( $value !== null ) {
758 if ( $fallbackValue !== null ) {
759 if ( in_array( $key, self::$mergeableMapKeys ) ) {
760 $value += $fallbackValue;
761 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
762 $value = array_unique( array_merge( $fallbackValue, $value ) );
763 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
764 $value = array_merge_recursive( $value, $fallbackValue );
765 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
766 if ( !empty( $value['inherit'] ) ) {
767 $value = array_merge( $fallbackValue, $value );
768 }
769
770 unset( $value['inherit'] );
771 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
772 $this->mergeMagicWords( $value, $fallbackValue );
773 }
774 }
775 } else {
776 $value = $fallbackValue;
777 }
778 }
779
784 protected function mergeMagicWords( &$value, $fallbackValue ) {
785 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
786 if ( !isset( $value[$magicName] ) ) {
787 $value[$magicName] = $fallbackInfo;
788 } else {
789 $oldSynonyms = array_slice( $fallbackInfo, 1 );
790 $newSynonyms = array_slice( $value[$magicName], 1 );
791 $synonyms = array_values( array_unique( array_merge(
792 $newSynonyms, $oldSynonyms ) ) );
793 $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
794 }
795 }
796 }
797
811 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
812 $used = false;
813 foreach ( $codeSequence as $code ) {
814 if ( isset( $fallbackValue[$code] ) ) {
815 $this->mergeItem( $key, $value, $fallbackValue[$code] );
816 $used = true;
817 }
818 }
819
820 return $used;
821 }
822
830 public function getMessagesDirs() {
831 global $IP;
832
833 return [
834 'core' => "$IP/languages/i18n",
835 'exif' => "$IP/languages/i18n/exif",
836 'api' => "$IP/includes/api/i18n",
837 'rest' => "$IP/includes/Rest/i18n",
838 'oojs-ui' => "$IP/resources/lib/ooui/i18n",
839 'paramvalidator' => "$IP/includes/libs/ParamValidator/i18n",
840 ] + $this->options->get( 'MessagesDirs' );
841 }
842
849 public function recache( $code ) {
850 if ( !$code ) {
851 throw new MWException( "Invalid language code requested" );
852 }
853 $this->recachedLangs[ $code ] = true;
854
855 # Initial values
856 $initialData = array_fill_keys( self::$allKeys, null );
857 $coreData = $initialData;
858 $deps = [];
859
860 # Load the primary localisation from the source file
861 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
862 $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
863
864 # Merge primary localisation
865 foreach ( $data as $key => $value ) {
866 $this->mergeItem( $key, $coreData[ $key ], $value );
867 }
868
869 # Fill in the fallback if it's not there already
870 // @phan-suppress-next-line PhanSuspiciousValueComparison
871 if ( ( $coreData['fallback'] === null || $coreData['fallback'] === false ) && $code === 'en' ) {
872 $coreData['fallback'] = false;
873 $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
874 } else {
875 if ( $coreData['fallback'] !== null ) {
876 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
877 } else {
878 $coreData['fallbackSequence'] = [];
879 }
880 $len = count( $coreData['fallbackSequence'] );
881
882 # Before we add the 'en' fallback for messages, keep a copy of
883 # the original fallback sequence
884 $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
885
886 # Ensure that the sequence ends at 'en' for messages
887 if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
888 $coreData['fallbackSequence'][] = 'en';
889 }
890 }
891
892 $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
893 $messageDirs = $this->getMessagesDirs();
894
895 # Load non-JSON localisation data for extensions
896 $extensionData = array_fill_keys( $codeSequence, $initialData );
897 foreach ( $this->options->get( 'ExtensionMessagesFiles' ) as $extension => $fileName ) {
898 if ( isset( $messageDirs[$extension] ) ) {
899 # This extension has JSON message data; skip the PHP shim
900 continue;
901 }
902
903 $data = $this->readPHPFile( $fileName, 'extension' );
904 $used = false;
905
906 foreach ( $data as $key => $item ) {
907 foreach ( $codeSequence as $csCode ) {
908 if ( isset( $item[$csCode] ) ) {
909 $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
910 $used = true;
911 }
912 }
913 }
914
915 if ( $used ) {
916 $deps[] = new FileDependency( $fileName );
917 }
918 }
919
920 # Load the localisation data for each fallback, then merge it into the full array
921 $allData = $initialData;
922 foreach ( $codeSequence as $csCode ) {
923 $csData = $initialData;
924
925 # Load core messages and the extension localisations.
926 foreach ( $messageDirs as $dirs ) {
927 foreach ( (array)$dirs as $dir ) {
928 $fileName = "$dir/$csCode.json";
929 $data = $this->readJSONFile( $fileName );
930
931 foreach ( $data as $key => $item ) {
932 $this->mergeItem( $key, $csData[$key], $item );
933 }
934
935 $deps[] = new FileDependency( $fileName );
936 }
937 }
938
939 # Merge non-JSON extension data
940 if ( isset( $extensionData[$csCode] ) ) {
941 foreach ( $extensionData[$csCode] as $key => $item ) {
942 $this->mergeItem( $key, $csData[$key], $item );
943 }
944 }
945
946 if ( $csCode === $code ) {
947 # Merge core data into extension data
948 foreach ( $coreData as $key => $item ) {
949 $this->mergeItem( $key, $csData[$key], $item );
950 }
951 } else {
952 # Load the secondary localisation from the source file to
953 # avoid infinite cycles on cyclic fallbacks
954 $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
955 # Only merge the keys that make sense to merge
956 foreach ( self::$allKeys as $key ) {
957 if ( !isset( $fbData[ $key ] ) ) {
958 continue;
959 }
960
961 if ( ( $coreData[ $key ] ) === null || $this->isMergeableKey( $key ) ) {
962 $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
963 }
964 }
965 }
966
967 # Allow extensions an opportunity to adjust the data for this
968 # fallback
969 $this->hookRunner->onLocalisationCacheRecacheFallback( $this, $csCode, $csData );
970
971 # Merge the data for this fallback into the final array
972 if ( $csCode === $code ) {
973 $allData = $csData;
974 } else {
975 foreach ( self::$allKeys as $key ) {
976 if ( !isset( $csData[$key] ) ) {
977 continue;
978 }
979
980 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
981 if ( $allData[$key] === null || $this->isMergeableKey( $key ) ) {
982 $this->mergeItem( $key, $allData[$key], $csData[$key] );
983 }
984 }
985 }
986 }
987
988 # Add cache dependencies for any referenced globals
989 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
990 // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
991 // We use the key 'wgMessagesDirs' for historical reasons.
992 $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' );
993 $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
994
995 # Add dependencies to the cache entry
996 $allData['deps'] = $deps;
997
998 # Replace spaces with underscores in namespace names
999 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
1000
1001 # And do the same for special page aliases. $page is an array.
1002 foreach ( $allData['specialPageAliases'] as &$page ) {
1003 $page = str_replace( ' ', '_', $page );
1004 }
1005 # Decouple the reference to prevent accidental damage
1006 unset( $page );
1007
1008 # If there were no plural rules, return an empty array
1009 if ( $allData['pluralRules'] === null ) {
1010 $allData['pluralRules'] = [];
1011 }
1012 if ( $allData['compiledPluralRules'] === null ) {
1013 $allData['compiledPluralRules'] = [];
1014 }
1015 # If there were no plural rule types, return an empty array
1016 if ( $allData['pluralRuleTypes'] === null ) {
1017 $allData['pluralRuleTypes'] = [];
1018 }
1019
1020 # Set the list keys
1021 $allData['list'] = [];
1022 foreach ( self::$splitKeys as $key ) {
1023 $allData['list'][$key] = array_keys( $allData[$key] );
1024 }
1025 # Run hooks
1026 $unused = true; // Used to be $purgeBlobs, removed in 1.34
1027 $this->hookRunner->onLocalisationCacheRecache( $this, $code, $allData, $unused );
1028
1029 if ( $allData['namespaceNames'] === null ) {
1030 throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
1031 'Check that your languages/messages/MessagesEn.php file is intact.' );
1032 }
1033
1034 # Set the preload key
1035 $allData['preload'] = $this->buildPreload( $allData );
1036
1037 # Save to the process cache and register the items loaded
1038 $this->data[$code] = $allData;
1039 foreach ( $allData as $key => $item ) {
1040 $this->loadedItems[$code][$key] = true;
1041 }
1042
1043 # Save to the persistent cache
1044 $this->store->startWrite( $code );
1045 foreach ( $allData as $key => $value ) {
1046 if ( in_array( $key, self::$splitKeys ) ) {
1047 foreach ( $value as $subkey => $subvalue ) {
1048 $this->store->set( "$key:$subkey", $subvalue );
1049 }
1050 } else {
1051 $this->store->set( $key, $value );
1052 }
1053 }
1054 $this->store->finishWrite();
1055
1056 # Clear out the MessageBlobStore
1057 # HACK: If using a null (i.e. disabled) storage backend, we
1058 # can't write to the MessageBlobStore either
1059 if ( !$this->store instanceof LCStoreNull ) {
1060 foreach ( $this->clearStoreCallbacks as $callback ) {
1061 $callback();
1062 }
1063 }
1064 }
1065
1074 protected function buildPreload( $data ) {
1075 $preload = [ 'messages' => [] ];
1076 foreach ( self::$preloadedKeys as $key ) {
1077 $preload[$key] = $data[$key];
1078 }
1079
1080 foreach ( $data['preloadedMessages'] as $subkey ) {
1081 $subitem = $data['messages'][$subkey] ?? null;
1082 $preload['messages'][$subkey] = $subitem;
1083 }
1084
1085 return $preload;
1086 }
1087
1093 public function unload( $code ) {
1094 unset( $this->data[$code] );
1095 unset( $this->loadedItems[$code] );
1096 unset( $this->loadedSubitems[$code] );
1097 unset( $this->initialisedLangs[$code] );
1098 unset( $this->shallowFallbacks[$code] );
1099
1100 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1101 if ( $fbCode === $code ) {
1102 $this->unload( $shallowCode );
1103 }
1104 }
1105 }
1106
1110 public function unloadAll() {
1111 foreach ( $this->initialisedLangs as $lang => $unused ) {
1112 $this->unload( $lang );
1113 }
1114 }
1115
1119 public function disableBackend() {
1120 $this->store = new LCStoreNull;
1121 $this->manualRecache = false;
1122 }
1123}
$IP
Definition WebStart.php:49
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.
__construct(ServiceOptions $options, LCStore $store, LoggerInterface $logger, array $clearStoreCallbacks, LanguageNameUtils $langNameUtils, HookContainer $hookContainer)
For constructor parameters, see the documentation in DefaultSettings.php for $wgLocalisationCacheConf...
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.
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,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
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