Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
PageTranslationSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use DifferenceEngine;
7use ErrorPageError;
8use InvalidArgumentException;
9use JobQueueGroup;
10use ManualLogEntry;
11use MediaWiki\Cache\LinkBatchFactory;
12use MediaWiki\Content\ContentHandler;
21use MediaWiki\Extension\TranslationNotifications\SpecialNotifyTranslators;
22use MediaWiki\Html\Html;
23use MediaWiki\Language\FormatterFactory;
24use MediaWiki\Languages\LanguageFactory;
25use MediaWiki\Page\PageRecord;
26use MediaWiki\Permissions\PermissionManager;
27use MediaWiki\Request\WebRequest;
28use MediaWiki\Revision\MutableRevisionRecord;
29use MediaWiki\Revision\SlotRecord;
30use MediaWiki\SpecialPage\SpecialPage;
31use MediaWiki\Status\StatusFormatter;
32use MediaWiki\Title\Title;
33use MediaWiki\User\User;
34use MediaWiki\Widget\ToggleSwitchWidget;
35use MediaWiki\Xml\Xml;
36use OOUI\ButtonInputWidget;
37use OOUI\CheckboxInputWidget;
38use OOUI\DropdownInputWidget;
39use OOUI\FieldLayout;
40use OOUI\FieldsetLayout;
41use OOUI\HtmlSnippet;
42use OOUI\RadioInputWidget;
43use OOUI\TextInputWidget;
44use PermissionsError;
45use UnexpectedValueException;
46use UserBlockedError;
47use Wikimedia\Rdbms\IDBAccessObject;
48use Wikimedia\Rdbms\IResultWrapper;
49use function count;
50use function wfEscapeWikiText;
51
63class PageTranslationSpecialPage extends SpecialPage {
64 private const DISPLAY_STATUS_MAPPING = [
65 TranslatablePageStatus::PROPOSED => 'proposed',
66 TranslatablePageStatus::ACTIVE => 'active',
67 TranslatablePageStatus::OUTDATED => 'outdated',
68 TranslatablePageStatus::BROKEN => 'broken'
69 ];
70 private LanguageFactory $languageFactory;
71 private LinkBatchFactory $linkBatchFactory;
72 private JobQueueGroup $jobQueueGroup;
73 private PermissionManager $permissionManager;
74 private TranslatablePageMarker $translatablePageMarker;
75 private TranslatablePageParser $translatablePageParser;
76 private MessageGroupMetadata $messageGroupMetadata;
77 private TranslatablePageView $translatablePageView;
78 private TranslatablePageStateStore $translatablePageStateStore;
79 private StatusFormatter $statusFormatter;
80
81 public function __construct(
82 LanguageFactory $languageFactory,
83 LinkBatchFactory $linkBatchFactory,
84 JobQueueGroup $jobQueueGroup,
85 PermissionManager $permissionManager,
86 TranslatablePageMarker $translatablePageMarker,
87 TranslatablePageParser $translatablePageParser,
88 MessageGroupMetadata $messageGroupMetadata,
89 TranslatablePageView $translatablePageView,
90 TranslatablePageStateStore $translatablePageStateStore,
91 FormatterFactory $formatterFactory
92 ) {
93 parent::__construct( 'PageTranslation' );
94 $this->languageFactory = $languageFactory;
95 $this->linkBatchFactory = $linkBatchFactory;
96 $this->jobQueueGroup = $jobQueueGroup;
97 $this->permissionManager = $permissionManager;
98 $this->translatablePageMarker = $translatablePageMarker;
99 $this->translatablePageParser = $translatablePageParser;
100 $this->messageGroupMetadata = $messageGroupMetadata;
101 $this->translatablePageView = $translatablePageView;
102 $this->translatablePageStateStore = $translatablePageStateStore;
103 $this->statusFormatter = $formatterFactory->getStatusFormatter( $this );
104 }
105
107 public function doesWrites(): bool {
108 return true;
109 }
110
111 protected function getGroupName(): string {
112 return 'translation';
113 }
114
116 public function execute( $parameters ) {
117 $this->setHeaders();
118
119 $user = $this->getUser();
120 $request = $this->getRequest();
121
122 $target = $request->getText( 'target', $parameters ?? '' );
123 $revision = $request->getIntOrNull( 'revision' );
124 $action = $request->getVal( 'do' );
125 $out = $this->getOutput();
126 $out->addModules( 'ext.translate.special.pagetranslation' );
127 $out->addModuleStyles( [
128 'ext.translate.specialpages.styles',
129 'mediawiki.codex.messagebox.styles',
130 ] );
131 $out->addHelpLink( 'Help:Extension:Translate/Page_translation_example' );
132 $out->enableOOUI();
133
134 if ( $target === '' ) {
135 $this->listPages();
136
137 return;
138 }
139
140 $title = Title::newFromText( $target );
141 if ( !$title ) {
142 $out->wrapWikiMsg( Html::errorBox( '$1' ), [ 'tpt-badtitle', $target ] );
143 $out->addWikiMsg( 'tpt-list-pages-in-translations' );
144
145 return;
146 }
147
148 $this->getSkin()->setRelevantTitle( $title );
149
150 if ( !$title->exists() ) {
151 $out->wrapWikiMsg(
152 Html::errorBox( '$1' ),
153 [ 'tpt-nosuchpage', $title->getPrefixedText() ]
154 );
155 $out->addWikiMsg( 'tpt-list-pages-in-translations' );
156
157 return;
158 }
159
160 if ( $action === 'settings' && !$this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
161 $this->showTranslationStateRestricted();
162 return;
163 }
164
165 $block = $this->getBlock( $request, $user, $title );
166 if ( $action === 'settings' && !$request->wasPosted() ) {
167 $this->showTranslationSettings( $title, $block );
168 return;
169 }
170
171 if ( $block ) {
172 throw $block;
173 }
174
175 // Check token for all POST actions here
176 $csrfTokenSet = $this->getContext()->getCsrfTokenSet();
177 if ( $request->wasPosted() && !$csrfTokenSet->matchTokenField( 'token' ) ) {
178 throw new PermissionsError( 'pagetranslation' );
179 }
180
181 if ( $action === 'settings' && $request->wasPosted() ) {
182 $this->handleTranslationState( $title, $request->getRawVal( 'translatable-page-state' ) ?? '' );
183 return;
184 }
185
186 // Anything other than listing the pages or manipulating settings needs permissions
187 if ( !$user->isAllowed( 'pagetranslation' ) ) {
188 throw new PermissionsError( 'pagetranslation' );
189 }
190
191 if ( $action === 'mark' ) {
192 // Has separate form
193 $this->onActionMark( $title, $revision );
194
195 return;
196 }
197
198 // On GET requests, show form which has token
199 if ( !$request->wasPosted() ) {
200 if ( $action === 'unlink' ) {
201 $this->showUnlinkConfirmation( $title );
202 } else {
203 $params = [
204 'do' => $action,
205 'target' => $title->getPrefixedText(),
206 'revision' => $revision,
207 ];
208 $this->showGenericConfirmation( $params );
209 }
210
211 return;
212 }
213
214 if ( $action === 'discourage' || $action === 'encourage' ) {
215 $id = TranslatablePage::getMessageGroupIdFromTitle( $title );
216 $current = MessageGroups::getPriority( $id );
217
218 if ( $action === 'encourage' ) {
219 $new = '';
220 } else {
221 $new = 'discouraged';
222 }
223
224 if ( $new !== $current ) {
225 MessageGroups::setPriority( $id, $new );
226 $entry = new ManualLogEntry( 'pagetranslation', $action );
227 $entry->setPerformer( $user );
228 $entry->setTarget( $title );
229 $logId = $entry->insert();
230 $entry->publish( $logId );
231 }
232
233 // Defer stats purging of parent aggregate groups. Shared groups can contain other
234 // groups as well, which we do not need to update. We could filter non-aggregate
235 // groups out, or use MessageGroups::getParentGroups, though it has an inconvenient
236 // return value format for this use case.
237 $group = MessageGroups::getGroup( $id );
238 if ( $group ) {
239 $sharedGroupIds = MessageGroups::getSharedGroups( $group );
240 if ( $sharedGroupIds !== [] ) {
241 $job = RebuildMessageGroupStatsJob::newRefreshGroupsJob( $sharedGroupIds );
242 $this->jobQueueGroup->push( $job );
243 }
244 }
245
246 // Show updated page with a notice
247 $this->listPages();
248
249 return;
250 }
251
252 if ( $action === 'unlink' || $action === 'unmark' ) {
253 try {
254 $this->translatablePageMarker->unmarkPage(
255 TranslatablePage::newFromTitle( $title ),
256 $user,
257 $this,
258 $action === 'unlink'
259 );
260
261 $out->wrapWikiMsg(
262 Html::successBox( '$1' ),
263 [ 'tpt-unmarked', $title->getPrefixedText() ]
264 );
265 } catch ( TranslatablePageMarkException $e ) {
266 $out->wrapWikiMsg(
267 Html::errorBox( '$1' ),
268 $e->getMessageObject()
269 );
270 }
271
272 $out->addWikiMsg( 'tpt-list-pages-in-translations' );
273 }
274 }
275
276 protected function onActionMark( Title $title, ?int $revision ): void {
277 $request = $this->getRequest();
278 $out = $this->getOutput();
279 $translateTitle = $request->getCheck( 'translatetitle' );
280
281 try {
282 $operation = $this->translatablePageMarker->getMarkOperation(
283 $title->toPageRecord(
284 $request->wasPosted() ? IDBAccessObject::READ_LATEST : IDBAccessObject::READ_NORMAL
285 ),
286 $revision,
287 // If the request was not posted, validate all the units so that initially we display all the errors
288 // and then the user can choose whether they want to translate the title
289 !$request->wasPosted() || $translateTitle
290 );
291 } catch ( TranslatablePageMarkException $e ) {
292 $out->addHTML( Html::errorBox( $this->msg( $e->getMessageObject() )->parse() ) );
293 $out->addWikiMsg( 'tpt-list-pages-in-translations' );
294 return;
295 }
296
297 $unitNameValidationResult = $operation->getUnitValidationStatus();
298 // Non-fatal error which prevents saving
299 if ( $unitNameValidationResult->isOK() && $request->wasPosted() ) {
300 // Fetch priority language related information
301 [ $priorityLanguages, $forcePriorityLanguage, $priorityLanguageReason ] =
302 $this->getPriorityLanguage( $this->getRequest() );
303
304 $unitFuzzySelector = $request->getRawVal( 'unit-fuzzy-selector' );
305 if ( $unitFuzzySelector === 'all' ) {
306 $noFuzzyUnits = [];
307 } else {
308 // Get IDs of all changed units
309 $allChangedUnits = array_map(
310 static fn ( $unit ) => $unit->id,
311 array_filter(
312 $operation->getUnits(),
313 static fn ( $unit ) => $unit->type === 'changed'
314 )
315 );
316
317 if ( $unitFuzzySelector === 'none' ) {
318 $noFuzzyUnits = $allChangedUnits;
319 } else { // custom
320 $fuzzyUnits = $request->getArray( 'tpt-sect-fuzzy' ) ?? [];
321 // Filter the units that should not be fuzzied
322 $noFuzzyUnits = array_filter(
323 $allChangedUnits,
324 static fn ( $value ) => !in_array( $value, $fuzzyUnits )
325 );
326 }
327 }
328
329 $translatablePageSettings = new TranslatablePageSettings(
330 $priorityLanguages,
331 $forcePriorityLanguage,
332 $priorityLanguageReason,
333 $noFuzzyUnits,
334 $translateTitle,
335 $request->getCheck( 'use-latest-syntax' ),
336 $request->getCheck( 'transclusion' )
337 );
338
339 try {
340 $unitCount = $this->translatablePageMarker->markForTranslation(
341 $operation,
342 $translatablePageSettings,
343 $this,
344 $this->getUser()
345 );
346 $this->showSuccess( $operation->getPage(), $operation->isFirstMark(), $unitCount );
347 } catch ( TranslatablePageMarkException $e ) {
348 $out->wrapWikiMsg(
349 Html::errorBox( '$1' ),
350 $e->getMessageObject()
351 );
352 }
353 } else {
354 if ( !$unitNameValidationResult->isOK() ) {
355 $out->addHTML(
356 Html::errorBox( $this->statusFormatter->getHTML( $unitNameValidationResult ) )
357 );
358 }
359
360 $this->showPage( $operation );
361 }
362 }
363
371 private function showSuccess( TranslatablePage $page, bool $firstMark, int $unitCount ): void {
372 $titleText = $page->getTitle()->getPrefixedText();
373 $num = $this->getLanguage()->formatNum( $unitCount );
374 $link = SpecialPage::getTitleFor( 'Translate' )->getFullURL( [
375 'group' => $page->getMessageGroupId(),
376 'action' => 'page',
377 'filter' => '',
378 ] );
379
380 $this->getOutput()->wrapWikiMsg(
381 Html::successBox( '$1' ),
382 [ 'tpt-saveok', $titleText, $num, $link ]
383 );
384
385 // If the page is being marked for translation for the first time
386 // add a link to Special:PageMigration.
387 if ( $firstMark ) {
388 $this->getOutput()->addWikiMsg( 'tpt-saveok-first' );
389 }
390
391 // If TranslationNotifications is installed, and the user can notify
392 // translators, add a convenience link.
393 if ( method_exists( SpecialNotifyTranslators::class, 'execute' ) &&
394 $this->getUser()->isAllowed( SpecialNotifyTranslators::$right )
395 ) {
396 $link = SpecialPage::getTitleFor( 'NotifyTranslators' )->getFullURL(
397 [ 'tpage' => $page->getTitle()->getArticleID() ]
398 );
399 $this->getOutput()->addWikiMsg( 'tpt-offer-notify', $link );
400 }
401
402 $this->getOutput()->addWikiMsg( 'tpt-list-pages-in-translations' );
403 }
404
405 private function showGenericConfirmation( array $params ): void {
406 $formParams = [
407 'method' => 'post',
408 'action' => $this->getPageTitle()->getLocalURL(),
409 ];
410
411 $params['title'] = $this->getPageTitle()->getPrefixedText();
412 $params['token'] = $this->getContext()->getCsrfTokenSet()->getToken();
413
414 $hidden = '';
415 foreach ( $params as $key => $value ) {
416 $hidden .= Html::hidden( $key, $value );
417 }
418
419 $this->getOutput()->addHTML(
420 Html::openElement( 'form', $formParams ) .
421 $hidden .
422 $this->msg( 'tpt-generic-confirm' )->parseAsBlock() .
423 Html::submitButton(
424 $this->msg( 'tpt-generic-button' )->text(),
425 [ 'class' => 'mw-ui-button mw-ui-progressive' ]
426 ) .
427 Html::closeElement( 'form' )
428 );
429 }
430
431 private function showUnlinkConfirmation( Title $target ): void {
432 $formParams = [
433 'method' => 'post',
434 'action' => $this->getPageTitle()->getLocalURL(),
435 ];
436
437 $this->getOutput()->addHTML(
438 Html::openElement( 'form', $formParams ) .
439 Html::hidden( 'do', 'unlink' ) .
440 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
441 Html::hidden( 'target', $target->getPrefixedText() ) .
442 Html::hidden( 'token', $this->getContext()->getCsrfTokenSet()->getToken() ) .
443 $this->msg( 'tpt-unlink-confirm', $target->getPrefixedText() )->parseAsBlock() .
444 Html::submitButton(
445 $this->msg( 'tpt-unlink-button' )->text(),
446 [ 'class' => 'mw-ui-button mw-ui-destructive' ]
447 ) .
448 Html::closeElement( 'form' )
449 );
450 }
451
456 public static function loadPagesFromDB(): IResultWrapper {
457 $dbr = Utilities::getSafeReadDB();
458 return $dbr->newSelectQueryBuilder()
459 ->select( [
460 'page_id',
461 'page_namespace',
462 'page_title',
463 'page_latest',
464 'rt_revision' => 'MAX(rt_revision)',
465 'rt_type'
466 ] )
467 ->from( 'page' )
468 ->join( 'revtag', null, 'page_id=rt_page' )
469 ->where( [
470 'rt_type' => [ RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG ],
471 ] )
472 ->orderBy( [ 'page_namespace', 'page_title' ] )
473 ->groupBy( [ 'page_id', 'page_namespace', 'page_title', 'page_latest', 'rt_type' ] )
474 ->caller( __METHOD__ )
475 ->fetchResultSet();
476 }
477
482 public static function buildPageArray( IResultWrapper $res ): array {
483 $pages = [];
484 foreach ( $res as $r ) {
485 // We have multiple rows for same page, because of different tags
486 if ( !isset( $pages[$r->page_id] ) ) {
487 $pages[$r->page_id] = [];
488 $title = Title::newFromRow( $r );
489 $pages[$r->page_id]['title'] = $title;
490 $pages[$r->page_id]['latest'] = (int)$title->getLatestRevID();
491 }
492
493 $tag = $r->rt_type;
494 $pages[$r->page_id][$tag] = (int)$r->rt_revision;
495 }
496
497 return $pages;
498 }
499
506 private function classifyPages( array $pages ): array {
507 $out = [
508 // The ideal state for pages: marked and up to date
509 'active' => [],
510 'proposed' => [],
511 'outdated' => [],
512 'broken' => [],
513 ];
514
515 if ( $pages === [] ) {
516 return $out;
517 }
518
519 // Preload stuff for performance
520 $messageGroupIdsForPreload = [];
521 foreach ( $pages as $i => $page ) {
522 $id = TranslatablePage::getMessageGroupIdFromTitle( $page['title'] );
523 $messageGroupIdsForPreload[] = $id;
524 $pages[$i]['groupid'] = $id;
525 }
526 // Performance optimization: load only data we need to classify the pages
527 $metadata = $this->messageGroupMetadata->loadBasicMetadataForTranslatablePages(
528 $messageGroupIdsForPreload,
529 [ 'transclusion', 'version' ]
530 );
531
532 foreach ( $pages as $page ) {
533 $groupId = $page['groupid'];
534 $group = MessageGroups::getGroup( $groupId );
535
536 $page['discouraged'] = false;
537 if ( $group ) {
538 $page['discouraged'] = MessageGroups::getPriority( $group ) === 'discouraged';
539 }
540 $page['version'] = $metadata[$groupId]['version'] ?? TranslatablePageMarker::DEFAULT_SYNTAX_VERSION;
541 $page['transclusion'] = $metadata[$groupId]['transclusion'] ?? false;
542
543 // TODO: Eventually we should query the status directly from the TranslatableBundleStore
544 $tpStatus = TranslatablePage::determineStatus(
545 $page[RevTagStore::TP_READY_TAG] ?? null,
546 $page[RevTagStore::TP_MARK_TAG] ?? null,
547 $page['latest']
548 );
549
550 if ( !$tpStatus ) {
551 // Ignore pages for which status could not be determined.
552 continue;
553 }
554
555 $out[self::DISPLAY_STATUS_MAPPING[$tpStatus->getId()]][] = $page;
556 }
557
558 return $out;
559 }
560
561 public function listPages(): void {
562 $out = $this->getOutput();
563
564 $res = self::loadPagesFromDB();
565 $allPages = self::buildPageArray( $res );
566
567 $pagesWithProposedState = [];
568 if ( $this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
569 $pagesWithProposedState = $this->translatablePageStateStore->getRequested();
570 }
571
572 if ( !count( $allPages ) && !count( $pagesWithProposedState ) ) {
573 $out->addWikiMsg( 'tpt-list-nopages' );
574
575 return;
576 }
577
578 $lb = $this->linkBatchFactory->newLinkBatch();
579 $lb->setCaller( __METHOD__ );
580 foreach ( $allPages as $page ) {
581 $lb->addObj( $page['title'] );
582 }
583
584 foreach ( $pagesWithProposedState as $title ) {
585 $lb->addObj( $title );
586 }
587 $lb->execute();
588
589 $types = $this->classifyPages( $allPages );
590
591 $pages = $types['proposed'];
592 if ( $pages || $pagesWithProposedState ) {
593 $out->wrapWikiMsg( '== $1 ==', 'tpt-new-pages-title' );
594 if ( $pages ) {
595 $out->addWikiMsg( 'tpt-new-pages', count( $pages ) );
596 $out->addHTML( $this->getPageList( $pages, 'proposed' ) );
597 }
598
599 if ( $pagesWithProposedState ) {
600 $out->addWikiMsg( 'tpt-proposed-state-pages', count( $pagesWithProposedState ) );
601 $out->addHTML( $this->displayPagesWithProposedState( $pagesWithProposedState ) );
602 }
603 }
604
605 $pages = $types['broken'];
606 if ( $pages ) {
607 $out->wrapWikiMsg( '== $1 ==', 'tpt-other-pages-title' );
608 $out->addWikiMsg( 'tpt-other-pages', count( $pages ) );
609 $out->addHTML( $this->getPageList( $pages, 'broken' ) );
610 }
611
612 $pages = $types['outdated'];
613 if ( $pages ) {
614 $out->wrapWikiMsg( '== $1 ==', 'tpt-outdated-pages-title' );
615 $out->addWikiMsg( 'tpt-outdated-pages', count( $pages ) );
616 $out->addHTML( $this->getPageList( $pages, 'outdated' ) );
617 }
618
619 $pages = $types['active'];
620 if ( $pages ) {
621 $out->wrapWikiMsg( '== $1 ==', 'tpt-old-pages-title' );
622 $out->addWikiMsg( 'tpt-old-pages', count( $pages ) );
623 $out->addHTML( $this->getPageList( $pages, 'active' ) );
624 }
625 }
626
627 private function actionLinks( array $page, string $type ): string {
628 // Performance optimization to avoid calling $this->msg in a loop
629 static $messageCache = null;
630 if ( $messageCache === null ) {
631 $messageCache = [
632 'mark' => $this->msg( 'tpt-rev-mark' )->text(),
633 'mark-tooltip' => $this->msg( 'tpt-rev-mark-tooltip' )->text(),
634 'encourage' => $this->msg( 'tpt-rev-encourage' )->text(),
635 'encourage-tooltip' => $this->msg( 'tpt-rev-encourage-tooltip' )->text(),
636 'discourage' => $this->msg( 'tpt-rev-discourage' )->text(),
637 'discourage-tooltip' => $this->msg( 'tpt-rev-discourage-tooltip' )->text(),
638 'unmark' => $this->msg( 'tpt-rev-unmark' )->text(),
639 'unmark-tooltip' => $this->msg( 'tpt-rev-unmark-tooltip' )->text(),
640 'pipe-separator' => $this->msg( 'pipe-separator' )->escaped(),
641 ];
642 }
643
644 $actions = [];
646 $title = $page['title'];
647 $user = $this->getUser();
648
649 // Class to allow one-click POSTs
650 $js = [ 'class' => 'mw-translate-jspost' ];
651
652 if ( $user->isAllowed( 'pagetranslation' ) ) {
653 // Enable re-marking of all pages to allow changing of priority languages
654 // or migration to the new syntax version
655 if ( $type !== 'broken' ) {
656 $actions[] = $this->getLinkRenderer()->makeKnownLink(
657 $this->getPageTitle(),
658 $messageCache['mark'],
659 [ 'title' => $messageCache['mark-tooltip'] ],
660 [
661 'do' => 'mark',
662 'target' => $title->getPrefixedText(),
663 'revision' => $title->getLatestRevID(),
664 ]
665 );
666 }
667
668 if ( $type !== 'proposed' ) {
669 if ( $page['discouraged'] ) {
670 $actions[] = $this->getLinkRenderer()->makeKnownLink(
671 $this->getPageTitle(),
672 $messageCache['encourage'],
673 [ 'title' => $messageCache['encourage-tooltip'] ] + $js,
674 [
675 'do' => 'encourage',
676 'target' => $title->getPrefixedText(),
677 'revision' => -1,
678 ]
679 );
680 } else {
681 $actions[] = $this->getLinkRenderer()->makeKnownLink(
682 $this->getPageTitle(),
683 $messageCache['discourage'],
684 [ 'title' => $messageCache['discourage-tooltip'] ] + $js,
685 [
686 'do' => 'discourage',
687 'target' => $title->getPrefixedText(),
688 'revision' => -1,
689 ]
690 );
691 }
692
693 $actions[] = $this->getLinkRenderer()->makeKnownLink(
694 $this->getPageTitle(),
695 $messageCache['unmark'],
696 [ 'title' => $messageCache['unmark-tooltip'] ],
697 [
698 'do' => $type === 'broken' ? 'unmark' : 'unlink',
699 'target' => $title->getPrefixedText(),
700 'revision' => -1,
701 ]
702 );
703 }
704 }
705
706 if ( !$actions ) {
707 return '';
708 }
709
710 return '<div>' . implode( $messageCache['pipe-separator'], $actions ) . '</div>';
711 }
712
713 private function showPage( TranslatablePageMarkOperation $operation ): void {
714 $page = $operation->getPage();
715 $out = $this->getOutput();
716 $out->addWikiMsg( 'tpt-showpage-intro' );
717
718 $this->addPageForm(
719 $page->getTitle(),
720 'mw-tpt-sp-markform',
721 'mark',
722 $page->getRevision()
723 );
724
725 $out->wrapWikiMsg( '==$1==', 'tpt-sections-oldnew' );
726
727 $diffOld = $this->msg( 'tpt-diff-old' )->escaped();
728 $diffNew = $this->msg( 'tpt-diff-new' )->escaped();
729 $hasChanges = false;
730
731 // Check whether page title was previously marked for translation.
732 // If the page is marked for translation the first time, default to checked,
733 // unless the page is a template. T305240
734 $defaultChecked = (
735 $operation->isFirstMark() &&
736 !$page->getTitle()->inNamespace( NS_TEMPLATE )
737 ) || $page->hasPageDisplayTitle();
738
739 $sourceLanguage = $this->languageFactory->getLanguage( $page->getSourceLanguageCode() );
740
741 $hideUnchangedUnitToggle = '';
742 // Toggle for unchanged translation units
743 if ( array_filter(
744 $operation->getUnits(),
745 static fn ( $unit ) => $unit->type === 'old' && $unit->id !== TranslatablePage::DISPLAY_TITLE_UNIT_ID
746 ) ) {
747 $hideUnchangedUnitToggle = ( new FieldLayout(
748 new ToggleSwitchWidget( [
749 'name' => 'unchanged-translation-units',
750 'selected' => false
751 ] ),
752 [
753 'label' => $this->msg( 'tpt-translate-hide-unchanged-units' )->text(),
754 'align' => 'left',
755 ]
756 ) )->toString();
757 }
758
759 // Check if there are changed units
760 $requireUpdatesDropdown = '';
761 if ( array_filter(
762 $operation->getUnits(),
763 static fn ( $unit ) => $unit->type === 'changed'
764 ) ) {
765 $requireUpdatesDropdown = ( new FieldLayout(
766 new DropdownInputWidget( [
767 'name' => 'unit-fuzzy-selector',
768 'options' => [
769 [
770 'data' => 'all',
771 'label' => $this->msg( 'tpt-fuzzy-select-all' )->text()
772 ],
773 [
774 'data' => 'none',
775 'label' => $this->msg( 'tpt-fuzzy-select-none' )->text()
776 ],
777 [
778 'data' => 'custom',
779 'label' => $this->msg( 'tpt-fuzzy-select-custom' )->text()
780 ]
781 ],
782 'value' => 'custom'
783 ] ),
784 [
785 'label' => $this->msg( 'tpt-fuzzy-select-label' )->text(),
786 'align' => 'left',
787 ]
788 ) )->toString();
789 }
790
791 // General area
792 if ( $hideUnchangedUnitToggle !== '' || $requireUpdatesDropdown !== '' ) {
793 $out->addHTML( MessageWebImporter::makeSectionElement(
794 $this->msg( 'tpt-general-area-header' )->text(),
795 'general',
796 $hideUnchangedUnitToggle . $requireUpdatesDropdown
797 ) );
798
799 $out->addHTML( '<hr>' );
800 }
801
802 foreach ( $operation->getUnits() as $s ) {
803 if ( $s->id === TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
804 // Set section type as new if title previously unchecked
805 $s->type = $defaultChecked ? $s->type : 'new';
806
807 // Checkbox for page title optional translation
808 $checkBox = new FieldLayout(
809 new CheckboxInputWidget( [
810 'name' => 'translatetitle',
811 'selected' => $operation->titleTranslationState === TranslateTitleEnum::DEFAULT_CHECKED,
812 'disabled' => $operation->titleTranslationState === TranslateTitleEnum::DISABLED,
813 ] ),
814 [
815 'label' => $this->msg( 'tpt-translate-title' )->text(),
816 'align' => 'inline',
817 'classes' => [ 'mw-tpt-m-vertical' ]
818 ]
819 );
820 $out->addHTML( $checkBox->toString() );
821 }
822
823 if ( $s->type === 'new' ) {
824 $hasChanges = true;
825 $name = $this->msg( 'tpt-section-new', $s->id )->escaped();
826 } else {
827 $name = $this->msg( 'tpt-section', $s->id )->escaped();
828 }
829
830 if ( $s->type === 'changed' ) {
831 $hasChanges = true;
832 $diff = new DifferenceEngine();
833 $diff->setTextLanguage( $sourceLanguage );
834 $diff->setReducedLineNumbers();
835
836 $tpTitle = $page->getTitle();
837 $oldContent = ContentHandler::makeContent( $s->getOldText(), $tpTitle );
838 $oldRevision = new MutableRevisionRecord( $tpTitle );
839 $oldRevision->setContent( SlotRecord::MAIN, $oldContent );
840
841 $newContent = ContentHandler::makeContent( $s->getText(), $tpTitle );
842 $newRevision = new MutableRevisionRecord( $tpTitle );
843 $newRevision->setContent( SlotRecord::MAIN, $newContent );
844
845 $diff->setRevisions( $oldRevision, $newRevision );
846
847 $text = $diff->getDiff( $diffOld, $diffNew );
848 $diffOld = $diffNew = null;
849 $diff->showDiffStyle();
850
851 $checkLabel = new FieldLayout(
852 new CheckboxInputWidget( [
853 'name' => 'tpt-sect-fuzzy[]',
854 'value' => $s->id,
855 'selected' => !$s->onlyTvarsChanged()
856 ] ),
857 [
858 'label' => $this->msg( 'tpt-action-fuzzy' )->text(),
859 'align' => 'inline',
860 'classes' => [ 'mw-tpt-m-vertical', 'mw-tpt-action-field' ],
861 ]
862 );
863 $text = $checkLabel->toString() . $text;
864 } else {
865 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
866 }
867
868 # For changed text, the language is set by $diff->setTextLanguage()
869 $lang = $s->type === 'changed' ? null : $sourceLanguage;
870 $out->addHTML( MessageWebImporter::makeSectionElement(
871 $name,
872 $s->type,
873 $text,
874 $lang,
875 $s->id === TranslatablePage::DISPLAY_TITLE_UNIT_ID ?
876 [ 'mw-tpt-sp-section-type-title' ] :
877 []
878 ) );
879
880 foreach ( $s->getIssues() as $issue ) {
881 $severity = $issue->getSeverity();
882 if ( $severity === TranslationUnitIssue::WARNING ) {
883 $box = Html::warningBox( $this->msg( $issue )->escaped() );
884 } elseif ( $severity === TranslationUnitIssue::ERROR ) {
885 $box = Html::errorBox( $this->msg( $issue )->escaped() );
886 } else {
887 throw new UnexpectedValueException(
888 "Unknown severity: $severity for key: {$issue->getKey()}"
889 );
890 }
891
892 $out->addHTML( $box );
893 }
894 }
895
896 if ( $operation->getDeletedUnits() ) {
897 $hasChanges = true;
898 $out->wrapWikiMsg( '==$1==', 'tpt-sections-deleted' );
899
900 foreach ( $operation->getDeletedUnits() as $s ) {
901 $name = $this->msg( 'tpt-section-deleted', $s->id )->escaped();
902 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
903 $out->addHTML( MessageWebImporter::makeSectionElement(
904 $name,
905 'deleted',
906 $text,
907 $sourceLanguage
908 ) );
909 }
910 }
911
912 // Display template changes if applicable
913 $markedTag = $page->getMarkedTag();
914 if ( $markedTag !== null ) {
915 $hasChanges = true;
916 $newTemplate = $operation->getParserOutput()->sourcePageTemplateForDiffs();
917 $tpTitle = $page->getTitle();
918 $oldPage = TranslatablePage::newFromRevision( $tpTitle, $markedTag );
919 $oldTemplate = $this->translatablePageParser
920 ->parse( $oldPage->getText() )
921 ->sourcePageTemplateForDiffs();
922
923 if ( $oldTemplate !== $newTemplate ) {
924 $out->wrapWikiMsg( '==$1==', 'tpt-sections-template' );
925
926 $diff = new DifferenceEngine();
927 $diff->setTextLanguage( $sourceLanguage );
928
929 $oldContent = ContentHandler::makeContent( $oldTemplate, $tpTitle );
930 $oldRevision = new MutableRevisionRecord( $tpTitle );
931 $oldRevision->setContent( SlotRecord::MAIN, $oldContent );
932
933 $newContent = ContentHandler::makeContent( $newTemplate, $tpTitle );
934 $newRevision = new MutableRevisionRecord( $tpTitle );
935 $newRevision->setContent( SlotRecord::MAIN, $newContent );
936
937 $diff->setRevisions( $oldRevision, $newRevision );
938
939 $text = $diff->getDiff(
940 $this->msg( 'tpt-diff-old' )->escaped(),
941 $this->msg( 'tpt-diff-new' )->escaped()
942 );
943 $diff->showDiffStyle();
944 $diff->setReducedLineNumbers();
945
946 $out->addHTML( Xml::tags( 'div', [], $text ) );
947 }
948 }
949
950 if ( !$hasChanges ) {
951 $out->wrapWikiMsg( Html::successBox( '$1' ), 'tpt-mark-nochanges' );
952 }
953
954 $this->priorityLanguagesForm( $page );
955
956 // If an existing page does not have the supportsTransclusion flag, keep the checkbox unchecked,
957 // If the page is being marked for translation for the first time, the checkbox can be checked
958 $this->templateTransclusionForm( $page, $page->supportsTransclusion() ?? $operation->isFirstMark() );
959
960 $version = $this->messageGroupMetadata->getWithDefaultValue(
961 $page->getMessageGroupId(), 'version', TranslatablePageMarker::DEFAULT_SYNTAX_VERSION
962 );
963 $this->syntaxVersionForm( $version, $operation->isFirstMark() );
964
965 $submitButton = new FieldLayout(
966 new ButtonInputWidget( [
967 'label' => $this->msg( 'tpt-submit' )->text(),
968 'type' => 'submit',
969 'flags' => [ 'primary', 'progressive' ],
970 ] ),
971 [
972 'label' => null,
973 'align' => 'top',
974 ]
975 );
976
977 $out->addHTML( $submitButton->toString() );
978 $out->addHTML( '</form>' );
979 }
980
981 private function priorityLanguagesForm( TranslatablePage $page ): void {
982 $groupId = $page->getMessageGroupId();
983 $interfaceLanguage = $this->getLanguage()->getCode();
984 $storedLanguages = (string)$this->messageGroupMetadata->get( $groupId, 'prioritylangs' );
985 $default = $storedLanguages !== '' ? explode( ',', $storedLanguages ) : [];
986
987 $priorityReason = $this->messageGroupMetadata->get( $groupId, 'priorityreason' );
988 $priorityReason = $priorityReason !== false ? $priorityReason : '';
989
990 $form = new FieldsetLayout( [
991 'items' => [
992 new FieldLayout(
993 new LanguagesMultiselectWidget( [
994 'infusable' => true,
995 'name' => 'prioritylangs',
996 'id' => 'mw-translate-SpecialPageTranslation-prioritylangs',
997 'languages' => Utilities::getLanguageNames( $interfaceLanguage ),
998 'default' => $default,
999 ] ),
1000 [
1001 'label' => $this->msg( 'tpt-select-prioritylangs' )->text(),
1002 'align' => 'top',
1003 'help' => new HtmlSnippet( Html::element(
1004 'span',
1005 [ 'class' => 'tux-nojs' ],
1006 $this->msg( 'tpt-select-prioritylangs-help' )->text()
1007 ) ),
1008 'helpInline' => true,
1009 ]
1010 ),
1011 new FieldLayout(
1012 new CheckboxInputWidget( [
1013 'name' => 'forcelimit',
1014 'selected' => $this->messageGroupMetadata->get( $groupId, 'priorityforce' ) === 'on',
1015 ] ),
1016 [
1017 'label' => $this->msg( 'tpt-select-prioritylangs-force' )->text(),
1018 'align' => 'inline',
1019 'help' => $this->msg( 'tpt-select-no-prioritylangs-force' )->text(),
1020 'helpInline' => true,
1021 ]
1022 ),
1023 new FieldLayout(
1024 new TextInputWidget( [
1025 'name' => 'priorityreason',
1026 'value' => $priorityReason
1027 ] ),
1028 [
1029 'label' => $this->msg( 'tpt-select-prioritylangs-reason' )->text(),
1030 'align' => 'top',
1031 ]
1032 ),
1033
1034 ],
1035 ] );
1036
1037 $this->getOutput()->wrapWikiMsg( '==$1==', 'tpt-sections-prioritylangs' );
1038 $this->getOutput()->addHTML( $form->toString() );
1039 }
1040
1041 private function syntaxVersionForm( string $version, bool $firstMark ): void {
1042 $out = $this->getOutput();
1043
1044 if ( $version === TranslatablePageMarker::LATEST_SYNTAX_VERSION || $firstMark ) {
1045 return;
1046 }
1047
1048 $out->wrapWikiMsg( '==$1==', 'tpt-sections-syntaxversion' );
1049 $out->addWikiMsg(
1050 'tpt-syntaxversion-text',
1051 '<code>' . wfEscapeWikiText( '<span lang="en" dir="ltr">...</span>' ) . '</code>',
1052 '<code>' . wfEscapeWikiText( '<translate nowrap>...</translate>' ) . '</code>'
1053 );
1054
1055 $checkBox = new FieldLayout(
1056 new CheckboxInputWidget( [
1057 'name' => 'use-latest-syntax'
1058 ] ),
1059 [
1060 'label' => $out->msg( 'tpt-syntaxversion-label' )->text(),
1061 'align' => 'inline',
1062 ]
1063 );
1064
1065 $out->addHTML( $checkBox->toString() );
1066 }
1067
1068 private function templateTransclusionForm( TranslatablePage $page, bool $supportsTransclusion ): void {
1069 $out = $this->getOutput();
1070 $out->wrapWikiMsg( '==$1==', 'tpt-transclusion' );
1071
1072 $checkBox = new FieldLayout(
1073 new CheckboxInputWidget( [
1074 'name' => 'transclusion',
1075 'selected' => $supportsTransclusion
1076 ] ),
1077 [
1078 'label' => $out->msg( 'tpt-transclusion-label' )->text(),
1079 'align' => 'inline',
1080 'help' => $out->msg( 'tpt-transclusion-help' )
1081 ->params( $page->getTitle()->getSubpage( 'de' )->getPrefixedText() )
1082 ->text(),
1083 'helpInline' => true,
1084 ]
1085 );
1086
1087 $out->addHTML( $checkBox->toString() );
1088 }
1089
1090 private function getPriorityLanguage( WebRequest $request ): array {
1091 // Get the priority languages from the request
1092 // We've to do some extra work here because if JS is disabled, we will be getting
1093 // the values split by newline.
1094 $priorityLanguages = rtrim( trim( $request->getVal( 'prioritylangs', '' ) ), ',' );
1095 $priorityLanguages = str_replace( "\n", ',', $priorityLanguages );
1096 $priorityLanguages = array_map( 'trim', explode( ',', $priorityLanguages ) );
1097 $priorityLanguages = array_unique( array_filter( $priorityLanguages ) );
1098
1099 $forcePriorityLanguage = $request->getCheck( 'forcelimit' );
1100 $priorityLanguageReason = trim( $request->getText( 'priorityreason' ) );
1101
1102 return [ $priorityLanguages, $forcePriorityLanguage, $priorityLanguageReason ];
1103 }
1104
1105 private function getPageList( array $pages, string $type ): string {
1106 $items = [];
1107 $tagsTextCache = [];
1108
1109 $tagDiscouraged = $this->msg( 'tpt-tag-discouraged' )->escaped();
1110 $tagOldSyntax = $this->msg( 'tpt-tag-oldsyntax' )->escaped();
1111 $tagNoTransclusionSupport = $this->msg( 'tpt-tag-no-transclusion-support' )->escaped();
1112
1113 foreach ( $pages as $page ) {
1114 $link = $this->getLinkRenderer()->makeKnownLink( $page['title'] );
1115 $acts = $this->actionLinks( $page, $type );
1116 $tags = [];
1117 if ( $page['discouraged'] ) {
1118 $tags[] = $tagDiscouraged;
1119 }
1120 if ( $type !== 'proposed' ) {
1121 if ( $page['version'] !== TranslatablePageMarker::LATEST_SYNTAX_VERSION ) {
1122 $tags[] = $tagOldSyntax;
1123 }
1124
1125 if ( $page['transclusion'] !== '1' ) {
1126 $tags[] = $tagNoTransclusionSupport;
1127 }
1128 }
1129
1130 $tagList = '';
1131 if ( $tags ) {
1132 // Performance optimization to avoid calling $this->msg in a loop
1133 $tagsKey = implode( '', $tags );
1134 $tagsTextCache[$tagsKey] ??= $this->msg( 'parentheses' )
1135 ->rawParams( $this->getLanguage()->pipeList( $tags ) )
1136 ->escaped();
1137
1138 $tagList = Html::rawElement(
1139 'span',
1140 [ 'class' => 'mw-tpt-actions' ],
1141 $tagsTextCache[$tagsKey]
1142 );
1143 }
1144
1145 $items[] = "<li class='mw-tpt-pagelist-item'>$link $tagList $acts</li>";
1146 }
1147
1148 return '<ol>' . implode( '', $items ) . '</ol>';
1149 }
1150
1152 private function displayPagesWithProposedState( array $pagesWithProposedState ): string {
1153 $items = [];
1154 $preparePageAction = $this->msg( 'tpt-prepare-page' )->text();
1155 $preparePageTooltip = $this->msg( 'tpt-prepare-page-tooltip' )->text();
1156 $linkRenderer = $this->getLinkRenderer();
1157 foreach ( $pagesWithProposedState as $pageRecord ) {
1158 $link = $linkRenderer->makeKnownLink( $pageRecord );
1159 $action = $linkRenderer->makeKnownLink(
1160 SpecialPage::getTitleFor( 'PagePreparation' ),
1161 $preparePageAction,
1162 [ 'title' => $preparePageTooltip ],
1163 [ 'page' => ( Title::newFromPageReference( $pageRecord ) )->getPrefixedText() ]
1164 );
1165 $items[] = "<li class='mw-tpt-pagelist-item'>$link <div>$action</div></li>";
1166 }
1167 return '<ol>' . implode( '', $items ) . '</ol>';
1168 }
1169
1170 private function showTranslationSettings( Title $target, ?ErrorPageError $block ): void {
1171 $out = $this->getOutput();
1172 $out->setPageTitle( $this->msg( 'tpt-translation-settings-page-title' )->text() );
1173
1174 $currentState = $this->translatablePageStateStore->get( $target );
1175
1176 if ( !$this->translatablePageView->canManageTranslationSettings( $target, $this->getUser() ) ) {
1177 $out->wrapWikiMsg( Html::errorBox( '$1' ), 'tpt-translation-settings-restricted' );
1178 $out->addWikiMsg( 'tpt-list-pages-in-translations' );
1179 return;
1180 }
1181
1182 if ( $block ) {
1183 $out->wrapWikiMsg( Html::errorBox( '$1' ), $block->getMessageObject() );
1184 }
1185
1186 if ( $currentState ) {
1187 $this->displayStateInfoMessage( $target, $currentState );
1188 }
1189
1190 $this->addPageForm( $target, 'mw-tpt-sp-settings', 'settings', null );
1191 $out->addHTML(
1192 Html::rawElement(
1193 'p',
1194 [ 'class' => 'mw-tpt-vm' ],
1195 Html::element( 'strong', [], $this->msg( 'tpt-translation-settings-subtitle' )->text() )
1196 )
1197 );
1198
1199 $currentStateId = $currentState ? $currentState->getStateId() : null;
1200 $options = new FieldsetLayout( [
1201 'items' => [
1202 new FieldLayout(
1203 new RadioInputWidget( [
1204 'name' => 'translatable-page-state',
1205 'value' => 'ignored',
1206 'selected' => $currentStateId === TranslatableBundleState::IGNORE
1207 ] ),
1208 [
1209 'label' => $this->msg( 'tpt-translation-settings-ignore' )->text(),
1210 'align' => 'inline',
1211 'help' => $this->msg( 'tpt-translation-settings-ignore-hint' )->text(),
1212 'helpInline' => true,
1213 ]
1214 ),
1215 new FieldLayout(
1216 new RadioInputWidget( [
1217 'name' => 'translatable-page-state',
1218 'value' => 'unstable',
1219 'selected' => $currentStateId === null
1220 ] ),
1221 [
1222 'label' => $this->msg( 'tpt-translation-settings-unstable' )->text(),
1223 'align' => 'inline',
1224 'help' => $this->msg( 'tpt-translation-settings-unstable-hint' )->text(),
1225 'helpInline' => true,
1226 ]
1227 ),
1228 new FieldLayout(
1229 new RadioInputWidget( [
1230 'name' => 'translatable-page-state',
1231 'value' => 'proposed',
1232 'selected' => $currentStateId === TranslatableBundleState::PROPOSE
1233 ] ),
1234 [
1235 'label' => $this->msg( 'tpt-translation-settings-propose' )->text(),
1236 'align' => 'inline',
1237 'help' => $this->msg( 'tpt-translation-settings-propose-hint' )->text(),
1238 'helpInline' => true,
1239 ]
1240 ),
1241 ],
1242 ] );
1243
1244 $out->addHTML( $options->toString() );
1245
1246 $submitButton = new FieldLayout(
1247 new ButtonInputWidget( [
1248 'label' => $this->msg( 'tpt-translation-settings-save' )->text(),
1249 'type' => 'submit',
1250 'flags' => [ 'primary', 'progressive' ],
1251 'disabled' => $block !== null,
1252 ] )
1253 );
1254
1255 $out->addHTML( $submitButton->toString() );
1256 $out->addHTML( Html::closeElement( 'form' ) );
1257 }
1258
1259 private function handleTranslationState( Title $title, string $selectedState ): void {
1260 $validStateValues = [ 'ignored', 'unstable', 'proposed' ];
1261 $out = $this->getOutput();
1262 if ( !in_array( $selectedState, $validStateValues ) ) {
1263 throw new InvalidArgumentException( "Invalid translation state selected: $selectedState" );
1264 }
1265
1266 $user = $this->getUser();
1267 if ( !$this->translatablePageView->canManageTranslationSettings( $title, $user ) ) {
1268 $this->showTranslationStateRestricted();
1269 return;
1270 }
1271
1272 $bundleState = TranslatableBundleState::newFromText( $selectedState );
1273 if ( $selectedState === 'unstable' ) {
1274 $this->translatablePageStateStore->remove( $title );
1275 } else {
1276 $this->translatablePageStateStore->set( $title, $bundleState );
1277 }
1278
1279 $this->displayStateInfoMessage( $title, $bundleState );
1280 $out->setPageTitle( $this->msg( 'tpt-translation-settings-page-title' )->text() );
1281 $out->addWikiMsg( 'tpt-list-pages-in-translations' );
1282 }
1283
1284 private function addPageForm(
1285 Title $target,
1286 string $formClass,
1287 string $action,
1288 ?int $revision
1289 ): void {
1290 $formParams = [
1291 'method' => 'post',
1292 'action' => $this->getPageTitle()->getLocalURL(),
1293 'class' => $formClass
1294 ];
1295
1296 $this->getOutput()->addHTML(
1297 Xml::openElement( 'form', $formParams ) .
1298 Html::hidden( 'do', $action ) .
1299 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
1300 ( $revision ? Html::hidden( 'revision', $revision ) : '' ) .
1301 Html::hidden( 'target', $target->getPrefixedText() ) .
1302 Html::hidden( 'token', $this->getContext()->getCsrfTokenSet()->getToken() )
1303 );
1304 }
1305
1306 private function displayStateInfoMessage( Title $title, TranslatableBundleState $bundleState ): void {
1307 $stateId = $bundleState->getStateId();
1308 if ( $stateId === TranslatableBundleState::UNSTABLE ) {
1309 $infoMessage = $this->msg( 'tpt-translation-settings-unstable-notice' );
1310 } elseif ( $stateId === TranslatableBundleState::PROPOSE ) {
1311 $userHasPageTranslationRight = $this->getUser()->isAllowed( 'pagetranslation' );
1312 if ( $userHasPageTranslationRight ) {
1313 $infoMessage = $this->msg( 'tpt-translation-settings-proposed-pagetranslation-notice' )->params(
1314 'https://www.mediawiki.org/wiki/Special:MyLanguage/' .
1315 'Help:Extension:Translate/Page_translation_administration',
1316 $title->getFullURL( 'action=edit' ),
1317 SpecialPage::getTitleFor( 'PagePreparation' )
1318 ->getFullURL( [ 'page' => $title->getPrefixedText() ] )
1319 );
1320 } else {
1321 $infoMessage = $this->msg( 'tpt-translation-settings-proposed-editor-notice' );
1322 }
1323 } else {
1324 $infoMessage = $this->msg( 'tpt-translation-settings-ignored-notice' );
1325 }
1326
1327 $this->getOutput()->wrapWikiMsg( Html::noticeBox( '$1', '' ), $infoMessage );
1328 }
1329
1330 private function getBlock( WebRequest $request, User $user, Title $title ): ?ErrorPageError {
1331 if ( $this->permissionManager->isBlockedFrom( $user, $title, !$request->wasPosted() ) ) {
1332 $block = $user->getBlock();
1333 if ( $block ) {
1334 return new UserBlockedError(
1335 $block,
1336 $user,
1337 $this->getLanguage(),
1338 $request->getIP()
1339 );
1340 }
1341
1342 return new PermissionsError( 'pagetranslation', [ 'badaccess-group0' ] );
1343 }
1344
1345 return null;
1346 }
1347
1348 private function showTranslationStateRestricted(): void {
1349 $out = $this->getOutput();
1350 $out->wrapWikiMsg( Html::errorBox( "$1" ), 'tpt-translation-settings-restricted' );
1351 $out->addWikiMsg( 'tpt-list-pages-in-translations' );
1352 }
1353}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, '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:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), $services->getContentLanguageCode() ->toString(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):?MessageGroupSubscriptionHookHandler { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, '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->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory(), $services->get( 'Translate:HookRunner'),);}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, '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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array
Factory class for accessing message groups individually by id or all of them as a list.
Class to manage revision tags for translatable bundles.
Stores and validates possible translation states for translatable bundles.
Offers functionality for reading and updating Translate group related metadata.
A special page for marking revisions of pages for translation.
static buildPageArray(IResultWrapper $res)
TODO: Move this function to SyncTranslatableBundleStatusMaintenanceScript once we start using the tra...
static loadPagesFromDB()
TODO: Move this function to SyncTranslatableBundleStatusMaintenanceScript once we start using the tra...
Exception thrown when TranslatablePageMarker is unable to unmark a page for translation.
Service to mark/unmark pages from translation and perform related validations.
Generates ParserOutput from text or removes all tags from a text.
Logic and code to generate various aspects related to how translatable pages are displayed.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:29