59 private const LATEST_SYNTAX_VERSION =
'2';
60 private const DEFAULT_SYNTAX_VERSION =
'1';
61 private const DISPLAY_STATUS_MAPPING = [
62 TranslatablePageStatus::PROPOSED =>
'proposed',
63 TranslatablePageStatus::ACTIVE =>
'active',
64 TranslatablePageStatus::OUTDATED =>
'outdated',
65 TranslatablePageStatus::BROKEN =>
'broken'
68 private $languageNameUtils;
70 private $languageFactory;
72 private $translationUnitStoreFactory;
74 private $translatablePageParser;
76 private $linkBatchFactory;
78 private $jobQueueGroup;
80 private $loadBalancer;
82 private $messageIndex;
84 public function __construct(
85 LanguageNameUtils $languageNameUtils,
86 LanguageFactory $languageFactory,
89 LinkBatchFactory $linkBatchFactory,
90 JobQueueGroup $jobQueueGroup,
91 ILoadBalancer $loadBalancer,
94 parent::__construct(
'PageTranslation' );
95 $this->languageNameUtils = $languageNameUtils;
96 $this->languageFactory = $languageFactory;
97 $this->translationUnitStoreFactory = $translationUnitStoreFactory;
98 $this->translatablePageParser = $translatablePageParser;
99 $this->linkBatchFactory = $linkBatchFactory;
100 $this->jobQueueGroup = $jobQueueGroup;
101 $this->loadBalancer = $loadBalancer;
102 $this->messageIndex = $messageIndex;
105 public function doesWrites():
bool {
109 protected function getGroupName():
string {
110 return 'translation';
113 public function execute( $parameters ) {
116 $user = $this->getUser();
117 $request = $this->getRequest();
119 $target = $request->getText(
'target', $parameters ??
'' );
120 $revision = $request->getInt(
'revision', 0 );
121 $action = $request->getVal(
'do' );
122 $out = $this->getOutput();
123 $out->addModules(
'ext.translate.special.pagetranslation' );
124 $out->addHelpLink(
'Help:Extension:Translate/Page_translation_example' );
127 if ( $target ===
'' ) {
134 if ( !$user->isAllowed(
'pagetranslation' ) ) {
135 throw new PermissionsError(
'pagetranslation' );
138 $title = Title::newFromText( $target );
140 $out->wrapWikiMsg( Html::errorBox(
'$1' ), [
'tpt-badtitle', $target ] );
141 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
144 } elseif ( !$title->exists() ) {
146 Html::errorBox(
'$1' ),
147 [
'tpt-nosuchpage', $title->getPrefixedText() ]
149 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
155 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
156 if ( $permissionManager->isBlockedFrom( $user, $title, !$request->wasPosted() ) ) {
157 $block = $user->getBlock();
159 throw new UserBlockedError(
162 $this->getLanguage(),
167 throw new PermissionsError(
'pagetranslation', [
'badaccess-group0' ] );
172 $csrfTokenSet = $this->getContext()->getCsrfTokenSet();
173 if ( $request->wasPosted() && !$csrfTokenSet->matchTokenField(
'token' ) ) {
174 throw new PermissionsError(
'pagetranslation' );
177 if ( $action ===
'mark' ) {
179 $this->onActionMark( $title, $revision );
185 if ( !$request->wasPosted() ) {
186 if ( $action ===
'unlink' ) {
187 $this->showUnlinkConfirmation( $title );
191 'target' => $title->getPrefixedText(),
192 'revision' => $revision,
194 $this->showGenericConfirmation( $params );
200 if ( $action ===
'discourage' || $action ===
'encourage' ) {
202 $current = MessageGroups::getPriority( $id );
204 if ( $action ===
'encourage' ) {
207 $new =
'discouraged';
210 if ( $new !== $current ) {
211 MessageGroups::setPriority( $id, $new );
212 $entry =
new ManualLogEntry(
'pagetranslation', $action );
213 $entry->setPerformer( $user );
214 $entry->setTarget( $title );
215 $logid = $entry->insert();
216 $entry->publish( $logid );
223 $group = MessageGroups::getGroup( $id );
224 $sharedGroupIds = MessageGroups::getSharedGroups( $group );
225 if ( $sharedGroupIds !== [] ) {
227 $this->jobQueueGroup->push( $job );
236 if ( $action ===
'unlink' ) {
239 $content = ContentHandler::makeContent(
240 $page->getStrippedSourcePageText(),
244 $status = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title )->doUserEditContent(
247 $this->msg(
'tpt-unlink-summary' )->inContentLanguage()->text(),
248 EDIT_FORCE_BOT | EDIT_UPDATE
251 if ( !$status->isOK() ) {
253 Html::errorBox(
'$1' ),
254 [
'tpt-edit-failed', $status->getWikiText() ]
256 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
262 $this->unmarkPage( $page, $user );
264 Html::successBox(
'$1' ),
265 [
'tpt-unmarked', $title->getPrefixedText() ]
267 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
272 if ( $action ===
'unmark' ) {
274 $this->unmarkPage( $page, $user );
276 Html::successBox(
'$1' ),
277 [
'tpt-unmarked', $title->getPrefixedText() ]
279 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
283 protected function onActionMark( Title $title,
int $revision ):
void {
284 $request = $this->getRequest();
285 $out = $this->getOutput();
287 $out->addModuleStyles(
'ext.translate.specialpages.styles' );
289 if ( $revision === 0 ) {
291 $revision = (int)$title->getLatestRevID();
295 if ( $revision !== (
int)$title->getLatestRevID() ) {
297 $target = $title->getFullURL( [
'oldid' => $revision ] );
298 $link =
"<span class='plainlinks'>[$target $revision]</span>";
300 Html::warningBox(
'$1' ),
301 [
'tpt-oldrevision', $title->getPrefixedText(), $link ]
303 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
311 if ( $page->getReadyTag() !== $title->getLatestRevID() ) {
313 Html::errorBox(
'$1' ),
314 [
'tpt-notsuitable', $title->getPrefixedText(), Message::plaintextParam(
'<translate>' ) ]
316 $out->addWikiMsg(
'tpt-list-pages-in-translations' );
321 $firstMark = $page->getMarkedTag() ===
null;
323 $parse = $this->translatablePageParser->parse( $page->getText() );
324 [ $units, $deletedUnits ] = $this->prepareTranslationUnits( $page, $parse );
326 $error = $this->validateUnitIds( $units );
329 if ( !$error && $request->wasPosted() ) {
332 if ( !$request->getCheck(
'translatetitle' ) ) {
333 $units = array_filter( $units,
static function ( $s ) {
334 return $s->id !== TranslatablePage::DISPLAY_TITLE_UNIT_ID;
338 $setVersion = $firstMark || $request->getCheck(
'use-latest-syntax' );
339 $transclusion = $request->getCheck(
'transclusion' );
341 $err = $this->
markForTranslation( $page, $parse, $units, $setVersion, $transclusion );
344 call_user_func_array( [ $out,
'addWikiMsg' ], $err );
346 $this->showSuccess( $page, $firstMark, count( $units ) );
352 $this->showPage( $page, $parse, $units, $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 $tables = [
'page',
'revtag' ];
468 'MAX(rt_revision) AS rt_revision',
473 'rt_type' => [ RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG ],
476 'ORDER BY' =>
'page_namespace, page_title',
477 'GROUP BY' =>
'page_id, page_namespace, page_title, page_latest, rt_type',
480 return $dbr->select( $tables, $vars, $conds, __METHOD__, $options );
489 foreach ( $res as $r ) {
491 if ( !isset( $pages[$r->page_id] ) ) {
492 $pages[$r->page_id] = [];
493 $title = Title::newFromRow( $r );
494 $pages[$r->page_id][
'title'] = $title;
495 $pages[$r->page_id][
'latest'] = (int)$title->getLatestRevID();
499 $pages[$r->page_id][$tag] = (int)$r->rt_revision;
512 private function classifyPages( array $pages ): array {
514 $messageGroupIdsForPreload = [];
515 foreach ( $pages as $i => $page ) {
516 $id = TranslatablePage::getMessageGroupIdFromTitle( $page[
'title'] );
517 $messageGroupIdsForPreload[] = $id;
518 $pages[$i][
'groupid'] = $id;
521 $metadata = TranslateMetadata::loadBasicMetadataForTranslatablePages(
522 $messageGroupIdsForPreload,
523 [
'transclusion',
'version' ]
534 foreach ( $pages as $page ) {
535 $groupId = $page[
'groupid'];
536 $group = MessageGroups::getGroup( $groupId );
537 $page[
'discouraged'] = MessageGroups::getPriority( $group ) ===
'discouraged';
538 $page[
'version'] = $metadata[$groupId][
'version'] ?? self::DEFAULT_SYNTAX_VERSION;
539 $page[
'transclusion'] = $metadata[$groupId][
'transclusion'] ??
false;
542 $tpStatus = TranslatablePage::determineStatus(
543 $page[RevTagStore::TP_READY_TAG] ??
null,
544 $page[RevTagStore::TP_MARK_TAG] ??
null,
553 $out[self::DISPLAY_STATUS_MAPPING[$tpStatus->getId()]][] = $page;
559 public function listPages(): void {
560 $out = $this->getOutput();
562 $res = self::loadPagesFromDB();
563 $allPages = self::buildPageArray( $res );
564 if ( !count( $allPages ) ) {
565 $out->addWikiMsg(
'tpt-list-nopages' );
570 $lb = $this->linkBatchFactory->newLinkBatch();
571 $lb->setCaller( __METHOD__ );
572 foreach ( $allPages as $page ) {
573 $lb->addObj( $page[
'title'] );
577 $types = $this->classifyPages( $allPages );
579 $pages = $types[
'proposed'];
581 $out->wrapWikiMsg(
'== $1 ==',
'tpt-new-pages-title' );
582 $out->addWikiMsg(
'tpt-new-pages', count( $pages ) );
583 $out->addHTML( $this->getPageList( $pages,
'proposed' ) );
586 $pages = $types[
'broken'];
588 $out->wrapWikiMsg(
'== $1 ==',
'tpt-other-pages-title' );
589 $out->addWikiMsg(
'tpt-other-pages', count( $pages ) );
590 $out->addHTML( $this->getPageList( $pages,
'broken' ) );
593 $pages = $types[
'outdated'];
595 $out->wrapWikiMsg(
'== $1 ==',
'tpt-outdated-pages-title' );
596 $out->addWikiMsg(
'tpt-outdated-pages', count( $pages ) );
597 $out->addHTML( $this->getPageList( $pages,
'outdated' ) );
600 $pages = $types[
'active'];
602 $out->wrapWikiMsg(
'== $1 ==',
'tpt-old-pages-title' );
603 $out->addWikiMsg(
'tpt-old-pages', count( $pages ) );
604 $out->addHTML( $this->getPageList( $pages,
'active' ) );
608 private function actionLinks( array $page,
string $type ): string {
610 static $messageCache = null;
611 if ( $messageCache ===
null ) {
613 'mark' => $this->msg(
'tpt-rev-mark' )->text(),
614 'mark-tooltip' => $this->msg(
'tpt-rev-mark-tooltip' )->text(),
615 'encourage' => $this->msg(
'tpt-rev-encourage' )->text(),
616 'encourage-tooltip' => $this->msg(
'tpt-rev-encourage-tooltip' )->text(),
617 'discourage' => $this->msg(
'tpt-rev-discourage' )->text(),
618 'discourage-tooltip' => $this->msg(
'tpt-rev-discourage-tooltip' )->text(),
619 'unmark' => $this->msg(
'tpt-rev-unmark' )->text(),
620 'unmark-tooltip' => $this->msg(
'tpt-rev-unmark-tooltip' )->text(),
621 'pipe-separator' => $this->msg(
'pipe-separator' )->escaped(),
627 $title = $page[
'title'];
628 $user = $this->getUser();
631 $js = [
'class' =>
'mw-translate-jspost' ];
633 if ( $user->isAllowed(
'pagetranslation' ) ) {
636 if ( $type !==
'broken' ) {
637 $actions[] = $this->getLinkRenderer()->makeKnownLink(
638 $this->getPageTitle(),
639 $messageCache[
'mark'],
640 [
'title' => $messageCache[
'mark-tooltip'] ],
643 'target' => $title->getPrefixedText(),
644 'revision' => $title->getLatestRevID(),
649 if ( $type !==
'proposed' ) {
650 if ( $page[
'discouraged'] ) {
651 $actions[] = $this->getLinkRenderer()->makeKnownLink(
652 $this->getPageTitle(),
653 $messageCache[
'encourage'],
654 [
'title' => $messageCache[
'encourage-tooltip'] ] + $js,
657 'target' => $title->getPrefixedText(),
662 $actions[] = $this->getLinkRenderer()->makeKnownLink(
663 $this->getPageTitle(),
664 $messageCache[
'discourage'],
665 [
'title' => $messageCache[
'discourage-tooltip'] ] + $js,
667 'do' =>
'discourage',
668 'target' => $title->getPrefixedText(),
674 $actions[] = $this->getLinkRenderer()->makeKnownLink(
675 $this->getPageTitle(),
676 $messageCache[
'unmark'],
677 [
'title' => $messageCache[
'unmark-tooltip'] ],
679 'do' => $type ===
'broken' ?
'unmark' :
'unlink',
680 'target' => $title->getPrefixedText(),
691 return '<div>' . implode( $messageCache[
'pipe-separator'], $actions ) .
'</div>';
699 private function validateUnitIds( array $units ): bool {
701 $status = Status::newGood();
703 $ic = preg_quote( TranslationUnit::UNIT_MARKER_INVALID_CHARS,
'~' );
704 foreach ( $units as $s ) {
705 if ( preg_match(
"~[$ic]~", $s->id ) ) {
706 $status->fatal(
'tpt-invalid', $s->id );
712 if ( isset( $usedNames[$s->id] ) ) {
716 $status->fatal(
'tpt-duplicate', $s->id );
718 $usedNames[$s->id] =
true;
721 if ( $status->isOK() ) {
724 $this->getOutput()->addHTML(
726 $status->getHTML(
false,
false, $this->getLanguage() )
734 private function prepareTranslationUnits( TranslatablePage $page, ParserOutput $parse ): array {
737 $store = $this->translationUnitStoreFactory->getReader( $page->getTitle() );
738 $storedUnits = $store->getUnits();
739 $parsedUnits = $parse->units();
742 $displayTitle =
new TranslationUnit(
743 $page->getTitle()->getPrefixedText(),
744 TranslatablePage::DISPLAY_TITLE_UNIT_ID
746 $parsedUnits = [ TranslatablePage::DISPLAY_TITLE_UNIT_ID => $displayTitle ] + $parsedUnits;
749 foreach ( array_keys( $storedUnits ) as $key ) {
750 $highest = max( $highest, (
int)$key );
752 foreach ( $parsedUnits as $_ ) {
753 $highest = max( $highest, (
int)$_->id );
756 foreach ( $parsedUnits as $s ) {
759 if ( $s->id === TranslationUnit::NEW_UNIT_ID ) {
761 $s->id = (string)( ++$highest );
763 if ( isset( $storedUnits[$s->id] ) ) {
764 $storedText = $storedUnits[$s->id]->text;
765 if ( $s->text !== $storedText ) {
766 $s->type =
'changed';
767 $s->oldText = $storedText;
774 $deletedUnits = $storedUnits;
775 foreach ( $parsedUnits as $s ) {
776 unset( $deletedUnits[$s->id] );
779 return [ $parsedUnits, $deletedUnits ];
782 private function showPage(
783 TranslatablePage $page,
789 $out = $this->getOutput();
790 $out->setSubtitle( $this->getLinkRenderer()->makeKnownLink( $page->getTitle() ) );
791 $out->addWikiMsg(
'tpt-showpage-intro' );
795 'action' => $this->getPageTitle()->getLocalURL(),
796 'class' =>
'mw-tpt-sp-markform',
800 Xml::openElement(
'form', $formParams ) .
801 Html::hidden(
'do',
'mark' ) .
802 Html::hidden(
'title', $this->getPageTitle()->getPrefixedText() ) .
803 Html::hidden(
'revision', $page->getRevision() ) .
804 Html::hidden(
'target', $page->getTitle()->getPrefixedText() ) .
805 Html::hidden(
'token', $this->getContext()->getCsrfTokenSet()->getToken() )
808 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-oldnew' );
810 $diffOld = $this->msg(
'tpt-diff-old' )->escaped();
811 $diffNew = $this->msg(
'tpt-diff-new' )->escaped();
816 $defaultChecked = $firstMark || $page->hasPageDisplayTitle();
818 $sourceLanguage = $this->languageFactory->getLanguage( $page->getSourceLanguageCode() );
820 foreach ( $sections as $s ) {
821 if ( $s->id === TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
823 $s->type = $defaultChecked ? $s->type :
'new';
826 $checkBox =
new FieldLayout(
827 new CheckboxInputWidget( [
828 'name' =>
'translatetitle',
829 'selected' => $defaultChecked,
832 'label' => $this->msg(
'tpt-translate-title' )->text(),
834 'classes' => [
'mw-tpt-m-vertical' ]
837 $out->addHTML( $checkBox->toString() );
840 if ( $s->type ===
'new' ) {
842 $name = $this->msg(
'tpt-section-new', $s->id )->escaped();
844 $name = $this->msg(
'tpt-section', $s->id )->escaped();
847 if ( $s->type ===
'changed' ) {
849 $diff =
new DifferenceEngine();
850 $diff->setTextLanguage( $sourceLanguage );
851 $diff->setReducedLineNumbers();
853 $oldContent = ContentHandler::makeContent( $s->getOldText(), $diff->getTitle() );
854 $newContent = ContentHandler::makeContent( $s->getText(), $diff->getTitle() );
856 $diff->setContent( $oldContent, $newContent );
858 $text = $diff->getDiff( $diffOld, $diffNew );
859 $diffOld = $diffNew =
null;
860 $diff->showDiffStyle();
862 $id =
"tpt-sect-{$s->id}-action-nofuzzy";
863 $checkLabel =
new FieldLayout(
864 new CheckboxInputWidget( [
869 'label' => $this->msg(
'tpt-action-nofuzzy' )->text(),
871 'classes' => [
'mw-tpt-m-vertical' ]
874 $text = $checkLabel->toString() . $text;
876 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
879 # For changed text, the language is set by $diff->setTextLanguage()
880 $lang = $s->type ===
'changed' ? null : $sourceLanguage;
881 $out->addHTML( MessageWebImporter::makeSectionElement(
888 foreach ( $s->getIssues() as $issue ) {
889 $severity = $issue->getSeverity();
890 if ( $severity === TranslationUnitIssue::WARNING ) {
891 $box = Html::warningBox( $this->msg( $issue )->escaped() );
892 } elseif ( $severity === TranslationUnitIssue::ERROR ) {
893 $box = Html::errorBox( $this->msg( $issue )->escaped() );
895 throw new MWException(
896 "Unknown severity: $severity for key: {$issue->getKey()}"
900 $out->addHTML( $box );
904 if ( $deletedUnits ) {
906 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-deleted' );
908 foreach ( $deletedUnits as $s ) {
909 $name = $this->msg(
'tpt-section-deleted', $s->id )->escaped();
910 $text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
911 $out->addHTML( MessageWebImporter::makeSectionElement(
921 if ( $page->getMarkedTag() !==
null ) {
923 $newTemplate = $parse->sourcePageTemplateForDiffs();
924 $oldPage = TranslatablePage::newFromRevision(
926 $page->getMarkedTag()
928 $oldTemplate = $this->translatablePageParser
929 ->parse( $oldPage->getText() )
930 ->sourcePageTemplateForDiffs();
932 if ( $oldTemplate !== $newTemplate ) {
933 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-template' );
935 $diff =
new DifferenceEngine();
936 $diff->setTextLanguage( $sourceLanguage );
938 $oldContent = ContentHandler::makeContent( $oldTemplate, $diff->getTitle() );
939 $newContent = ContentHandler::makeContent( $newTemplate, $diff->getTitle() );
941 $diff->setContent( $oldContent, $newContent );
943 $text = $diff->getDiff(
944 $this->msg(
'tpt-diff-old' )->escaped(),
945 $this->msg(
'tpt-diff-new' )->escaped()
947 $diff->showDiffStyle();
948 $diff->setReducedLineNumbers();
950 $out->addHTML( Xml::tags(
'div', [], $text ) );
954 if ( !$hasChanges ) {
955 $out->wrapWikiMsg( Html::successBox(
'$1' ),
'tpt-mark-nochanges' );
958 $this->priorityLanguagesForm( $page );
962 $this->templateTransclusionForm( $page->supportsTransclusion() ?? $firstMark );
965 $page->getMessageGroupId(),
'version', self::DEFAULT_SYNTAX_VERSION
967 $this->syntaxVersionForm( $version, $firstMark );
969 $submitButton =
new FieldLayout(
970 new ButtonInputWidget( [
971 'label' => $this->msg(
'tpt-submit' )->text(),
973 'flags' => [
'primary',
'progressive' ],
981 $out->addHTML( $submitButton->toString() );
982 $out->addHTML(
'</form>' );
985 private function priorityLanguagesForm( TranslatablePage $page ): void {
986 $groupId = $page->getMessageGroupId();
987 $interfaceLanguage = $this->getLanguage()->getCode();
988 $storedLanguages = (string)TranslateMetadata::get( $groupId,
'prioritylangs' );
989 $default = $storedLanguages !==
'' ? explode(
',', $storedLanguages ) : [];
992 $priorityReason = $priorityReason !==
false ? $priorityReason :
'';
994 $form =
new FieldsetLayout( [
997 new LanguagesMultiselectWidget( [
999 'name' =>
'prioritylangs',
1000 'id' =>
'mw-translate-SpecialPageTranslation-prioritylangs',
1001 'languages' => Utilities::getLanguageNames( $interfaceLanguage ),
1002 'default' => $default,
1005 'label' => $this->msg(
'tpt-select-prioritylangs' )->text(),
1010 new CheckboxInputWidget( [
1011 'name' =>
'forcelimit',
1012 'selected' => TranslateMetadata::get( $groupId,
'priorityforce' ) ===
'on',
1015 'label' => $this->msg(
'tpt-select-prioritylangs-force' )->text(),
1016 'align' =>
'inline',
1020 new TextInputWidget( [
1021 'name' =>
'priorityreason',
1022 'value' => $priorityReason
1025 'label' => $this->msg(
'tpt-select-prioritylangs-reason' )->text(),
1033 $this->getOutput()->wrapWikiMsg(
'==$1==',
'tpt-sections-prioritylangs' );
1034 $this->getOutput()->addHTML( $form->toString() );
1037 private function syntaxVersionForm(
string $version,
bool $firstMark ): void {
1038 $out = $this->getOutput();
1040 if ( $version === self::LATEST_SYNTAX_VERSION || $firstMark ) {
1044 $out->wrapWikiMsg(
'==$1==',
'tpt-sections-syntaxversion' );
1046 'tpt-syntaxversion-text',
1047 '<code>' . wfEscapeWikiText(
'<span lang="en" dir="ltr">...</span>' ) .
'</code>',
1048 '<code>' . wfEscapeWikiText(
'<translate nowrap>...</translate>' ) .
'</code>'
1051 $checkBox =
new FieldLayout(
1052 new CheckboxInputWidget( [
1053 'name' =>
'use-latest-syntax'
1056 'label' => $out->msg(
'tpt-syntaxversion-label' )->text(),
1057 'align' =>
'inline',
1061 $out->addHTML( $checkBox->toString() );
1064 private function templateTransclusionForm(
bool $supportsTransclusion ): void {
1065 $out = $this->getOutput();
1066 $out->wrapWikiMsg(
'==$1==',
'tpt-transclusion' );
1068 $checkBox =
new FieldLayout(
1069 new CheckboxInputWidget( [
1070 'name' =>
'transclusion',
1071 'selected' => $supportsTransclusion
1074 'label' => $out->msg(
'tpt-transclusion-label' )->text(),
1075 'align' =>
'inline',
1079 $out->addHTML( $checkBox->toString() );
1102 bool $updateVersion,
1106 $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $page->
getTitle() );
1107 $content = ContentHandler::makeContent(
1112 $status = $wikiPage->doUserEditContent(
1115 $this->msg(
'tpt-mark-summary' )->inContentLanguage()->text(),
1116 EDIT_FORCE_BOT | EDIT_UPDATE
1119 if ( !$status->isOK() ) {
1120 return [
'tpt-edit-failed', $status->getWikiText() ];
1124 $newRevisionRecord = $status->value[
'revision-record'];
1127 if ( $newRevisionRecord instanceof RevisionRecord ) {
1128 $newRevisionId = $newRevisionRecord->getId();
1130 $newRevisionId =
null;
1136 if ( $newRevisionId ===
null ) {
1137 $newRevisionId = $page->
getTitle()->getLatestRevID();
1143 $maxid = (int)TranslateMetadata::get( $groupId,
'maxid' );
1145 $pageId = $page->
getTitle()->getArticleID();
1147 foreach ( array_values( $sections ) as $index => $s ) {
1148 $maxid = max( $maxid, (
int)$s->id );
1149 $changed[] = $s->id;
1151 if ( $this->getRequest()->getCheck(
"tpt-sect-{$s->id}-action-nofuzzy" ) ) {
1157 'trs_page' => $pageId,
1158 'trs_key' => $s->id,
1159 'trs_text' => $s->getText(),
1160 'trs_order' => $index
1164 $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
1166 'translate_sections',
1167 [
'trs_page' => $page->
getTitle()->getArticleID() ],
1170 $dbw->insert(
'translate_sections', $inserts, __METHOD__ );
1171 TranslateMetadata::set( $groupId,
'maxid', $maxid );
1172 if ( $updateVersion ) {
1173 TranslateMetadata::set( $groupId,
'version', self::LATEST_SYNTAX_VERSION );
1176 $page->setTransclusion( $transclusion );
1179 MessageGroups::singleton()->recache();
1183 $newKeys = $group->makeGroupKeys( $changed );
1184 $this->messageIndex->storeInterim( $group, $newKeys );
1186 $job = UpdateTranslatablePageJob::newFromPage( $page, $sections );
1187 $this->jobQueueGroup->push( $job );
1189 $this->handlePriorityLanguages( $this->getRequest(), $page );
1192 $entry =
new ManualLogEntry(
'pagetranslation',
'mark' );
1193 $entry->setPerformer( $this->getUser() );
1194 $entry->setTarget( $page->
getTitle() );
1195 $entry->setParameters( [
1196 'revision' => $newRevisionId,
1197 'changed' => count( $changed ),
1199 $logid = $entry->insert();
1200 $entry->publish( $logid );
1203 $page->
getTitle()->invalidateCache();
1217 $npLangs = rtrim( trim( $request->getVal(
'prioritylangs',
'' ) ),
',' );
1218 $npLangs = implode(
',', explode(
"\n", $npLangs ) );
1219 $npLangs = array_map(
'trim', explode(
',', $npLangs ) );
1220 $npLangs = array_unique( $npLangs );
1222 $npForce = $request->getCheck(
'forcelimit' ) ?
'on' :
'off';
1223 $npReason = trim( $request->getText(
'priorityreason' ) );
1226 $languages = $this->languageNameUtils->getLanguageNames();
1227 foreach ( $npLangs as $index => $language ) {
1228 if ( !array_key_exists( $language, $languages ) ) {
1229 unset( $npLangs[$index] );
1232 $npLangs = implode(
',', $npLangs );
1233 if ( $npLangs ===
'' ) {
1241 $opLangs = TranslateMetadata::get( $groupId,
'prioritylangs' );
1242 $opForce = TranslateMetadata::get( $groupId,
'priorityforce' );
1243 $opReason = TranslateMetadata::get( $groupId,
'priorityreason' );
1245 TranslateMetadata::set( $groupId,
'prioritylangs', $npLangs );
1246 TranslateMetadata::set( $groupId,
'priorityforce', $npForce );
1247 TranslateMetadata::set( $groupId,
'priorityreason', $npReason );
1249 if ( $opLangs !== $npLangs || $opForce !== $npForce || $opReason !== $npReason ) {
1250 $logComment = $npReason ===
false ?
'' : $npReason;
1252 'languages' => $npLangs,
1253 'force' => $npForce,
1254 'reason' => $npReason,
1257 $entry =
new ManualLogEntry(
'pagetranslation',
'prioritylanguages' );
1258 $entry->setPerformer( $this->getUser() );
1259 $entry->setTarget( $page->
getTitle() );
1260 $entry->setParameters( $params );
1261 $entry->setComment( $logComment );
1262 $logid = $entry->insert();
1263 $entry->publish( $logid );
1267 private function getPageList( array $pages,
string $type ): string {
1269 $tagsTextCache = [];
1271 $tagDiscouraged = $this->msg(
'tpt-tag-discouraged' )->escaped();
1272 $tagOldSyntax = $this->msg(
'tpt-tag-oldsyntax' )->escaped();
1273 $tagNoTransclusionSupport = $this->msg(
'tpt-tag-no-transclusion-support' )->escaped();
1275 foreach ( $pages as $page ) {
1276 $link = $this->getLinkRenderer()->makeKnownLink( $page[
'title'] );
1277 $acts = $this->actionLinks( $page, $type );
1279 if ( $page[
'discouraged'] ) {
1280 $tags[] = $tagDiscouraged;
1282 if ( $type !==
'proposed' ) {
1283 if ( $page[
'version'] !== self::LATEST_SYNTAX_VERSION ) {
1284 $tags[] = $tagOldSyntax;
1287 if ( $page[
'transclusion'] !==
'1' ) {
1288 $tags[] = $tagNoTransclusionSupport;
1295 $tagsKey = implode(
'', $tags );
1296 $tagsTextCache[$tagsKey] = $tagsTextCache[$tagsKey] ??
1297 $this->msg(
'parentheses' )
1298 ->rawParams( $this->getLanguage()->pipeList( $tags ) )
1301 $tagList = Html::rawElement(
1303 [
'class' =>
'mw-tpt-actions' ],
1304 $tagsTextCache[$tagsKey]
1308 $items[] =
"<li>$link $tagList $acts</li>";
1311 return '<ol>' . implode(
"", $items ) .
'</ol>';