32 private ServiceOptions $options;
34 private LanguageNameUtils $langNameUtils;
35 private ILoadBalancer $loadBalancer;
37 private Language $contentLanguage;
40 private LinkBatchFactory $linkBatchFactory;
41 private LanguageFactory $languageFactory;
43 private int $period = 180;
45 public const CONSTRUCTOR_OPTIONS = [
46 'TranslateMessageNamespaces',
49 public function __construct(
52 LanguageNameUtils $langNameUtils,
53 ILoadBalancer $loadBalancer,
55 Language $contentLanguage,
57 LinkBatchFactory $linkBatchFactory,
58 LanguageFactory $languageFactory
60 parent::__construct(
'SupportedLanguages' );
61 $this->options =
new ServiceOptions( self::CONSTRUCTOR_OPTIONS, $config );
62 $this->translatorActivity = $translatorActivity;
63 $this->langNameUtils = $langNameUtils;
64 $this->loadBalancer = $loadBalancer;
65 $this->configHelper = $configHelper;
66 $this->contentLanguage = $contentLanguage;
67 $this->progressStatsTableFactory = $progressStatsTableFactory;
68 $this->linkBatchFactory = $linkBatchFactory;
69 $this->languageFactory = $languageFactory;
72 protected function getGroupName() {
76 public function getDescription() {
77 return $this->msg(
'supportedlanguages' );
80 public function execute( $par ):
void {
81 $out = $this->getOutput();
82 $lang = $this->getLanguage();
83 $this->progressStatsTable = $this->progressStatsTableFactory->newFromContext( $this->getContext() );
86 $out->addModuleStyles(
'ext.translate.specialpages.styles' );
89 'Help:Extension:Translate/Statistics_and_reporting#List_of_languages_and_translators'
92 $this->outputHeader(
'supportedlanguages-summary' );
93 $dbr = $this->loadBalancer->getConnection( DB_REPLICA );
94 $dbType = $dbr->getType();
95 if ( $dbType ===
'sqlite' || $dbType ===
'postgres' ) {
99 $out->msg(
'supportedlanguages-' . $dbType .
'-error' )->parse()
105 $out->addWikiMsg(
'supportedlanguages-localsummary' );
107 $names = $this->langNameUtils->getLanguageNames( LanguageNameUtils::AUTONYMS, LanguageNameUtils::ALL );
108 $languages = $this->languageCloud();
110 $languages = array_intersect_key( $languages, $names );
112 $this->outputLanguageCloud( $languages, $names );
113 $out->addWikiMsg(
'supportedlanguages-count', $lang->formatNum( count( $languages ) ) );
120 $language = strtolower( $par );
122 $data = $this->translatorActivity->inLanguage( $language );
125 $out->addHTML( Html::errorBox( $this->msg(
'generic-pool-error' )->parse() ) );
127 }
catch ( InvalidArgumentException $e ) {
128 $errorMessageHtml = $this->msg(
'translate-activelanguages-invalid-code' )
129 ->params( LanguageCode::bcp47( $language ) )
131 $out->addHTML( Html::errorBox( $errorMessageHtml ) );
135 $users = $data[
'users'];
136 $users = $this->filterUsers( $users, $language );
137 $this->preQueryUsers( $users );
138 $this->showLanguage( $language, $users, (
int)$data[
'asOfTime'] );
141 private function showLanguage(
string $code, array $users,
int $cachedAt ):
void {
142 $out = $this->getOutput();
143 $lang = $this->getLanguage();
147 $linkInfo[
'rc'][
'title'] = SpecialPage::getTitleFor(
'Recentchanges' );
148 $linkInfo[
'rc'][
'msg'] = $this->msg(
'supportedlanguages-recenttranslations' )->text();
149 $linkInfo[
'stats'][
'title'] = SpecialPage::getTitleFor(
'LanguageStats' );
150 $linkInfo[
'stats'][
'msg'] = $this->msg(
'languagestats' )->text();
152 $local = $this->langNameUtils->getLanguageName( $code, $lang->getCode() );
153 $native = $this->langNameUtils->getLanguageName( $code );
154 $statLanguage = $this->languageFactory->getLanguage( $code );
155 $bcp47Code = $statLanguage->getHtmlCode();
157 $span = Html::rawElement(
'span', [
'lang' => $bcp47Code,
'dir' => $statLanguage->getDir() ], $native );
159 if ( $local !== $native ) {
161 $headerText = $this->msg(
'supportedlanguages-portallink' )
162 ->params( $bcp47Code, $local, $span )->parse();
165 $headerText = $this->msg(
'supportedlanguages-portallink-nocldr' )
166 ->params( $bcp47Code, $span )->parse();
169 $out->addHTML( Html::rawElement(
'h2', [
'id' => $code ], $headerText ) );
173 $links[] = $this->getLinkRenderer()->makeKnownLink(
174 $linkInfo[
'stats'][
'title'],
175 $linkInfo[
'stats'][
'msg'],
179 'suppresscomplete' =>
'1'
182 $links[] = $this->getLinkRenderer()->makeKnownLink(
183 $linkInfo[
'rc'][
'title'],
184 $linkInfo[
'rc'][
'msg'],
187 'translations' =>
'only',
188 'trailer' =>
'/' . $code
191 $linkList = $lang->listToText( $links );
193 $out->addHTML(
'<p>' . $linkList .
"</p>\n" );
194 $this->makeUserList( $users );
196 $ageString = $this->getLanguage()->formatTimePeriod(
198 [
'noabbrevs' =>
true,
'avoid' =>
'avoidseconds' ]
200 $out->addWikiMsg(
'supportedlanguages-colorlegend', $this->getColorLegend() );
201 $out->addWikiMsg(
'translate-supportedlanguages-cached', $ageString );
204 private function languageCloud(): array {
206 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
207 $cachekey = $cache->makeKey(
'translate-supportedlanguages-language-cloud',
'v2' );
209 $data = $cache->get( $cachekey );
210 if ( is_array( $data ) ) {
214 $dbr = $this->loadBalancer->getConnection( DB_REPLICA );
215 $tables = [
'recentchanges' ];
216 $fields = [
'substring_index(rc_title, \'/\', -1) as lang',
'count(*) as count' ];
217 $timestamp = $dbr->timestamp( (
int)wfTimestamp() - 60 * 60 * 24 * $this->period );
219 'rc_timestamp > ' . $dbr->addQuotes( $timestamp ),
220 'rc_namespace' => $this->options->get(
'TranslateMessageNamespaces' ),
221 'rc_title' . $dbr->buildLike( $dbr->anyString(),
'/', $dbr->anyString() ),
223 $options = [
'GROUP BY' =>
'lang',
'HAVING' =>
'count > 20',
'ORDER BY' =>
'NULL' ];
225 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options );
228 foreach ( $res as $row ) {
229 $data[$row->lang] = (int)$row->count;
232 $cache->set( $cachekey, $data, 3600 );
237 protected function filterUsers( array $users,
string $code ): array {
238 foreach ( $users as $index => $user ) {
239 $username = $user[TranslatorActivityQuery::USER_NAME];
241 if ( $this->configHelper->isAuthorExcluded(
'#', $code, $username ) ) {
242 unset( $users[$index] );
249 protected function outputLanguageCloud( array $languages, array $names ) {
250 global $wgTranslateDocumentationLanguageCode;
252 $out = $this->getOutput();
254 $out->addHTML(
'<div class="tagcloud autonym">' );
256 foreach ( $languages as $k => $v ) {
259 $size = round( log( $v ) * 20 ) + 10;
261 if ( $langAttribute === $wgTranslateDocumentationLanguageCode ) {
262 $langAttribute = $this->contentLanguage->getHtmlCode();
266 'href' => $this->getPageTitle( $k )->getLocalURL(),
268 'style' =>
"font-size:$size%",
269 'lang' => $langAttribute,
272 $tag = Html::element(
'a', $params, $name );
273 $out->addHTML( $tag .
"\n" );
275 $out->addHTML(
'</div>' );
278 private function makeUserList( array $userStats ):
void {
283 $period = $this->period;
287 usort( $userStats,
static function ( $a, $b ) {
289 $a[TranslatorActivityQuery::USER_TRANSLATIONS]
291 $b[TranslatorActivityQuery::USER_TRANSLATIONS]
295 foreach ( $userStats as $stats ) {
296 $username = $stats[TranslatorActivityQuery::USER_NAME];
297 $title = Title::makeTitleSafe( NS_USER, $username );
299 LoggerFactory::getInstance(
'Translate' )->warning(
300 "T248125: Got Title-invalid username '{username}'",
301 [
'username' => $username ]
306 $count = $stats[TranslatorActivityQuery::USER_TRANSLATIONS];
307 $lastTranslationTimestamp = $stats[TranslatorActivityQuery::USER_LAST_ACTIVITY];
309 $enc = htmlspecialchars( $username );
313 $styles[
'font-size'] = round( log( $count, 10 ) * 30 ) + 70 .
'%';
315 $last = (int)wfTimestamp() - (int)wfTimestamp( TS_UNIX, $lastTranslationTimestamp );
316 $last = round( $last / $day );
318 $this->msg(
'supportedlanguages-activity', $username )
319 ->numParams( $count, $last )
321 $last = max( 1, min( $period, $last ) );
322 $styles[
'border-bottom'] =
323 '3px solid #' . $this->progressStatsTable->getBackgroundColor( ( $period - $last ) / $period );
325 $stylestr = $this->formatStyle( $styles );
327 $attribs[
'style'] = $stylestr;
331 $this->getLinkRenderer()->makeLink( $title,
new HtmlArmor( $enc ), $attribs );
335 $usernameForGender =
'';
336 if ( count( $userStats ) === 1 ) {
337 $usernameForGender = $userStats[0][TranslatorActivityQuery::USER_NAME];
340 $linkList = $this->getLanguage()->listToText( $links );
341 $html =
"<p class='mw-translate-spsl-translators'>";
342 $html .= $this->msg(
'supportedlanguages-translators' )
343 ->rawParams( $linkList )
344 ->numParams( count( $links ) )
345 ->params( $usernameForGender )
348 $this->getOutput()->addHTML( $html );
351 protected function formatStyle( $styles ) {
353 foreach ( $styles as $key => $value ) {
354 $stylestr .=
"$key:$value;";
360 protected function preQueryUsers( array $users ):
void {
361 $lb = $this->linkBatchFactory->newLinkBatch();
362 foreach ( $users as $data ) {
363 $username = $data[TranslatorActivityQuery::USER_NAME];
364 $user = Title::capitalize( $username, NS_USER );
365 $lb->add( NS_USER, $user );
366 $lb->add( NS_USER_TALK, $user );
371 protected function getColorLegend() {
373 $period = $this->period;
375 for ( $i = 0; $i <= $period; $i += 30 ) {
376 $iFormatted = htmlspecialchars( $this->getLanguage()->formatNum( $i ) );
377 $legend .=
'<span style="background-color:#' .
378 $this->progressStatsTable->getBackgroundColor( ( $period - $i ) / $period ) .
379 "\"> $iFormatted</span>";