Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
RefreshTranslationProgressStatsMaintenanceScript
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 4
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
 getLanguages
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 cliProgressBar
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Diagnostics;
5
6use Language;
7use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
8use MediaWiki\Extension\Translate\MessageProcessing\StringMatcher;
9use MediaWiki\Extension\Translate\Statistics\RebuildMessageGroupStatsJob;
10use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript;
11use MediaWiki\Language\RawMessage;
12use TranslateUtils;
13
14/**
15 * This maintenance script is responsible for refreshing the cached translation progress stats
16 * to fix any outdated entries.
17 *
18 * @author Niklas Laxström
19 * @license GPL-2.0-or-later
20 * @since 2024.02
21 */
22class RefreshTranslationProgressStatsMaintenanceScript extends BaseMaintenanceScript {
23    private Language $language;
24
25    public function __construct() {
26        parent::__construct();
27        $this->addDescription( 'Refresh cached message group stats to fix any outdated entries' );
28
29        $this->addOption(
30            'group',
31            'Which groups to scan. Comma-separated list with wildcard (*) accepted.',
32            self::OPTIONAL,
33            self::HAS_ARG
34        );
35        $this->addOption(
36            'language',
37            'Which languages to scan. Comma-separated list with wildcard (*) accepted.',
38            self::OPTIONAL,
39            self::HAS_ARG
40        );
41
42        $this->addOption(
43            'use-job-queue',
44            'Use job queue (asynchronous).',
45            self::OPTIONAL,
46            self::NO_ARG
47        );
48
49        $this->requireExtension( 'Translate' );
50    }
51
52    /** @inheritDoc */
53    public function execute() {
54        $this->language = $this->getServiceContainer()->getContentLanguage();
55
56        $groupInput = $this->getOption( 'group', '*' );
57        $groupIdPattern = self::commaList2Array( $groupInput );
58        $groupIds = MessageGroups::expandWildcards( $groupIdPattern );
59
60        // All language codes are internally lower case in MediaWiki
61        $languageInput = strtolower( $this->getOption( 'language', '*' ) );
62        $languagePattern = self::commaList2Array( $languageInput );
63        $languages = $this->getLanguages( $languagePattern );
64
65        $useJobQueue = $this->hasOption( 'use-job-queue' );
66        $jobQueueGroup = $this->getServiceContainer()->getJobQueueGroup();
67        $jobCount = count( $groupIds ) * count( $languages );
68        $counter = 0;
69
70        $startTime = microtime( true );
71
72        foreach ( $groupIds as $groupId ) {
73            $jobs = [];
74            foreach ( $languages as $language ) {
75                $jobs[] = $job = RebuildMessageGroupStatsJob::newJob( [
76                    RebuildMessageGroupStatsJob::REFRESH => true,
77                    RebuildMessageGroupStatsJob::GROUP_ID => $groupId,
78                    RebuildMessageGroupStatsJob::LANGUAGE_CODE => $language
79                ] );
80
81                if ( !$useJobQueue ) {
82                    $job->run();
83                }
84                $elapsed = microtime( true ) - $startTime;
85                $this->output( "\033[0K\r" . $this->cliProgressBar( $jobCount, ++$counter, $elapsed ) );
86            }
87
88            if ( $useJobQueue ) {
89                $jobQueueGroup->push( $jobs );
90            }
91
92        }
93
94        if ( $useJobQueue ) {
95            $message = new RawMessage( "\nQueued {{PLURAL:$1|$1 job|$1 jobs}}.", [ $jobCount ] );
96            $this->output( $message->text() );
97        }
98        $this->output( "\n" );
99    }
100
101    private function getLanguages( array $patterns ): array {
102        $allLanguages = array_keys( TranslateUtils::getLanguageNames( null ) );
103
104        if ( in_array( '*', $patterns, true ) ) {
105            return $allLanguages;
106        }
107
108        $matchedLanguages = array_intersect( $patterns, $allLanguages );
109        $patterns = array_diff( $patterns, $matchedLanguages );
110        $remainingLanguages = array_diff( $allLanguages, $matchedLanguages );
111
112        if ( $patterns === [] ) {
113            return $matchedLanguages;
114        }
115
116        // Slow path for the ones with wildcards
117        $matcher = new StringMatcher( '', $patterns );
118        foreach ( $remainingLanguages as $id ) {
119            if ( $matcher->matches( $id ) ) {
120                $matchedLanguages[] = $id;
121            }
122        }
123
124        return $matchedLanguages;
125    }
126
127    private function cliProgressBar( int $total, int $current, float $elapsed ): string {
128        $barLength = 50;
129        $progress = $current / $total;
130        $filledLength = (int)round( $barLength * $progress );
131        $bar = str_repeat( '#', $filledLength ) . str_repeat( '-', ( $barLength - $filledLength ) );
132        $percent = $current / $total * 100;
133        $estimatedRemaining = $this->language->formatDuration( (int)( $elapsed / $progress - $elapsed ) );
134
135        return sprintf( 'Progress: [%s] %.2f%% | %s remaining', $bar, $percent, $estimatedRemaining );
136    }
137}