41 private LanguageNameUtils $languageNameUtils;
43 private array $targetValueName = [
'code',
'language' ];
45 private array $totals;
47 private bool $nothing =
false;
49 private bool $incomplete =
false;
51 private bool $noComplete =
true;
53 private bool $noEmpty =
false;
55 private string $target;
63 private array $statsCounted = [];
64 private array $states = [];
65 private LinkBatchFactory $linkBatchFactory;
67 private JobQueueGroup $jobQueueGroup;
68 private ILoadBalancer $loadBalancer;
71 public function __construct(
72 LinkBatchFactory $linkBatchFactory,
74 LanguageNameUtils $languageNameUtils,
75 JobQueueGroup $jobQueueGroup,
76 ILoadBalancer $loadBalancer,
79 parent::__construct(
'LanguageStats' );
80 $this->totals = MessageGroupStats::getEmptyStats();
81 $this->linkBatchFactory = $linkBatchFactory;
82 $this->progressStatsTableFactory = $progressStatsTableFactory;
83 $this->languageNameUtils = $languageNameUtils;
84 $this->jobQueueGroup = $jobQueueGroup;
85 $this->loadBalancer = $loadBalancer;
86 $this->groupReviewStore = $groupReviewStore;
89 public function isIncludable() {
93 protected function getGroupName() {
97 public function execute( $par ) {
98 $this->target = $this->getLanguage()->getCode();
99 $request = $this->getRequest();
101 $this->purge = $request->getVal(
'action' ) ===
'purge';
102 if ( $this->purge && !$request->wasPosted() ) {
103 self::showPurgeForm( $this->getContext() );
107 $this->table = $this->progressStatsTableFactory->newFromContext( $this->getContext() );
110 $this->outputHeader();
112 $out = $this->getOutput();
114 $out->addModules(
'ext.translate.special.languagestats' );
115 $out->addModuleStyles(
'ext.translate.statstable' );
117 $params = $par ? explode(
'/', $par ) : [];
119 if ( isset( $params[0] ) && trim( $params[0] ) ) {
120 $this->target = $params[0];
123 if ( isset( $params[1] ) ) {
124 $this->noComplete = (bool)$params[1];
127 if ( isset( $params[2] ) ) {
128 $this->noEmpty = (bool)$params[2];
132 $submitted = !$this->including() && $request->getVal(
'x' ) ===
'D';
135 foreach ( $this->targetValueName as $key ) {
136 $this->target = $request->getVal( $key, $this->target );
138 $this->noComplete = $request->getBool(
140 $this->noComplete && !$submitted
142 $this->noEmpty = $request->getBool(
'suppressempty', $this->noEmpty && !$submitted );
144 if ( !$this->including() ) {
145 $out->addHelpLink(
'Help:Extension:Translate/Statistics_and_reporting' );
149 if ( $this->isValidValue( $this->target ) ) {
150 $this->outputIntroduction();
152 $stats = $this->loadStatistics( $this->target, MessageGroupStats::FLAG_CACHE_ONLY );
153 $output = $this->getTable( $stats );
154 if ( $this->incomplete ) {
156 "<div class='error'>$1</div>",
157 'translate-langstats-incomplete'
161 if ( $this->incomplete || $this->purge ) {
162 DeferredUpdates::addCallableUpdate(
function () {
173 $jobParams = $this->getCacheRebuildJobParameters( $this->target );
174 $jobParams[
'purge' ] = $this->purge;
175 $this->jobQueueGroup->push( MessageGroupStatsRebuildJob::newJob( $jobParams ) );
178 if ( !$this->purge ) {
179 $this->loadStatistics( $this->target );
183 if ( $this->nothing ) {
184 $out->wrapWikiMsg(
"<div class='error'>$1</div>",
'translate-mgs-nothing' );
186 $out->addHTML( $output );
187 } elseif ( $submitted ) {
188 $this->invalidTarget();
198 private function loadStatistics(
string $target,
int $flags = 0 ): array {
199 return MessageGroupStats::forLanguage( $target, $flags );
202 private function getCacheRebuildJobParameters( $target ): array {
203 return [
'languagecode' => $target ];
207 private function isValidValue(
string $value ):
bool {
208 $langs = $this->languageNameUtils->getLanguageNames();
210 return isset( $langs[$value] );
214 private function invalidTarget():
void {
215 $this->getOutput()->wrapWikiMsg(
216 "<div class='error'>$1</div>",
217 'translate-page-no-such-language'
221 public static function showPurgeForm( IContextSource $context ):
void {
225 'vertical-label' =>
true,
227 'default' => $context->msg(
'confirm-purge-top' )->parse()
231 $derivativeContext =
new DerivativeContext( $context );
232 $requestValues = $derivativeContext->getRequest()->getQueryValues();
234 HTMLForm::factory(
'ooui', $formDescriptor, $derivativeContext )
235 ->setWrapperLegendMsg(
'confirm-purge-title' )
236 ->setSubmitTextMsg(
'confirm_purge_button' )
237 ->addHiddenFields( $requestValues )
242 private function addForm():
void {
246 'name' =>
'language',
248 'label' => $this->msg(
'translate-language-code-field-name' )->text(),
250 'default' => $this->target,
252 'suppresscomplete' => [
254 'label' => $this->msg(
'translate-suppress-complete' )->text(),
255 'name' =>
'suppresscomplete',
256 'id' =>
'suppresscomplete',
257 'default' => $this->noComplete,
261 'label' => $this->msg(
'translate-ls-noempty' )->text(),
262 'name' =>
'suppressempty',
263 'id' =>
'suppressempty',
264 'default' => $this->noEmpty,
268 $context =
new DerivativeContext( $this->getContext() );
269 $context->setTitle( $this->getPageTitle() );
271 $htmlForm = HTMLForm::factory(
'ooui', $formDescriptor, $context );
276 $val = $this->getRequest()->getVal(
'group' );
277 if ( $val !==
null ) {
278 $htmlForm->addHiddenField(
'group', $val );
282 ->addHiddenField(
'x',
'D' )
284 ->setSubmitTextMsg(
'translate-ls-submit' )
285 ->setWrapperLegendMsg(
'translate-mgs-fieldset' )
287 ->displayForm(
false );
291 private function outputIntroduction():
void {
292 $languageName = Utilities::getLanguageName(
294 $this->getLanguage()->getCode()
297 $rcInLangLink = $this->getLinkRenderer()->makeKnownLink(
298 SpecialPage::getTitleFor(
'Translate',
'!recent' ),
299 $this->msg(
'languagestats-recenttranslations' )->text(),
302 'action' =>
'proofread',
303 'language' => $this->target
307 $out = $this->msg(
'languagestats-stats-for', $languageName )->rawParams( $rcInLangLink )
309 $this->getOutput()->addHTML( $out );
312 private function getWorkflowStateCell(
string $messageGroupId ):
string {
313 if ( $this->states === [] ) {
317 return $this->table->makeWorkflowStateCell(
318 $this->states[$messageGroupId] ??
null,
319 MessageGroups::getGroup( $messageGroupId ),
324 private function getTable( array $stats ):
string {
325 global $wgTranslateWorkflowStates;
327 $table = $this->table;
332 $lb = $this->linkBatchFactory->newLinkBatch();
333 foreach ( MessageGroups::getAllGroups() as $group ) {
335 $lb->addObj( $group->getTitle() );
338 $lb->setCaller( __METHOD__ )->execute();
340 $structure = MessageGroups::getGroupStructure();
342 if ( $wgTranslateWorkflowStates ) {
343 $this->states = $this->groupReviewStore->getWorkflowStatesForLanguage(
345 array_keys( $structure )
348 $this->table->addExtraColumn( $this->msg(
'translate-stats-workflow' ) );
351 foreach ( $structure as $item ) {
352 $out .= $this->makeGroupGroup( $item, $stats );
356 $table->setMainColumnHeader( $this->msg(
'translate-ls-column-group' ) );
358 $out .= Html::closeElement(
'tbody' );
360 $out .= Html::openElement(
'tfoot' );
362 $this->msg(
'translate-languagestats-overall' ),
365 $out .= Html::closeElement(
'tfoot' );
367 $out .= Html::closeElement(
'table' );
371 $this->nothing =
true;
386 private function makeGroupGroup( $item, array $cache,
MessageGroup $parent =
null ):
string {
387 if ( !is_array( $item ) ) {
388 return $this->makeGroupRow( $item, $cache, $parent );
393 $top = array_shift( $item );
394 $out .= $this->makeGroupRow( $top, $cache, $parent );
397 foreach ( $item as $subgroup ) {
398 $out .= $this->makeGroupGroup( $subgroup, $cache, $top );
408 private function makeGroupRow(
413 $groupId = $group->
getId();
415 if ( $this->table->isExcluded( $group, $this->target ) ) {
419 $stats = $cache[$groupId];
420 $total = $stats[MessageGroupStats::TOTAL];
421 $translated = $stats[MessageGroupStats::TRANSLATED];
422 $fuzzy = $stats[MessageGroupStats::FUZZY];
425 if ( $this->noComplete && $fuzzy === 0 && $translated === $total ) {
428 if ( $this->noEmpty && $translated === 0 && $fuzzy === 0 ) {
432 if ( $total ===
null ) {
433 $this->incomplete =
true;
438 !isset( $this->statsCounted[$groupId] )
440 $this->totals = MessageGroupStats::multiAdd( $this->totals, $stats );
441 $this->statsCounted[$groupId] =
true;
446 $params[] = $this->states[$groupId] ??
'';
447 $params[] = md5( $groupId );
448 $params[] = $this->getLanguage()->getCode();
449 $params[] = md5( $this->target );
450 $params[] = $parent ? $parent->getId() :
'!';
452 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
454 return $cache->getWithSetCallback(
455 $cache->makeKey( __METHOD__ .
'-v3', implode(
'-', $params ) ),
457 function () use ( $translated, $total, $groupId, $group, $parent, $stats ) {
460 if ( $translated === $total ) {
461 $extra = [
'action' =>
'proofread' ];
465 $rowParams[
'data-groupid'] = $groupId;
466 $rowParams[
'class'] = get_class( $group );
468 $rowParams[
'data-parentgroup'] = $parent->getId();
472 Html::openElement(
'tr', $rowParams ) .
477 $this->table->makeGroupLink( $group, $this->target, $extra )
478 ) . $this->table->makeNumberColumns( $stats ) .
479 $this->getWorkflowStateCell( $groupId ) .
481 Html::closeElement(
'tr' ) .