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