messagegroups/MessageGroupBase.php
<?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
*/
use MediaWiki\Context\IContextSource;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupStates;
use MediaWiki\Extension\Translate\MessageLoading\MessageCollection;
use MediaWiki\Extension\Translate\MessageProcessing\StringMatcher;
use MediaWiki\Extension\Translate\Services;
use MediaWiki\Extension\Translate\TranslatorInterface\Insertable\CombinedInsertablesSuggester;
use MediaWiki\Extension\Translate\TranslatorInterface\Insertable\InsertableFactory;
use MediaWiki\Extension\Translate\Utilities\Utilities;
use MediaWiki\Extension\Translate\Validation\ValidationRunner;
use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
/**
* This class implements some basic functions that wrap around the YAML
* message group configurations. These message groups use the file format classes
* and are managed with Special:ManageMessageGroups and
* importExternalTranslations.php.
*
* @see https://www.mediawiki.org/wiki/Help:Extension:Translate/Group_configuration
* @ingroup MessageGroup
*/
abstract class MessageGroupBase implements MessageGroup {
/** @var array */
protected $conf;
/** @var int|false */
protected $namespace;
/** @var StringMatcher|null */
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 ?? 'en';
}
public function getDefinitions() {
$defs = $this->load( $this->getSourceLanguage() );
return $defs;
}
protected function getFromConf( $section, $key = null ) {
if ( $key === null ) {
return $this->conf[$section] ?? null;
}
return $this->conf[$section][$key] ?? null;
}
public function getValidator() {
$validatorConfigs = $this->getFromConf( 'VALIDATORS' );
if ( $validatorConfigs === null ) {
return null;
}
$msgValidator = new ValidationRunner( $this->getId() );
foreach ( $validatorConfigs as $config ) {
try {
$msgValidator->addValidator( $config );
} catch ( Exception $e ) {
$id = $this->getId();
throw new InvalidArgumentException(
"Unable to construct validator for message group $id: " . $e->getMessage(),
0,
$e
);
}
}
return $msgValidator;
}
public function getMangler() {
if ( $this->mangler === null ) {
$class = $this->getFromConf( 'MANGLER', 'class' ) ?? StringMatcher::class;
if ( $class === 'StringMatcher' || $class === StringMatcher::class ) {
$this->mangler = new StringMatcher();
$manglerConfig = $this->conf['MANGLER'] ?? null;
if ( $manglerConfig ) {
$this->mangler->setConf( $manglerConfig );
}
return $this->mangler;
}
throw new InvalidArgumentException(
'Unable to create StringMangler for group ' . $this->getId() . ': ' .
"Custom StringManglers ($class) are currently not supported."
);
}
return $this->mangler;
}
/**
* Returns the configured InsertablesSuggester if any.
* @since 2013.09
* @return CombinedInsertablesSuggester
*/
public function getInsertablesSuggester() {
$suggesters = [];
$insertableConf = $this->getFromConf( 'INSERTABLES' ) ?? [];
foreach ( $insertableConf as $config ) {
if ( !isset( $config['class'] ) ) {
throw new InvalidArgumentException(
'Insertable configuration for group: ' . $this->getId() .
' does not provide a class.'
);
}
if ( !is_string( $config['class'] ) ) {
throw new InvalidArgumentException(
'Expected Insertable class to be string, got: ' . get_debug_type( $config['class'] ) .
' for group: ' . $this->getId()
);
}
$suggesters[] = InsertableFactory::make( $config['class'], $config['params'] ?? [] );
}
// Get validators marked as insertable
$messageValidator = $this->getValidator();
if ( $messageValidator ) {
$suggesters = array_merge( $suggesters, $messageValidator->getInsertableValidators() );
}
return new CombinedInsertablesSuggester( $suggesters );
}
/** @inheritDoc */
public function getKeys() {
return array_keys( $this->getDefinitions() );
}
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 ( !str_contains( $pattern, '*' ) ) {
$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->matches( $key ) ) {
$matches[] = $key;
}
}
}
return $matches;
}
protected function getRawTags( $type = null ) {
if ( !isset( $this->conf['TAGS'] ) ) {
return [];
}
$tags = $this->conf['TAGS'];
if ( !$type ) {
return $tags;
}
return $tags[$type] ?? [];
}
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 );
}
$index = MediaWikiServices::getInstance()->getContentLanguage()
->getNsIndex( $ns );
if ( !$index ) {
throw new RuntimeException( "No valid namespace defined, got $ns." );
}
return $index;
}
protected function isSourceLanguage( $code ) {
return $code === $this->getSourceLanguage();
}
/**
* Get the message group workflow state configuration.
* @return MessageGroupStates
*/
public function getMessageGroupStates() {
global $wgTranslateWorkflowStates;
$conf = $wgTranslateWorkflowStates ?: [];
Services::getInstance()->getHookRunner()
->onTranslate_modifyMessageGroupStates( $this->getId(), $conf );
return new MessageGroupStates( $conf );
}
/** @inheritDoc */
public function getTranslatableLanguages() {
$groupConfiguration = $this->getConfiguration();
if ( !isset( $groupConfiguration['LANGUAGES'] ) ) {
// No LANGUAGES section in the configuration.
return null;
}
$codes = array_flip( array_keys( Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS ) ) );
$lists = $groupConfiguration['LANGUAGES'];
$exclusionList = $lists['exclude'] ?? null;
if ( $exclusionList !== null ) {
if ( $exclusionList === '*' ) {
// All excluded languages
$codes = [];
} elseif ( is_array( $exclusionList ) ) {
foreach ( $exclusionList as $code ) {
unset( $codes[$code] );
}
}
} else {
// Treat lack of explicit exclusion list the same as excluding everything. This way,
// when one defines only inclusions, it means that only those languages are allowed.
$codes = [];
}
$disabledLanguages = Services::getInstance()->getConfigHelper()->getDisabledTargetLanguages();
// DWIM with $wgTranslateDisabledTargetLanguages, e.g. languages in that list should not unexpectedly
// be enabled when an inclusion list is used to include any language.
$checks = [ $this->getId(), strtok( $this->getId(), '-' ), '*' ];
foreach ( $checks as $check ) {
if ( isset( $disabledLanguages[ $check ] ) ) {
foreach ( array_keys( $disabledLanguages[ $check ] ) as $excludedCode ) {
unset( $codes[ $excludedCode ] );
}
}
}
$inclusionList = $lists['include'] ?? null;
if ( $inclusionList !== null ) {
if ( $inclusionList === '*' ) {
// All languages included (except $wgTranslateDisabledTargetLanguages)
return null;
} elseif ( is_array( $inclusionList ) ) {
foreach ( $inclusionList as $code ) {
$codes[$code] = true;
}
}
}
return $codes;
}
public function getSupportConfig(): ?array {
return $this->getFromConf( 'BASIC', 'support' );
}
/** @inheritDoc */
public function getRelatedPage(): ?LinkTarget {
return null;
}
}