3namespace MediaWiki\Extension\Translate\PageTranslation;
14use MediaWiki\CommentStore\CommentStoreComment;
22use MediaWiki\Html\Html;
23use MediaWiki\Languages\LanguageNameUtils;
24use MediaWiki\Linker\LinkTarget;
25use MediaWiki\Logger\LoggerFactory;
26use MediaWiki\MainConfigNames;
27use MediaWiki\MediaWikiServices;
28use MediaWiki\Page\PageIdentity;
29use MediaWiki\Revision\MutableRevisionRecord;
30use MediaWiki\Revision\RenderedRevision;
31use MediaWiki\Revision\RevisionRecord;
32use MediaWiki\Revision\SlotRecord;
33use MediaWiki\SpecialPage\SpecialPage;
34use MediaWiki\Status\Status;
35use MediaWiki\Storage\EditResult;
36use MediaWiki\StubObject\StubUserLang;
37use MediaWiki\Title\Title;
38use MediaWiki\User\User;
39use MediaWiki\User\UserIdentity;
49use Wikimedia\ScopedCallback;
61 private const PAGEPROP_HAS_LANGUAGES_TAG =
'translate-has-languages-tag';
63 public static $allowTargetEdit =
false;
67 public static $renderingContext =
false;
69 private static $languageLinkData = [];
78 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
79 if ( $text === null ) {
87 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
93 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
94 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
95 $text = $translatablePageParser->cleanupTags( $text );
99 $title = MediaWikiServices::getInstance()
101 ->castFromPageReference( $wikitextParser->getPage() );
111 $wikitextParser->getOutput()->setPageProperty(
'translate-is-translation',
true );
114 self::$renderingContext =
true;
116 $name = $page->getPageDisplayTitle( $code );
118 $name = $wikitextParser->recursivePreprocess( $name );
120 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
121 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
122 $name = $langConv->convert( $name );
123 $wikitextParser->getOutput()->setDisplayTitle( $name );
125 self::$renderingContext =
false;
126 }
catch ( Exception $e ) {
127 LoggerFactory::getInstance(
'Translate' )->error(
128 'T302754 Failed to set display title for page {title}',
130 'title' => $title->getPrefixedDBkey(),
132 'pageid' => $title->getId(),
141 'languagecode' => $code,
142 'messagegroupid' => $page->getMessageGroupId(),
143 'sourcepagetitle' => [
144 'namespace' => $page->getTitle()->getNamespace(),
145 'dbkey' => $page->getTitle()->getDBkey()
149 $wikitextParser->getOutput()->setExtensionData(
'translate-translation-page', $extensionData );
151 $wikitextParser->getOutput()->setExtensionData(
'Translate-noeditsection',
true );
162 $translatablePageParser =
Services::getInstance()->getTranslatablePageParser();
164 if ( $translatablePageParser->containsMarkup( $text ) ) {
166 $parserOutput = $translatablePageParser->parse( $text );
168 $text = $parserOutput->sourcePageTextForRendering(
169 $wikitextParser->getTargetLanguage()
171 $wikitextParser->getOutput()->addModuleStyles( [
175 wfDebug(
'ParsingFailure caught; expected' );
181 $text = $unit->getTextForTrans();
196 if ( $out->getExtensionData(
'Translate-noeditsection' ) ) {
197 $options[
'enableSectionEditLinks'] =
false;
213 ?LinkTarget $contextLink,
214 ?LinkTarget $templateLink,
216 ?RevisionRecord &$revRecord
218 if ( !$templateLink ) {
222 $templateTitle = Title::castFromLinkTarget( $templateLink );
224 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
225 if ( $templateTranslationPage ) {
228 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
231 LoggerFactory::getInstance(
'Translate' )->warning(
232 "T323863: Could not fetch any revision record for '{groupid}'",
233 [
'groupid' => $templateTranslationPage->getMessageGroupId() ]
239 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
243 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
245 if ( !( $translatableTemplatePage->supportsTransclusion() ??
false ) ) {
250 $store = MediaWikiServices::getInstance()->getRevisionStore();
252 if ( $contextLink ) {
254 $templateTranslationTitle = $templateTitle->getSubpage(
255 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
258 if ( $templateTranslationTitle ) {
259 if ( $templateTranslationTitle->exists() ) {
261 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
266 $revRecord =
new MutableRevisionRecord( $templateTranslationTitle );
274 $sourceTemplateTitle = $templateTitle->getSubpage(
275 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
277 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
278 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
291 if ( TranslatablePage::isTranslationPage( $title ) ) {
292 [ , $code ] = Utilities::figureMessage( $title->getText() );
293 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
305 if ( TranslatablePage::isSourcePage( $title ) ) {
306 $msg = wfMessage(
'translate-edit-tag-warning' )->inContentLanguage();
307 if ( !$msg->isDisabled() ) {
308 $notices[
'translate-tag'] = $msg->parseAsBlock();
311 $notices[] = Html::warningBox(
312 wfMessage(
'tps-edit-sourcepage-text' )->parse(),
313 'translate-edit-documentation'
318 $request = RequestContext::getMain()->getRequest();
319 if ( $request->getVal(
'action' ) ===
'visualeditor' &&
320 $request->getVal(
'paction' ) !==
'wikitext'
322 $notices[] = Html::warningBox(
323 wfMessage(
'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
324 'translate-edit-documentation'
336 global $wgTranslatePageTranslationULS;
338 $title = $out->getTitle();
339 $isSource = TranslatablePage::isSourcePage( $title );
340 $isTranslation = TranslatablePage::isTranslationPage( $title );
342 if ( $isSource || $isTranslation ) {
343 if ( $wgTranslatePageTranslationULS ) {
344 $out->addModules(
'ext.translate.pagetranslation.uls' );
349 $out->addModuleStyles(
'ext.translate.edit.documentation.styles' );
352 if ( $isTranslation ) {
355 $out->addModuleStyles(
'ext.translate' );
358 $out->addJsConfigVars(
'wgTranslatePageTranslation', $isTranslation ?
'translation' :
'source' );
369 return !TranslatablePage::isTranslationPage( $out->getTitle() );
385 TextContent $content,
392 if ( $user->equals( FuzzyBot::getUser() ) ) {
402 $page = TranslatablePage::newFromTitle( $group->getTitle() );
405 if ( !$handle->
isDoc() ) {
407 DeferredUpdates::addCallableUpdate(
408 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
410 self::updateTranslationPage( $page, $code, $user, $flags, $summary,
null, $unitTitle );
416 private static function updateTranslationPage(
422 ?
string $triggerAction =
null,
423 ?Title $unitTitle =
null
425 $source = $page->getTitle();
426 $target = $source->getSubpage( $code );
427 $mwInstance = MediaWikiServices::getInstance();
430 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
433 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() :
null;
434 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
435 $job->setUser( $user );
436 $job->setSummary( $summary );
437 $job->setFlags( $flags );
438 $mwInstance->getJobQueueGroup()->push( $job );
442 $wikiPageFactory = $mwInstance->getWikiPageFactory();
443 foreach ( $pages as $title ) {
444 if ( $title->equals( $target ) ) {
449 $wikiPage = $wikiPageFactory->newFromTitle( $title );
450 $wikiPage->doPurge();
452 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
453 $sourceWikiPage->doPurge();
461 $variableIDs[] =
'translatablepage';
469 array &$variableCache,
474 switch ( $magicWordId ) {
475 case 'translatablepage':
476 $title = Title::castFromPageReference( $parser->getPage() );
477 $pageStatus = self::getTranslatablePageStatus( $title );
478 $ret = $pageStatus !==
null ? $pageStatus[
'page']->getTitle()->getPrefixedText() :
'';
479 $variableCache[$magicWordId] = $ret;
490 public static function languages( $data, $params, $parser ) {
491 global $wgPageTranslationLanguageList;
493 if ( $wgPageTranslationLanguageList ===
'sidebar-only' ) {
497 self::$renderingContext =
true;
498 $context =
new ScopedCallback(
static function () {
499 self::$renderingContext =
false;
504 $parser->getOutput()->setPageProperty( self::PAGEPROP_HAS_LANGUAGES_TAG,
true );
506 $currentTitle = $parser->getTitle();
507 $pageStatus = self::getTranslatablePageStatus( $currentTitle );
508 if ( !$pageStatus ) {
512 $page = $pageStatus[
'page' ];
513 $status = $pageStatus[
'languages' ];
520 $userLang = $parser->getOptions()->getUserLangObj();
521 $userLangCode = $userLang->getCode();
525 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
528 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
529 foreach ( $status as $code => $percent ) {
531 $name = Utilities::getLanguageName( $code, LanguageNameUtils::AUTONYMS );
534 $suffix = ( $code === $sourceLanguage ) ?
'' :
"/$code";
535 $targetTitleString = $pageTitle->getDBkey() . $suffix;
536 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
539 if ( $code === $userLangCode ) {
540 $classes[] =
'mw-pt-languages-ui';
543 $linker = $parser->getLinkRenderer();
544 $lang = $langFactory->getLanguage( $code );
545 if ( $currentTitle->equals( $subpage ) ) {
546 $classes[] =
'mw-pt-languages-selected';
547 $classes = array_merge( $classes, self::tpProgressIcon( (
float)$percent ) );
550 'lang' => $lang->getHtmlCode(),
551 'dir' => $lang->getDir(),
554 $contents = Html::element(
'span', $attribs, $name );
555 } elseif ( $subpage->isKnown() ) {
557 if ( !is_string( $pagename ) ) {
558 $pagename = $subpage->getPrefixedText();
561 $classes = array_merge( $classes, self::tpProgressIcon( (
float)$percent ) );
563 $title = wfMessage(
'tpt-languages-nonzero' )
564 ->page( $parser->getPage() )
565 ->inLanguage( $userLang )
566 ->params( $pagename )
567 ->numParams( 100 * $percent )
572 'lang' => $lang->getHtmlCode(),
573 'dir' => $lang->getDir(),
576 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
581 $specialTranslateTitle = SpecialPage::getTitleFor(
'Translate' );
591 'title' => wfMessage(
'tpt-languages-zero' )
592 ->page( $parser->getPage() )
593 ->inLanguage( $userLang )
596 'lang' => $lang->getHtmlCode(),
597 'dir' => $lang->getDir(),
599 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
601 $languages[ $name ] = Html::rawElement(
'li', [], $contents );
606 $languages = array_values( $languages );
607 $languages = implode(
"\n", $languages );
609 $out = Html::openElement(
'div', [
610 'class' =>
'mw-pt-languages noprint navigation-not-searchable',
611 'lang' => $userLang->getHtmlCode(),
612 'dir' => $userLang->getDir()
614 $out .= Html::rawElement(
'div', [
'class' =>
'mw-pt-languages-label' ],
615 wfMessage(
'tpt-languages-legend' )
616 ->page( $parser->getPage() )
617 ->inLanguage( $userLang )
620 $out .= Html::rawElement(
622 [
'class' =>
'mw-pt-languages-list' ],
625 $out .= Html::closeElement(
'div' );
627 $parser->getOutput()->addModuleStyles( [
628 'ext.translate.tag.languages',
640 private static function tpProgressIcon(
float $percent ) {
641 $classes = [
'mw-pt-progress' ];
643 if ( $percent < 15 ) {
644 $classes[] =
'mw-pt-progress--low';
645 } elseif ( $percent < 70 ) {
646 $classes[] =
'mw-pt-progress--med';
647 } elseif ( $percent < 100 ) {
648 $classes[] =
'mw-pt-progress--high';
650 $classes[] =
'mw-pt-progress--complete';
659 private static function getTranslatablePageStatus( ?Title $title ): ?array {
660 if ( $title === null ) {
664 $page = TranslatablePage::newFromTitle( $title );
666 $page = TranslatablePage::isTranslationPage( $title );
669 if ( $page ===
false || $page->
getMarkedTag() ===
null ) {
673 $status = $page->getTranslationPercentages();
678 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
680 $priorityLanguages = $messageGroupMetadata->get( $page->
getMessageGroupId(),
'prioritylangs' );
681 if ( (
string)$priorityLanguages !==
'' ) {
682 $status += array_fill_keys( explode(
',', $priorityLanguages ), 0 );
687 'languages' => $status
697 global $wgPageTranslationLanguageList;
699 if ( $wgPageTranslationLanguageList ===
'tag-only' ) {
703 if ( $wgPageTranslationLanguageList ===
'sidebar-fallback' ) {
704 $pageProps = MediaWikiServices::getInstance()->getPageProps();
705 $languageProp = $pageProps->getProperties( $title, self::PAGEPROP_HAS_LANGUAGES_TAG );
706 if ( $languageProp !== [] ) {
713 $status = self::getTranslatablePageStatus( $title );
718 self::$renderingContext =
true;
719 $context =
new ScopedCallback(
static function () {
720 self::$renderingContext =
false;
723 $page = $status[
'page' ];
724 $languages = $status[
'languages' ];
725 $mwServices = MediaWikiServices::getInstance();
726 $en = $mwServices->getLanguageFactory()->getLanguage(
'en' );
728 $newLanguageLinks = [];
731 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
732 foreach ( array_keys( $languages ) as $code ) {
733 $title = $page->getTitle()->getSubpage( $code );
734 $lb->addObj( $title );
737 $languageNameUtils = $mwServices->getLanguageNameUtils();
738 foreach ( $languages as $code => $percentage ) {
739 $title = $page->getTitle()->getSubpage( $code );
740 $key =
"x-pagetranslation:{$title->getPrefixedText()}";
741 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
743 if ( $title->exists() ) {
744 $href = $title->getLocalURL();
745 $classes = self::tpProgressIcon( (
float)$percentage );
746 $title = wfMessage(
'tpt-languages-nonzero' )
747 ->params( $translatedName )
748 ->numParams( 100 * $percentage );
750 $href = SpecialPage::getTitleFor(
'Translate' )->getLocalURL( [
751 'group' => $page->getMessageGroupId(),
754 $classes = [
'mw-pt-progress--none' ];
755 $title = wfMessage(
'tpt-languages-zero' );
758 self::$languageLinkData[ $key ] = [
761 'percentage' => $percentage,
762 'classes' => $classes,
763 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
767 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][
'autonym' ];
770 asort( $newLanguageLinks );
771 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
787 if ( !str_starts_with( $link[
'text'],
'x-pagetranslation:' ) ||
788 !isset( self::$languageLinkData[ $link[
'text'] ] )
793 $data = self::$languageLinkData[ $link[
'text' ] ];
795 $link[
'class' ] .=
' ' . implode(
' ', $data[
'classes' ] );
796 $link[
'href' ] = $data[
'href' ];
797 $link[
'text' ] = $data[
'autonym' ];
798 $link[
'title' ] = $data[
'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
799 $link[
'lang'] = LanguageCode::bcp47( $data[
'language' ] );
800 $link[
'hreflang'] = LanguageCode::bcp47( $data[
'language' ] );
802 $out->addModuleStyles(
'ext.translate.tag.languages' );
820 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
822 if ( $syntaxErrorStatus ) {
823 $status->merge( $syntaxErrorStatus );
824 return $syntaxErrorStatus->isGood();
830 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
832 if ( !$content instanceof WikitextContent || !$page ) {
836 $text = $content->getText();
839 $text = TextContent::normalizeLineEndings( $text );
840 $status = Status::newGood();
841 $parser = Services::getInstance()->getTranslatablePageParser();
842 if ( $parser->containsMarkup( $text ) ) {
844 $parser->parse( $text );
845 }
catch ( ParsingFailure $e ) {
846 $status->fatal( ...( $e->getMessageSpecification() ) );
865 RenderedRevision $renderedRevision,
867 CommentStoreComment $summary,
871 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
873 $status = self::tpSyntaxError(
874 $renderedRevision->getRevision()->getPage(),
879 $hookStatus->merge( $status );
880 return $status->isGood();
898 UserIdentity $userIdentity,
901 RevisionRecord $revisionRecord,
902 EditResult $editResult
904 $content = $wikiPage->getContent();
907 if ( $content instanceof WikitextContent ) {
908 $text = $content->getText();
914 $parser = Services::getInstance()->getTranslatablePageParser();
915 if ( $parser->containsMarkup( $text ) ) {
917 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
922 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
923 $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() );
942 $parentId = $rev->getParentId();
943 if ( $parentId === 0 || $parentId ===
null ) {
948 $prevRev = MediaWikiServices::getInstance()
949 ->getRevisionLookup()
950 ->getRevisionById( $parentId );
952 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
957 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
958 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
959 $bundle = $bundleFactory->getBundle( $title );
962 $bundleStore = $bundleFactory->getStore( $bundle );
963 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
985 if ( !$handle->isPageTranslation() || $action ===
'read' ) {
992 if ( $handle->isValid() ) {
993 $group = $handle->getGroup();
994 $groupId = $group->getId();
995 $permissionTitleCheck =
null;
998 $permissionTitleCheck = $group->getTitle();
1002 $permissionTitleCheck = Title::newFromID( $group->getBundlePageId() );
1005 if ( $permissionTitleCheck ) {
1007 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1008 if ( $permissionManager->isBlockedFrom( $user, $permissionTitleCheck ) ) {
1009 $block = $user->getBlock();
1011 $error =
new UserBlockedError( $block, $user );
1012 $errorMessage = $error->getMessageObject();
1013 $result = array_merge( [ $errorMessage->getKey() ], $errorMessage->getParams() );
1022 if ( $action !==
'create' ) {
1026 if ( !$handle->isValid() ) {
1032 $statsdDataFactory = MediaWikiServices::getInstance()->getStatsdDataFactory();
1033 $statsdDataFactory->increment(
'translate.slow_translatable_page_check' );
1034 $translatablePage = self::checkTranslatablePageSlow( $title );
1035 if ( $translatablePage ) {
1036 $groupId = $translatablePage->getMessageGroupId();
1037 $statsdDataFactory->increment(
'translate.slow_translatable_page_check_valid' );
1044 $error = self::getTranslationRestrictions( $handle, $groupId );
1045 $result = $error ?: $result;
1046 return $error === [];
1050 LoggerFactory::getInstance(
'Translate' )->info(
1051 'Unknown translation page: {title}',
1052 [
'title' => $title->getPrefixedDBkey() ]
1054 $result = [
'tpt-unknown-page' ];
1058 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?
TranslatablePage {
1060 $translationPageTitle = Title::newFromText(
1061 $parts[
'sourcepage' ] .
'/' . $parts[
'language' ]
1063 if ( !$translationPageTitle ) {
1067 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1068 if ( !$translatablePage ) {
1072 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1073 $store = $factory->getReader( $translatablePage->getTitle() );
1074 $units = $store->getNames();
1076 if ( !in_array( $parts[
'section' ], $units ) ) {
1080 return $translatablePage;
1090 private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) {
1091 global $wgTranslateDocumentationLanguageCode;
1094 if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) {
1098 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
1100 $force = $messageGroupMetadata->get( $groupId,
'priorityforce' );
1101 if ( $force !==
'on' ) {
1106 $languages = $messageGroupMetadata->get( $groupId,
'prioritylangs' );
1107 $reason = $messageGroupMetadata->get( $groupId,
'priorityreason' );
1108 if ( !$languages ) {
1110 return [
'tpt-translation-restricted-no-priority-languages', $reason ];
1112 return [
'tpt-translation-restricted-no-priority-languages-no-reason' ];
1115 $filter = array_flip( explode(
',', $languages ) );
1116 if ( !isset( $filter[$handle->getCode()] ) ) {
1118 return [
'tpt-translation-restricted', $reason ];
1121 return [
'tpt-translation-restricted-no-reason' ];
1137 if ( self::$allowTargetEdit ) {
1142 'read',
'deletedtext',
'deletedhistory',
1143 'deleterevision',
'suppressrevision',
'viewsuppressed',
1147 $needsPageTranslationRight = in_array( $action, [
'delete',
'undelete' ] );
1148 if ( in_array( $action, $inclusionList ) ||
1149 ( $needsPageTranslationRight && $user->isAllowed(
'pagetranslation' ) )
1154 $page = TranslatablePage::isTranslationPage( $title );
1156 if ( $needsPageTranslationRight ) {
1157 $result = User::newFatalPermissionDeniedStatus(
'pagetranslation' )->getMessage();
1161 [ , $code ] = Utilities::figureMessage( $title->getText() );
1162 $mwService = MediaWikiServices::getInstance();
1164 $translationUrl = $mwService->getUrlUtils()->expand(
1170 ':' . $page->
getTitle()->getPrefixedText(),
1192 $title = $article->getTitle();
1193 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1194 $isDeletableBundle = $bundle && $bundle->isDeletable();
1195 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1196 $new = SpecialPage::getTitleFor(
1197 'PageTranslationDeletePage',
1198 $title->getPrefixedText()
1200 $out->redirect( $new->getFullURL() );
1214 if ( $article->getOldID() ) {
1218 $articleTitle = $article->getTitle();
1219 $transPage = TranslatablePage::isTranslationPage( $articleTitle );
1220 $context = $article->getContext();
1222 self::translationPageHeader( $context, $transPage );
1224 $viewTranslatablePage = Services::getInstance()->getTranslatablePageView();
1225 $user = $context->getUser();
1226 if ( $viewTranslatablePage->canDisplayTranslationSettingsBanner( $articleTitle, $user ) ) {
1227 $output = $context->getOutput();
1228 $pageUrl = SpecialPage::getTitleFor(
'PageTranslation' )->getFullURL( [
1230 'target' => $articleTitle->getPrefixedDBkey(),
1234 $context->msg(
'pt-cta-mark-translation', $pageUrl ),
1235 'translate-cta-pt-mark'
1239 self::sourcePageHeader( $context );
1244 private static function sourcePageHeader( IContextSource $context ) {
1245 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1247 $language = $context->getLanguage();
1248 $title = $context->getTitle();
1250 $page = TranslatablePage::newFromTitle( $title );
1254 $latest = $title->getLatestRevID();
1257 if ( $marked && $context->getUser()->isAllowed(
'translate' ) ) {
1258 $actions[] = self::getTranslateLink( $context, $page,
null );
1261 $hasChanges = $ready === $latest && $marked !== $latest;
1262 if ( $hasChanges ) {
1263 $diffUrl = $title->getFullURL( [
'oldid' => $marked,
'diff' => $latest ] );
1265 if ( $context->getUser()->isAllowed(
'pagetranslation' ) ) {
1266 $pageTranslation = SpecialPage::getTitleFor(
'PageTranslation' );
1267 $params = [
'target' => $title->getPrefixedText(),
'do' =>
'mark' ];
1269 if ( $marked ===
null ) {
1271 $linkDesc = $context->msg(
'translate-tag-markthis' )->text();
1272 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1274 $markUrl = $pageTranslation->getFullURL( $params );
1275 $actions[] = $context->msg(
'translate-tag-markthisagain', $diffUrl, $markUrl )
1279 $actions[] = $context->msg(
'translate-tag-hasnew', $diffUrl )->parse();
1283 if ( !count( $actions ) ) {
1287 $header = Html::rawElement(
1290 'class' =>
'mw-pt-translate-header noprint nomobile',
1291 'dir' => $language->getDir(),
1292 'lang' => $language->getHtmlCode(),
1294 $language->semicolonList( $actions )
1297 $context->getOutput()->addHTML( $header );
1300 private static function getTranslateLink(
1301 IContextSource $context,
1302 TranslatablePage $page,
1305 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1307 return $linker->makeKnownLink(
1308 SpecialPage::getTitleFor(
'Translate' ),
1309 $context->msg(
'translate-tag-translate-link-desc' )->text(),
1312 'group' => $page->getMessageGroupId(),
1313 'language' => $langCode,
1320 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1321 global $wgTranslateKeepOutdatedTranslations;
1323 $title = $context->getTitle();
1324 if ( !$title->exists() ) {
1328 [ , $code ] = Utilities::figureMessage( $title->getText() );
1331 $pers = $page->getTranslationPercentages();
1333 if ( isset( $pers[$code] ) ) {
1334 $per = $pers[$code] * 100;
1337 $language = $context->getLanguage();
1338 $output = $context->getOutput();
1340 if ( $page->getSourceLanguageCode() === $code ) {
1342 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1344 $mwService = MediaWikiServices::getInstance();
1346 $translationUrl = $mwService->getUrlUtils()->expand(
1347 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1350 $msg = $context->msg(
'tpt-translation-intro',
1352 ':' . $page->getTitle()->getPrefixedText(),
1353 $language->formatNum( $per )
1357 $header = Html::rawElement(
1360 'class' =>
'mw-pt-translate-header noprint',
1361 'dir' => $language->getDir(),
1362 'lang' => $language->getHtmlCode(),
1367 $output->addHTML( $header );
1369 if ( $wgTranslateKeepOutdatedTranslations ) {
1370 $groupId = $page->getMessageGroupId();
1372 $stats = MessageGroupStats::forItem( $groupId, $code );
1373 if ( $stats[MessageGroupStats::FUZZY] ) {
1375 $wrap = Html::rawElement(
1378 'class' =>
'mw-pt-translate-header',
1379 'dir' => $language->getDir(),
1380 'lang' => $language->getHtmlCode()
1382 '<span class="mw-translate-fuzzy">$1</span>'
1385 $output->wrapWikiMsg( $wrap, [
'tpt-translation-intro-fuzzy' ] );
1395 $movePageSpec = $list[
'Movepage'] ??
null;
1398 if ( $movePageSpec ===
null ) {
1402 $list[
'Movepage'] = [
1403 'class' => MoveTranslatableBundleSpecialPage::class,
1406 'PermissionManager',
1407 'Translate:TranslatableBundleMover',
1408 'Translate:TranslatableBundleFactory',
1426 if ( $action ===
'read' ) {
1430 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1431 $key = $cache->makeKey(
'pt-lock', sha1( $title->getPrefixedText() ) );
1432 if ( $cache->get( $key ) ===
'locked' ) {
1433 $result = [
'pt-locked-page' ];
1449 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1451 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1452 if ( !$isTranslationPage
1453 && !TranslatablePage::isSourcePage( $out->getTitle() )
1459 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1461 $out->isArticle() &&
1462 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1464 $ptext = $out->getTitle()->getPrefixedText();
1465 $links = explode(
'/', $ptext );
1466 if ( count( $links ) > 1 ) {
1467 array_pop( $links );
1468 if ( $isTranslationPage ) {
1470 array_pop( $links );
1475 $lang = $skin->getLanguage();
1477 foreach ( $links as $link ) {
1478 $growinglink .= $link;
1480 $linkObj = Title::newFromText( $growinglink );
1482 if ( $linkObj && $linkObj->isKnown() ) {
1483 $getlink = $linker->makeKnownLink(
1484 SpecialPage::getTitleFor(
'MyLanguage', $growinglink ),
1491 $subpages .= $lang->getDirMarkEntity() . $skin->msg(
'pipe-separator' )->escaped();
1493 $subpages .=
'< ';
1496 $subpages .= $getlink;
1502 $growinglink .=
'/';
1519 $title = $skin->getTitle();
1521 $code = $handle->getCode();
1522 $page = TranslatablePage::isTranslationPage( $title );
1524 if ( !$page || $page->getSourceLanguageCode() === $code ) {
1528 $user = $skin->getUser();
1529 if ( isset( $tabs[
'views'][
'edit'] ) ) {
1531 $tabs[
'views'][
'edit'][
'text'] = $skin->msg(
'tpt-tab-translate' )->text();
1532 $tabs[
'views'][
'edit'][
'href'] = $page->getTranslationUrl( $code );
1533 } elseif ( $user->isAllowed(
'translate' ) ) {
1534 $mwInstance = MediaWikiServices::getInstance();
1535 $namespaceProtection = $mwInstance->getMainConfig()->get( MainConfigNames::NamespaceProtection );
1536 $permissionManager = $mwInstance->getPermissionManager();
1538 !$permissionManager->userHasAllRights(
1539 $user, ...(array)( $namespaceProtection[ NS_TRANSLATIONS ] ?? [] )
1546 'text' => $skin->msg(
'tpt-tab-translate' )->text(),
1547 'href' => $page->getTranslationUrl( $code ),
1551 $viewsourcePos = array_keys( array_keys( $tabs[
'views'] ),
'viewsource',
true )[0] ??
null;
1553 if ( $viewsourcePos !==
null ) {
1556 array_splice( $tabs[
'views'], $viewsourcePos, 1, [
'translate' => $tab ] );
1560 $tabs[
'views'][
'translate'] = $tab;
1578 LinkTarget $oldLinkTarget,
1579 LinkTarget $newLinkTarget,
1580 UserIdentity $userIdentity,
1584 RevisionRecord $revisionRecord
1586 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1591 if ( defined(
'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1595 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1596 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1598 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1601 if ( !$handle->isValid() || $handle->isDoc() ) {
1605 $group = $handle->getGroup();
1610 $language = $handle->getCode();
1613 if ( $language ===
'' ) {
1619 if ( $group !== $groupLast ) {
1620 $groupLast = $group;
1621 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1622 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1645 $title = $unit->getTitle();
1647 if ( !$handle->isValid() ) {
1651 $group = $handle->getGroup();
1656 $mwServices = MediaWikiServices::getInstance();
1659 $mwServices->getJobQueueGroup()->push(
1660 RebuildMessageGroupStatsJob::newRefreshGroupsJob( [ $group->getId() ] )
1664 if ( self::$isDeleteTranslatableBundleJobRunning ) {
1668 $target = $group->getTitle();
1669 $langCode = $handle->getCode();
1670 $fname = __METHOD__;
1672 $dbw = $mwServices->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1673 $callback =
function () use (
1682 $translationPageTitle = $target->getSubpage( $langCode );
1685 if ( !$translationPageTitle || !$translationPageTitle->exists( IDBAccessObject::READ_LATEST ) ) {
1689 $dbw->startAtomic( $fname );
1691 $page = TranslatablePage::newFromTitle( $target );
1693 if ( !$handle->isDoc() ) {
1694 $unitTitle = $handle->getTitle();
1696 self::updateTranslationPage(
1697 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1701 $dbw->endAtomic( $fname );
1704 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1712 foreach ( $titles as $index => $title ) {
1714 if ( Utilities::isTranslationPage( $handle ) ) {
1715 unset( $titles[ $index ] );
1725 foreach ( $titles as $index => $title ) {
1728 TranslatablePage::isSourcePage( $title ) ||
1729 Utilities::isTranslationPage( $handle )
1731 unset( $titles[ $index ] );
Hooks for page translation.
static tpSyntaxCheckForEditContent( $context, $content, $status, $summary)
Display nice error when editing content.
static tpSyntaxCheck(RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus)
When attempting to save, last resort.
static onReplaceTextFilterPageTitlesForRename(array &$titles)
Removes translatable and translation pages from the list of titles to be renamed Hook: ReplaceTextFil...
static bool $isDeleteTranslatableBundleJobRunning
State flag used by DeleteTranslatableBundleJob for performance optimizations.
static onMovePageTranslationUnits(LinkTarget $oldLinkTarget, LinkTarget $newLinkTarget, UserIdentity $userIdentity, int $oldid, int $newid, string $reason, RevisionRecord $revisionRecord)
Hook to update source and destination translation pages on moving translation units Hook: PageMoveCom...
static replaceMovePage(&$list)
Hook: SpecialPage_initList.
static onTitleGetEditNotices(Title $title, int $oldid, array &$notices)
Display an edit notice for translatable source pages if it's enabled Hook: TitleGetEditNotices.
static onParserGetVariableValueSwitch(Parser $parser, array &$variableCache, string $magicWordId, ?string &$ret, PPFrame $frame)
Hook: ParserGetVariableValueSwitch.
static onGetUserPermissionsErrorsExpensive(Title $title, User $user, $action, &$result)
Prevent creation of orphan translation units in Translations namespace.
static preprocessTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforePreprocess.
static onDeleteTranslationUnit(WikiPage $unit, User $user, $reason, $id, $content, $logEntry)
Hook to update translation page on deleting a translation unit Hook: ArticleDeleteComplete.
static preventDirectEditing(Title $title, User $user, $action, &$result)
Prevent editing of translation pages directly.
static renderTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforeInternalParse.
static onParserOutputPostCacheTransform(ParserOutput $out, &$text, array &$options)
Hook: ParserOutputPostCacheTransform.
static onSectionSave(WikiPage $wikiPage, User $user, TextContent $content, $summary, $minor, $flags, MessageHandle $handle)
This is triggered after an edit to translation unit page.
static fetchTranslatableTemplateAndTitle(?LinkTarget $contextLink, ?LinkTarget $templateLink, bool &$skip, ?RevisionRecord &$revRecord)
This sets &$revRecord to the revision of transcluded page translation if it exists,...
static onBeforePageDisplay(OutputPage $out, Skin $skin)
Hook: BeforePageDisplay.
static translatablePageHeader( $article, &$outputDone, &$pcache)
Hook: ArticleViewHeader.
static onPageContentLanguage(Title $title, &$pageLang)
Set the right page content language for translated pages ("Page/xx").
static lockedPagesCheck(Title $title, User $user, $action, &$result)
Hook: getUserPermissionsErrorsExpensive.
static updateTranstagOnNullRevisions(RevisionRecord $rev)
Page moving and page protection (and possibly other things) creates null revisions.
static onVisualEditorBeforeEditor(OutputPage $out, Skin $skin)
Hook: onVisualEditorBeforeEditor.
static replaceSubtitle(&$subpages, ?Skin $skin, OutputPage $out)
Hook: SkinSubPageSubtitle.
static formatLanguageLink(array &$link, Title $linkTitle, Title $pageTitle, OutputPage $out)
Hooks: SkinTemplateGetLanguageLink.
static onGetMagicVariableIDs(&$variableIDs)
Hook: GetMagicVariableIDs.
static languages( $data, $params, $parser)
static addLanguageLinks(Title $title, array &$languageLinks)
Hooks: LanguageLinks.
static disableDelete( $article, $out, &$reason)
Redirects the delete action to our own for translatable pages.
static onReplaceTextFilterPageTitlesForEdit(array &$titles)
Removes translation pages from the list of page titles to be edited Hook: ReplaceTextFilterPageTitles...
static translateTab(Skin $skin, array &$tabs)
Converts the edit tab (if exists) for translation pages to translate tab.
static addTranstagAfterSave(WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revisionRecord, EditResult $editResult)
Hook: PageSaveComplete.
Represents a parsing output produced by TranslatablePageParser.
Represents any kind of failure to parse a translatable page source code.
Mixed bag of methods related to translatable pages.
addReadyTag(int $revision)
Adds a tag which indicates that this page source is ready for marking for translation.
getMessageGroupId()
@inheritDoc
getMarkedTag()
Returns the latest revision which has marked tag, if any.
getTranslationPages()
@inheritDoc
getReadyTag()
Returns the latest revision which has ready tag, if any.
getTranslationUrl( $code=false)
Produces a link to translation view of a translation page.
getPageDisplayTitle(string $languageCode)
Get translated page title.
static isTranslationPage(Title $title)
This class represents one translation unit in a translatable page.
Wraps the translatable page sections into a message group.