Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageGroupStatsTable
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 7
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 get
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
42
 areStatsIncomplete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeRow
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
156
 getMainColumnCell
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getWorkflowStateCell
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 filterPriorityLangs
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Statistics;
5
6use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupReviewStore;
7use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
8use MediaWiki\Extension\Translate\Utilities\Utilities;
9use MediaWiki\Html\Html;
10use MediaWiki\Language\Language;
11use MediaWiki\Linker\LinkRenderer;
12use MediaWiki\SpecialPage\SpecialPage;
13use MediaWiki\Title\Title;
14use MessageGroup;
15use MessageLocalizer;
16
17/**
18 * Used to build the table displayed on Special:MessageGroupStats
19 * @author Abijeet Patro
20 * @since 2023.01
21 * @license GPL-2.0-or-later
22 */
23class MessageGroupStatsTable {
24    private LinkRenderer $linkRenderer;
25    private MessageLocalizer $localizer;
26    private Language $interfaceLanguage;
27    private StatsTable $table;
28    private MessageGroupReviewStore $groupReviewStore;
29    private MessageGroupMetadata $messageGroupMetadata;
30    /** Flag to set if not all numbers are available. */
31    private bool $incompleteStats;
32    private array $languageNames;
33    private Title $translateTitle;
34    /** Keys are state names and values are numbers */
35    private array $states;
36    private bool $haveTranslateWorkflowStates;
37
38    public function __construct(
39        StatsTable $table,
40        LinkRenderer $linkRenderer,
41        MessageLocalizer $localizer,
42        Language $interfaceLanguage,
43        MessageGroupReviewStore $groupReviewStore,
44        MessageGroupMetadata $messageGroupMetadata,
45        bool $haveTranslateWorkflowStates
46    ) {
47        $this->table = $table;
48        $this->linkRenderer = $linkRenderer;
49        $this->incompleteStats = false;
50        $this->localizer = $localizer;
51        $this->interfaceLanguage = $interfaceLanguage;
52        $this->groupReviewStore = $groupReviewStore;
53        $this->messageGroupMetadata = $messageGroupMetadata;
54        $this->haveTranslateWorkflowStates = $haveTranslateWorkflowStates;
55        $this->languageNames = Utilities::getLanguageNames( $this->interfaceLanguage->getCode() );
56        $this->translateTitle = SpecialPage::getTitleFor( 'Translate' );
57    }
58
59    public function get(
60        array $stats,
61        MessageGroup $group,
62        bool $noComplete,
63        bool $noEmpty
64    ): ?string {
65        $out = '';
66        $rowCount = 0;
67        $totals = MessageGroupStats::getEmptyStats();
68        $groupId = $group->getId();
69
70        $languages = array_keys(
71            Utilities::getLanguageNames( $this->interfaceLanguage->getCode() )
72        );
73        sort( $languages );
74        $this->filterPriorityLangs( $languages, $groupId, $stats );
75
76        // If workflow states are configured, adds a workflow states column
77        if ( $this->haveTranslateWorkflowStates ) {
78            $this->table->addExtraColumn( $this->localizer->msg( 'translate-stats-workflow' ) );
79        }
80
81        foreach ( $languages as $code ) {
82            if ( $this->table->isExcluded( $group, $code ) ) {
83                continue;
84            }
85
86            $languageStats = $stats[$code];
87            $row = $this->makeRow(
88                $this->table,
89                $code,
90                $languageStats,
91                $group,
92                $rowCount,
93                $noComplete,
94                $noEmpty
95            );
96            if ( $row ) {
97                $rowCount += 1;
98                $out .= $row;
99                $totals = MessageGroupStats::multiAdd( $totals, $languageStats );
100            }
101        }
102
103        if ( $out ) {
104            $this->table->setMainColumnHeader( $this->localizer->msg( 'translate-mgs-column-language' ) );
105            $out = $this->table->createHeader() . "\n" . $out;
106            $out .= Html::closeElement( 'tbody' );
107
108            $out .= Html::openElement( 'tfoot' );
109            $out .= $this->table->makeTotalRow(
110                $this->localizer->msg( 'translate-mgs-totals' )->numParams( $rowCount ),
111                $totals
112            );
113            $out .= Html::closeElement( 'tfoot' );
114
115            $out .= Html::closeElement( 'table' );
116
117            return $out;
118        } else {
119            return null;
120        }
121    }
122
123    public function areStatsIncomplete(): bool {
124        return $this->incompleteStats;
125    }
126
127    private function makeRow(
128        StatsTable $table,
129        string $languageCode,
130        array $stats,
131        MessageGroup $group,
132        int $rowCount,
133        bool $noComplete,
134        bool $noEmpty
135    ): ?string {
136        $total = $stats[MessageGroupStats::TOTAL];
137        $translated = $stats[MessageGroupStats::TRANSLATED];
138        $fuzzy = $stats[MessageGroupStats::FUZZY];
139        $extra = [];
140
141        if ( $total === null ) {
142            $this->incompleteStats = true;
143        } else {
144            if ( $noComplete && $fuzzy === 0 && $translated === $total ) {
145                return null;
146            }
147
148            if ( $noEmpty && $translated === 0 && $fuzzy === 0 ) {
149                return null;
150            }
151
152            // Skip below 2% if "don't show without translations" is checked.
153            if ( $noEmpty && ( $translated / $total ) < 0.02 ) {
154                return null;
155            }
156
157            if ( $translated === $total ) {
158                $extra = [ 'action' => 'proofread' ];
159            }
160        }
161
162        $rowParams = [];
163        if ( $rowCount % 2 === 0 ) {
164            $rowParams[ 'class' ] = 'tux-statstable-even';
165        }
166
167        $out = "\t" . Html::openElement( 'tr', $rowParams );
168        $out .= "\n\t\t" . $this->getMainColumnCell( $languageCode, $extra, $group->getId() );
169        $out .= $table->makeNumberColumns( $stats );
170        $out .= $this->getWorkflowStateCell( $table, $languageCode, $group );
171
172        $out .= "\n\t" . Html::closeElement( 'tr' ) . "\n";
173
174        return $out;
175    }
176
177    private function getMainColumnCell( string $code, array $params, string $groupId ): string {
178        if ( isset( $this->languageNames[$code] ) ) {
179            $text = "$code{$this->languageNames[$code]}";
180        } else {
181            $text = $code;
182        }
183
184        // Do not render links when generating table for MessagePrefixMessageGroup
185        // as this is a dynamic group whose contents are based on user input
186        if ( $groupId === '!prefix' ) {
187            return Html::rawElement( 'td', [], $text );
188        }
189
190        $queryParameters = $params + [
191            'group' => $groupId,
192            'language' => $code
193        ];
194
195        $link = $this->linkRenderer->makeKnownLink(
196            $this->translateTitle,
197            $text,
198            [],
199            $queryParameters
200        );
201
202        return Html::rawElement( 'td', [], $link );
203    }
204
205    /** If workflow states are configured, adds a cell with the workflow state to the row */
206    private function getWorkflowStateCell( StatsTable $table, string $language, MessageGroup $group ): string {
207        if ( !$this->haveTranslateWorkflowStates ) {
208            return '';
209        }
210
211        $this->states ??= $this->groupReviewStore->getWorkflowStatesForGroup( $group->getId() );
212        return $table->makeWorkflowStateCell( $this->states[$language] ?? null, $group, $language );
213    }
214
215    /**
216     * Filter an array of languages based on whether a priority set of
217     * languages present for the passed group. If priority languages are
218     * present, to that list add languages with more than 0% translation.
219     */
220    private function filterPriorityLangs( array &$languages, string $group, array $cache ): void {
221        $filterLangs = $this->messageGroupMetadata->get( $group, 'prioritylangs' );
222        if ( $filterLangs === false || strlen( $filterLangs ) === 0 ) {
223            // No restrictions, keep everything
224            return;
225        }
226        $filter = array_flip( explode( ',', $filterLangs ) );
227        foreach ( $languages as $id => $code ) {
228            if ( isset( $filter[$code] ) ) {
229                continue;
230            }
231            $translated = $cache[$code][1];
232            if ( $translated === 0 ) {
233                unset( $languages[$id] );
234            }
235        }
236    }
237}