40 private $languageNameUtils;
44 private $targetValueName = [
'code',
'language' ];
51 private $nothing =
false;
56 private $incomplete =
false;
61 private $noComplete =
true;
66 private $noEmpty =
false;
80 private $statsCounted = [];
84 private $linkBatchFactory;
86 private $progressStatsTableFactory;
88 private $jobQueueGroup;
90 public function __construct(
91 LinkBatchFactory $linkBatchFactory,
93 LanguageNameUtils $languageNameUtils,
94 JobQueueGroup $jobQueueGroup
96 parent::__construct(
'LanguageStats' );
97 $this->totals = MessageGroupStats::getEmptyStats();
98 $this->linkBatchFactory = $linkBatchFactory;
99 $this->progressStatsTableFactory = $progressStatsTableFactory;
100 $this->languageNameUtils = $languageNameUtils;
101 $this->jobQueueGroup = $jobQueueGroup;
104 public function isIncludable() {
108 protected function getGroupName() {
109 return 'translation';
112 public function execute( $par ) {
113 $this->target = $this->getLanguage()->getCode();
114 $request = $this->getRequest();
116 $this->purge = $request->getVal(
'action' ) ===
'purge';
117 if ( $this->purge && !$request->wasPosted() ) {
118 self::showPurgeForm( $this->getContext() );
122 $this->table = $this->progressStatsTableFactory->newFromContext( $this->getContext() );
125 $this->outputHeader();
127 $out = $this->getOutput();
129 $out->addModules(
'ext.translate.special.languagestats' );
130 $out->addModuleStyles(
'ext.translate.statstable' );
132 $params = $par ? explode(
'/', $par ) : [];
134 if ( isset( $params[0] ) && trim( $params[0] ) ) {
135 $this->target = $params[0];
138 if ( isset( $params[1] ) ) {
139 $this->noComplete = (bool)$params[1];
142 if ( isset( $params[2] ) ) {
143 $this->noEmpty = (bool)$params[2];
147 $submitted = !$this->including() && $request->getVal(
'x' ) ===
'D';
150 foreach ( $this->targetValueName as $key ) {
151 $this->target = $request->getVal( $key, $this->target );
153 $this->noComplete = $request->getBool(
155 $this->noComplete && !$submitted
157 $this->noEmpty = $request->getBool(
'suppressempty', $this->noEmpty && !$submitted );
159 if ( !$this->including() ) {
160 $out->addHelpLink(
'Help:Extension:Translate/Statistics_and_reporting' );
164 if ( $this->isValidValue( $this->target ) ) {
165 $this->outputIntroduction();
167 $stats = $this->loadStatistics( $this->target, MessageGroupStats::FLAG_CACHE_ONLY );
168 $output = $this->getTable( $stats );
169 if ( $this->incomplete ) {
171 "<div class='error'>$1</div>",
172 'translate-langstats-incomplete'
176 if ( $this->incomplete || $this->purge ) {
177 DeferredUpdates::addCallableUpdate(
function () {
188 $jobParams = $this->getCacheRebuildJobParameters( $this->target );
189 $jobParams[
'purge' ] = $this->purge;
190 $this->jobQueueGroup->push( MessageGroupStatsRebuildJob::newJob( $jobParams ) );
193 if ( !$this->purge ) {
194 $this->loadStatistics( $this->target );
198 if ( $this->nothing ) {
199 $out->wrapWikiMsg(
"<div class='error'>$1</div>",
'translate-mgs-nothing' );
201 $out->addHTML( $output );
202 } elseif ( $submitted ) {
203 $this->invalidTarget();
213 private function loadStatistics(
string $target,
int $flags = 0 ): array {
214 return MessageGroupStats::forLanguage( $target, $flags );
217 private function getCacheRebuildJobParameters( $target ): array {
218 return [
'languagecode' => $target ];
222 private function isValidValue(
string $value ):
bool {
223 $langs = $this->languageNameUtils->getLanguageNames();
225 return isset( $langs[$value] );
229 private function invalidTarget():
void {
230 $this->getOutput()->wrapWikiMsg(
231 "<div class='error'>$1</div>",
232 'translate-page-no-such-language'
236 public static function showPurgeForm( IContextSource $context ):
void {
240 'vertical-label' =>
true,
242 'default' => $context->msg(
'confirm-purge-top' )->parse()
246 $derivativeContext =
new DerivativeContext( $context );
247 $requestValues = $derivativeContext->getRequest()->getQueryValues();
249 HTMLForm::factory(
'ooui', $formDescriptor, $derivativeContext )
250 ->setWrapperLegendMsg(
'confirm-purge-title' )
251 ->setSubmitTextMsg(
'confirm_purge_button' )
252 ->addHiddenFields( $requestValues )
257 private function addForm():
void {
261 'name' =>
'language',
263 'label' => $this->msg(
'translate-language-code-field-name' )->text(),
265 'default' => $this->target,
267 'suppresscomplete' => [
269 'label' => $this->msg(
'translate-suppress-complete' )->text(),
270 'name' =>
'suppresscomplete',
271 'id' =>
'suppresscomplete',
272 'default' => $this->noComplete,
276 'label' => $this->msg(
'translate-ls-noempty' )->text(),
277 'name' =>
'suppressempty',
278 'id' =>
'suppressempty',
279 'default' => $this->noEmpty,
283 $context =
new DerivativeContext( $this->getContext() );
284 $context->setTitle( $this->getPageTitle() );
286 $htmlForm = HtmlForm::factory(
'ooui', $formDescriptor, $context );
291 $val = $this->getRequest()->getVal(
'group' );
292 if ( $val !==
null ) {
293 $htmlForm->addHiddenField(
'group', $val );
297 ->addHiddenField(
'x',
'D' )
299 ->setSubmitTextMsg(
'translate-ls-submit' )
300 ->setWrapperLegendMsg(
'translate-mgs-fieldset' )
302 ->displayForm(
false );
306 private function outputIntroduction():
void {
307 $languageName = TranslateUtils::getLanguageName(
309 $this->getLanguage()->getCode()
312 $rcInLangLink = $this->getLinkRenderer()->makeKnownLink(
313 SpecialPage::getTitleFor(
'Translate',
'!recent' ),
314 $this->msg(
'languagestats-recenttranslations' )->text(),
317 'action' =>
'proofread',
318 'language' => $this->target
322 $out = $this->msg(
'languagestats-stats-for', $languageName )->rawParams( $rcInLangLink )
324 $this->getOutput()->addHTML( $out );
328 private function addWorkflowStatesColumn():
void {
329 global $wgTranslateWorkflowStates;
331 if ( $wgTranslateWorkflowStates ) {
332 $this->states = $this->getWorkflowStates();
335 $this->table->addExtraColumn( $this->msg(
'translate-stats-workflow' ) );
339 private function getWorkflowStateCell(
string $messageGroupId ):
string {
341 if ( !isset( $this->states ) ) {
345 return $this->table->makeWorkflowStateCell(
346 $this->states[$messageGroupId] ??
null,
347 MessageGroups::getGroup( $messageGroupId ),
352 private function getTable( array $stats ):
string {
353 $table = $this->table;
355 $this->addWorkflowStatesColumn();
360 $lb = $this->linkBatchFactory->newLinkBatch();
361 foreach ( MessageGroups::getAllGroups() as $group ) {
363 $lb->addObj( $group->getTitle() );
366 $lb->setCaller( __METHOD__ )->execute();
368 $structure = MessageGroups::getGroupStructure();
369 foreach ( $structure as $item ) {
370 $out .= $this->makeGroupGroup( $item, $stats );
374 $table->setMainColumnHeader( $this->msg(
'translate-ls-column-group' ) );
375 $out = $table->createHeader() .
"\n" . $out;
376 $out .= Html::closeElement(
'tbody' );
378 $out .= Html::openElement(
'tfoot' );
379 $out .= $table->makeTotalRow(
380 $this->msg(
'translate-languagestats-overall' ),
383 $out .= Html::closeElement(
'tfoot' );
385 $out .= Html::closeElement(
'table' );
389 $this->nothing =
true;
404 private function makeGroupGroup( $item, array $cache,
MessageGroup $parent =
null ):
string {
405 if ( !is_array( $item ) ) {
406 return $this->makeGroupRow( $item, $cache, $parent );
411 $top = array_shift( $item );
412 $out .= $this->makeGroupRow( $top, $cache, $parent );
415 foreach ( $item as $subgroup ) {
416 $out .= $this->makeGroupGroup( $subgroup, $cache, $top );
426 private function makeGroupRow(
431 $groupId = $group->
getId();
433 if ( $this->table->isExcluded( $groupId, $this->target ) ) {
437 $stats = $cache[$groupId];
438 $total = $stats[MessageGroupStats::TOTAL];
439 $translated = $stats[MessageGroupStats::TRANSLATED];
440 $fuzzy = $stats[MessageGroupStats::FUZZY];
443 if ( $this->noComplete && $fuzzy === 0 && $translated === $total ) {
446 if ( $this->noEmpty && $translated === 0 && $fuzzy === 0 ) {
450 if ( $total ===
null ) {
451 $this->incomplete =
true;
456 !isset( $this->statsCounted[$groupId] )
458 $this->totals = MessageGroupStats::multiAdd( $this->totals, $stats );
459 $this->statsCounted[$groupId] =
true;
464 $params[] = $this->states[$groupId] ??
'';
465 $params[] = md5( $groupId );
466 $params[] = $this->getLanguage()->getCode();
467 $params[] = md5( $this->target );
468 $params[] = $parent ? $parent->getId() :
'!';
470 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
472 return $cache->getWithSetCallback(
473 $cache->makeKey( __METHOD__ .
'-v3', implode(
'-', $params ) ),
475 function () use ( $translated, $total, $groupId, $group, $parent, $stats ) {
478 if ( $translated === $total ) {
479 $extra = [
'action' =>
'proofread' ];
483 $rowParams[
'data-groupid'] = $groupId;
484 $rowParams[
'class'] = get_class( $group );
486 $rowParams[
'data-parentgroup'] = $parent->getId();
490 Html::openElement(
'tr', $rowParams ) .
495 $this->table->makeGroupLink( $group, $this->target, $extra )
496 ) . $this->table->makeNumberColumns( $stats ) .
497 $this->getWorkflowStateCell( $groupId ) .
499 Html::closeElement(
'tr' ) .
505 private function getWorkflowStates(): array {
506 $db = wfGetDB( DB_REPLICA );
508 'translate_groupreviews',
509 [
'tgr_state',
'tgr_group' ],
510 [
'tgr_lang' => $this->target ],
515 foreach ( $res as $row ) {
516 $states[$row->tgr_group] = $row->tgr_state;