msg( 'supportedlanguages' )->text(); } public function execute( $par ) { $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' ); $out->addHelpLink( 'Help:Extension:Translate/Statistics_and_reporting#List_of_languages_and_translators' ); $this->outputHeader( 'supportedlanguages-summary' ); $dbr = wfGetDB( DB_REPLICA ); if ( $dbr->getType() === 'sqlite' ) { $out->wrapWikiMsg( '
$1
', 'supportedlanguages-sqlite-error' ); return; } $out->addWikiMsg( 'supportedlanguages-localsummary' ); $names = Language::fetchLanguageNames( null, 'all' ); $languages = $this->languageCloud(); // There might be all sorts of subpages which are not languages $languages = array_intersect_key( $languages, $names ); $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( '
$1
', 'generic-pool-error' ); return; } global $wgTranslateAuthorBlacklist; $users = $this->filterUsers( $users, $code, $wgTranslateAuthorBlacklist ); $this->preQueryUsers( $users ); $this->showLanguage( $code, $users ); } } protected function showLanguage( $code, $users ) { $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' ); $linkInfo['rc']['msg'] = $this->msg( 'supportedlanguages-recenttranslations' )->text(); $linkInfo['stats']['title'] = SpecialPage::getTitleFor( 'LanguageStats' ); $linkInfo['stats']['msg'] = $this->msg( 'languagestats' )->text(); $local = Language::fetchLanguageName( $code, $lang->getCode(), 'all' ); $native = Language::fetchLanguageName( $code, null, 'all' ); if ( $local !== $native ) { $headerText = $this->msg( 'supportedlanguages-portallink' ) ->params( $code, $local, $native )->escaped(); } else { // No CLDR, so a less localised header and link title. $headerText = $this->msg( 'supportedlanguages-portallink-nocldr' ) ->params( $code, $native )->escaped(); } $out->addHTML( Html::rawElement( 'h2', [ 'id' => $code ], $headerText ) ); // Add useful links for language stats and recent changes for the language. $links = []; $links[] = $this->getLinkRenderer()->makeKnownLink( $linkInfo['stats']['title'], $linkInfo['stats']['msg'], [], [ 'code' => $code, 'suppresscomplete' => '1' ] ); $links[] = $this->getLinkRenderer()->makeKnownLink( $linkInfo['rc']['title'], $linkInfo['rc']['msg'], [], [ 'translations' => 'only', 'trailer' => '/' . $code ] ); $linkList = $lang->listToText( $links ); $out->addHTML( '

' . $linkList . "

\n" ); $this->makeUserList( $users, $userStats ); } 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; } } $dbr = wfGetDB( DB_REPLICA ); $tables = [ 'recentchanges' ]; $fields = [ 'substring_index(rc_title, \'/\', -1) as lang', 'count(*) as count' ]; $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - 60 * 60 * 24 * $this->period ); $conds = [ # Without the quotes the rc_timestamp index isn't used and this query is much slower "rc_timestamp > '$timestamp'", 'rc_namespace' => $wgTranslateMessageNamespaces, 'rc_title' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ), ]; $options = [ 'GROUP BY' => 'lang', 'HAVING' => 'count > 20', 'ORDER BY' => 'NULL' ]; $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options ); $data = []; foreach ( $res as $row ) { $data[$row->lang] = $row->count; } $cache->set( $cachekey, $data, 3600 ); 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, $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; if ( preg_match( $regex, $hash ) ) { if ( $type === 'white' ) { $blacklisted = false; break; } else { $blacklisted = true; } } } if ( $blacklisted ) { unset( $users[$username] ); } } return $users; } protected function outputLanguageCloud( array $languages, array $names ) { $out = $this->getOutput(); $out->addHTML( '
' ); foreach ( $languages as $k => $v ) { $name = $names[$k]; $size = round( log( $v ) * 20 ) + 10; $params = [ 'href' => $this->getPageTitle( $k )->getLocalURL(), 'class' => 'tag', 'style' => "font-size:$size%", 'lang' => $k, ]; $tag = Html::element( 'a', $params, $name ); $out->addHTML( $tag . "\n" ); } $out->addHTML( '
' ); } protected function makeUserList( $users, $stats ) { $day = 60 * 60 * 24; // Scale of the activity colors, anything // longer than this is just inactive $period = $this->period; $links = []; $statsTable = new StatsTable(); arsort( $users ); foreach ( $users as $username => $count ) { $title = Title::makeTitleSafe( NS_USER, $username ); $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 . '%'; $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 = "$enc"; } $stylestr = $this->formatStyle( $styles ); if ( $stylestr ) { $attribs['style'] = $stylestr; } $links[] = $this->getLinkRenderer()->makeLink( $title, new HtmlArmor( $enc ), $attribs ); } // for GENDER support $username = ''; if ( count( $users ) === 1 ) { $keys = array_keys( $users ); $username = $keys[0]; } $linkList = $this->getLanguage()->listToText( $links ); $html = "

"; $html .= $this->msg( 'supportedlanguages-translators' ) ->rawParams( $linkList ) ->numParams( count( $links ) ) ->params( $username ) ->escaped(); $html .= "

\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 ) { $stylestr .= "$key:$value;"; } return $stylestr; } protected function preQueryUsers( $users ) { $lb = new LinkBatch; foreach ( $users as $user => $count ) { $user = Title::capitalize( $user, NS_USER ); $lb->add( NS_USER, $user ); $lb->add( NS_USER_TALK, $user ); } $lb->execute(); } protected function getColorLegend() { $legend = ''; $period = $this->period; $statsTable = new StatsTable(); for ( $i = 0; $i <= $period; $i += 30 ) { $iFormatted = htmlspecialchars( $this->getLanguage()->formatNum( $i ) ); $legend .= ' $iFormatted"; } return $legend; } }