diff options
Diffstat (limited to 'MLEB/Translate/specials/SpecialSupportedLanguages.php')
-rw-r--r-- | MLEB/Translate/specials/SpecialSupportedLanguages.php | 284 |
1 files changed, 84 insertions, 200 deletions
diff --git a/MLEB/Translate/specials/SpecialSupportedLanguages.php b/MLEB/Translate/specials/SpecialSupportedLanguages.php index 63f78109..07113217 100644 --- a/MLEB/Translate/specials/SpecialSupportedLanguages.php +++ b/MLEB/Translate/specials/SpecialSupportedLanguages.php @@ -8,6 +8,13 @@ * @license GPL-2.0-or-later */ +use MediaWiki\Extensions\Translate\Services; +use MediaWiki\Extensions\Translate\Statistics\StatisticsUnavailable; +use MediaWiki\Extensions\Translate\Statistics\TranslatorActivity; +use MediaWiki\Extensions\Translate\Statistics\TranslatorActivityQuery; +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; + /** * Implements special page Special:SupportedLanguages. The wiki administrator * must define NS_PORTAL, otherwise this page does not work. This page displays @@ -19,18 +26,24 @@ * @ingroup SpecialPage TranslateSpecialPage Stats */ class SpecialSupportedLanguages extends SpecialPage { - /// Whether to skip and regenerate caches - protected $purge = false; + private $options; + + /** @var TranslatorActivity */ + private $translatorActivity; /// Cutoff time for inactivity in days protected $period = 180; public function __construct() { parent::__construct( 'SupportedLanguages' ); + // TODO: Use construction injection when 1.33 is no longer supported + // TODO: Only inject the needed configuration options when 1.33 is no longer supported + $this->options = MediaWikiServices::getInstance()->getMainConfig(); + $this->translatorActivity = Services::getInstance()->getTranslatorActivity(); } protected function getGroupName() { - return 'wiki'; + return 'translation'; } public function getDescription() { @@ -41,9 +54,6 @@ class SpecialSupportedLanguages extends SpecialPage { $out = $this->getOutput(); $lang = $this->getLanguage(); - // Only for manual debugging nowdays - $this->purge = false; - $this->setHeaders(); $out->addModules( 'ext.translate.special.supportedlanguages' ); $out->addModuleStyles( 'ext.translate.special.supportedlanguages' ); @@ -72,32 +82,29 @@ class SpecialSupportedLanguages extends SpecialPage { $this->outputLanguageCloud( $languages, $names ); $out->addWikiMsg( 'supportedlanguages-count', $lang->formatNum( count( $languages ) ) ); - if ( $par && Language::isKnownLanguageTag( $par ) ) { - $code = $par; - - $out->addWikiMsg( 'supportedlanguages-colorlegend', $this->getColorLegend() ); - - $users = $this->fetchTranslators( $code ); - if ( $users === false ) { - // generic-pool-error is from MW core - $out->wrapWikiMsg( '<div class="warningbox">$1</div>', 'generic-pool-error' ); - return; - } + if ( !$par || !Language::isKnownLanguageTag( $par ) ) { + return; + } - global $wgTranslateAuthorBlacklist; - $users = $this->filterUsers( $users, $code, $wgTranslateAuthorBlacklist ); - $this->preQueryUsers( $users ); - $this->showLanguage( $code, $users ); + $language = $par; + try { + $data = $this->translatorActivity->inLanguage( $language ); + } catch ( StatisticsUnavailable $e ) { + // generic-pool-error is from MW core + $out->wrapWikiMsg( '<div class="warningbox">$1</div>', 'generic-pool-error' ); + return; } + + $users = $data['users']; + $users = $this->filterUsers( $users, $language ); + $this->preQueryUsers( $users ); + $this->showLanguage( $language, $users, $data['asOfTime'] ); } - protected function showLanguage( $code, $users ) { + protected function showLanguage( string $code, array $users, int $cachedAt ): void { $out = $this->getOutput(); $lang = $this->getLanguage(); - $usernames = array_keys( $users ); - $userStats = $this->getUserStats( $usernames ); - // Information to be used inside the foreach loop. $linkInfo = []; $linkInfo['rc']['title'] = SpecialPage::getTitleFor( 'Recentchanges' ); @@ -142,21 +149,23 @@ class SpecialSupportedLanguages extends SpecialPage { $linkList = $lang->listToText( $links ); $out->addHTML( '<p>' . $linkList . "</p>\n" ); - $this->makeUserList( $users, $userStats ); + $this->makeUserList( $users ); + + $ageString = $this->getLanguage()->formatTimePeriod( + time() - $cachedAt, + [ 'noabbrevs' => true, 'avoid' => 'avoidseconds' ] + ); + $out->addWikiMsg( 'supportedlanguages-colorlegend', $this->getColorLegend() ); + $out->addWikiMsg( 'translate-supportedlanguages-cached', $ageString ); } protected function languageCloud() { - global $wgTranslateMessageNamespaces; - $cache = wfGetCache( CACHE_ANYTHING ); $cachekey = wfMemcKey( 'translate-supportedlanguages-language-cloud' ); - if ( $this->purge ) { - $cache->delete( $cachekey ); - } else { - $data = $cache->get( $cachekey ); - if ( is_array( $data ) ) { - return $data; - } + + $data = $cache->get( $cachekey ); + if ( is_array( $data ) ) { + return $data; } $dbr = wfGetDB( DB_REPLICA ); @@ -166,7 +175,7 @@ class SpecialSupportedLanguages extends SpecialPage { $conds = [ # Without the quotes the rc_timestamp index isn't used and this query is much slower "rc_timestamp > '$timestamp'", - 'rc_namespace' => $wgTranslateMessageNamespaces, + 'rc_namespace' => $this->options->get( 'TranslateMessageNamespaces' ), 'rc_title' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ), ]; $options = [ 'GROUP BY' => 'lang', 'HAVING' => 'count > 20', 'ORDER BY' => 'NULL' ]; @@ -183,106 +192,16 @@ class SpecialSupportedLanguages extends SpecialPage { return $data; } - /** - * Fetch the translators for a language with caching - * - * @param string $code - * @return array|bool Map of (user name => page count) or false on failure - */ - public function fetchTranslators( $code ) { - $cache = wfGetCache( CACHE_ANYTHING ); - $cachekey = wfMemcKey( 'translate-supportedlanguages-translator-list-v1', $code ); - - if ( $this->purge ) { - $cache->delete( $cachekey ); - $data = false; - } else { - $staleCutoffUnix = time() - 3600; - $data = $cache->get( $cachekey ); - if ( is_array( $data ) && $data['asOfTime'] > $staleCutoffUnix ) { - return $data['users']; - } - } - - $that = $this; - $work = new PoolCounterWorkViaCallback( - 'TranslateFetchTranslators', - "TranslateFetchTranslators-$code", - [ - 'doWork' => function () use ( $that, $code, $cache, $cachekey ) { - $users = $that->loadTranslators( $code ); - $newData = [ 'users' => $users, 'asOfTime' => time() ]; - $cache->set( $cachekey, $newData, 86400 ); - return $users; - }, - 'doCachedWork' => function () use ( $cache, $cachekey ) { - $newData = $cache->get( $cachekey ); - // Use new cache value from other thread - return is_array( $newData ) ? $newData['users'] : false; - }, - 'fallback' => function () use ( $data ) { - // Use stale cache if possible - return is_array( $data ) ? $data['users'] : false; - } - ] - ); - - return $work->execute(); - } - - /** - * Fetch the translators for a language - * - * @param string $code - * @return array Map of (user name => page count) - */ - public function loadTranslators( $code ) { - global $wgTranslateMessageNamespaces; - - $dbr = wfGetDB( DB_REPLICA, 'vslow' ); - - if ( class_exists( ActorMigration::class ) ) { - $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' ); - } else { - $actorQuery = [ - 'tables' => [], - 'fields' => [ 'rev_user_text' => 'rev_user_text' ], - 'joins' => [], - ]; - } - - $tables = [ 'page', 'revision' ] + $actorQuery['tables']; - $fields = [ - 'rev_user_text' => $actorQuery['fields']['rev_user_text'], - 'count(page_id) as count' - ]; - $conds = [ - 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code ), - 'page_namespace' => $wgTranslateMessageNamespaces, - ]; - $options = [ 'GROUP BY' => $actorQuery['fields']['rev_user_text'], 'ORDER BY' => 'NULL' ]; - $joins = [ - 'revision' => [ 'JOIN', 'page_id=rev_page' ], - ] + $actorQuery['joins']; - - $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $joins ); - - $data = []; - foreach ( $res as $row ) { - $data[$row->rev_user_text] = $row->count; - } - - return $data; - } + protected function filterUsers( array $users, string $code ): array { + $blacklist = $this->options->get( 'TranslateAuthorBlacklist' ); - protected function filterUsers( array $users, $code, $blacklist ) { foreach ( array_keys( $users ) as $username ) { # We do not know the group $hash = "#;$code;$username"; $blacklisted = false; foreach ( $blacklist as $rule ) { - list( $type, $regex ) = $rule; + [ $type, $regex ] = $rule; if ( preg_match( $regex, $hash ) ) { if ( $type === 'white' ) { @@ -324,7 +243,7 @@ class SpecialSupportedLanguages extends SpecialPage { $out->addHTML( '</div>' ); } - protected function makeUserList( $users, $stats ) { + protected function makeUserList( array $userStats ): void { $day = 60 * 60 * 24; // Scale of the activity colors, anything @@ -334,30 +253,41 @@ class SpecialSupportedLanguages extends SpecialPage { $links = []; $statsTable = new StatsTable(); - arsort( $users ); - foreach ( $users as $username => $count ) { + // List users in descending order by number of translations in this language + uasort( $userStats, function ( $a, $b ) { + return -( + $a[TranslatorActivityQuery::USER_TRANSLATIONS] + <=> + $b[TranslatorActivityQuery::USER_TRANSLATIONS] + ); + } ); + + foreach ( $userStats as $username => $stats ) { $title = Title::makeTitleSafe( NS_USER, $username ); + if ( !$title ) { + LoggerFactory::getInstance( 'Translate' )->warning( + "T248125: Got Title-invalid username '{username}'", + [ 'username' => $username ] + ); + continue; + } + + $count = $stats[TranslatorActivityQuery::USER_TRANSLATIONS]; + $lastTranslationTimestamp = $stats[TranslatorActivityQuery::USER_LAST_ACTIVITY]; + $enc = htmlspecialchars( $username ); $attribs = []; $styles = []; - if ( isset( $stats[$username][0] ) ) { - if ( $count === -1 ) { - $count = $stats[$username][0]; - } + $styles['font-size'] = round( log( $count, 10 ) * 30 ) + 70 . '%'; - $styles['font-size'] = round( log( $count, 10 ) * 30 ) + 70 . '%'; - - $last = wfTimestamp( TS_UNIX ) - wfTimestamp( TS_UNIX, $stats[$username][1] ); - $last = round( $last / $day ); - $attribs['title'] = $this->msg( 'supportedlanguages-activity', $username ) - ->numParams( $count, $last )->text(); - $last = max( 1, min( $period, $last ) ); - $styles['border-bottom'] = '3px solid #' . - $statsTable->getBackgroundColor( ( $period - $last ) / $period ); - } else { - $enc = "<del>$enc</del>"; - } + $last = wfTimestamp( TS_UNIX ) - wfTimestamp( TS_UNIX, $lastTranslationTimestamp ); + $last = round( $last / $day ); + $attribs['title'] = $this->msg( 'supportedlanguages-activity', $username ) + ->numParams( $count, $last )->text(); + $last = max( 1, min( $period, $last ) ); + $styles['border-bottom'] = '3px solid #' . + $statsTable->getBackgroundColor( ( $period - $last ) / $period ); $stylestr = $this->formatStyle( $styles ); if ( $stylestr ) { @@ -368,10 +298,9 @@ class SpecialSupportedLanguages extends SpecialPage { } // for GENDER support - $username = ''; - if ( count( $users ) === 1 ) { - $keys = array_keys( $users ); - $username = $keys[0]; + $usernameForGender = ''; + if ( count( $userStats ) === 1 ) { + $usernameForGender = array_key_first( $userStats ); } $linkList = $this->getLanguage()->listToText( $links ); @@ -379,57 +308,12 @@ class SpecialSupportedLanguages extends SpecialPage { $html .= $this->msg( 'supportedlanguages-translators' ) ->rawParams( $linkList ) ->numParams( count( $links ) ) - ->params( $username ) + ->params( $usernameForGender ) ->escaped(); $html .= "</p>\n"; $this->getOutput()->addHTML( $html ); } - protected function getUserStats( $users ) { - $cache = wfGetCache( CACHE_ANYTHING ); - $dbr = wfGetDB( DB_REPLICA ); - $keys = []; - - foreach ( $users as $username ) { - $keys[] = wfMemcKey( 'translate', 'sl-usertats', $username ); - } - - $cached = $cache->getMulti( $keys ); - $data = []; - - foreach ( $users as $index => $username ) { - $cachekey = $keys[$index]; - - if ( !$this->purge && isset( $cached[$cachekey] ) ) { - $data[$username] = $cached[$cachekey]; - continue; - } - - if ( class_exists( ActorMigration::class ) ) { - $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' ); - $tables = [ 'user', 'r' => [ 'revision' ] + $actorQuery['tables'] ]; - $joins = [ - 'r' => [ 'JOIN', 'user_id = rev_user' ], - ] + $actorQuery['joins']; - } else { - $tables = [ 'user', 'revision' ]; - $joins = [ 'revision' => [ 'JOIN', 'user_id = rev_user' ] ]; - } - - $fields = [ 'user_name', 'user_editcount', 'MAX(rev_timestamp) as lastedit' ]; - $conds = [ - 'user_name' => $username, - ]; - - $res = $dbr->selectRow( $tables, $fields, $conds, __METHOD__, [], $joins ); - $data[$username] = [ $res->user_editcount, $res->lastedit ]; - - $cache->set( $cachekey, $data[$username], 3600 ); - } - - return $data; - } - protected function formatStyle( $styles ) { $stylestr = ''; foreach ( $styles as $key => $value ) { @@ -439,9 +323,9 @@ class SpecialSupportedLanguages extends SpecialPage { return $stylestr; } - protected function preQueryUsers( $users ) { + protected function preQueryUsers( array $users ): void { $lb = new LinkBatch; - foreach ( $users as $user => $count ) { + foreach ( $users as $user => $data ) { $user = Title::capitalize( $user, NS_USER ); $lb->add( NS_USER, $user ); $lb->add( NS_USER_TALK, $user ); |