Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FindUnsynchronizedDefinitionsMaintenanceScript
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 4
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
72
 getGroups
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getSideBySide
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Diagnostics;
5
6use FileBasedMessageGroup;
7use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
8use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript;
9use MediaWiki\Shell\Shell;
10use MediaWiki\Title\Title;
11
12/**
13 * @since 2021.01
14 * @license GPL-2.0-or-later
15 * @author Niklas Laxström
16 */
17class FindUnsynchronizedDefinitionsMaintenanceScript extends BaseMaintenanceScript {
18    public function __construct() {
19        parent::__construct();
20        $this->addDescription(
21            'This scripts finds definition pages in the wiki that do not have the expected ' .
22            'content with regards to the message group definition cache for file based message ' .
23            'groups. This causes the definition diff to appear for translations when it should ' .
24            'not. See https://phabricator.wikimedia.org/T270844'
25        );
26
27        $this->addArg(
28            'group-pattern',
29            'For example page-*,main',
30            self::REQUIRED
31        );
32        $this->addOption(
33            'ignore-trailing-whitespace',
34            'Ignore trailing whitespace',
35            self::OPTIONAL,
36            self::NO_ARG,
37            'w'
38        );
39        $this->addOption(
40            'fix',
41            'Try to fix the issues by triggering reprocessing'
42        );
43
44        $this->requireExtension( 'Translate' );
45    }
46
47    /** @inheritDoc */
48    public function execute() {
49        $ignoreTrailingWhitespace = $this->getOption( 'ignore-trailing-whitespace' );
50        $groups = $this->getGroups( $this->getArg( 0 ) );
51        $matched = count( $groups );
52        $this->output( "Pattern matched $matched file based message group(s).\n" );
53        $this->output( "Left side is the expected value. Right side is the actual value in wiki.\n" );
54
55        $groupsWithIssues = [];
56        foreach ( $groups as $group ) {
57            $sourceLanguage = $group->getSourceLanguage();
58            $collection = $group->initCollection( $sourceLanguage );
59            $collection->loadTranslations();
60
61            foreach ( $collection->keys() as $mkey => $title ) {
62                $message = $collection[$mkey];
63                $definition = $message->definition() ?? '';
64                $translation = $message->translation() ?? '';
65
66                $differs = $ignoreTrailingWhitespace
67                    ? rtrim( $definition ) !== $translation
68                    : $definition !== $translation;
69
70                if ( $differs ) {
71                    $groupsWithIssues[$group->getId()] = $group;
72                    echo Title::newFromLinkTarget( $title )->getPrefixedText() . "\n";
73                    echo $this->getSideBySide( "'$definition'", "'$translation'", 80 ) . "\n";
74                }
75            }
76        }
77
78        if ( $this->hasOption( 'fix' ) && $groupsWithIssues ) {
79            foreach ( $groupsWithIssues as $group ) {
80                $cache = $group->getMessageGroupCache( $group->getSourceLanguage() );
81                $cache->invalidate();
82            }
83            $script = realpath( __DIR__ . '/../../scripts/importExternalTranslations.php' );
84            $groupPattern = implode( ',', array_keys( $groupsWithIssues ) );
85            $command = Shell::makeScriptCommand( $script, [ '--group', $groupPattern ] )->getCommandString();
86            echo "Now run the following command and finish the sync in the wiki:\n$command\n";
87        }
88    }
89
90    /** @return FileBasedMessageGroup[] */
91    private function getGroups( string $patternList ): array {
92        $patterns = array_map( 'trim', explode( ',', $patternList ) );
93        $groupIds = MessageGroups::expandWildcards( $patterns );
94        $groups = MessageGroups::getGroupsById( $groupIds );
95
96        foreach ( $groups as $index => $group ) {
97            if ( !$group instanceof FileBasedMessageGroup ) {
98                unset( $groups[$index] );
99            }
100        }
101
102        // @phan-suppress-next-line PhanTypeMismatchReturn
103        return $groups;
104    }
105
106    private function getSideBySide( string $a, string $b, int $width ): string {
107        $wrapWidth = (int)floor( ( $width - 3 ) / 2 );
108        $aArray = explode( "\n", wordwrap( $a, $wrapWidth, "\n", true ) );
109        $bArray = explode( "\n", wordwrap( $b, $wrapWidth, "\n", true ) );
110        $lines = max( count( $aArray ), count( $bArray ) );
111
112        $out = '';
113        for ( $i = 0; $i < $lines; $i++ ) {
114            $out .= sprintf(
115                "%-{$wrapWidth}s | %-{$wrapWidth}s\n",
116                $aArray[$i] ?? '',
117                $bArray[$i] ?? ''
118            );
119        }
120        return $out;
121    }
122}