Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageGroupStatsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Statistics;
5
6use Config;
7use DeferredUpdates;
8use HTMLForm;
9use JobQueueGroup;
10use MediaWiki\Config\ServiceOptions;
14use MediaWiki\Html\Html;
15use MediaWiki\Languages\LanguageNameUtils;
16use MediaWiki\SpecialPage\SpecialPage;
18
28class MessageGroupStatsSpecialPage extends SpecialPage {
30 private bool $noComplete = true;
32 private bool $noEmpty = false;
34 private string $target;
36 private ?string $targetType = null;
37 private ServiceOptions $options;
38 private JobQueueGroup $jobQueueGroup;
39 private MessageGroupStatsTableFactory $messageGroupStatsTableFactory;
40 private EntitySearch $entitySearch;
41 private MessagePrefixStats $messagePrefixStats;
42 private LanguageNameUtils $languageNameUtils;
43 private MessageGroupMetadata $messageGroupMetadata;
44
45 private const GROUPS = 'group';
46 private const MESSAGES = 'messages';
47
48 private const CONSTRUCTOR_OPTIONS = [
49 'TranslateMessagePrefixStatsLimit',
50 ];
51
52 public function __construct(
53 Config $config,
54 JobQueueGroup $jobQueueGroup,
55 MessageGroupStatsTableFactory $messageGroupStatsTableFactory,
56 EntitySearch $entitySearch,
57 MessagePrefixStats $messagePrefixStats,
58 LanguageNameUtils $languageNameUtils,
59 MessageGroupMetadata $messageGroupMetadata
60 ) {
61 parent::__construct( 'MessageGroupStats' );
62 $this->options = new ServiceOptions( self::CONSTRUCTOR_OPTIONS, $config );
63 $this->jobQueueGroup = $jobQueueGroup;
64 $this->messageGroupStatsTableFactory = $messageGroupStatsTableFactory;
65 $this->entitySearch = $entitySearch;
66 $this->messagePrefixStats = $messagePrefixStats;
67 $this->languageNameUtils = $languageNameUtils;
68 $this->messageGroupMetadata = $messageGroupMetadata;
69 }
70
71 public function getDescription() {
72 return $this->msg( 'translate-mgs-pagename' );
73 }
74
75 public function isIncludable() {
76 return true;
77 }
78
79 protected function getGroupName() {
80 return 'translation';
81 }
82
83 public function execute( $par ) {
84 $request = $this->getRequest();
85
86 $purge = $request->getVal( 'action' ) === 'purge';
87 if ( $purge && !$request->wasPosted() ) {
88 LanguageStatsSpecialPage::showPurgeForm( $this->getContext() );
89 return;
90 }
91
92 $this->setHeaders();
93 $this->outputHeader();
94
95 $out = $this->getOutput();
96
97 $out->addModules( 'ext.translate.special.languagestats' );
98 $out->addModuleStyles( 'ext.translate.statstable' );
99 $out->addModuleStyles( 'ext.translate.special.groupstats' );
100
101 $params = $par ? explode( '/', $par ) : [];
102
103 if ( isset( $params[0] ) && trim( $params[0] ) ) {
104 $this->target = $params[0];
105 }
106
107 if ( isset( $params[1] ) ) {
108 $this->noComplete = (bool)$params[1];
109 }
110
111 if ( isset( $params[2] ) ) {
112 $this->noEmpty = (bool)$params[2];
113 }
114
115 // Whether the form has been submitted, only relevant if not including
116 $submitted = !$this->including() && $request->getVal( 'x' ) === 'D';
117
118 $this->target = $request->getVal( self::GROUPS, $this->target ?? '' );
119 if ( $this->target !== '' ) {
120 $this->targetType = self::GROUPS;
121 } else {
122 $this->target = $request->getVal( self::MESSAGES, '' );
123 if ( $this->target !== '' ) {
124 $this->targetType = self::MESSAGES;
125 }
126 }
127
128 // Default booleans to false if the form was submitted
129 $this->noComplete = $request->getBool(
130 'suppresscomplete',
131 $this->noComplete && !$submitted
132 );
133 $this->noEmpty = $request->getBool( 'suppressempty', $this->noEmpty && !$submitted );
134
135 if ( !$this->including() ) {
136 $out->addHelpLink( 'Help:Extension:Translate/Statistics_and_reporting' );
137 $this->addForm();
138 }
139
140 $stats = $output = null;
141 if ( $this->targetType === self::GROUPS && $this->isValidGroup( $this->target ) ) {
142 $this->outputIntroduction();
143
144 $stats = $this->loadStatistics( $this->target, MessageGroupStats::FLAG_CACHE_ONLY );
145
146 $messageGroupStatsTable = $this->messageGroupStatsTableFactory->newFromContext( $this->getContext() );
147 $output = $messageGroupStatsTable->get(
148 $stats,
149 MessageGroups::getGroup( $this->target ),
150 $this->noComplete,
151 $this->noEmpty
152 );
153
154 $incomplete = $messageGroupStatsTable->areStatsIncomplete();
155 if ( $incomplete ) {
156 $out->wrapWikiMsg(
157 "<div class='error'>$1</div>",
158 'translate-langstats-incomplete'
159 );
160 }
161
162 if ( $incomplete || $purge ) {
163 DeferredUpdates::addCallableUpdate( function () use ( $purge ) {
164 // Attempt to recache on the fly the missing stats, unless a
165 // purge was requested, because that is likely to time out.
166 // Even though this is executed inside a deferred update, it
167 // counts towards the maximum execution time limit. If that is
168 // reached, or any other failure happens, no updates at all
169 // will be written into the database, as it does only single
170 // update at the end. Hence we always add a job too, so that
171 // even the slower updates will get done at some point. In
172 // regular case (no purge), the job sees that the stats are
173 // already updated, so it is not much of an overhead.
174 $jobParams = $this->getCacheRebuildJobParameters( $this->target );
175 $jobParams[ 'purge' ] = $purge;
176 $job = RebuildMessageGroupStatsJob::newJob( $jobParams );
177 $this->jobQueueGroup->push( $job );
178
179 // $purge is only true if request was posted
180 if ( !$purge ) {
181 $this->loadStatistics( $this->target );
182 }
183 } );
184 }
185 } elseif ( $this->targetType === self::MESSAGES ) {
186 $messagesWithPrefix = $this->entitySearch->matchMessages( $this->target );
187 if ( $messagesWithPrefix ) {
188 $messageWithPrefixLimit = $this->options->get( 'TranslateMessagePrefixStatsLimit' );
189 if ( count( $messagesWithPrefix ) > $messageWithPrefixLimit ) {
190 $out->addHTML(
191 Html::errorBox(
192 $this->msg( 'translate-mgs-message-prefix-limit' )
193 ->params( $messageWithPrefixLimit )
194 ->parse()
195 )
196 );
197 return;
198 }
199
200 $stats = $this->messagePrefixStats->forAll( ...$messagesWithPrefix );
201 $messageGroupStatsTable = $this->messageGroupStatsTableFactory
202 ->newFromContext( $this->getContext() );
203 $output = $messageGroupStatsTable->get(
204 $stats,
206 $this->noComplete,
207 $this->noEmpty
208 );
209 }
210 }
211
212 if ( $output ) {
213 // If output is present, put it on the page
214 $out->addHTML( $output );
215 } elseif ( $stats !== null ) {
216 // Output not present, but stats are present. Probably an issue?
217 $out->addHTML( Html::warningBox( $this->msg( 'translate-mgs-nothing' )->parse() ) );
218 } elseif ( $submitted ) {
219 $this->invalidTarget();
220 }
221 }
222
223 private function loadStatistics( string $target, int $flags = 0 ): array {
224 return MessageGroupStats::forGroup( $target, $flags );
225 }
226
227 private function getCacheRebuildJobParameters( string $target ): array {
228 return [ 'groupid' => $target ];
229 }
230
231 private function isValidGroup( ?string $value ): bool {
232 if ( $value === null ) {
233 return false;
234 }
235
236 $group = MessageGroups::getGroup( $value );
237 if ( $group ) {
238 if ( MessageGroups::isDynamic( $group ) ) {
239 /* Dynamic groups are not listed, but it is possible to end up
240 * on this page with a dynamic group by navigating from
241 * translation or proofreading activity or by giving group id
242 * of dynamic group explicitly. Ignore dynamic group to avoid
243 * throwing exceptions later. */
244 $group = false;
245 } else {
246 $this->target = $group->getId();
247 }
248 }
249
250 return (bool)$group;
251 }
252
253 private function invalidTarget(): void {
254 $this->getOutput()->wrapWikiMsg(
255 "<div class='error'>$1</div>",
256 [ 'translate-mgs-invalid-group', $this->target ]
257 );
258 }
259
260 private function outputIntroduction(): void {
261 $priorityLangs = $this->messageGroupMetadata->get( $this->target, 'prioritylangs' );
262 if ( $priorityLangs ) {
263 $languagesFormatted = $this->formatLanguageList( explode( ',', $priorityLangs ) );
264 $hasPriorityForce = $this->messageGroupMetadata->get( $this->target, 'priorityforce' ) === 'on';
265 if ( $hasPriorityForce ) {
266 $this->getOutput()->addWikiMsg( 'tpt-priority-languages-force', $languagesFormatted );
267 } else {
268 $this->getOutput()->addWikiMsg( 'tpt-priority-languages', $languagesFormatted );
269 }
270 }
271 }
272
273 private function formatLanguageList( array $codes ): string {
274 foreach ( $codes as &$value ) {
275 $value = $this->languageNameUtils->getLanguageName( $value, $this->getLanguage()->getCode() )
276 . $this->msg( 'word-separator' )->plain()
277 . $this->msg( 'parentheses', $value )->plain();
278 }
279
280 return $this->getLanguage()->listToText( $codes );
281 }
282
283 private function addForm(): void {
284 $formDescriptor = [
285 'select' => [
286 'type' => 'select',
287 'name' => self::GROUPS,
288 'id' => self::GROUPS,
289 'label' => $this->msg( 'translate-mgs-group' )->text(),
290 'options' => $this->getGroupOptions(),
291 'default' => $this->targetType === self::GROUPS ? $this->target : null,
292 'cssclass' => 'message-group-selector'
293 ],
294 'input' => [
295 'type' => 'text',
296 'name' => self::MESSAGES,
297 'id' => self::MESSAGES,
298 'label' => $this->msg( 'translate-mgs-prefix' )->text(),
299 'default' => $this->targetType === self::MESSAGES ? $this->target : null,
300 'cssclass' => 'message-prefix-selector'
301 ],
302 'nocomplete-check' => [
303 'type' => 'check',
304 'name' => 'suppresscomplete',
305 'id' => 'suppresscomplete',
306 'label' => $this->msg( 'translate-mgs-nocomplete' )->text(),
307 'default' => $this->noComplete,
308 ],
309 'noempty-check' => [
310 'type' => 'check',
311 'name' => 'suppressempty',
312 'id' => 'suppressempty',
313 'label' => $this->msg( 'translate-mgs-noempty' )->text(),
314 'default' => $this->noEmpty,
315 ]
316 ];
317
318 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
319
320 /* Since these pages are in the tabgroup with Special:Translate,
321 * it makes sense to retain the selected group/language parameter
322 * on post requests even when not relevant to the current page. */
323 $val = $this->getRequest()->getVal( 'language' );
324 if ( $val !== null ) {
325 $htmlForm->addHiddenField( 'language', $val );
326 }
327
328 $htmlForm
329 ->addHiddenField( 'x', 'D' ) // To detect submission
330 ->setMethod( 'get' )
331 ->setId( 'mw-message-group-stats-form' )
332 ->setSubmitTextMsg( 'translate-mgs-submit' )
333 ->setWrapperLegendMsg( 'translate-mgs-fieldset' )
334 ->prepareForm()
335 ->displayForm( false );
336 }
337
339 private function getGroupOptions(): array {
340 $options = [ '' => null ];
341 $groups = MessageGroups::getAllGroups();
342
343 foreach ( $groups as $id => $class ) {
344 if ( MessageGroups::getGroup( $id )->exists() ) {
345 $options[$class->getLabel()] = $id;
346 }
347 }
348
349 return $options;
350 }
351}
Factory class for accessing message groups individually by id or all of them as a list.
Offers functionality for reading and updating Translate group related metadata.
Implements includable special page Special:MessageGroupStats which provides translation statistics fo...
const FLAG_CACHE_ONLY
If stats are not cached, do not attempt to calculate them on the fly.
static forGroup(string $groupId, int $flags=0)
Returns stats for all languages in given group.
This class abstracts MessagePrefix statistics calculation and storing.
Service for searching message groups and message keys.
Contains an unmanaged message group for fetching stats using message prefixes.