61 private const LATEST_SYNTAX_VERSION =
'2';
62 private const DEFAULT_SYNTAX_VERSION =
'1';
63 private const DISPLAY_STATUS_MAPPING = [
64 TranslatablePageStatus::PROPOSED =>
'proposed',
65 TranslatablePageStatus::ACTIVE =>
'active',
66 TranslatablePageStatus::OUTDATED =>
'outdated',
67 TranslatablePageStatus::BROKEN =>
'broken'
69 private LanguageNameUtils $languageNameUtils;
70 private LanguageFactory $languageFactory;
73 private LinkBatchFactory $linkBatchFactory;
74 private JobQueueGroup $jobQueueGroup;
75 private ILoadBalancer $loadBalancer;
77 private TitleParser $titleParser;
79 public function __construct(
80 LanguageNameUtils $languageNameUtils,
81 LanguageFactory $languageFactory,
84 LinkBatchFactory $linkBatchFactory,
85 JobQueueGroup $jobQueueGroup,
86 ILoadBalancer $loadBalancer,
88 TitleParser $titleParser
90 parent::__construct(
'PageTranslation' );
91 $this->languageNameUtils = $languageNameUtils;
92 $this->languageFactory = $languageFactory;
93 $this->translationUnitStoreFactory = $translationUnitStoreFactory;
94 $this->translatablePageParser = $translatablePageParser;
95 $this->linkBatchFactory = $linkBatchFactory;
96 $this->jobQueueGroup = $jobQueueGroup;
97 $this->loadBalancer = $loadBalancer;
98 $this->messageIndex = $messageIndex;
99 $this->titleParser = $titleParser;
102 public function doesWrites():
bool {
106 protected function getGroupName():
string {
107 return 'translation';
110 public function execute( $parameters ) {
113 $user = $this->getUser();
114 $request = $this->getRequest();
116 $target = $request->getText(
'target', $parameters ??
'' );
117 $revision = $request->getInt(
'revision', 0 );
118 $action = $request->getVal(
'do' );
119 $out = $this->getOutput();
120 $out->addModules(
'ext.translate.special.pagetranslation' );
121 $out->addModuleStyles(
'ext.translate.specialpages.styles' );
122 $out->addHelpLink(
'Help:Extension:Translate/Page_translation_example' );
125 if ( $target ===
'' ) {
132 if ( !$user->isAllowed(
'pagetranslation' ) ) {
133 throw new PermissionsError(
'pagetranslation' );
136 $title = Title::newFromText( $target );
138 $out->wrapWikiMsg( Html::errorBox(
'$1' ), [
'tpt-badtitle', $target ] );
139 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
142 } elseif ( !$title->exists() ) {
144 Html::errorBox(
'$1' ),
145 [
'tpt-nosuchpage', $title->getPrefixedText() ]
147 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
153 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
154 if ( $permissionManager->isBlockedFrom( $user, $title, !$request->wasPosted() ) ) {
155 $block = $user->getBlock();
157 throw new UserBlockedError(
160 $this->getLanguage(),
165 throw new PermissionsError(
'pagetranslation', [
'badaccess-group0' ] );
170 $csrfTokenSet = $this->getContext()->getCsrfTokenSet();
171 if ( $request->wasPosted() && !$csrfTokenSet->matchTokenField(
'token' ) ) {
172 throw new PermissionsError(
'pagetranslation' );
175 if ( $action ===
'mark' ) {
177 $this->onActionMark( $title, $revision );
183 if ( !$request->wasPosted() ) {
184 if ( $action ===
'unlink' ) {
185 $this->showUnlinkConfirmation( $title );
189 'target' => $title->getPrefixedText(),
190 'revision' => $revision,
192 $this->showGenericConfirmation( $params );
198 if ( $action ===
'discourage' || $action ===
'encourage' ) {
200 $current = MessageGroups::getPriority( $id );
202 if ( $action ===
'encourage' ) {
205 $new =
'discouraged';
208 if ( $new !== $current ) {
209 MessageGroups::setPriority( $id, $new );
210 $entry =
new ManualLogEntry(
'pagetranslation', $action );
211 $entry->setPerformer( $user );
212 $entry->setTarget( $title );
213 $logid = $entry->insert();
214 $entry->publish( $logid );
221 $group = MessageGroups::getGroup( $id );
222 $sharedGroupIds = MessageGroups::getSharedGroups( $group );
223 if ( $sharedGroupIds !== [] ) {
225 $this->jobQueueGroup->push( $job );
234 if ( $action ===
'unlink' ) {
237 $content = ContentHandler::makeContent(
238 $page->getStrippedSourcePageText(),
242 $status = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title )->doUserEditContent(
245 $this->msg(
'tpt-unlink-summary' )->inContentLanguage()->text(),
246 EDIT_FORCE_BOT | EDIT_UPDATE
249 if ( !$status->isOK() ) {
251 Html::errorBox(
'$1' ),
252 [
'tpt-edit-failed', $status->getWikiText() ]
254 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
260 $this->unmarkPage( $page, $user );
262 Html::successBox(
'$1' ),
263 [
'tpt-unmarked', $title->getPrefixedText() ]
265 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
270 if ( $action ===
'unmark' ) {
272 $this->unmarkPage( $page, $user );
274 Html::successBox(
'$1' ),
275 [
'tpt-unmarked', $title->getPrefixedText() ]
277 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
281 protected function onActionMark( Title $title,
int $revision ):
void {
282 $request = $this->getRequest();
283 $out = $this->getOutput();
285 if ( $revision === 0 ) {
287 $revision = (int)$title->getLatestRevID();
291 if ( $revision !== (
int)$title->getLatestRevID() ) {
293 $target = $title->getFullURL( [
'oldid' => $revision ] );
294 $link =
"<span class='plainlinks'>[$target $revision]</span>";
296 Html::warningBox(
'$1' ),
297 [
'tpt-oldrevision', $title->getPrefixedText(), $link ]
299 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
307 if ( $page->getReadyTag() !== $title->getLatestRevID() ) {
309 Html::errorBox(
'$1' ),
310 [
'tpt-notsuitable', $title->getPrefixedText(), Message::plaintextParam(
'<translate>' ) ]
312 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
317 $firstMark = $page->getMarkedTag() ===
null;
319 $parse = $this->translatablePageParser->parse( $page->getText() );
320 [ $allUnits, $deletedUnits ] = $this->prepareTranslationUnits( $page, $parse );
322 $translateTitle = $request->getCheck(
'translatetitle' );
323 $unitsForTranslation = $allUnits;
325 if ( !$translateTitle ) {
326 $unitsForTranslation = array_filter( $allUnits,
static function ( $s ) {
327 return $s->id !== TranslatablePage::DISPLAY_TITLE_UNIT_ID;
331 $error = $this->validateUnitIds(
334 $request->wasPosted() ? $unitsForTranslation : $allUnits
338 if ( !$error && $request->wasPosted() ) {
339 $setVersion = $firstMark || $request->getCheck(
'use-latest-syntax' );
340 $transclusion = $request->getCheck(
'transclusion' );
342 $err = $this->
markForTranslation( $page, $parse, $unitsForTranslation, $setVersion, $transclusion );
344 call_user_func_array( [ $out,
'addWikiMsg' ], $err );
346 $this->showSuccess( $page, $firstMark, count( $unitsForTranslation ) );
352 $this->showPage( $page, $parse, $allUnits, $deletedUnits, $firstMark );
362 private function showSuccess(
365 $titleText = $page->
getTitle()->getPrefixedText();
366 $num = $this->getLanguage()->formatNum( $unitCount );
367 $link = SpecialPage::getTitleFor(
'Translate' )->getFullURL( [
373 $this->getOutput()->wrapWikiMsg(
374 Html::successBox(
'$1' ),
375 [
'tpt-saveok', $titleText, $num, $link ]
381 $this->getOutput()->addWikiMsg(
'tpt-saveok-first' );
386 if ( method_exists( SpecialNotifyTranslators::class,
'execute' ) &&
387 $this->getUser()->isAllowed( SpecialNotifyTranslators::$right )
389 $link = SpecialPage::getTitleFor(
'NotifyTranslators' )->getFullURL(
390 [
'tpage' => $page->
getTitle()->getArticleID() ]
392 $this->getOutput()->addWikiMsg(
'tpt-offer-notify', $link );
395 $this->getOutput()->addWikiMsg(
'tpt-list-pages-in-translations' );
398 protected function showGenericConfirmation( array $params ):
void {
401 'action' => $this->getPageTitle()->getLocalURL(),
404 $params[
'title'] = $this->getPageTitle()->getPrefixedText();
405 $params[
'token'] = $this->getContext()->getCsrfTokenSet()->getToken();
408 foreach ( $params as $key => $value ) {
409 $hidden .= Html::hidden( $key, $value );
412 $this->getOutput()->addHTML(
413 Html::openElement(
'form', $formParams ) .
415 $this->msg(
'tpt-generic-confirm' )->parseAsBlock() .
417 $this->msg(
'tpt-generic-button' )->text(),
418 [
'class' =>
'mw-ui-button mw-ui-progressive' ]
420 Html::closeElement(
'form' )
424 protected function showUnlinkConfirmation( Title $target ):
void {
427 'action' => $this->getPageTitle()->getLocalURL(),
430 $this->getOutput()->addHTML(
431 Html::openElement(
'form', $formParams ) .
432 Html::hidden(
'do',
'unlink' ) .
433 Html::hidden(
'title', $this->getPageTitle()->getPrefixedText() ) .
434 Html::hidden(
'target', $target->getPrefixedText() ) .
435 Html::hidden(
'token', $this->getContext()->getCsrfTokenSet()->getToken() ) .
436 $this->msg(
'tpt-unlink-confirm', $target->getPrefixedText() )->parseAsBlock() .
438 $this->msg(
'tpt-unlink-button' )->text(),
439 [
'class' =>
'mw-ui-button mw-ui-destructive' ]
441 Html::closeElement(
'form' )
445 protected function unmarkPage(
TranslatablePage $page, UserIdentity $user ):
void {
447 $page->
getTitle()->invalidateCache();
449 $entry =
new ManualLogEntry(
'pagetranslation',
'unmark' );
450 $entry->setPerformer( $user );
451 $entry->setTarget( $page->
getTitle() );
452 $logid = $entry->insert();
453 $entry->publish( $logid );
462 return $dbr->newSelectQueryBuilder()
468 'MAX(rt_revision) AS rt_revision',
471 ->tables( [
'page',
'revtag' ] )
474 'rt_type' => [ RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG ],
476 ->orderBy( [
'page_namespace',
'page_title' ] )
477 ->groupBy( [
'page_id',
'page_namespace',
'page_title',
'page_latest',
'rt_type' ] )
478 ->caller( __METHOD__ )
488 foreach ( $res as $r ) {
490 if ( !isset( $pages[$r->page_id] ) ) {
491 $pages[$r->page_id] = [];
492 $title = Title::newFromRow( $r );
493 $pages[$r->page_id][
'title'] = $title;
494 $pages[$r->page_id][
'latest'] = (int)$title->getLatestRevID();
498 $pages[$r->page_id][$tag] = (int)$r->rt_revision;
511 private function classifyPages( array $pages ): array {
513 $messageGroupIdsForPreload = [];
514 foreach ( $pages as $i => $page ) {
515 $id = TranslatablePage::getMessageGroupIdFromTitle( $page[
'title'] );
516 $messageGroupIdsForPreload[] = $id;
517 $pages[$i][
'groupid'] = $id;
520 $metadata = TranslateMetadata::loadBasicMetadataForTranslatablePages(
521 $messageGroupIdsForPreload,
522 [
'transclusion',
'version' ]
533 foreach ( $pages as $page ) {
534 $groupId = $page[
'groupid'];
535 $group = MessageGroups::getGroup( $groupId );
536 $page[
'discouraged'] = MessageGroups::getPriority( $group ) ===
'discouraged';
537 $page[
'version'] = $metadata[$groupId][
'version'] ?? self::DEFAULT_SYNTAX_VERSION;
538 $page[
'transclusion'] = $metadata[$groupId][
'transclusion'] ??
false;
541 $tpStatus = TranslatablePage::determineStatus(
542 $page[RevTagStore::TP_READY_TAG] ??
null,
543 $page[RevTagStore::TP_MARK_TAG] ??
null,
552 $out[self::DISPLAY_STATUS_MAPPING[$tpStatus->getId()]][] = $page;
558 public function listPages(): void {
559 $out = $this->getOutput();
561 $res = self::loadPagesFromDB();
562 $allPages = self::buildPageArray( $res );
563 if ( !count( $allPages ) ) {
564 $out->addWikiMsg(
'tpt-list-nopages' );
569 $lb = $this->linkBatchFactory->newLinkBatch();
570 $lb->setCaller( __METHOD__ );
571 foreach ( $allPages as $page ) {
572 $lb->addObj( $page[
'title'] );
576 $types = $this->classifyPages( $allPages );
578 $pages = $types[
'proposed'];
580 $out->wrapWikiMsg(
'== $1 ==',
'tpt-new-pages-title' );
581 $out->addWikiMsg(
'tpt-new-pages', count( $pages ) );
582 $out->addHTML( $this->getPageList( $pages,
'proposed' ) );
585 $pages = $types[
'broken'];
587 $out->wrapWikiMsg(
'== $1 ==',
'tpt-other-pages-title' );
588 $out->addWikiMsg(
'tpt-other-pages', count( $pages ) );
589 $out->addHTML( $this->getPageList( $pages,
'broken' ) );
592 $pages = $types[
'outdated'];
594 $out->wrapWikiMsg(
'== $1 ==',
'tpt-outdated-pages-title' );
595 $out->addWikiMsg(
'tpt-outdated-pages', count( $pages ) );
596 $out->addHTML( $this->getPageList( $pages,
'outdated' ) );
599 $pages = $types[
'active'];
601 $out->wrapWikiMsg(
'== $1 ==',
'tpt-old-pages-title' );
602 $out->addWikiMsg(
'tpt-old-pages', count( $pages ) );
603 $out->addHTML( $this->getPageList( $pages,
'active' ) );
607 private function actionLinks( array $page,
string $type ): string {
609 static $messageCache = null;
610 if ( $messageCache ===
null ) {
612 'mark' => $this->msg(
'tpt-rev-mark' )->text(),
613 'mark-tooltip' => $this->msg(
'tpt-rev-mark-tooltip' )->text(),
614 'encourage' => $this->msg(
'tpt-rev-encourage' )->text(),
615 'encourage-tooltip' => $this->msg(
'tpt-rev-encourage-tooltip' )->text(),
616 'discourage' => $this->msg(
'tpt-rev-discourage' )->text(),
617 'discourage-tooltip' => $this->msg(
'tpt-rev-discourage-tooltip' )->text(),
618 'unmark' => $this->msg(
'tpt-rev-unmark' )->text(),
619 'unmark-tooltip' => $this->msg(
'tpt-rev-unmark-tooltip' )->text(),
620 'pipe-separator' => $this->msg(
'pipe-separator' )->escaped(),
626 $title = $page[
'title'];
627 $user = $this->getUser();
630 $js = [
'class' =>
'mw-translate-jspost' ];
632 if ( $user->isAllowed(
'pagetranslation' ) ) {
635 if ( $type !==
'broken' ) {
636 $actions[] = $this->getLinkRenderer()->makeKnownLink(
637 $this->getPageTitle(),
638 $messageCache[
'mark'],
639 [
'title' => $messageCache[
'mark-tooltip'] ],
642 'target' => $title->getPrefixedText(),
643 'revision' => $title->getLatestRevID(),
648 if ( $type !==
'proposed' ) {
649 if ( $page[
'discouraged'] ) {
650 $actions[] = $this->getLinkRenderer()->makeKnownLink(
651 $this->getPageTitle(),
652 $messageCache[
'encourage'],
653 [
'title' => $messageCache[
'encourage-tooltip'] ] + $js,
656 'target' => $title->getPrefixedText(),
661 $actions[] = $this->getLinkRenderer()->makeKnownLink(
662 $this->getPageTitle(),
663 $messageCache[
'discourage'],
664 [
'title' => $messageCache[
'discourage-tooltip'] ] + $js,
666 'do' =>
'discourage',
667 'target' => $title->getPrefixedText(),
673 $actions[] = $this->getLinkRenderer()->makeKnownLink(
674 $this->getPageTitle(),
675 $messageCache[
'unmark'],
676 [
'title' => $messageCache[
'unmark-tooltip'] ],
678 'do' => $type ===
'broken' ?
'unmark' :
'unlink',
679 'target' => $title->getPrefixedText(),
690 return '<div>' . implode( $messageCache[
'pipe-separator'], $actions ) .
'</div>';
699 private function validateUnitIds( Title $pageTitle, array $units ): bool {
701 $status = Status::newGood();
703 $ic = preg_quote( TranslationUnit::UNIT_MARKER_INVALID_CHARS,
'~' );
704 foreach ( $units as $s ) {
705 $unitStatus = Status::newGood();
708 $longestUnitTitle =
'Translations:' . $pageTitle->getPrefixedDBkey() .
'/' . $s->id .
'/xx-yyyyyyyyyy';
710 $this->titleParser->parseTitle( $longestUnitTitle );
711 }
catch ( MalformedTitleException $e ) {
712 if ( $e->getErrorMessage() ===
'title-invalid-too-long' ) {
714 'tpt-unit-title-too-long',
716 Message::numParam( strlen( $longestUnitTitle ) ),
717 $e->getErrorMessageParameters()[ 0 ],
718 $pageTitle->getPrefixedText()
721 $unitStatus->fatal(
'tpt-unit-title-invalid', $s->id, $e->getMessageObject() );
726 if ( $unitStatus->isGood() && preg_match(
"~[$ic]~", $s->id ) ) {
727 $unitStatus->fatal(
'tpt-invalid', $s->id );
733 if ( isset( $usedNames[$s->id] ) ) {
737 $unitStatus->fatal(
'tpt-duplicate', $s->id );
740 $status->merge( $unitStatus );
741 $usedNames[$s->id] =
true;
744 if ( $status->isOK() ) {
747 $this->getOutput()->addHTML(
749 $status->getHTML(
false,
false, $this->getLanguage() )
757 private function prepareTranslationUnits( TranslatablePage $page, ParserOutput $parse ): array {
760 $store = $this->translationUnitStoreFactory->getReader( $page->getTitle() );
761 $storedUnits = $store->getUnits();
762 $parsedUnits = $parse->units();
765 $displayTitle =
new TranslationUnit(
766 $page->getTitle()->getPrefixedText(),
767 TranslatablePage::DISPLAY_TITLE_UNIT_ID
769 $parsedUnits = [ TranslatablePage::DISPLAY_TITLE_UNIT_ID => $displayTitle ] + $parsedUnits;
772 foreach ( array_keys( $storedUnits ) as $key ) {
773 $highest = max( $highest, (
int)$key );
775 foreach ( $parsedUnits as $_ ) {
776 $highest = max( $highest, (
int)$_->id );
779 foreach ( $parsedUnits as $s ) {
782 if ( $s->id === TranslationUnit::NEW_UNIT_ID ) {
784 $s->id = (string)( ++$highest );
786 if ( isset( $storedUnits[$s->id] ) ) {
787 $storedText = $storedUnits[$s->id]->text;
788 if ( $s->text !== $storedText ) {
789 $s->type =
'changed';
790 $s->oldText = $storedText;
797 $deletedUnits = $storedUnits;
798 foreach ( $parsedUnits as $s ) {
799 unset( $deletedUnits[$s->id] );
802 return [ $parsedUnits, $deletedUnits ];
805 private function showPage(
806 TranslatablePage $page,
812 $out = $this->getOutput();
813 $out->addBacklinkSubtitle( $page->getTitle() );
814 $out->addWikiMsg(
'tpt-showpage-intro' );
818 'action' => $this->getPageTitle()->getLocalURL(),
819 'class' =>
'mw-tpt-sp-markform',
823 Xml::openElement(
'form', $formParams ) .
824 Html::hidden(
'do',
'mark' ) .
825 Html::hidden(
'title', $this->getPageTitle()->getPrefixedText() ) .
826 Html::hidden(
'revision', $page->getRevision() ) .
827 Html::hidden(
'target', $page->getTitle()->getPrefixedText() ) .
828 Html::hidden(
'token', $this->getContext()->getCsrfTokenSet()->getToken() )
831 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-oldnew' );
833 $diffOld = $this->msg(
'tpt-diff-old' )->escaped();
834 $diffNew = $this->msg(
'tpt-diff-new' )->escaped();
839 $defaultChecked = $firstMark || $page->hasPageDisplayTitle();
841 $sourceLanguage = $this->languageFactory->getLanguage( $page->getSourceLanguageCode() );
843 foreach ( $sections as $s ) {
844 if ( $s->id === TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
846 $s->type = $defaultChecked ? $s->type :
'new';
849 $checkBox =
new FieldLayout(
850 new CheckboxInputWidget( [
851 'name' =>
'translatetitle',
852 'selected' => $defaultChecked,
855 'label' => $this->msg(
'tpt-translate-title' )->text(),
857 'classes' => [
'mw-tpt-m-vertical' ]
860 $out->addHTML( $checkBox->toString() );
863 if ( $s->type ===
'new' ) {
865 $name = $this->msg(
'tpt-section-new', $s->id )->escaped();
867 $name = $this->msg(
'tpt-section', $s->id )->escaped();
870 if ( $s->type ===
'changed' ) {
872 $diff =
new DifferenceEngine();
873 $diff->setTextLanguage( $sourceLanguage );
874 $diff->setReducedLineNumbers();
876 $oldContent = ContentHandler::makeContent( $s->getOldText(), $diff->getTitle() );
877 $newContent = ContentHandler::makeContent( $s->getText(), $diff->getTitle() );
879 $diff->setContent( $oldContent, $newContent );
881 $text = $diff->getDiff( $diffOld, $diffNew );
882 $diffOld = $diffNew =
null;
883 $diff->showDiffStyle();
885 $id =
"tpt-sect-{$s->id}-action-nofuzzy";
886 $checkLabel =
new FieldLayout(
887 new CheckboxInputWidget( [
892 'label' => $this->msg(
'tpt-action-nofuzzy' )->text(),
894 'classes' => [
'mw-tpt-m-vertical' ]
897 $text = $checkLabel->toString() . $text;
899 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
902 # For changed text, the language is set by $diff->setTextLanguage()
903 $lang = $s->type ===
'changed' ? null : $sourceLanguage;
904 $out->addHTML( MessageWebImporter::makeSectionElement(
911 foreach ( $s->getIssues() as $issue ) {
912 $severity = $issue->getSeverity();
913 if ( $severity === TranslationUnitIssue::WARNING ) {
914 $box = Html::warningBox( $this->msg( $issue )->escaped() );
915 } elseif ( $severity === TranslationUnitIssue::ERROR ) {
916 $box = Html::errorBox( $this->msg( $issue )->escaped() );
918 throw new UnexpectedValueException(
919 "Unknown severity: $severity for key: {$issue->getKey()}"
923 $out->addHTML( $box );
927 if ( $deletedUnits ) {
929 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-deleted' );
931 foreach ( $deletedUnits as $s ) {
932 $name = $this->msg(
'tpt-section-deleted', $s->id )->escaped();
933 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
934 $out->addHTML( MessageWebImporter::makeSectionElement(
944 $markedTag = $page->getMarkedTag();
945 if ( $markedTag !==
null ) {
947 $newTemplate = $parse->sourcePageTemplateForDiffs();
948 $oldPage = TranslatablePage::newFromRevision(
952 $oldTemplate = $this->translatablePageParser
953 ->parse( $oldPage->getText() )
954 ->sourcePageTemplateForDiffs();
956 if ( $oldTemplate !== $newTemplate ) {
957 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-template' );
959 $diff =
new DifferenceEngine();
960 $diff->setTextLanguage( $sourceLanguage );
962 $oldContent = ContentHandler::makeContent( $oldTemplate, $diff->getTitle() );
963 $newContent = ContentHandler::makeContent( $newTemplate, $diff->getTitle() );
965 $diff->setContent( $oldContent, $newContent );
967 $text = $diff->getDiff(
968 $this->msg(
'tpt-diff-old' )->escaped(),
969 $this->msg(
'tpt-diff-new' )->escaped()
971 $diff->showDiffStyle();
972 $diff->setReducedLineNumbers();
974 $out->addHTML( Xml::tags(
'div', [], $text ) );
978 if ( !$hasChanges ) {
979 $out->wrapWikiMsg( Html::successBox(
'$1' ),
'tpt-mark-nochanges' );
982 $this->priorityLanguagesForm( $page );
986 $this->templateTransclusionForm( $page->supportsTransclusion() ?? $firstMark );
989 $page->getMessageGroupId(),
'version', self::DEFAULT_SYNTAX_VERSION
991 $this->syntaxVersionForm( $version, $firstMark );
993 $submitButton =
new FieldLayout(
994 new ButtonInputWidget( [
995 'label' => $this->msg(
'tpt-submit' )->text(),
997 'flags' => [
'primary',
'progressive' ],
1005 $out->addHTML( $submitButton->toString() );
1006 $out->addHTML(
'</form>' );
1009 private function priorityLanguagesForm( TranslatablePage $page ): void {
1010 $groupId = $page->getMessageGroupId();
1011 $interfaceLanguage = $this->getLanguage()->getCode();
1012 $storedLanguages = (string)TranslateMetadata::get( $groupId,
'prioritylangs' );
1013 $default = $storedLanguages !==
'' ? explode(
',', $storedLanguages ) : [];
1016 $priorityReason = $priorityReason !==
false ? $priorityReason :
'';
1018 $form =
new FieldsetLayout( [
1021 new LanguagesMultiselectWidget( [
1022 'infusable' =>
true,
1023 'name' =>
'prioritylangs',
1024 'id' =>
'mw-translate-SpecialPageTranslation-prioritylangs',
1025 'languages' => Utilities::getLanguageNames( $interfaceLanguage ),
1026 'default' => $default,
1029 'label' => $this->msg(
'tpt-select-prioritylangs' )->text(),
1034 new CheckboxInputWidget( [
1035 'name' =>
'forcelimit',
1036 'selected' => TranslateMetadata::get( $groupId,
'priorityforce' ) ===
'on',
1039 'label' => $this->msg(
'tpt-select-prioritylangs-force' )->text(),
1040 'align' =>
'inline',
1044 new TextInputWidget( [
1045 'name' =>
'priorityreason',
1046 'value' => $priorityReason
1049 'label' => $this->msg(
'tpt-select-prioritylangs-reason' )->text(),
1057 $this->getOutput()->wrapWikiMsg(
'==$1==',
'tpt-sections-prioritylangs' );
1058 $this->getOutput()->addHTML( $form->toString() );
1061 private function syntaxVersionForm(
string $version,
bool $firstMark ): void {
1062 $out = $this->getOutput();
1064 if ( $version === self::LATEST_SYNTAX_VERSION || $firstMark ) {
1068 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-syntaxversion' );
1070 'tpt-syntaxversion-text',
1071 '<code>' . wfEscapeWikiText(
'<span lang="en" dir="ltr">...</span>' ) .
'</code>',
1072 '<code>' . wfEscapeWikiText(
'<translate nowrap>...</translate>' ) .
'</code>'
1075 $checkBox =
new FieldLayout(
1076 new CheckboxInputWidget( [
1077 'name' =>
'use-latest-syntax'
1080 'label' => $out->msg(
'tpt-syntaxversion-label' )->text(),
1081 'align' =>
'inline',
1085 $out->addHTML( $checkBox->toString() );
1088 private function templateTransclusionForm(
bool $supportsTransclusion ): void {
1089 $out = $this->getOutput();
1090 $out->wrapWikiMsg(
'==$1==',
'tpt-transclusion' );
1092 $checkBox =
new FieldLayout(
1093 new CheckboxInputWidget( [
1094 'name' =>
'transclusion',
1095 'selected' => $supportsTransclusion
1098 'label' => $out->msg(
'tpt-transclusion-label' )->text(),
1099 'align' =>
'inline',
1103 $out->addHTML( $checkBox->toString() );
1126 bool $updateVersion,
1130 $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $page->
getTitle() );
1131 $content = ContentHandler::makeContent(
1136 $status = $wikiPage->doUserEditContent(
1139 $this->msg(
'tpt-mark-summary' )->inContentLanguage()->text(),
1140 EDIT_FORCE_BOT | EDIT_UPDATE
1143 if ( !$status->isOK() ) {
1144 return [
'tpt-edit-failed', $status->getWikiText() ];
1148 $newRevisionRecord = $status->value[
'revision-record'];
1151 $newRevisionId = $newRevisionRecord instanceof RevisionRecord
1152 ? $newRevisionRecord->getId()
1158 $newRevisionId ??= $page->
getTitle()->getLatestRevID();
1163 $maxid = (int)TranslateMetadata::get( $groupId,
'maxid' );
1165 $pageId = $page->
getTitle()->getArticleID();
1167 foreach ( array_values( $sections ) as $index => $s ) {
1168 $maxid = max( $maxid, (
int)$s->id );
1169 $changed[] = $s->id;
1171 if ( $this->getRequest()->getCheck(
"tpt-sect-{$s->id}-action-nofuzzy" ) ) {
1177 'trs_page' => $pageId,
1178 'trs_key' => $s->id,
1179 'trs_text' => $s->getText(),
1180 'trs_order' => $index
1184 $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
1186 'translate_sections',
1187 [
'trs_page' => $page->
getTitle()->getArticleID() ],
1190 $dbw->insert(
'translate_sections', $inserts, __METHOD__ );
1191 TranslateMetadata::set( $groupId,
'maxid', $maxid );
1192 if ( $updateVersion ) {
1193 TranslateMetadata::set( $groupId,
'version', self::LATEST_SYNTAX_VERSION );
1196 $page->setTransclusion( $transclusion );
1199 MessageGroups::singleton()->recache();
1203 $newKeys = $group->makeGroupKeys( $changed );
1204 $this->messageIndex->storeInterim( $group, $newKeys );
1206 $job = UpdateTranslatablePageJob::newFromPage( $page, $sections );
1207 $this->jobQueueGroup->push( $job );
1209 $this->handlePriorityLanguages( $this->getRequest(), $page );
1212 $entry =
new ManualLogEntry(
'pagetranslation',
'mark' );
1213 $entry->setPerformer( $this->getUser() );
1214 $entry->setTarget( $page->
getTitle() );
1215 $entry->setParameters( [
1216 'revision' => $newRevisionId,
1217 'changed' => count( $changed ),
1219 $logid = $entry->insert();
1220 $entry->publish( $logid );
1223 $page->
getTitle()->invalidateCache();
1237 $npLangs = rtrim( trim( $request->getVal(
'prioritylangs',
'' ) ),
',' );
1238 $npLangs = str_replace(
"\n",
',', $npLangs );
1239 $npLangs = array_map(
'trim', explode(
',', $npLangs ) );
1240 $npLangs = array_unique( $npLangs );
1242 $npForce = $request->getCheck(
'forcelimit' ) ?
'on' :
'off';
1243 $npReason = trim( $request->getText(
'priorityreason' ) );
1246 $languages = $this->languageNameUtils->getLanguageNames();
1247 foreach ( $npLangs as $index => $language ) {
1248 if ( !array_key_exists( $language, $languages ) ) {
1249 unset( $npLangs[$index] );
1252 $npLangs = implode(
',', $npLangs );
1253 if ( $npLangs ===
'' ) {
1261 $opLangs = TranslateMetadata::get( $groupId,
'prioritylangs' );
1262 $opForce = TranslateMetadata::get( $groupId,
'priorityforce' );
1263 $opReason = TranslateMetadata::get( $groupId,
'priorityreason' );
1265 TranslateMetadata::set( $groupId,
'prioritylangs', $npLangs );
1266 TranslateMetadata::set( $groupId,
'priorityforce', $npForce );
1267 TranslateMetadata::set( $groupId,
'priorityreason', $npReason );
1269 if ( $opLangs !== $npLangs || $opForce !== $npForce || $opReason !== $npReason ) {
1270 $logComment = $npReason ===
false ?
'' : $npReason;
1272 'languages' => $npLangs,
1273 'force' => $npForce,
1274 'reason' => $npReason,
1277 $entry =
new ManualLogEntry(
'pagetranslation',
'prioritylanguages' );
1278 $entry->setPerformer( $this->getUser() );
1279 $entry->setTarget( $page->
getTitle() );
1280 $entry->setParameters( $params );
1281 $entry->setComment( $logComment );
1282 $logid = $entry->insert();
1283 $entry->publish( $logid );
1287 private function getPageList( array $pages,
string $type ): string {
1289 $tagsTextCache = [];
1291 $tagDiscouraged = $this->msg(
'tpt-tag-discouraged' )->escaped();
1292 $tagOldSyntax = $this->msg(
'tpt-tag-oldsyntax' )->escaped();
1293 $tagNoTransclusionSupport = $this->msg(
'tpt-tag-no-transclusion-support' )->escaped();
1295 foreach ( $pages as $page ) {
1296 $link = $this->getLinkRenderer()->makeKnownLink( $page[
'title'] );
1297 $acts = $this->actionLinks( $page, $type );
1299 if ( $page[
'discouraged'] ) {
1300 $tags[] = $tagDiscouraged;
1302 if ( $type !==
'proposed' ) {
1303 if ( $page[
'version'] !== self::LATEST_SYNTAX_VERSION ) {
1304 $tags[] = $tagOldSyntax;
1307 if ( $page[
'transclusion'] !==
'1' ) {
1308 $tags[] = $tagNoTransclusionSupport;
1315 $tagsKey = implode(
'', $tags );
1316 $tagsTextCache[$tagsKey] = $tagsTextCache[$tagsKey] ??
1317 $this->msg(
'parentheses' )
1318 ->rawParams( $this->getLanguage()->pipeList( $tags ) )
1321 $tagList = Html::rawElement(
1323 [
'class' =>
'mw-tpt-actions' ],
1324 $tagsTextCache[$tagsKey]
1328 $items[] =
"<li class='mw-tpt-pagelist-item'>$link $tagList $acts</li>";
1331 return '<ol>' . implode(
"", $items ) .
'</ol>';