3namespace MediaWiki\Extension\Translate\PageTranslation;
6use CommentStoreComment;
19use MediaWiki\Languages\LanguageNameUtils;
20use MediaWiki\Linker\LinkTarget;
21use MediaWiki\Logger\LoggerFactory;
22use MediaWiki\MainConfigNames;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Page\PageIdentity;
25use MediaWiki\Revision\MutableRevisionRecord;
26use MediaWiki\Revision\RenderedRevision;
27use MediaWiki\Revision\RevisionRecord;
28use MediaWiki\Revision\SlotRecord;
29use MediaWiki\Storage\EditResult;
30use MediaWiki\User\UserIdentity;
48use Wikimedia\ScopedCallback;
61 public static $allowTargetEdit =
false;
63 public static $jobQueueRunning =
false;
65 public static $renderingContext =
false;
67 private static $languageLinkData = [];
76 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
77 if ( $text === null ) {
85 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
91 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
92 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
93 $text = $translatablePageParser->cleanupTags( $text );
97 $title = MediaWikiServices::getInstance()
99 ->castFromPageReference( $wikitextParser->getPage() );
110 self::$renderingContext =
true;
111 [ , $code ] = Utilities::figureMessage( $title->getText() );
112 $name = $page->getPageDisplayTitle( $code );
114 $name = $wikitextParser->recursivePreprocess( $name );
116 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
117 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
118 $name = $langConv->convert( $name );
119 $wikitextParser->getOutput()->setDisplayTitle( $name );
121 self::$renderingContext =
false;
122 }
catch ( Exception $e ) {
123 LoggerFactory::getInstance(
'Translate' )->error(
124 'T302754 Failed to set display title for page {title}',
126 'title' => $title->getPrefixedDBkey(),
128 'pageid' => $title->getId(),
137 'languagecode' => $code,
138 'messagegroupid' => $page->getMessageGroupId(),
139 'sourcepagetitle' => [
140 'namespace' => $page->getTitle()->getNamespace(),
141 'dbkey' => $page->getTitle()->getDBkey()
145 $wikitextParser->getOutput()->setExtensionData(
146 'translate-translation-page', $extensionData
150 $wikitextParser->getOutput()->setExtensionData(
'Translate-noeditsection',
true );
161 $translatablePageParser =
Services::getInstance()->getTranslatablePageParser();
163 if ( $translatablePageParser->containsMarkup( $text ) ) {
165 $parserOutput = $translatablePageParser->parse( $text );
167 $text = $parserOutput->sourcePageTextForRendering(
168 $wikitextParser->getTargetLanguage()
170 $wikitextParser->getOutput()->addModuleStyles( [
174 wfDebug(
'ParsingFailure caught; expected' );
180 $text = $unit->getTextForTrans();
195 if ( $out->getExtensionData(
'Translate-noeditsection' ) ) {
196 $options[
'enableSectionEditLinks'] =
false;
212 ?LinkTarget $contextLink,
213 ?LinkTarget $templateLink,
215 ?RevisionRecord &$revRecord
217 if ( !$templateLink ) {
221 $templateTitle = Title::castFromLinkTarget( $templateLink );
223 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
224 if ( $templateTranslationPage ) {
227 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
230 LoggerFactory::getInstance(
'Translate' )->warning(
231 "T323863: Could not fetch any revision record for '{groupid}'",
232 [
'groupid' => $templateTranslationPage->getMessageGroupId() ]
238 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
242 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
244 if ( !( $translatableTemplatePage->supportsTransclusion() ??
false ) ) {
249 $store = MediaWikiServices::getInstance()->getRevisionStore();
251 if ( $contextLink ) {
253 $templateTranslationTitle = $templateTitle->getSubpage(
254 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
257 if ( $templateTranslationTitle ) {
258 if ( $templateTranslationTitle->exists() ) {
260 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
265 $revRecord =
new MutableRevisionRecord( $templateTranslationTitle );
273 $sourceTemplateTitle = $templateTitle->getSubpage(
274 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
276 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
277 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
290 if ( TranslatablePage::isTranslationPage( $title ) ) {
291 [ , $code ] = Utilities::figureMessage( $title->getText() );
292 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
304 if ( TranslatablePage::isSourcePage( $title ) ) {
305 $msg = wfMessage(
'translate-edit-tag-warning' )->inContentLanguage();
306 if ( !$msg->isDisabled() ) {
307 $notices[
'translate-tag'] = $msg->parseAsBlock();
310 $notices[] = Html::warningBox(
311 wfMessage(
'tps-edit-sourcepage-text' )->parse(),
312 'translate-edit-documentation'
317 $request = RequestContext::getMain()->getRequest();
318 if ( $request->getVal(
'action' ) ===
'visualeditor' &&
319 $request->getVal(
'paction' ) !==
'wikitext'
321 $notices[] = Html::warningBox(
322 wfMessage(
'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
323 'translate-edit-documentation'
335 global $wgTranslatePageTranslationULS;
337 $title = $out->getTitle();
338 $isSource = TranslatablePage::isSourcePage( $title );
339 $isTranslation = TranslatablePage::isTranslationPage( $title );
341 if ( $isSource || $isTranslation ) {
342 if ( $wgTranslatePageTranslationULS ) {
343 $out->addModules(
'ext.translate.pagetranslation.uls' );
348 $out->addModuleStyles(
'ext.translate.edit.documentation.styles' );
351 if ( $isTranslation ) {
354 $out->addModuleStyles(
'ext.translate' );
357 $out->addJsConfigVars(
'wgTranslatePageTranslation', $isTranslation ?
'translation' :
'source' );
368 return !TranslatablePage::isTranslationPage( $out->getTitle() );
384 TextContent $content,
391 if ( $user->equals( FuzzyBot::getUser() ) ) {
401 $page = TranslatablePage::newFromTitle( $group->getTitle() );
404 if ( !$handle->
isDoc() ) {
406 DeferredUpdates::addCallableUpdate(
407 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
409 self::updateTranslationPage( $page, $code, $user, $flags, $summary,
null, $unitTitle );
415 private static function updateTranslationPage(
421 ?
string $triggerAction =
null,
422 ?Title $unitTitle =
null
424 $source = $page->getTitle();
425 $target = $source->getSubpage( $code );
426 $mwInstance = MediaWikiServices::getInstance();
429 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
432 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() :
null;
433 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
434 $job->setUser( $user );
435 $job->setSummary( $summary );
436 $job->setFlags( $flags );
437 $mwInstance->getJobQueueGroup()->push( $job );
441 $wikiPageFactory = $mwInstance->getWikiPageFactory();
442 foreach ( $pages as $title ) {
443 if ( $title->equals( $target ) ) {
448 $wikiPage = $wikiPageFactory->newFromTitle( $title );
449 $wikiPage->doPurge();
451 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
452 $sourceWikiPage->doPurge();
460 $variableIDs[] =
'translatablepage';
468 array &$variableCache,
473 switch ( $magicWordId ) {
474 case 'translatablepage':
475 $title = Title::castFromPageReference( $parser->getPage() );
476 $pageStatus = self::getTranslatablePageStatus( $title );
477 $ret = $pageStatus !==
null ? $pageStatus[
'page']->getTitle()->getPrefixedText() :
'';
478 $variableCache[$magicWordId] = $ret;
489 public static function languages( $data, $params, $parser ) {
490 global $wgPageTranslationLanguageList;
492 if ( $wgPageTranslationLanguageList ===
'sidebar-only' ) {
496 self::$renderingContext =
true;
497 $context =
new ScopedCallback(
static function () {
498 self::$renderingContext =
false;
502 if ( $wgPageTranslationLanguageList ===
'sidebar-fallback' ) {
503 $parser->getOutput()->addLanguageLink(
'x-pagetranslation-tag' );
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 ->inLanguage( $userLang )
565 ->params( $pagename )
566 ->numParams( 100 * $percent )
571 'lang' => $lang->getHtmlCode(),
572 'dir' => $lang->getDir(),
575 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
580 $specialTranslateTitle = SpecialPage::getTitleFor(
'Translate' );
590 'title' => wfMessage(
'tpt-languages-zero' )->inLanguage( $userLang )->text(),
592 'lang' => $lang->getHtmlCode(),
593 'dir' => $lang->getDir(),
595 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
597 $languages[ $name ] = Html::rawElement(
'li', [], $contents );
602 $languages = array_values( $languages );
603 $languages = implode(
"\n", $languages );
605 $out = Html::openElement(
'div', [
606 'class' =>
'mw-pt-languages noprint',
607 'lang' => $userLang->getHtmlCode(),
608 'dir' => $userLang->getDir()
610 $out .= Html::rawElement(
'div', [
'class' =>
'mw-pt-languages-label' ],
611 wfMessage(
'tpt-languages-legend' )->inLanguage( $userLang )->escaped()
613 $out .= Html::rawElement(
615 [
'class' =>
'mw-pt-languages-list' ],
618 $out .= Html::closeElement(
'div' );
620 $parser->getOutput()->addModuleStyles( [
621 'ext.translate.tag.languages',
633 private static function tpProgressIcon(
float $percent ) {
634 $classes = [
'mw-pt-progress' ];
636 if ( $percent < 20 ) {
637 $classes[] =
'mw-pt-progress--stub';
638 } elseif ( $percent < 40 ) {
639 $classes[] =
'mw-pt-progress--low';
640 } elseif ( $percent < 60 ) {
641 $classes[] =
'mw-pt-progress--med';
642 } elseif ( $percent < 80 ) {
643 $classes[] =
'mw-pt-progress--high';
645 $classes[] =
'mw-pt-progress--complete';
654 private static function getTranslatablePageStatus( ?Title $title ): ?array {
655 if ( $title === null ) {
659 $page = TranslatablePage::newFromTitle( $title );
661 $page = TranslatablePage::isTranslationPage( $title );
664 if ( $page ===
false || $page->
getMarkedTag() ===
null ) {
668 $status = $page->getTranslationPercentages();
677 if ( (
string)$priorityLangs !==
'' ) {
678 $filter = array_flip( explode(
',', $priorityLangs ) );
680 if ( $filter !==
null ) {
682 if ( $priorityForce ===
'on' ) {
685 $status = array_intersect_key( $status, $filter );
687 foreach ( $filter as $langCode => $value ) {
688 if ( !isset( $status[$langCode] ) ) {
690 $status[$langCode] = 0;
697 'languages' => $status
707 global $wgPageTranslationLanguageList;
709 $hasLanguagesTag =
false;
710 foreach ( $languageLinks as $index => $name ) {
711 if ( $name ===
'x-pagetranslation-tag' ) {
712 $hasLanguagesTag =
true;
713 unset( $languageLinks[ $index ] );
717 if ( $wgPageTranslationLanguageList ===
'tag-only' ) {
721 if ( $wgPageTranslationLanguageList ===
'sidebar-fallback' && $hasLanguagesTag ) {
727 $status = self::getTranslatablePageStatus( $title );
732 self::$renderingContext =
true;
733 $context =
new ScopedCallback(
static function () {
734 self::$renderingContext =
false;
737 $page = $status[
'page' ];
738 $languages = $status[
'languages' ];
739 $mwServices = MediaWikiServices::getInstance();
740 $en = $mwServices->getLanguageFactory()->getLanguage(
'en' );
742 $newLanguageLinks = [];
745 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
746 foreach ( array_keys( $languages ) as $code ) {
747 $title = $page->getTitle()->getSubpage( $code );
748 $lb->addObj( $title );
751 $languageNameUtils = $mwServices->getLanguageNameUtils();
752 foreach ( $languages as $code => $percentage ) {
753 $title = $page->getTitle()->getSubpage( $code );
754 $key =
"x-pagetranslation:{$title->getPrefixedText()}";
755 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
757 if ( $title->exists() ) {
758 $href = $title->getLocalURL();
759 $classes = self::tpProgressIcon( (
float)$percentage );
760 $title = wfMessage(
'tpt-languages-nonzero' )
761 ->params( $translatedName )
762 ->numParams( 100 * $percentage );
764 $href = SpecialPage::getTitleFor(
'Translate' )->getLocalURL( [
765 'group' => $page->getMessageGroupId(),
768 $classes = [
'mw-pt-progress--none' ];
769 $title = wfMessage(
'tpt-languages-zero' );
772 self::$languageLinkData[ $key ] = [
775 'percentage' => $percentage,
776 'classes' => $classes,
777 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
781 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][
'autonym' ];
784 asort( $newLanguageLinks );
785 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
801 if ( !str_starts_with( $link[
'text'],
'x-pagetranslation:' ) ||
802 !isset( self::$languageLinkData[ $link[
'text'] ] )
807 $data = self::$languageLinkData[ $link[
'text' ] ];
809 $link[
'class' ] .=
' ' . implode(
' ', $data[
'classes' ] );
810 $link[
'href' ] = $data[
'href' ];
811 $link[
'text' ] = $data[
'autonym' ];
812 $link[
'title' ] = $data[
'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
813 $link[
'lang'] = LanguageCode::bcp47( $data[
'language' ] );
814 $link[
'hreflang'] = LanguageCode::bcp47( $data[
'language' ] );
816 $out->addModuleStyles(
'ext.translate.tag.languages' );
834 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
836 if ( $syntaxErrorStatus ) {
837 $status->merge( $syntaxErrorStatus );
838 return $syntaxErrorStatus->isGood();
844 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
846 if ( !$content instanceof WikitextContent || !$page ) {
850 $text = $content->getText();
853 $text = TextContent::normalizeLineEndings( $text );
854 $status = Status::newGood();
855 $parser = Services::getInstance()->getTranslatablePageParser();
856 if ( $parser->containsMarkup( $text ) ) {
858 $parser->parse( $text );
859 }
catch ( ParsingFailure $e ) {
860 $status->fatal( ...( $e->getMessageSpecification() ) );
879 RenderedRevision $renderedRevision,
881 CommentStoreComment $summary,
885 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
887 $status = self::tpSyntaxError(
888 $renderedRevision->getRevision()->getPage(),
893 $hookStatus->merge( $status );
894 return $status->isGood();
912 UserIdentity $userIdentity,
915 RevisionRecord $revisionRecord,
916 EditResult $editResult
918 $content = $wikiPage->getContent();
921 if ( $content instanceof WikitextContent ) {
922 $text = $content->getText();
928 $parser = Services::getInstance()->getTranslatablePageParser();
929 if ( $parser->containsMarkup( $text ) ) {
931 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
936 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
937 $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() );
956 $parentId = $rev->getParentId();
957 if ( $parentId === 0 || $parentId ===
null ) {
962 $prevRev = MediaWikiServices::getInstance()
963 ->getRevisionLookup()
964 ->getRevisionById( $parentId );
966 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
971 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
972 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
973 $bundle = $bundleFactory->getBundle( $title );
976 $bundleStore = $bundleFactory->getStore( $bundle );
977 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
999 if ( !$handle->isPageTranslation() || $action ===
'read' ) {
1006 if ( $handle->isValid() ) {
1007 $group = $handle->getGroup();
1008 $groupId = $group->getId();
1009 $permissionTitleCheck =
null;
1012 $permissionTitleCheck = $group->getTitle();
1016 $permissionTitleCheck = Title::newFromID( $group->getBundlePageId() );
1019 if ( $permissionTitleCheck ) {
1021 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1022 if ( $permissionManager->isBlockedFrom( $user, $permissionTitleCheck ) ) {
1023 $block = $user->getBlock();
1025 $error =
new UserBlockedError( $block, $user );
1026 $errorMessage = $error->getMessageObject();
1027 $result = array_merge( [ $errorMessage->getKey() ], $errorMessage->getParams() );
1036 if ( $action !==
'create' ) {
1040 if ( !$handle->isValid() ) {
1046 $statsdDataFactory = MediaWikiServices::getInstance()->getStatsdDataFactory();
1047 $statsdDataFactory->increment(
'translate.slow_translatable_page_check' );
1048 $translatablePage = self::checkTranslatablePageSlow( $title );
1049 if ( $translatablePage ) {
1050 $groupId = $translatablePage->getMessageGroupId();
1051 $statsdDataFactory->increment(
'translate.slow_translatable_page_check_valid' );
1058 $error = self::getTranslationRestrictions( $handle, $groupId );
1059 $result = $error ?: $result;
1060 return $error === [];
1064 LoggerFactory::getInstance(
'Translate' )->info(
1065 'Unknown translation page: {title}',
1066 [
'title' => $title->getPrefixedDBkey() ]
1068 $result = [
'tpt-unknown-page' ];
1072 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?
TranslatablePage {
1074 $translationPageTitle = Title::newFromText(
1075 $parts[
'sourcepage' ] .
'/' . $parts[
'language' ]
1077 if ( !$translationPageTitle ) {
1081 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1082 if ( !$translatablePage ) {
1086 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1087 $store = $factory->getReader( $translatablePage->getTitle() );
1088 $units = $store->getNames();
1090 if ( !in_array( $parts[
'section' ], $units ) ) {
1094 return $translatablePage;
1104 private static function getTranslationRestrictions(
MessageHandle $handle, $groupId ) {
1105 global $wgTranslateDocumentationLanguageCode;
1108 if ( $handle->
getCode() === $wgTranslateDocumentationLanguageCode ) {
1114 if ( $force !==
'on' ) {
1120 $filter = array_flip( explode(
',', $languages ) );
1121 if ( !isset( $filter[$handle->
getCode()] ) ) {
1124 return [
'tpt-translation-restricted', $reason ];
1127 return [
'tpt-translation-restricted-no-reason' ];
1143 if ( self::$allowTargetEdit ) {
1148 'read',
'deletedtext',
'deletedhistory',
1149 'deleterevision',
'suppressrevision',
'viewsuppressed',
1153 $needsPageTranslationRight = in_array( $action, [
'delete',
'undelete' ] );
1154 if ( in_array( $action, $inclusionList ) ||
1155 $needsPageTranslationRight && $user->isAllowed(
'pagetranslation' )
1160 $page = TranslatablePage::isTranslationPage( $title );
1162 if ( $needsPageTranslationRight ) {
1163 $result = User::newFatalPermissionDeniedStatus(
'pagetranslation' )->getMessage();
1167 [ , $code ] = Utilities::figureMessage( $title->getText() );
1168 $mwService = MediaWikiServices::getInstance();
1170 $translationUrl = $mwService->getUrlUtils()->expand(
1176 ':' . $page->
getTitle()->getPrefixedText(),
1198 $title = $article->getTitle();
1199 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1200 $isDeletableBundle = $bundle && $bundle->isDeletable();
1201 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1202 $new = SpecialPage::getTitleFor(
1203 'PageTranslationDeletePage',
1204 $title->getPrefixedText()
1206 $out->redirect( $new->getFullURL() );
1220 if ( $article->getOldID() ) {
1224 $transPage = TranslatablePage::isTranslationPage( $article->getTitle() );
1225 $context = $article->getContext();
1227 self::translationPageHeader( $context, $transPage );
1230 self::sourcePageHeader( $context );
1234 private static function sourcePageHeader( IContextSource $context ) {
1235 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1237 $language = $context->getLanguage();
1238 $title = $context->getTitle();
1240 $page = TranslatablePage::newFromTitle( $title );
1244 $latest = $title->getLatestRevID();
1247 if ( $marked && $context->getUser()->isAllowed(
'translate' ) ) {
1248 $actions[] = self::getTranslateLink( $context, $page,
null );
1251 $hasChanges = $ready === $latest && $marked !== $latest;
1252 if ( $hasChanges ) {
1253 $diffUrl = $title->getFullURL( [
'oldid' => $marked,
'diff' => $latest ] );
1255 if ( $context->getUser()->isAllowed(
'pagetranslation' ) ) {
1256 $pageTranslation = SpecialPage::getTitleFor(
'PageTranslation' );
1257 $params = [
'target' => $title->getPrefixedText(),
'do' =>
'mark' ];
1259 if ( $marked ===
null ) {
1261 $linkDesc = $context->msg(
'translate-tag-markthis' )->text();
1262 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1264 $markUrl = $pageTranslation->getFullURL( $params );
1265 $actions[] = $context->msg(
'translate-tag-markthisagain', $diffUrl, $markUrl )
1269 $actions[] = $context->msg(
'translate-tag-hasnew', $diffUrl )->parse();
1273 if ( !count( $actions ) ) {
1277 $header = Html::rawElement(
1280 'class' =>
'mw-pt-translate-header noprint nomobile',
1281 'dir' => $language->getDir(),
1282 'lang' => $language->getHtmlCode(),
1284 $language->semicolonList( $actions )
1287 $context->getOutput()->addHTML( $header );
1290 private static function getTranslateLink(
1291 IContextSource $context,
1292 TranslatablePage $page,
1295 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1297 return $linker->makeKnownLink(
1298 SpecialPage::getTitleFor(
'Translate' ),
1299 $context->msg(
'translate-tag-translate-link-desc' )->text(),
1302 'group' => $page->getMessageGroupId(),
1303 'language' => $langCode,
1310 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1311 global $wgTranslateKeepOutdatedTranslations;
1313 $title = $context->getTitle();
1314 if ( !$title->exists() ) {
1318 [ , $code ] = Utilities::figureMessage( $title->getText() );
1321 $pers = $page->getTranslationPercentages();
1323 if ( isset( $pers[$code] ) ) {
1324 $per = $pers[$code] * 100;
1327 $language = $context->getLanguage();
1328 $output = $context->getOutput();
1330 if ( $page->getSourceLanguageCode() === $code ) {
1332 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1334 $mwService = MediaWikiServices::getInstance();
1336 $translationUrl = $mwService->getUrlUtils()->expand(
1337 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1340 $msg = $context->msg(
'tpt-translation-intro',
1342 ':' . $page->getTitle()->getPrefixedText(),
1343 $language->formatNum( $per )
1347 $header = Html::rawElement(
1350 'class' =>
'mw-pt-translate-header noprint',
1351 'dir' => $language->getDir(),
1352 'lang' => $language->getHtmlCode(),
1357 $output->addHTML( $header );
1359 if ( $wgTranslateKeepOutdatedTranslations ) {
1360 $groupId = $page->getMessageGroupId();
1363 if ( $stats[MessageGroupStats::FUZZY] ) {
1365 $wrap = Html::rawElement(
1368 'class' =>
'mw-pt-translate-header',
1369 'dir' => $language->getDir(),
1370 'lang' => $language->getHtmlCode()
1372 '<span class="mw-translate-fuzzy">$1</span>'
1375 $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'
1415 if ( $action ===
'read' ) {
1419 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1420 $key = $cache->makeKey(
'pt-lock', sha1( $title->getPrefixedText() ) );
1421 if ( $cache->get( $key ) ===
'locked' ) {
1422 $result = [
'pt-locked-page' ];
1438 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1440 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1441 if ( !$isTranslationPage
1442 && !TranslatablePage::isSourcePage( $out->getTitle() )
1448 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1450 $out->isArticle() &&
1451 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1453 $ptext = $out->getTitle()->getPrefixedText();
1454 $links = explode(
'/', $ptext );
1455 if ( count( $links ) > 1 ) {
1456 array_pop( $links );
1457 if ( $isTranslationPage ) {
1459 array_pop( $links );
1464 $lang = $skin->getLanguage();
1466 foreach ( $links as $link ) {
1467 $growinglink .= $link;
1469 $linkObj = Title::newFromText( $growinglink );
1471 if ( $linkObj && $linkObj->isKnown() ) {
1472 $getlink = $linker->makeKnownLink(
1473 SpecialPage::getTitleFor(
'MyLanguage', $growinglink ),
1480 $subpages .= $lang->getDirMarkEntity() . $skin->msg(
'pipe-separator' )->escaped();
1482 $subpages .=
'< ';
1485 $subpages .= $getlink;
1491 $growinglink .=
'/';
1508 $title = $skin->getTitle();
1511 $page = TranslatablePage::isTranslationPage( $title );
1513 if ( !$page || $page->getSourceLanguageCode() === $code ) {
1517 $user = $skin->getUser();
1518 if ( isset( $tabs[
'views'][
'edit'] ) ) {
1520 $tabs[
'views'][
'edit'][
'text'] = $skin->msg(
'tpt-tab-translate' )->text();
1521 $tabs[
'views'][
'edit'][
'href'] = $page->getTranslationUrl( $code );
1522 } elseif ( $user->isAllowed(
'translate' ) ) {
1523 $mwInstance = MediaWikiServices::getInstance();
1524 $namespaceProtection = $mwInstance->getMainConfig()->get( MainConfigNames::NamespaceProtection );
1525 $permissionManager = $mwInstance->getPermissionManager();
1527 !$permissionManager->userHasAllRights(
1528 $user, ...(array)( $namespaceProtection[ NS_TRANSLATIONS ] ?? [] )
1535 'text' => $skin->msg(
'tpt-tab-translate' )->text(),
1536 'href' => $page->getTranslationUrl( $code ),
1540 $viewsourcePos = array_keys( array_keys( $tabs[
'views'] ),
'viewsource',
true )[0] ??
null;
1542 if ( $viewsourcePos !==
null ) {
1545 array_splice( $tabs[
'views'], $viewsourcePos, 1, [
'translate' => $tab ] );
1549 $tabs[
'views'][
'translate'] = $tab;
1567 LinkTarget $oldLinkTarget,
1568 LinkTarget $newLinkTarget,
1569 UserIdentity $userIdentity,
1573 RevisionRecord $revisionRecord
1575 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1580 if ( defined(
'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1584 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1585 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1587 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1599 $language = $handle->
getCode();
1602 if ( (
string)$language ===
'' ) {
1608 if ( $group !== $groupLast ) {
1609 $groupLast = $group;
1610 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1611 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1635 if ( self::$jobQueueRunning ) {
1639 $title = $unit->getTitle();
1650 $target = $group->getTitle();
1651 $langCode = $handle->
getCode();
1652 $fname = __METHOD__;
1654 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1655 $callback =
function () use (
1664 $translationPageTitle = $target->getSubpage( $langCode );
1667 if ( !$translationPageTitle || !$translationPageTitle->exists( Title::READ_LATEST ) ) {
1671 $dbw->startAtomic( $fname );
1673 $page = TranslatablePage::newFromTitle( $target );
1675 MessageGroupStats::forItem(
1676 $page->getMessageGroupId(),
1678 MessageGroupStats::FLAG_NO_CACHE
1681 if ( !$handle->
isDoc() ) {
1684 self::updateTranslationPage(
1685 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1689 $dbw->endAtomic( $fname );
1692 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1700 foreach ( $titles as $index => $title ) {
1702 if ( Utilities::isTranslationPage( $handle ) ) {
1703 unset( $titles[ $index ] );
1713 foreach ( $titles as $index => $title ) {
1716 TranslatablePage::isSourcePage( $title ) ||
1717 Utilities::isTranslationPage( $handle )
1719 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 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.
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.