MediaWiki REL1_35
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
54 private $manualRecache = false;
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', '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',
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
236 public const CONSTRUCTOR_OPTIONS = [
237 // True to treat all files as expired until they are regenerated by this object.
238 'forceRecache',
239 'manualRecache',
240 'ExtensionMessagesFiles',
241 'MessagesDirs',
242 ];
243
260 public function __construct(
261 ServiceOptions $options,
262 LCStore $store,
263 LoggerInterface $logger,
264 array $clearStoreCallbacks,
265 LanguageNameUtils $langNameUtils,
266 HookContainer $hookContainer
267 ) {
268 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
269
270 $this->options = $options;
271 $this->store = $store;
272 $this->logger = $logger;
273 $this->clearStoreCallbacks = $clearStoreCallbacks;
274 $this->langNameUtils = $langNameUtils;
275 $this->hookRunner = new HookRunner( $hookContainer );
276
277 // Keep this separate from $this->options so it can be mutable
278 $this->manualRecache = $options->get( 'manualRecache' );
279 }
280
287 public function isMergeableKey( $key ) {
288 if ( $this->mergeableKeys === null ) {
289 $this->mergeableKeys = array_flip( array_merge(
290 self::$mergeableMapKeys,
291 self::$mergeableListKeys,
292 self::$mergeableAliasListKeys,
293 self::$optionalMergeKeys,
294 self::$magicWordKeys
295 ) );
296 }
297
298 return isset( $this->mergeableKeys[$key] );
299 }
300
310 public function getItem( $code, $key ) {
311 if ( !isset( $this->loadedItems[$code][$key] ) ) {
312 $this->loadItem( $code, $key );
313 }
314
315 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
316 return $this->shallowFallbacks[$code];
317 }
318
319 return $this->data[$code][$key];
320 }
321
329 public function getSubitem( $code, $key, $subkey ) {
330 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
331 !isset( $this->loadedItems[$code][$key] )
332 ) {
333 $this->loadSubitem( $code, $key, $subkey );
334 }
335
336 return $this->data[$code][$key][$subkey] ?? null;
337 }
338
351 public function getSubitemList( $code, $key ) {
352 if ( in_array( $key, self::$splitKeys ) ) {
353 return $this->getSubitem( $code, 'list', $key );
354 } else {
355 $item = $this->getItem( $code, $key );
356 if ( is_array( $item ) ) {
357 return array_keys( $item );
358 } else {
359 return false;
360 }
361 }
362 }
363
369 protected function loadItem( $code, $key ) {
370 if ( !isset( $this->initialisedLangs[$code] ) ) {
371 $this->initLanguage( $code );
372 }
373
374 // Check to see if initLanguage() loaded it for us
375 if ( isset( $this->loadedItems[$code][$key] ) ) {
376 return;
377 }
378
379 if ( isset( $this->shallowFallbacks[$code] ) ) {
380 $this->loadItem( $this->shallowFallbacks[$code], $key );
381
382 return;
383 }
384
385 if ( in_array( $key, self::$splitKeys ) ) {
386 $subkeyList = $this->getSubitem( $code, 'list', $key );
387 foreach ( $subkeyList as $subkey ) {
388 if ( isset( $this->data[$code][$key][$subkey] ) ) {
389 continue;
390 }
391 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
392 }
393 } else {
394 $this->data[$code][$key] = $this->store->get( $code, $key );
395 }
396
397 $this->loadedItems[$code][$key] = true;
398 }
399
406 protected function loadSubitem( $code, $key, $subkey ) {
407 if ( !in_array( $key, self::$splitKeys ) ) {
408 $this->loadItem( $code, $key );
409
410 return;
411 }
412
413 if ( !isset( $this->initialisedLangs[$code] ) ) {
414 $this->initLanguage( $code );
415 }
416
417 // Check to see if initLanguage() loaded it for us
418 if ( isset( $this->loadedItems[$code][$key] ) ||
419 isset( $this->loadedSubitems[$code][$key][$subkey] )
420 ) {
421 return;
422 }
423
424 if ( isset( $this->shallowFallbacks[$code] ) ) {
425 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
426
427 return;
428 }
429
430 $value = $this->store->get( $code, "$key:$subkey" );
431 $this->data[$code][$key][$subkey] = $value;
432 $this->loadedSubitems[$code][$key][$subkey] = true;
433 }
434
442 public function isExpired( $code ) {
443 if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
444 $this->logger->debug( __METHOD__ . "($code): forced reload" );
445
446 return true;
447 }
448
449 $deps = $this->store->get( $code, 'deps' );
450 $keys = $this->store->get( $code, 'list' );
451 $preload = $this->store->get( $code, 'preload' );
452 // Different keys may expire separately for some stores
453 if ( $deps === null || $keys === null || $preload === null ) {
454 $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one" );
455
456 return true;
457 }
458
459 foreach ( $deps as $dep ) {
460 // Because we're unserializing stuff from cache, we
461 // could receive objects of classes that don't exist
462 // anymore (e.g. uninstalled extensions)
463 // When this happens, always expire the cache
464 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
465 $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
466 get_class( $dep ) );
467
468 return true;
469 }
470 }
471
472 return false;
473 }
474
480 protected function initLanguage( $code ) {
481 if ( isset( $this->initialisedLangs[$code] ) ) {
482 return;
483 }
484
485 $this->initialisedLangs[$code] = true;
486
487 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
488 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
489 $this->initShallowFallback( $code, 'en' );
490
491 return;
492 }
493
494 # Recache the data if necessary
495 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
496 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
497 $this->recache( $code );
498 } elseif ( $code === 'en' ) {
499 throw new MWException( 'MessagesEn.php is missing.' );
500 } else {
501 $this->initShallowFallback( $code, 'en' );
502 }
503
504 return;
505 }
506
507 # Preload some stuff
508 $preload = $this->getItem( $code, 'preload' );
509 if ( $preload === null ) {
510 if ( $this->manualRecache ) {
511 // No Messages*.php file. Do shallow fallback to en.
512 if ( $code === 'en' ) {
513 throw new MWException( 'No localisation cache found for English. ' .
514 'Please run maintenance/rebuildLocalisationCache.php.' );
515 }
516 $this->initShallowFallback( $code, 'en' );
517
518 return;
519 } else {
520 throw new MWException( 'Invalid or missing localisation cache.' );
521 }
522 }
523 $this->data[$code] = $preload;
524 foreach ( $preload as $key => $item ) {
525 if ( in_array( $key, self::$splitKeys ) ) {
526 foreach ( $item as $subkey => $subitem ) {
527 $this->loadedSubitems[$code][$key][$subkey] = true;
528 }
529 } else {
530 $this->loadedItems[$code][$key] = true;
531 }
532 }
533 }
534
541 public function initShallowFallback( $primaryCode, $fallbackCode ) {
542 $this->data[$primaryCode] =& $this->data[$fallbackCode];
543 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
544 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
545 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
546 }
547
555 protected function readPHPFile( $_fileName, $_fileType ) {
556 include $_fileName;
557
558 $data = [];
559 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
560 foreach ( self::$allKeys as $key ) {
561 // Not all keys are set in language files, so
562 // check they exist first
563 if ( isset( $$key ) ) {
564 $data[$key] = $$key;
565 }
566 }
567 } elseif ( $_fileType == 'aliases' ) {
568 // @phan-suppress-next-line PhanImpossibleCondition May be set in included file
569 if ( isset( $aliases ) ) {
570 $data['aliases'] = $aliases;
571 }
572 } else {
573 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
574 }
575
576 return $data;
577 }
578
585 public function readJSONFile( $fileName ) {
586 if ( !is_readable( $fileName ) ) {
587 return [];
588 }
589
590 $json = file_get_contents( $fileName );
591 if ( $json === false ) {
592 return [];
593 }
594
595 $data = FormatJson::decode( $json, true );
596 if ( $data === null ) {
597 throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
598 }
599
600 // Remove keys starting with '@', they're reserved for metadata and non-message data
601 foreach ( $data as $key => $unused ) {
602 if ( $key === '' || $key[0] === '@' ) {
603 unset( $data[$key] );
604 }
605 }
606
607 // The JSON format only supports messages, none of the other variables, so wrap the data
608 return [ 'messages' => $data ];
609 }
610
617 public function getCompiledPluralRules( $code ) {
618 $rules = $this->getPluralRules( $code );
619 if ( $rules === null ) {
620 return null;
621 }
622 try {
623 $compiledRules = Evaluator::compile( $rules );
624 } catch ( CLDRPluralRuleError $e ) {
625 $this->logger->debug( $e->getMessage() );
626
627 return [];
628 }
629
630 return $compiledRules;
631 }
632
640 public function getPluralRules( $code ) {
641 if ( $this->pluralRules === null ) {
642 $this->loadPluralFiles();
643 }
644 return $this->pluralRules[$code] ?? null;
645 }
646
654 public function getPluralRuleTypes( $code ) {
655 if ( $this->pluralRuleTypes === null ) {
656 $this->loadPluralFiles();
657 }
658 return $this->pluralRuleTypes[$code] ?? null;
659 }
660
664 protected function loadPluralFiles() {
665 global $IP;
666 $cldrPlural = "$IP/languages/data/plurals.xml";
667 $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
668 // Load CLDR plural rules
669 $this->loadPluralFile( $cldrPlural );
670 if ( file_exists( $mwPlural ) ) {
671 // Override or extend
672 $this->loadPluralFile( $mwPlural );
673 }
674 }
675
683 protected function loadPluralFile( $fileName ) {
684 // Use file_get_contents instead of DOMDocument::load (T58439)
685 $xml = file_get_contents( $fileName );
686 if ( !$xml ) {
687 throw new MWException( "Unable to read plurals file $fileName" );
688 }
689 $doc = new DOMDocument;
690 $doc->loadXML( $xml );
691 $rulesets = $doc->getElementsByTagName( "pluralRules" );
692 foreach ( $rulesets as $ruleset ) {
693 $codes = $ruleset->getAttribute( 'locales' );
694 $rules = [];
695 $ruleTypes = [];
696 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
697 foreach ( $ruleElements as $elt ) {
698 $ruleType = $elt->getAttribute( 'count' );
699 if ( $ruleType === 'other' ) {
700 // Don't record "other" rules, which have an empty condition
701 continue;
702 }
703 $rules[] = $elt->nodeValue;
704 $ruleTypes[] = $ruleType;
705 }
706 foreach ( explode( ' ', $codes ) as $code ) {
707 $this->pluralRules[$code] = $rules;
708 $this->pluralRuleTypes[$code] = $ruleTypes;
709 }
710 }
711 }
712
722 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
723 global $IP;
724
725 // This reads in the PHP i18n file with non-messages l10n data
726 $fileName = $this->langNameUtils->getMessagesFileName( $code );
727 if ( !file_exists( $fileName ) ) {
728 $data = [];
729 } else {
730 $deps[] = new FileDependency( $fileName );
731 $data = $this->readPHPFile( $fileName, 'core' );
732 }
733
734 # Load CLDR plural rules for JavaScript
735 $data['pluralRules'] = $this->getPluralRules( $code );
736 # And for PHP
737 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
738 # Load plural rule types
739 $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
740
741 $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
742 $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
743
744 return $data;
745 }
746
754 protected function mergeItem( $key, &$value, $fallbackValue ) {
755 if ( $value !== null ) {
756 if ( $fallbackValue !== null ) {
757 if ( in_array( $key, self::$mergeableMapKeys ) ) {
758 $value += $fallbackValue;
759 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
760 $value = array_unique( array_merge( $fallbackValue, $value ) );
761 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
762 $value = array_merge_recursive( $value, $fallbackValue );
763 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
764 if ( !empty( $value['inherit'] ) ) {
765 $value = array_merge( $fallbackValue, $value );
766 }
767
768 if ( isset( $value['inherit'] ) ) {
769 unset( $value['inherit'] );
770 }
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
Stable to extend.
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