3namespace MediaWiki\Extension\Translate\PageTranslation;
6use CommentStoreComment;
19use MediaWiki\Languages\LanguageNameUtils;
20use MediaWiki\Linker\LinkTarget;
21use MediaWiki\Logger\LoggerFactory;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Page\PageIdentity;
24use MediaWiki\Revision\MutableRevisionRecord;
25use MediaWiki\Revision\RenderedRevision;
26use MediaWiki\Revision\RevisionRecord;
27use MediaWiki\Revision\SlotRecord;
28use MediaWiki\Storage\EditResult;
29use MediaWiki\User\UserIdentity;
46use Wikimedia\ScopedCallback;
59 public static $allowTargetEdit =
false;
61 public static $jobQueueRunning =
false;
63 public static $renderingContext =
false;
65 private static $languageLinkData = [];
74 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
75 if ( $text === null ) {
83 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
89 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
90 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
91 $text = $translatablePageParser->cleanupTags( $text );
95 $title = MediaWikiServices::getInstance()
97 ->castFromPageReference( $wikitextParser->getPage() );
109 self::$renderingContext =
true;
110 [ , $code ] = Utilities::figureMessage( $title->getText() );
111 $name = $page->getPageDisplayTitle( $code );
113 $name = $wikitextParser->recursivePreprocess( $name );
115 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
116 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
117 $name = $langConv->convert( $name );
118 $wikitextParser->getOutput()->setDisplayTitle( $name );
120 self::$renderingContext =
false;
121 }
catch ( Exception $e ) {
122 LoggerFactory::getInstance(
'Translate' )->error(
123 'T302754 Failed to set display title for page {title}',
125 'title' => $title->getPrefixedDBkey(),
127 'pageid' => $title->getId(),
136 'languagecode' => $code,
137 'messagegroupid' => $page->getMessageGroupId(),
138 'sourcepagetitle' => [
139 'namespace' => $page->getTitle()->getNamespace(),
140 'dbkey' => $page->getTitle()->getDBkey()
144 $wikitextParser->getOutput()->setExtensionData(
145 'translate-translation-page', $extensionData
149 $wikitextParser->getOutput()->setExtensionData(
'Translate-noeditsection',
true );
160 $translatablePageParser =
Services::getInstance()->getTranslatablePageParser();
162 if ( $translatablePageParser->containsMarkup( $text ) ) {
164 $parserOutput = $translatablePageParser->parse( $text );
166 $text = $parserOutput->sourcePageTextForRendering(
167 $wikitextParser->getTargetLanguage()
169 $wikitextParser->getOutput()->addModuleStyles( [
173 wfDebug(
'ParsingFailure caught; expected' );
179 $text = $unit->getTextForTrans();
194 if ( $out->getExtensionData(
'Translate-noeditsection' ) ) {
195 $options[
'enableSectionEditLinks'] =
false;
211 ?LinkTarget $contextLink,
212 ?LinkTarget $templateLink,
214 ?RevisionRecord &$revRecord
216 if ( !$templateLink ) {
220 $templateTitle = Title::castFromLinkTarget( $templateLink );
222 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
223 if ( $templateTranslationPage ) {
226 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
232 LoggerFactory::getInstance(
'Translate' )->warning(
233 "T323863: Did not find message group for '{groupid}'",
234 [
'groupid' => $templateTranslationPage->getMessageGroupId() ]
242 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
246 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
248 if ( !( $translatableTemplatePage->supportsTransclusion() ??
false ) ) {
253 $store = MediaWikiServices::getInstance()->getRevisionStore();
255 if ( $contextLink ) {
257 $templateTranslationTitle = $templateTitle->getSubpage(
258 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
261 if ( $templateTranslationTitle ) {
262 if ( $templateTranslationTitle->exists() ) {
264 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
269 $revRecord =
new MutableRevisionRecord( $templateTranslationTitle );
277 $sourceTemplateTitle = $templateTitle->getSubpage(
278 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
280 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
281 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
294 if ( TranslatablePage::isTranslationPage( $title ) ) {
295 [ , $code ] = Utilities::figureMessage( $title->getText() );
296 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
308 if ( TranslatablePage::isSourcePage( $title ) ) {
309 $msg = wfMessage(
'translate-edit-tag-warning' )->inContentLanguage();
310 if ( !$msg->isDisabled() ) {
311 $notices[
'translate-tag'] = $msg->parseAsBlock();
314 $notices[] = Html::warningBox(
315 wfMessage(
'tps-edit-sourcepage-text' )->parse(),
316 'translate-edit-documentation'
321 $request = RequestContext::getMain()->getRequest();
322 if ( $request->getVal(
'action' ) ===
'visualeditor' &&
323 $request->getVal(
'paction' ) !==
'wikitext'
325 $notices[] = Html::warningBox(
326 wfMessage(
'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
327 'translate-edit-documentation'
340 global $wgTranslatePageTranslationULS;
342 $title = $out->getTitle();
343 $isSource = TranslatablePage::isSourcePage( $title );
344 $isTranslation = TranslatablePage::isTranslationPage( $title );
346 if ( $isSource || $isTranslation ) {
347 if ( $wgTranslatePageTranslationULS ) {
348 $out->addModules(
'ext.translate.pagetranslation.uls' );
353 $out->addModuleStyles(
'ext.translate.edit.documentation.styles' );
356 if ( $isTranslation ) {
359 $out->addModuleStyles(
'ext.translate' );
360 $out->addJsConfigVars(
'wgTranslatePageTranslation',
'translation' );
362 $out->addJsConfigVars(
'wgTranslatePageTranslation',
'source' );
376 return !TranslatablePage::isTranslationPage( $out->getTitle() );
393 TextContent $content,
400 if ( $user->equals( FuzzyBot::getUser() ) ) {
410 $page = TranslatablePage::newFromTitle( $group->getTitle() );
413 if ( !$handle->
isDoc() ) {
415 DeferredUpdates::addCallableUpdate(
416 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
418 self::updateTranslationPage( $page, $code, $user, $flags, $summary,
null, $unitTitle );
426 private static function updateTranslationPage(
432 ?
string $triggerAction =
null,
433 ?Title $unitTitle =
null
435 $source = $page->getTitle();
436 $target = $source->getSubpage( $code );
437 $mwInstance = MediaWikiServices::getInstance();
440 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
443 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() :
null;
444 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
445 $job->setUser( $user );
446 $job->setSummary( $summary );
447 $job->setFlags( $flags );
448 $mwInstance->getJobQueueGroup()->push( $job );
452 $wikiPageFactory = $mwInstance->getWikiPageFactory();
453 foreach ( $pages as $title ) {
454 if ( $title->equals( $target ) ) {
459 $wikiPage = $wikiPageFactory->newFromTitle( $title );
460 $wikiPage->doPurge();
462 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
463 $sourceWikiPage->doPurge();
472 public static function languages( $data, $params, $parser ) {
473 global $wgPageTranslationLanguageList;
475 if ( $wgPageTranslationLanguageList ===
'sidebar-only' ) {
479 self::$renderingContext =
true;
480 $context =
new ScopedCallback(
static function () {
481 self::$renderingContext =
false;
485 if ( $wgPageTranslationLanguageList ===
'sidebar-fallback' ) {
486 $parser->getOutput()->addLanguageLink(
'x-pagetranslation-tag' );
489 $currentTitle = $parser->getTitle();
490 $pageStatus = self::getTranslatablePageStatus( $currentTitle );
491 if ( !$pageStatus ) {
495 $page = $pageStatus[
'page' ];
496 $status = $pageStatus[
'languages' ];
503 $userLang = $parser->getOptions()->getUserLangObj();
504 $userLangCode = $userLang->getCode();
508 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
511 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
512 foreach ( $status as $code => $percent ) {
514 $name = Utilities::getLanguageName( $code, LanguageNameUtils::AUTONYMS );
517 $suffix = ( $code === $sourceLanguage ) ?
'' :
"/$code";
518 $targetTitleString = $pageTitle->getDBkey() . $suffix;
519 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
522 if ( $code === $userLangCode ) {
523 $classes[] =
'mw-pt-languages-ui';
526 $linker = $parser->getLinkRenderer();
527 $lang = $langFactory->getLanguage( $code );
528 if ( $currentTitle->equals( $subpage ) ) {
529 $classes[] =
'mw-pt-languages-selected';
530 $classes = array_merge( $classes, self::tpProgressIcon( (
float)$percent ) );
533 'lang' => $lang->getHtmlCode(),
534 'dir' => $lang->getDir(),
537 $contents = Html::element(
'span', $attribs, $name );
538 } elseif ( $subpage->isKnown() ) {
540 if ( !is_string( $pagename ) ) {
541 $pagename = $subpage->getPrefixedText();
544 $classes = array_merge( $classes, self::tpProgressIcon( (
float)$percent ) );
546 $title = wfMessage(
'tpt-languages-nonzero' )
547 ->inLanguage( $userLang )
548 ->params( $pagename )
549 ->numParams( 100 * $percent )
554 'lang' => $lang->getHtmlCode(),
555 'dir' => $lang->getDir(),
558 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
563 $specialTranslateTitle = SpecialPage::getTitleFor(
'Translate' );
573 'title' => wfMessage(
'tpt-languages-zero' )->inLanguage( $userLang )->text(),
575 'lang' => $lang->getHtmlCode(),
576 'dir' => $lang->getDir(),
578 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
580 $languages[ $name ] = Html::rawElement(
'li', [], $contents );
585 $languages = array_values( $languages );
586 $languages = implode(
"\n", $languages );
588 $out = Html::openElement(
'div', [
589 'class' =>
'mw-pt-languages noprint',
590 'lang' => $userLang->getHtmlCode(),
591 'dir' => $userLang->getDir()
593 $out .= Html::rawElement(
'div', [
'class' =>
'mw-pt-languages-label' ],
594 wfMessage(
'tpt-languages-legend' )->inLanguage( $userLang )->escaped()
596 $out .= Html::rawElement(
598 [
'class' =>
'mw-pt-languages-list' ],
601 $out .= Html::closeElement(
'div' );
603 $parser->getOutput()->addModuleStyles( [
604 'ext.translate.tag.languages',
616 private static function tpProgressIcon(
float $percent ) {
617 $classes = [
'mw-pt-progress' ];
619 if ( $percent < 20 ) {
620 $classes[] =
'mw-pt-progress--stub';
621 } elseif ( $percent < 40 ) {
622 $classes[] =
'mw-pt-progress--low';
623 } elseif ( $percent < 60 ) {
624 $classes[] =
'mw-pt-progress--med';
625 } elseif ( $percent < 80 ) {
626 $classes[] =
'mw-pt-progress--high';
628 $classes[] =
'mw-pt-progress--complete';
638 private static function getTranslatablePageStatus( Title $title ) {
640 $page = TranslatablePage::newFromTitle( $title );
642 $page = TranslatablePage::isTranslationPage( $title );
645 if ( $page ===
false || $page->
getMarkedTag() ===
null ) {
649 $status = $page->getTranslationPercentages();
658 if ( (
string)$priorityLangs !==
'' ) {
659 $filter = array_flip( explode(
',', $priorityLangs ) );
661 if ( $filter !==
null ) {
663 if ( $priorityForce ===
'on' ) {
666 $status = array_intersect_key( $status, $filter );
668 foreach ( $filter as $langCode => $value ) {
669 if ( !isset( $status[$langCode] ) ) {
671 $status[$langCode] = 0;
678 'languages' => $status
688 global $wgPageTranslationLanguageList;
690 $hasLanguagesTag =
false;
691 foreach ( $languageLinks as $index => $name ) {
692 if ( $name ===
'x-pagetranslation-tag' ) {
693 $hasLanguagesTag =
true;
694 unset( $languageLinks[ $index ] );
698 if ( $wgPageTranslationLanguageList ===
'tag-only' ) {
702 if ( $wgPageTranslationLanguageList ===
'sidebar-fallback' && $hasLanguagesTag ) {
708 $status = self::getTranslatablePageStatus( $title );
713 self::$renderingContext =
true;
714 $context =
new ScopedCallback(
static function () {
715 self::$renderingContext =
false;
718 $page = $status[
'page' ];
719 $languages = $status[
'languages' ];
720 $mwServices = MediaWikiServices::getInstance();
721 $en = $mwServices->getLanguageFactory()->getLanguage(
'en' );
723 $newLanguageLinks = [];
726 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
727 foreach ( array_keys( $languages ) as $code ) {
728 $title = $page->getTitle()->getSubpage( $code );
729 $lb->addObj( $title );
732 $languageNameUtils = $mwServices->getLanguageNameUtils();
733 foreach ( $languages as $code => $percentage ) {
734 $title = $page->getTitle()->getSubpage( $code );
735 $key =
"x-pagetranslation:{$title->getPrefixedText()}";
736 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
738 if ( $title->exists() ) {
739 $href = $title->getLocalURL();
740 $classes = self::tpProgressIcon( (
float)$percentage );
741 $title = wfMessage(
'tpt-languages-nonzero' )
742 ->params( $translatedName )
743 ->numParams( 100 * $percentage );
745 $href = SpecialPage::getTitleFor(
'Translate' )->getLocalURL( [
746 'group' => $page->getMessageGroupId(),
749 $classes = [
'mw-pt-progress--none' ];
750 $title = wfMessage(
'tpt-languages-zero' );
753 self::$languageLinkData[ $key ] = [
756 'percentage' => $percentage,
757 'classes' => $classes,
758 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
762 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][
'autonym' ];
765 asort( $newLanguageLinks );
766 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
782 if ( substr( $link[
'text' ], 0, 18 ) !==
'x-pagetranslation:' ) {
786 if ( !isset( self::$languageLinkData[ $link[
'text' ] ] ) ) {
790 $data = self::$languageLinkData[ $link[
'text' ] ];
792 $link[
'class' ] .=
' ' . implode(
' ', $data[
'classes' ] );
793 $link[
'href' ] = $data[
'href' ];
794 $link[
'text' ] = $data[
'autonym' ];
795 $link[
'title' ] = $data[
'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
796 $link[
'lang'] = LanguageCode::bcp47( $data[
'language' ] );
797 $link[
'hreflang'] = LanguageCode::bcp47( $data[
'language' ] );
799 $out->addModuleStyles(
'ext.translate.tag.languages' );
817 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
819 if ( $syntaxErrorStatus ) {
820 $status->merge( $syntaxErrorStatus );
821 return $syntaxErrorStatus->isGood();
827 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
829 if ( !$content instanceof WikitextContent || !$page ) {
833 $text = $content->getText();
836 $text = TextContent::normalizeLineEndings( $text );
837 $status = Status::newGood();
838 $parser = Services::getInstance()->getTranslatablePageParser();
839 if ( $parser->containsMarkup( $text ) ) {
841 $parser->parse( $text );
842 }
catch ( ParsingFailure $e ) {
843 $status->fatal( ...( $e->getMessageSpecification() ) );
862 RenderedRevision $renderedRevision,
864 CommentStoreComment $summary,
868 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
870 $status = self::tpSyntaxError(
871 $renderedRevision->getRevision()->getPage(),
876 $hookStatus->merge( $status );
877 return $status->isGood();
896 UserIdentity $userIdentity,
899 RevisionRecord $revisionRecord,
900 EditResult $editResult
902 $content = $wikiPage->getContent();
905 if ( $content instanceof WikitextContent ) {
906 $text = $content->getText();
912 $parser = Services::getInstance()->getTranslatablePageParser();
913 if ( $parser->containsMarkup( $text ) ) {
915 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
920 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
921 $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 ) {
1100 if ( $force !==
'on' ) {
1106 $filter = array_flip( explode(
',', $languages ) );
1107 if ( !isset( $filter[$handle->
getCode()] ) ) {
1110 return [
'tpt-translation-restricted', $reason ];
1113 return [
'tpt-translation-restricted-no-reason' ];
1129 if ( self::$allowTargetEdit ) {
1134 'read',
'deletedtext',
'deletedhistory',
1135 'deleterevision',
'suppressrevision',
'viewsuppressed',
1139 $needsPageTranslationRight = in_array( $action, [
'delete',
'undelete' ] );
1140 if ( in_array( $action, $inclusionList ) ||
1141 $needsPageTranslationRight && $user->isAllowed(
'pagetranslation' )
1146 $page = TranslatablePage::isTranslationPage( $title );
1148 if ( $needsPageTranslationRight ) {
1149 $result = User::newFatalPermissionDeniedStatus(
'pagetranslation' )->getMessage();
1153 [ , $code ] = Utilities::figureMessage( $title->getText() );
1154 $mwService = MediaWikiServices::getInstance();
1156 if ( method_exists( $mwService,
'getUrlUtils' ) ) {
1157 $translationUrl = $mwService->getUrlUtils()->expand(
1162 $translationUrl = wfExpandUrl( $page->
getTranslationUrl( $code ), PROTO_RELATIVE );
1167 ':' . $page->
getTitle()->getPrefixedText(),
1189 $title = $article->getTitle();
1190 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1191 $isDeletableBundle = $bundle && $bundle->isDeletable();
1192 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1193 $new = SpecialPage::getTitleFor(
1194 'PageTranslationDeletePage',
1195 $title->getPrefixedText()
1197 $out->redirect( $new->getFullURL() );
1212 if ( $article->getOldID() ) {
1216 $transPage = TranslatablePage::isTranslationPage( $article->getTitle() );
1217 $context = $article->getContext();
1219 self::translationPageHeader( $context, $transPage );
1222 self::sourcePageHeader( $context );
1228 private static function sourcePageHeader( IContextSource $context ) {
1229 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1231 $language = $context->getLanguage();
1232 $title = $context->getTitle();
1234 $page = TranslatablePage::newFromTitle( $title );
1238 $latest = $title->getLatestRevID();
1241 if ( $marked && $context->getUser()->isAllowed(
'translate' ) ) {
1242 $actions[] = self::getTranslateLink( $context, $page,
null );
1245 $hasChanges = $ready === $latest && $marked !== $latest;
1246 if ( $hasChanges ) {
1247 $diffUrl = $title->getFullURL( [
'oldid' => $marked,
'diff' => $latest ] );
1249 if ( $context->getUser()->isAllowed(
'pagetranslation' ) ) {
1250 $pageTranslation = SpecialPage::getTitleFor(
'PageTranslation' );
1251 $params = [
'target' => $title->getPrefixedText(),
'do' =>
'mark' ];
1253 if ( $marked ===
null ) {
1255 $linkDesc = $context->msg(
'translate-tag-markthis' )->text();
1256 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1258 $markUrl = $pageTranslation->getFullURL( $params );
1259 $actions[] = $context->msg(
'translate-tag-markthisagain', $diffUrl, $markUrl )
1263 $actions[] = $context->msg(
'translate-tag-hasnew', $diffUrl )->parse();
1267 if ( !count( $actions ) ) {
1271 $header = Html::rawElement(
1274 'class' =>
'mw-pt-translate-header noprint nomobile',
1275 'dir' => $language->getDir(),
1276 'lang' => $language->getHtmlCode(),
1278 $language->semicolonList( $actions )
1281 $context->getOutput()->addHTML( $header );
1284 private static function getTranslateLink(
1285 IContextSource $context,
1286 TranslatablePage $page,
1289 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1291 return $linker->makeKnownLink(
1292 SpecialPage::getTitleFor(
'Translate' ),
1293 $context->msg(
'translate-tag-translate-link-desc' )->text(),
1296 'group' => $page->getMessageGroupId(),
1297 'language' => $langCode,
1304 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1305 global $wgTranslateKeepOutdatedTranslations;
1307 $title = $context->getTitle();
1308 if ( !$title->exists() ) {
1312 [ , $code ] = Utilities::figureMessage( $title->getText() );
1315 $pers = $page->getTranslationPercentages();
1317 if ( isset( $pers[$code] ) ) {
1318 $per = $pers[$code] * 100;
1321 $language = $context->getLanguage();
1322 $output = $context->getOutput();
1324 if ( $page->getSourceLanguageCode() === $code ) {
1326 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1328 $mwService = MediaWikiServices::getInstance();
1330 if ( method_exists( $mwService,
'getUrlUtils' ) ) {
1331 $translationUrl = $mwService->getUrlUtils()->expand(
1332 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1336 $translationUrl = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE );
1339 $msg = $context->msg(
'tpt-translation-intro',
1341 ':' . $page->getTitle()->getPrefixedText(),
1342 $language->formatNum( $per )
1346 $header = Html::rawElement(
1349 'class' =>
'mw-pt-translate-header noprint',
1350 'dir' => $language->getDir(),
1351 'lang' => $language->getHtmlCode(),
1356 $output->addHTML( $header );
1358 if ( $wgTranslateKeepOutdatedTranslations ) {
1359 $groupId = $page->getMessageGroupId();
1362 if ( $stats[MessageGroupStats::FUZZY] ) {
1364 $wrap = Html::rawElement(
1367 'class' =>
'mw-pt-translate-header',
1368 'dir' => $language->getDir(),
1369 'lang' => $language->getHtmlCode()
1371 '<span class="mw-translate-fuzzy">$1</span>'
1374 $output->wrapWikiMsg( $wrap, [
'tpt-translation-intro-fuzzy' ] );
1385 $movePageSpec = $list[
'Movepage'] ??
null;
1388 if ( $movePageSpec ===
null ) {
1392 $list[
'Movepage'] = [
1393 'class' => MoveTranslatableBundleSpecialPage::class,
1396 'PermissionManager',
1397 'Translate:TranslatableBundleMover',
1398 'Translate:TranslatableBundleFactory'
1417 if ( $action ===
'read' ) {
1421 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1422 $key = $cache->makeKey(
'pt-lock', sha1( $title->getPrefixedText() ) );
1423 if ( $cache->get( $key ) ===
'locked' ) {
1424 $result = [
'pt-locked-page' ];
1440 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1442 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1443 if ( !$isTranslationPage
1444 && !TranslatablePage::isSourcePage( $out->getTitle() )
1450 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1452 $out->isArticle() &&
1453 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1455 $ptext = $out->getTitle()->getPrefixedText();
1456 if ( strpos( $ptext,
'/' ) !==
false ) {
1457 $links = explode(
'/', $ptext );
1458 array_pop( $links );
1459 if ( $isTranslationPage ) {
1461 array_pop( $links );
1466 $lang = $skin->getLanguage();
1468 foreach ( $links as $link ) {
1469 $growinglink .= $link;
1471 $linkObj = Title::newFromText( $growinglink );
1473 if ( is_object( $linkObj ) && $linkObj->isKnown() ) {
1474 $getlink = $linker->makeKnownLink(
1475 SpecialPage::getTitleFor(
'MyLanguage', $growinglink ),
1482 $subpages .= $lang->getDirMarkEntity() . $skin->msg(
'pipe-separator' )->escaped();
1484 $subpages .=
'< ';
1487 $subpages .= $getlink;
1493 $growinglink .=
'/';
1511 $title = $skin->getTitle();
1514 $page = TranslatablePage::isTranslationPage( $title );
1519 if ( $page->getSourceLanguageCode() === $code ) {
1523 if ( isset( $tabs[
'views'][
'edit'] ) ) {
1524 $tabs[
'views'][
'edit'][
'text'] = $skin->msg(
'tpt-tab-translate' )->text();
1525 $tabs[
'views'][
'edit'][
'href'] = $page->getTranslationUrl( $code );
1544 LinkTarget $oldLinkTarget,
1545 LinkTarget $newLinkTarget,
1546 UserIdentity $userIdentity,
1550 RevisionRecord $revisionRecord
1552 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1557 if ( defined(
'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1561 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1562 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1564 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1571 if ( $handle->
isDoc() ) {
1580 $language = $handle->
getCode();
1583 if ( (
string)$language ===
'' ) {
1589 if ( $group !== $groupLast ) {
1590 $groupLast = $group;
1591 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1592 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1616 if ( self::$jobQueueRunning ) {
1620 $title = $unit->getTitle();
1632 $target = $group->getTitle();
1633 $langCode = $handle->
getCode();
1634 $fname = __METHOD__;
1636 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1637 $callback =
function () use (
1646 $translationPageTitle = $target->getSubpage( $langCode );
1649 if ( !$translationPageTitle || !$translationPageTitle->exists( Title::READ_LATEST ) ) {
1653 $dbw->startAtomic( $fname );
1655 $page = TranslatablePage::newFromTitle( $target );
1657 MessageGroupStats::forItem(
1658 $page->getMessageGroupId(),
1660 MessageGroupStats::FLAG_NO_CACHE
1663 if ( !$handle->
isDoc() ) {
1666 self::updateTranslationPage(
1667 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1671 $dbw->endAtomic( $fname );
1674 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
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 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 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 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 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.
getMessageGroup()
Returns MessageGroup used for translating this page.
getPageDisplayTitle(string $languageCode)
Get translated page title.
static isTranslationPage(Title $title)
This class represents one translation unit in a translatable page.
This class abstract MessageGroup statistics calculation and storing.
static forItem( $id, $code, $flags=0)
Returns stats for given group in given language.
Class for pointing to messages, like Title class is for titles.
isDoc()
Determine whether the current handle is for message documentation.
getGroup()
Get the primary MessageGroup this message belongs to.
isValid()
Checks if the handle corresponds to a known message.
getTitle()
Get the original title.
getCode()
Returns the language code.
Wraps the translatable page sections into a message group.