Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
Hooks.php
1<?php
2
3namespace MediaWiki\Extension\Translate\PageTranslation;
4
5use Article;
6use Content;
7use DeferredUpdates;
8use Exception;
9use IContextSource;
10use IDBAccessObject;
11use Language;
12use LanguageCode;
13use ManualLogEntry;
14use MediaWiki\CommentStore\CommentStoreComment;
22use MediaWiki\Html\Html;
23use MediaWiki\Languages\LanguageNameUtils;
24use MediaWiki\Linker\LinkTarget;
25use MediaWiki\Logger\LoggerFactory;
26use MediaWiki\MainConfigNames;
27use MediaWiki\MediaWikiServices;
28use MediaWiki\Page\PageIdentity;
29use MediaWiki\Revision\MutableRevisionRecord;
30use MediaWiki\Revision\RenderedRevision;
31use MediaWiki\Revision\RevisionRecord;
32use MediaWiki\Revision\SlotRecord;
33use MediaWiki\SpecialPage\SpecialPage;
34use MediaWiki\Status\Status;
35use MediaWiki\Storage\EditResult;
36use MediaWiki\StubObject\StubUserLang;
37use MediaWiki\Title\Title;
38use MediaWiki\User\User;
39use MediaWiki\User\UserIdentity;
40use ObjectCache;
41use OutputPage;
42use Parser;
43use ParserOutput;
44use PPFrame;
45use RequestContext;
46use Skin;
47use TextContent;
48use UserBlockedError;
49use Wikimedia\ScopedCallback;
50use WikiPage;
52use WikitextContent;
53
60class Hooks {
61 private const PAGEPROP_HAS_LANGUAGES_TAG = 'translate-has-languages-tag';
62 // Uuugly hacks
63 public static $allowTargetEdit = false;
65 public static bool $isDeleteTranslatableBundleJobRunning = false;
66 // Check if we are just rendering tags or such
67 public static $renderingContext = false;
68 // Used to communicate data between LanguageLinks and SkinTemplateGetLanguageLink hooks.
69 private static $languageLinkData = [];
70
78 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
79 if ( $text === null ) {
80 // SMW is unhelpfully sending null text if the source contains section tags. Do not explode.
81 return;
82 }
83
84 self::preprocessTagPage( $wikitextParser, $text, $state );
85
86 // Skip further interface message parsing
87 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
88 return;
89 }
90
91 // For section previews, perform additional clean-up, given tags are often
92 // unbalanced when we preview one section only.
93 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
94 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
95 $text = $translatablePageParser->cleanupTags( $text );
96 }
97
98 // Set display title
99 $title = MediaWikiServices::getInstance()
100 ->getTitleFactory()
101 ->castFromPageReference( $wikitextParser->getPage() );
102 if ( !$title ) {
103 return;
104 }
105
106 $page = TranslatablePage::isTranslationPage( $title );
107 if ( !$page ) {
108 return;
109 }
110
111 $wikitextParser->getOutput()->setPageProperty( 'translate-is-translation', true );
112
113 try {
114 self::$renderingContext = true;
115 [ , $code ] = Utilities::figureMessage( $title->getText() );
116 $name = $page->getPageDisplayTitle( $code );
117 if ( $name ) {
118 $name = $wikitextParser->recursivePreprocess( $name );
119
120 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
121 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
122 $name = $langConv->convert( $name );
123 $wikitextParser->getOutput()->setDisplayTitle( $name );
124 }
125 self::$renderingContext = false;
126 } catch ( Exception $e ) {
127 LoggerFactory::getInstance( 'Translate' )->error(
128 'T302754 Failed to set display title for page {title}',
129 [
130 'title' => $title->getPrefixedDBkey(),
131 'text' => $text,
132 'pageid' => $title->getId(),
133 ]
134 );
135
136 // Re-throw to preserve behavior
137 throw $e;
138 }
139
140 $extensionData = [
141 'languagecode' => $code,
142 'messagegroupid' => $page->getMessageGroupId(),
143 'sourcepagetitle' => [
144 'namespace' => $page->getTitle()->getNamespace(),
145 'dbkey' => $page->getTitle()->getDBkey()
146 ]
147 ];
148
149 $wikitextParser->getOutput()->setExtensionData( 'translate-translation-page', $extensionData );
150 // Disable edit section links
151 $wikitextParser->getOutput()->setExtensionData( 'Translate-noeditsection', true );
152 }
153
161 public static function preprocessTagPage( $wikitextParser, &$text, $state ): void {
162 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
163
164 if ( $translatablePageParser->containsMarkup( $text ) ) {
165 try {
166 $parserOutput = $translatablePageParser->parse( $text );
167 // If parsing succeeds, replace text and add styles
168 $text = $parserOutput->sourcePageTextForRendering(
169 $wikitextParser->getTargetLanguage()
170 );
171 $wikitextParser->getOutput()->addModuleStyles( [
172 'ext.translate',
173 ] );
174 } catch ( ParsingFailure $e ) {
175 wfDebug( 'ParsingFailure caught; expected' );
176 }
177 } else {
178 // If the text doesn't contain <translate> markup, it can still contain <tvar> in the
179 // context of a Parsoid template expansion sub-pipeline. We strip these as well.
180 $unit = new TranslationUnit( $text );
181 $text = $unit->getTextForTrans();
182 }
183 }
184
191 public static function onParserOutputPostCacheTransform(
192 ParserOutput $out,
193 &$text,
194 array &$options
195 ) {
196 if ( $out->getExtensionData( 'Translate-noeditsection' ) ) {
197 $options['enableSectionEditLinks'] = false;
198 }
199 }
200
212 public static function fetchTranslatableTemplateAndTitle(
213 ?LinkTarget $contextLink,
214 ?LinkTarget $templateLink,
215 bool &$skip,
216 ?RevisionRecord &$revRecord
217 ): void {
218 if ( !$templateLink ) {
219 return;
220 }
221
222 $templateTitle = Title::castFromLinkTarget( $templateLink );
223
224 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
225 if ( $templateTranslationPage ) {
226 // Template is referring to a translation page, fetch it and incase it doesn't
227 // exist, fetch the source fallback.
228 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
229 if ( !$revRecord ) {
230 // In rare cases fetching of the source fallback might fail. See: T323863
231 LoggerFactory::getInstance( 'Translate' )->warning(
232 "T323863: Could not fetch any revision record for '{groupid}'",
233 [ 'groupid' => $templateTranslationPage->getMessageGroupId() ]
234 );
235 }
236 return;
237 }
238
239 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
240 return;
241 }
242
243 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
244
245 if ( !( $translatableTemplatePage->supportsTransclusion() ?? false ) ) {
246 // Page being transcluded does not support language aware transclusion
247 return;
248 }
249
250 $store = MediaWikiServices::getInstance()->getRevisionStore();
251
252 if ( $contextLink ) {
253 // Fetch the context page language, and then check if template is present in that language
254 $templateTranslationTitle = $templateTitle->getSubpage(
255 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
256 );
257
258 if ( $templateTranslationTitle ) {
259 if ( $templateTranslationTitle->exists() ) {
260 // Template is present in the context page language, fetch the revision record and return
261 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
262 } else {
263 // In case the template has not been translated to the context page language,
264 // we assign a MutableRevisionRecord in order to add a dependency, so that when
265 // it is created, the newly created page is loaded rather than the fallback
266 $revRecord = new MutableRevisionRecord( $templateTranslationTitle );
267 }
268 return;
269 }
270 }
271
272 // Context page information not available OR the template translation title could not be determined.
273 // Fetch and return the RevisionRecord of the template in the source language
274 $sourceTemplateTitle = $templateTitle->getSubpage(
275 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
276 );
277 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
278 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
279 }
280 }
281
288 public static function onPageContentLanguage( Title $title, &$pageLang ) {
289 // For translation pages, parse plural, grammar etc. with correct language,
290 // and set the right direction
291 if ( TranslatablePage::isTranslationPage( $title ) ) {
292 [ , $code ] = Utilities::figureMessage( $title->getText() );
293 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
294 }
295 }
296
304 public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) {
305 if ( TranslatablePage::isSourcePage( $title ) ) {
306 $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
307 if ( !$msg->isDisabled() ) {
308 $notices['translate-tag'] = $msg->parseAsBlock();
309 }
310
311 $notices[] = Html::warningBox(
312 wfMessage( 'tps-edit-sourcepage-text' )->parse(),
313 'translate-edit-documentation'
314 );
315
316 // The check is "we're using visual editor for WYSIWYG" (as opposed to "for wikitext
317 // edition") - the message will not be displayed in that case.
318 $request = RequestContext::getMain()->getRequest();
319 if ( $request->getVal( 'action' ) === 'visualeditor' &&
320 $request->getVal( 'paction' ) !== 'wikitext'
321 ) {
322 $notices[] = Html::warningBox(
323 wfMessage( 'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
324 'translate-edit-documentation'
325 );
326 }
327 }
328 }
329
335 public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
336 global $wgTranslatePageTranslationULS;
337
338 $title = $out->getTitle();
339 $isSource = TranslatablePage::isSourcePage( $title );
340 $isTranslation = TranslatablePage::isTranslationPage( $title );
341
342 if ( $isSource || $isTranslation ) {
343 if ( $wgTranslatePageTranslationULS ) {
344 $out->addModules( 'ext.translate.pagetranslation.uls' );
345 }
346
347 if ( $isSource ) {
348 // Adding a help notice
349 $out->addModuleStyles( 'ext.translate.edit.documentation.styles' );
350 }
351
352 if ( $isTranslation ) {
353 // Source pages get this module via <translate>, but for translation
354 // pages we need to add it manually.
355 $out->addModuleStyles( 'ext.translate' );
356 }
357
358 $out->addJsConfigVars( 'wgTranslatePageTranslation', $isTranslation ? 'translation' : 'source' );
359 }
360 }
361
368 public static function onVisualEditorBeforeEditor( OutputPage $out, Skin $skin ) {
369 return !TranslatablePage::isTranslationPage( $out->getTitle() );
370 }
371
382 public static function onSectionSave(
383 WikiPage $wikiPage,
384 User $user,
385 TextContent $content,
386 $summary,
387 $minor,
388 $flags,
389 MessageHandle $handle
390 ) {
391 // FuzzyBot may do some duplicate work already worked on by other jobs
392 if ( $user->equals( FuzzyBot::getUser() ) ) {
393 return;
394 }
395
396 $group = $handle->getGroup();
397 if ( !$group instanceof WikiPageMessageGroup ) {
398 return;
399 }
400
401 // Finally we know the title and can construct a Translatable page
402 $page = TranslatablePage::newFromTitle( $group->getTitle() );
403
404 // Update the target translation page
405 if ( !$handle->isDoc() ) {
406 $code = $handle->getCode();
407 DeferredUpdates::addCallableUpdate(
408 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
409 $unitTitle = $handle->getTitle();
410 self::updateTranslationPage( $page, $code, $user, $flags, $summary, null, $unitTitle );
411 }
412 );
413 }
414 }
415
416 private static function updateTranslationPage(
417 TranslatablePage $page,
418 string $code,
419 User $user,
420 int $flags,
421 string $summary,
422 ?string $triggerAction = null,
423 ?Title $unitTitle = null
424 ): void {
425 $source = $page->getTitle();
426 $target = $source->getSubpage( $code );
427 $mwInstance = MediaWikiServices::getInstance();
428
429 // We don't know and don't care
430 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
431
432 // Update the target page
433 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() : null;
434 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
435 $job->setUser( $user );
436 $job->setSummary( $summary );
437 $job->setFlags( $flags );
438 $mwInstance->getJobQueueGroup()->push( $job );
439
440 // Invalidate caches so that language bar is up-to-date
441 $pages = $page->getTranslationPages();
442 $wikiPageFactory = $mwInstance->getWikiPageFactory();
443 foreach ( $pages as $title ) {
444 if ( $title->equals( $target ) ) {
445 // Handled by the RenderTranslationPageJob
446 continue;
447 }
448
449 $wikiPage = $wikiPageFactory->newFromTitle( $title );
450 $wikiPage->doPurge();
451 }
452 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
453 $sourceWikiPage->doPurge();
454 }
455
460 public static function onGetMagicVariableIDs( &$variableIDs ): void {
461 $variableIDs[] = 'translatablepage';
462 }
463
467 public static function onParserGetVariableValueSwitch(
468 Parser $parser,
469 array &$variableCache,
470 string $magicWordId,
471 ?string &$ret,
472 PPFrame $frame
473 ): void {
474 switch ( $magicWordId ) {
475 case 'translatablepage':
476 $title = Title::castFromPageReference( $parser->getPage() );
477 $pageStatus = self::getTranslatablePageStatus( $title );
478 $ret = $pageStatus !== null ? $pageStatus['page']->getTitle()->getPrefixedText() : '';
479 $variableCache[$magicWordId] = $ret;
480 break;
481 }
482 }
483
490 public static function languages( $data, $params, $parser ) {
491 global $wgPageTranslationLanguageList;
492
493 if ( $wgPageTranslationLanguageList === 'sidebar-only' ) {
494 return '';
495 }
496
497 self::$renderingContext = true;
498 $context = new ScopedCallback( static function () {
499 self::$renderingContext = false;
500 } );
501
502 // Store a property that we can avoid adding language links when
503 // $wgPageTranslationLanguageList === 'sidebar-fallback'
504 $parser->getOutput()->setPageProperty( self::PAGEPROP_HAS_LANGUAGES_TAG, true );
505
506 $currentTitle = $parser->getTitle();
507 $pageStatus = self::getTranslatablePageStatus( $currentTitle );
508 if ( !$pageStatus ) {
509 return '';
510 }
511
512 $page = $pageStatus[ 'page' ];
513 $status = $pageStatus[ 'languages' ];
514 $pageTitle = $page->getTitle();
515
516 // Sort by language code, which seems to be the only sane method
517 ksort( $status );
518
519 // This way the parser knows to fragment the parser cache by language code
520 $userLang = $parser->getOptions()->getUserLangObj();
521 $userLangCode = $userLang->getCode();
522 // Should call $page->getMessageGroup()->getSourceLanguage(), but
523 // group is sometimes null on WMF during page moves, reason unknown.
524 // This should do the same thing for now.
525 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
526
527 $languages = [];
528 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
529 foreach ( $status as $code => $percent ) {
530 // Get autonyms (null)
531 $name = Utilities::getLanguageName( $code, LanguageNameUtils::AUTONYMS );
532
533 // Add links to other languages
534 $suffix = ( $code === $sourceLanguage ) ? '' : "/$code";
535 $targetTitleString = $pageTitle->getDBkey() . $suffix;
536 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
537
538 $classes = [];
539 if ( $code === $userLangCode ) {
540 $classes[] = 'mw-pt-languages-ui';
541 }
542
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 ) );
548 $attribs = [
549 'class' => $classes,
550 'lang' => $lang->getHtmlCode(),
551 'dir' => $lang->getDir(),
552 ];
553
554 $contents = Html::element( 'span', $attribs, $name );
555 } elseif ( $subpage->isKnown() ) {
556 $pagename = $page->getPageDisplayTitle( $code );
557 if ( !is_string( $pagename ) ) {
558 $pagename = $subpage->getPrefixedText();
559 }
560
561 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
562
563 $title = wfMessage( 'tpt-languages-nonzero' )
564 ->page( $parser->getPage() )
565 ->inLanguage( $userLang )
566 ->params( $pagename )
567 ->numParams( 100 * $percent )
568 ->text();
569 $attribs = [
570 'title' => $title,
571 'class' => $classes,
572 'lang' => $lang->getHtmlCode(),
573 'dir' => $lang->getDir(),
574 ];
575
576 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
577 } else {
578 /* When language is included because it is a priority language,
579 * but translations don't exist link directly to the
580 * translation view. */
581 $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' );
582 $params = [
583 'group' => $page->getMessageGroupId(),
584 'language' => $code,
585 'task' => 'view'
586 ];
587
588 $classes[] = 'new'; // For red link color
589
590 $attribs = [
591 'title' => wfMessage( 'tpt-languages-zero' )
592 ->page( $parser->getPage() )
593 ->inLanguage( $userLang )
594 ->text(),
595 'class' => $classes,
596 'lang' => $lang->getHtmlCode(),
597 'dir' => $lang->getDir(),
598 ];
599 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
600 }
601 $languages[ $name ] = Html::rawElement( 'li', [], $contents );
602 }
603
604 // Sort languages by autonym
605 ksort( $languages );
606 $languages = array_values( $languages );
607 $languages = implode( "\n", $languages );
608
609 $out = Html::openElement( 'div', [
610 'class' => 'mw-pt-languages noprint navigation-not-searchable',
611 'lang' => $userLang->getHtmlCode(),
612 'dir' => $userLang->getDir()
613 ] );
614 $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ],
615 wfMessage( 'tpt-languages-legend' )
616 ->page( $parser->getPage() )
617 ->inLanguage( $userLang )
618 ->escaped()
619 );
620 $out .= Html::rawElement(
621 'ul',
622 [ 'class' => 'mw-pt-languages-list' ],
623 $languages
624 );
625 $out .= Html::closeElement( 'div' );
626
627 $parser->getOutput()->addModuleStyles( [
628 'ext.translate.tag.languages',
629 ] );
630
631 return $out;
632 }
633
640 private static function tpProgressIcon( float $percent ) {
641 $classes = [ 'mw-pt-progress' ];
642 $percent *= 100;
643 if ( $percent < 15 ) {
644 $classes[] = 'mw-pt-progress--low';
645 } elseif ( $percent < 70 ) {
646 $classes[] = 'mw-pt-progress--med';
647 } elseif ( $percent < 100 ) {
648 $classes[] = 'mw-pt-progress--high';
649 } else {
650 $classes[] = 'mw-pt-progress--complete';
651 }
652 return $classes;
653 }
654
659 private static function getTranslatablePageStatus( ?Title $title ): ?array {
660 if ( $title === null ) {
661 return null;
662 }
663 // Check if this is a source page or a translation page
664 $page = TranslatablePage::newFromTitle( $title );
665 if ( $page->getMarkedTag() === null ) {
666 $page = TranslatablePage::isTranslationPage( $title );
667 }
668
669 if ( $page === false || $page->getMarkedTag() === null ) {
670 return null;
671 }
672
673 $status = $page->getTranslationPercentages();
674 if ( !$status ) {
675 return null;
676 }
677
678 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
679 // If priority languages have been set, always show those languages
680 $priorityLanguages = $messageGroupMetadata->get( $page->getMessageGroupId(), 'prioritylangs' );
681 if ( (string)$priorityLanguages !== '' ) {
682 $status += array_fill_keys( explode( ',', $priorityLanguages ), 0 );
683 }
684
685 return [
686 'page' => $page,
687 'languages' => $status
688 ];
689 }
690
696 public static function addLanguageLinks( Title $title, array &$languageLinks ) {
697 global $wgPageTranslationLanguageList;
698
699 if ( $wgPageTranslationLanguageList === 'tag-only' ) {
700 return;
701 }
702
703 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) {
704 $pageProps = MediaWikiServices::getInstance()->getPageProps();
705 $languageProp = $pageProps->getProperties( $title, self::PAGEPROP_HAS_LANGUAGES_TAG );
706 if ( $languageProp !== [] ) {
707 return;
708 }
709 }
710
711 // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only'
712
713 $status = self::getTranslatablePageStatus( $title );
714 if ( !$status ) {
715 return;
716 }
717
718 self::$renderingContext = true;
719 $context = new ScopedCallback( static function () {
720 self::$renderingContext = false;
721 } );
722
723 $page = $status[ 'page' ];
724 $languages = $status[ 'languages' ];
725 $mwServices = MediaWikiServices::getInstance();
726 $en = $mwServices->getLanguageFactory()->getLanguage( 'en' );
727
728 $newLanguageLinks = [];
729
730 // Batch the Title::exists queries used below
731 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
732 foreach ( array_keys( $languages ) as $code ) {
733 $title = $page->getTitle()->getSubpage( $code );
734 $lb->addObj( $title );
735 }
736 $lb->execute();
737 $languageNameUtils = $mwServices->getLanguageNameUtils();
738 foreach ( $languages as $code => $percentage ) {
739 $title = $page->getTitle()->getSubpage( $code );
740 $key = "x-pagetranslation:{$title->getPrefixedText()}";
741 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
742
743 if ( $title->exists() ) {
744 $href = $title->getLocalURL();
745 $classes = self::tpProgressIcon( (float)$percentage );
746 $title = wfMessage( 'tpt-languages-nonzero' )
747 ->params( $translatedName )
748 ->numParams( 100 * $percentage );
749 } else {
750 $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [
751 'group' => $page->getMessageGroupId(),
752 'language' => $code,
753 ] );
754 $classes = [ 'mw-pt-progress--none' ];
755 $title = wfMessage( 'tpt-languages-zero' );
756 }
757
758 self::$languageLinkData[ $key ] = [
759 'href' => $href,
760 'language' => $code,
761 'percentage' => $percentage,
762 'classes' => $classes,
763 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
764 'title' => $title,
765 ];
766
767 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][ 'autonym' ];
768 }
769
770 asort( $newLanguageLinks );
771 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
772 }
773
781 public static function formatLanguageLink(
782 array &$link,
783 Title $linkTitle,
784 Title $pageTitle,
785 OutputPage $out
786 ) {
787 if ( !str_starts_with( $link['text'], 'x-pagetranslation:' ) ||
788 !isset( self::$languageLinkData[ $link['text'] ] )
789 ) {
790 return;
791 }
792
793 $data = self::$languageLinkData[ $link[ 'text' ] ];
794
795 $link[ 'class' ] .= ' ' . implode( ' ', $data[ 'classes' ] );
796 $link[ 'href' ] = $data[ 'href' ];
797 $link[ 'text' ] = $data[ 'autonym' ];
798 $link[ 'title' ] = $data[ 'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
799 $link[ 'lang'] = LanguageCode::bcp47( $data[ 'language' ] );
800 $link[ 'hreflang'] = LanguageCode::bcp47( $data[ 'language' ] );
801
802 $out->addModuleStyles( 'ext.translate.tag.languages' );
803 }
804
814 public static function tpSyntaxCheckForEditContent(
815 $context,
816 $content,
817 $status,
818 $summary
819 ) {
820 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
821
822 if ( $syntaxErrorStatus ) {
823 $status->merge( $syntaxErrorStatus );
824 return $syntaxErrorStatus->isGood();
825 }
826
827 return true;
828 }
829
830 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
831 // T163254: Ignore translation markup on non-wikitext pages
832 if ( !$content instanceof WikitextContent || !$page ) {
833 return null;
834 }
835
836 $text = $content->getText();
837
838 // See T154500
839 $text = TextContent::normalizeLineEndings( $text );
840 $status = Status::newGood();
841 $parser = Services::getInstance()->getTranslatablePageParser();
842 if ( $parser->containsMarkup( $text ) ) {
843 try {
844 $parser->parse( $text );
845 } catch ( ParsingFailure $e ) {
846 $status->fatal( ...( $e->getMessageSpecification() ) );
847 }
848 }
849
850 return $status;
851 }
852
864 public static function tpSyntaxCheck(
865 RenderedRevision $renderedRevision,
866 UserIdentity $user,
867 CommentStoreComment $summary,
868 $flags,
869 Status $hookStatus
870 ) {
871 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
872
873 $status = self::tpSyntaxError(
874 $renderedRevision->getRevision()->getPage(),
875 $content
876 );
877
878 if ( $status ) {
879 $hookStatus->merge( $status );
880 return $status->isGood();
881 }
882
883 return true;
884 }
885
896 public static function addTranstagAfterSave(
897 WikiPage $wikiPage,
898 UserIdentity $userIdentity,
899 string $summary,
900 int $flags,
901 RevisionRecord $revisionRecord,
902 EditResult $editResult
903 ) {
904 $content = $wikiPage->getContent();
905
906 // T163254: Disable page translation on non-wikitext pages
907 if ( $content instanceof WikitextContent ) {
908 $text = $content->getText();
909 } else {
910 // Not applicable
911 return;
912 }
913
914 $parser = Services::getInstance()->getTranslatablePageParser();
915 if ( $parser->containsMarkup( $text ) ) {
916 // Add the ready tag
917 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
918 $page->addReadyTag( $revisionRecord->getId() );
919 }
920
921 // Schedule a deferred status update for the translatable page.
922 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
923 $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() );
924 }
925
941 public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) {
942 $parentId = $rev->getParentId();
943 if ( $parentId === 0 || $parentId === null ) {
944 // No parent, bail out.
945 return;
946 }
947
948 $prevRev = MediaWikiServices::getInstance()
949 ->getRevisionLookup()
950 ->getRevisionById( $parentId );
951
952 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
953 // Not a null revision, bail out.
954 return;
955 }
956
957 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
958 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
959 $bundle = $bundleFactory->getBundle( $title );
960
961 if ( $bundle ) {
962 $bundleStore = $bundleFactory->getStore( $bundle );
963 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
964 }
965 }
966
978 Title $title,
979 User $user,
980 $action,
981 &$result
982 ) {
983 $handle = new MessageHandle( $title );
984
985 if ( !$handle->isPageTranslation() || $action === 'read' ) {
986 return true;
987 }
988
989 $isValid = true;
990 $groupId = null;
991
992 if ( $handle->isValid() ) {
993 $group = $handle->getGroup();
994 $groupId = $group->getId();
995 $permissionTitleCheck = null;
996
997 if ( $group instanceof WikiPageMessageGroup ) {
998 $permissionTitleCheck = $group->getTitle();
999 } elseif ( $group instanceof MessageBundleMessageGroup ) {
1000 // TODO: This check for MessageBundle related permission should be in
1001 // the MessageBundleTranslation/Hook
1002 $permissionTitleCheck = Title::newFromID( $group->getBundlePageId() );
1003 }
1004
1005 if ( $permissionTitleCheck ) {
1006 // Check for blocks
1007 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1008 if ( $permissionManager->isBlockedFrom( $user, $permissionTitleCheck ) ) {
1009 $block = $user->getBlock();
1010 if ( $block ) {
1011 $error = new UserBlockedError( $block, $user );
1012 $errorMessage = $error->getMessageObject();
1013 $result = array_merge( [ $errorMessage->getKey() ], $errorMessage->getParams() );
1014 return false;
1015 }
1016 }
1017 }
1018 }
1019
1020 // Allow editing units that become orphaned in regular use, so that
1021 // people can delete them or fix links or other issues in them.
1022 if ( $action !== 'create' ) {
1023 return true;
1024 }
1025
1026 if ( !$handle->isValid() ) {
1027 // TODO: These checks may no longer be needed
1028 // Sometimes the message index can be out of date. Either the rebuild job failed or
1029 // it just hasn't finished yet. Do a secondary check to make sure we are not
1030 // inconveniencing translators for no good reason.
1031 // See https://phabricator.wikimedia.org/T221119
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' );
1038 } else {
1039 $isValid = false;
1040 }
1041 }
1042
1043 if ( $isValid ) {
1044 $error = self::getTranslationRestrictions( $handle, $groupId );
1045 $result = $error ?: $result;
1046 return $error === [];
1047 }
1048
1049 // Don't allow editing invalid messages that do not belong to any translatable page
1050 LoggerFactory::getInstance( 'Translate' )->info(
1051 'Unknown translation page: {title}',
1052 [ 'title' => $title->getPrefixedDBkey() ]
1053 );
1054 $result = [ 'tpt-unknown-page' ];
1055 return false;
1056 }
1057
1058 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?TranslatablePage {
1059 $parts = TranslatablePage::parseTranslationUnit( $unit );
1060 $translationPageTitle = Title::newFromText(
1061 $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ]
1062 );
1063 if ( !$translationPageTitle ) {
1064 return null;
1065 }
1066
1067 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1068 if ( !$translatablePage ) {
1069 return null;
1070 }
1071
1072 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1073 $store = $factory->getReader( $translatablePage->getTitle() );
1074 $units = $store->getNames();
1075
1076 if ( !in_array( $parts[ 'section' ], $units ) ) {
1077 return null;
1078 }
1079
1080 return $translatablePage;
1081 }
1082
1090 private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) {
1091 global $wgTranslateDocumentationLanguageCode;
1092
1093 // Allow adding message documentation even when translation is restricted
1094 if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) {
1095 return [];
1096 }
1097
1098 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
1099 // Check if anything is prevented for the group in the first place
1100 $force = $messageGroupMetadata->get( $groupId, 'priorityforce' );
1101 if ( $force !== 'on' ) {
1102 return [];
1103 }
1104
1105 // And finally check whether the language is in the inclusion list
1106 $languages = $messageGroupMetadata->get( $groupId, 'prioritylangs' );
1107 $reason = $messageGroupMetadata->get( $groupId, 'priorityreason' );
1108 if ( !$languages ) {
1109 if ( $reason ) {
1110 return [ 'tpt-translation-restricted-no-priority-languages', $reason ];
1111 }
1112 return [ 'tpt-translation-restricted-no-priority-languages-no-reason' ];
1113 }
1114
1115 $filter = array_flip( explode( ',', $languages ) );
1116 if ( !isset( $filter[$handle->getCode()] ) ) {
1117 if ( $reason ) {
1118 return [ 'tpt-translation-restricted', $reason ];
1119 }
1120
1121 return [ 'tpt-translation-restricted-no-reason' ];
1122 }
1123
1124 return [];
1125 }
1126
1136 public static function preventDirectEditing( Title $title, User $user, $action, &$result ) {
1137 if ( self::$allowTargetEdit ) {
1138 return true;
1139 }
1140
1141 $inclusionList = [
1142 'read', 'deletedtext', 'deletedhistory',
1143 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884
1144 'review', // FlaggedRevs
1145 'patrol', // T151172
1146 ];
1147 $needsPageTranslationRight = in_array( $action, [ 'delete', 'undelete' ] );
1148 if ( in_array( $action, $inclusionList ) ||
1149 ( $needsPageTranslationRight && $user->isAllowed( 'pagetranslation' ) )
1150 ) {
1151 return true;
1152 }
1153
1154 $page = TranslatablePage::isTranslationPage( $title );
1155 if ( $page !== false && $page->getMarkedTag() ) {
1156 if ( $needsPageTranslationRight ) {
1157 $result = User::newFatalPermissionDeniedStatus( 'pagetranslation' )->getMessage();
1158 return false;
1159 }
1160
1161 [ , $code ] = Utilities::figureMessage( $title->getText() );
1162 $mwService = MediaWikiServices::getInstance();
1163
1164 $translationUrl = $mwService->getUrlUtils()->expand(
1165 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1166 );
1167
1168 $result = [
1169 'tpt-target-page',
1170 ':' . $page->getTitle()->getPrefixedText(),
1171 // This url shouldn't get cached
1172 $translationUrl
1173 ];
1174
1175 return false;
1176 }
1177
1178 return true;
1179 }
1180
1191 public static function disableDelete( $article, $out, &$reason ) {
1192 $title = $article->getTitle();
1193 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1194 $isDeletableBundle = $bundle && $bundle->isDeletable();
1195 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1196 $new = SpecialPage::getTitleFor(
1197 'PageTranslationDeletePage',
1198 $title->getPrefixedText()
1199 );
1200 $out->redirect( $new->getFullURL() );
1201 }
1202
1203 return true;
1204 }
1205
1213 public static function translatablePageHeader( $article, &$outputDone, &$pcache ) {
1214 if ( $article->getOldID() ) {
1215 return;
1216 }
1217
1218 $articleTitle = $article->getTitle();
1219 $transPage = TranslatablePage::isTranslationPage( $articleTitle );
1220 $context = $article->getContext();
1221 if ( $transPage ) {
1222 self::translationPageHeader( $context, $transPage );
1223 } else {
1224 $viewTranslatablePage = Services::getInstance()->getTranslatablePageView();
1225 $user = $context->getUser();
1226 if ( $viewTranslatablePage->canDisplayTranslationSettingsBanner( $articleTitle, $user ) ) {
1227 $output = $context->getOutput();
1228 $pageUrl = SpecialPage::getTitleFor( 'PageTranslation' )->getFullURL( [
1229 'do' => 'settings',
1230 'target' => $articleTitle->getPrefixedDBkey(),
1231 ] );
1232 $output->addHTML(
1233 Html::noticeBox(
1234 $context->msg( 'pt-cta-mark-translation', $pageUrl ),
1235 'translate-cta-pt-mark'
1236 )
1237 );
1238 } else {
1239 self::sourcePageHeader( $context );
1240 }
1241 }
1242 }
1243
1244 private static function sourcePageHeader( IContextSource $context ) {
1245 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1246
1247 $language = $context->getLanguage();
1248 $title = $context->getTitle();
1249
1250 $page = TranslatablePage::newFromTitle( $title );
1251
1252 $marked = $page->getMarkedTag();
1253 $ready = $page->getReadyTag();
1254 $latest = $title->getLatestRevID();
1255
1256 $actions = [];
1257 if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) {
1258 $actions[] = self::getTranslateLink( $context, $page, null );
1259 }
1260
1261 $hasChanges = $ready === $latest && $marked !== $latest;
1262 if ( $hasChanges ) {
1263 $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] );
1264
1265 if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) {
1266 $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' );
1267 $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ];
1268
1269 if ( $marked === null ) {
1270 // This page has never been marked
1271 $linkDesc = $context->msg( 'translate-tag-markthis' )->text();
1272 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1273 } else {
1274 $markUrl = $pageTranslation->getFullURL( $params );
1275 $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl )
1276 ->parse();
1277 }
1278 } else {
1279 $actions[] = $context->msg( 'translate-tag-hasnew', $diffUrl )->parse();
1280 }
1281 }
1282
1283 if ( !count( $actions ) ) {
1284 return;
1285 }
1286
1287 $header = Html::rawElement(
1288 'div',
1289 [
1290 'class' => 'mw-pt-translate-header noprint nomobile',
1291 'dir' => $language->getDir(),
1292 'lang' => $language->getHtmlCode(),
1293 ],
1294 $language->semicolonList( $actions )
1295 );
1296
1297 $context->getOutput()->addHTML( $header );
1298 }
1299
1300 private static function getTranslateLink(
1301 IContextSource $context,
1302 TranslatablePage $page,
1303 ?string $langCode
1304 ): string {
1305 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1306
1307 return $linker->makeKnownLink(
1308 SpecialPage::getTitleFor( 'Translate' ),
1309 $context->msg( 'translate-tag-translate-link-desc' )->text(),
1310 [],
1311 [
1312 'group' => $page->getMessageGroupId(),
1313 'language' => $langCode,
1314 'action' => 'page',
1315 'filter' => '',
1316 ]
1317 );
1318 }
1319
1320 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1321 global $wgTranslateKeepOutdatedTranslations;
1322
1323 $title = $context->getTitle();
1324 if ( !$title->exists() ) {
1325 return;
1326 }
1327
1328 [ , $code ] = Utilities::figureMessage( $title->getText() );
1329
1330 // Get the translation percentage
1331 $pers = $page->getTranslationPercentages();
1332 $per = 0;
1333 if ( isset( $pers[$code] ) ) {
1334 $per = $pers[$code] * 100;
1335 }
1336
1337 $language = $context->getLanguage();
1338 $output = $context->getOutput();
1339
1340 if ( $page->getSourceLanguageCode() === $code ) {
1341 // If we are on the source language page, link to translate for user's language
1342 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1343 } else {
1344 $mwService = MediaWikiServices::getInstance();
1345
1346 $translationUrl = $mwService->getUrlUtils()->expand(
1347 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1348 );
1349
1350 $msg = $context->msg( 'tpt-translation-intro',
1351 $translationUrl,
1352 ':' . $page->getTitle()->getPrefixedText(),
1353 $language->formatNum( $per )
1354 )->parse();
1355 }
1356
1357 $header = Html::rawElement(
1358 'div',
1359 [
1360 'class' => 'mw-pt-translate-header noprint',
1361 'dir' => $language->getDir(),
1362 'lang' => $language->getHtmlCode(),
1363 ],
1364 $msg
1365 );
1366
1367 $output->addHTML( $header );
1368
1369 if ( $wgTranslateKeepOutdatedTranslations ) {
1370 $groupId = $page->getMessageGroupId();
1371 // This is already calculated and cached by above call to getTranslationPercentages
1372 $stats = MessageGroupStats::forItem( $groupId, $code );
1373 if ( $stats[MessageGroupStats::FUZZY] ) {
1374 // Only show if there is fuzzy messages
1375 $wrap = Html::rawElement(
1376 'div',
1377 [
1378 'class' => 'mw-pt-translate-header',
1379 'dir' => $language->getDir(),
1380 'lang' => $language->getHtmlCode()
1381 ],
1382 '<span class="mw-translate-fuzzy">$1</span>'
1383 );
1384
1385 $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] );
1386 }
1387 }
1388 }
1389
1394 public static function replaceMovePage( &$list ) {
1395 $movePageSpec = $list['Movepage'] ?? null;
1396
1397 // This should never happen, but apparently is happening? See: T296568
1398 if ( $movePageSpec === null ) {
1399 return;
1400 }
1401
1402 $list['Movepage'] = [
1403 'class' => MoveTranslatableBundleSpecialPage::class,
1404 'services' => [
1405 'ObjectFactory',
1406 'PermissionManager',
1407 'Translate:TranslatableBundleMover',
1408 'Translate:TranslatableBundleFactory',
1409 'FormatterFactory'
1410 ],
1411 'args' => [
1412 $movePageSpec
1413 ]
1414 ];
1415 }
1416
1425 public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) {
1426 if ( $action === 'read' ) {
1427 return true;
1428 }
1429
1430 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1431 $key = $cache->makeKey( 'pt-lock', sha1( $title->getPrefixedText() ) );
1432 if ( $cache->get( $key ) === 'locked' ) {
1433 $result = [ 'pt-locked-page' ];
1434
1435 return false;
1436 }
1437
1438 return true;
1439 }
1440
1448 public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) {
1449 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1450
1451 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1452 if ( !$isTranslationPage
1453 && !TranslatablePage::isSourcePage( $out->getTitle() )
1454 ) {
1455 return true;
1456 }
1457
1458 // Copied from Skin::subPageSubtitle()
1459 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1460 if (
1461 $out->isArticle() &&
1462 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1463 ) {
1464 $ptext = $out->getTitle()->getPrefixedText();
1465 $links = explode( '/', $ptext );
1466 if ( count( $links ) > 1 ) {
1467 array_pop( $links );
1468 if ( $isTranslationPage ) {
1469 // Also remove language code page
1470 array_pop( $links );
1471 }
1472 $c = 0;
1473 $growinglink = '';
1474 $display = '';
1475 $lang = $skin->getLanguage();
1476
1477 foreach ( $links as $link ) {
1478 $growinglink .= $link;
1479 $display .= $link;
1480 $linkObj = Title::newFromText( $growinglink );
1481
1482 if ( $linkObj && $linkObj->isKnown() ) {
1483 $getlink = $linker->makeKnownLink(
1484 SpecialPage::getTitleFor( 'MyLanguage', $growinglink ),
1485 $display
1486 );
1487
1488 $c++;
1489
1490 if ( $c > 1 ) {
1491 $subpages .= $lang->getDirMarkEntity() . $skin->msg( 'pipe-separator' )->escaped();
1492 } else {
1493 $subpages .= '&lt; ';
1494 }
1495
1496 $subpages .= $getlink;
1497 $display = '';
1498 } else {
1499 $display .= '/';
1500 }
1501
1502 $growinglink .= '/';
1503 }
1504 }
1505
1506 return false;
1507 }
1508
1509 return true;
1510 }
1511
1518 public static function translateTab( Skin $skin, array &$tabs ) {
1519 $title = $skin->getTitle();
1520 $handle = new MessageHandle( $title );
1521 $code = $handle->getCode();
1522 $page = TranslatablePage::isTranslationPage( $title );
1523 // The source language has a subpage too, but cannot be translated
1524 if ( !$page || $page->getSourceLanguageCode() === $code ) {
1525 return;
1526 }
1527
1528 $user = $skin->getUser();
1529 if ( isset( $tabs['views']['edit'] ) ) {
1530 // There is an edit tab, just replace its text and URL with ours, keeping the tooltip and access key
1531 $tabs['views']['edit']['text'] = $skin->msg( 'tpt-tab-translate' )->text();
1532 $tabs['views']['edit']['href'] = $page->getTranslationUrl( $code );
1533 } elseif ( $user->isAllowed( 'translate' ) ) {
1534 $mwInstance = MediaWikiServices::getInstance();
1535 $namespaceProtection = $mwInstance->getMainConfig()->get( MainConfigNames::NamespaceProtection );
1536 $permissionManager = $mwInstance->getPermissionManager();
1537 if (
1538 !$permissionManager->userHasAllRights(
1539 $user, ...(array)( $namespaceProtection[ NS_TRANSLATIONS ] ?? [] )
1540 )
1541 ) {
1542 return;
1543 }
1544
1545 $tab = [
1546 'text' => $skin->msg( 'tpt-tab-translate' )->text(),
1547 'href' => $page->getTranslationUrl( $code ),
1548 ];
1549
1550 // Get the position of the viewsource tab within the array (if any)
1551 $viewsourcePos = array_keys( array_keys( $tabs['views'] ), 'viewsource', true )[0] ?? null;
1552
1553 if ( $viewsourcePos !== null ) {
1554 // Remove the viewsource tab and insert the translate tab at its place. Showing the tooltip
1555 // of the viewsource tab for the translate tab would be confusing.
1556 array_splice( $tabs['views'], $viewsourcePos, 1, [ 'translate' => $tab ] );
1557 } else {
1558 // We have neither an edit tab nor a viewsource tab to replace with the translate tab,
1559 // put the translate tab at the end
1560 $tabs['views']['translate'] = $tab;
1561 }
1562 }
1563 }
1564
1577 public static function onMovePageTranslationUnits(
1578 LinkTarget $oldLinkTarget,
1579 LinkTarget $newLinkTarget,
1580 UserIdentity $userIdentity,
1581 int $oldid,
1582 int $newid,
1583 string $reason,
1584 RevisionRecord $revisionRecord
1585 ) {
1586 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1587 // MoveTranslatableBundleJob takes care of handling updates because it performs
1588 // a lot of moves at once. As a performance optimization, skip this hook if
1589 // we detect moves from that job. As there isn't a good way to pass information
1590 // to this hook what originated the move, we use some heuristics.
1591 if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1592 return;
1593 }
1594
1595 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1596 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1597 $groupLast = null;
1598 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1599 $handle = new MessageHandle( $title );
1600 // Documentation pages are never translation pages
1601 if ( !$handle->isValid() || $handle->isDoc() ) {
1602 continue;
1603 }
1604
1605 $group = $handle->getGroup();
1606 if ( !$group instanceof WikiPageMessageGroup ) {
1607 continue;
1608 }
1609
1610 $language = $handle->getCode();
1611
1612 // Ignore pages such as Translations:Page/unit without language code
1613 if ( $language === '' ) {
1614 continue;
1615 }
1616
1617 // Update the page only once if source and destination units
1618 // belong to the same page
1619 if ( $group !== $groupLast ) {
1620 $groupLast = $group;
1621 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1622 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1623 }
1624 }
1625 }
1626
1637 public static function onDeleteTranslationUnit(
1638 WikiPage $unit,
1639 User $user,
1640 $reason,
1641 $id,
1642 $content,
1643 $logEntry
1644 ) {
1645 $title = $unit->getTitle();
1646 $handle = new MessageHandle( $title );
1647 if ( !$handle->isValid() ) {
1648 return;
1649 }
1650
1651 $group = $handle->getGroup();
1652 if ( !$group instanceof WikiPageMessageGroup ) {
1653 return;
1654 }
1655
1656 $mwServices = MediaWikiServices::getInstance();
1657 // During deletions this may cause creation of a lot of duplicate jobs. It is expected that
1658 // job queue will deduplicate them to reduce the number of jobs actually run.
1659 $mwServices->getJobQueueGroup()->push(
1660 RebuildMessageGroupStatsJob::newRefreshGroupsJob( [ $group->getId() ] )
1661 );
1662
1663 // Logic to update translation pages, skipped if we are in a middle of a deletion
1664 if ( self::$isDeleteTranslatableBundleJobRunning ) {
1665 return;
1666 }
1667
1668 $target = $group->getTitle();
1669 $langCode = $handle->getCode();
1670 $fname = __METHOD__;
1671
1672 $dbw = $mwServices->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1673 $callback = function () use (
1674 $dbw,
1675 $target,
1676 $handle,
1677 $langCode,
1678 $user,
1679 $reason,
1680 $fname
1681 ) {
1682 $translationPageTitle = $target->getSubpage( $langCode );
1683 // Do a more thorough check for the translation page in case the translation page is deleted in a
1684 // different transaction.
1685 if ( !$translationPageTitle || !$translationPageTitle->exists( IDBAccessObject::READ_LATEST ) ) {
1686 return;
1687 }
1688
1689 $dbw->startAtomic( $fname );
1690
1691 $page = TranslatablePage::newFromTitle( $target );
1692
1693 if ( !$handle->isDoc() ) {
1694 $unitTitle = $handle->getTitle();
1695 // Assume that $user and $reason for the first deletion is the same for all
1696 self::updateTranslationPage(
1697 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1698 );
1699 }
1700
1701 $dbw->endAtomic( $fname );
1702 };
1703
1704 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1705 }
1706
1711 public static function onReplaceTextFilterPageTitlesForEdit( array &$titles ): void {
1712 foreach ( $titles as $index => $title ) {
1713 $handle = new MessageHandle( $title );
1714 if ( Utilities::isTranslationPage( $handle ) ) {
1715 unset( $titles[ $index ] );
1716 }
1717 }
1718 }
1719
1724 public static function onReplaceTextFilterPageTitlesForRename( array &$titles ): void {
1725 foreach ( $titles as $index => $title ) {
1726 $handle = new MessageHandle( $title );
1727 if (
1728 TranslatablePage::isSourcePage( $title ) ||
1729 Utilities::isTranslationPage( $handle )
1730 ) {
1731 unset( $titles[ $index ] );
1732 }
1733 }
1734 }
1735}
Class for pointing to messages, like Title class is for titles.
getGroup()
Get the primary MessageGroup this message belongs to.
figureMessage()
Recommended to use getCode and getKey instead.
isDoc()
Determine whether the current handle is for message documentation.
static tpSyntaxCheckForEditContent( $context, $content, $status, $summary)
Display nice error when editing content.
Definition Hooks.php:814
static tpSyntaxCheck(RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus)
When attempting to save, last resort.
Definition Hooks.php:864
static onReplaceTextFilterPageTitlesForRename(array &$titles)
Removes translatable and translation pages from the list of titles to be renamed Hook: ReplaceTextFil...
Definition Hooks.php:1724
static bool $isDeleteTranslatableBundleJobRunning
State flag used by DeleteTranslatableBundleJob for performance optimizations.
Definition Hooks.php:65
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...
Definition Hooks.php:1577
static replaceMovePage(&$list)
Hook: SpecialPage_initList.
Definition Hooks.php:1394
static onTitleGetEditNotices(Title $title, int $oldid, array &$notices)
Display an edit notice for translatable source pages if it's enabled Hook: TitleGetEditNotices.
Definition Hooks.php:304
static onParserGetVariableValueSwitch(Parser $parser, array &$variableCache, string $magicWordId, ?string &$ret, PPFrame $frame)
Hook: ParserGetVariableValueSwitch.
Definition Hooks.php:467
static onGetUserPermissionsErrorsExpensive(Title $title, User $user, $action, &$result)
Prevent creation of orphan translation units in Translations namespace.
Definition Hooks.php:977
static preprocessTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforePreprocess.
Definition Hooks.php:161
static onDeleteTranslationUnit(WikiPage $unit, User $user, $reason, $id, $content, $logEntry)
Hook to update translation page on deleting a translation unit Hook: ArticleDeleteComplete.
Definition Hooks.php:1637
static preventDirectEditing(Title $title, User $user, $action, &$result)
Prevent editing of translation pages directly.
Definition Hooks.php:1136
static renderTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforeInternalParse.
Definition Hooks.php:78
static onParserOutputPostCacheTransform(ParserOutput $out, &$text, array &$options)
Hook: ParserOutputPostCacheTransform.
Definition Hooks.php:191
static onSectionSave(WikiPage $wikiPage, User $user, TextContent $content, $summary, $minor, $flags, MessageHandle $handle)
This is triggered after an edit to translation unit page.
Definition Hooks.php:382
static fetchTranslatableTemplateAndTitle(?LinkTarget $contextLink, ?LinkTarget $templateLink, bool &$skip, ?RevisionRecord &$revRecord)
This sets &$revRecord to the revision of transcluded page translation if it exists,...
Definition Hooks.php:212
static onBeforePageDisplay(OutputPage $out, Skin $skin)
Hook: BeforePageDisplay.
Definition Hooks.php:335
static translatablePageHeader( $article, &$outputDone, &$pcache)
Hook: ArticleViewHeader.
Definition Hooks.php:1213
static onPageContentLanguage(Title $title, &$pageLang)
Set the right page content language for translated pages ("Page/xx").
Definition Hooks.php:288
static lockedPagesCheck(Title $title, User $user, $action, &$result)
Hook: getUserPermissionsErrorsExpensive.
Definition Hooks.php:1425
static updateTranstagOnNullRevisions(RevisionRecord $rev)
Page moving and page protection (and possibly other things) creates null revisions.
Definition Hooks.php:941
static onVisualEditorBeforeEditor(OutputPage $out, Skin $skin)
Hook: onVisualEditorBeforeEditor.
Definition Hooks.php:368
static replaceSubtitle(&$subpages, ?Skin $skin, OutputPage $out)
Hook: SkinSubPageSubtitle.
Definition Hooks.php:1448
static formatLanguageLink(array &$link, Title $linkTitle, Title $pageTitle, OutputPage $out)
Hooks: SkinTemplateGetLanguageLink.
Definition Hooks.php:781
static onGetMagicVariableIDs(&$variableIDs)
Hook: GetMagicVariableIDs.
Definition Hooks.php:460
static languages( $data, $params, $parser)
Definition Hooks.php:490
static addLanguageLinks(Title $title, array &$languageLinks)
Hooks: LanguageLinks.
Definition Hooks.php:696
static disableDelete( $article, $out, &$reason)
Redirects the delete action to our own for translatable pages.
Definition Hooks.php:1191
static onReplaceTextFilterPageTitlesForEdit(array &$titles)
Removes translation pages from the list of page titles to be edited Hook: ReplaceTextFilterPageTitles...
Definition Hooks.php:1711
static translateTab(Skin $skin, array &$tabs)
Converts the edit tab (if exists) for translation pages to translate tab.
Definition Hooks.php:1518
static addTranstagAfterSave(WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revisionRecord, EditResult $editResult)
Hook: PageSaveComplete.
Definition Hooks.php:896
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.
getMarkedTag()
Returns the latest revision which has marked tag, if any.
getReadyTag()
Returns the latest revision which has ready tag, if any.
getTranslationUrl( $code=false)
Produces a link to translation view of a translation page.
getPageDisplayTitle(string $languageCode)
Get translated page title.
This class represents one translation unit in a translatable page.
Minimal service container.
Definition Services.php:58
This class aims to provide efficient mechanism for fetching translation completion stats.
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
Wraps the translatable page sections into a message group.