src/MessageBundleTranslation/MessageBundleStore.php
<?php
declare( strict_types = 1 );
namespace MediaWiki\Extension\Translate\MessageBundleTranslation;
use InvalidArgumentException;
use JobQueueGroup;
use MediaWiki\Context\RequestContext;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundle;
use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleStore;
use MediaWiki\Extension\Translate\MessageLoading\MessageIndex;
use MediaWiki\Extension\Translate\MessageLoading\RebuildMessageIndexJob;
use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\Message\Message;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Specials\SpecialPageLanguage;
use MediaWiki\Title\Title;
/**
* @author Abijeet Patro
* @author Niklas Laxström
* @since 2022.04
* @license GPL-2.0-or-later
*/
class MessageBundleStore implements TranslatableBundleStore {
private RevTagStore $revTagStore;
private JobQueueGroup $jobQueue;
private LanguageNameUtils $languageNameUtils;
private MessageIndex $messageIndex;
private MessageGroupMetadata $messageGroupMetadata;
private const METADATA_KEYS_DB = [
'priorityforce',
'prioritylangs'
];
public function __construct(
RevTagStore $revTagStore,
JobQueueGroup $jobQueue,
LanguageNameUtils $languageNameUtils,
MessageIndex $messageIndex,
MessageGroupMetadata $messageGroupMetadata
) {
$this->revTagStore = $revTagStore;
$this->jobQueue = $jobQueue;
$this->languageNameUtils = $languageNameUtils;
$this->messageIndex = $messageIndex;
$this->messageGroupMetadata = $messageGroupMetadata;
}
public function move( Title $oldName, Title $newName ): void {
$oldBundle = new MessageBundle( $oldName );
$newBundle = new MessageBundle( $newName );
$this->messageGroupMetadata->moveMetadata(
$oldBundle->getMessageGroupId(),
$newBundle->getMessageGroupId(),
self::METADATA_KEYS_DB
);
MessageBundle::clearSourcePageCache();
MessageGroups::singleton()->recache();
// Update message index now so that, when after this job the MoveTranslationUnits hook
// runs in deferred updates, it will not run RebuildMessageIndexJob (T175834).
// Notice: currently this code is only called on CLI or in jobs, but this is not very
// obvious. messageIndex->rebuild() should never be called during web requests due to
// its slowness.
$this->messageIndex->rebuild();
}
public function handleNullRevisionInsert( TranslatableBundle $bundle, RevisionRecord $revision ): void {
if ( !$bundle instanceof MessageBundle ) {
throw new InvalidArgumentException(
'Expected $bundle to be of type MessageBundle, got ' . get_class( $bundle )
);
}
$this->revTagStore->replaceTag( $bundle->getTitle(), RevTagStore::MB_VALID_TAG, $revision->getId() );
MessageBundle::clearSourcePageCache();
}
public function delete( Title $title ): void {
$this->revTagStore->removeTags( $title, RevTagStore::MB_VALID_TAG );
$bundle = new MessageBundle( $title );
$this->messageGroupMetadata->clearMetadata( $bundle->getMessageGroupId(), self::METADATA_KEYS_DB );
MessageBundle::clearSourcePageCache();
MessageGroups::singleton()->recache();
$this->jobQueue->push( RebuildMessageIndexJob::newJob( __METHOD__ ) );
}
public function validate( Title $pageTitle, MessageBundleContent $content ): void {
$content->validate();
// Verify that the language code is valid
$metadata = $content->getMetadata();
$sourceLanguageCode = $metadata->getSourceLanguageCode();
if ( $sourceLanguageCode ) {
if ( !$this->languageNameUtils->isKnownLanguageTag( $sourceLanguageCode ) ) {
throw new MalformedBundle(
'translate-messagebundle-error-invalid-sourcelanguage', [ $sourceLanguageCode ]
);
}
$revisionId = $this->revTagStore->getLatestRevisionWithTag( $pageTitle, RevTagStore::MB_VALID_TAG );
// If request wants the source language to be changed after creation, then throw an exception
if ( $revisionId !== null && $sourceLanguageCode !== $pageTitle->getPageLanguage()->getCode() ) {
throw new MalformedBundle( 'translate-messagebundle-sourcelanguage-changed' );
}
}
$priorityLanguageCodes = $metadata->getPriorityLanguages();
if ( $priorityLanguageCodes ) {
$invalidLanguageCodes = [];
foreach ( $priorityLanguageCodes as $languageCode ) {
if ( !is_string( $languageCode ) ) {
throw new MalformedBundle( 'translate-messagebundle-error-invalid-prioritylanguage-format' );
}
if ( !$this->languageNameUtils->isKnownLanguageTag( $languageCode ) ) {
$invalidLanguageCodes[] = $languageCode;
}
}
if ( $invalidLanguageCodes ) {
throw new MalformedBundle(
'translate-messagebundle-error-invalid-prioritylanguage',
[ Message::listParam( $invalidLanguageCodes ), count( $invalidLanguageCodes ) ]
);
}
}
}
public function save(
Title $pageTitle,
RevisionRecord $revisionRecord,
MessageBundleContent $content
): void {
// Validate the content before saving
$this->validate( $pageTitle, $content );
$previousRevisionId = $this->revTagStore->getLatestRevisionWithTag( $pageTitle, RevTagStore::MB_VALID_TAG );
if ( $previousRevisionId !== null ) {
$this->revTagStore->removeTags( $pageTitle, RevTagStore::MB_VALID_TAG );
}
if ( $content->isValid() ) {
// Bundle is valid and contains translatable messages
$this->revTagStore->replaceTag( $pageTitle, RevTagStore::MB_VALID_TAG, $revisionRecord->getId() );
MessageBundle::clearSourcePageCache();
// Defer most of the heavy work to the job queue
$job = UpdateMessageBundleJob::newJob( $pageTitle, $revisionRecord->getId(), $previousRevisionId );
$this->jobQueue->push( $job );
// A new message bundle, set the source language.
$definedLanguageCode = $content->getMetadata()->getSourceLanguageCode();
$pageLanguageCode = $pageTitle->getPageLanguage()->getCode();
if ( $previousRevisionId === null ) {
if ( $definedLanguageCode !== $pageLanguageCode ) {
$context = RequestContext::getMain();
SpecialPageLanguage::changePageLanguage(
$context,
$pageTitle,
$definedLanguageCode,
wfMessage( 'translate-messagebundle-change-sourcelanguage' )->inContentLanguage()
);
}
}
// Save the metadata
$messageBundle = new MessageBundle( $pageTitle );
$groupId = $messageBundle->getMessageGroupId();
$metadata = $content->getMetadata();
$priorityForce = $metadata->areOnlyPriorityLanguagesAllowed() ? 'on' : false;
$priorityLanguages = $metadata->getPriorityLanguages();
$priorityLanguages = $priorityLanguages ? implode( ',', $priorityLanguages ) : false;
$this->messageGroupMetadata->set( $groupId, 'prioritylangs', $priorityLanguages );
$this->messageGroupMetadata->set( $groupId, 'priorityforce', $priorityForce );
$description = $metadata->getDescription();
$this->messageGroupMetadata->set( $groupId, 'description', $description ?? false );
$label = $metadata->getLabel();
$this->messageGroupMetadata->set( $groupId, 'label', $label ?? false );
}
// What should we do if there are no messages? Use the previous version? Remove the group?
// Currently, the bundle is removed from translation.
}
}