2declare( strict_types = 1 );
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
10use InvalidArgumentException;
14use MediaWiki\MediaWikiServices;
38 private $groupLoaders;
48 private const CACHE_VERSION = 4;
51 protected function init(): void {
52 if ( is_array( $this->groups ) ) {
57 $groups = $value[
'cc'];
60 $groups += $loader->getGroups();
67 global $wgAutoloadClasses;
69 $regenerator =
function () {
70 global $wgAutoloadClasses;
72 $groups = $deps = $autoload = [];
77 Services::getInstance()->getHookRunner()
78 ->onTranslatePostInitGroups( $groups, $deps, $autoload );
80 self::appendAutoloader( $autoload, $wgAutoloadClasses );
83 'ts' => wfTimestamp( TS_MW ),
85 'autoload' => $autoload
87 $wrapper =
new DependencyWrapper( $value, $deps );
88 $wrapper->initialiseDeps();
93 $cache = $this->getCache();
95 $wrapper = $cache->getWithSetCallback(
101 'checkKeys' => [ $this->getCacheKey() ],
102 'touchedCallback' =>
static function ( $value ) {
103 return ( $value instanceof DependencyWrapper && $value->isExpired() )
107 'minAsOf' => $recache ? INF : $cache::MIN_TIMESTAMP_NONE,
111 $value = $wrapper->getValue();
112 self::appendAutoloader( $value[
'autoload'], $wgAutoloadClasses );
123 foreach ( $groups as $id => $mixed ) {
124 if ( !is_object( $mixed ) ) {
125 $groups[$id] = call_user_func( $mixed, $id );
129 $this->groups = $groups;
135 $cache = $this->getCache();
136 $cache->touchCheckKey( $this->getCacheKey() );
138 $this->clearProcessCache();
140 foreach ( $this->getCacheGroupLoaders() as $cacheLoader ) {
141 $cacheLoader->recache();
145 $value = $this->getCachedGroupDefinitions(
'recache' );
146 $groups = $value[
'cc'];
148 foreach ( $this->getGroupLoaders() as $loader ) {
149 $groups += $loader->getGroups();
152 $this->initGroupsFromDefinitions( $groups );
161 $self = self::singleton();
163 $cache = $self->getCache();
164 $cache->delete( $self->getCacheKey(), 1 );
166 foreach ( $self->getCacheGroupLoaders() as $cacheLoader ) {
167 $cacheLoader->clearCache();
170 $self->clearProcessCache();
180 $this->groups = null;
181 $this->groupLoaders =
null;
184 protected function getCache(): WANObjectCache {
185 if ( $this->cache === null ) {
186 return MediaWikiServices::getInstance()->getMainWANObjectCache();
193 public function setCache( ?WANObjectCache $cache =
null ) {
194 $this->cache = $cache;
199 return $this->getCache()->makeKey(
'translate-groups',
'v' . self::CACHE_VERSION );
209 foreach ( $additions as $class => $file ) {
210 if ( isset( $to[$class] ) && $to[$class] !== $file ) {
211 $msg =
"Autoload conflict for $class: {$to[$class]} !== $file";
212 trigger_error( $msg, E_USER_WARNING );
226 if ( $this->groupLoaders !== null ) {
227 return $this->groupLoaders;
230 $cache = $this->getCache();
232 $groupLoaderInstances = $this->groupLoaders = [];
236 'database' => Utilities::getSafeReadDB(),
240 Services::getInstance()->getHookRunner()
241 ->onTranslateInitGroupLoaders( $groupLoaderInstances, $deps );
243 if ( $groupLoaderInstances === [] ) {
244 return $this->groupLoaders;
247 foreach ( $groupLoaderInstances as $loader ) {
249 throw new InvalidArgumentException(
250 "MessageGroupLoader - $loader must implement the " .
251 "MessageGroupLoader interface."
255 $this->groupLoaders[] = $loader;
258 return $this->groupLoaders;
268 return array_filter( $this->getGroupLoaders(), static function ( $groupLoader ) {
280 $groups = self::singleton()->getGroups();
281 $id = self::normalizeId( $id );
283 if ( isset( $groups[$id] ) ) {
287 if ( $id !==
'' && $id[0] ===
'!' ) {
288 $dynamic = self::getDynamicGroups();
289 if ( isset( $dynamic[$id] ) ) {
290 return new $dynamic[$id];
303 if ( str_starts_with( $id,
'page-' ) ) {
304 $id = strtr( $id,
'_',
' ' );
307 global $wgTranslateGroupAliases;
308 if ( isset( $wgTranslateGroupAliases[$id] ) ) {
309 $id = $wgTranslateGroupAliases[$id];
315 public static function exists(
string $id ): bool {
316 return (bool)self::getGroup( $id );
324 }, $loader->loadAggregateGroups() );
325 return in_array( $name, $labels,
true );
333 return self::singleton()->getGroups();
346 $id = $group->getId();
348 $id = self::normalizeId( $group );
351 return Services::getInstance()->getMessageGroupReviewStore()->getGroupPriority( $id ) ??
'';
360 public static function setPriority( $group,
string $priority =
'' ): void {
362 $id = $group->getId();
364 $id = self::normalizeId( $group );
367 $priority = $priority ===
'' ? null : $priority;
368 Services::getInstance()->getMessageGroupReviewStore()->setGroupPriority( $id, $priority );
371 public static function isDynamic(
MessageGroup $group ): bool {
372 $id = $group->getId();
374 return ( $id[0] ??
null ) ===
'!';
386 $keys = array_keys( $group->getDefinitions() );
387 $title = Title::makeTitle( $group->
getNamespace(), $keys[0] );
389 $ids = $handle->getGroupIds();
390 foreach ( $ids as $index => $id ) {
391 if ( $id === $group->
getId() ) {
392 unset( $ids[$index] );
405 $ids = self::getSharedGroups( $targetGroup );
410 $targetId = $targetGroup->
getId();
415 $structure = self::getGroupStructure();
416 foreach ( $structure as $index => $group ) {
418 unset( $structure[$index] );
420 $structure[$index] = array_shift( $group );
430 $pathFinder =
static function ( &$paths, $group, $targetId, $prefix =
'' )
431 use ( &$pathFinder ) {
433 foreach ( $group->getGroups() as $subgroup ) {
434 $subId = $subgroup->getId();
435 if ( $subId === $targetId ) {
440 $pathFinder( $paths, $subgroup, $targetId,
"$prefix|$subId" );
446 foreach ( $ids as $id ) {
448 $group = self::getGroup( $id );
455 foreach ( $structure as $rootGroup ) {
457 if ( $rootGroup->getId() === $group->getId() ) {
459 $pathFinder( $paths, $rootGroup, $targetId, $id );
466 return array_map(
static function (
string $pathString ): array {
467 return explode(
'|', $pathString );
471 public static function singleton(): self {
473 if ( !$instance instanceof
self ) {
474 $instance =
new self();
488 return $this->groups;
499 public static function getGroupsById( array $ids,
bool $skipMeta =
false ): array {
501 foreach ( $ids as $id ) {
502 $group = self::getGroup( $id );
504 if ( $group !==
null ) {
505 if ( $skipMeta && $group->isMeta() ) {
508 $groups[$id] = $group;
511 wfDebug( __METHOD__ .
": Invalid message group id: $id\n" );
529 foreach ( $ids as $index => $id ) {
531 if ( strcspn( $id,
'*?' ) === strlen( $id ) ) {
532 $g = self::getGroup( $id );
534 $all[] = $g->getId();
536 unset( $ids[$index] );
545 $matcher =
new StringMatcher(
'', $ids );
546 foreach ( self::getAllGroups() as $id => $_ ) {
547 if ( $matcher->matches( $id ) ) {
558 '!recent' =>
'RecentMessageGroup',
559 '!additions' =>
'RecentAdditionsMessageGroup',
560 '!sandbox' =>
'SandboxMessageGroup',
574 $groups = self::getAllGroups();
575 foreach ( $groups as $id => $group ) {
576 if ( !$group instanceof $type ) {
577 unset( $groups[$id] );
595 $groups = self::getAllGroups();
599 foreach ( $groups as $id => $o ) {
600 if ( !$o->exists() ) {
601 unset( $groups[$id], $tree[$id] );
606 foreach ( $o->getGroups() as $sid => $so ) {
607 unset( $tree[$sid] );
612 usort( $tree, [ self::class,
'groupLabelSort' ] );
617 foreach ( $tree as $index => $group ) {
619 $tree[$index] = self::subGroups( $group );
628 array_walk_recursive(
630 static function (
MessageGroup $group ) use ( &$used ) {
631 $used[$group->
getId()] =
true;
634 $unused = array_diff_key( $groups, $used );
636 foreach ( $unused as $index => $group ) {
638 unset( $unused[$index] );
643 $participants = implode(
', ', array_keys( $unused ) );
644 throw new RuntimeException(
"Found cyclic aggregate message groups: $participants" );
652 $al = $a->getLabel();
655 return strcasecmp( $al, $bl );
669 array &$childIds = [],
670 string $fname =
'caller'
672 static $recursionGuard = [];
674 $pid = $parent->
getId();
675 if ( isset( $recursionGuard[$pid] ) ) {
679 $tid = $recursionGuard[$tid];
682 }
while ( $tid !== $pid );
683 $path = implode(
' > ', $path );
684 throw new RuntimeException(
"Found cyclic aggregate message groups: $path" );
688 $tree = array_values( $parent->
getGroups() );
689 usort( $tree, [ self::class,
'groupLabelSort' ] );
691 foreach ( $tree as $index => $group ) {
693 $sid = $group->
getId();
694 $recursionGuard[$pid] = $sid;
695 $tree[$index] = self::subGroups( $group, $childIds, __METHOD__ );
696 unset( $recursionGuard[$pid] );
703 array_unshift( $tree, $parent );
705 if ( $fname !== __METHOD__ ) {
707 $childIds = array_values( $childIds );
721 foreach ( $groups as $group ) {
722 $language = $group->getSourceLanguage();
723 if ( $seen ===
'' ) {
725 } elseif ( $language !== $seen ) {
749 $groupId = $group->
getId();
750 $cacheKey =
"$groupId:$targetLanguage";
752 if ( !isset( $cache[$cacheKey] ) ) {
753 $supportedLanguages = Utilities::getLanguageNames(
'en' );
756 $included = isset( $inclusionList[$targetLanguage] );
757 $excluded = TranslateMetadata::isExcluded( $groupId, $targetLanguage );
759 $cache[$cacheKey] = [
760 'relevant' => $included && !$excluded,
764 $groupTags = $group->
getTags();
765 foreach ( [
'ignored',
'optional' ] as $tag ) {
766 if ( isset( $groupTags[$tag] ) ) {
767 foreach ( $groupTags[$tag] as $key ) {
769 $cache[$cacheKey][
'tags'][ucfirst( $key )] =
true;
775 return $cache[$cacheKey][
'relevant'] &&
776 !isset( $cache[$cacheKey][
'tags'][ucfirst( $handle->
getKey() )] );
780class_alias( MessageGroups::class,
'MessageGroups' );
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Loads AggregateMessageGroup, and handles the related cache.
Groups multiple message groups together as one group.
getGroups()
Returns a list of message groups that this group consists of.
This class implements some basic functions that wrap around the YAML message group configurations.
getLabel(IContextSource $context=null)
Returns the human readable label (as plain text).
getId()
Returns the unique identifier for this group.
An abstract class to be implemented by group loaders / stores.
Class for pointing to messages, like Title class is for titles.
getGroup()
Get the primary MessageGroup this message belongs to.
isValid()
Checks if the handle corresponds to a known message.
getKey()
Returns the identified or guessed message key.
To be implemented by MessageGroupLoaders that use the MessageGroupWANCache.
Interface for message groups.
getTranslatableLanguages()
Get all the translatable languages for a group, considering the inclusion and exclusion list.
getTags( $type=null)
Returns message tags.
getNamespace()
Returns the namespace where messages are placed.
getId()
Returns the unique identifier for this group.
getLabel(IContextSource $context=null)
Returns the human readable label (as plain text).