2declare( strict_types = 1 );
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
10use InvalidArgumentException;
13use MediaWiki\MediaWikiServices;
35 private static $prioritycache;
39 private $groupLoaders;
49 private const CACHE_VERSION = 4;
52 protected function init(): void {
53 if ( is_array( $this->groups ) ) {
58 $groups = $value[
'cc'];
61 $groups += $loader->getGroups();
68 global $wgAutoloadClasses;
70 $regenerator =
function () {
71 global $wgAutoloadClasses;
73 $groups = $deps = $autoload = [];
78 MediaWikiServices::getInstance()
80 ->run(
'TranslatePostInitGroups', [ &$groups, &$deps, &$autoload ] );
82 self::appendAutoloader( $autoload, $wgAutoloadClasses );
85 'ts' => wfTimestamp( TS_MW ),
87 'autoload' => $autoload
89 $wrapper =
new DependencyWrapper( $value, $deps );
90 $wrapper->initialiseDeps();
95 $cache = $this->getCache();
97 $wrapper = $cache->getWithSetCallback(
103 'checkKeys' => [ $this->getCacheKey() ],
104 'touchedCallback' =>
static function ( $value ) {
105 return ( $value instanceof DependencyWrapper && $value->isExpired() )
109 'minAsOf' => $recache ? INF : $cache::MIN_TIMESTAMP_NONE,
113 $value = $wrapper->getValue();
114 self::appendAutoloader( $value[
'autoload'], $wgAutoloadClasses );
125 foreach ( $groups as $id => $mixed ) {
126 if ( !is_object( $mixed ) ) {
127 $groups[$id] = call_user_func( $mixed, $id );
131 $this->groups = $groups;
137 $cache = $this->getCache();
138 $cache->touchCheckKey( $this->getCacheKey() );
140 $this->clearProcessCache();
142 foreach ( $this->getCacheGroupLoaders() as $cacheLoader ) {
143 $cacheLoader->recache();
147 $value = $this->getCachedGroupDefinitions(
'recache' );
148 $groups = $value[
'cc'];
150 foreach ( $this->getGroupLoaders() as $loader ) {
151 $groups += $loader->getGroups();
154 $this->initGroupsFromDefinitions( $groups );
163 $self = self::singleton();
165 $cache = $self->getCache();
166 $cache->delete( $self->getCacheKey(), 1 );
168 foreach ( $self->getCacheGroupLoaders() as $cacheLoader ) {
169 $cacheLoader->clearCache();
172 $self->clearProcessCache();
182 $this->groups = null;
183 $this->groupLoaders =
null;
185 self::$prioritycache =
null;
188 protected function getCache(): WANObjectCache {
189 if ( $this->cache === null ) {
190 return MediaWikiServices::getInstance()->getMainWANObjectCache();
197 public function setCache( ?WANObjectCache $cache =
null ) {
198 $this->cache = $cache;
203 return $this->getCache()->makeKey(
'translate-groups',
'v' . self::CACHE_VERSION );
213 foreach ( $additions as $class => $file ) {
214 if ( isset( $to[$class] ) && $to[$class] !== $file ) {
215 $msg =
"Autoload conflict for $class: {$to[$class]} !== $file";
216 trigger_error( $msg, E_USER_WARNING );
230 if ( $this->groupLoaders !== null ) {
231 return $this->groupLoaders;
234 $cache = $this->getCache();
236 $groupLoaderInstances = $this->groupLoaders = [];
240 'database' => Utilities::getSafeReadDB(),
244 MediaWikiServices::getInstance()
246 ->run(
'TranslateInitGroupLoaders', [ &$groupLoaderInstances, $deps ] );
248 if ( $groupLoaderInstances === [] ) {
249 return $this->groupLoaders;
252 foreach ( $groupLoaderInstances as $loader ) {
254 throw new InvalidArgumentException(
255 "MessageGroupLoader - $loader must implement the " .
256 "MessageGroupLoader interface."
260 $this->groupLoaders[] = $loader;
263 return $this->groupLoaders;
273 return array_filter( $this->getGroupLoaders(), static function ( $groupLoader ) {
285 $groups = self::singleton()->getGroups();
286 $id = self::normalizeId( $id );
288 if ( isset( $groups[$id] ) ) {
292 if ( $id !==
'' && $id[0] ===
'!' ) {
293 $dynamic = self::getDynamicGroups();
294 if ( isset( $dynamic[$id] ) ) {
295 return new $dynamic[$id];
308 if ( strpos( $id,
'page-' ) === 0 ) {
309 $id = strtr( $id,
'_',
' ' );
312 global $wgTranslateGroupAliases;
313 if ( isset( $wgTranslateGroupAliases[$id] ) ) {
314 $id = $wgTranslateGroupAliases[$id];
320 public static function exists(
string $id ): bool {
321 return (bool)self::getGroup( $id );
329 }, $loader->loadAggregateGroups() );
330 return in_array( $name, $labels,
true );
338 return self::singleton()->getGroups();
350 if ( self::$prioritycache === null ) {
351 self::$prioritycache = [];
353 $dbr = MediaWikiServices::getInstance()
354 ->getDBLoadBalancer()
355 ->getConnection( DB_REPLICA );
357 $res = $dbr->newSelectQueryBuilder()
358 ->select( [
'tgr_group',
'tgr_state' ] )
359 ->from(
'translate_groupreviews' )
360 ->where( [
'tgr_lang' =>
'*priority' ] )
361 ->caller( __METHOD__ )
364 foreach ( $res as $row ) {
365 self::$prioritycache[$row->tgr_group] = $row->tgr_state;
370 $id = $group->getId();
372 $id = self::normalizeId( $group );
375 return self::$prioritycache[$id] ??
'';
384 public static function setPriority( $group,
string $priority =
'' ): void {
386 $id = $group->getId();
388 $id = self::normalizeId( $group );
392 self::$prioritycache[$id] = $priority;
394 $dbw = MediaWikiServices::getInstance()
395 ->getDBLoadBalancer()
396 ->getConnection( DB_PRIMARY );
398 $table =
'translate_groupreviews';
401 'tgr_lang' =>
'*priority',
402 'tgr_state' => $priority
405 if ( $priority ===
'' ) {
406 unset( $row[
'tgr_state'] );
407 $dbw->delete( $table, $row, __METHOD__ );
409 $index = [
'tgr_group',
'tgr_lang' ];
410 $dbw->replace( $table, [ $index ], $row, __METHOD__ );
414 public static function isDynamic(
MessageGroup $group ): bool {
415 $id = $group->getId();
417 return ( $id[0] ??
null ) ===
'!';
429 $keys = array_keys( $group->getDefinitions() );
430 $title = Title::makeTitle( $group->
getNamespace(), $keys[0] );
432 $ids = $handle->getGroupIds();
433 foreach ( $ids as $index => $id ) {
434 if ( $id === $group->
getId() ) {
435 unset( $ids[$index] );
448 $ids = self::getSharedGroups( $targetGroup );
453 $targetId = $targetGroup->
getId();
458 $structure = self::getGroupStructure();
459 foreach ( $structure as $index => $group ) {
461 unset( $structure[$index] );
463 $structure[$index] = array_shift( $group );
473 $pathFinder =
static function ( &$paths, $group, $targetId, $prefix =
'' )
474 use ( &$pathFinder ) {
476 foreach ( $group->getGroups() as $subgroup ) {
477 $subId = $subgroup->getId();
478 if ( $subId === $targetId ) {
483 $pathFinder( $paths, $subgroup, $targetId,
"$prefix|$subId" );
489 foreach ( $ids as $id ) {
491 $group = self::getGroup( $id );
498 foreach ( $structure as $rootGroup ) {
500 if ( $rootGroup->getId() === $group->getId() ) {
502 $pathFinder( $paths, $rootGroup, $targetId, $id );
509 return array_map(
static function (
string $pathString ): array {
510 return explode(
'|', $pathString );
514 public static function singleton(): self {
516 if ( !$instance instanceof
self ) {
517 $instance =
new self();
531 return $this->groups;
542 public static function getGroupsById( array $ids,
bool $skipMeta =
false ): array {
544 foreach ( $ids as $id ) {
545 $group = self::getGroup( $id );
547 if ( $group !==
null ) {
548 if ( $skipMeta && $group->isMeta() ) {
551 $groups[$id] = $group;
554 wfDebug( __METHOD__ .
": Invalid message group id: $id\n" );
572 foreach ( $ids as $index => $id ) {
574 if ( strcspn( $id,
'*?' ) === strlen( $id ) ) {
575 $g = self::getGroup( $id );
577 $all[] = $g->getId();
579 unset( $ids[$index] );
588 $matcher =
new StringMatcher(
'', $ids );
589 foreach ( self::getAllGroups() as $id => $_ ) {
590 if ( $matcher->matches( $id ) ) {
601 '!recent' =>
'RecentMessageGroup',
602 '!additions' =>
'RecentAdditionsMessageGroup',
603 '!sandbox' =>
'SandboxMessageGroup',
617 $groups = self::getAllGroups();
618 foreach ( $groups as $id => $group ) {
619 if ( !$group instanceof $type ) {
620 unset( $groups[$id] );
639 $groups = self::getAllGroups();
643 foreach ( $groups as $id => $o ) {
644 if ( !$o->exists() ) {
645 unset( $groups[$id], $tree[$id] );
650 foreach ( $o->getGroups() as $sid => $so ) {
651 unset( $tree[$sid] );
656 usort( $tree, [ self::class,
'groupLabelSort' ] );
661 foreach ( $tree as $index => $group ) {
663 $tree[$index] = self::subGroups( $group );
672 array_walk_recursive(
674 static function (
MessageGroup $group ) use ( &$used ) {
675 $used[$group->
getId()] =
true;
678 $unused = array_diff_key( $groups, $used );
680 foreach ( $unused as $index => $group ) {
682 unset( $unused[$index] );
687 $participants = implode(
', ', array_keys( $unused ) );
688 throw new MWException(
"Found cyclic aggregate message groups: $participants" );
696 $al = $a->getLabel();
699 return strcasecmp( $al, $bl );
714 array &$childIds = [],
715 string $fname =
'caller'
717 static $recursionGuard = [];
719 $pid = $parent->
getId();
720 if ( isset( $recursionGuard[$pid] ) ) {
724 $tid = $recursionGuard[$tid];
727 }
while ( $tid !== $pid );
728 $path = implode(
' > ', $path );
729 throw new MWException(
"Found cyclic aggregate message groups: $path" );
733 $tree = array_values( $parent->
getGroups() );
734 usort( $tree, [ self::class,
'groupLabelSort' ] );
736 foreach ( $tree as $index => $group ) {
738 $sid = $group->
getId();
739 $recursionGuard[$pid] = $sid;
740 $tree[$index] = self::subGroups( $group, $childIds, __METHOD__ );
741 unset( $recursionGuard[$pid] );
748 array_unshift( $tree, $parent );
750 if ( $fname !== __METHOD__ ) {
752 $childIds = array_values( $childIds );
766 foreach ( $groups as $group ) {
767 $language = $group->getSourceLanguage();
768 if ( $seen ===
'' ) {
770 } elseif ( $language !== $seen ) {
794 $groupId = $group->
getId();
795 $cacheKey =
"$groupId:$targetLanguage";
797 if ( !isset( $cache[$cacheKey] ) ) {
798 $supportedLanguages = Utilities::getLanguageNames(
'en' );
801 $included = isset( $inclusionList[$targetLanguage] );
802 $excluded = TranslateMetadata::isExcluded( $groupId, $targetLanguage );
804 $cache[$cacheKey] = [
805 'relevant' => $included && !$excluded,
809 $groupTags = $group->
getTags();
810 foreach ( [
'ignored',
'optional' ] as $tag ) {
811 if ( isset( $groupTags[$tag] ) ) {
812 foreach ( $groupTags[$tag] as $key ) {
814 $cache[$cacheKey][
'tags'][ucfirst( $key )] =
true;
820 return $cache[$cacheKey][
'relevant'] &&
821 !isset( $cache[$cacheKey][
'tags'][ucfirst( $handle->
getKey() )] );
825class_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:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $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:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, '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(), new RevTagStore(), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(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());}, '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).