Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 265 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
TranslateSpecialPage | |
0.00% |
0 / 265 |
|
0.00% |
0 / 15 |
1892 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
6 | |||
setup | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
182 | |||
tuxSettingsForm | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
messageSelector | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
6 | |||
tuxGroupSelector | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
6 | |||
tuxLanguageSelector | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
6 | |||
tuxGroupSubscription | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
tuxGroupDescription | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getGroupDescription | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
tuxGroupWarning | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
tuxWorkflowSelector | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tabify | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\TranslatorInterface; |
5 | |
6 | use AggregateMessageGroup; |
7 | use Language; |
8 | use MediaWiki\Extension\Translate\HookRunner; |
9 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
10 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
11 | use MediaWiki\Html\Html; |
12 | use MediaWiki\Languages\LanguageFactory; |
13 | use MediaWiki\Languages\LanguageNameUtils; |
14 | use MediaWiki\Logger\LoggerFactory; |
15 | use MediaWiki\MediaWikiServices; |
16 | use MessageGroup; |
17 | use Psr\Log\LoggerInterface; |
18 | use Skin; |
19 | use SpecialPage; |
20 | |
21 | /** |
22 | * Implements the core of Translate extension - a special page which shows |
23 | * a list of messages in a format defined by Tasks. |
24 | * |
25 | * @author Niklas Laxström |
26 | * @author Siebrand Mazeland |
27 | * @license GPL-2.0-or-later |
28 | * @ingroup SpecialPage TranslateSpecialPage |
29 | */ |
30 | class TranslateSpecialPage extends SpecialPage { |
31 | private ?MessageGroup $group = null; |
32 | private array $options = []; |
33 | private Language $contentLanguage; |
34 | private LanguageFactory $languageFactory; |
35 | private LanguageNameUtils $languageNameUtils; |
36 | private HookRunner $hookRunner; |
37 | private LoggerInterface $logger; |
38 | |
39 | public function __construct( |
40 | Language $contentLanguage, |
41 | LanguageFactory $languageFactory, |
42 | LanguageNameUtils $languageNameUtils, |
43 | HookRunner $hookRunner |
44 | ) { |
45 | parent::__construct( 'Translate' ); |
46 | $this->contentLanguage = $contentLanguage; |
47 | $this->languageFactory = $languageFactory; |
48 | $this->languageNameUtils = $languageNameUtils; |
49 | $this->hookRunner = $hookRunner; |
50 | $this->logger = LoggerFactory::getInstance( 'Translate' ); |
51 | } |
52 | |
53 | public function doesWrites() { |
54 | return true; |
55 | } |
56 | |
57 | protected function getGroupName() { |
58 | return 'translation'; |
59 | } |
60 | |
61 | /** @inheritDoc */ |
62 | public function execute( $parameters ) { |
63 | $out = $this->getOutput(); |
64 | $out->addModuleStyles( [ |
65 | 'ext.translate.special.translate.styles', |
66 | 'jquery.uls.grid', |
67 | 'mediawiki.ui.button' |
68 | ] ); |
69 | |
70 | $this->setHeaders(); |
71 | |
72 | $this->setup( $parameters ); |
73 | |
74 | // Redirect old export URLs to Special:ExportTranslations |
75 | if ( $this->getRequest()->getText( 'taction' ) === 'export' ) { |
76 | $exportPage = SpecialPage::getTitleFor( 'ExportTranslations' ); |
77 | $out->redirect( $exportPage->getLocalURL( $this->options ) ); |
78 | } |
79 | |
80 | $out->addModules( 'ext.translate.special.translate' ); |
81 | $out->addJsConfigVars( |
82 | 'wgTranslateLanguages', |
83 | Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS ) |
84 | ); |
85 | |
86 | $out->addHTML( Html::openElement( 'div', [ |
87 | 'class' => 'grid ext-translate-container', |
88 | ] ) ); |
89 | |
90 | $out->addHTML( $this->tuxSettingsForm() ); |
91 | $out->addHTML( $this->messageSelector() ); |
92 | |
93 | $table = new MessageTable( $this->getContext(), $this->group, $this->options['language'] ); |
94 | $output = $table->fullTable(); |
95 | |
96 | $out->addHTML( $output ); |
97 | $out->addHTML( Html::closeElement( 'div' ) ); |
98 | } |
99 | |
100 | private function setup( ?string $parameters ): void { |
101 | $request = $this->getRequest(); |
102 | |
103 | $defaults = [ |
104 | 'language' => $this->getLanguage()->getCode(), |
105 | 'group' => '!additions', |
106 | ]; |
107 | |
108 | // Dump everything here |
109 | $nonDefaults = []; |
110 | $parameters = array_map( 'trim', explode( ';', (string)$parameters ) ); |
111 | |
112 | foreach ( $parameters as $_ ) { |
113 | if ( $_ === '' ) { |
114 | continue; |
115 | } |
116 | |
117 | if ( str_contains( $_, '=' ) ) { |
118 | [ $key, $value ] = array_map( 'trim', explode( '=', $_, 2 ) ); |
119 | } else { |
120 | $key = 'group'; |
121 | $value = $_; |
122 | } |
123 | |
124 | if ( isset( $defaults[$key] ) ) { |
125 | $nonDefaults[$key] = $value; |
126 | } |
127 | } |
128 | |
129 | foreach ( array_keys( $defaults ) as $key ) { |
130 | $value = $request->getVal( $key ); |
131 | if ( is_string( $value ) ) { |
132 | $nonDefaults[$key] = $value; |
133 | } |
134 | } |
135 | |
136 | $this->hookRunner->onTranslateGetSpecialTranslateOptions( $defaults, $nonDefaults ); |
137 | |
138 | $this->options = $nonDefaults + $defaults; |
139 | $this->group = MessageGroups::getGroup( $this->options['group'] ); |
140 | if ( $this->group ) { |
141 | $this->options['group'] = $this->group->getId(); |
142 | } else { |
143 | $this->group = MessageGroups::getGroup( $defaults['group'] ); |
144 | if ( |
145 | isset( $nonDefaults['group'] ) && |
146 | str_starts_with( $nonDefaults['group'], 'page-' ) && |
147 | !str_contains( $nonDefaults['group'], '+' ) |
148 | ) { |
149 | // https://phabricator.wikimedia.org/T320220 |
150 | $this->logger->debug( |
151 | "[Special:Translate] Requested group {groupId} doesn't exist.", |
152 | [ 'groupId' => $nonDefaults['group'] ] |
153 | ); |
154 | } |
155 | } |
156 | |
157 | if ( !$this->languageNameUtils->isKnownLanguageTag( $this->options['language'] ) ) { |
158 | $this->options['language'] = $defaults['language']; |
159 | } |
160 | |
161 | if ( MessageGroups::isDynamic( $this->group ) ) { |
162 | // @phan-suppress-next-line PhanUndeclaredMethod |
163 | $this->group->setLanguage( $this->options['language'] ); |
164 | } |
165 | } |
166 | |
167 | private function tuxSettingsForm(): string { |
168 | $noJs = Html::errorBox( |
169 | $this->msg( 'tux-nojs' )->escaped(), |
170 | '', |
171 | 'tux-nojs' |
172 | ); |
173 | |
174 | $attrs = [ 'class' => 'row tux-editor-header' ]; |
175 | $selectors = $this->tuxGroupSelector() . |
176 | $this->tuxLanguageSelector() . |
177 | $this->tuxGroupSubscription() . |
178 | $this->tuxGroupDescription() . |
179 | $this->tuxWorkflowSelector() . |
180 | $this->tuxGroupWarning(); |
181 | |
182 | return Html::rawElement( 'div', $attrs, $selectors ) . $noJs; |
183 | } |
184 | |
185 | private function messageSelector(): string { |
186 | $output = Html::openElement( 'div', [ 'class' => 'row tux-messagetable-header hide' ] ); |
187 | $output .= Html::openElement( 'div', [ 'class' => 'nine columns' ] ); |
188 | $output .= Html::openElement( 'ul', [ 'class' => 'row tux-message-selector' ] ); |
189 | $userId = $this->getUser()->getId(); |
190 | $tabs = [ |
191 | 'all' => '', |
192 | 'untranslated' => '!translated', |
193 | 'outdated' => 'fuzzy', |
194 | 'translated' => 'translated', |
195 | 'unproofread' => "translated|!reviewer:$userId|!last-translator:$userId", |
196 | ]; |
197 | |
198 | foreach ( $tabs as $tab => $filter ) { |
199 | // Possible classes and messages, for grepping: |
200 | // tux-tab-all |
201 | // tux-tab-untranslated |
202 | // tux-tab-outdated |
203 | // tux-tab-translated |
204 | // tux-tab-unproofread |
205 | $tabClass = "tux-tab-$tab"; |
206 | $link = Html::element( 'a', [ 'href' => '#' ], $this->msg( $tabClass )->text() ); |
207 | $output .= Html::rawElement( 'li', [ |
208 | 'class' => 'column ' . $tabClass, |
209 | 'data-filter' => $filter, |
210 | 'data-title' => $tab, |
211 | ], $link ); |
212 | } |
213 | |
214 | // Check boxes for the "more" tab. |
215 | $container = Html::openElement( 'ul', [ 'class' => 'column tux-message-selector' ] ); |
216 | $container .= Html::rawElement( 'li', |
217 | [ 'class' => 'column' ], |
218 | Html::element( 'input', [ |
219 | 'type' => 'checkbox', 'name' => 'optional', 'value' => '1', |
220 | 'checked' => false, |
221 | 'id' => 'tux-option-optional', |
222 | 'data-filter' => 'optional' |
223 | ] ) . "\u{00A0}" . Html::label( |
224 | $this->msg( 'tux-message-filter-optional-messages-label' )->text(), |
225 | 'tux-option-optional' |
226 | ) |
227 | ); |
228 | |
229 | $container .= Html::closeElement( 'ul' ); |
230 | $output .= Html::openElement( 'li', [ 'class' => 'column more' ] ) . |
231 | $this->msg( 'ellipsis' )->escaped() . |
232 | $container . |
233 | Html::closeElement( 'li' ); |
234 | |
235 | $output .= Html::closeElement( 'ul' ); |
236 | $output .= Html::closeElement( 'div' ); // close nine columns |
237 | $output .= Html::openElement( 'div', [ 'class' => 'three columns' ] ); |
238 | $output .= Html::rawElement( |
239 | 'div', |
240 | [ 'class' => 'tux-message-filter-wrapper' ], |
241 | Html::element( 'input', [ |
242 | 'class' => 'tux-message-filter-box', |
243 | 'type' => 'search', |
244 | 'placeholder' => $this->msg( 'tux-message-filter-placeholder' )->text() |
245 | ] ) |
246 | ); |
247 | |
248 | // close three columns and the row |
249 | $output .= Html::closeElement( 'div' ) . Html::closeElement( 'div' ); |
250 | |
251 | return $output; |
252 | } |
253 | |
254 | private function tuxGroupSelector(): string { |
255 | $groupClass = [ 'grouptitle', 'grouplink' ]; |
256 | if ( $this->group instanceof AggregateMessageGroup ) { |
257 | $groupClass[] = 'tux-breadcrumb__item--aggregate'; |
258 | } |
259 | |
260 | // @todo FIXME The selector should have expanded parent-child lists |
261 | return Html::openElement( 'div', [ |
262 | 'class' => 'eight columns tux-breadcrumb', |
263 | 'data-language' => $this->options['language'], |
264 | ] ) . |
265 | Html::element( 'span', |
266 | [ 'class' => 'grouptitle' ], |
267 | $this->msg( 'translate-msggroupselector-projects' )->text() |
268 | ) . |
269 | Html::element( 'span', |
270 | [ 'class' => 'grouptitle grouplink tux-breadcrumb__item--aggregate' ], |
271 | $this->msg( 'translate-msggroupselector-search-all' )->text() |
272 | ) . |
273 | Html::element( 'span', |
274 | [ |
275 | 'class' => $groupClass, |
276 | 'data-msggroupid' => $this->group->getId(), |
277 | ], |
278 | $this->group->getLabel( $this->getContext() ) |
279 | ) . |
280 | Html::closeElement( 'div' ); |
281 | } |
282 | |
283 | private function tuxLanguageSelector(): string { |
284 | global $wgTranslateDocumentationLanguageCode; |
285 | |
286 | if ( $this->options['language'] === $wgTranslateDocumentationLanguageCode ) { |
287 | $targetLangName = $this->msg( 'translate-documentation-language' )->text(); |
288 | $targetLanguage = $this->contentLanguage; |
289 | } else { |
290 | $targetLangName = $this->languageNameUtils->getLanguageName( $this->options['language'] ); |
291 | $targetLanguage = $this->languageFactory->getLanguage( $this->options['language'] ); |
292 | } |
293 | |
294 | $label = Html::element( 'span', [], $this->msg( 'tux-languageselector' )->text() ); |
295 | |
296 | $languageIcon = Html::element( |
297 | 'span', |
298 | [ 'class' => 'ext-translate-language-icon' ] |
299 | ); |
300 | |
301 | $targetLanguageName = Html::element( |
302 | 'span', |
303 | [ |
304 | 'class' => 'ext-translate-target-language', |
305 | 'dir' => $targetLanguage->getDir(), |
306 | 'lang' => $targetLanguage->getHtmlCode() |
307 | ], |
308 | $targetLangName |
309 | ); |
310 | |
311 | $expandIcon = Html::element( |
312 | 'span', |
313 | [ 'class' => 'ext-translate-language-selector-expand' ] |
314 | ); |
315 | |
316 | $value = Html::rawElement( |
317 | 'span', |
318 | [ |
319 | 'class' => 'uls mw-ui-button', |
320 | 'tabindex' => 0, |
321 | 'title' => $this->msg( 'tux-select-target-language' )->text() |
322 | ], |
323 | $languageIcon . $targetLanguageName . $expandIcon |
324 | ); |
325 | |
326 | return Html::rawElement( |
327 | 'div', |
328 | [ 'class' => 'four columns ext-translate-language-selector' ], |
329 | "$label $value" |
330 | ); |
331 | } |
332 | |
333 | private function tuxGroupSubscription(): string { |
334 | return Html::rawElement( |
335 | 'div', |
336 | [ 'class' => 'twelve columns tux-watch-group' ] |
337 | ); |
338 | } |
339 | |
340 | private function tuxGroupDescription(): string { |
341 | // Initialize an empty warning box to be filled client-side. |
342 | return Html::rawElement( |
343 | 'div', |
344 | [ 'class' => 'twelve columns description' ], |
345 | $this->getGroupDescription( $this->group ) |
346 | ); |
347 | } |
348 | |
349 | private function getGroupDescription( MessageGroup $group ): string { |
350 | $description = $group->getDescription( $this->getContext() ); |
351 | return $description === null ? |
352 | '' : $this->getOutput()->parseAsInterface( $description ); |
353 | } |
354 | |
355 | private function tuxGroupWarning(): string { |
356 | if ( $this->options['group'] === '' ) { |
357 | return Html::warningBox( |
358 | $this->msg( 'tux-translate-page-no-such-group' )->parse(), |
359 | 'tux-group-warning twelve column' |
360 | ); |
361 | } |
362 | |
363 | return ''; |
364 | } |
365 | |
366 | private function tuxWorkflowSelector(): string { |
367 | return Html::element( 'div', [ 'class' => 'tux-workflow twelve columns' ] ); |
368 | } |
369 | |
370 | /** |
371 | * Adds the task-based tabs on Special:Translate and few other special pages. |
372 | * Hook: SkinTemplateNavigation::Universal |
373 | */ |
374 | public static function tabify( Skin $skin, array &$tabs ): bool { |
375 | $title = $skin->getTitle(); |
376 | if ( !$title->isSpecialPage() ) { |
377 | return true; |
378 | } |
379 | [ $alias, $sub ] = MediaWikiServices::getInstance() |
380 | ->getSpecialPageFactory()->resolveAlias( $title->getText() ); |
381 | |
382 | $pagesInGroup = [ 'Translate', 'LanguageStats', 'MessageGroupStats', 'ExportTranslations' ]; |
383 | if ( !in_array( $alias, $pagesInGroup, true ) ) { |
384 | return true; |
385 | } |
386 | |
387 | // Extract subpage syntax, otherwise the values are not passed forward |
388 | $params = []; |
389 | if ( $sub !== null && trim( $sub ) !== '' ) { |
390 | if ( $alias === 'Translate' || $alias === 'MessageGroupStats' ) { |
391 | $params['group'] = $sub; |
392 | } elseif ( $alias === 'LanguageStats' ) { |
393 | // Breaks if additional parameters besides language are code provided |
394 | $params['language'] = $sub; |
395 | } |
396 | } |
397 | |
398 | $request = $skin->getRequest(); |
399 | // However, query string params take precedence |
400 | $params['language'] = $request->getRawVal( 'language' ) ?? ''; |
401 | $params['group'] = $request->getRawVal( 'group' ) ?? ''; |
402 | |
403 | // Remove empty values from params |
404 | $params = array_filter( $params, static function ( string $param ) { |
405 | return $param !== ''; |
406 | } ); |
407 | |
408 | $translate = SpecialPage::getTitleFor( 'Translate' ); |
409 | $languageStatistics = SpecialPage::getTitleFor( 'LanguageStats' ); |
410 | $messageGroupStatistics = SpecialPage::getTitleFor( 'MessageGroupStats' ); |
411 | |
412 | // Clear the special page tab that might be there already |
413 | $tabs['namespaces'] = []; |
414 | |
415 | $tabs['namespaces']['translate'] = [ |
416 | 'text' => wfMessage( 'translate-taction-translate' )->text(), |
417 | 'href' => $translate->getLocalURL( $params ), |
418 | 'class' => 'tux-tab', |
419 | ]; |
420 | |
421 | if ( $alias === 'Translate' ) { |
422 | $tabs['namespaces']['translate']['class'] .= ' selected'; |
423 | } |
424 | |
425 | $tabs['views']['lstats'] = [ |
426 | 'text' => wfMessage( 'translate-taction-lstats' )->text(), |
427 | 'href' => $languageStatistics->getLocalURL( $params ), |
428 | 'class' => 'tux-tab', |
429 | ]; |
430 | if ( $alias === 'LanguageStats' ) { |
431 | $tabs['views']['lstats']['class'] .= ' selected'; |
432 | } |
433 | |
434 | $tabs['views']['mstats'] = [ |
435 | 'text' => wfMessage( 'translate-taction-mstats' )->text(), |
436 | 'href' => $messageGroupStatistics->getLocalURL( $params ), |
437 | 'class' => 'tux-tab', |
438 | ]; |
439 | |
440 | if ( $alias === 'MessageGroupStats' ) { |
441 | $tabs['views']['mstats']['class'] .= ' selected'; |
442 | } |
443 | |
444 | $tabs['views']['export'] = [ |
445 | 'text' => wfMessage( 'translate-taction-export' )->text(), |
446 | 'href' => SpecialPage::getTitleFor( 'ExportTranslations' )->getLocalURL( $params ), |
447 | 'class' => 'tux-tab', |
448 | ]; |
449 | |
450 | return true; |
451 | } |
452 | } |