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