24 private const TABLE =
'translate_groupstats';
39 protected static $updates = [];
41 private static $languages;
50 return [ 0, 0, 0, 0 ];
60 return [
null,
null,
null, null ];
63 private static function isValidLanguage( $code ) {
64 $languages = self::getLanguages();
65 return in_array( $code, $languages );
68 private static function isValidMessageGroup(
MessageGroup $group =
null ) {
81 public static function forItem( $id, $code, $flags = 0 ) {
82 $group = MessageGroups::getGroup( $id );
83 if ( !self::isValidMessageGroup( $group ) || !self::isValidLanguage( $code ) ) {
84 return self::getUnknownStats();
87 $res = self::selectRowsIdLang( [ $id ], [ $code ], $flags );
88 $stats = self::extractResults( $res, [ $id ] );
90 if ( !isset( $stats[$id][$code] ) ) {
91 $stats[$id][$code] = self::forItemInternal( $stats, $group, $code, $flags );
94 self::queueUpdates( $flags );
96 return $stats[$id][$code];
106 if ( !self::isValidLanguage( $code ) ) {
108 $groups = MessageGroups::singleton()->getGroups();
109 $ids = array_keys( $groups );
110 foreach ( $ids as $id ) {
111 $stats[$id] = self::getUnknownStats();
117 $stats = self::forLanguageInternal( $code, [], $flags );
119 foreach ( $stats as $group => $languages ) {
120 $flattened[$group] = $languages[$code];
123 self::queueUpdates( $flags );
134 public static function forGroup( $id, $flags = 0 ) {
135 $group = MessageGroups::getGroup( $id );
136 if ( !self::isValidMessageGroup( $group ) ) {
137 $languages = self::getLanguages();
139 foreach ( $languages as $code ) {
140 $stats[$code] = self::getUnknownStats();
146 $stats = self::forGroupInternal( $group, [], $flags );
148 self::queueUpdates( $flags );
161 $groups = MessageGroups::singleton()->getGroups();
163 foreach ( $groups as $g ) {
164 $stats = self::forGroupInternal( $g, $stats, $flags );
167 self::queueUpdates( $flags );
179 $code = $handle->getCode();
180 if ( !self::isValidLanguage( $code ) ) {
183 $groups = self::getSortedGroupsForClearing( $handle->
getGroupIds() );
184 self::internalClearGroups( $code, $groups, 0 );
193 public static function clearGroup( $id,
int $flags = 0 ): void {
194 $languages = self::getLanguages();
195 $groups = self::getSortedGroupsForClearing( (array)$id );
198 foreach ( $languages as $code ) {
199 self::internalClearGroups( $code, $groups, $flags );
210 private static function internalClearGroups( $code, array $groups,
int $flags ): void {
212 foreach ( $groups as $group ) {
214 self::forItemInternal( $stats, $group, $code, $flags );
216 self::queueUpdates( 0 );
230 private static function getSortedGroupsForClearing( array $ids ) {
231 $groups = array_map( [ MessageGroups::class,
'getGroup' ], $ids );
233 $groups = array_filter( $groups );
237 foreach ( $groups as $group ) {
239 $aggs[$group->getId()] = $group;
241 $sorted[$group->getId()] = $group;
245 return array_merge( $sorted, $aggs );
253 private static function getLanguages() {
254 if ( self::$languages ===
null ) {
255 $languages = array_keys( TranslateUtils::getLanguageNames(
'en' ) );
257 self::$languages = $languages;
260 return self::$languages;
263 public static function clearLanguage( $code ) {
264 if ( !count( $code ) ) {
267 $dbw = wfGetDB( DB_PRIMARY );
268 $conds = [
'tgs_lang' => $code ];
269 $dbw->delete( self::TABLE, $conds, __METHOD__ );
270 wfDebugLog(
'messagegroupstats',
'Cleared ' . serialize( $conds ) );
279 $dbw = wfGetDB( DB_PRIMARY );
280 $dbw->delete( self::TABLE,
'*', __METHOD__ );
281 wfDebugLog(
'messagegroupstats',
'Cleared everything :(' );
293 protected static function extractResults( $res, array $ids, array $stats = [] ) {
295 $idmap = array_combine( array_map(
'self::getDatabaseIdForGroupId', $ids ), $ids );
297 foreach ( $res as $row ) {
298 if ( !isset( $idmap[$row->tgs_group] ) ) {
304 $realId = $idmap[$row->tgs_group];
305 $stats[$realId][$row->tgs_lang] = self::extractNumbers( $row );
318 self::TOTAL => (int)$row->tgs_total,
319 self::TRANSLATED => (
int)$row->tgs_translated,
320 self::FUZZY => (int)$row->tgs_fuzzy,
321 self::PROOFREAD => (
int)$row->tgs_proofread,
332 $groups = MessageGroups::singleton()->getGroups();
334 $ids = array_keys( $groups );
335 $res = self::selectRowsIdLang(
null, [ $code ], $flags );
336 $stats = self::extractResults( $res, $ids, $stats );
338 foreach ( $groups as $id => $group ) {
339 if ( isset( $stats[$id][$code] ) ) {
342 $stats[$id][$code] = self::forItemInternal( $stats, $group, $code, $flags );
356 foreach ( $agg->
getGroups() as $group ) {
358 $flattened += self::expandAggregates( $group );
360 $flattened[$group->getId()] = $group;
374 $id = $group->
getId();
376 $res = self::selectRowsIdLang( [ $id ],
null, $flags );
377 $stats = self::extractResults( $res, [ $id ], $stats );
380 $languages = self::getLanguages();
381 foreach ( $languages as $code ) {
382 if ( isset( $stats[$id][$code] ) ) {
385 $stats[$id][$code] = self::forItemInternal( $stats, $group, $code, $flags );
389 foreach ( array_keys( $stats ) as $key ) {
390 ksort( $stats[$key] );
405 if ( $flags & self::FLAG_NO_CACHE ) {
410 if ( $ids !==
null ) {
411 $dbids = array_map(
'self::getDatabaseIdForGroupId', $ids );
412 $conds[
'tgs_group'] = $dbids;
415 if ( $codes !==
null ) {
416 $conds[
'tgs_lang'] = $codes;
419 $dbr = TranslateUtils::getSafeReadDB();
420 $res = $dbr->select( self::TABLE,
'*', $conds, __METHOD__ );
433 $id = $group->
getId();
435 if ( $flags & self::FLAG_CACHE_ONLY ) {
436 $stats[$id][$code] = self::getUnknownStats();
437 return $stats[$id][$code];
445 $databaseGroupId = self::getDatabaseIdForGroupId( $id );
446 $uniqueKey =
"$databaseGroupId|$code";
447 $queuedValue = self::$updates[$uniqueKey] ??
null;
448 if ( $queuedValue && !( $flags & self::FLAG_NO_CACHE ) ) {
450 self::TOTAL => $queuedValue[
'tgs_total'],
451 self::TRANSLATED => $queuedValue[
'tgs_translated'],
452 self::FUZZY => $queuedValue[
'tgs_fuzzy'],
453 self::PROOFREAD => $queuedValue[
'tgs_proofread'],
458 $aggregates = self::calculateAggregageGroup( $stats, $group, $code, $flags );
460 $aggregates = self::calculateGroup( $group, $code );
463 $stats[$id][$code] = $aggregates;
466 if ( $aggregates[self::TOTAL] ===
null ) {
470 self::$updates[$uniqueKey] = [
471 'tgs_group' => $databaseGroupId,
473 'tgs_total' => $aggregates[self::TOTAL],
474 'tgs_translated' => $aggregates[self::TRANSLATED],
475 'tgs_fuzzy' => $aggregates[self::FUZZY],
476 'tgs_proofread' => $aggregates[self::PROOFREAD],
481 if ( count( self::$updates ) % 100 === 0 ) {
482 self::queueUpdates( $flags );
488 private static function calculateAggregageGroup( &$stats, $group, $code, $flags ) {
489 $aggregates = self::getEmptyStats();
491 $expanded = self::expandAggregates( $group );
492 $subGroupIds = array_keys( $expanded );
495 foreach ( $subGroupIds as $index => $sid ) {
496 if ( isset( $stats[$sid][$code] ) ) {
497 unset( $subGroupIds[ $index ] );
501 if ( $subGroupIds !== [] ) {
502 $res = self::selectRowsIdLang( $subGroupIds, [ $code ], $flags );
503 $stats = self::extractResults( $res, $subGroupIds, $stats );
506 foreach ( $expanded as $sid => $subgroup ) {
513 if ( !isset( $stats[$sid][$code] ) ) {
514 $stats[$sid][$code] = self::forItemInternal( $stats, $subgroup, $code, $flags );
517 if ( !TranslateMetadata::isExcluded( $sid, $code ) ) {
518 $aggregates = self::multiAdd( $aggregates, $stats[$sid][$code] );
525 public static function multiAdd( &$a, $b ) {
526 if ( $a[0] ===
null || $b[0] ===
null ) {
527 return array_fill( 0, count( $a ),
null );
529 foreach ( $a as $i => &$v ) {
542 global $wgTranslateDocumentationLanguageCode;
547 $code === $wgTranslateDocumentationLanguageCode
551 if ( $cache->exists() ) {
552 $template = $cache->getExtra()[
'TEMPLATE'] ?? [];
554 foreach ( $template as $key => $data ) {
555 if ( isset( $data[
'comments'][
'.'] ) ) {
559 $collection->setInFile( $infile );
563 $collection->filter(
'ignored' );
564 $collection->filterUntranslatedOptional();
566 $total = count( $collection );
569 $collection->filter(
'fuzzy' );
570 $fuzzy = $total - count( $collection );
573 $collection->filter(
'hastranslation',
false );
574 $translated = count( $collection );
578 $collection->filter(
'reviewer',
false );
579 $proofread = count( $collection );
582 self::TOTAL => $total,
583 self::TRANSLATED => $translated,
584 self::FUZZY => $fuzzy,
585 self::PROOFREAD => $proofread,
589 protected static function queueUpdates( $flags ) {
590 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
594 if ( self::$updates === [] ) {
598 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
599 $dbw = $lb->getConnectionRef( DB_PRIMARY );
600 $table = self::TABLE;
601 $callers = wfGetAllCallers( 50 );
603 $updateOp = self::withLock(
607 static function ( IDatabase $dbw, $method ) use ( $table, $callers ) {
609 if ( self::$updates === [] ) {
614 if ( count( self::$updates ) > 100 ) {
615 $groups = array_unique( array_column( self::$updates,
'tgs_group' ) );
616 LoggerFactory::getInstance(
'Translate' )->warning(
617 "Huge translation update of {count} rows for group(s) {groups}",
619 'count' => count( self::$updates ),
620 'groups' => implode(
', ', $groups ),
621 'callers' => $callers,
626 $primaryKey = [
'tgs_group',
'tgs_lang' ];
627 $dbw->replace( $table, [ $primaryKey ], array_values( self::$updates ), $method );
632 if ( $flags & self::FLAG_IMMEDIATE_WRITES ) {
633 call_user_func( $updateOp );
635 DeferredUpdates::addCallableUpdate( $updateOp );
639 protected static function withLock( IDatabase $dbw, $key, $method, $callback ) {
641 return static function () use ( $dbw, $key, $method, $callback, $fname ) {
642 $lockName =
'MessageGroupStats:' . $key;
643 if ( !$dbw->lock( $lockName, $fname, 1 ) ) {
647 $dbw->commit( $fname,
'flush' );
648 call_user_func( $callback, $dbw, $method );
649 $dbw->commit( $fname,
'flush' );
651 $dbw->unlock( $lockName, $fname );
655 public static function getDatabaseIdForGroupId( $id ) {
657 if ( strlen( $id ) <= 72 ) {
661 $hash = hash(
'sha256', $id,
false );
662 $dbid = substr( $id, 0, 50 ) .
'||' . substr( $hash, 0, 20 );