summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/MessageGroups.php
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Translate/MessageGroups.php
first commit
Diffstat (limited to 'www/wiki/extensions/Translate/MessageGroups.php')
-rw-r--r--www/wiki/extensions/Translate/MessageGroups.php978
1 files changed, 978 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/MessageGroups.php b/www/wiki/extensions/Translate/MessageGroups.php
new file mode 100644
index 00000000..ba9c1cb9
--- /dev/null
+++ b/www/wiki/extensions/Translate/MessageGroups.php
@@ -0,0 +1,978 @@
+<?php
+/**
+ * This file contains a class for working with message groups.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2008-2013, Niklas Laxström, Siebrand Mazeland
+ * @license GPL-2.0-or-later
+ */
+use \MediaWiki\MediaWikiServices;
+
+/**
+ * Factory class for accessing message groups individually by id or
+ * all of them as an list.
+ * @todo Clean up the mixed static/member method interface.
+ */
+class MessageGroups {
+ /**
+ * @var string[]|null Cache for message group priorities
+ */
+ protected static $prioritycache;
+
+ /**
+ * @var MessageGroup[]|null Map of (group ID => MessageGroup)
+ */
+ protected $groups;
+
+ /**
+ * @var WANObjectCache|null
+ */
+ protected $cache;
+
+ /**
+ * Tracks the current cache verison. Update this when there are incompatible changes
+ * with the last version of the cache to force a new key to be used. The older cache
+ * will automatically expire and be cleared off.
+ * @var int
+ */
+ const CACHE_VERSION = 2;
+
+ /**
+ * Initialises the list of groups
+ */
+ protected function init() {
+ if ( is_array( $this->groups ) ) {
+ return; // groups already initialized
+ }
+
+ $value = $this->getCachedGroupDefinitions();
+ $groups = $value['cc'];
+
+ $this->initGroupsFromDefinitions( $groups );
+ }
+
+ /**
+ * @param bool|string $recache Either "recache" or false
+ * @return array
+ */
+ protected function getCachedGroupDefinitions( $recache = false ) {
+ global $wgAutoloadClasses, $wgVersion;
+
+ $regenerator = function () {
+ global $wgAutoloadClasses;
+
+ $groups = $deps = $autoload = [];
+ // This constructs the list of all groups from multiple different sources.
+ // When possible, a cache dependency is created to automatically recreate
+ // the cache when configuration changes.
+ Hooks::run( 'TranslatePostInitGroups', [ &$groups, &$deps, &$autoload ] );
+ // Register autoloaders for this request, both values modified by reference
+ self::appendAutoloader( $autoload, $wgAutoloadClasses );
+
+ $value = [
+ 'ts' => wfTimestamp( TS_MW ),
+ 'cc' => $groups,
+ 'autoload' => $autoload
+ ];
+ $wrapper = new DependencyWrapper( $value, $deps );
+ $wrapper->initialiseDeps();
+
+ return $wrapper; // save the new value to cache
+ };
+
+ $cache = $this->getCache();
+ /** @var DependencyWrapper $wrapper */
+ $wrapper = $cache->getWithSetCallback(
+ self::getCacheKey(),
+ $cache::TTL_DAY,
+ $regenerator,
+ [
+ 'lockTSE' => 30, // avoid stampedes (mutex)
+ 'checkKeys' => [ self::getCacheKey() ],
+ 'touchedCallback' => function ( $value ) {
+ return ( $value instanceof DependencyWrapper && $value->isExpired() )
+ ? time() // treat value as if it just expired (for "lockTSE")
+ : null;
+ },
+ 'minAsOf' => $recache ? INF : $cache::MIN_TIMESTAMP_NONE, // "miss" on recache
+ ]
+ );
+
+ // B/C for "touchedCallback" param not existing
+ if ( version_compare( $wgVersion, '1.33', '<' ) && $wrapper->isExpired() ) {
+ $wrapper = $regenerator();
+ $cache->set( self::getCacheKey(), $wrapper, $cache::TTL_DAY );
+ }
+
+ $value = $wrapper->getValue();
+ self::appendAutoloader( $value['autoload'], $wgAutoloadClasses );
+
+ return $value;
+ }
+
+ /**
+ * Expand process cached groups to objects
+ *
+ * @param array $groups Map of (group ID => mixed)
+ */
+ protected function initGroupsFromDefinitions( $groups ) {
+ foreach ( $groups as $id => $mixed ) {
+ if ( !is_object( $mixed ) ) {
+ $groups[$id] = call_user_func( $mixed, $id );
+ }
+ }
+
+ $this->groups = $groups;
+ }
+
+ /**
+ * Immediately update the cache.
+ *
+ * @since 2015.04
+ */
+ public function recache() {
+ // Purge the value from all datacenters
+ $cache = $this->getCache();
+ $cache->touchCheckKey( self::getCacheKey() );
+ // Reload the cache value and update the local datacenter
+ $value = $this->getCachedGroupDefinitions( 'recache' );
+ $groups = $value['cc'];
+
+ $this->clearProcessCache();
+ $this->initGroupsFromDefinitions( $groups );
+ }
+
+ /**
+ * Manually reset group cache.
+ *
+ * Use when automatic dependency tracking fails.
+ */
+ public static function clearCache() {
+ $self = self::singleton();
+
+ $cache = $self->getCache();
+ $cache->delete( self::getCacheKey(), 1 );
+
+ $self->clearProcessCache();
+ }
+
+ /**
+ * Manually reset the process cache.
+ *
+ * This is helpful for long running scripts where the process cache might get stale
+ * even though the global cache is updated.
+ * @since 2016.08
+ */
+ public function clearProcessCache() {
+ $this->groups = null;
+ }
+
+ /**
+ * Returns a cacher object.
+ *
+ * @return WANObjectCache
+ */
+ protected function getCache() {
+ if ( $this->cache === null ) {
+ return MediaWikiServices::getInstance()->getMainWANObjectCache();
+ } else {
+ return $this->cache;
+ }
+ }
+
+ /**
+ * Override cache, for example during tests.
+ *
+ * @param WANObjectCache|null $cache
+ */
+ public function setCache( WANObjectCache $cache = null ) {
+ $this->cache = $cache;
+ }
+
+ /**
+ * Returns the cache key.
+ *
+ * @return string
+ */
+ protected static function getCacheKey() {
+ $self = self::singleton();
+ $cache = $self->getCache();
+
+ return $cache->makeKey( 'translate-groups', 'v' . self::CACHE_VERSION );
+ }
+
+ /**
+ * Safely merges first array to second array, throwing warning on duplicates and removing
+ * duplicates from the first array.
+ * @param array &$additions Things to append
+ * @param array &$to Where to append
+ */
+ protected static function appendAutoloader( array &$additions, array &$to ) {
+ foreach ( $additions as $class => $file ) {
+ if ( isset( $to[$class] ) && $to[$class] !== $file ) {
+ $msg = "Autoload conflict for $class: {$to[$class]} !== $file";
+ trigger_error( $msg, E_USER_WARNING );
+ continue;
+ }
+
+ $to[$class] = $file;
+ }
+ }
+
+ /**
+ * Hook: TranslatePostInitGroups
+ * @param array &$groups
+ * @param array &$deps
+ * @param array &$autoload
+ */
+ public static function getTranslatablePages( array &$groups, array &$deps, array &$autoload ) {
+ global $wgEnablePageTranslation;
+
+ $deps[] = new GlobalDependency( 'wgEnablePageTranslation' );
+
+ if ( !$wgEnablePageTranslation ) {
+ return;
+ }
+
+ $db = TranslateUtils::getSafeReadDB();
+
+ $tables = [ 'page', 'revtag' ];
+ $vars = [ 'page_id', 'page_namespace', 'page_title' ];
+ $conds = [ 'page_id=rt_page', 'rt_type' => RevTag::getType( 'tp:mark' ) ];
+ $options = [ 'GROUP BY' => 'rt_page' ];
+ $res = $db->select( $tables, $vars, $conds, __METHOD__, $options );
+
+ foreach ( $res as $r ) {
+ $title = Title::newFromRow( $r );
+ $id = TranslatablePage::getMessageGroupIdFromTitle( $title );
+ $groups[$id] = new WikiPageMessageGroup( $id, $title );
+ }
+ }
+
+ /**
+ * Hook: TranslatePostInitGroups
+ * @param array &$groups
+ * @param array &$deps
+ * @param array &$autoload
+ */
+ public static function getConfiguredGroups( array &$groups, array &$deps, array &$autoload ) {
+ global $wgTranslateGroupFiles;
+
+ $deps[] = new GlobalDependency( 'wgTranslateGroupFiles' );
+
+ $parser = new MessageGroupConfigurationParser();
+ foreach ( $wgTranslateGroupFiles as $configFile ) {
+ $deps[] = new FileDependency( realpath( $configFile ) );
+
+ $yaml = file_get_contents( $configFile );
+ $fgroups = $parser->getHopefullyValidConfigurations(
+ $yaml,
+ function ( $index, $config, $errmsg ) use ( $configFile ) {
+ trigger_error( "Document $index in $configFile is invalid: $errmsg", E_USER_WARNING );
+ }
+ );
+
+ foreach ( $fgroups as $id => $conf ) {
+ if ( !empty( $conf['AUTOLOAD'] ) && is_array( $conf['AUTOLOAD'] ) ) {
+ $dir = dirname( $configFile );
+ $additions = array_map( function ( $file ) use ( $dir ) {
+ return "$dir/$file";
+ }, $conf['AUTOLOAD'] );
+ self::appendAutoloader( $additions, $autoload );
+ }
+
+ $groups[$id] = MessageGroupBase::factory( $conf );
+ }
+ }
+ }
+
+ /**
+ * Hook: TranslatePostInitGroups
+ * @param array &$groups
+ * @param array &$deps
+ * @param array &$autoload
+ */
+ public static function getWorkflowGroups( array &$groups, array &$deps, array &$autoload ) {
+ global $wgTranslateWorkflowStates;
+
+ $deps[] = new GlobalDependency( 'wgTranslateWorkflowStates' );
+
+ if ( $wgTranslateWorkflowStates ) {
+ $groups['translate-workflow-states'] = new WorkflowStatesMessageGroup();
+ }
+ }
+
+ /**
+ * Hook: TranslatePostInitGroups
+ * @param array &$groups
+ * @param array &$deps
+ * @param array &$autoload
+ */
+ public static function getAggregateGroups( array &$groups, array &$deps, array &$autoload ) {
+ $groups += self::loadAggregateGroups();
+ }
+
+ /**
+ * Hook: TranslatePostInitGroups
+ * @param array &$groups
+ * @param array &$deps
+ * @param array &$autoload
+ */
+ public static function getCCGroups( array &$groups, array &$deps, array &$autoload ) {
+ global $wgTranslateCC;
+
+ if ( $wgTranslateCC !== [] ) {
+ wfDeprecated( '$wgTranslateCC' );
+ }
+
+ $deps[] = new GlobalDependency( 'wgTranslateCC' );
+
+ $groups += $wgTranslateCC;
+ }
+
+ /**
+ * Fetch a message group by id.
+ *
+ * @param string $id Message group id.
+ * @return MessageGroup|null if it doesn't exist.
+ */
+ public static function getGroup( $id ) {
+ $groups = self::singleton()->getGroups();
+ $id = self::normalizeId( $id );
+
+ if ( isset( $groups[$id] ) ) {
+ return $groups[$id];
+ }
+
+ if ( (string)$id !== '' && $id[0] === '!' ) {
+ $dynamic = self::getDynamicGroups();
+ if ( isset( $dynamic[$id] ) ) {
+ return new $dynamic[$id];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Fixes the id and resolves aliases.
+ *
+ * @param string $id
+ * @return string
+ * @since 2016.01
+ */
+ public static function normalizeId( $id ) {
+ /* Translatable pages use spaces, but MW occasionally likes to
+ * normalize spaces to underscores */
+ if ( strpos( $id, 'page-' ) === 0 ) {
+ $id = strtr( $id, '_', ' ' );
+ }
+
+ global $wgTranslateGroupAliases;
+ if ( isset( $wgTranslateGroupAliases[$id] ) ) {
+ $id = $wgTranslateGroupAliases[$id];
+ }
+
+ return $id;
+ }
+
+ /**
+ * @param string $id
+ * @return bool
+ */
+ public static function exists( $id ) {
+ return (bool)self::getGroup( $id );
+ }
+
+ /**
+ * Check if a particular aggregate group label exists
+ * @param string $name
+ * @return bool
+ */
+ public static function labelExists( $name ) {
+ $groups = self::loadAggregateGroups();
+ $labels = array_map( function ( $g ) {
+ /** @var MessageGroup $g */
+ return $g->getLabel();
+ }, $groups );
+ return (bool)in_array( $name, $labels, true );
+ }
+
+ /**
+ * Get all enabled message groups.
+ * @return MessageGroup[] Map of (string => MessageGroup)
+ */
+ public static function getAllGroups() {
+ return self::singleton()->getGroups();
+ }
+
+ /**
+ * We want to de-emphasize time sensitive groups like news for 2009.
+ * They can still exist in the system, but should not appear in front
+ * of translators looking to do some useful work.
+ *
+ * @param MessageGroup|string $group Message group ID
+ * @return string Message group priority
+ * @since 2011-12-12
+ */
+ public static function getPriority( $group ) {
+ if ( !isset( self::$prioritycache ) ) {
+ self::$prioritycache = [];
+ // Abusing this table originally intented for other purposes
+ $db = wfGetDB( DB_REPLICA );
+ $table = 'translate_groupreviews';
+ $fields = [ 'tgr_group', 'tgr_state' ];
+ $conds = [ 'tgr_lang' => '*priority' ];
+ $res = $db->select( $table, $fields, $conds, __METHOD__ );
+ foreach ( $res as $row ) {
+ self::$prioritycache[$row->tgr_group] = $row->tgr_state;
+ }
+ }
+
+ if ( $group instanceof MessageGroup ) {
+ $id = $group->getId();
+ } else {
+ $id = self::normalizeId( $group );
+ }
+
+ return self::$prioritycache[$id] ?? '';
+ }
+
+ /**
+ * Sets the message group priority.
+ *
+ * @param MessageGroup|string $group Message group
+ * @param string $priority Priority (empty string to unset)
+ * @since 2013-03-01
+ */
+ public static function setPriority( $group, $priority = '' ) {
+ if ( $group instanceof MessageGroup ) {
+ $id = $group->getId();
+ } else {
+ $id = self::normalizeId( $group );
+ }
+
+ self::$prioritycache[$id] = $priority;
+
+ $dbw = wfGetDB( DB_MASTER );
+ $table = 'translate_groupreviews';
+ $row = [
+ 'tgr_group' => $id,
+ 'tgr_lang' => '*priority',
+ 'tgr_state' => $priority,
+ ];
+
+ if ( $priority === '' ) {
+ unset( $row['tgr_state'] );
+ $dbw->delete( $table, $row, __METHOD__ );
+ } else {
+ $index = [ 'tgr_group', 'tgr_lang' ];
+ $dbw->replace( $table, [ $index ], $row, __METHOD__ );
+ }
+ }
+
+ /**
+ * @since 2011-12-28
+ * @param MessageGroup $group
+ * @return bool
+ */
+ public static function isDynamic( MessageGroup $group ) {
+ $id = $group->getId();
+
+ return (string)$id !== '' && $id[0] === '!';
+ }
+
+ /**
+ * Returns a list of message groups that share (certain) messages
+ * with this group.
+ * @since 2011-12-25; renamed in 2012-12-10 from getParentGroups.
+ * @param MessageGroup $group
+ * @return string[]
+ */
+ public static function getSharedGroups( MessageGroup $group ) {
+ // Take the first message, get a handle for it and check
+ // if that message belongs to other groups. Those are the
+ // parent aggregate groups. Ideally we loop over all keys,
+ // but this should be enough.
+ $keys = array_keys( $group->getDefinitions() );
+ $title = Title::makeTitle( $group->getNamespace(), $keys[0] );
+ $handle = new MessageHandle( $title );
+ $ids = $handle->getGroupIds();
+ foreach ( $ids as $index => $id ) {
+ if ( $id === $group->getId() ) {
+ unset( $ids[$index] );
+ }
+ }
+
+ return $ids;
+ }
+
+ /**
+ * Returns a list of parent message groups. If message group exists
+ * in multiple places in the tree, multiple lists are returned.
+ * @since 2012-12-10
+ * @param MessageGroup $targetGroup
+ * @return array[]
+ */
+ public static function getParentGroups( MessageGroup $targetGroup ) {
+ $ids = self::getSharedGroups( $targetGroup );
+ if ( $ids === [] ) {
+ return [];
+ }
+
+ $targetId = $targetGroup->getId();
+
+ /* Get the group structure. We will be using this to find which
+ * of our candidates are top-level groups. Prefilter it to only
+ * contain aggregate groups. */
+ $structure = self::getGroupStructure();
+ foreach ( $structure as $index => $group ) {
+ if ( $group instanceof MessageGroup ) {
+ unset( $structure[$index] );
+ } else {
+ $structure[$index] = array_shift( $group );
+ }
+ }
+
+ /* Now that we have all related groups, use them to find all paths
+ * from top-level groups to target group with any number of subgroups
+ * in between. */
+ $paths = [];
+
+ /* This function recursively finds paths to the target group */
+ $pathFinder = function ( &$paths, $group, $targetId, $prefix = '' )
+ use ( &$pathFinder ) {
+ if ( $group instanceof AggregateMessageGroup ) {
+ /**
+ * @var MessageGroup $subgroup
+ */
+ foreach ( $group->getGroups() as $subgroup ) {
+ $subId = $subgroup->getId();
+ if ( $subId === $targetId ) {
+ $paths[] = $prefix;
+ continue;
+ }
+
+ $pathFinder( $paths, $subgroup, $targetId, "$prefix|$subId" );
+ }
+ }
+ };
+
+ // Iterate over the top-level groups only
+ foreach ( $ids as $id ) {
+ // First, find a top level groups
+ $group = self::getGroup( $id );
+
+ // Quick escape for leaf groups
+ if ( !$group instanceof AggregateMessageGroup ) {
+ continue;
+ }
+
+ foreach ( $structure as $rootGroup ) {
+ /**
+ * @var MessageGroup $rootGroup
+ */
+ if ( $rootGroup->getId() === $group->getId() ) {
+ // Yay we found a top-level group
+ $pathFinder( $paths, $rootGroup, $targetId, $id );
+ break; // No we have one or more paths appended into $paths
+ }
+ }
+ }
+
+ // And finally explode the strings
+ foreach ( $paths as $index => $pathString ) {
+ $paths[$index] = explode( '|', $pathString );
+ }
+
+ return $paths;
+ }
+
+ /**
+ * Constructor function.
+ * @return self
+ */
+ public static function singleton() {
+ static $instance;
+ if ( !$instance instanceof self ) {
+ $instance = new self();
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Get all enabled non-dynamic message groups.
+ *
+ * @return MessageGroup[] Map of (group ID => MessageGroup)
+ */
+ public function getGroups() {
+ $this->init();
+
+ return $this->groups;
+ }
+
+ /**
+ * Get message groups for corresponding message group ids.
+ *
+ * @param string[] $ids Group IDs
+ * @param bool $skipMeta Skip aggregate message groups
+ * @return MessageGroup[]
+ * @since 2012-02-13
+ */
+ public static function getGroupsById( array $ids, $skipMeta = false ) {
+ $groups = [];
+ foreach ( $ids as $id ) {
+ $group = self::getGroup( $id );
+
+ if ( $group !== null ) {
+ if ( $skipMeta && $group->isMeta() ) {
+ continue;
+ } else {
+ $groups[$id] = $group;
+ }
+ } else {
+ wfDebug( __METHOD__ . ": Invalid message group id: $id\n" );
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * If the list of message group ids contains wildcards, this function will match
+ * them against the list of all supported message groups and return matched
+ * message group ids.
+ * @param string[]|string $ids
+ * @return string[]
+ * @since 2012-02-13
+ */
+ public static function expandWildcards( $ids ) {
+ $all = [];
+
+ $ids = (array)$ids;
+ foreach ( $ids as $index => $id ) {
+ // Fast path, no wildcards
+ if ( strcspn( $id, '*?' ) === strlen( $id ) ) {
+ $g = self::getGroup( $id );
+ if ( $g ) {
+ $all[] = $g->getId();
+ }
+ unset( $ids[$index] );
+ }
+ }
+
+ if ( $ids === [] ) {
+ return $all;
+ }
+
+ // Slow path for the ones with wildcards
+ $matcher = new StringMatcher( '', $ids );
+ foreach ( self::getAllGroups() as $id => $_ ) {
+ if ( $matcher->match( $id ) ) {
+ $all[] = $id;
+ }
+ }
+
+ return $all;
+ }
+
+ /**
+ * Contents on these groups changes on a whim.
+ * @since 2011-12-28
+ * @return array
+ */
+ public static function getDynamicGroups() {
+ return [
+ '!recent' => 'RecentMessageGroup',
+ '!additions' => 'RecentAdditionsMessageGroup',
+ '!sandbox' => 'SandboxMessageGroup',
+ ];
+ }
+
+ /**
+ * Get only groups of specific type (class).
+ * @param string $type Class name of wanted type
+ * @return MessageGroupBase[] Map of (group ID => MessageGroupBase)
+ * @since 2012-04-30
+ */
+ public static function getGroupsByType( $type ) {
+ $groups = self::getAllGroups();
+ foreach ( $groups as $id => $group ) {
+ if ( !$group instanceof $type ) {
+ unset( $groups[$id] );
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Returns a tree of message groups. First group in each subgroup is
+ * the aggregate group. Groups can be nested infinitely, though in practice
+ * other code might not handle more than two (or even one) nesting levels.
+ * One group can exist multiple times in differents parts of the tree.
+ * In other words: [Group1, Group2, [AggGroup, Group3, Group4]]
+ *
+ * @throws MWException If cyclic structure is detected.
+ * @return array Map of (group ID => MessageGroup or recursive array)
+ */
+ public static function getGroupStructure() {
+ $groups = self::getAllGroups();
+
+ // Determine the top level groups of the tree
+ $tree = $groups;
+ /**
+ * @var MessageGroup $o
+ */
+ foreach ( $groups as $id => $o ) {
+ if ( !$o->exists() ) {
+ unset( $groups[$id], $tree[$id] );
+ continue;
+ }
+
+ if ( $o instanceof AggregateMessageGroup ) {
+ /**
+ * @var AggregateMessageGroup $o
+ */
+ foreach ( $o->getGroups() as $sid => $so ) {
+ unset( $tree[$sid] );
+ }
+ }
+ }
+
+ // Work around php bug: https://bugs.php.net/bug.php?id=50688
+ // Triggered by ApiQueryMessageGroups for example
+ Wikimedia\suppressWarnings();
+ usort( $tree, [ __CLASS__, 'groupLabelSort' ] );
+ Wikimedia\restoreWarnings();
+
+ /* Now we have two things left in $tree array:
+ * - solitaries: top-level non-aggregate message groups
+ * - top-level aggregate message groups */
+ foreach ( $tree as $index => $group ) {
+ if ( $group instanceof AggregateMessageGroup ) {
+ $tree[$index] = self::subGroups( $group );
+ }
+ }
+
+ /* Essentially we are done now. Cyclic groups can cause part of the
+ * groups not be included at all, because they have all unset each
+ * other in the first loop. So now we check if there are groups left
+ * over. */
+ $used = [];
+ // Hack to allow passing by reference
+ array_walk_recursive( $tree, [ __CLASS__, 'collectGroupIds' ], [ &$used ] );
+ $unused = array_diff( array_keys( $groups ), array_keys( $used ) );
+ if ( count( $unused ) ) {
+ foreach ( $unused as $index => $id ) {
+ if ( !$groups[$id] instanceof AggregateMessageGroup ) {
+ unset( $unused[$index] );
+ }
+ }
+
+ // Only list the aggregate groups, other groups cannot cause cycles
+ $participants = implode( ', ', $unused );
+ throw new MWException( "Found cyclic aggregate message groups: $participants" );
+ }
+
+ return $tree;
+ }
+
+ /**
+ * See getGroupStructure, just collects ids into array
+ * @param MessageGroup $value
+ * @param string $key
+ * @param bool $used
+ */
+ public static function collectGroupIds( MessageGroup $value, $key, $used ) {
+ $used[0][$value->getId()] = true;
+ }
+
+ /**
+ * Sorts groups by label value
+ * @param MessageGroup $a
+ * @param MessageGroup $b
+ * @return int
+ */
+ public static function groupLabelSort( $a, $b ) {
+ $al = $a->getLabel();
+ $bl = $b->getLabel();
+
+ return strcasecmp( $al, $bl );
+ }
+
+ /**
+ * Like getGroupStructure but start from one root which must be an
+ * AggregateMessageGroup.
+ *
+ * @param AggregateMessageGroup $parent
+ * @param string[] &$childIds Flat list of child group IDs [returned]
+ * @param string $fname Calling method name; used to identify recursion [optional]
+ * @throws MWException
+ * @return array
+ * @since Public since 2012-11-29
+ */
+ public static function subGroups(
+ AggregateMessageGroup $parent,
+ array &$childIds = [],
+ $fname = 'caller'
+) {
+ static $recursionGuard = [];
+
+ $pid = $parent->getId();
+ if ( isset( $recursionGuard[$pid] ) ) {
+ $tid = $pid;
+ $path = [ $tid ];
+ do {
+ $tid = $recursionGuard[$tid];
+ $path[] = $tid;
+ // Until we have gone full cycle
+ } while ( $tid !== $pid );
+ $path = implode( ' > ', $path );
+ throw new MWException( "Found cyclic aggregate message groups: $path" );
+ }
+
+ // We don't care about the ids.
+ $tree = array_values( $parent->getGroups() );
+ usort( $tree, [ __CLASS__, 'groupLabelSort' ] );
+ // Expand aggregate groups (if any left) after sorting to form a tree
+ foreach ( $tree as $index => $group ) {
+ if ( $group instanceof AggregateMessageGroup ) {
+ $sid = $group->getId();
+ $recursionGuard[$pid] = $sid;
+ $tree[$index] = self::subGroups( $group, $childIds, __METHOD__ );
+ unset( $recursionGuard[$pid] );
+
+ $childIds[$sid] = 1;
+ }
+ }
+
+ // Parent group must be first item in the array
+ array_unshift( $tree, $parent );
+
+ if ( $fname !== __METHOD__ ) {
+ // Move the IDs from the keys to the value for final return
+ $childIds = array_values( $childIds );
+ }
+
+ return $tree;
+ }
+
+ /**
+ * Checks whether all the message groups have the same source language.
+ * @param array $groups A list of message groups objects.
+ * @return string Language code if the languages are the same, empty string otherwise.
+ * @since 2013.09
+ */
+ public static function haveSingleSourceLanguage( array $groups ) {
+ $seen = '';
+
+ foreach ( $groups as $group ) {
+ $language = $group->getSourceLanguage();
+ if ( $seen === '' ) {
+ $seen = $language;
+ } elseif ( $language !== $seen ) {
+ return '';
+ }
+ }
+
+ return $seen;
+ }
+
+ /**
+ * Get all the aggregate messages groups defined in translate_metadata table.
+ *
+ * @return MessageGroup[]
+ */
+ protected static function loadAggregateGroups() {
+ $dbr = TranslateUtils::getSafeReadDB();
+ $tables = [ 'translate_metadata' ];
+ $field = 'tmd_group';
+ $conds = [ 'tmd_key' => 'subgroups' ];
+ $groupIds = $dbr->selectFieldValues( $tables, $field, $conds, __METHOD__ );
+ TranslateMetadata::preloadGroups( $groupIds );
+
+ $groups = [];
+ foreach ( $groupIds as $id ) {
+ $conf = [];
+ $conf['BASIC'] = [
+ 'id' => $id,
+ 'label' => TranslateMetadata::get( $id, 'name' ),
+ 'description' => TranslateMetadata::get( $id, 'description' ),
+ 'meta' => 1,
+ 'class' => 'AggregateMessageGroup',
+ 'namespace' => NS_TRANSLATIONS,
+ ];
+ $conf['GROUPS'] = TranslateMetadata::getSubgroups( $id );
+ $group = MessageGroupBase::factory( $conf );
+
+ $groups[$id] = $group;
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Filters out messages that should not be translated under normal
+ * conditions.
+ *
+ * @param MessageHandle $handle Handle for the translation target.
+ * @return bool
+ * @since 2013.10
+ */
+ public static function isTranslatableMessage( MessageHandle $handle ) {
+ static $cache = [];
+
+ if ( !$handle->isValid() ) {
+ return false;
+ }
+
+ $group = $handle->getGroup();
+ $groupId = $group->getId();
+ $language = $handle->getCode();
+ $cacheKey = "$groupId:$language";
+
+ if ( !isset( $cache[$cacheKey] ) ) {
+ $allowed = true;
+ $discouraged = false;
+
+ $whitelist = $group->getTranslatableLanguages();
+ if ( is_array( $whitelist ) && !isset( $whitelist[$language] ) ) {
+ $allowed = false;
+ }
+
+ if ( self::getPriority( $group ) === 'discouraged' ) {
+ $discouraged = true;
+ } else {
+ $priorityLanguages = TranslateMetadata::get( $groupId, 'prioritylangs' );
+ if ( $priorityLanguages ) {
+ $map = array_flip( explode( ',', $priorityLanguages ) );
+ if ( !isset( $map[$language] ) ) {
+ $discouraged = true;
+ }
+ }
+ }
+
+ $cache[$cacheKey] = [
+ 'relevant' => $allowed && !$discouraged,
+ 'tags' => [],
+ ];
+
+ $groupTags = $group->getTags();
+ foreach ( [ 'ignored', 'optional' ] as $tag ) {
+ if ( isset( $groupTags[$tag] ) ) {
+ foreach ( $groupTags[$tag] as $key ) {
+ // TODO: ucfirst should not be here
+ $cache[$cacheKey]['tags'][ucfirst( $key )] = true;
+ }
+ }
+ }
+ }
+
+ return $cache[$cacheKey]['relevant'] &&
+ !isset( $cache[$cacheKey]['tags'][ucfirst( $handle->getKey() )] );
+ }
+}