3namespace MediaWiki\Extension\Translate\PageTranslation;
6use CommentStoreComment;
18use MediaWiki\Linker\LinkTarget;
19use MediaWiki\Logger\LoggerFactory;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Page\PageIdentity;
22use MediaWiki\Revision\MutableRevisionRecord;
23use MediaWiki\Revision\RenderedRevision;
24use MediaWiki\Revision\RevisionRecord;
25use MediaWiki\Revision\SlotRecord;
26use MediaWiki\Storage\EditResult;
27use MediaWiki\User\UserIdentity;
44use Wikimedia\ScopedCallback;
57 public static $allowTargetEdit =
false;
59 public static $jobQueueRunning =
false;
61 public static $renderingContext =
false;
63 private static $languageLinkData = [];
72 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
73 if ( $text === null ) {
81 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
87 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
88 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
89 $text = $translatablePageParser->cleanupTags( $text );
93 $title = MediaWikiServices::getInstance()
95 ->castFromPageReference( $wikitextParser->getPage() );
107 self::$renderingContext =
true;
108 [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
109 $name = $page->getPageDisplayTitle( $code );
111 $name = $wikitextParser->recursivePreprocess( $name );
113 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
114 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
115 $name = $langConv->convert( $name );
116 $wikitextParser->getOutput()->setDisplayTitle( $name );
118 self::$renderingContext =
false;
119 }
catch ( Exception $e ) {
120 LoggerFactory::getInstance(
'Translate' )->error(
121 'T302754 Failed to set display title for page {title}',
123 'title' => $title->getPrefixedDBkey(),
125 'pageid' => $title->getId(),
134 'languagecode' => $code,
135 'messagegroupid' => $page->getMessageGroupId(),
136 'sourcepagetitle' => [
137 'namespace' => $page->getTitle()->getNamespace(),
138 'dbkey' => $page->getTitle()->getDBkey()
142 $wikitextParser->getOutput()->setExtensionData(
143 'translate-translation-page', $extensionData
147 $wikitextParser->getOutput()->setExtensionData(
'Translate-noeditsection',
true );
158 $translatablePageParser =
Services::getInstance()->getTranslatablePageParser();
160 if ( $translatablePageParser->containsMarkup( $text ) ) {
162 $parserOutput = $translatablePageParser->parse( $text );
164 $text = $parserOutput->sourcePageTextForRendering(
165 $wikitextParser->getTargetLanguage()
167 $wikitextParser->getOutput()->addModuleStyles( [
171 wfDebug(
'ParsingFailure caught; expected' );
177 $text = $unit->getTextForTrans();
192 if ( $out->getExtensionData(
'Translate-noeditsection' ) ) {
193 $options[
'enableSectionEditLinks'] =
false;
209 ?LinkTarget $contextLink,
210 ?LinkTarget $templateLink,
212 ?RevisionRecord &$revRecord
214 if ( !$templateLink ) {
218 $templateTitle = Title::castFromLinkTarget( $templateLink );
220 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
221 if ( $templateTranslationPage ) {
224 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
228 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
232 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
234 if ( !( $translatableTemplatePage->supportsTransclusion() ??
false ) ) {
239 $store = MediaWikiServices::getInstance()->getRevisionStore();
241 if ( $contextLink ) {
243 $templateTranslationTitle = $templateTitle->getSubpage(
244 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
247 if ( $templateTranslationTitle ) {
248 if ( $templateTranslationTitle->exists() ) {
250 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
255 $revRecord =
new MutableRevisionRecord( $templateTranslationTitle );
263 $sourceTemplateTitle = $templateTitle->getSubpage(
264 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
266 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
267 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
280 if ( TranslatablePage::isTranslationPage( $title ) ) {
281 [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
282 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
294 if ( TranslatablePage::isSourcePage( $title ) ) {
295 $msg = wfMessage(
'translate-edit-tag-warning' )->inContentLanguage();
296 if ( !$msg->isDisabled() ) {
297 $notices[
'translate-tag'] = $msg->parseAsBlock();
300 $notices[] = Html::warningBox(
301 wfMessage(
'tps-edit-sourcepage-text' )->parse(),
302 'translate-edit-documentation'
309 $request = RequestContext::getMain()->getRequest();
310 if ( $request->getVal(
'action' ) ===
'visualeditor' &&
311 $request->getVal(
'paction' ) !==
'wikitext'
313 $notices[] = Html::warningBox(
314 wfMessage(
'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
315 'translate-edit-documentation'
328 global $wgTranslatePageTranslationULS;
330 $title = $out->getTitle();
331 $isSource = TranslatablePage::isSourcePage( $title );
332 $isTranslation = TranslatablePage::isTranslationPage( $title );
334 if ( $isSource || $isTranslation ) {
335 if ( $wgTranslatePageTranslationULS ) {
336 $out->addModules(
'ext.translate.pagetranslation.uls' );
341 $out->addModuleStyles(
'ext.translate.edit.documentation.styles' );
342 $out->addModules(
'ext.translate.edit.documentation' );
345 if ( $isTranslation ) {
348 $out->addModuleStyles(
'ext.translate' );
349 $out->addJsConfigVars(
'wgTranslatePageTranslation',
'translation' );
351 $out->addJsConfigVars(
'wgTranslatePageTranslation',
'source' );
365 return !TranslatablePage::isTranslationPage( $out->getTitle() );
382 TextContent $content,
389 if ( $user->equals( FuzzyBot::getUser() ) ) {
399 $page = TranslatablePage::newFromTitle( $group->getTitle() );
402 if ( !$handle->
isDoc() ) {
404 DeferredUpdates::addCallableUpdate(
405 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
407 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();
461 public static function languages( $data, $params, $parser ) {
462 global $wgPageTranslationLanguageList;
464 if ( $wgPageTranslationLanguageList ===
'sidebar-only' ) {
468 self::$renderingContext =
true;
469 $context =
new ScopedCallback(
static function () {
470 self::$renderingContext =
false;
474 if ( $wgPageTranslationLanguageList ===
'sidebar-fallback' ) {
475 $parser->getOutput()->addLanguageLink(
'x-pagetranslation-tag' );
478 $currentTitle = $parser->getTitle();
479 $pageStatus = self::getTranslatablePageStatus( $currentTitle );
480 if ( !$pageStatus ) {
484 $page = $pageStatus[
'page' ];
485 $status = $pageStatus[
'languages' ];
492 $userLang = $parser->getOptions()->getUserLangObj();
493 $userLangCode = $userLang->getCode();
497 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
500 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
501 foreach ( $status as $code => $percent ) {
503 $name = TranslateUtils::getLanguageName( $code,
null );
506 $suffix = ( $code === $sourceLanguage ) ?
'' :
"/$code";
507 $targetTitleString = $pageTitle->getDBkey() . $suffix;
508 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
511 if ( $code === $userLangCode ) {
512 $classes[] =
'mw-pt-languages-ui';
515 $linker = $parser->getLinkRenderer();
516 $lang = $langFactory->getLanguage( $code );
517 if ( $currentTitle->equals( $subpage ) ) {
518 $classes[] =
'mw-pt-languages-selected';
519 $classes = array_merge( $classes, self::tpProgressIcon( (
float)$percent ) );
522 'lang' => $lang->getHtmlCode(),
523 'dir' => $lang->getDir(),
526 $contents = Html::Element(
'span', $attribs, $name );
527 } elseif ( $subpage->isKnown() ) {
529 if ( !is_string( $pagename ) ) {
530 $pagename = $subpage->getPrefixedText();
533 $classes = array_merge( $classes, self::tpProgressIcon( (
float)$percent ) );
535 $title = wfMessage(
'tpt-languages-nonzero' )
536 ->inLanguage( $userLang )
537 ->params( $pagename )
538 ->numParams( 100 * $percent )
543 'lang' => $lang->getHtmlCode(),
544 'dir' => $lang->getDir(),
547 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
552 $specialTranslateTitle = SpecialPage::getTitleFor(
'Translate' );
562 'title' => wfMessage(
'tpt-languages-zero' )->inLanguage( $userLang )->text(),
564 'lang' => $lang->getHtmlCode(),
565 'dir' => $lang->getDir(),
567 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
569 $languages[ $name ] = Html::rawElement(
'li', [], $contents );
574 $languages = array_values( $languages );
575 $languages = implode(
"\n", $languages );
577 $out = Html::openElement(
'div', [
578 'class' =>
'mw-pt-languages noprint',
579 'lang' => $userLang->getHtmlCode(),
580 'dir' => $userLang->getDir()
582 $out .= Html::rawElement(
'div', [
'class' =>
'mw-pt-languages-label' ],
583 wfMessage(
'tpt-languages-legend' )->inLanguage( $userLang )->escaped()
585 $out .= Html::rawElement(
587 [
'class' =>
'mw-pt-languages-list' ],
590 $out .= Html::closeElement(
'div' );
592 $parser->getOutput()->addModuleStyles( [
593 'ext.translate.tag.languages',
605 private static function tpProgressIcon(
float $percent ) {
606 $classes = [
'mw-pt-progress' ];
608 if ( $percent < 20 ) {
609 $classes[] =
'mw-pt-progress--stub';
610 } elseif ( $percent < 40 ) {
611 $classes[] =
'mw-pt-progress--low';
612 } elseif ( $percent < 60 ) {
613 $classes[] =
'mw-pt-progress--med';
614 } elseif ( $percent < 80 ) {
615 $classes[] =
'mw-pt-progress--high';
617 $classes[] =
'mw-pt-progress--complete';
627 private static function getTranslatablePageStatus( Title $title ) {
629 $page = TranslatablePage::newFromTitle( $title );
631 $page = TranslatablePage::isTranslationPage( $title );
634 if ( $page ===
false || $page->
getMarkedTag() ===
null ) {
638 $status = $page->getTranslationPercentages();
647 if ( (
string)$priorityLangs !==
'' ) {
648 $filter = array_flip( explode(
',', $priorityLangs ) );
650 if ( $filter !==
null ) {
652 if ( $priorityForce ===
'on' ) {
655 $status = array_intersect_key( $status, $filter );
657 foreach ( $filter as $langCode => $value ) {
658 if ( !isset( $status[$langCode] ) ) {
660 $status[$langCode] = 0;
667 'languages' => $status
677 global $wgPageTranslationLanguageList;
679 $hasLanguagesTag =
false;
680 foreach ( $languageLinks as $index => $name ) {
681 if ( $name ===
'x-pagetranslation-tag' ) {
682 $hasLanguagesTag =
true;
683 unset( $languageLinks[ $index ] );
687 if ( $wgPageTranslationLanguageList ===
'tag-only' ) {
691 if ( $wgPageTranslationLanguageList ===
'sidebar-fallback' && $hasLanguagesTag ) {
697 $status = self::getTranslatablePageStatus( $title );
702 self::$renderingContext =
true;
703 $context =
new ScopedCallback(
static function () {
704 self::$renderingContext =
false;
707 $page = $status[
'page' ];
708 $languages = $status[
'languages' ];
709 $mwServices = MediaWikiServices::getInstance();
710 $en = $mwServices->getLanguageFactory()->getLanguage(
'en' );
712 $newLanguageLinks = [];
715 $lb =
new LinkBatch();
716 foreach ( array_keys( $languages ) as $code ) {
717 $title = $page->getTitle()->getSubpage( $code );
718 $lb->addObj( $title );
721 $languageNameUtils = $mwServices->getLanguageNameUtils();
722 foreach ( $languages as $code => $percentage ) {
723 $title = $page->getTitle()->getSubpage( $code );
724 $key =
"x-pagetranslation:{$title->getPrefixedText()}";
725 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
727 if ( $title->exists() ) {
728 $href = $title->getLocalURL();
729 $classes = self::tpProgressIcon( (
float)$percentage );
730 $title = wfMessage(
'tpt-languages-nonzero' )
731 ->params( $translatedName )
732 ->numParams( 100 * $percentage );
734 $href = SpecialPage::getTitleFor(
'Translate' )->getLocalURL( [
735 'group' => $page->getMessageGroupId(),
738 $classes = [
'mw-pt-progress--none' ];
739 $title = wfMessage(
'tpt-languages-zero' );
742 self::$languageLinkData[ $key ] = [
745 'percentage' => $percentage,
746 'classes' => $classes,
747 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
751 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][
'autonym' ];
754 asort( $newLanguageLinks );
755 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
771 if ( substr( $link[
'text' ], 0, 18 ) !==
'x-pagetranslation:' ) {
775 if ( !isset( self::$languageLinkData[ $link[
'text' ] ] ) ) {
779 $data = self::$languageLinkData[ $link[
'text' ] ];
781 $link[
'class' ] .=
' ' . implode(
' ', $data[
'classes' ] );
782 $link[
'href' ] = $data[
'href' ];
783 $link[
'text' ] = $data[
'autonym' ];
784 $link[
'title' ] = $data[
'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
785 $link[
'lang'] = LanguageCode::bcp47( $data[
'language' ] );
786 $link[
'hreflang'] = LanguageCode::bcp47( $data[
'language' ] );
788 $out->addModuleStyles(
'ext.translate.tag.languages' );
806 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
808 if ( $syntaxErrorStatus ) {
809 $status->merge( $syntaxErrorStatus );
810 return $syntaxErrorStatus->isGood();
816 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
818 if ( !$content instanceof WikitextContent || !$page ) {
822 $text = $content->getText();
825 $text = str_replace( [
"\r\n",
"\r" ],
"\n", rtrim( $text ) );
826 $status = Status::newGood();
827 $parser = Services::getInstance()->getTranslatablePageParser();
828 if ( $parser->containsMarkup( $text ) ) {
830 $parser->parse( $text );
831 }
catch ( ParsingFailure $e ) {
832 $status->fatal( ...( $e->getMessageSpecification() ) );
851 RenderedRevision $renderedRevision,
853 CommentStoreComment $summary,
857 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
859 $status = self::tpSyntaxError(
860 $renderedRevision->getRevision()->getPage(),
865 $hookStatus->merge( $status );
866 return $status->isGood();
885 UserIdentity $userIdentity,
888 RevisionRecord $revisionRecord,
889 EditResult $editResult
891 $content = $wikiPage->getContent();
894 if ( $content instanceof WikitextContent ) {
895 $text = $content->getText();
901 $parser = Services::getInstance()->getTranslatablePageParser();
902 if ( $parser->containsMarkup( $text ) ) {
904 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
927 $parentId = $rev->getParentId();
928 if ( $parentId === 0 || $parentId ===
null ) {
933 $prevRev = MediaWikiServices::getInstance()
934 ->getRevisionLookup()
935 ->getRevisionById( $parentId );
937 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
942 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
943 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
944 $bundle = $bundleFactory->getBundle( $title );
947 $bundleStore = $bundleFactory->getStore( $bundle );
948 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
973 if ( $action !==
'create' || !$handle->isPageTranslation() ) {
980 if ( $handle->isValid() ) {
981 $groupId = $handle->getGroup()->getId();
987 MediaWikiServices::getInstance()->getStatsdDataFactory()
988 ->increment(
'translate.slow_translatable_page_check' );
989 $translatablePage = self::checkTranslatablePageSlow( $title );
990 if ( $translatablePage ) {
991 $groupId = $translatablePage->getMessageGroupId();
998 $error = self::getTranslationRestrictions( $handle, $groupId );
999 $result = $error ?: $result;
1000 return $error === [];
1004 LoggerFactory::getInstance(
'Translate' )->info(
1005 'Unknown translation page: {title}',
1006 [
'title' => $title->getPrefixedDBkey() ]
1008 $result = [
'tpt-unknown-page' ];
1012 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?
TranslatablePage {
1014 $translationPageTitle = Title::newFromText(
1015 $parts[
'sourcepage' ] .
'/' . $parts[
'language' ]
1017 if ( !$translationPageTitle ) {
1021 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1022 if ( !$translatablePage ) {
1026 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1027 $store = $factory->getReader( $translatablePage->getTitle() );
1028 $units = $store->getNames();
1030 if ( !in_array( $parts[
'section' ], $units ) ) {
1034 return $translatablePage;
1044 private static function getTranslationRestrictions(
MessageHandle $handle, $groupId ) {
1045 global $wgTranslateDocumentationLanguageCode;
1048 if ( $handle->
getCode() === $wgTranslateDocumentationLanguageCode ) {
1054 if ( $force !==
'on' ) {
1060 $filter = array_flip( explode(
',', $languages ) );
1061 if ( !isset( $filter[$handle->
getCode()] ) ) {
1064 return [
'tpt-translation-restricted', $reason ];
1067 return [
'tpt-translation-restricted-no-reason' ];
1083 if ( self::$allowTargetEdit ) {
1088 'read',
'delete',
'undelete',
'deletedtext',
'deletedhistory',
1089 'deleterevision',
'suppressrevision',
'viewsuppressed',
1093 if ( in_array( $action, $inclusionList ) ) {
1097 $page = TranslatablePage::isTranslationPage( $title );
1099 [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
1100 $mwService = MediaWikiServices::getInstance();
1102 if ( method_exists( $mwService,
'getUrlUtils' ) ) {
1103 $translationUrl = $mwService->getUrlUtils()->expand(
1108 $translationUrl = wfExpandUrl( $page->
getTranslationUrl( $code ), PROTO_RELATIVE );
1113 ':' . $page->
getTitle()->getPrefixedText(),
1135 $title = $article->getTitle();
1136 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1137 $isDeletableBundle = $bundle && $bundle->isDeletable();
1138 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1139 $new = SpecialPage::getTitleFor(
1140 'PageTranslationDeletePage',
1141 $title->getPrefixedText()
1143 $out->redirect( $new->getFullURL() );
1158 if ( $article->getOldID() ) {
1162 $transPage = TranslatablePage::isTranslationPage( $article->getTitle() );
1163 $context = $article->getContext();
1165 self::translationPageHeader( $context, $transPage );
1168 self::sourcePageHeader( $context );
1174 private static function sourcePageHeader( IContextSource $context ) {
1175 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1177 $language = $context->getLanguage();
1178 $title = $context->getTitle();
1180 $page = TranslatablePage::newFromTitle( $title );
1184 $latest = $title->getLatestRevID();
1187 if ( $marked && $context->getUser()->isAllowed(
'translate' ) ) {
1188 $actions[] = self::getTranslateLink( $context, $page,
null );
1191 $hasChanges = $ready === $latest && $marked !== $latest;
1192 if ( $hasChanges ) {
1193 $diffUrl = $title->getFullURL( [
'oldid' => $marked,
'diff' => $latest ] );
1195 if ( $context->getUser()->isAllowed(
'pagetranslation' ) ) {
1196 $pageTranslation = SpecialPage::getTitleFor(
'PageTranslation' );
1197 $params = [
'target' => $title->getPrefixedText(),
'do' =>
'mark' ];
1199 if ( $marked ===
null ) {
1201 $linkDesc = $context->msg(
'translate-tag-markthis' )->text();
1202 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1204 $markUrl = $pageTranslation->getFullURL( $params );
1205 $actions[] = $context->msg(
'translate-tag-markthisagain', $diffUrl, $markUrl )
1209 $actions[] = $context->msg(
'translate-tag-hasnew', $diffUrl )->parse();
1213 if ( !count( $actions ) ) {
1217 $header = Html::rawElement(
1220 'class' =>
'mw-pt-translate-header noprint nomobile',
1221 'dir' => $language->getDir(),
1222 'lang' => $language->getHtmlCode(),
1224 $language->semicolonList( $actions )
1227 $context->getOutput()->addHTML( $header );
1230 private static function getTranslateLink(
1231 IContextSource $context,
1232 TranslatablePage $page,
1235 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1237 return $linker->makeKnownLink(
1238 SpecialPage::getTitleFor(
'Translate' ),
1239 $context->msg(
'translate-tag-translate-link-desc' )->text(),
1242 'group' => $page->getMessageGroupId(),
1243 'language' => $langCode,
1250 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1251 global $wgTranslateKeepOutdatedTranslations;
1253 $title = $context->getTitle();
1254 if ( !$title->exists() ) {
1261 $pers = $page->getTranslationPercentages();
1263 if ( isset( $pers[$code] ) ) {
1264 $per = $pers[$code] * 100;
1267 $language = $context->getLanguage();
1268 $output = $context->getOutput();
1270 if ( $page->getSourceLanguageCode() === $code ) {
1272 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1274 $mwService = MediaWikiServices::getInstance();
1276 if ( method_exists( $mwService,
'getUrlUtils' ) ) {
1277 $translationUrl = $mwService->getUrlUtils()->expand(
1278 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1282 $translationUrl = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE );
1285 $msg = $context->msg(
'tpt-translation-intro',
1287 ':' . $page->getTitle()->getPrefixedText(),
1288 $language->formatNum( $per )
1292 $header = Html::rawElement(
1295 'class' =>
'mw-pt-translate-header noprint',
1296 'dir' => $language->getDir(),
1297 'lang' => $language->getHtmlCode(),
1302 $output->addHTML( $header );
1304 if ( $wgTranslateKeepOutdatedTranslations ) {
1305 $groupId = $page->getMessageGroupId();
1308 if ( $stats[MessageGroupStats::FUZZY] ) {
1310 $wrap = Html::rawElement(
1313 'class' =>
'mw-pt-translate-header',
1314 'dir' => $language->getDir(),
1315 'lang' => $language->getHtmlCode()
1317 '<span class="mw-translate-fuzzy">$1</span>'
1320 $output->wrapWikiMsg( $wrap, [
'tpt-translation-intro-fuzzy' ] );
1331 $movePageSpec = $list[
'Movepage'] ??
null;
1334 if ( $movePageSpec ===
null ) {
1338 $list[
'Movepage'] = [
1339 'class' => MoveTranslatableBundleSpecialPage::class,
1342 'PermissionManager',
1343 'Translate:TranslatableBundleMover',
1344 'Translate:TranslatableBundleFactory'
1363 if ( $action ===
'read' ) {
1367 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1368 $key = $cache->makeKey(
'pt-lock', sha1( $title->getPrefixedText() ) );
1369 if ( $cache->get( $key ) ===
'locked' ) {
1370 $result = [
'pt-locked-page' ];
1386 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1388 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1389 if ( !$isTranslationPage
1390 && !TranslatablePage::isSourcePage( $out->getTitle() )
1396 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1398 $out->isArticle() &&
1399 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1401 $ptext = $out->getTitle()->getPrefixedText();
1402 if ( strpos( $ptext,
'/' ) !==
false ) {
1403 $links = explode(
'/', $ptext );
1404 array_pop( $links );
1405 if ( $isTranslationPage ) {
1407 array_pop( $links );
1412 $lang = $skin->getLanguage();
1414 foreach ( $links as $link ) {
1415 $growinglink .= $link;
1417 $linkObj = Title::newFromText( $growinglink );
1419 if ( is_object( $linkObj ) && $linkObj->isKnown() ) {
1420 $getlink = $linker->makeKnownLink(
1421 SpecialPage::getTitleFor(
'MyLanguage', $growinglink ),
1428 $subpages .= $lang->getDirMarkEntity() . $skin->msg(
'pipe-separator' )->escaped();
1430 $subpages .=
'< ';
1433 $subpages .= $getlink;
1439 $growinglink .=
'/';
1457 $title = $skin->getTitle();
1460 $page = TranslatablePage::isTranslationPage( $title );
1465 if ( $page->getSourceLanguageCode() === $code ) {
1469 if ( isset( $tabs[
'views'][
'edit'] ) ) {
1470 $tabs[
'views'][
'edit'][
'text'] = $skin->msg(
'tpt-tab-translate' )->text();
1471 $tabs[
'views'][
'edit'][
'href'] = $page->getTranslationUrl( $code );
1490 LinkTarget $oldLinkTarget,
1491 LinkTarget $newLinkTarget,
1492 UserIdentity $userIdentity,
1496 RevisionRecord $revisionRecord
1498 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1503 if ( defined(
'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1507 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1508 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1510 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1517 if ( $handle->
isDoc() ) {
1526 $language = $handle->
getCode();
1529 if ( (
string)$language ===
'' ) {
1535 if ( $group !== $groupLast ) {
1536 $groupLast = $group;
1537 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1538 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1562 if ( self::$jobQueueRunning ) {
1566 $title = $unit->getTitle();
1578 $target = $group->getTitle();
1579 $langCode = $handle->
getCode();
1580 $fname = __METHOD__;
1582 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1583 $callback =
function () use (
1592 $translationPageTitle = $target->getSubpage( $langCode );
1595 if ( !$translationPageTitle || !$translationPageTitle->exists( Title::READ_LATEST ) ) {
1599 $dbw->startAtomic( $fname );
1601 $page = TranslatablePage::newFromTitle( $target );
1603 MessageGroupStats::forItem(
1604 $page->getMessageGroupId(),
1606 MessageGroupStats::FLAG_NO_CACHE
1609 if ( !$handle->
isDoc() ) {
1612 self::updateTranslationPage(
1613 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1617 $dbw->endAtomic( $fname );
1620 $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.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
static figureMessage( $text)
Splits page name into message key and language code.
Wraps the translatable page sections into a message group.