58 private const DISPLAY_STATUS_MAPPING = [
59 TranslatablePageStatus::PROPOSED =>
'proposed',
60 TranslatablePageStatus::ACTIVE =>
'active',
61 TranslatablePageStatus::OUTDATED =>
'outdated',
62 TranslatablePageStatus::BROKEN =>
'broken'
64 private LanguageFactory $languageFactory;
65 private LinkBatchFactory $linkBatchFactory;
66 private JobQueueGroup $jobQueueGroup;
67 private PermissionManager $permissionManager;
74 public function __construct(
75 LanguageFactory $languageFactory,
76 LinkBatchFactory $linkBatchFactory,
77 JobQueueGroup $jobQueueGroup,
78 PermissionManager $permissionManager,
85 parent::__construct(
'PageTranslation' );
86 $this->languageFactory = $languageFactory;
87 $this->linkBatchFactory = $linkBatchFactory;
88 $this->jobQueueGroup = $jobQueueGroup;
89 $this->permissionManager = $permissionManager;
90 $this->translatablePageMarker = $translatablePageMarker;
91 $this->translatablePageParser = $translatablePageParser;
92 $this->messageGroupMetadata = $messageGroupMetadata;
93 $this->translatablePageView = $translatablePageView;
94 $this->translatablePageStateStore = $translatablePageStateStore;
97 public function doesWrites():
bool {
101 protected function getGroupName():
string {
102 return 'translation';
105 public function execute( $parameters ) {
108 $user = $this->getUser();
109 $request = $this->getRequest();
111 $target = $request->getText(
'target', $parameters ??
'' );
112 $revision = $request->getIntOrNull(
'revision' );
113 $action = $request->getVal(
'do' );
114 $out = $this->getOutput();
115 $out->addModules(
'ext.translate.special.pagetranslation' );
116 $out->addModuleStyles(
'ext.translate.specialpages.styles' );
117 $out->addHelpLink(
'Help:Extension:Translate/Page_translation_example' );
120 if ( $target ===
'' ) {
126 $title = Title::newFromText( $target );
128 $out->wrapWikiMsg( Html::errorBox(
'$1' ), [
'tpt-badtitle', $target ] );
129 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
132 } elseif ( !$title->exists() ) {
134 Html::errorBox(
'$1' ),
135 [
'tpt-nosuchpage', $title->getPrefixedText() ]
137 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
142 if ( $action ===
'settings' && !$this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
143 $this->showTranslationStateRestricted();
147 $block = $this->getBlock( $request, $user, $title );
148 if ( $action ===
'settings' && !$request->wasPosted() ) {
149 $this->showTranslationSettings( $title, $block );
158 $csrfTokenSet = $this->getContext()->getCsrfTokenSet();
159 if ( $request->wasPosted() && !$csrfTokenSet->matchTokenField(
'token' ) ) {
160 throw new PermissionsError(
'pagetranslation' );
163 if ( $action ===
'settings' && $request->wasPosted() ) {
164 $this->handleTranslationState( $title, $request->getRawVal(
'translatable-page-state' ) );
169 if ( !$user->isAllowed(
'pagetranslation' ) ) {
170 throw new PermissionsError(
'pagetranslation' );
173 if ( $action ===
'mark' ) {
175 $this->onActionMark( $title, $revision );
181 if ( !$request->wasPosted() ) {
182 if ( $action ===
'unlink' ) {
183 $this->showUnlinkConfirmation( $title );
187 'target' => $title->getPrefixedText(),
188 'revision' => $revision,
190 $this->showGenericConfirmation( $params );
196 if ( $action ===
'discourage' || $action ===
'encourage' ) {
198 $current = MessageGroups::getPriority( $id );
200 if ( $action ===
'encourage' ) {
203 $new =
'discouraged';
206 if ( $new !== $current ) {
207 MessageGroups::setPriority( $id, $new );
208 $entry =
new ManualLogEntry(
'pagetranslation', $action );
209 $entry->setPerformer( $user );
210 $entry->setTarget( $title );
211 $logId = $entry->insert();
212 $entry->publish( $logId );
219 $group = MessageGroups::getGroup( $id );
220 $sharedGroupIds = MessageGroups::getSharedGroups( $group );
221 if ( $sharedGroupIds !== [] ) {
222 $job = RebuildMessageGroupStatsJob::newRefreshGroupsJob( $sharedGroupIds );
223 $this->jobQueueGroup->push( $job );
232 if ( $action ===
'unlink' || $action ===
'unmark' ) {
234 $this->translatablePageMarker->unmarkPage(
241 Html::successBox(
'$1' ),
242 [
'tpt-unmarked', $title->getPrefixedText() ]
246 Html::errorBox(
'$1' ),
247 $e->getMessageObject()
251 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
255 protected function onActionMark( Title $title, ?
int $revision ):
void {
256 $request = $this->getRequest();
257 $out = $this->getOutput();
258 $translateTitle = $request->getCheck(
'translatetitle' );
261 $operation = $this->translatablePageMarker->getMarkOperation(
262 $title->toPageRecord(
263 $request->wasPosted() ? IDBAccessObject::READ_LATEST : IDBAccessObject::READ_NORMAL
268 !$request->wasPosted() || $translateTitle
271 $out->addHTML( Html::errorBox( $this->msg( $e->getMessageObject() )->parse() ) );
272 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
276 $unitNameValidationResult = $operation->getUnitValidationStatus();
278 if ( $unitNameValidationResult->isOK() && $request->wasPosted() ) {
280 [ $priorityLanguages, $forcePriorityLanguage, $priorityLanguageReason ] =
281 $this->getPriorityLanguage( $this->getRequest() );
283 $noFuzzyUnits = array_filter(
285 '/^tpt-sect-(.*)-action-nofuzzy$|.*/',
287 array_keys( $request->getValues() )
297 $noFuzzyUnits = str_replace(
'_',
' ', $noFuzzyUnits );
301 $forcePriorityLanguage,
302 $priorityLanguageReason,
305 $request->getCheck(
'use-latest-syntax' ),
306 $request->getCheck(
'transclusion' )
310 $unitCount = $this->translatablePageMarker->markForTranslation(
312 $translatablePageSettings,
315 $this->showSuccess( $operation->getPage(), $operation->isFirstMark(), $unitCount );
318 Html::errorBox(
'$1' ),
319 $e->getMessageObject()
323 if ( !$unitNameValidationResult->isOK() ) {
326 $unitNameValidationResult->getHTML(
false,
false, $this->getLanguage() )
331 $this->showPage( $operation );
342 private function showSuccess(
TranslatablePage $page,
bool $firstMark,
int $unitCount ):
void {
343 $titleText = $page->
getTitle()->getPrefixedText();
344 $num = $this->getLanguage()->formatNum( $unitCount );
345 $link = SpecialPage::getTitleFor(
'Translate' )->getFullURL( [
351 $this->getOutput()->wrapWikiMsg(
352 Html::successBox(
'$1' ),
353 [
'tpt-saveok', $titleText, $num, $link ]
359 $this->getOutput()->addWikiMsg(
'tpt-saveok-first' );
364 if ( method_exists( SpecialNotifyTranslators::class,
'execute' ) &&
365 $this->getUser()->isAllowed( SpecialNotifyTranslators::$right )
367 $link = SpecialPage::getTitleFor(
'NotifyTranslators' )->getFullURL(
368 [
'tpage' => $page->
getTitle()->getArticleID() ]
370 $this->getOutput()->addWikiMsg(
'tpt-offer-notify', $link );
373 $this->getOutput()->addWikiMsg(
'tpt-list-pages-in-translations' );
376 private function showGenericConfirmation( array $params ):
void {
379 'action' => $this->getPageTitle()->getLocalURL(),
382 $params[
'title'] = $this->getPageTitle()->getPrefixedText();
383 $params[
'token'] = $this->getContext()->getCsrfTokenSet()->getToken();
386 foreach ( $params as $key => $value ) {
387 $hidden .= Html::hidden( $key, $value );
390 $this->getOutput()->addHTML(
391 Html::openElement(
'form', $formParams ) .
393 $this->msg(
'tpt-generic-confirm' )->parseAsBlock() .
395 $this->msg(
'tpt-generic-button' )->text(),
396 [
'class' =>
'mw-ui-button mw-ui-progressive' ]
398 Html::closeElement(
'form' )
402 private function showUnlinkConfirmation( Title $target ):
void {
405 'action' => $this->getPageTitle()->getLocalURL(),
408 $this->getOutput()->addHTML(
409 Html::openElement(
'form', $formParams ) .
410 Html::hidden(
'do',
'unlink' ) .
411 Html::hidden(
'title', $this->getPageTitle()->getPrefixedText() ) .
412 Html::hidden(
'target', $target->getPrefixedText() ) .
413 Html::hidden(
'token', $this->getContext()->getCsrfTokenSet()->getToken() ) .
414 $this->msg(
'tpt-unlink-confirm', $target->getPrefixedText() )->parseAsBlock() .
416 $this->msg(
'tpt-unlink-button' )->text(),
417 [
'class' =>
'mw-ui-button mw-ui-destructive' ]
419 Html::closeElement(
'form' )
429 return $dbr->newSelectQueryBuilder()
435 'rt_revision' =>
'MAX(rt_revision)',
439 ->join(
'revtag',
null,
'page_id=rt_page' )
441 'rt_type' => [ RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG ],
443 ->orderBy( [
'page_namespace',
'page_title' ] )
444 ->groupBy( [
'page_id',
'page_namespace',
'page_title',
'page_latest',
'rt_type' ] )
445 ->caller( __METHOD__ )
455 foreach ( $res as $r ) {
457 if ( !isset( $pages[$r->page_id] ) ) {
458 $pages[$r->page_id] = [];
459 $title = Title::newFromRow( $r );
460 $pages[$r->page_id][
'title'] = $title;
461 $pages[$r->page_id][
'latest'] = (int)$title->getLatestRevID();
465 $pages[$r->page_id][$tag] = (int)$r->rt_revision;
477 private function classifyPages( array $pages ): array {
486 if ( $pages === [] ) {
491 $messageGroupIdsForPreload = [];
492 foreach ( $pages as $i => $page ) {
493 $id = TranslatablePage::getMessageGroupIdFromTitle( $page[
'title'] );
494 $messageGroupIdsForPreload[] = $id;
495 $pages[$i][
'groupid'] = $id;
498 $metadata = $this->messageGroupMetadata->loadBasicMetadataForTranslatablePages(
499 $messageGroupIdsForPreload,
500 [
'transclusion',
'version' ]
503 foreach ( $pages as $page ) {
504 $groupId = $page[
'groupid'];
505 $group = MessageGroups::getGroup( $groupId );
506 $page[
'discouraged'] = MessageGroups::getPriority( $group ) ===
'discouraged';
507 $page[
'version'] = $metadata[$groupId][
'version'] ?? TranslatablePageMarker::DEFAULT_SYNTAX_VERSION;
508 $page[
'transclusion'] = $metadata[$groupId][
'transclusion'] ??
false;
511 $tpStatus = TranslatablePage::determineStatus(
512 $page[RevTagStore::TP_READY_TAG] ??
null,
513 $page[RevTagStore::TP_MARK_TAG] ??
null,
522 $out[self::DISPLAY_STATUS_MAPPING[$tpStatus->getId()]][] = $page;
528 public function listPages(): void {
529 $out = $this->getOutput();
531 $res = self::loadPagesFromDB();
532 $allPages = self::buildPageArray( $res );
534 $pagesWithProposedState = [];
535 if ( $this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
536 $pagesWithProposedState = $this->translatablePageStateStore->getRequested();
539 if ( !count( $allPages ) && !count( $pagesWithProposedState ) ) {
540 $out->addWikiMsg(
'tpt-list-nopages' );
545 $lb = $this->linkBatchFactory->newLinkBatch();
546 $lb->setCaller( __METHOD__ );
547 foreach ( $allPages as $page ) {
548 $lb->addObj( $page[
'title'] );
551 foreach ( $pagesWithProposedState as $title ) {
552 $lb->addObj( $title );
556 $types = $this->classifyPages( $allPages );
558 $pages = $types[
'proposed'];
559 if ( $pages || $pagesWithProposedState ) {
560 $out->wrapWikiMsg(
'== $1 ==',
'tpt-new-pages-title' );
562 $out->addWikiMsg(
'tpt-new-pages', count( $pages ) );
563 $out->addHTML( $this->getPageList( $pages,
'proposed' ) );
566 if ( $pagesWithProposedState ) {
567 $out->addWikiMsg(
'tpt-proposed-state-pages', count( $pagesWithProposedState ) );
568 $out->addHTML( $this->displayPagesWithProposedState( $pagesWithProposedState ) );
572 $pages = $types[
'broken'];
574 $out->wrapWikiMsg(
'== $1 ==',
'tpt-other-pages-title' );
575 $out->addWikiMsg(
'tpt-other-pages', count( $pages ) );
576 $out->addHTML( $this->getPageList( $pages,
'broken' ) );
579 $pages = $types[
'outdated'];
581 $out->wrapWikiMsg(
'== $1 ==',
'tpt-outdated-pages-title' );
582 $out->addWikiMsg(
'tpt-outdated-pages', count( $pages ) );
583 $out->addHTML( $this->getPageList( $pages,
'outdated' ) );
586 $pages = $types[
'active'];
588 $out->wrapWikiMsg(
'== $1 ==',
'tpt-old-pages-title' );
589 $out->addWikiMsg(
'tpt-old-pages', count( $pages ) );
590 $out->addHTML( $this->getPageList( $pages,
'active' ) );
594 private function actionLinks( array $page,
string $type ): string {
596 static $messageCache = null;
597 if ( $messageCache ===
null ) {
599 'mark' => $this->msg(
'tpt-rev-mark' )->text(),
600 'mark-tooltip' => $this->msg(
'tpt-rev-mark-tooltip' )->text(),
601 'encourage' => $this->msg(
'tpt-rev-encourage' )->text(),
602 'encourage-tooltip' => $this->msg(
'tpt-rev-encourage-tooltip' )->text(),
603 'discourage' => $this->msg(
'tpt-rev-discourage' )->text(),
604 'discourage-tooltip' => $this->msg(
'tpt-rev-discourage-tooltip' )->text(),
605 'unmark' => $this->msg(
'tpt-rev-unmark' )->text(),
606 'unmark-tooltip' => $this->msg(
'tpt-rev-unmark-tooltip' )->text(),
607 'pipe-separator' => $this->msg(
'pipe-separator' )->escaped(),
613 $title = $page[
'title'];
614 $user = $this->getUser();
617 $js = [
'class' =>
'mw-translate-jspost' ];
619 if ( $user->isAllowed(
'pagetranslation' ) ) {
622 if ( $type !==
'broken' ) {
623 $actions[] = $this->getLinkRenderer()->makeKnownLink(
624 $this->getPageTitle(),
625 $messageCache[
'mark'],
626 [
'title' => $messageCache[
'mark-tooltip'] ],
629 'target' => $title->getPrefixedText(),
630 'revision' => $title->getLatestRevID(),
635 if ( $type !==
'proposed' ) {
636 if ( $page[
'discouraged'] ) {
637 $actions[] = $this->getLinkRenderer()->makeKnownLink(
638 $this->getPageTitle(),
639 $messageCache[
'encourage'],
640 [
'title' => $messageCache[
'encourage-tooltip'] ] + $js,
643 'target' => $title->getPrefixedText(),
648 $actions[] = $this->getLinkRenderer()->makeKnownLink(
649 $this->getPageTitle(),
650 $messageCache[
'discourage'],
651 [
'title' => $messageCache[
'discourage-tooltip'] ] + $js,
653 'do' =>
'discourage',
654 'target' => $title->getPrefixedText(),
660 $actions[] = $this->getLinkRenderer()->makeKnownLink(
661 $this->getPageTitle(),
662 $messageCache[
'unmark'],
663 [
'title' => $messageCache[
'unmark-tooltip'] ],
665 'do' => $type ===
'broken' ?
'unmark' :
'unlink',
666 'target' => $title->getPrefixedText(),
677 return '<div>' . implode( $messageCache[
'pipe-separator'], $actions ) .
'</div>';
680 private function showPage( TranslatablePageMarkOperation $operation ): void {
681 $page = $operation->getPage();
682 $out = $this->getOutput();
683 $out->addBacklinkSubtitle( $page->getTitle() );
684 $out->addWikiMsg(
'tpt-showpage-intro' );
688 'mw-tpt-sp-markform',
693 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-oldnew' );
695 $diffOld = $this->msg(
'tpt-diff-old' )->escaped();
696 $diffNew = $this->msg(
'tpt-diff-new' )->escaped();
703 $operation->isFirstMark() &&
704 !$page->getTitle()->inNamespace( NS_TEMPLATE )
705 ) || $page->hasPageDisplayTitle();
707 $sourceLanguage = $this->languageFactory->getLanguage( $page->getSourceLanguageCode() );
709 foreach ( $operation->getUnits() as $s ) {
710 if ( $s->id === TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
712 $s->type = $defaultChecked ? $s->type :
'new';
715 $checkBox =
new FieldLayout(
716 new CheckboxInputWidget( [
717 'name' =>
'translatetitle',
718 'selected' => $defaultChecked,
721 'label' => $this->msg(
'tpt-translate-title' )->text(),
723 'classes' => [
'mw-tpt-m-vertical' ]
726 $out->addHTML( $checkBox->toString() );
729 if ( $s->type ===
'new' ) {
731 $name = $this->msg(
'tpt-section-new', $s->id )->escaped();
733 $name = $this->msg(
'tpt-section', $s->id )->escaped();
736 if ( $s->type ===
'changed' ) {
738 $diff =
new DifferenceEngine();
739 $diff->setTextLanguage( $sourceLanguage );
740 $diff->setReducedLineNumbers();
742 $oldContent = ContentHandler::makeContent( $s->getOldText(), $diff->getTitle() );
743 $newContent = ContentHandler::makeContent( $s->getText(), $diff->getTitle() );
745 $diff->setContent( $oldContent, $newContent );
747 $text = $diff->getDiff( $diffOld, $diffNew );
748 $diffOld = $diffNew =
null;
749 $diff->showDiffStyle();
751 $id =
"tpt-sect-{$s->id}-action-nofuzzy";
752 $checkLabel =
new FieldLayout(
753 new CheckboxInputWidget( [
755 'selected' => $s->onlyTvarsChanged()
758 'label' => $this->msg(
'tpt-action-nofuzzy' )->text(),
760 'classes' => [
'mw-tpt-m-vertical' ]
763 $text = $checkLabel->toString() . $text;
765 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
768 # For changed text, the language is set by $diff->setTextLanguage()
769 $lang = $s->type ===
'changed' ? null : $sourceLanguage;
770 $out->addHTML( MessageWebImporter::makeSectionElement(
777 foreach ( $s->getIssues() as $issue ) {
778 $severity = $issue->getSeverity();
779 if ( $severity === TranslationUnitIssue::WARNING ) {
780 $box = Html::warningBox( $this->msg( $issue )->escaped() );
781 } elseif ( $severity === TranslationUnitIssue::ERROR ) {
782 $box = Html::errorBox( $this->msg( $issue )->escaped() );
784 throw new UnexpectedValueException(
785 "Unknown severity: $severity for key: {$issue->getKey()}"
789 $out->addHTML( $box );
793 if ( $operation->getDeletedUnits() ) {
795 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-deleted' );
797 foreach ( $operation->getDeletedUnits() as $s ) {
798 $name = $this->msg(
'tpt-section-deleted', $s->id )->escaped();
799 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
800 $out->addHTML( MessageWebImporter::makeSectionElement(
810 $markedTag = $page->getMarkedTag();
811 if ( $markedTag !==
null ) {
813 $newTemplate = $operation->getParserOutput()->sourcePageTemplateForDiffs();
814 $oldPage = TranslatablePage::newFromRevision(
818 $oldTemplate = $this->translatablePageParser
819 ->parse( $oldPage->getText() )
820 ->sourcePageTemplateForDiffs();
822 if ( $oldTemplate !== $newTemplate ) {
823 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-template' );
825 $diff =
new DifferenceEngine();
826 $diff->setTextLanguage( $sourceLanguage );
828 $oldContent = ContentHandler::makeContent( $oldTemplate, $diff->getTitle() );
829 $newContent = ContentHandler::makeContent( $newTemplate, $diff->getTitle() );
831 $diff->setContent( $oldContent, $newContent );
833 $text = $diff->getDiff(
834 $this->msg(
'tpt-diff-old' )->escaped(),
835 $this->msg(
'tpt-diff-new' )->escaped()
837 $diff->showDiffStyle();
838 $diff->setReducedLineNumbers();
840 $out->addHTML( Xml::tags(
'div', [], $text ) );
844 if ( !$hasChanges ) {
845 $out->wrapWikiMsg( Html::successBox(
'$1' ),
'tpt-mark-nochanges' );
848 $this->priorityLanguagesForm( $page );
852 $this->templateTransclusionForm( $page->supportsTransclusion() ?? $operation->isFirstMark() );
854 $version = $this->messageGroupMetadata->getWithDefaultValue(
855 $page->getMessageGroupId(),
'version', TranslatablePageMarker::DEFAULT_SYNTAX_VERSION
857 $this->syntaxVersionForm( $version, $operation->isFirstMark() );
859 $submitButton =
new FieldLayout(
860 new ButtonInputWidget( [
861 'label' => $this->msg(
'tpt-submit' )->text(),
863 'flags' => [
'primary',
'progressive' ],
871 $out->addHTML( $submitButton->toString() );
872 $out->addHTML(
'</form>' );
875 private function priorityLanguagesForm( TranslatablePage $page ): void {
876 $groupId = $page->getMessageGroupId();
877 $interfaceLanguage = $this->getLanguage()->getCode();
878 $storedLanguages = (string)$this->messageGroupMetadata->get( $groupId,
'prioritylangs' );
879 $default = $storedLanguages !==
'' ? explode(
',', $storedLanguages ) : [];
881 $priorityReason = $this->messageGroupMetadata->get( $groupId,
'priorityreason' );
882 $priorityReason = $priorityReason !==
false ? $priorityReason :
'';
884 $form =
new FieldsetLayout( [
887 new LanguagesMultiselectWidget( [
889 'name' =>
'prioritylangs',
890 'id' =>
'mw-translate-SpecialPageTranslation-prioritylangs',
891 'languages' => Utilities::getLanguageNames( $interfaceLanguage ),
892 'default' => $default,
895 'label' => $this->msg(
'tpt-select-prioritylangs' )->text(),
900 new CheckboxInputWidget( [
901 'name' =>
'forcelimit',
902 'selected' => $this->messageGroupMetadata->get( $groupId,
'priorityforce' ) ===
'on',
905 'label' => $this->msg(
'tpt-select-prioritylangs-force' )->text(),
907 'help' =>
new HtmlSnippet( $this->msg(
'tpt-select-no-prioritylangs-force' )->parse() ),
911 new TextInputWidget( [
912 'name' =>
'priorityreason',
913 'value' => $priorityReason
916 'label' => $this->msg(
'tpt-select-prioritylangs-reason' )->text(),
924 $this->getOutput()->wrapWikiMsg(
'==$1==',
'tpt-sections-prioritylangs' );
925 $this->getOutput()->addHTML( $form->toString() );
928 private function syntaxVersionForm(
string $version,
bool $firstMark ): void {
929 $out = $this->getOutput();
931 if ( $version === TranslatablePageMarker::LATEST_SYNTAX_VERSION || $firstMark ) {
935 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-syntaxversion' );
937 'tpt-syntaxversion-text',
938 '<code>' . wfEscapeWikiText(
'<span lang="en" dir="ltr">...</span>' ) .
'</code>',
939 '<code>' . wfEscapeWikiText(
'<translate nowrap>...</translate>' ) .
'</code>'
942 $checkBox =
new FieldLayout(
943 new CheckboxInputWidget( [
944 'name' =>
'use-latest-syntax'
947 'label' => $out->msg(
'tpt-syntaxversion-label' )->text(),
952 $out->addHTML( $checkBox->toString() );
955 private function templateTransclusionForm(
bool $supportsTransclusion ): void {
956 $out = $this->getOutput();
957 $out->wrapWikiMsg(
'==$1==',
'tpt-transclusion' );
959 $checkBox =
new FieldLayout(
960 new CheckboxInputWidget( [
961 'name' =>
'transclusion',
962 'selected' => $supportsTransclusion
965 'label' => $out->msg(
'tpt-transclusion-label' )->text(),
970 $out->addHTML( $checkBox->toString() );
973 private function getPriorityLanguage( WebRequest $request ): array {
977 $priorityLanguages = rtrim( trim( $request->getVal(
'prioritylangs',
'' ) ),
',' );
978 $priorityLanguages = str_replace(
"\n",
',', $priorityLanguages );
979 $priorityLanguages = array_map(
'trim', explode(
',', $priorityLanguages ) );
980 $priorityLanguages = array_unique( array_filter( $priorityLanguages ) );
982 $forcePriorityLanguage = $request->getCheck(
'forcelimit' );
983 $priorityLanguageReason = trim( $request->getText(
'priorityreason' ) );
985 return [ $priorityLanguages, $forcePriorityLanguage, $priorityLanguageReason ];
988 private function getPageList( array $pages,
string $type ): string {
992 $tagDiscouraged = $this->msg(
'tpt-tag-discouraged' )->escaped();
993 $tagOldSyntax = $this->msg(
'tpt-tag-oldsyntax' )->escaped();
994 $tagNoTransclusionSupport = $this->msg(
'tpt-tag-no-transclusion-support' )->escaped();
996 foreach ( $pages as $page ) {
997 $link = $this->getLinkRenderer()->makeKnownLink( $page[
'title'] );
998 $acts = $this->actionLinks( $page, $type );
1000 if ( $page[
'discouraged'] ) {
1001 $tags[] = $tagDiscouraged;
1003 if ( $type !==
'proposed' ) {
1004 if ( $page[
'version'] !== TranslatablePageMarker::LATEST_SYNTAX_VERSION ) {
1005 $tags[] = $tagOldSyntax;
1008 if ( $page[
'transclusion'] !==
'1' ) {
1009 $tags[] = $tagNoTransclusionSupport;
1016 $tagsKey = implode(
'', $tags );
1017 $tagsTextCache[$tagsKey] ??= $this->msg(
'parentheses' )
1018 ->rawParams( $this->getLanguage()->pipeList( $tags ) )
1021 $tagList = Html::rawElement(
1023 [
'class' =>
'mw-tpt-actions' ],
1024 $tagsTextCache[$tagsKey]
1028 $items[] =
"<li class='mw-tpt-pagelist-item'>$link $tagList $acts</li>";
1031 return '<ol>' . implode(
'', $items ) .
'</ol>';
1035 private function displayPagesWithProposedState( array $pagesWithProposedState ): string {
1037 $preparePageAction = $this->msg(
'tpt-prepare-page' )->text();
1038 $preparePageTooltip = $this->msg(
'tpt-prepare-page-tooltip' )->text();
1039 $linkRenderer = $this->getLinkRenderer();
1040 foreach ( $pagesWithProposedState as $pageRecord ) {
1041 $link = $linkRenderer->makeKnownLink( $pageRecord );
1042 $action = $linkRenderer->makeKnownLink(
1043 SpecialPage::getTitleFor(
'PagePreparation' ),
1045 [
'title' => $preparePageTooltip ],
1046 [
'page' => ( Title::newFromPageReference( $pageRecord ) )->getPrefixedText() ]
1048 $items[] =
"<li class='mw-tpt-pagelist-item'>$link <div>$action</div></li>";
1050 return '<ol>' . implode(
'', $items ) .
'</ol>';
1053 private function showTranslationSettings( Title $target, ?ErrorPageError $block ): void {
1054 $out = $this->getOutput();
1055 $out->setPageTitle( $this->msg(
'tpt-translation-settings-page-title' )->text() );
1056 $out->addBacklinkSubtitle( $target );
1058 $currentState = $this->translatablePageStateStore->get( $target );
1060 if ( !$this->translatablePageView->canManageTranslationSettings( $target, $this->getUser() ) ) {
1061 $out->wrapWikiMsg( Html::errorBox(
'$1' ),
'tpt-translation-settings-restricted' );
1062 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
1067 $out->wrapWikiMsg( Html::errorBox(
'$1' ), $block->getMessageObject() );
1070 if ( $currentState ) {
1071 $this->displayStateInfoMessage( $target, $currentState );
1074 $this->addPageForm( $target,
'mw-tpt-sp-settings',
'settings',
null );
1078 [
'class' =>
'mw-tpt-vm' ],
1079 Html::element(
'strong', [], $this->msg(
'tpt-translation-settings-subtitle' ) )
1083 $currentStateId = $currentState ? $currentState->getStateId() :
null;
1084 $options =
new FieldsetLayout( [
1087 new RadioInputWidget( [
1088 'name' =>
'translatable-page-state',
1089 'value' =>
'ignored',
1090 'selected' => $currentStateId === TranslatableBundleState::IGNORE
1093 'label' => $this->msg(
'tpt-translation-settings-ignore' )->text(),
1094 'align' =>
'inline',
1095 'help' => $this->msg(
'tpt-translation-settings-ignore-hint' )->text(),
1096 'helpInline' =>
true,
1100 new RadioInputWidget( [
1101 'name' =>
'translatable-page-state',
1102 'value' =>
'unstable',
1103 'selected' => $currentStateId ===
null
1106 'label' => $this->msg(
'tpt-translation-settings-unstable' )->text(),
1107 'align' =>
'inline',
1108 'help' => $this->msg(
'tpt-translation-settings-unstable-hint' )->text(),
1109 'helpInline' =>
true,
1113 new RadioInputWidget( [
1114 'name' =>
'translatable-page-state',
1115 'value' =>
'proposed',
1116 'selected' => $currentStateId === TranslatableBundleState::PROPOSE
1119 'label' => $this->msg(
'tpt-translation-settings-propose' )->text(),
1120 'align' =>
'inline',
1121 'help' => $this->msg(
'tpt-translation-settings-propose-hint' )->text(),
1122 'helpInline' =>
true,
1128 $out->addHTML( $options->toString() );
1130 $submitButton =
new FieldLayout(
1131 new ButtonInputWidget( [
1132 'label' => $this->msg(
'tpt-translation-settings-save' )->text(),
1134 'flags' => [
'primary',
'progressive' ],
1135 'disabled' => $block !==
null,
1139 $out->addHTML( $submitButton->toString() );
1140 $out->addHTML( Html::closeElement(
'form' ) );
1143 private function handleTranslationState( Title $title,
string $selectedState ): void {
1144 $validStateValues = [
'ignored',
'unstable',
'proposed' ];
1145 $out = $this->getOutput();
1146 if ( !in_array( $selectedState, $validStateValues ) ) {
1147 throw new InvalidArgumentException(
"Invalid translation state selected: $selectedState" );
1150 $user = $this->getUser();
1151 if ( !$this->translatablePageView->canManageTranslationSettings( $title, $user ) ) {
1152 $this->showTranslationStateRestricted();
1156 $bundleState = TranslatableBundleState::newFromText( $selectedState );
1157 if ( $selectedState ===
'unstable' ) {
1158 $this->translatablePageStateStore->remove( $title );
1160 $this->translatablePageStateStore->set( $title, $bundleState );
1163 $this->displayStateInfoMessage( $title, $bundleState );
1164 $out->setPageTitle( $this->msg(
'tpt-translation-settings-page-title' )->text() );
1165 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
1168 private function addPageForm(
1176 'action' => $this->getPageTitle()->getLocalURL(),
1177 'class' => $formClass
1180 $this->getOutput()->addHTML(
1181 Xml::openElement(
'form', $formParams ) .
1182 Html::hidden(
'do', $action ) .
1183 Html::hidden(
'title', $this->getPageTitle()->getPrefixedText() ) .
1184 ( $revision ? Html::hidden(
'revision', $revision ) :
'' ) .
1185 Html::hidden(
'target', $target->getPrefixedText() ) .
1186 Html::hidden(
'token', $this->getContext()->getCsrfTokenSet()->getToken() )
1190 private function displayStateInfoMessage( Title $title, TranslatableBundleState $bundleState ): void {
1191 $stateId = $bundleState->getStateId();
1192 if ( $stateId === TranslatableBundleState::UNSTABLE ) {
1193 $infoMessage = $this->msg(
'tpt-translation-settings-unstable-notice' );
1194 } elseif ( $stateId === TranslatableBundleState::PROPOSE ) {
1195 $userHasPageTranslationRight = $this->getUser()->isAllowed(
'pagetranslation' );
1196 if ( $userHasPageTranslationRight ) {
1197 $infoMessage = $this->msg(
'tpt-translation-settings-proposed-pagetranslation-notice' )->params(
1198 'https://www.mediawiki.org/wiki/Special:MyLanguage/' .
1199 'Help:Extension:Translate/Page_translation_administration',
1200 $title->getFullURL(
'action=edit' ),
1201 SpecialPage::getTitleFor(
'PagePreparation' )
1202 ->getFullURL( [
'page' => $title->getPrefixedText() ] )
1205 $infoMessage = $this->msg(
'tpt-translation-settings-proposed-editor-notice' );
1208 $infoMessage = $this->msg(
'tpt-translation-settings-ignored-notice' );
1211 $this->getOutput()->wrapWikiMsg( Html::noticeBox(
'$1',
'' ), $infoMessage );
1214 private function getBlock( WebRequest $request, User $user, Title $title ): ?ErrorPageError {
1215 if ( $this->permissionManager->isBlockedFrom( $user, $title, !$request->wasPosted() ) ) {
1216 $block = $user->getBlock();
1218 return new UserBlockedError(
1221 $this->getLanguage(),
1226 return new PermissionsError(
'pagetranslation', [
'badaccess-group0' ] );
1232 private function showTranslationStateRestricted(): void {
1233 $out = $this->getOutput();
1234 $out->wrapWikiMsg( Html::errorBox(
"$1" ),
'tpt-translation-settings-restricted' );
1235 $out->addWikiMsg(
'tpt-list-pages-in-translations' );