diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Translate/messagegroups |
first commit
Diffstat (limited to 'www/wiki/extensions/Translate/messagegroups')
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; + } +} |