MediaWiki 1.40.4
LocalisationCache.php
Go to the documentation of this file.
1<?php
21use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
22use CLDRPluralRuleParser\Evaluator;
28use Psr\Log\LoggerInterface;
29
47 public const VERSION = 5;
48
50 private $options;
51
57 private $manualRecache;
58
65 protected $data = [];
66
70 protected $sourceLanguage = [];
71
77 private $store;
78
82 private $logger;
83
85 private $hookRunner;
86
88 private $clearStoreCallbacks;
89
91 private $langNameUtils;
92
101 private $loadedItems = [];
102
107 private $loadedSubitems = [];
108
114 private $initialisedLangs = [];
115
121 private $shallowFallbacks = [];
122
126 private $recachedLangs = [];
127
131 public static $allKeys = [
132 'fallback', 'namespaceNames', 'bookstoreList',
133 'magicWords', 'messages', 'rtl',
134 'digitTransformTable', 'separatorTransformTable',
135 'minimumGroupingDigits', 'fallback8bitEncoding',
136 'linkPrefixExtension', 'linkTrail', 'linkPrefixCharset',
137 'namespaceAliases', 'dateFormats', 'datePreferences',
138 'datePreferenceMigrationMap', 'defaultDateFormat',
139 'specialPageAliases', 'imageFiles', 'preloadedMessages',
140 'namespaceGenderAliases', 'digitGroupingPattern', 'pluralRules',
141 'pluralRuleTypes', 'compiledPluralRules',
142 ];
143
148 public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
149 'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
150 ];
151
155 public static $mergeableListKeys = [];
156
161 public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
162
168 public static $optionalMergeKeys = [ 'bookstoreList' ];
169
173 public static $magicWordKeys = [ 'magicWords' ];
174
178 public static $splitKeys = [ 'messages' ];
179
184 public static $sourcePrefixKeys = [ 'messages' ];
185
189 protected const SOURCEPREFIX_SEPARATOR = ':';
190
194 public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
195
200 private $pluralRules = null;
201
214 private $pluralRuleTypes = null;
215
216 private $mergeableKeys = null;
217
226 public static function getStoreFromConf( array $conf, $fallbackCacheDir ): LCStore {
227 $storeArg = [];
228 $storeArg['directory'] =
229 $conf['storeDirectory'] ?: $fallbackCacheDir;
230
231 if ( !empty( $conf['storeClass'] ) ) {
232 $storeClass = $conf['storeClass'];
233 } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
234 ( $conf['store'] === 'detect' && $storeArg['directory'] )
235 ) {
236 $storeClass = LCStoreCDB::class;
237 } elseif ( $conf['store'] === 'db' || $conf['store'] === 'detect' ) {
238 $storeClass = LCStoreDB::class;
239 $storeArg['server'] = $conf['storeServer'] ?? [];
240 } elseif ( $conf['store'] === 'array' ) {
241 $storeClass = LCStoreStaticArray::class;
242 } else {
243 throw new MWException(
244 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
245 );
246 }
247
248 return new $storeClass( $storeArg );
249 }
250
254 public const CONSTRUCTOR_OPTIONS = [
255 // True to treat all files as expired until they are regenerated by this object.
256 'forceRecache',
257 'manualRecache',
258 MainConfigNames::ExtensionMessagesFiles,
259 MainConfigNames::MessagesDirs,
260 ];
261
275 public function __construct(
276 ServiceOptions $options,
277 LCStore $store,
278 LoggerInterface $logger,
279 array $clearStoreCallbacks,
280 LanguageNameUtils $langNameUtils,
281 HookContainer $hookContainer
282 ) {
283 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
284
285 $this->options = $options;
286 $this->store = $store;
287 $this->logger = $logger;
288 $this->clearStoreCallbacks = $clearStoreCallbacks;
289 $this->langNameUtils = $langNameUtils;
290 $this->hookRunner = new HookRunner( $hookContainer );
291
292 // Keep this separate from $this->options so it can be mutable
293 $this->manualRecache = $options->get( 'manualRecache' );
294 }
295
302 public function isMergeableKey( $key ) {
303 if ( $this->mergeableKeys === null ) {
304 $this->mergeableKeys = array_fill_keys( array_merge(
305 self::$mergeableMapKeys,
306 self::$mergeableListKeys,
307 self::$mergeableAliasListKeys,
308 self::$optionalMergeKeys,
309 self::$magicWordKeys
310 ), true );
311 }
312
313 return isset( $this->mergeableKeys[$key] );
314 }
315
325 public function getItem( $code, $key ) {
326 if ( !isset( $this->loadedItems[$code][$key] ) ) {
327 $this->loadItem( $code, $key );
328 }
329
330 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
331 return $this->shallowFallbacks[$code];
332 }
333
334 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
335 return $this->data[$code][$key];
336 }
337
345 public function getSubitem( $code, $key, $subkey ) {
346 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
347 !isset( $this->loadedItems[$code][$key] )
348 ) {
349 $this->loadSubitem( $code, $key, $subkey );
350 }
351
352 return $this->data[$code][$key][$subkey] ?? null;
353 }
354
364 public function getSubitemWithSource( $code, $key, $subkey ) {
365 $subitem = $this->getSubitem( $code, $key, $subkey );
366 // Undefined in the backend.
367 if ( $subitem === null ) {
368 return null;
369 }
370
371 // The source language should have been set, but to avoid Phan error and be double sure.
372 return [ $subitem, $this->sourceLanguage[$code][$key][$subkey] ?? $code ];
373 }
374
387 public function getSubitemList( $code, $key ) {
388 if ( in_array( $key, self::$splitKeys ) ) {
389 return $this->getSubitem( $code, 'list', $key );
390 } else {
391 $item = $this->getItem( $code, $key );
392 if ( is_array( $item ) ) {
393 return array_keys( $item );
394 } else {
395 return false;
396 }
397 }
398 }
399
405 protected function loadItem( $code, $key ) {
406 if ( !isset( $this->initialisedLangs[$code] ) ) {
407 $this->initLanguage( $code );
408 }
409
410 // Check to see if initLanguage() loaded it for us
411 if ( isset( $this->loadedItems[$code][$key] ) ) {
412 return;
413 }
414
415 if ( isset( $this->shallowFallbacks[$code] ) ) {
416 $this->loadItem( $this->shallowFallbacks[$code], $key );
417
418 return;
419 }
420
421 if ( in_array( $key, self::$splitKeys ) ) {
422 $subkeyList = $this->getSubitem( $code, 'list', $key );
423 foreach ( $subkeyList as $subkey ) {
424 if ( isset( $this->data[$code][$key][$subkey] ) ) {
425 continue;
426 }
427 $this->loadSubitem( $code, $key, $subkey );
428 }
429 } else {
430 $this->data[$code][$key] = $this->store->get( $code, $key );
431 }
432
433 $this->loadedItems[$code][$key] = true;
434 }
435
442 protected function loadSubitem( $code, $key, $subkey ) {
443 if ( !in_array( $key, self::$splitKeys ) ) {
444 $this->loadItem( $code, $key );
445
446 return;
447 }
448
449 if ( !isset( $this->initialisedLangs[$code] ) ) {
450 $this->initLanguage( $code );
451 }
452
453 // Check to see if initLanguage() loaded it for us
454 if ( isset( $this->loadedItems[$code][$key] ) ||
455 isset( $this->loadedSubitems[$code][$key][$subkey] )
456 ) {
457 return;
458 }
459
460 if ( isset( $this->shallowFallbacks[$code] ) ) {
461 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
462
463 return;
464 }
465
466 $value = $this->store->get( $code, "$key:$subkey" );
467 if ( $value !== null && in_array( $key, self::$sourcePrefixKeys ) ) {
468 [
469 $this->sourceLanguage[$code][$key][$subkey],
470 $this->data[$code][$key][$subkey]
471 ] = explode( self::SOURCEPREFIX_SEPARATOR, $value, 2 );
472 } else {
473 $this->data[$code][$key][$subkey] = $value;
474 }
475
476 $this->loadedSubitems[$code][$key][$subkey] = true;
477 }
478
486 public function isExpired( $code ) {
487 if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
488 $this->logger->debug( __METHOD__ . "($code): forced reload" );
489
490 return true;
491 }
492
493 $deps = $this->store->get( $code, 'deps' );
494 $keys = $this->store->get( $code, 'list' );
495 $preload = $this->store->get( $code, 'preload' );
496 // Different keys may expire separately for some stores
497 if ( $deps === null || $keys === null || $preload === null ) {
498 $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one" );
499
500 return true;
501 }
502
503 foreach ( $deps as $dep ) {
504 // Because we're unserializing stuff from cache, we
505 // could receive objects of classes that don't exist
506 // anymore (e.g. uninstalled extensions)
507 // When this happens, always expire the cache
508 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
509 $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
510 get_class( $dep ) );
511
512 return true;
513 }
514 }
515
516 return false;
517 }
518
524 protected function initLanguage( $code ) {
525 if ( isset( $this->initialisedLangs[$code] ) ) {
526 return;
527 }
528
529 $this->initialisedLangs[$code] = true;
530
531 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
532 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
533 $this->initShallowFallback( $code, 'en' );
534
535 return;
536 }
537
538 # Recache the data if necessary
539 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
540 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
541 $this->recache( $code );
542 } elseif ( $code === 'en' ) {
543 throw new MWException( 'MessagesEn.php is missing.' );
544 } else {
545 $this->initShallowFallback( $code, 'en' );
546 }
547
548 return;
549 }
550
551 # Preload some stuff
552 $preload = $this->getItem( $code, 'preload' );
553 if ( $preload === null ) {
554 if ( $this->manualRecache ) {
555 // No Messages*.php file. Do shallow fallback to en.
556 if ( $code === 'en' ) {
557 throw new MWException( 'No localisation cache found for English. ' .
558 'Please run maintenance/rebuildLocalisationCache.php.' );
559 }
560 $this->initShallowFallback( $code, 'en' );
561
562 return;
563 } else {
564 throw new MWException( 'Invalid or missing localisation cache.' );
565 }
566 }
567
568 foreach ( self::$sourcePrefixKeys as $key ) {
569 if ( !isset( $preload[$key] ) ) {
570 continue;
571 }
572 foreach ( $preload[$key] as $subkey => $value ) {
573 if ( $value !== null ) {
574 [
575 $this->sourceLanguage[$code][$key][$subkey],
576 $preload[$key][$subkey]
577 ] = explode( self::SOURCEPREFIX_SEPARATOR, $value, 2 );
578 } else {
579 $preload[$key][$subkey] = null;
580 }
581 }
582 }
583
584 $this->data[$code] = $preload;
585 foreach ( $preload as $key => $item ) {
586 if ( in_array( $key, self::$splitKeys ) ) {
587 foreach ( $item as $subkey => $subitem ) {
588 $this->loadedSubitems[$code][$key][$subkey] = true;
589 }
590 } else {
591 $this->loadedItems[$code][$key] = true;
592 }
593 }
594 }
595
602 public function initShallowFallback( $primaryCode, $fallbackCode ) {
603 $this->data[$primaryCode] =& $this->data[$fallbackCode];
604 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
605 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
606 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
607 }
608
616 protected function readPHPFile( $_fileName, $_fileType ) {
617 include $_fileName;
618
619 $data = [];
620 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
621 foreach ( self::$allKeys as $key ) {
622 // Not all keys are set in language files, so
623 // check they exist first
624 if ( isset( $$key ) ) {
625 $data[$key] = $$key;
626 }
627 }
628 } elseif ( $_fileType == 'aliases' ) {
629 // @phan-suppress-next-line PhanImpossibleCondition May be set in included file
630 if ( isset( $aliases ) ) {
631 $data['aliases'] = $aliases;
632 }
633 } else {
634 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
635 }
636
637 return $data;
638 }
639
646 public function readJSONFile( $fileName ) {
647 if ( !is_readable( $fileName ) ) {
648 return [];
649 }
650
651 $json = file_get_contents( $fileName );
652 if ( $json === false ) {
653 return [];
654 }
655
656 $data = FormatJson::decode( $json, true );
657 if ( $data === null ) {
658 throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
659 }
660
661 // Remove keys starting with '@', they're reserved for metadata and non-message data
662 foreach ( $data as $key => $unused ) {
663 if ( $key === '' || $key[0] === '@' ) {
664 unset( $data[$key] );
665 }
666 }
667
668 return $data;
669 }
670
677 public function getCompiledPluralRules( $code ) {
678 $rules = $this->getPluralRules( $code );
679 if ( $rules === null ) {
680 return null;
681 }
682 try {
683 $compiledRules = Evaluator::compile( $rules );
684 } catch ( CLDRPluralRuleError $e ) {
685 $this->logger->debug( $e->getMessage() );
686
687 return [];
688 }
689
690 return $compiledRules;
691 }
692
700 public function getPluralRules( $code ) {
701 if ( $this->pluralRules === null ) {
702 $this->loadPluralFiles();
703 }
704 return $this->pluralRules[$code] ?? null;
705 }
706
714 public function getPluralRuleTypes( $code ) {
715 if ( $this->pluralRuleTypes === null ) {
716 $this->loadPluralFiles();
717 }
718 return $this->pluralRuleTypes[$code] ?? null;
719 }
720
724 protected function loadPluralFiles() {
725 foreach ( $this->getPluralFiles() as $fileName ) {
726 $this->loadPluralFile( $fileName );
727 }
728 }
729
730 private function getPluralFiles(): array {
731 global $IP;
732 return [
733 // Load CLDR plural rules
734 "$IP/languages/data/plurals.xml",
735 // Override or extend with MW-specific rules
736 "$IP/languages/data/plurals-mediawiki.xml",
737 ];
738 }
739
747 protected function loadPluralFile( $fileName ) {
748 // Use file_get_contents instead of DOMDocument::load (T58439)
749 $xml = file_get_contents( $fileName );
750 if ( !$xml ) {
751 throw new MWException( "Unable to read plurals file $fileName" );
752 }
753 $doc = new DOMDocument;
754 $doc->loadXML( $xml );
755 $rulesets = $doc->getElementsByTagName( "pluralRules" );
756 foreach ( $rulesets as $ruleset ) {
757 $codes = $ruleset->getAttribute( 'locales' );
758 $rules = [];
759 $ruleTypes = [];
760 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
761 foreach ( $ruleElements as $elt ) {
762 $ruleType = $elt->getAttribute( 'count' );
763 if ( $ruleType === 'other' ) {
764 // Don't record "other" rules, which have an empty condition
765 continue;
766 }
767 $rules[] = $elt->nodeValue;
768 $ruleTypes[] = $ruleType;
769 }
770 foreach ( explode( ' ', $codes ) as $code ) {
771 $this->pluralRules[$code] = $rules;
772 $this->pluralRuleTypes[$code] = $ruleTypes;
773 }
774 }
775 }
776
786 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
787 // This reads in the PHP i18n file with non-messages l10n data
788 $fileName = $this->langNameUtils->getMessagesFileName( $code );
789 if ( !is_file( $fileName ) ) {
790 $data = [];
791 } else {
792 $deps[] = new FileDependency( $fileName );
793 $data = $this->readPHPFile( $fileName, 'core' );
794 }
795
796 // Load CLDR plural rules for JavaScript
797 $data['pluralRules'] = $this->getPluralRules( $code );
798 // And for PHP
799 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
800 // Load plural rule types
801 $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
802
803 foreach ( $this->getPluralFiles() as $fileName ) {
804 $deps[] = new FileDependency( $fileName );
805 }
806
807 return $data;
808 }
809
817 protected function mergeItem( $key, &$value, $fallbackValue ) {
818 if ( $value !== null ) {
819 if ( $fallbackValue !== null ) {
820 if ( in_array( $key, self::$mergeableMapKeys ) ) {
821 $value += $fallbackValue;
822 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
823 $value = array_unique( array_merge( $fallbackValue, $value ) );
824 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
825 $value = array_merge_recursive( $value, $fallbackValue );
826 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
827 if ( !empty( $value['inherit'] ) ) {
828 $value = array_merge( $fallbackValue, $value );
829 }
830
831 unset( $value['inherit'] );
832 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
833 $this->mergeMagicWords( $value, $fallbackValue );
834 }
835 }
836 } else {
837 $value = $fallbackValue;
838 }
839 }
840
845 protected function mergeMagicWords( &$value, $fallbackValue ) {
846 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
847 if ( !isset( $value[$magicName] ) ) {
848 $value[$magicName] = $fallbackInfo;
849 } else {
850 $oldSynonyms = array_slice( $fallbackInfo, 1 );
851 $newSynonyms = array_slice( $value[$magicName], 1 );
852 $synonyms = array_values( array_unique( array_merge(
853 $newSynonyms, $oldSynonyms ) ) );
854 $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
855 }
856 }
857 }
858
866 public function getMessagesDirs() {
867 global $IP;
868
869 return [
870 'core' => "$IP/languages/i18n",
871 'exif' => "$IP/languages/i18n/exif",
872 'api' => "$IP/includes/api/i18n",
873 'rest' => "$IP/includes/Rest/i18n",
874 'oojs-ui' => "$IP/resources/lib/ooui/i18n",
875 'paramvalidator' => "$IP/includes/libs/ParamValidator/i18n",
876 ] + $this->options->get( MainConfigNames::MessagesDirs );
877 }
878
885 public function recache( $code ) {
886 if ( !$code ) {
887 throw new MWException( "Invalid language code requested" );
888 }
889 $this->recachedLangs[ $code ] = true;
890
891 # Initial values
892 $initialData = array_fill_keys( self::$allKeys, null );
893 $coreData = $initialData;
894 $deps = [];
895
896 # Load the primary localisation from the source file
897 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
898 $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
899
900 # Merge primary localisation
901 foreach ( $data as $key => $value ) {
902 $this->mergeItem( $key, $coreData[ $key ], $value );
903 }
904
905 # Fill in the fallback if it's not there already
906 // @phan-suppress-next-line PhanSuspiciousValueComparison
907 if ( ( $coreData['fallback'] === null || $coreData['fallback'] === false ) && $code === 'en' ) {
908 $coreData['fallback'] = false;
909 $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
910 } else {
911 if ( $coreData['fallback'] !== null ) {
912 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
913 } else {
914 $coreData['fallbackSequence'] = [];
915 }
916 $len = count( $coreData['fallbackSequence'] );
917
918 # Before we add the 'en' fallback for messages, keep a copy of
919 # the original fallback sequence
920 $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
921
922 # Ensure that the sequence ends at 'en' for messages
923 if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
924 $coreData['fallbackSequence'][] = 'en';
925 }
926 }
927
928 $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
929 $messageDirs = $this->getMessagesDirs();
930
931 # Load non-JSON localisation data for extensions
932 $extensionData = array_fill_keys( $codeSequence, $initialData );
933 foreach ( $this->options->get( MainConfigNames::ExtensionMessagesFiles ) as $extension => $fileName ) {
934 if ( isset( $messageDirs[$extension] ) ) {
935 # This extension has JSON message data; skip the PHP shim
936 continue;
937 }
938
939 $data = $this->readPHPFile( $fileName, 'extension' );
940 $used = false;
941
942 foreach ( $data as $key => $item ) {
943 foreach ( $codeSequence as $csCode ) {
944 if ( isset( $item[$csCode] ) ) {
945 // Keep the behaviour the same as for json messages.
946 // TODO: Consider deprecating using a PHP file for messages.
947 if ( in_array( $key, self::$sourcePrefixKeys ) ) {
948 foreach ( $item[$csCode] as $subkey => $_ ) {
949 $this->sourceLanguage[$code][$key][$subkey] ??= $csCode;
950 }
951 }
952 $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
953 $used = true;
954 }
955 }
956 }
957
958 if ( $used ) {
959 $deps[] = new FileDependency( $fileName );
960 }
961 }
962
963 # Load the localisation data for each fallback, then merge it into the full array
964 $allData = $initialData;
965 foreach ( $codeSequence as $csCode ) {
966 $csData = $initialData;
967
968 # Load core messages and the extension localisations.
969 foreach ( $messageDirs as $dirs ) {
970 foreach ( (array)$dirs as $dir ) {
971 $fileName = "$dir/$csCode.json";
972 $messages = $this->readJSONFile( $fileName );
973
974 foreach ( $messages as $subkey => $_ ) {
975 $this->sourceLanguage[$code]['messages'][$subkey] ??= $csCode;
976 }
977 $this->mergeItem( 'messages', $csData['messages'], $messages );
978
979 $deps[] = new FileDependency( $fileName );
980 }
981 }
982
983 # Merge non-JSON extension data
984 if ( isset( $extensionData[$csCode] ) ) {
985 foreach ( $extensionData[$csCode] as $key => $item ) {
986 $this->mergeItem( $key, $csData[$key], $item );
987 }
988 }
989
990 if ( $csCode === $code ) {
991 # Merge core data into extension data
992 foreach ( $coreData as $key => $item ) {
993 $this->mergeItem( $key, $csData[$key], $item );
994 }
995 } else {
996 # Load the secondary localisation from the source file to
997 # avoid infinite cycles on cyclic fallbacks
998 $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
999 # Only merge the keys that make sense to merge
1000 foreach ( self::$allKeys as $key ) {
1001 if ( !isset( $fbData[ $key ] ) ) {
1002 continue;
1003 }
1004
1005 if ( ( $coreData[ $key ] ) === null || $this->isMergeableKey( $key ) ) {
1006 $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
1007 }
1008 }
1009 }
1010
1011 # Allow extensions an opportunity to adjust the data for this
1012 # fallback
1013 $this->hookRunner->onLocalisationCacheRecacheFallback( $this, $csCode, $csData );
1014
1015 # Merge the data for this fallback into the final array
1016 if ( $csCode === $code ) {
1017 $allData = $csData;
1018 } else {
1019 foreach ( self::$allKeys as $key ) {
1020 if ( !isset( $csData[$key] ) ) {
1021 continue;
1022 }
1023
1024 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
1025 if ( $allData[$key] === null || $this->isMergeableKey( $key ) ) {
1026 $this->mergeItem( $key, $allData[$key], $csData[$key] );
1027 }
1028 }
1029 }
1030 }
1031
1032 if ( !isset( $allData['rtl'] ) ) {
1033 throw new MWException( __METHOD__ . ': Localisation data failed validation check! ' .
1034 'Check that your languages/messages/MessagesEn.php file is intact.' );
1035 }
1036
1037 # Add cache dependencies for any referenced globals
1038 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
1039 // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
1040 // We use the key 'wgMessagesDirs' for historical reasons.
1041 $deps['wgMessagesDirs'] = new MainConfigDependency( MainConfigNames::MessagesDirs );
1042 $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
1043
1044 # Add dependencies to the cache entry
1045 $allData['deps'] = $deps;
1046
1047 # Replace spaces with underscores in namespace names
1048 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
1049
1050 # And do the same for special page aliases. $page is an array.
1051 foreach ( $allData['specialPageAliases'] as &$page ) {
1052 $page = str_replace( ' ', '_', $page );
1053 }
1054 # Decouple the reference to prevent accidental damage
1055 unset( $page );
1056
1057 # If there were no plural rules, return an empty array
1058 $allData['pluralRules'] ??= [];
1059 $allData['compiledPluralRules'] ??= [];
1060 # If there were no plural rule types, return an empty array
1061 $allData['pluralRuleTypes'] ??= [];
1062
1063 # Set the list keys
1064 $allData['list'] = [];
1065 foreach ( self::$splitKeys as $key ) {
1066 $allData['list'][$key] = array_keys( $allData[$key] );
1067 }
1068 # Run hooks
1069 $unused = true; // Used to be $purgeBlobs, removed in 1.34
1070 $this->hookRunner->onLocalisationCacheRecache( $this, $code, $allData, $unused );
1071
1072 # Save to the process cache and register the items loaded
1073 $this->data[$code] = $allData;
1074 foreach ( $allData as $key => $item ) {
1075 $this->loadedItems[$code][$key] = true;
1076 }
1077
1078 # Prefix each item with its source language code before save
1079 foreach ( self::$sourcePrefixKeys as $key ) {
1080 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
1081 foreach ( $allData[$key] as $subKey => $value ) {
1082 // The source language should have been set, but to avoid Phan error and be double sure.
1083 $allData[$key][$subKey] = ( $this->sourceLanguage[$code][$key][$subKey] ?? $code ) .
1084 self::SOURCEPREFIX_SEPARATOR . $value;
1085 }
1086 }
1087
1088 # Set the preload key
1089 $allData['preload'] = $this->buildPreload( $allData );
1090
1091 # Save to the persistent cache
1092 $this->store->startWrite( $code );
1093 foreach ( $allData as $key => $value ) {
1094 if ( in_array( $key, self::$splitKeys ) ) {
1095 foreach ( $value as $subkey => $subvalue ) {
1096 $this->store->set( "$key:$subkey", $subvalue );
1097 }
1098 } else {
1099 $this->store->set( $key, $value );
1100 }
1101 }
1102 $this->store->finishWrite();
1103
1104 # Clear out the MessageBlobStore
1105 # HACK: If using a null (i.e. disabled) storage backend, we
1106 # can't write to the MessageBlobStore either
1107 if ( !$this->store instanceof LCStoreNull ) {
1108 foreach ( $this->clearStoreCallbacks as $callback ) {
1109 $callback();
1110 }
1111 }
1112 }
1113
1122 protected function buildPreload( $data ) {
1123 $preload = [ 'messages' => [] ];
1124 foreach ( self::$preloadedKeys as $key ) {
1125 $preload[$key] = $data[$key];
1126 }
1127
1128 foreach ( $data['preloadedMessages'] as $subkey ) {
1129 $subitem = $data['messages'][$subkey] ?? null;
1130 $preload['messages'][$subkey] = $subitem;
1131 }
1132
1133 return $preload;
1134 }
1135
1141 public function unload( $code ) {
1142 unset( $this->data[$code] );
1143 unset( $this->loadedItems[$code] );
1144 unset( $this->loadedSubitems[$code] );
1145 unset( $this->initialisedLangs[$code] );
1146 unset( $this->shallowFallbacks[$code] );
1147 unset( $this->sourceLanguage[$code] );
1148
1149 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1150 if ( $fbCode === $code ) {
1151 $this->unload( $shallowCode );
1152 }
1153 }
1154 }
1155
1159 public function unloadAll() {
1160 foreach ( $this->initialisedLangs as $lang => $unused ) {
1161 $this->unload( $lang );
1162 }
1163 }
1164
1168 public function disableBackend() {
1169 $this->store = new LCStoreNull;
1170 $this->manualRecache = false;
1171 }
1172}
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:93
Base class to represent dependencies for LocalisationCache entries.
isExpired()
Returns true if the dependency is expired, false otherwise.
Depend on a PHP constant.
Depend on a file.
Depend on a PHP global variable.
Null store backend, used to avoid DB errors during install.
Caching for the contents of localisation files.
static $magicWordKeys
Keys for items that are formatted like $magicWords.
buildPreload( $data)
Build the preload item from the given pre-cache data.
getSubitemWithSource( $code, $key, $subkey)
Get a subitem with its source language.
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...
static $sourcePrefixKeys
Keys for items that will be prefixed with its source language code, which should be stripped out when...
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.
const SOURCEPREFIX_SEPARATOR
Separator for the source language prefix.
readPHPFile( $_fileName, $_fileType)
Read a PHP file containing localisation data.
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.
__construct(ServiceOptions $options, LCStore $store, LoggerInterface $logger, array $clearStoreCallbacks, LanguageNameUtils $langNameUtils, HookContainer $hookContainer)
For constructor parameters, \MediaWiki\MainConfigSchema::LocalisationCacheConf.
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.
$sourceLanguage
The source language of cached data items.
getSubitem( $code, $key, $subkey)
Get a subitem, for instance a single message for a given language.
mergeMagicWords(&$value, $fallbackValue)
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.
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.
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.
getPluralRuleTypes( $code)
Get the plural rule types for a given language from the XML files.
loadSubitem( $code, $key, $subkey)
Load a subitem into the cache.
MediaWiki exception.
Depend on a MW configuration variable.
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.
A class containing constants representing the names of configuration variables.
Interface for the persistence layer of LocalisationCache.
Definition LCStore.php:40
if(!isset( $args[0])) $lang