summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/messagegroups
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
first commit
Diffstat (limited to 'www/wiki/extensions/Translate/messagegroups')
-rw-r--r--www/wiki/extensions/Translate/messagegroups/AggregateMessageGroup.php180
-rw-r--r--www/wiki/extensions/Translate/messagegroups/FileBasedMessageGroup.php185
-rw-r--r--www/wiki/extensions/Translate/messagegroups/MediaWikiExtensionMessageGroup.php50
-rw-r--r--www/wiki/extensions/Translate/messagegroups/MessageGroup.php171
-rw-r--r--www/wiki/extensions/Translate/messagegroups/MessageGroupBase.php452
-rw-r--r--www/wiki/extensions/Translate/messagegroups/MessageGroupOld.php409
-rw-r--r--www/wiki/extensions/Translate/messagegroups/RecentAdditionsMessageGroup.php66
-rw-r--r--www/wiki/extensions/Translate/messagegroups/RecentMessageGroup.php170
-rw-r--r--www/wiki/extensions/Translate/messagegroups/SandboxMessageGroup.php173
-rw-r--r--www/wiki/extensions/Translate/messagegroups/WikiMessageGroup.php89
-rw-r--r--www/wiki/extensions/Translate/messagegroups/WikiPageMessageGroup.php248
-rw-r--r--www/wiki/extensions/Translate/messagegroups/WorkflowStatesMessageGroup.php77
12 files changed, 2270 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/messagegroups/AggregateMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/AggregateMessageGroup.php
new file mode 100644
index 00000000..040521d5
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/AggregateMessageGroup.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * This file a contains a message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010-2013, Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Groups multiple message groups together as one big group.
+ *
+ * Limitations:
+ * - Only groups in the same namespace.
+ * @ingroup MessageGroup
+ */
+class AggregateMessageGroup extends MessageGroupBase {
+ public function exists() {
+ // Group exists if there are any subgroups.
+ $exists = (bool)$this->conf['GROUPS'];
+
+ return $exists;
+ }
+
+ public function load( $code ) {
+ $messages = [];
+
+ /**
+ * @var $group MessageGroup
+ */
+ foreach ( $this->getGroups() as $group ) {
+ $messages += $group->load( $code );
+ }
+
+ return $messages;
+ }
+
+ public function getMangler() {
+ if ( !isset( $this->mangler ) ) {
+ $this->mangler = StringMatcher::EmptyMatcher();
+ }
+
+ return $this->mangler;
+ }
+
+ public function getGroups() {
+ if ( !isset( $this->groups ) ) {
+ $groups = [];
+ $ids = (array)$this->conf['GROUPS'];
+ $ids = MessageGroups::expandWildcards( $ids );
+
+ foreach ( $ids as $id ) {
+ // Do not try to include self and go to infinite loop.
+ if ( $id === $this->getId() ) {
+ continue;
+ }
+
+ $group = MessageGroups::getGroup( $id );
+ if ( $group === null ) {
+ error_log( "Invalid group id in {$this->getId()}: $id" );
+ continue;
+ }
+
+ if ( MessageGroups::getPriority( $group ) === 'discouraged' ) {
+ continue;
+ }
+
+ $groups[$id] = $group;
+ }
+
+ $this->groups = $groups;
+ }
+
+ return $this->groups;
+ }
+
+ protected function loadMessagesFromCache( $groups ) {
+ $messages = [];
+ foreach ( $groups as $group ) {
+ if ( $group instanceof MessageGroupOld ) {
+ $messages += $group->getDefinitions();
+ continue;
+ }
+
+ if ( $group instanceof self ) {
+ $messages += $this->loadMessagesFromCache( $group->getGroups() );
+ continue;
+ }
+
+ $cache = new MessageGroupCache( $group );
+ if ( $cache->exists() ) {
+ foreach ( $cache->getKeys() as $key ) {
+ $messages[$key] = $cache->get( $key );
+ }
+ }
+ }
+
+ return $messages;
+ }
+
+ public function initCollection( $code ) {
+ $messages = $this->loadMessagesFromCache( $this->getGroups() );
+ $namespace = $this->getNamespace();
+ $definitions = new MessageDefinitions( $messages, $namespace );
+ $collection = MessageCollection::newFromDefinitions( $definitions, $code );
+
+ $this->setTags( $collection );
+
+ return $collection;
+ }
+
+ /**
+ * @param string $key Message key
+ * @param string $code Language code
+ * @return null|string
+ */
+ public function getMessage( $key, $code ) {
+ /* Just hand over the message content retrieval to the primary message
+ * group directly. This used to iterate over the subgroups looking for
+ * the primary group, but that might actually be under some other
+ * aggregate message group.
+ * @todo Implement getMessageContent to avoid hardcoding the namespace
+ * here.
+ */
+ $title = Title::makeTitle( $this->getNamespace(), $key );
+ $handle = new MessageHandle( $title );
+ $groupId = MessageIndex::getPrimaryGroupId( $handle );
+ if ( $groupId === $this->getId() ) {
+ // Message key owned by aggregate group.
+ // Should not ever happen, but it does.
+ error_log( "AggregateMessageGroup $groupId cannot be primary owner of key $key" );
+
+ return null;
+ }
+
+ $group = MessageGroups::getGroup( $groupId );
+ if ( $group ) {
+ return $group->getMessage( $key, $code );
+ } else {
+ return null;
+ }
+ }
+
+ public function getTags( $type = null ) {
+ $tags = [];
+
+ /**
+ * @var $group MessageGroup
+ */
+ foreach ( $this->getGroups() as $group ) {
+ $tags = array_merge_recursive( $tags, $group->getTags( $type ) );
+ }
+
+ return $tags;
+ }
+
+ public function getKeys() {
+ $keys = [];
+ /**
+ * @var $group MessageGroup
+ */
+ foreach ( $this->getGroups() as $group ) {
+ // @todo Not all oldstyle groups have getKeys yet
+ if ( method_exists( $group, 'getKeys' ) ) {
+ $moreKeys = $group->getKeys();
+ } else {
+ $moreKeys = array_keys( $group->getDefinitions() );
+ }
+
+ // Array merge is *really* slow (tested in PHP 7.1), so avoiding it. A loop
+ // followed by array_unique (which we need anyway) is magnitudes faster.
+ foreach ( $moreKeys as $key ) {
+ $keys[] = $key;
+ }
+ }
+
+ return array_values( array_unique( $keys ) );
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/FileBasedMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/FileBasedMessageGroup.php
new file mode 100644
index 00000000..627a7f89
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/FileBasedMessageGroup.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * This file a contains a message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010-2013, Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * This class implements default behavior for file based message groups.
+ *
+ * File based message groups are primary type of groups at translatewiki.net,
+ * while other projects may use mainly page translation message groups, or
+ * custom type of message groups.
+ * @ingroup MessageGroup
+ */
+class FileBasedMessageGroup extends MessageGroupBase implements MetaYamlSchemaExtender {
+ protected $reverseCodeMap;
+
+ /**
+ * Constructs a FileBasedMessageGroup from any normal message group.
+ * Useful for doing special Gettext exports from any group.
+ * @param MessageGroup $group
+ * @return self
+ */
+ public static function newFromMessageGroup( $group ) {
+ $conf = [
+ 'BASIC' => [
+ 'class' => self::class,
+ 'id' => $group->getId(),
+ 'label' => $group->getLabel(),
+ 'namespace' => $group->getNamespace(),
+ ],
+ 'FILES' => [
+ 'sourcePattern' => '',
+ 'targetPattern' => '',
+ ],
+ ];
+
+ return MessageGroupBase::factory( $conf );
+ }
+
+ public function exists() {
+ return $this->getFFS()->exists();
+ }
+
+ public function load( $code ) {
+ /** @var $ffs FFS */
+ $ffs = $this->getFFS();
+ $data = $ffs->read( $code );
+
+ return $data ? $data['MESSAGES'] : [];
+ }
+
+ /**
+ * @param string $code Language code.
+ * @return string
+ * @throws MWException
+ */
+ public function getSourceFilePath( $code ) {
+ if ( $this->isSourceLanguage( $code ) ) {
+ $pattern = $this->getFromConf( 'FILES', 'definitionFile' );
+ if ( $pattern !== null ) {
+ return $this->replaceVariables( $pattern, $code );
+ }
+ }
+
+ $pattern = $this->getFromConf( 'FILES', 'sourcePattern' );
+ if ( $pattern === null ) {
+ throw new MWException( 'No source file pattern defined.' );
+ }
+
+ return $this->replaceVariables( $pattern, $code );
+ }
+
+ public function getTargetFilename( $code ) {
+ // Check if targetPattern explicitly defined
+ $pattern = $this->getFromConf( 'FILES', 'targetPattern' );
+ if ( $pattern !== null ) {
+ return $this->replaceVariables( $pattern, $code );
+ }
+
+ // Check if definitionFile is explicitly defined
+ if ( $this->isSourceLanguage( $code ) ) {
+ $pattern = $this->getFromConf( 'FILES', 'definitionFile' );
+ }
+
+ // Fallback to sourcePattern which must be defined
+ if ( $pattern === null ) {
+ $pattern = $this->getFromConf( 'FILES', 'sourcePattern' );
+ }
+
+ if ( $pattern === null ) {
+ throw new MWException( 'No source file pattern defined.' );
+ }
+
+ // For exports, the scripts take output directory. We want to
+ // return a path where the prefix is current directory instead
+ // of full path of the source location.
+ $pattern = str_replace( '%GROUPROOT%', '.', $pattern );
+ return $this->replaceVariables( $pattern, $code );
+ }
+
+ /**
+ * @param string $pattern
+ * @param string $code Language code.
+ * @return string
+ * @since 2014.02 Made public
+ */
+ public function replaceVariables( $pattern, $code ) {
+ global $IP, $wgTranslateGroupRoot;
+
+ $variables = [
+ '%CODE%' => $this->mapCode( $code ),
+ '%MWROOT%' => $IP,
+ '%GROUPROOT%' => $wgTranslateGroupRoot,
+ ];
+
+ Hooks::run( 'TranslateMessageGroupPathVariables', [ $this, &$variables ] );
+
+ return str_replace( array_keys( $variables ), array_values( $variables ), $pattern );
+ }
+
+ /**
+ * @param string $code Language code.
+ * @return string
+ */
+ public function mapCode( $code ) {
+ if ( !isset( $this->conf['FILES']['codeMap'] ) ) {
+ return $code;
+ }
+
+ if ( isset( $this->conf['FILES']['codeMap'][$code] ) ) {
+ return $this->conf['FILES']['codeMap'][$code];
+ } else {
+ if ( !isset( $this->reverseCodeMap ) ) {
+ $this->reverseCodeMap = array_flip( $this->conf['FILES']['codeMap'] );
+ }
+
+ if ( isset( $this->reverseCodeMap[$code] ) ) {
+ return 'x-invalidLanguageCode';
+ }
+
+ return $code;
+ }
+ }
+
+ public static function getExtraSchema() {
+ $schema = [
+ 'root' => [
+ '_type' => 'array',
+ '_children' => [
+ 'FILES' => [
+ '_type' => 'array',
+ '_children' => [
+ 'class' => [
+ '_type' => 'text',
+ '_not_empty' => true,
+ ],
+ 'codeMap' => [
+ '_type' => 'array',
+ '_ignore_extra_keys' => true,
+ '_children' => [],
+ ],
+ 'definitionFile' => [
+ '_type' => 'text',
+ ],
+ 'sourcePattern' => [
+ '_type' => 'text',
+ '_not_empty' => true,
+ ],
+ 'targetPattern' => [
+ '_type' => 'text',
+ ],
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ return $schema;
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/MediaWikiExtensionMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/MediaWikiExtensionMessageGroup.php
new file mode 100644
index 00000000..6cea415a
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/MediaWikiExtensionMessageGroup.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * This file a contains a message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Message group for %MediaWiki extensions.
+ * @ingroup MessageGroup
+ */
+class MediaWikiExtensionMessageGroup extends FileBasedMessageGroup {
+ /**
+ * MediaWiki extensions all should have key in their i18n files
+ * describing them. This override method implements the logic
+ * to retrieve them. Also URLs are included if available.
+ * Needs the Configure extension.
+ * @param IContextSource|null $context
+ * @return string
+ */
+ public function getDescription( IContextSource $context = null ) {
+ $language = $this->getSourceLanguage();
+ if ( $context ) {
+ $language = $context->getLanguage()->getCode();
+ }
+
+ $msgkey = $this->getFromConf( 'BASIC', 'descriptionmsg' );
+ $desc = '';
+ if ( $msgkey ) {
+ $desc = $this->getMessage( $msgkey, $language );
+ if ( (string)$desc === '' ) {
+ $desc = $this->getMessage( $msgkey, $this->getSourceLanguage() );
+ }
+ }
+
+ if ( (string)$desc === '' ) {
+ // That failed, default to 'description'
+ $desc = parent::getDescription( $context );
+ }
+
+ $url = $this->getFromConf( 'BASIC', 'extensionurl' );
+ if ( $url ) {
+ $desc .= "\n\n$url";
+ }
+
+ return $desc;
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/MessageGroup.php b/www/wiki/extensions/Translate/messagegroups/MessageGroup.php
new file mode 100644
index 00000000..254a7481
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/MessageGroup.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * This file holds a message group interface.
+ *
+ * @file
+ * @defgroup MessageGroup Message group
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010-2013, Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Interface for message groups.
+ *
+ * Message groups are the heart of the Translate extension. They encapsulate
+ * a set of messages each. Aside from basic information like id, label and
+ * description, the class defines which mangler, message checker and file
+ * system support (FFS), if any, the group uses.
+ *
+ * @ingroup MessageGroup
+ */
+interface MessageGroup {
+ /**
+ * Returns the parsed YAML configuration.
+ * @todo Remove from the interface. Only usage is in FFS. Figure out a better way.
+ * @return array
+ */
+ public function getConfiguration();
+
+ /**
+ * Returns the unique identifier for this group.
+ * @return string
+ */
+ public function getId();
+
+ /**
+ * Returns the human readable label (as plain text).
+ * Parameter $context was added in 2012-10-22.
+ * @param IContextSource|null $context Context can be used by subclasses to provide
+ * translated descriptions, for example.
+ * @return string
+ */
+ public function getLabel( IContextSource $context = null );
+
+ /**
+ * Returns a longer description about the group. Description can use wikitext.
+ * Parameter $context was added in 2012-10-22.
+ * @param IContextSource|null $context Context can be used by subclasses to provide
+ * translated descriptions, for example.
+ * @return string
+ */
+ public function getDescription( IContextSource $context = null );
+
+ /**
+ * Returns an icon for this message group if any.
+ * @return string|null File reference in one of the supported protocols:
+ * - file://Filename.ext - Accessible via MediaWiki functions
+ * @since 2012-12-04
+ */
+ public function getIcon();
+
+ /**
+ * Returns the namespace where messages are placed.
+ * @return int
+ */
+ public function getNamespace();
+
+ /**
+ * @todo Unclear usage. Perhaps rename to isSecondary with the only purpose
+ * suppress warnings about message key conflicts.
+ * @return bool
+ */
+ public function isMeta();
+
+ /**
+ * If this function returns false, the message group is ignored and treated
+ * like it would not be configured at all. Useful for graceful degradation.
+ * Try to keep the check fast to avoid performance problems.
+ * @return bool
+ */
+ public function exists();
+
+ /**
+ * Returns a FFS object that handles reading and writing messages to files.
+ * May also return null if it doesn't make sense.
+ * @return FFS or null
+ */
+ public function getFFS();
+
+ /**
+ * Returns a message checker object or null.
+ * @todo Make an interface for message checkers.
+ * @return MessageChecker or null
+ */
+ public function getChecker();
+
+ /**
+ * Return a message mangler or null.
+ * @todo Make an interface for message manglers
+ * @return StringMatcher or null
+ */
+ public function getMangler();
+
+ /**
+ * Initialises a message collection with the given language code,
+ * message definitions and message tags.
+ * @param string $code
+ * @return MessageCollection
+ */
+ public function initCollection( $code );
+
+ /**
+ * Returns a list of messages in a given language code. For some groups
+ * that list may be identical with the translation in the wiki. For other
+ * groups the messages may be loaded from a file (and differ from the
+ * current translations or definitions).
+ * @param string $code
+ * @return array
+ */
+ public function load( $code );
+
+ /**
+ * Shortcut for load( getSourceLanguage() ).
+ * @return string[]
+ */
+ public function getDefinitions();
+
+ /**
+ * Returns message tags. If type is given, only message keys with that
+ * tag are returned. Otherwise an array[tag => keys] is returned.
+ * @param string|null $type
+ * @return array
+ */
+ public function getTags( $type = null );
+
+ /**
+ * Returns the definition or translation for given message key in given
+ * language code.
+ * @param string $key Message key
+ * @param string $code Language code
+ * @return string|null
+ */
+ public function getMessage( $key, $code );
+
+ /**
+ * Returns language code depicting the language of source text.
+ * @return string
+ */
+ public function getSourceLanguage();
+
+ /**
+ * Get the message group workflow state configuration.
+ * @return MessageGroupStates
+ */
+ public function getMessageGroupStates();
+
+ /**
+ * Get all the translatable languages for a group, considering the whitelisting
+ * and blacklisting.
+ * @return array|null The language codes as array keys.
+ */
+ public function getTranslatableLanguages();
+
+ /**
+ * List of available message types mapped to the classes
+ * implementing them.
+ *
+ * @return array
+ */
+ public function getTranslationAids();
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/MessageGroupBase.php b/www/wiki/extensions/Translate/messagegroups/MessageGroupBase.php
new file mode 100644
index 00000000..665e8203
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/MessageGroupBase.php
@@ -0,0 +1,452 @@
+<?php
+/**
+ * This file contains a base implementation of managed message groups.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010-2013, Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * This class implements some basic functions that wrap around the YAML
+ * message group configurations. These message groups use the FFS classes
+ * and are managed with Special:ManageMessageGroups and
+ * processMessageChanges.php.
+ *
+ * @see https://www.mediawiki.org/wiki/Help:Extension:Translate/Group_configuration
+ * @ingroup MessageGroup
+ */
+abstract class MessageGroupBase implements MessageGroup {
+ protected $conf;
+ protected $namespace;
+ protected $groups;
+
+ /**
+ * @var StringMatcher
+ */
+ protected $mangler;
+
+ protected function __construct() {
+ }
+
+ /**
+ * @param array $conf
+ *
+ * @return MessageGroup
+ */
+ public static function factory( $conf ) {
+ $obj = new $conf['BASIC']['class']();
+ $obj->conf = $conf;
+ $obj->namespace = $obj->parseNamespace();
+
+ return $obj;
+ }
+
+ public function getConfiguration() {
+ return $this->conf;
+ }
+
+ public function getId() {
+ return $this->getFromConf( 'BASIC', 'id' );
+ }
+
+ public function getLabel( IContextSource $context = null ) {
+ return $this->getFromConf( 'BASIC', 'label' );
+ }
+
+ public function getDescription( IContextSource $context = null ) {
+ return $this->getFromConf( 'BASIC', 'description' );
+ }
+
+ public function getIcon() {
+ return $this->getFromConf( 'BASIC', 'icon' );
+ }
+
+ public function getNamespace() {
+ return $this->namespace;
+ }
+
+ public function isMeta() {
+ return $this->getFromConf( 'BASIC', 'meta' );
+ }
+
+ public function getSourceLanguage() {
+ $conf = $this->getFromConf( 'BASIC', 'sourcelanguage' );
+
+ return $conf !== null ? $conf : 'en';
+ }
+
+ public function getDefinitions() {
+ $defs = $this->load( $this->getSourceLanguage() );
+
+ return $defs;
+ }
+
+ protected function getFromConf( $section, $key ) {
+ return $this->conf[$section][$key] ?? null;
+ }
+
+ /**
+ * @return FFS
+ * @throws MWException
+ */
+ public function getFFS() {
+ $class = $this->getFromConf( 'FILES', 'class' );
+
+ if ( $class === null ) {
+ return null;
+ }
+
+ if ( !class_exists( $class ) ) {
+ throw new MWException( "FFS class $class does not exist." );
+ }
+
+ return new $class( $this );
+ }
+
+ public function getChecker() {
+ $class = $this->getFromConf( 'CHECKER', 'class' );
+
+ if ( $class === null ) {
+ return null;
+ }
+
+ if ( !class_exists( $class ) ) {
+ throw new MWException( "Checker class $class does not exist." );
+ }
+
+ $checker = new $class( $this );
+ $checks = $this->getFromConf( 'CHECKER', 'checks' );
+
+ if ( !is_array( $checks ) ) {
+ throw new MWException( "Checker class $class not supplied with proper checks." );
+ }
+
+ foreach ( $checks as $check ) {
+ $checker->addCheck( [ $checker, $check ] );
+ }
+
+ return $checker;
+ }
+
+ public function getMangler() {
+ if ( !isset( $this->mangler ) ) {
+ $class = $this->getFromConf( 'MANGLER', 'class' );
+
+ if ( $class === null ) {
+ $this->mangler = StringMatcher::EmptyMatcher();
+
+ return $this->mangler;
+ }
+
+ if ( !class_exists( $class ) ) {
+ throw new MWException( "Mangler class $class does not exist." );
+ }
+
+ /**
+ * @todo Branch handling, merge with upper branch keys
+ */
+ $this->mangler = new $class();
+ $this->mangler->setConf( $this->conf['MANGLER'] );
+ }
+
+ return $this->mangler;
+ }
+
+ /**
+ * Returns the configured InsertablesSuggester if any.
+ * @since 2013.09
+ * @return CombinedInsertablesSuggester
+ */
+ public function getInsertablesSuggester() {
+ $allClasses = [];
+
+ $class = $this->getFromConf( 'INSERTABLES', 'class' );
+ if ( $class !== null ) {
+ $allClasses[] = $class;
+ }
+
+ $classes = $this->getFromConf( 'INSERTABLES', 'classes' );
+ if ( $classes !== null ) {
+ $allClasses = array_merge( $allClasses, $classes );
+ }
+
+ array_unique( $allClasses, SORT_REGULAR );
+
+ $suggesters = [];
+
+ foreach ( $allClasses as $class ) {
+ if ( !class_exists( $class ) ) {
+ throw new MWException( "InsertablesSuggester class $class does not exist." );
+ }
+
+ $suggesters[] = new $class();
+ }
+
+ return new CombinedInsertablesSuggester( $suggesters );
+ }
+
+ /**
+ * Optimized version of array_keys( $_->getDefinitions() ).
+ * @return array
+ * @since 2012-08-21
+ */
+ public function getKeys() {
+ $cache = new MessageGroupCache( $this, $this->getSourceLanguage() );
+ if ( !$cache->exists() ) {
+ return array_keys( $this->getDefinitions() );
+ } else {
+ return $cache->getKeys();
+ }
+ }
+
+ /**
+ * @param string $code Language code.
+ * @return MessageCollection
+ */
+ public function initCollection( $code ) {
+ $namespace = $this->getNamespace();
+ $messages = [];
+
+ $cache = new MessageGroupCache( $this, $this->getSourceLanguage() );
+ if ( !$cache->exists() ) {
+ wfWarn( "By-passing message group cache for {$this->getId()}" );
+ $messages = $this->getDefinitions();
+ } else {
+ foreach ( $cache->getKeys() as $key ) {
+ $messages[$key] = $cache->get( $key );
+ }
+ }
+
+ $definitions = new MessageDefinitions( $messages, $namespace );
+ $collection = MessageCollection::newFromDefinitions( $definitions, $code );
+ $this->setTags( $collection );
+
+ return $collection;
+ }
+
+ /**
+ * @param string $key Message key
+ * @param string $code Language code
+ * @return string|null
+ */
+ public function getMessage( $key, $code ) {
+ $cache = new MessageGroupCache( $this, $code );
+ if ( $cache->exists() ) {
+ $msg = $cache->get( $key );
+
+ if ( $msg !== false ) {
+ return $msg;
+ }
+
+ // Try harder
+ $nkey = str_replace( ' ', '_', strtolower( $key ) );
+ $keys = $cache->getKeys();
+
+ foreach ( $keys as $k ) {
+ if ( $nkey === str_replace( ' ', '_', strtolower( $k ) ) ) {
+ return $cache->get( $k );
+ }
+ }
+
+ return null;
+ } else {
+ return null;
+ }
+ }
+
+ public function getTags( $type = null ) {
+ if ( $type === null ) {
+ $taglist = [];
+
+ foreach ( $this->getRawTags() as $type => $patterns ) {
+ $taglist[$type] = $this->parseTags( $patterns );
+ }
+
+ return $taglist;
+ } else {
+ return $this->parseTags( $this->getRawTags( $type ) );
+ }
+ }
+
+ protected function parseTags( $patterns ) {
+ $messageKeys = $this->getKeys();
+
+ $matches = [];
+
+ /**
+ * Collect exact keys, no point running them trough string matcher
+ */
+ foreach ( $patterns as $index => $pattern ) {
+ if ( strpos( $pattern, '*' ) === false ) {
+ $matches[] = $pattern;
+ unset( $patterns[$index] );
+ }
+ }
+
+ if ( count( $patterns ) ) {
+ /**
+ * Rest of the keys contain wildcards.
+ */
+ $mangler = new StringMatcher( '', $patterns );
+
+ /**
+ * Use mangler to find messages that match.
+ */
+ foreach ( $messageKeys as $key ) {
+ if ( $mangler->match( $key ) ) {
+ $matches[] = $key;
+ }
+ }
+ }
+
+ return $matches;
+ }
+
+ protected function getRawTags( $type = null ) {
+ if ( !isset( $this->conf['TAGS'] ) ) {
+ return [];
+ }
+
+ $tags = $this->conf['TAGS'];
+ if ( !$type ) {
+ return $tags;
+ }
+
+ if ( isset( $tags[$type] ) ) {
+ return $tags[$type];
+ }
+
+ return [];
+ }
+
+ protected function setTags( MessageCollection $collection ) {
+ foreach ( $this->getTags() as $type => $tags ) {
+ $collection->setTags( $type, $tags );
+ }
+ }
+
+ protected function parseNamespace() {
+ $ns = $this->getFromConf( 'BASIC', 'namespace' );
+
+ if ( is_int( $ns ) ) {
+ return $ns;
+ }
+
+ if ( defined( $ns ) ) {
+ return constant( $ns );
+ }
+
+ global $wgContLang;
+
+ $index = $wgContLang->getNsIndex( $ns );
+
+ if ( !$index ) {
+ throw new MWException( "No valid namespace defined, got $ns." );
+ }
+
+ return $index;
+ }
+
+ protected function isSourceLanguage( $code ) {
+ return $code === $this->getSourceLanguage();
+ }
+
+ /**
+ * @deprecated Use getMessageGroupStates
+ */
+ public function getWorkflowConfiguration() {
+ global $wgTranslateWorkflowStates;
+ if ( !$wgTranslateWorkflowStates ) {
+ // Not configured
+ $conf = [];
+ } else {
+ $conf = $wgTranslateWorkflowStates;
+ }
+
+ return $conf;
+ }
+
+ /**
+ * Get the message group workflow state configuration.
+ * @return MessageGroupStates
+ */
+ public function getMessageGroupStates() {
+ // @todo Replace deprecated call.
+ $conf = $this->getWorkflowConfiguration();
+
+ Hooks::run( 'Translate:modifyMessageGroupStates', [ $this->getId(), &$conf ] );
+
+ return new MessageGroupStates( $conf );
+ }
+
+ /**
+ * Get all the translatable languages for a group, considering the whitelisting
+ * and blacklisting.
+ * @return array|null The language codes as array keys.
+ */
+ public function getTranslatableLanguages() {
+ global $wgTranslateBlacklist;
+
+ $groupConfiguration = $this->getConfiguration();
+ if ( !isset( $groupConfiguration['LANGUAGES'] ) ) {
+ // No LANGUAGES section in the configuration.
+ return null;
+ }
+
+ $codes = array_flip( array_keys( TranslateUtils::getLanguageNames( null ) ) );
+
+ $lists = $groupConfiguration['LANGUAGES'];
+ if ( isset( $lists['blacklist'] ) ) {
+ $blacklist = $lists['blacklist'];
+ if ( $blacklist === '*' ) {
+ // All languages blacklisted
+ $codes = [];
+ } elseif ( is_array( $blacklist ) ) {
+ foreach ( $blacklist as $code ) {
+ unset( $codes[$code] );
+ }
+ }
+ } else {
+ // Treat lack of explicit blacklist the same as blacklisting everything. This way,
+ // when one defines only whitelist, it means that only those languages are allowed.
+ $codes = [];
+ }
+
+ // DWIM with $wgTranslateBlacklist, e.g. languages in that list should not unexpectedly
+ // be enabled when a whitelist is used to whitelist any language.
+ $checks = [ $this->getId(), strtok( $this->getId(), '-' ), '*' ];
+ foreach ( $checks as $check ) {
+ if ( isset( $wgTranslateBlacklist[ $check ] ) ) {
+ foreach ( array_keys( $wgTranslateBlacklist[ $check ] ) as $blacklistedCode ) {
+ unset( $codes[ $blacklistedCode ] );
+ }
+ }
+ }
+
+ if ( isset( $lists['whitelist'] ) ) {
+ $whitelist = $lists['whitelist'];
+ if ( $whitelist === '*' ) {
+ // All languages whitelisted (except $wgTranslateBlacklist)
+ return null;
+ } elseif ( is_array( $whitelist ) ) {
+ foreach ( $whitelist as $code ) {
+ $codes[$code] = true;
+ }
+ }
+ }
+
+ return $codes;
+ }
+
+ /**
+ * List of available message types mapped to the classes
+ * implementing them. Default implementation (all).
+ *
+ * @return array
+ */
+ public function getTranslationAids() {
+ return TranslationAid::getTypes();
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/MessageGroupOld.php b/www/wiki/extensions/Translate/messagegroups/MessageGroupOld.php
new file mode 100644
index 00000000..34af7a36
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/MessageGroupOld.php
@@ -0,0 +1,409 @@
+<?php
+/**
+ * This file contains the base information of unmanaged message groups.
+ * These classes don't use Yaml configuration nor Special:ManageMessageGroups
+ * nor processMessageChanges.php
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2008-2013, Niklas Laxström, Siebrand Mazeland
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * This is the interface and base implementation of unmanaged
+ * message groups.
+ * @todo Rename the class
+ * @ingroup MessageGroup
+ */
+abstract class MessageGroupOld implements MessageGroup {
+ /**
+ * Human-readable name of this group
+ */
+ protected $label = 'none';
+
+ /**
+ * @param IContextSource|null $context
+ * @return string
+ */
+ public function getLabel( IContextSource $context = null ) {
+ return $this->label;
+ }
+
+ /**
+ * @param string $value
+ */
+ public function setLabel( $value ) {
+ $this->label = $value;
+ }
+
+ /**
+ * Group-wide unique id of this group. Used also for sorting.
+ */
+ protected $id = 'none';
+
+ /**
+ * @return string
+ */
+ public function getId() {
+ return $this->id;
+ }
+
+ /**
+ * @param string $value
+ */
+ public function setId( $value ) {
+ $this->id = $value;
+ }
+
+ /**
+ * The namespace where all the messages of this group belong.
+ * If the group has messages from multiple namespaces, set this to false
+ * and look how RecentMessageGroup implements the definitions.
+ */
+ protected $namespace = NS_MEDIAWIKI;
+
+ /**
+ * Get the namespace where all the messages of this group belong.
+ * @return int
+ */
+ public function getNamespace() {
+ return $this->namespace;
+ }
+
+ /**
+ * Set the namespace where all the messages of this group belong.
+ * @param int $ns
+ */
+ public function setNamespace( $ns ) {
+ $this->namespace = $ns;
+ }
+
+ /**
+ * List of messages that are hidden by default, but can still be translated if
+ * needed.
+ */
+ protected $optional = [];
+
+ /**
+ * @return array
+ */
+ public function getOptional() {
+ return $this->optional;
+ }
+
+ /**
+ * @param array $value
+ */
+ public function setOptional( $value ) {
+ $this->optional = $value;
+ }
+
+ /**
+ * List of messages that are always hidden and cannot be translated.
+ */
+ protected $ignored = [];
+
+ /**
+ * @return array
+ */
+ public function getIgnored() {
+ return $this->ignored;
+ }
+
+ /**
+ * @param array $value
+ */
+ public function setIgnored( $value ) {
+ $this->ignored = $value;
+ }
+
+ /**
+ * Holds descripton of this group. Description is a wiki text snippet that
+ * gives information about this group to translators.
+ */
+ protected $description = null;
+
+ public function getDescription( IContextSource $context = null ) {
+ return $this->description;
+ }
+
+ public function setDescription( $value ) {
+ $this->description = $value;
+ }
+
+ public function getIcon() {
+ return null;
+ }
+
+ /**
+ * Meta groups consist of multiple groups or parts of other groups. This info
+ * is used on many places, like when creating message index.
+ */
+ protected $meta = false;
+
+ public function isMeta() {
+ return $this->meta;
+ }
+
+ public function setMeta( $value ) {
+ $this->meta = $value;
+ }
+
+ public function getSourceLanguage() {
+ return 'en';
+ }
+
+ /**
+ * To avoid key conflicts between groups or separated changed messages between
+ * branches one can set a message key mangler.
+ */
+ protected $mangler = null;
+
+ /**
+ * @return StringMatcher
+ */
+ public function getMangler() {
+ if ( !isset( $this->mangler ) ) {
+ $this->mangler = StringMatcher::EmptyMatcher();
+ }
+
+ return $this->mangler;
+ }
+
+ public function setMangler( $value ) {
+ $this->mangler = $value;
+ }
+
+ public function load( $code ) {
+ return [];
+ }
+
+ /**
+ * This function returns array of type key => definition of all messages
+ * this message group handles.
+ *
+ * @throws MWException
+ * @return Array of messages definitions indexed by key.
+ */
+ public function getDefinitions() {
+ $defs = $this->load( $this->getSourceLanguage() );
+ if ( !is_array( $defs ) ) {
+ throw new MWException( 'Unable to load definitions for ' . $this->getLabel() );
+ }
+
+ return $defs;
+ }
+
+ /**
+ * This function can be used for meta message groups to list their "own"
+ * messages. For example branched message groups can exclude the messages they
+ * share with each other.
+ * @return array
+ */
+ public function getUniqueDefinitions() {
+ return $this->meta ? [] : $this->getDefinitions();
+ }
+
+ /**
+ * Returns of stored translation of message specified by the $key in language
+ * code $code.
+ *
+ * @param string $key Message key
+ * @param string $code Language code
+ * @return Mixed List of stored translation or \null.
+ */
+ public function getMessage( $key, $code ) {
+ if ( !isset( $this->messages[$code] ) ) {
+ $this->messages[$code] = self::normaliseKeys( $this->load( $code ) );
+ }
+ $key = strtolower( str_replace( ' ', '_', $key ) );
+
+ return $this->messages[$code][$key] ?? null;
+ }
+
+ public static function normaliseKeys( $array ) {
+ if ( !is_array( $array ) ) {
+ return null;
+ }
+
+ $new = [];
+ foreach ( $array as $key => $v ) {
+ $key = strtolower( str_replace( ' ', '_', $key ) );
+ $new[$key] = $v;
+ }
+
+ return $new;
+ }
+
+ /**
+ * All the messages for this group, by language code.
+ */
+ protected $messages = [];
+
+ /**
+ * Returns path to the file where translation of language code $code are.
+ *
+ * @param string $code
+ * @return string Path to the file or false if not applicable.
+ */
+ public function getMessageFile( $code ) {
+ return false;
+ }
+
+ public function getPath() {
+ return false;
+ }
+
+ /**
+ * @param string $code
+ * @return bool|string
+ */
+ public function getMessageFileWithPath( $code ) {
+ $path = $this->getPath();
+ $file = $this->getMessageFile( $code );
+
+ if ( !$path || !$file ) {
+ return false;
+ }
+
+ return "$path/$file";
+ }
+
+ public function getSourceFilePath( $code ) {
+ return $this->getMessageFileWithPath( $code );
+ }
+
+ /**
+ * Creates a new MessageCollection for this group.
+ *
+ * @param string $code Language code for this collection.
+ * @param bool $unique Whether to build collection for messages unique to this
+ * group only.
+ * @return MessageCollection
+ */
+ public function initCollection( $code, $unique = false ) {
+ if ( !$unique ) {
+ $definitions = $this->getDefinitions();
+ } else {
+ $definitions = $this->getUniqueDefinitions();
+ }
+
+ $defs = new MessageDefinitions( $definitions, $this->getNamespace() );
+ $collection = MessageCollection::newFromDefinitions( $defs, $code );
+
+ foreach ( $this->getTags() as $type => $tags ) {
+ $collection->setTags( $type, $tags );
+ }
+
+ return $collection;
+ }
+
+ public function __construct() {
+ }
+
+ /**
+ * Can be overwritten to retun false if something is wrong.
+ * @return bool
+ */
+ public function exists() {
+ return true;
+ }
+
+ public function getChecker() {
+ return null;
+ }
+
+ public function getTags( $type = null ) {
+ $tags = [
+ 'optional' => $this->optional,
+ 'ignored' => $this->ignored,
+ ];
+
+ if ( !$type ) {
+ return $tags;
+ }
+
+ return $tags[$type] ?? [];
+ }
+
+ /**
+ * @param string $code
+ * @return bool
+ */
+ protected function isSourceLanguage( $code ) {
+ return $code === $this->getSourceLanguage();
+ }
+
+ /**
+ * Unsupported stuff, just to satisfy the new interface
+ * @param array $conf
+ */
+ public function setConfiguration( $conf ) {
+ }
+
+ public function getConfiguration() {
+ }
+
+ public function getFFS() {
+ return null;
+ }
+
+ /**
+ * @deprecated Use getMessageGroupStates
+ */
+ public function getWorkflowConfiguration() {
+ global $wgTranslateWorkflowStates;
+ if ( !$wgTranslateWorkflowStates ) {
+ // Not configured
+ $conf = [];
+ } else {
+ $conf = $wgTranslateWorkflowStates;
+ }
+
+ return $conf;
+ }
+
+ /**
+ * Get the message group workflow state configuration.
+ * @return MessageGroupStates
+ */
+ public function getMessageGroupStates() {
+ // @todo Replace deprecated call.
+ $conf = $this->getWorkflowConfiguration();
+
+ Hooks::run( 'Translate:modifyMessageGroupStates', [ $this->getId(), &$conf ] );
+
+ return new MessageGroupStates( $conf );
+ }
+
+ /**
+ * Get all the translatable languages for a group, considering the whitelisting
+ * and blacklisting.
+ * @return array|null The language codes as array keys.
+ */
+ public function getTranslatableLanguages() {
+ return null;
+ }
+
+ protected static function addContext( Message $message, IContextSource $context = null ) {
+ if ( $context ) {
+ $message->inLanguage( $context->getLanguage() );
+ } else {
+ $message->inLanguage( 'en' );
+ }
+
+ return $message;
+ }
+
+ /**
+ * List of available message types mapped to the classes
+ * implementing them. Default implementation (all).
+ *
+ * @return array
+ */
+ public function getTranslationAids() {
+ return TranslationAid::getTypes();
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/RecentAdditionsMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/RecentAdditionsMessageGroup.php
new file mode 100644
index 00000000..217af1e5
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/RecentAdditionsMessageGroup.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * This file contains an unmanaged message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2012-2013, Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * @since 2012-11-01
+ * @ingroup MessageGroup
+ */
+class RecentAdditionsMessageGroup extends RecentMessageGroup {
+ protected $groupInfoCache = [];
+
+ public function getId() {
+ return '!additions';
+ }
+
+ public function getLabel( IContextSource $context = null ) {
+ $msg = wfMessage( 'translate-dynagroup-additions-label' );
+ $msg = self::addContext( $msg, $context );
+
+ return $msg->plain();
+ }
+
+ public function getDescription( IContextSource $context = null ) {
+ $msg = wfMessage( 'translate-dynagroup-additions-desc' );
+ $msg = self::addContext( $msg, $context );
+
+ return $msg->plain();
+ }
+
+ protected function getQueryConditions() {
+ global $wgTranslateMessageNamespaces;
+ $db = wfGetDB( DB_REPLICA );
+ $conds = [
+ 'rc_title ' . $db->buildLike( $db->anyString(), '/en' ),
+ 'rc_namespace' => $wgTranslateMessageNamespaces,
+ 'rc_type != ' . RC_LOG,
+ 'rc_id > ' . $this->getRCCutoff(),
+ ];
+
+ if ( class_exists( ActorMigration::class ) ) {
+ $conds[] = ActorMigration::newMigration()
+ ->getWhere( $db, 'rc_user', FuzzyBot::getUser() )['conds'];
+ } else {
+ $conds['rc_user'] = FuzzyBot::getUser()->getId();
+ }
+
+ return $conds;
+ }
+
+ /**
+ * Filters out messages that should not be displayed here
+ * as they are not displayed in other places.
+ *
+ * @param MessageHandle $handle
+ * @return bool
+ */
+ protected function matchingMessage( MessageHandle $handle ) {
+ return MessageGroups::isTranslatableMessage( $handle );
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/RecentMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/RecentMessageGroup.php
new file mode 100644
index 00000000..60593494
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/RecentMessageGroup.php
@@ -0,0 +1,170 @@
+<?php
+/**
+ * This file contains an unmanaged message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2008-2013, Niklas Laxström, Siebrand Mazeland
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * @since 2011-11-28
+ * @ingroup MessageGroup
+ */
+class RecentMessageGroup extends WikiMessageGroup {
+ /*
+ * Yes this is very ugly hack and should not be removed.
+ * @see MessageCollection::getPages()
+ */
+ protected $namespace = false;
+
+ protected $language;
+
+ /**
+ * These groups are always generated for one language. Method setLanguage
+ * must be called before calling getDefinitions.
+ */
+ public function __construct() {
+ }
+
+ public function setLanguage( $code ) {
+ $this->language = $code;
+ }
+
+ public function getId() {
+ return '!recent';
+ }
+
+ public function getLabel( IContextSource $context = null ) {
+ $msg = wfMessage( 'translate-dynagroup-recent-label' );
+ $msg = self::addContext( $msg, $context );
+
+ return $msg->plain();
+ }
+
+ public function getDescription( IContextSource $context = null ) {
+ $msg = wfMessage( 'translate-dynagroup-recent-desc' );
+ $msg = self::addContext( $msg, $context );
+
+ return $msg->plain();
+ }
+
+ protected function getRCCutoff() {
+ $db = wfGetDB( DB_REPLICA );
+ $tables = 'recentchanges';
+ $max = $db->selectField( $tables, 'MAX(rc_id)', [], __METHOD__ );
+
+ return max( 0, $max - 50000 );
+ }
+
+ /**
+ * Allows subclasses to partially customize the query.
+ * @return array
+ */
+ protected function getQueryConditions() {
+ global $wgTranslateMessageNamespaces;
+ $db = wfGetDB( DB_REPLICA );
+ $conds = [
+ 'rc_title ' . $db->buildLike( $db->anyString(), '/' . $this->language ),
+ 'rc_namespace' => $wgTranslateMessageNamespaces,
+ 'rc_type != ' . RC_LOG,
+ 'rc_id > ' . $this->getRCCutoff(),
+ ];
+
+ return $conds;
+ }
+
+ /**
+ * Allows subclasses to filter out more unwanted messages.
+ *
+ * @param MessageHandle $msg
+ * @return bool
+ */
+ protected function matchingMessage( MessageHandle $msg ) {
+ return true;
+ }
+
+ public function getDefinitions() {
+ if ( !$this->language ) {
+ throw new MWException( 'Language not set' );
+ }
+
+ $db = wfGetDB( DB_REPLICA );
+
+ if ( is_callable( RecentChange::class, 'getQueryInfo' ) ) {
+ $rcQuery = RecentChange::getQueryInfo();
+ $tables = $rcQuery['tables'];
+ $joins = $rcQuery['joins'];
+ } else {
+ $tables = 'recentchanges';
+ $joins = [];
+ }
+
+ $fields = [ 'rc_namespace', 'rc_title' ];
+ $conds = $this->getQueryConditions();
+ $options = [
+ 'ORDER BY' => 'rc_id DESC',
+ 'LIMIT' => 5000
+ ];
+ $res = $db->select( $tables, $fields, $conds, __METHOD__, $options, $joins );
+
+ $groupIdsPreload = [];
+ foreach ( $res as $row ) {
+ $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+ $handle = new MessageHandle( $title );
+ if ( $handle->isValid() ) {
+ $groupIdsPreload[] = $handle->getGroup()->getId();
+ }
+ }
+ TranslateMetadata::preloadGroups( $groupIdsPreload );
+
+ $defs = [];
+ foreach ( $res as $row ) {
+ $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+ $handle = new MessageHandle( $title );
+
+ if ( !$handle->isValid() || !$this->matchingMessage( $handle ) ) {
+ continue;
+ }
+
+ $messageKey = $handle->getKey();
+ $fullKey = $row->rc_namespace . ':' . $messageKey;
+
+ /* Note: due to bugs, getMessage might return null even for
+ * known messages. These negatives are not cached, but that
+ * should be rare enough case to not affect performance. */
+ if ( !isset( $defs[$fullKey] ) ) {
+ $group = $handle->getGroup();
+ $msg = $group->getMessage( $messageKey, $group->getSourceLanguage() );
+
+ if ( $msg !== null ) {
+ $defs[$fullKey] = $msg;
+ }
+ }
+ }
+
+ return $defs;
+ }
+
+ public function getChecker() {
+ return null;
+ }
+
+ /**
+ * Subpage language code, if any in the title, is ignored.
+ * @param MessageHandle $handle
+ * @return null|string
+ * @throws MWException
+ */
+ public function getMessageContent( MessageHandle $handle ) {
+ $groupId = MessageIndex::getPrimaryGroupId( $handle );
+ $group = MessageGroups::getGroup( $groupId );
+ if ( $group ) {
+ return $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
+ }
+
+ throw new MWException( 'Could not find group for ' . $handle->getKey() );
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/SandboxMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/SandboxMessageGroup.php
new file mode 100644
index 00000000..f0aa1d8b
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/SandboxMessageGroup.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * This file contains an unmanaged message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * @since 2013.06
+ * @ingroup MessageGroup
+ */
+class SandboxMessageGroup extends WikiMessageGroup {
+ /*
+ * Yes this is very ugly hack and should not be removed.
+ * @see MessageCollection::getPages()
+ */
+ protected $namespace = false;
+
+ protected $language;
+
+ /**
+ * #setLanguage must be called before calling getDefinitions.
+ */
+ public function __construct() {
+ }
+
+ public function setLanguage( $code ) {
+ $this->language = $code;
+ }
+
+ public function getId() {
+ return '!sandbox';
+ }
+
+ public function getLabel( IContextSource $context = null ) {
+ // Should not be visible
+ return 'Sandbox messages';
+ }
+
+ public function getDescription( IContextSource $context = null ) {
+ // Should not be visible
+ return 'Suggests messages to translate for sandboxed users';
+ }
+
+ public function getDefinitions() {
+ global $wgTranslateSandboxSuggestions, $wgTranslateSandboxLimit;
+
+ // This will contain the list of messages shown to the user
+ $list = [];
+
+ // Ugly
+ $store = new TranslationStashStorage( wfGetDB( DB_MASTER ) );
+ $user = RequestContext::getMain()->getUser();
+ $translations = $store->getTranslations( $user );
+
+ // Add messages the user has already translated first, so he
+ // can go back and correct them.
+ foreach ( $translations as $translation ) {
+ $title = $translation->getTitle();
+ $handle = new MessageHandle( $title );
+ $index = $title->getNamespace() . ':' . $handle->getKey();
+ $list[$index] = '';
+ }
+
+ // Always add the regular suggestions
+ foreach ( $wgTranslateSandboxSuggestions as $titleText ) {
+ $title = Title::newFromText( $titleText );
+ if ( !$title ) {
+ wfWarn( "Invalid title in \$wgTranslateSandboxSuggestions: $titleText" );
+ continue;
+ }
+
+ $index = $title->getNamespace() . ':' . $handle->getKey();
+ // This index might already exist, but that is okay
+ $list[$index] = '';
+ }
+
+ // Get some random keys
+ $all = MessageIndex::singleton()->getKeys();
+ // In case there aren't any messages
+ if ( $all === [] ) {
+ return $list;
+ }
+ $min = 0;
+ $max = count( $all ) - 1; // Indexes are zero-based
+
+ // Get some message. Will be filtered to less below.
+ for ( $i = count( $list ); $i < 100; $i++ ) {
+ $list[$all[rand( $min, $max )]] = '';
+ }
+
+ // Fetch definitions, slowly, one by one
+ $count = 0;
+
+ // Provide twice the number of messages than the limit
+ // to have a buffer in case the user skips some messages
+ $messagesToProvide = $wgTranslateSandboxLimit * 2;
+
+ foreach ( $list as $index => &$translation ) {
+ list( $ns, $page ) = explode( ':', $index, 2 );
+ $title = Title::makeTitle( $ns, "$page/{$this->language}" );
+ $handle = new MessageHandle( $title );
+
+ if ( MessageGroups::isTranslatableMessage( $handle ) ) {
+ // Modified by reference
+ $translation = $this->getMessageContent( $handle );
+ if ( $translation === null ) {
+ // Something is not in sync or badly broken. Handle gracefully.
+ unset( $list[$index] );
+ wfWarn( "No message definition for $index while preparing the sandbox" );
+
+ continue;
+ }
+ } else {
+ // This might include messages that the user has already translated
+ // or messages given in $wgTranslateSandboxSuggestions or just dated
+ // message index.
+ unset( $list[$index] );
+
+ continue;
+ }
+
+ $count++;
+
+ if ( $count === $messagesToProvide ) {
+ break;
+ }
+ }
+
+ // Remove the extra entries
+ $list = array_slice( $list, 0, $messagesToProvide );
+
+ return $list;
+ }
+
+ public function getChecker() {
+ return null;
+ }
+
+ /**
+ * Subpage language code, if any in the title, is ignored.
+ * @param MessageHandle $handle
+ * @return null|string
+ */
+ public function getMessageContent( MessageHandle $handle ) {
+ $groupId = MessageIndex::getPrimaryGroupId( $handle );
+ $group = MessageGroups::getGroup( $groupId );
+ $key = $handle->getKey();
+
+ $source = $group->getMessage( $key, $group->getSourceLanguage() );
+ if ( $source !== null ) {
+ return $source;
+ }
+
+ // Try harder
+ if ( method_exists( $group, 'getKeys' ) ) {
+ $keys = $group->getKeys();
+ } else {
+ $keys = array_keys( $group->getDefinitions() );
+ }
+ // Try to find the original key with correct case
+ foreach ( $keys as $realkey ) {
+ if ( $key === strtolower( $realkey ) ) {
+ $key = $realkey;
+ break;
+ }
+ }
+
+ return $group->getMessage( $key, $group->getSourceLanguage() );
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/WikiMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/WikiMessageGroup.php
new file mode 100644
index 00000000..e7b08240
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/WikiMessageGroup.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * This file contains an unmanaged message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2008-2013, Niklas Laxström, Siebrand Mazeland
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Group for messages that can be controlled via a page in %MediaWiki namespace.
+ *
+ * In the page comments start with # and continue till the end of the line.
+ * The page should contain list of page names in %MediaWiki namespace, without
+ * the namespace prefix. Use underscores for spaces in page names, since
+ * whitespace separates the page names from each other.
+ * @ingroup MessageGroup
+ */
+class WikiMessageGroup extends MessageGroupOld {
+ protected $source;
+
+ /**
+ * Constructor.
+ *
+ * @param string $id Unique id for this group.
+ * @param string $source Mediawiki message that contains list of message keys.
+ */
+ public function __construct( $id, $source ) {
+ parent::__construct();
+ $this->id = $id;
+ $this->source = $source;
+ }
+
+ /**
+ * Defaults to wiki content language.
+ * @return string Language code
+ */
+ public function getSourceLanguage() {
+ global $wgLanguageCode;
+
+ return $wgLanguageCode;
+ }
+
+ /**
+ * Fetch definitions from database.
+ * @return array Array of messages keys with definitions.
+ */
+ public function getDefinitions() {
+ $definitions = [];
+
+ // In theory the page could have templates that are substitued
+ $source = wfMessage( $this->source );
+ if ( $source->isDisabled() ) {
+ return [];
+ }
+
+ $contents = $source->text();
+ $contents = preg_replace( '~^\s*#.*$~m', '', $contents );
+ $messages = preg_split( '/\s+/', $contents );
+
+ foreach ( $messages as $message ) {
+ if ( !$message ) {
+ continue;
+ }
+
+ $definitions[$message] = wfMessage( $message )->inContentLanguage()->plain();
+ }
+
+ return $definitions;
+ }
+
+ /**
+ * Returns of stored translation of message specified by the $key in language
+ * code $code.
+ *
+ * @param string $key Key of the message.
+ * @param string $code Language code.
+ * @return string|null The translation or null if it doesn't exists.
+ */
+ public function getMessage( $key, $code ) {
+ if ( $code && $this->getSourceLanguage() !== $code ) {
+ return TranslateUtils::getMessageContent( $key, $code );
+ } else {
+ return TranslateUtils::getMessageContent( $key, false );
+ }
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/WikiPageMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/WikiPageMessageGroup.php
new file mode 100644
index 00000000..5208d179
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/WikiPageMessageGroup.php
@@ -0,0 +1,248 @@
+<?php
+/**
+ * This file contains an unmanaged message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Wraps the translatable page sections into a message group.
+ * @ingroup PageTranslation MessageGroup
+ */
+class WikiPageMessageGroup extends WikiMessageGroup implements IDBAccessObject, \Serializable {
+ /**
+ * @var Title|string
+ */
+ protected $title;
+
+ /**
+ * @var int
+ */
+ protected $namespace = NS_TRANSLATIONS;
+
+ /**
+ * @param string $id
+ * @param Title|string $source
+ */
+ public function __construct( $id, $source ) {
+ $this->id = $id;
+ $this->title = $source;
+ }
+
+ public function getSourceLanguage() {
+ return $this->getTitle()->getPageLanguage()->getCode();
+ }
+
+ /**
+ * @return Title
+ */
+ public function getTitle() {
+ if ( is_string( $this->title ) ) {
+ $this->title = Title::newFromText( $this->title );
+ }
+
+ return $this->title;
+ }
+
+ /**
+ * Only used for caching to avoid repeating database queries
+ * for example during message index rebuild.
+ */
+ protected $definitions;
+
+ /**
+ * @return array
+ */
+ public function getDefinitions() {
+ if ( is_array( $this->definitions ) ) {
+ return $this->definitions;
+ }
+
+ $dbr = TranslateUtils::getSafeReadDB();
+ $tables = 'translate_sections';
+ $vars = [ 'trs_key', 'trs_text' ];
+ $conds = [ 'trs_page' => $this->getTitle()->getArticleID() ];
+ $options = [ 'ORDER BY' => 'trs_order' ];
+ $res = $dbr->select( $tables, $vars, $conds, __METHOD__, $options );
+
+ $defs = [];
+ $prefix = $this->getTitle()->getPrefixedDBkey() . '/';
+
+ foreach ( $res as $r ) {
+ $section = new TPSection();
+ $section->text = $r->trs_text;
+ $defs[$r->trs_key] = $section->getTextWithVariables();
+ }
+
+ $new_defs = [];
+ foreach ( $defs as $k => $v ) {
+ $k = str_replace( ' ', '_', $k );
+ $new_defs[$prefix . $k] = $v;
+ }
+
+ $this->definitions = $new_defs;
+ return $this->definitions;
+ }
+
+ /**
+ * Overriding the getLabel method and deriving the label from the title.
+ * Mainly to reduce the amount of data stored in the cache.
+ *
+ * @param IContextSource|null $context
+ * @return string
+ */
+ public function getLabel( IContextSource $context = null ) {
+ return $this->getTitle()->getPrefixedText();
+ }
+
+ /**
+ * Clear caches to avoid stale data.
+ *
+ * For example JobQueue can run for a longer time, and stale definitions would
+ * cause the total number of messages to be incorrect.
+ *
+ * @since 2016.04
+ */
+ public function clearCaches() {
+ $this->definitions = null;
+ }
+
+ public function load( $code ) {
+ if ( $this->isSourceLanguage( $code ) ) {
+ return $this->getDefinitions();
+ }
+
+ return [];
+ }
+
+ /**
+ * Returns of stored translation of message specified by the $key in language
+ * code $code.
+ *
+ * @param string $key Message key
+ * @param string $code Language code
+ * @param int $flags READ_* class constant bitfield
+ * @return string|null Stored translation or null.
+ */
+ public function getMessage( $key, $code, $flags = self::READ_LATEST ) {
+ if ( $this->isSourceLanguage( $code ) ) {
+ $stuff = $this->load( $code );
+
+ $title = Title::newFromText( $key );
+ if ( $title ) {
+ $key = $title->getPrefixedDBkey();
+ }
+
+ return $stuff[$key] ?? null;
+ }
+
+ $title = Title::makeTitleSafe( $this->getNamespace(), "$key/$code" );
+ if ( PageTranslationHooks::$renderingContext ) {
+ $revFlags = Revision::READ_NORMAL; // bug T95753
+ } else {
+ $revFlags = ( $flags & self::READ_LATEST ) == self::READ_LATEST
+ ? Revision::READ_LATEST
+ : Revision::READ_NORMAL;
+ }
+ $rev = Revision::newFromTitle( $title, false, $revFlags );
+
+ if ( !$rev ) {
+ return null;
+ }
+
+ return ContentHandler::getContentText( $rev->getContent() );
+ }
+
+ /**
+ * @return MediaWikiMessageChecker
+ */
+ public function getChecker() {
+ $checker = new MediaWikiMessageChecker( $this );
+ $checker->setChecks( [
+ [ $checker, 'pluralCheck' ],
+ [ $checker, 'braceBalanceCheck' ],
+ [ $checker, 'miscMWChecks' ]
+ ] );
+
+ return $checker;
+ }
+
+ public function getInsertablesSuggester() {
+ return new TranslatablePageInsertablesSuggester();
+ }
+
+ public function getDescription( IContextSource $context = null ) {
+ $title = $this->getTitle()->getPrefixedText();
+ $target = ":$title";
+ $pageLanguageCode = $this->getSourceLanguage();
+ $inLanguageCode = $context ? $context->getLanguage()->getCode() : null;
+ $languageName = Language::fetchLanguageName( $pageLanguageCode, $inLanguageCode );
+
+ // Allow for adding a custom group description by using
+ // "MediaWiki:Tp-custom-<group ID>".
+ $customText = '';
+ $msg = wfMessage( 'tp-custom-' . $this->id );
+ self::addContext( $msg, $context );
+ if ( $msg->exists() ) {
+ $customText = $msg->plain();
+ }
+
+ $msg = wfMessage( 'translate-tag-page-desc', $title, $target, $languageName, $pageLanguageCode );
+ self::addContext( $msg, $context );
+
+ return $msg->plain() . $customText;
+ }
+
+ public function serialize() {
+ $toSerialize = [
+ 'title' => $this->getTitle()->getPrefixedText(),
+ 'id' => $this->id,
+ '_v' => 1 // version - to track incompatible changes
+ ];
+
+ // NOTE: get_class_vars returns properties before the constructor has run so if any default
+ // values have to be set for properties, do them while declaring the properties themselves.
+ // Also any properties that are object will automatically be serialized because `===`
+ // does not actually compare object properties to see that they are same.
+
+ // Using array_diff_key to unset the properties already set earlier.
+ $defaultProps = array_diff_key( get_class_vars( self::class ), $toSerialize );
+
+ foreach ( $defaultProps as $prop => $defaultVal ) {
+ if ( $this->{$prop} === $defaultVal ) {
+ continue;
+ }
+
+ $toSerialize[$prop] = $this->{$prop};
+ }
+
+ return FormatJson::encode( $toSerialize, false, FormatJson::ALL_OK );
+ }
+
+ public function unserialize( $serialized ) {
+ $deserialized = FormatJson::decode( $serialized );
+ if ( $deserialized === false ) {
+ // Unrecoverable. This should not happen but still.
+ throw new \UnexpectedValueException(
+ 'Error while deserializing to WikiPageMessageGroup object - FormatJson::decode failed. ' .
+ "Serialize string - $serialized."
+ );
+ }
+
+ // Use as needed in the future to track incompatible changes.
+ // $version = $deserialized->_v;
+ // unset($deserialized->_v);
+
+ // Only set the properties that are present in the class and the deserialized object.
+ $classProps = array_keys( get_class_vars( self::class ) );
+
+ foreach ( $classProps as $prop ) {
+ if ( property_exists( $deserialized, $prop ) ) {
+ $this->{$prop} = $deserialized->{$prop};
+ }
+ }
+ }
+}
diff --git a/www/wiki/extensions/Translate/messagegroups/WorkflowStatesMessageGroup.php b/www/wiki/extensions/Translate/messagegroups/WorkflowStatesMessageGroup.php
new file mode 100644
index 00000000..c4a7dd74
--- /dev/null
+++ b/www/wiki/extensions/Translate/messagegroups/WorkflowStatesMessageGroup.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * This file contains an unmanaged message group implementation.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2008-2013, Niklas Laxström, Siebrand Mazeland
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * @ingroup MessageGroup
+ */
+class WorkflowStatesMessageGroup extends WikiMessageGroup {
+ // id and source are not needed
+ public function __construct() {
+ }
+
+ public function getId() {
+ return 'translate-workflow-states';
+ }
+
+ public function getLabel( IContextSource $context = null ) {
+ $msg = wfMessage( 'translate-workflowgroup-label' );
+ $msg = self::addContext( $msg, $context );
+
+ return $msg->plain();
+ }
+
+ public function getDescription( IContextSource $context = null ) {
+ $msg = wfMessage( 'translate-workflowgroup-desc' );
+ $msg = self::addContext( $msg, $context );
+
+ return $msg->plain();
+ }
+
+ public function getDefinitions() {
+ $groups = MessageGroups::getAllGroups();
+ $keys = [];
+
+ /**
+ * @var $g MessageGroup
+ */
+ foreach ( $groups as $g ) {
+ $states = $g->getMessageGroupStates()->getStates();
+ foreach ( array_keys( $states ) as $state ) {
+ $keys["Translate-workflow-state-$state"] = $state;
+ }
+ }
+
+ $defs = TranslateUtils::getContents( array_keys( $keys ), $this->getNamespace() );
+ foreach ( $keys as $key => $state ) {
+ if ( !isset( $defs[$key] ) ) {
+ // @todo Use jobqueue
+ $title = Title::makeTitleSafe( $this->getNamespace(), $key );
+ $page = new WikiPage( $title );
+ $content = ContentHandler::makeContent( $state, $title );
+
+ $page->doEditContent(
+ $content,
+ wfMessage( 'translate-workflow-autocreated-summary', $state )->inContentLanguage()->text(),
+ 0, /*flags*/
+ false, /* base revision id */
+ FuzzyBot::getUser()
+ );
+ } else {
+ // Use the wiki translation as definition if available.
+ // getContents returns array( content, last author )
+ list( $content, ) = $defs[$key];
+ $keys[$key] = $content;
+ }
+ }
+
+ return $keys;
+ }
+}