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