41 private $languageNameUtils;
45 private $targetValueName = [
'code',
'language' ];
52 private $nothing =
false;
57 private $incomplete =
false;
62 private $noComplete =
true;
67 private $noEmpty =
false;
81 private $statsCounted = [];
85 private $linkBatchFactory;
87 private $progressStatsTableFactory;
89 private $jobQueueGroup;
91 private $loadBalancer;
93 public function __construct(
94 LinkBatchFactory $linkBatchFactory,
96 LanguageNameUtils $languageNameUtils,
97 JobQueueGroup $jobQueueGroup,
98 ILoadBalancer $loadBalancer
100 parent::__construct(
'LanguageStats' );
101 $this->totals = MessageGroupStats::getEmptyStats();
102 $this->linkBatchFactory = $linkBatchFactory;
103 $this->progressStatsTableFactory = $progressStatsTableFactory;
104 $this->languageNameUtils = $languageNameUtils;
105 $this->jobQueueGroup = $jobQueueGroup;
106 $this->loadBalancer = $loadBalancer;
109 public function isIncludable() {
113 protected function getGroupName() {
114 return 'translation';
117 public function execute( $par ) {
118 $this->target = $this->getLanguage()->getCode();
119 $request = $this->getRequest();
121 $this->purge = $request->getVal(
'action' ) ===
'purge';
122 if ( $this->purge && !$request->wasPosted() ) {
123 self::showPurgeForm( $this->getContext() );
127 $this->table = $this->progressStatsTableFactory->newFromContext( $this->getContext() );
130 $this->outputHeader();
132 $out = $this->getOutput();
134 $out->addModules(
'ext.translate.special.languagestats' );
135 $out->addModuleStyles(
'ext.translate.statstable' );
137 $params = $par ? explode(
'/', $par ) : [];
139 if ( isset( $params[0] ) && trim( $params[0] ) ) {
140 $this->target = $params[0];
143 if ( isset( $params[1] ) ) {
144 $this->noComplete = (bool)$params[1];
147 if ( isset( $params[2] ) ) {
148 $this->noEmpty = (bool)$params[2];
152 $submitted = !$this->including() && $request->getVal(
'x' ) ===
'D';
155 foreach ( $this->targetValueName as $key ) {
156 $this->target = $request->getVal( $key, $this->target );
158 $this->noComplete = $request->getBool(
160 $this->noComplete && !$submitted
162 $this->noEmpty = $request->getBool(
'suppressempty', $this->noEmpty && !$submitted );
164 if ( !$this->including() ) {
165 $out->addHelpLink(
'Help:Extension:Translate/Statistics_and_reporting' );
169 if ( $this->isValidValue( $this->target ) ) {
170 $this->outputIntroduction();
172 $stats = $this->loadStatistics( $this->target, MessageGroupStats::FLAG_CACHE_ONLY );
173 $output = $this->getTable( $stats );
174 if ( $this->incomplete ) {
176 "<div class='error'>$1</div>",
177 'translate-langstats-incomplete'
181 if ( $this->incomplete || $this->purge ) {
182 DeferredUpdates::addCallableUpdate(
function () {
193 $jobParams = $this->getCacheRebuildJobParameters( $this->target );
194 $jobParams[
'purge' ] = $this->purge;
195 $this->jobQueueGroup->push( MessageGroupStatsRebuildJob::newJob( $jobParams ) );
198 if ( !$this->purge ) {
199 $this->loadStatistics( $this->target );
203 if ( $this->nothing ) {
204 $out->wrapWikiMsg(
"<div class='error'>$1</div>",
'translate-mgs-nothing' );
206 $out->addHTML( $output );
207 } elseif ( $submitted ) {
208 $this->invalidTarget();
218 private function loadStatistics(
string $target,
int $flags = 0 ): array {
219 return MessageGroupStats::forLanguage( $target, $flags );
222 private function getCacheRebuildJobParameters( $target ): array {
223 return [
'languagecode' => $target ];
227 private function isValidValue(
string $value ):
bool {
228 $langs = $this->languageNameUtils->getLanguageNames();
230 return isset( $langs[$value] );
234 private function invalidTarget():
void {
235 $this->getOutput()->wrapWikiMsg(
236 "<div class='error'>$1</div>",
237 'translate-page-no-such-language'
241 public static function showPurgeForm( IContextSource $context ):
void {
245 'vertical-label' =>
true,
247 'default' => $context->msg(
'confirm-purge-top' )->parse()
251 $derivativeContext =
new DerivativeContext( $context );
252 $requestValues = $derivativeContext->getRequest()->getQueryValues();
254 HTMLForm::factory(
'ooui', $formDescriptor, $derivativeContext )
255 ->setWrapperLegendMsg(
'confirm-purge-title' )
256 ->setSubmitTextMsg(
'confirm_purge_button' )
257 ->addHiddenFields( $requestValues )
262 private function addForm():
void {
266 'name' =>
'language',
268 'label' => $this->msg(
'translate-language-code-field-name' )->text(),
270 'default' => $this->target,
272 'suppresscomplete' => [
274 'label' => $this->msg(
'translate-suppress-complete' )->text(),
275 'name' =>
'suppresscomplete',
276 'id' =>
'suppresscomplete',
277 'default' => $this->noComplete,
281 'label' => $this->msg(
'translate-ls-noempty' )->text(),
282 'name' =>
'suppressempty',
283 'id' =>
'suppressempty',
284 'default' => $this->noEmpty,
288 $context =
new DerivativeContext( $this->getContext() );
289 $context->setTitle( $this->getPageTitle() );
291 $htmlForm = HTMLForm::factory(
'ooui', $formDescriptor, $context );
296 $val = $this->getRequest()->getVal(
'group' );
297 if ( $val !==
null ) {
298 $htmlForm->addHiddenField(
'group', $val );
302 ->addHiddenField(
'x',
'D' )
304 ->setSubmitTextMsg(
'translate-ls-submit' )
305 ->setWrapperLegendMsg(
'translate-mgs-fieldset' )
307 ->displayForm(
false );
311 private function outputIntroduction():
void {
312 $languageName = Utilities::getLanguageName(
314 $this->getLanguage()->getCode()
317 $rcInLangLink = $this->getLinkRenderer()->makeKnownLink(
318 SpecialPage::getTitleFor(
'Translate',
'!recent' ),
319 $this->msg(
'languagestats-recenttranslations' )->text(),
322 'action' =>
'proofread',
323 'language' => $this->target
327 $out = $this->msg(
'languagestats-stats-for', $languageName )->rawParams( $rcInLangLink )
329 $this->getOutput()->addHTML( $out );
333 private function addWorkflowStatesColumn():
void {
334 global $wgTranslateWorkflowStates;
336 if ( $wgTranslateWorkflowStates ) {
337 $this->states = $this->getWorkflowStates();
340 $this->table->addExtraColumn( $this->msg(
'translate-stats-workflow' ) );
344 private function getWorkflowStateCell(
string $messageGroupId ):
string {
346 if ( !isset( $this->states ) ) {
350 return $this->table->makeWorkflowStateCell(
351 $this->states[$messageGroupId] ??
null,
352 MessageGroups::getGroup( $messageGroupId ),
357 private function getTable( array $stats ):
string {
358 $table = $this->table;
360 $this->addWorkflowStatesColumn();
365 $lb = $this->linkBatchFactory->newLinkBatch();
366 foreach ( MessageGroups::getAllGroups() as $group ) {
368 $lb->addObj( $group->getTitle() );
371 $lb->setCaller( __METHOD__ )->execute();
373 $structure = MessageGroups::getGroupStructure();
374 foreach ( $structure as $item ) {
375 $out .= $this->makeGroupGroup( $item, $stats );
379 $table->setMainColumnHeader( $this->msg(
'translate-ls-column-group' ) );
380 $out = $table->createHeader() .
"\n" . $out;
381 $out .= Html::closeElement(
'tbody' );
383 $out .= Html::openElement(
'tfoot' );
384 $out .= $table->makeTotalRow(
385 $this->msg(
'translate-languagestats-overall' ),
388 $out .= Html::closeElement(
'tfoot' );
390 $out .= Html::closeElement(
'table' );
394 $this->nothing =
true;
409 private function makeGroupGroup( $item, array $cache,
MessageGroup $parent =
null ):
string {
410 if ( !is_array( $item ) ) {
411 return $this->makeGroupRow( $item, $cache, $parent );
416 $top = array_shift( $item );
417 $out .= $this->makeGroupRow( $top, $cache, $parent );
420 foreach ( $item as $subgroup ) {
421 $out .= $this->makeGroupGroup( $subgroup, $cache, $top );
431 private function makeGroupRow(
436 $groupId = $group->
getId();
438 if ( $this->table->isExcluded( $group, $this->target ) ) {
442 $stats = $cache[$groupId];
443 $total = $stats[MessageGroupStats::TOTAL];
444 $translated = $stats[MessageGroupStats::TRANSLATED];
445 $fuzzy = $stats[MessageGroupStats::FUZZY];
448 if ( $this->noComplete && $fuzzy === 0 && $translated === $total ) {
451 if ( $this->noEmpty && $translated === 0 && $fuzzy === 0 ) {
455 if ( $total ===
null ) {
456 $this->incomplete =
true;
461 !isset( $this->statsCounted[$groupId] )
463 $this->totals = MessageGroupStats::multiAdd( $this->totals, $stats );
464 $this->statsCounted[$groupId] =
true;
469 $params[] = $this->states[$groupId] ??
'';
470 $params[] = md5( $groupId );
471 $params[] = $this->getLanguage()->getCode();
472 $params[] = md5( $this->target );
473 $params[] = $parent ? $parent->getId() :
'!';
475 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
477 return $cache->getWithSetCallback(
478 $cache->makeKey( __METHOD__ .
'-v3', implode(
'-', $params ) ),
480 function () use ( $translated, $total, $groupId, $group, $parent, $stats ) {
483 if ( $translated === $total ) {
484 $extra = [
'action' =>
'proofread' ];
488 $rowParams[
'data-groupid'] = $groupId;
489 $rowParams[
'class'] = get_class( $group );
491 $rowParams[
'data-parentgroup'] = $parent->getId();
495 Html::openElement(
'tr', $rowParams ) .
500 $this->table->makeGroupLink( $group, $this->target, $extra )
501 ) . $this->table->makeNumberColumns( $stats ) .
502 $this->getWorkflowStateCell( $groupId ) .
504 Html::closeElement(
'tr' ) .
510 private function getWorkflowStates(): array {
511 $db = $this->loadBalancer->getConnection( DB_REPLICA );
513 'translate_groupreviews',
514 [
'tgr_state',
'tgr_group' ],
515 [
'tgr_lang' => $this->target ],
520 foreach ( $res as $row ) {
521 $states[$row->tgr_group] = $row->tgr_state;