wikimedia/mediawiki-extensions-Translate

View on GitHub
src/Diagnostics/FindUnsynchronizedDefinitionsMaintenanceScript.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\Diagnostics;

use FileBasedMessageGroup;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript;
use MediaWiki\Shell\Shell;
use MediaWiki\Title\Title;

/**
 * @since 2021.01
 * @license GPL-2.0-or-later
 * @author Niklas Laxström
 */
class FindUnsynchronizedDefinitionsMaintenanceScript extends BaseMaintenanceScript {
    public function __construct() {
        parent::__construct();
        $this->addDescription(
            'This scripts finds definition pages in the wiki that do not have the expected ' .
            'content with regards to the message group definition cache for file based message ' .
            'groups. This causes the definition diff to appear for translations when it should ' .
            'not. See https://phabricator.wikimedia.org/T270844'
        );

        $this->addArg(
            'group-pattern',
            'For example page-*,main',
            self::REQUIRED
        );
        $this->addOption(
            'ignore-trailing-whitespace',
            'Ignore trailing whitespace',
            self::OPTIONAL,
            self::NO_ARG,
            'w'
        );
        $this->addOption(
            'fix',
            'Try to fix the issues by triggering reprocessing'
        );

        $this->requireExtension( 'Translate' );
    }

    /** @inheritDoc */
    public function execute() {
        $ignoreTrailingWhitespace = $this->getOption( 'ignore-trailing-whitespace' );
        $groups = $this->getGroups( $this->getArg( 0 ) );
        $matched = count( $groups );
        $this->output( "Pattern matched $matched file based message group(s).\n" );
        $this->output( "Left side is the expected value. Right side is the actual value in wiki.\n" );

        $groupsWithIssues = [];
        foreach ( $groups as $group ) {
            $sourceLanguage = $group->getSourceLanguage();
            $collection = $group->initCollection( $sourceLanguage );
            $collection->loadTranslations();

            foreach ( $collection->keys() as $mkey => $title ) {
                $message = $collection[$mkey];
                $definition = $message->definition() ?? '';
                $translation = $message->translation() ?? '';

                $differs = $ignoreTrailingWhitespace
                    ? rtrim( $definition ) !== $translation
                    : $definition !== $translation;

                if ( $differs ) {
                    $groupsWithIssues[$group->getId()] = $group;
                    echo Title::newFromLinkTarget( $title )->getPrefixedText() . "\n";
                    echo $this->getSideBySide( "'$definition'", "'$translation'", 80 ) . "\n";
                }
            }
        }

        if ( $this->hasOption( 'fix' ) && $groupsWithIssues ) {
            foreach ( $groupsWithIssues as $group ) {
                $cache = $group->getMessageGroupCache( $group->getSourceLanguage() );
                $cache->invalidate();
            }
            $script = realpath( __DIR__ . '/../../scripts/importExternalTranslations.php' );
            $groupPattern = implode( ',', array_keys( $groupsWithIssues ) );
            $command = Shell::makeScriptCommand( $script, [ '--group', $groupPattern ] )->getCommandString();
            echo "Now run the following command and finish the sync in the wiki:\n$command\n";
        }
    }

    /** @return FileBasedMessageGroup[] */
    private function getGroups( string $patternList ): array {
        $patterns = array_map( 'trim', explode( ',', $patternList ) );
        $groupIds = MessageGroups::expandWildcards( $patterns );
        $groups = MessageGroups::getGroupsById( $groupIds );

        foreach ( $groups as $index => $group ) {
            if ( !$group instanceof FileBasedMessageGroup ) {
                unset( $groups[$index] );
            }
        }

        // @phan-suppress-next-line PhanTypeMismatchReturn
        return $groups;
    }

    private function getSideBySide( string $a, string $b, int $width ): string {
        $wrapWidth = (int)floor( ( $width - 3 ) / 2 );
        $aArray = explode( "\n", wordwrap( $a, $wrapWidth, "\n", true ) );
        $bArray = explode( "\n", wordwrap( $b, $wrapWidth, "\n", true ) );
        $lines = max( count( $aArray ), count( $bArray ) );

        $out = '';
        for ( $i = 0; $i < $lines; $i++ ) {
            $out .= sprintf(
                "%-{$wrapWidth}s | %-{$wrapWidth}s\n",
                $aArray[$i] ?? '',
                $bArray[$i] ?? ''
            );
        }
        return $out;
    }
}