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;
11use MediaWiki\Languages\LanguageFactory;
12use MediaWiki\Languages\LanguageNameUtils;
13use MediaWiki\MediaWikiServices;
14use MessageGroup;
16use MWException;
17use Skin;
18use 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( 'wgTranslateLanguages', TranslateUtils::getLanguageNames( null ) );
89
90 $out->addHTML( Html::openElement( 'div', [
91 'class' => 'grid ext-translate-container',
92 ] ) );
93
94 $out->addHTML( $this->tuxSettingsForm() );
95 $out->addHTML( $this->messageSelector() );
96
97 $table = new MessageTable( $this->getContext(), $this->group, $this->options['language'] );
98 $output = $table->fullTable();
99
100 $out->addHTML( $output );
101 $out->addHTML( Html::closeElement( 'div' ) );
102 }
103
104 protected function setup( ?string $parameters ): void {
105 $request = $this->getRequest();
106
107 $defaults = [
108 /* str */'language' => $this->getLanguage()->getCode(),
109 /* str */'group' => '!additions',
110 ];
111
112 // Dump everything here
113 $nondefaults = [];
114
115 $parameters = $parameters !== null ? array_map( 'trim', explode( ';', $parameters ) ) : [];
116 $pars = [];
117
118 foreach ( $parameters as $_ ) {
119 if ( $_ === '' ) {
120 continue;
121 }
122
123 if ( strpos( $_, '=' ) !== false ) {
124 [ $key, $value ] = array_map( 'trim', explode( '=', $_, 2 ) );
125 } else {
126 $key = 'group';
127 $value = $_;
128 }
129
130 $pars[$key] = $value;
131 }
132
133 foreach ( $defaults as $v => $t ) {
134 if ( is_bool( $t ) ) {
135 $r = isset( $pars[$v] ) ? (bool)$pars[$v] : $defaults[$v];
136 $r = $request->getBool( $v, $r );
137 } elseif ( is_int( $t ) ) {
138 $r = isset( $pars[$v] ) ? (int)$pars[$v] : $defaults[$v];
139 $r = $request->getInt( $v, $r );
140 } elseif ( is_string( $t ) ) {
141 $r = isset( $pars[$v] ) ? (string)$pars[$v] : $defaults[$v];
142 $r = $request->getText( $v, $r );
143 }
144
145 if ( !isset( $r ) ) {
146 throw new MWException( '$r was not set' );
147 }
148
149 if ( $defaults[$v] !== $r ) {
150 $nondefaults[$v] = $r;
151 }
152 }
153
154 $this->defaults = $defaults;
155 $this->nondefaults = $nondefaults;
156 Hooks::run( 'TranslateGetSpecialTranslateOptions', [ &$defaults, &$nondefaults ] );
157
158 $this->options = $nondefaults + $defaults;
159 $this->group = MessageGroups::getGroup( $this->options['group'] );
160 if ( $this->group ) {
161 $this->options['group'] = $this->group->getId();
162 } else {
163 $this->group = MessageGroups::getGroup( $this->defaults['group'] );
164 }
165
166 if ( !Language::isKnownLanguageTag( $this->options['language'] ) ) {
167 $this->options['language'] = $this->defaults['language'];
168 }
169
170 if ( MessageGroups::isDynamic( $this->group ) ) {
171 // @phan-suppress-next-line PhanUndeclaredMethod
172 $this->group->setLanguage( $this->options['language'] );
173 }
174 }
175
176 protected function tuxSettingsForm(): string {
177 $nojs = Html::errorBox(
178 $this->msg( 'tux-nojs' )->plain(),
179 '',
180 'tux-nojs'
181 );
182
183 $attrs = [ 'class' => 'row tux-editor-header' ];
184 $selectors = $this->tuxGroupSelector() .
185 $this->tuxLanguageSelector() .
186 $this->tuxGroupDescription() .
187 $this->tuxWorkflowSelector() .
188 $this->tuxGroupWarning();
189
190 return Html::rawElement( 'div', $attrs, $selectors ) . $nojs;
191 }
192
193 protected function messageSelector(): string {
194 $output = Html::openElement( 'div', [ 'class' => 'row tux-messagetable-header hide' ] );
195 $output .= Html::openElement( 'div', [ 'class' => 'nine columns' ] );
196 $output .= Html::openElement( 'ul', [ 'class' => 'row tux-message-selector' ] );
197 $userId = $this->getUser()->getId();
198 $tabs = [
199 'all' => '',
200 'untranslated' => '!translated',
201 'outdated' => 'fuzzy',
202 'translated' => 'translated',
203 'unproofread' => "translated|!reviewer:$userId|!last-translator:$userId",
204 ];
205
206 $params = $this->nondefaults;
207
208 foreach ( $tabs as $tab => $filter ) {
209 // Possible classes and messages, for grepping:
210 // tux-tab-all
211 // tux-tab-untranslated
212 // tux-tab-outdated
213 // tux-tab-translated
214 // tux-tab-unproofread
215 $tabClass = "tux-tab-$tab";
216 $taskParams = [ 'filter' => $filter ] + $params;
217 ksort( $taskParams );
218 $href = $this->getPageTitle()->getLocalURL( $taskParams );
219 $link = Html::element( 'a', [ 'href' => $href ], $this->msg( $tabClass )->text() );
220 $output .= Html::rawElement( 'li', [
221 'class' => 'column ' . $tabClass,
222 'data-filter' => $filter,
223 'data-title' => $tab,
224 ], $link );
225 }
226
227 // Check boxes for the "more" tab.
228 // The array keys are used as the name attribute of the checkbox.
229 // in the id attribute as tux-option-KEY,
230 // and and also for the data-filter attribute.
231 // The message is shown as the check box's label.
232 $options = [
233 'optional' => $this->msg( 'tux-message-filter-optional-messages-label' )->text(),
234 ];
235
236 $container = Html::openElement( 'ul', [ 'class' => 'column tux-message-selector' ] );
237 foreach ( $options as $optFilter => $optLabel ) {
238 $container .= Html::rawElement( 'li',
239 [ 'class' => 'column' ],
240 Xml::checkLabel(
241 $optLabel,
242 $optFilter,
243 "tux-option-$optFilter",
244 isset( $this->nondefaults[$optFilter] ),
245 [ 'data-filter' => $optFilter ]
246 )
247 );
248 }
249
250 $container .= Html::closeElement( 'ul' );
251
252 // @todo FIXME: Hard coded "ellipsis".
253 $output .= Html::openElement( 'li', [ 'class' => 'column more' ] ) .
254 '...' .
255 $container .
256 Html::closeElement( 'li' );
257
258 $output .= Html::closeElement( 'ul' );
259 $output .= Html::closeElement( 'div' ); // close nine columns
260 $output .= Html::openElement( 'div', [ 'class' => 'three columns' ] );
261 $output .= Html::rawElement(
262 'div',
263 [ 'class' => 'tux-message-filter-wrapper' ],
264 Html::element( 'input', [
265 'class' => 'tux-message-filter-box',
266 'type' => 'search',
267 'placeholder' => $this->msg( 'tux-message-filter-placeholder' )->text()
268 ] )
269 );
270
271 // close three columns and the row
272 $output .= Html::closeElement( 'div' ) . Html::closeElement( 'div' );
273
274 return $output;
275 }
276
277 protected function tuxGroupSelector(): string {
278 $groupClass = [ 'grouptitle', 'grouplink' ];
279 if ( $this->group instanceof AggregateMessageGroup ) {
280 $groupClass[] = 'tux-breadcrumb__item--aggregate';
281 }
282
283 // @todo FIXME The selector should have expanded parent-child lists
284 $output = Html::openElement( 'div', [
285 'class' => 'eight columns tux-breadcrumb',
286 'data-language' => $this->options['language'],
287 ] ) .
288 Html::element( 'span',
289 [ 'class' => 'grouptitle' ],
290 $this->msg( 'translate-msggroupselector-projects' )->text()
291 ) .
292 Html::element( 'span',
293 [ 'class' => 'grouptitle grouplink tux-breadcrumb__item--aggregate' ],
294 $this->msg( 'translate-msggroupselector-search-all' )->text()
295 ) .
296 Html::element( 'span',
297 [
298 'class' => $groupClass,
299 'data-msggroupid' => $this->group->getId(),
300 ],
301 $this->group->getLabel( $this->getContext() )
302 ) .
303 Html::closeElement( 'div' );
304
305 return $output;
306 }
307
308 protected function tuxLanguageSelector(): string {
309 global $wgTranslateDocumentationLanguageCode;
310
311 if ( $this->options['language'] === $wgTranslateDocumentationLanguageCode ) {
312 $targetLangName = $this->msg( 'translate-documentation-language' )->text();
313 $targetLanguage = $this->contentLanguage;
314 } else {
315 $targetLangName = $this->languageNameUtils->getLanguageName( $this->options['language'] );
316 $targetLanguage = $this->languageFactory->getLanguage( $this->options['language'] );
317 }
318
319 $label = Html::element( 'span', [], $this->msg( 'tux-languageselector' )->text() );
320
321 $languageIcon = Html::element(
322 'span',
323 [ 'class' => 'ext-translate-language-icon' ]
324 );
325
326 $targetLanguageName = Html::element(
327 'span',
328 [
329 'class' => 'ext-translate-target-language',
330 'dir' => $targetLanguage->getDir(),
331 'lang' => $targetLanguage->getHtmlCode()
332 ],
333 $targetLangName
334 );
335
336 $expandIcon = Html::element(
337 'span',
338 [ 'class' => 'ext-translate-language-selector-expand' ]
339 );
340
341 $value = Html::rawElement(
342 'span',
343 [
344 'class' => 'uls mw-ui-button',
345 'tabindex' => 0,
346 'title' => $this->msg( 'tux-select-target-language' )->text()
347 ],
348 $languageIcon . $targetLanguageName . $expandIcon
349 );
350
351 return Html::rawElement(
352 'div',
353 [ 'class' => 'four columns ext-translate-language-selector' ],
354 "$label $value"
355 );
356 }
357
358 protected function tuxGroupDescription(): string {
359 // Initialize an empty warning box to be filled client-side.
360 return Html::rawElement(
361 'div',
362 [ 'class' => 'twelve columns description' ],
363 $this->getGroupDescription( $this->group )
364 );
365 }
366
367 protected function getGroupDescription( MessageGroup $group ): string {
368 $description = $group->getDescription( $this->getContext() );
369 return $description === null ?
370 '' : $this->getOutput()->parseAsInterface( $description );
371 }
372
373 protected function tuxGroupWarning(): string {
374 if ( $this->options['group'] === '' ) {
375 return Html::rawElement(
376 'div',
377 [ 'class' => 'twelve columns group-warning' ],
378 $this->msg( 'tux-translate-page-no-such-group' )->parse()
379 );
380 }
381
382 // Initialize an empty warning box to be filled client-side.
383 return Html::element(
384 'div',
385 [ 'class' => 'twelve columns group-warning' ],
386 ''
387 );
388 }
389
390 protected function tuxWorkflowSelector(): string {
391 return Html::element( 'div', [ 'class' => 'tux-workflow twelve columns' ] );
392 }
393
398 public static function tabify( Skin $skin, array &$tabs ): bool {
399 $title = $skin->getTitle();
400 if ( !$title->isSpecialPage() ) {
401 return true;
402 }
403 [ $alias, $sub ] = MediaWikiServices::getInstance()
404 ->getSpecialPageFactory()->resolveAlias( $title->getText() );
405
406 $pagesInGroup = [ 'Translate', 'LanguageStats', 'MessageGroupStats', 'ExportTranslations' ];
407 if ( !in_array( $alias, $pagesInGroup, true ) ) {
408 return true;
409 }
410
411 // Extract subpage syntax, otherwise the values are not passed forward
412 $params = [];
413 if ( $sub !== null && trim( $sub ) !== '' ) {
414 if ( $alias === 'Translate' || $alias === 'MessageGroupStats' ) {
415 $params['group'] = $sub;
416 } elseif ( $alias === 'LanguageStats' ) {
417 // Breaks if additional parameters besides language are code provided
418 $params['language'] = $sub;
419 }
420 }
421
422 $request = $skin->getRequest();
423 // However, query string params take precedence
424 $params['language'] = $request->getVal( 'language' );
425 $params['group'] = $request->getVal( 'group' );
426
427 $translate = SpecialPage::getTitleFor( 'Translate' );
428 $languagestats = SpecialPage::getTitleFor( 'LanguageStats' );
429 $messagegroupstats = SpecialPage::getTitleFor( 'MessageGroupStats' );
430
431 // Clear the special page tab that might be there already
432 $tabs['namespaces'] = [];
433
434 $tabs['namespaces']['translate'] = [
435 'text' => wfMessage( 'translate-taction-translate' )->text(),
436 'href' => $translate->getLocalURL( $params ),
437 'class' => 'tux-tab',
438 ];
439
440 if ( $alias === 'Translate' ) {
441 $tabs['namespaces']['translate']['class'] .= ' selected';
442 }
443
444 $tabs['views']['lstats'] = [
445 'text' => wfMessage( 'translate-taction-lstats' )->text(),
446 'href' => $languagestats->getLocalURL( $params ),
447 'class' => 'tux-tab',
448 ];
449 if ( $alias === 'LanguageStats' ) {
450 $tabs['views']['lstats']['class'] .= ' selected';
451 }
452
453 $tabs['views']['mstats'] = [
454 'text' => wfMessage( 'translate-taction-mstats' )->text(),
455 'href' => $messagegroupstats->getLocalURL( $params ),
456 'class' => 'tux-tab',
457 ];
458
459 if ( $alias === 'MessageGroupStats' ) {
460 $tabs['views']['mstats']['class'] .= ' selected';
461 }
462
463 $tabs['views']['export'] = [
464 'text' => wfMessage( 'translate-taction-export' )->text(),
465 'href' => SpecialPage::getTitleFor( 'ExportTranslations' )->getLocalURL( $params ),
466 'class' => 'tux-tab',
467 ];
468
469 return true;
470 }
471}
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'), MessageIndex::singleton());}, '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: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: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: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());}, '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.
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.
Factory class for accessing message groups individually by id or all of them as an list.
static getGroup( $id)
Fetch a message group by id.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Interface for message groups.