wikimedia/mediawiki-extensions-Translate

View on GitHub
src/MessageBundleTranslation/MessageBundleStore.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?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.
    }
}