Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TranslateSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorInterface;
5
7use ErrorPageError;
8use Hooks;
9use Html;
10use Language;
13use MediaWiki\Languages\LanguageFactory;
14use MediaWiki\Languages\LanguageNameUtils;
15use MediaWiki\MediaWikiServices;
16use MessageGroup;
17use MWException;
18use Skin;
19use SpecialPage;
20use Xml;
21
31class TranslateSpecialPage extends SpecialPage {
33 protected $group;
34 protected $defaults;
35 protected $nondefaults = [];
36 protected $options;
38 private $contentLanguage;
40 private $languageFactory;
42 private $languageNameUtils;
43
44 public function __construct(
45 Language $contentLanguage,
46 LanguageFactory $languageFactory,
47 LanguageNameUtils $languageNameUtils
48 ) {
49 parent::__construct( 'Translate' );
50 $this->contentLanguage = $contentLanguage;
51 $this->languageFactory = $languageFactory;
52 $this->languageNameUtils = $languageNameUtils;
53 }
54
55 public function doesWrites() {
56 return true;
57 }
58
59 protected function getGroupName() {
60 return 'translation';
61 }
62
69 public function execute( $parameters ) {
70 $out = $this->getOutput();
71 $out->addModuleStyles( [
72 'ext.translate.special.translate.styles',
73 'jquery.uls.grid',
74 'mediawiki.ui.button'
75 ] );
76
77 $this->setHeaders();
78
79 $this->setup( $parameters );
80
81 // Redirect old export URLs to Special:ExportTranslations
82 if ( $this->getRequest()->getText( 'taction' ) === 'export' ) {
83 $exportPage = SpecialPage::getTitleFor( 'ExportTranslations' );
84 $out->redirect( $exportPage->getLocalURL( $this->nondefaults ) );
85 }
86
87 $out->addModules( 'ext.translate.special.translate' );
88 $out->addJsConfigVars(
89 'wgTranslateLanguages',
90 Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS )
91 );
92
93 $out->addHTML( Html::openElement( 'div', [
94 'class' => 'grid ext-translate-container',
95 ] ) );
96
97 $out->addHTML( $this->tuxSettingsForm() );
98 $out->addHTML( $this->messageSelector() );
99
100 $table = new MessageTable( $this->getContext(), $this->group, $this->options['language'] );
101 $output = $table->fullTable();
102
103 $out->addHTML( $output );
104 $out->addHTML( Html::closeElement( 'div' ) );
105 }
106
107 protected function setup( ?string $parameters ): void {
108 $request = $this->getRequest();
109
110 $defaults = [
111 /* str */'language' => $this->getLanguage()->getCode(),
112 /* str */'group' => '!additions',
113 ];
114
115 // Dump everything here
116 $nondefaults = [];
117
118 $parameters = $parameters !== null ? array_map( 'trim', explode( ';', $parameters ) ) : [];
119 $pars = [];
120
121 foreach ( $parameters as $_ ) {
122 if ( $_ === '' ) {
123 continue;
124 }
125
126 if ( strpos( $_, '=' ) !== false ) {
127 [ $key, $value ] = array_map( 'trim', explode( '=', $_, 2 ) );
128 } else {
129 $key = 'group';
130 $value = $_;
131 }
132
133 $pars[$key] = $value;
134 }
135
136 foreach ( $defaults as $v => $t ) {
137 if ( is_bool( $t ) ) {
138 $r = isset( $pars[$v] ) ? (bool)$pars[$v] : $defaults[$v];
139 $r = $request->getBool( $v, $r );
140 } elseif ( is_int( $t ) ) {
141 $r = isset( $pars[$v] ) ? (int)$pars[$v] : $defaults[$v];
142 $r = $request->getInt( $v, $r );
143 } elseif ( is_string( $t ) ) {
144 $r = isset( $pars[$v] ) ? (string)$pars[$v] : $defaults[$v];
145 $r = $request->getText( $v, $r );
146 }
147
148 if ( !isset( $r ) ) {
149 throw new MWException( '$r was not set' );
150 }
151
152 if ( $defaults[$v] !== $r ) {
153 $nondefaults[$v] = $r;
154 }
155 }
156
157 $this->defaults = $defaults;
158 $this->nondefaults = $nondefaults;
159 Hooks::run( 'TranslateGetSpecialTranslateOptions', [ &$defaults, &$nondefaults ] );
160
161 $this->options = $nondefaults + $defaults;
162 $this->group = MessageGroups::getGroup( $this->options['group'] );
163 if ( $this->group ) {
164 $this->options['group'] = $this->group->getId();
165 } else {
166 $this->group = MessageGroups::getGroup( $this->defaults['group'] );
167 }
168
169 if ( !$this->languageNameUtils->isKnownLanguageTag( $this->options['language'] ) ) {
170 $this->options['language'] = $this->defaults['language'];
171 }
172
173 if ( MessageGroups::isDynamic( $this->group ) ) {
174 // @phan-suppress-next-line PhanUndeclaredMethod
175 $this->group->setLanguage( $this->options['language'] );
176 }
177 }
178
179 protected function tuxSettingsForm(): string {
180 $nojs = Html::errorBox(
181 $this->msg( 'tux-nojs' )->plain(),
182 '',
183 'tux-nojs'
184 );
185
186 $attrs = [ 'class' => 'row tux-editor-header' ];
187 $selectors = $this->tuxGroupSelector() .
188 $this->tuxLanguageSelector() .
189 $this->tuxGroupDescription() .
190 $this->tuxWorkflowSelector() .
191 $this->tuxGroupWarning();
192
193 return Html::rawElement( 'div', $attrs, $selectors ) . $nojs;
194 }
195
196 protected function messageSelector(): string {
197 $output = Html::openElement( 'div', [ 'class' => 'row tux-messagetable-header hide' ] );
198 $output .= Html::openElement( 'div', [ 'class' => 'nine columns' ] );
199 $output .= Html::openElement( 'ul', [ 'class' => 'row tux-message-selector' ] );
200 $userId = $this->getUser()->getId();
201 $tabs = [
202 'all' => '',
203 'untranslated' => '!translated',
204 'outdated' => 'fuzzy',
205 'translated' => 'translated',
206 'unproofread' => "translated|!reviewer:$userId|!last-translator:$userId",
207 ];
208
209 $params = $this->nondefaults;
210
211 foreach ( $tabs as $tab => $filter ) {
212 // Possible classes and messages, for grepping:
213 // tux-tab-all
214 // tux-tab-untranslated
215 // tux-tab-outdated
216 // tux-tab-translated
217 // tux-tab-unproofread
218 $tabClass = "tux-tab-$tab";
219 $taskParams = [ 'filter' => $filter ] + $params;
220 ksort( $taskParams );
221 $href = $this->getPageTitle()->getLocalURL( $taskParams );
222 $link = Html::element( 'a', [ 'href' => $href ], $this->msg( $tabClass )->text() );
223 $output .= Html::rawElement( 'li', [
224 'class' => 'column ' . $tabClass,
225 'data-filter' => $filter,
226 'data-title' => $tab,
227 ], $link );
228 }
229
230 // Check boxes for the "more" tab.
231 // The array keys are used as the name attribute of the checkbox.
232 // in the id attribute as tux-option-KEY,
233 // and also for the data-filter attribute.
234 // The message is shown as the checkbox's label.
235 $options = [
236 'optional' => $this->msg( 'tux-message-filter-optional-messages-label' )->text(),
237 ];
238
239 $container = Html::openElement( 'ul', [ 'class' => 'column tux-message-selector' ] );
240 foreach ( $options as $optFilter => $optLabel ) {
241 $container .= Html::rawElement( 'li',
242 [ 'class' => 'column' ],
243 Xml::checkLabel(
244 $optLabel,
245 $optFilter,
246 "tux-option-$optFilter",
247 isset( $this->nondefaults[$optFilter] ),
248 [ 'data-filter' => $optFilter ]
249 )
250 );
251 }
252
253 $container .= Html::closeElement( 'ul' );
254
255 // @todo FIXME: Hard coded "ellipsis".
256 $output .= Html::openElement( 'li', [ 'class' => 'column more' ] ) .
257 '...' .
258 $container .
259 Html::closeElement( 'li' );
260
261 $output .= Html::closeElement( 'ul' );
262 $output .= Html::closeElement( 'div' ); // close nine columns
263 $output .= Html::openElement( 'div', [ 'class' => 'three columns' ] );
264 $output .= Html::rawElement(
265 'div',
266 [ 'class' => 'tux-message-filter-wrapper' ],
267 Html::element( 'input', [
268 'class' => 'tux-message-filter-box',
269 'type' => 'search',
270 'placeholder' => $this->msg( 'tux-message-filter-placeholder' )->text()
271 ] )
272 );
273
274 // close three columns and the row
275 $output .= Html::closeElement( 'div' ) . Html::closeElement( 'div' );
276
277 return $output;
278 }
279
280 protected function tuxGroupSelector(): string {
281 $groupClass = [ 'grouptitle', 'grouplink' ];
282 if ( $this->group instanceof AggregateMessageGroup ) {
283 $groupClass[] = 'tux-breadcrumb__item--aggregate';
284 }
285
286 // @todo FIXME The selector should have expanded parent-child lists
287 $output = Html::openElement( 'div', [
288 'class' => 'eight columns tux-breadcrumb',
289 'data-language' => $this->options['language'],
290 ] ) .
291 Html::element( 'span',
292 [ 'class' => 'grouptitle' ],
293 $this->msg( 'translate-msggroupselector-projects' )->text()
294 ) .
295 Html::element( 'span',
296 [ 'class' => 'grouptitle grouplink tux-breadcrumb__item--aggregate' ],
297 $this->msg( 'translate-msggroupselector-search-all' )->text()
298 ) .
299 Html::element( 'span',
300 [
301 'class' => $groupClass,
302 'data-msggroupid' => $this->group->getId(),
303 ],
304 $this->group->getLabel( $this->getContext() )
305 ) .
306 Html::closeElement( 'div' );
307
308 return $output;
309 }
310
311 protected function tuxLanguageSelector(): string {
312 global $wgTranslateDocumentationLanguageCode;
313
314 if ( $this->options['language'] === $wgTranslateDocumentationLanguageCode ) {
315 $targetLangName = $this->msg( 'translate-documentation-language' )->text();
316 $targetLanguage = $this->contentLanguage;
317 } else {
318 $targetLangName = $this->languageNameUtils->getLanguageName( $this->options['language'] );
319 $targetLanguage = $this->languageFactory->getLanguage( $this->options['language'] );
320 }
321
322 $label = Html::element( 'span', [], $this->msg( 'tux-languageselector' )->text() );
323
324 $languageIcon = Html::element(
325 'span',
326 [ 'class' => 'ext-translate-language-icon' ]
327 );
328
329 $targetLanguageName = Html::element(
330 'span',
331 [
332 'class' => 'ext-translate-target-language',
333 'dir' => $targetLanguage->getDir(),
334 'lang' => $targetLanguage->getHtmlCode()
335 ],
336 $targetLangName
337 );
338
339 $expandIcon = Html::element(
340 'span',
341 [ 'class' => 'ext-translate-language-selector-expand' ]
342 );
343
344 $value = Html::rawElement(
345 'span',
346 [
347 'class' => 'uls mw-ui-button',
348 'tabindex' => 0,
349 'title' => $this->msg( 'tux-select-target-language' )->text()
350 ],
351 $languageIcon . $targetLanguageName . $expandIcon
352 );
353
354 return Html::rawElement(
355 'div',
356 [ 'class' => 'four columns ext-translate-language-selector' ],
357 "$label $value"
358 );
359 }
360
361 protected function tuxGroupDescription(): string {
362 // Initialize an empty warning box to be filled client-side.
363 return Html::rawElement(
364 'div',
365 [ 'class' => 'twelve columns description' ],
366 $this->getGroupDescription( $this->group )
367 );
368 }
369
370 protected function getGroupDescription( MessageGroup $group ): string {
371 $description = $group->getDescription( $this->getContext() );
372 return $description === null ?
373 '' : $this->getOutput()->parseAsInterface( $description );
374 }
375
376 protected function tuxGroupWarning(): string {
377 if ( $this->options['group'] === '' ) {
378 return Html::warningBox(
379 $this->msg( 'tux-translate-page-no-such-group' )->parse(),
380 'tux-group-warning twelve column'
381 );
382 }
383
384 // Initialize an empty warning box to be filled client-side.
385 return Html::warningBox( '', 'tux-group-warning twelve column' );
386 }
387
388 protected function tuxWorkflowSelector(): string {
389 return Html::element( 'div', [ 'class' => 'tux-workflow twelve columns' ] );
390 }
391
396 public static function tabify( Skin $skin, array &$tabs ): bool {
397 $title = $skin->getTitle();
398 if ( !$title->isSpecialPage() ) {
399 return true;
400 }
401 [ $alias, $sub ] = MediaWikiServices::getInstance()
402 ->getSpecialPageFactory()->resolveAlias( $title->getText() );
403
404 $pagesInGroup = [ 'Translate', 'LanguageStats', 'MessageGroupStats', 'ExportTranslations' ];
405 if ( !in_array( $alias, $pagesInGroup, true ) ) {
406 return true;
407 }
408
409 // Extract subpage syntax, otherwise the values are not passed forward
410 $params = [];
411 if ( $sub !== null && trim( $sub ) !== '' ) {
412 if ( $alias === 'Translate' || $alias === 'MessageGroupStats' ) {
413 $params['group'] = $sub;
414 } elseif ( $alias === 'LanguageStats' ) {
415 // Breaks if additional parameters besides language are code provided
416 $params['language'] = $sub;
417 }
418 }
419
420 $request = $skin->getRequest();
421 // However, query string params take precedence
422 $params['language'] = $request->getRawVal( 'language' ) ?? '';
423 $params['group'] = $request->getRawVal( 'group' ) ?? '';
424
425 // Remove empty values from params
426 $params = array_filter( $params, static function ( string $param ) {
427 return $param !== '';
428 } );
429
430 $translate = SpecialPage::getTitleFor( 'Translate' );
431 $languagestats = SpecialPage::getTitleFor( 'LanguageStats' );
432 $messagegroupstats = SpecialPage::getTitleFor( 'MessageGroupStats' );
433
434 // Clear the special page tab that might be there already
435 $tabs['namespaces'] = [];
436
437 $tabs['namespaces']['translate'] = [
438 'text' => wfMessage( 'translate-taction-translate' )->text(),
439 'href' => $translate->getLocalURL( $params ),
440 'class' => 'tux-tab',
441 ];
442
443 if ( $alias === 'Translate' ) {
444 $tabs['namespaces']['translate']['class'] .= ' selected';
445 }
446
447 $tabs['views']['lstats'] = [
448 'text' => wfMessage( 'translate-taction-lstats' )->text(),
449 'href' => $languagestats->getLocalURL( $params ),
450 'class' => 'tux-tab',
451 ];
452 if ( $alias === 'LanguageStats' ) {
453 $tabs['views']['lstats']['class'] .= ' selected';
454 }
455
456 $tabs['views']['mstats'] = [
457 'text' => wfMessage( 'translate-taction-mstats' )->text(),
458 'href' => $messagegroupstats->getLocalURL( $params ),
459 'class' => 'tux-tab',
460 ];
461
462 if ( $alias === 'MessageGroupStats' ) {
463 $tabs['views']['mstats']['class'] .= ' selected';
464 }
465
466 $tabs['views']['export'] = [
467 'text' => wfMessage( 'translate-taction-export' )->text(),
468 'href' => SpecialPage::getTitleFor( 'ExportTranslations' )->getLocalURL( $params ),
469 'class' => 'tux-tab',
470 ];
471
472 return true;
473 }
474}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Groups multiple message groups together as one group.
Factory class for accessing message groups individually by id or all of them as a list.
Implements the core of Translate extension - a special page which shows a list of messages in a forma...
static tabify(Skin $skin, array &$tabs)
Adds the task-based tabs on Special:Translate and few other special pages.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:30
Interface for message groups.