Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
Hooks.php
1<?php
2
3namespace MediaWiki\Extension\Translate\PageTranslation;
4
5use Article;
6use CommentStoreComment;
7use Content;
8use DeferredUpdates;
9use Exception;
10use Html;
11use IContextSource;
12use Language;
13use LanguageCode;
14use ManualLogEntry;
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;
33use ObjectCache;
34use OutputPage;
35use Parser;
36use ParserOutput;
37use PPFrame;
38use RequestContext;
39use Skin;
40use SpecialPage;
41use Status;
42use StubUserLang;
43use TextContent;
44use Title;
46use User;
47use UserBlockedError;
48use Wikimedia\ScopedCallback;
49use WikiPage;
51use WikitextContent;
52
59class Hooks {
60 // Uuugly hacks
61 public static $allowTargetEdit = false;
62 // Check if job queue is running
63 public static $jobQueueRunning = false;
64 // Check if we are just rendering tags or such
65 public static $renderingContext = false;
66 // Used to communicate data between LanguageLinks and SkinTemplateGetLanguageLink hooks.
67 private static $languageLinkData = [];
68
76 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
77 if ( $text === null ) {
78 // SMW is unhelpfully sending null text if source contains section tags. Do not explode.
79 return;
80 }
81
82 self::preprocessTagPage( $wikitextParser, $text, $state );
83
84 // Skip further interface message parsing
85 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
86 return;
87 }
88
89 // For section previews, perform additional clean-up, given tags are often
90 // unbalanced when we preview one section only.
91 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
92 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
93 $text = $translatablePageParser->cleanupTags( $text );
94 }
95
96 // Set display title
97 $title = MediaWikiServices::getInstance()
98 ->getTitleFactory()
99 ->castFromPageReference( $wikitextParser->getPage() );
100 if ( !$title ) {
101 return;
102 }
103
104 $page = TranslatablePage::isTranslationPage( $title );
105 if ( !$page ) {
106 return;
107 }
108
109 try {
110 self::$renderingContext = true;
111 [ , $code ] = Utilities::figureMessage( $title->getText() );
112 $name = $page->getPageDisplayTitle( $code );
113 if ( $name ) {
114 $name = $wikitextParser->recursivePreprocess( $name );
115
116 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
117 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
118 $name = $langConv->convert( $name );
119 $wikitextParser->getOutput()->setDisplayTitle( $name );
120 }
121 self::$renderingContext = false;
122 } catch ( Exception $e ) {
123 LoggerFactory::getInstance( 'Translate' )->error(
124 'T302754 Failed to set display title for page {title}',
125 [
126 'title' => $title->getPrefixedDBkey(),
127 'text' => $text,
128 'pageid' => $title->getId(),
129 ]
130 );
131
132 // Re-throw to preserve behavior
133 throw $e;
134 }
135
136 $extensionData = [
137 'languagecode' => $code,
138 'messagegroupid' => $page->getMessageGroupId(),
139 'sourcepagetitle' => [
140 'namespace' => $page->getTitle()->getNamespace(),
141 'dbkey' => $page->getTitle()->getDBkey()
142 ]
143 ];
144
145 $wikitextParser->getOutput()->setExtensionData(
146 'translate-translation-page', $extensionData
147 );
148
149 // Disable edit section links
150 $wikitextParser->getOutput()->setExtensionData( 'Translate-noeditsection', true );
151 }
152
160 public static function preprocessTagPage( $wikitextParser, &$text, $state ): void {
161 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
162
163 if ( $translatablePageParser->containsMarkup( $text ) ) {
164 try {
165 $parserOutput = $translatablePageParser->parse( $text );
166 // If parsing succeeds, replace text and add styles
167 $text = $parserOutput->sourcePageTextForRendering(
168 $wikitextParser->getTargetLanguage()
169 );
170 $wikitextParser->getOutput()->addModuleStyles( [
171 'ext.translate',
172 ] );
173 } catch ( ParsingFailure $e ) {
174 wfDebug( 'ParsingFailure caught; expected' );
175 }
176 } else {
177 // If the text doesn't contain <translate> markup, it can still contain <tvar> in the
178 // context of a Parsoid template expansion sub-pipeline. We strip these as well.
179 $unit = new TranslationUnit( $text );
180 $text = $unit->getTextForTrans();
181 }
182 }
183
190 public static function onParserOutputPostCacheTransform(
191 ParserOutput $out,
192 &$text,
193 array &$options
194 ) {
195 if ( $out->getExtensionData( 'Translate-noeditsection' ) ) {
196 $options['enableSectionEditLinks'] = false;
197 }
198 }
199
211 public static function fetchTranslatableTemplateAndTitle(
212 ?LinkTarget $contextLink,
213 ?LinkTarget $templateLink,
214 bool &$skip,
215 ?RevisionRecord &$revRecord
216 ): void {
217 if ( !$templateLink ) {
218 return;
219 }
220
221 $templateTitle = Title::castFromLinkTarget( $templateLink );
222
223 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
224 if ( $templateTranslationPage ) {
225 // Template is referring to a translation page, fetch it and incase it doesn't
226 // exist, fetch the source fallback.
227 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
228 if ( !$revRecord ) {
229 // In rare cases fetching of the source fallback might fail. See: T323863
230 LoggerFactory::getInstance( 'Translate' )->warning(
231 "T323863: Could not fetch any revision record for '{groupid}'",
232 [ 'groupid' => $templateTranslationPage->getMessageGroupId() ]
233 );
234 }
235 return;
236 }
237
238 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
239 return;
240 }
241
242 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
243
244 if ( !( $translatableTemplatePage->supportsTransclusion() ?? false ) ) {
245 // Page being transcluded does not support language aware transclusion
246 return;
247 }
248
249 $store = MediaWikiServices::getInstance()->getRevisionStore();
250
251 if ( $contextLink ) {
252 // Fetch the context page language, and then check if template is present in that language
253 $templateTranslationTitle = $templateTitle->getSubpage(
254 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
255 );
256
257 if ( $templateTranslationTitle ) {
258 if ( $templateTranslationTitle->exists() ) {
259 // Template is present in the context page language, fetch the revision record and return
260 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
261 } else {
262 // In case the template has not been translated to the context page language,
263 // we assign a MutableRevisionRecord in order to add a dependency, so that when
264 // it is created, the newly created page is loaded rather than the fallback
265 $revRecord = new MutableRevisionRecord( $templateTranslationTitle );
266 }
267 return;
268 }
269 }
270
271 // Context page information not available OR the template translation title could not be determined.
272 // Fetch and return the RevisionRecord of the template in the source language
273 $sourceTemplateTitle = $templateTitle->getSubpage(
274 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
275 );
276 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
277 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
278 }
279 }
280
287 public static function onPageContentLanguage( Title $title, &$pageLang ) {
288 // For translation pages, parse plural, grammar etc. with correct language,
289 // and set the right direction
290 if ( TranslatablePage::isTranslationPage( $title ) ) {
291 [ , $code ] = Utilities::figureMessage( $title->getText() );
292 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
293 }
294 }
295
303 public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) {
304 if ( TranslatablePage::isSourcePage( $title ) ) {
305 $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
306 if ( !$msg->isDisabled() ) {
307 $notices['translate-tag'] = $msg->parseAsBlock();
308 }
309
310 $notices[] = Html::warningBox(
311 wfMessage( 'tps-edit-sourcepage-text' )->parse(),
312 'translate-edit-documentation'
313 );
314
315 // The check is "we're using visual editor for WYSIWYG" (as opposed to "for wikitext
316 // edition") - the message will not be displayed in that case.
317 $request = RequestContext::getMain()->getRequest();
318 if ( $request->getVal( 'action' ) === 'visualeditor' &&
319 $request->getVal( 'paction' ) !== 'wikitext'
320 ) {
321 $notices[] = Html::warningBox(
322 wfMessage( 'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
323 'translate-edit-documentation'
324 );
325 }
326 }
327 }
328
334 public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
335 global $wgTranslatePageTranslationULS;
336
337 $title = $out->getTitle();
338 $isSource = TranslatablePage::isSourcePage( $title );
339 $isTranslation = TranslatablePage::isTranslationPage( $title );
340
341 if ( $isSource || $isTranslation ) {
342 if ( $wgTranslatePageTranslationULS ) {
343 $out->addModules( 'ext.translate.pagetranslation.uls' );
344 }
345
346 if ( $isSource ) {
347 // Adding a help notice
348 $out->addModuleStyles( 'ext.translate.edit.documentation.styles' );
349 }
350
351 if ( $isTranslation ) {
352 // Source pages get this module via <translate>, but for translation
353 // pages we need to add it manually.
354 $out->addModuleStyles( 'ext.translate' );
355 }
356
357 $out->addJsConfigVars( 'wgTranslatePageTranslation', $isTranslation ? 'translation' : 'source' );
358 }
359 }
360
367 public static function onVisualEditorBeforeEditor( OutputPage $out, Skin $skin ) {
368 return !TranslatablePage::isTranslationPage( $out->getTitle() );
369 }
370
381 public static function onSectionSave(
382 WikiPage $wikiPage,
383 User $user,
384 TextContent $content,
385 $summary,
386 $minor,
387 $flags,
388 MessageHandle $handle
389 ) {
390 // FuzzyBot may do some duplicate work already worked on by other jobs
391 if ( $user->equals( FuzzyBot::getUser() ) ) {
392 return;
393 }
394
395 $group = $handle->getGroup();
396 if ( !$group instanceof WikiPageMessageGroup ) {
397 return;
398 }
399
400 // Finally we know the title and can construct a Translatable page
401 $page = TranslatablePage::newFromTitle( $group->getTitle() );
402
403 // Update the target translation page
404 if ( !$handle->isDoc() ) {
405 $code = $handle->getCode();
406 DeferredUpdates::addCallableUpdate(
407 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
408 $unitTitle = $handle->getTitle();
409 self::updateTranslationPage( $page, $code, $user, $flags, $summary, null, $unitTitle );
410 }
411 );
412 }
413 }
414
415 private static function updateTranslationPage(
416 TranslatablePage $page,
417 string $code,
418 User $user,
419 int $flags,
420 string $summary,
421 ?string $triggerAction = null,
422 ?Title $unitTitle = null
423 ): void {
424 $source = $page->getTitle();
425 $target = $source->getSubpage( $code );
426 $mwInstance = MediaWikiServices::getInstance();
427
428 // We don't know and don't care
429 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
430
431 // Update the target page
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 );
438
439 // Invalidate caches so that language bar is up-to-date
440 $pages = $page->getTranslationPages();
441 $wikiPageFactory = $mwInstance->getWikiPageFactory();
442 foreach ( $pages as $title ) {
443 if ( $title->equals( $target ) ) {
444 // Handled by the RenderTranslationPageJob
445 continue;
446 }
447
448 $wikiPage = $wikiPageFactory->newFromTitle( $title );
449 $wikiPage->doPurge();
450 }
451 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
452 $sourceWikiPage->doPurge();
453 }
454
459 public static function onGetMagicVariableIDs( &$variableIDs ): void {
460 $variableIDs[] = 'translatablepage';
461 }
462
466 public static function onParserGetVariableValueSwitch(
467 Parser $parser,
468 array &$variableCache,
469 string $magicWordId,
470 ?string &$ret,
471 PPFrame $frame
472 ): void {
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;
479 break;
480 }
481 }
482
489 public static function languages( $data, $params, $parser ) {
490 global $wgPageTranslationLanguageList;
491
492 if ( $wgPageTranslationLanguageList === 'sidebar-only' ) {
493 return '';
494 }
495
496 self::$renderingContext = true;
497 $context = new ScopedCallback( static function () {
498 self::$renderingContext = false;
499 } );
500
501 // Add a dummy language link that is removed in self::addLanguageLinks.
502 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) {
503 $parser->getOutput()->addLanguageLink( 'x-pagetranslation-tag' );
504 }
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 ->inLanguage( $userLang )
565 ->params( $pagename )
566 ->numParams( 100 * $percent )
567 ->text();
568 $attribs = [
569 'title' => $title,
570 'class' => $classes,
571 'lang' => $lang->getHtmlCode(),
572 'dir' => $lang->getDir(),
573 ];
574
575 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
576 } else {
577 /* When language is included because it is a priority language,
578 * but translations don't exist link directly to the
579 * translation view. */
580 $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' );
581 $params = [
582 'group' => $page->getMessageGroupId(),
583 'language' => $code,
584 'task' => 'view'
585 ];
586
587 $classes[] = 'new'; // For red link color
588
589 $attribs = [
590 'title' => wfMessage( 'tpt-languages-zero' )->inLanguage( $userLang )->text(),
591 'class' => $classes,
592 'lang' => $lang->getHtmlCode(),
593 'dir' => $lang->getDir(),
594 ];
595 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
596 }
597 $languages[ $name ] = Html::rawElement( 'li', [], $contents );
598 }
599
600 // Sort languages by autonym
601 ksort( $languages );
602 $languages = array_values( $languages );
603 $languages = implode( "\n", $languages );
604
605 $out = Html::openElement( 'div', [
606 'class' => 'mw-pt-languages noprint',
607 'lang' => $userLang->getHtmlCode(),
608 'dir' => $userLang->getDir()
609 ] );
610 $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ],
611 wfMessage( 'tpt-languages-legend' )->inLanguage( $userLang )->escaped()
612 );
613 $out .= Html::rawElement(
614 'ul',
615 [ 'class' => 'mw-pt-languages-list' ],
616 $languages
617 );
618 $out .= Html::closeElement( 'div' );
619
620 $parser->getOutput()->addModuleStyles( [
621 'ext.translate.tag.languages',
622 ] );
623
624 return $out;
625 }
626
633 private static function tpProgressIcon( float $percent ) {
634 $classes = [ 'mw-pt-progress' ];
635 $percent *= 100;
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';
644 } else {
645 $classes[] = 'mw-pt-progress--complete';
646 }
647 return $classes;
648 }
649
654 private static function getTranslatablePageStatus( ?Title $title ): ?array {
655 if ( $title === null ) {
656 return null;
657 }
658 // Check if this is a source page or a translation page
659 $page = TranslatablePage::newFromTitle( $title );
660 if ( $page->getMarkedTag() === null ) {
661 $page = TranslatablePage::isTranslationPage( $title );
662 }
663
664 if ( $page === false || $page->getMarkedTag() === null ) {
665 return null;
666 }
667
668 $status = $page->getTranslationPercentages();
669 if ( !$status ) {
670 return null;
671 }
672
673 // If priority languages have been set, always show those languages
674 $priorityLangs = TranslateMetadata::get( $page->getMessageGroupId(), 'prioritylangs' );
675 $priorityForce = TranslateMetadata::get( $page->getMessageGroupId(), 'priorityforce' );
676 $filter = null;
677 if ( (string)$priorityLangs !== '' ) {
678 $filter = array_flip( explode( ',', $priorityLangs ) );
679 }
680 if ( $filter !== null ) {
681 // If translation is restricted to some languages, only show them
682 if ( $priorityForce === 'on' ) {
683 // Do not filter the source language link
684 $filter[$page->getMessageGroup()->getSourceLanguage()] = true;
685 $status = array_intersect_key( $status, $filter );
686 }
687 foreach ( $filter as $langCode => $value ) {
688 if ( !isset( $status[$langCode] ) ) {
689 // We need to show all priority languages even if no translation started
690 $status[$langCode] = 0;
691 }
692 }
693 }
694
695 return [
696 'page' => $page,
697 'languages' => $status
698 ];
699 }
700
706 public static function addLanguageLinks( Title $title, array &$languageLinks ) {
707 global $wgPageTranslationLanguageList;
708
709 $hasLanguagesTag = false;
710 foreach ( $languageLinks as $index => $name ) {
711 if ( $name === 'x-pagetranslation-tag' ) {
712 $hasLanguagesTag = true;
713 unset( $languageLinks[ $index ] );
714 }
715 }
716
717 if ( $wgPageTranslationLanguageList === 'tag-only' ) {
718 return;
719 }
720
721 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' && $hasLanguagesTag ) {
722 return;
723 }
724
725 // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only'
726
727 $status = self::getTranslatablePageStatus( $title );
728 if ( !$status ) {
729 return;
730 }
731
732 self::$renderingContext = true;
733 $context = new ScopedCallback( static function () {
734 self::$renderingContext = false;
735 } );
736
737 $page = $status[ 'page' ];
738 $languages = $status[ 'languages' ];
739 $mwServices = MediaWikiServices::getInstance();
740 $en = $mwServices->getLanguageFactory()->getLanguage( 'en' );
741
742 $newLanguageLinks = [];
743
744 // Batch the Title::exists queries used below
745 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
746 foreach ( array_keys( $languages ) as $code ) {
747 $title = $page->getTitle()->getSubpage( $code );
748 $lb->addObj( $title );
749 }
750 $lb->execute();
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();
756
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 );
763 } else {
764 $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [
765 'group' => $page->getMessageGroupId(),
766 'language' => $code,
767 ] );
768 $classes = [ 'mw-pt-progress--none' ];
769 $title = wfMessage( 'tpt-languages-zero' );
770 }
771
772 self::$languageLinkData[ $key ] = [
773 'href' => $href,
774 'language' => $code,
775 'percentage' => $percentage,
776 'classes' => $classes,
777 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
778 'title' => $title,
779 ];
780
781 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][ 'autonym' ];
782 }
783
784 asort( $newLanguageLinks );
785 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
786 }
787
795 public static function formatLanguageLink(
796 array &$link,
797 Title $linkTitle,
798 Title $pageTitle,
799 OutputPage $out
800 ) {
801 if ( !str_starts_with( $link['text'], 'x-pagetranslation:' ) ||
802 !isset( self::$languageLinkData[ $link['text'] ] )
803 ) {
804 return;
805 }
806
807 $data = self::$languageLinkData[ $link[ 'text' ] ];
808
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' ] );
815
816 $out->addModuleStyles( 'ext.translate.tag.languages' );
817 }
818
828 public static function tpSyntaxCheckForEditContent(
829 $context,
830 $content,
831 $status,
832 $summary
833 ) {
834 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
835
836 if ( $syntaxErrorStatus ) {
837 $status->merge( $syntaxErrorStatus );
838 return $syntaxErrorStatus->isGood();
839 }
840
841 return true;
842 }
843
844 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
845 // T163254: Ignore translation markup on non-wikitext pages
846 if ( !$content instanceof WikitextContent || !$page ) {
847 return null;
848 }
849
850 $text = $content->getText();
851
852 // See T154500
853 $text = TextContent::normalizeLineEndings( $text );
854 $status = Status::newGood();
855 $parser = Services::getInstance()->getTranslatablePageParser();
856 if ( $parser->containsMarkup( $text ) ) {
857 try {
858 $parser->parse( $text );
859 } catch ( ParsingFailure $e ) {
860 $status->fatal( ...( $e->getMessageSpecification() ) );
861 }
862 }
863
864 return $status;
865 }
866
878 public static function tpSyntaxCheck(
879 RenderedRevision $renderedRevision,
880 UserIdentity $user,
881 CommentStoreComment $summary,
882 $flags,
883 Status $hookStatus
884 ) {
885 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
886
887 $status = self::tpSyntaxError(
888 $renderedRevision->getRevision()->getPage(),
889 $content
890 );
891
892 if ( $status ) {
893 $hookStatus->merge( $status );
894 return $status->isGood();
895 }
896
897 return true;
898 }
899
910 public static function addTranstagAfterSave(
911 WikiPage $wikiPage,
912 UserIdentity $userIdentity,
913 string $summary,
914 int $flags,
915 RevisionRecord $revisionRecord,
916 EditResult $editResult
917 ) {
918 $content = $wikiPage->getContent();
919
920 // T163254: Disable page translation on non-wikitext pages
921 if ( $content instanceof WikitextContent ) {
922 $text = $content->getText();
923 } else {
924 // Not applicable
925 return;
926 }
927
928 $parser = Services::getInstance()->getTranslatablePageParser();
929 if ( $parser->containsMarkup( $text ) ) {
930 // Add the ready tag
931 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
932 $page->addReadyTag( $revisionRecord->getId() );
933 }
934
935 // Schedule a deferred status update for the translatable page.
936 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
937 $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() );
938 }
939
955 public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) {
956 $parentId = $rev->getParentId();
957 if ( $parentId === 0 || $parentId === null ) {
958 // No parent, bail out.
959 return;
960 }
961
962 $prevRev = MediaWikiServices::getInstance()
963 ->getRevisionLookup()
964 ->getRevisionById( $parentId );
965
966 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
967 // Not a null revision, bail out.
968 return;
969 }
970
971 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
972 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
973 $bundle = $bundleFactory->getBundle( $title );
974
975 if ( $bundle ) {
976 $bundleStore = $bundleFactory->getStore( $bundle );
977 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
978 }
979 }
980
992 Title $title,
993 User $user,
994 $action,
995 &$result
996 ) {
997 $handle = new MessageHandle( $title );
998
999 if ( !$handle->isPageTranslation() || $action === 'read' ) {
1000 return true;
1001 }
1002
1003 $isValid = true;
1004 $groupId = null;
1005
1006 if ( $handle->isValid() ) {
1007 $group = $handle->getGroup();
1008 $groupId = $group->getId();
1009 $permissionTitleCheck = null;
1010
1011 if ( $group instanceof WikiPageMessageGroup ) {
1012 $permissionTitleCheck = $group->getTitle();
1013 } elseif ( $group instanceof MessageBundleMessageGroup ) {
1014 // TODO: This check for MessageBundle related permission should be in
1015 // the MessageBundleTranslation/Hook
1016 $permissionTitleCheck = Title::newFromID( $group->getBundlePageId() );
1017 }
1018
1019 if ( $permissionTitleCheck ) {
1020 // Check for blocks
1021 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1022 if ( $permissionManager->isBlockedFrom( $user, $permissionTitleCheck ) ) {
1023 $block = $user->getBlock();
1024 if ( $block ) {
1025 $error = new UserBlockedError( $block, $user );
1026 $errorMessage = $error->getMessageObject();
1027 $result = array_merge( [ $errorMessage->getKey() ], $errorMessage->getParams() );
1028 return false;
1029 }
1030 }
1031 }
1032 }
1033
1034 // Allow editing units that become orphaned in regular use, so that
1035 // people can delete them or fix links or other issues in them.
1036 if ( $action !== 'create' ) {
1037 return true;
1038 }
1039
1040 if ( !$handle->isValid() ) {
1041 // TODO: These checks may no longer be needed
1042 // Sometimes the message index can be out of date. Either the rebuild job failed or
1043 // it just hasn't finished yet. Do a secondary check to make sure we are not
1044 // inconveniencing translators for no good reason.
1045 // See https://phabricator.wikimedia.org/T221119
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' );
1052 } else {
1053 $isValid = false;
1054 }
1055 }
1056
1057 if ( $isValid ) {
1058 $error = self::getTranslationRestrictions( $handle, $groupId );
1059 $result = $error ?: $result;
1060 return $error === [];
1061 }
1062
1063 // Don't allow editing invalid messages that do not belong to any translatable page
1064 LoggerFactory::getInstance( 'Translate' )->info(
1065 'Unknown translation page: {title}',
1066 [ 'title' => $title->getPrefixedDBkey() ]
1067 );
1068 $result = [ 'tpt-unknown-page' ];
1069 return false;
1070 }
1071
1072 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?TranslatablePage {
1073 $parts = TranslatablePage::parseTranslationUnit( $unit );
1074 $translationPageTitle = Title::newFromText(
1075 $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ]
1076 );
1077 if ( !$translationPageTitle ) {
1078 return null;
1079 }
1080
1081 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1082 if ( !$translatablePage ) {
1083 return null;
1084 }
1085
1086 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1087 $store = $factory->getReader( $translatablePage->getTitle() );
1088 $units = $store->getNames();
1089
1090 if ( !in_array( $parts[ 'section' ], $units ) ) {
1091 return null;
1092 }
1093
1094 return $translatablePage;
1095 }
1096
1104 private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) {
1105 global $wgTranslateDocumentationLanguageCode;
1106
1107 // Allow adding message documentation even when translation is restricted
1108 if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) {
1109 return [];
1110 }
1111
1112 // Check if anything is prevented for the group in the first place
1113 $force = TranslateMetadata::get( $groupId, 'priorityforce' );
1114 if ( $force !== 'on' ) {
1115 return [];
1116 }
1117
1118 // And finally check whether the language is in the inclusion list
1119 $languages = TranslateMetadata::get( $groupId, 'prioritylangs' );
1120 $filter = array_flip( explode( ',', $languages ) );
1121 if ( !isset( $filter[$handle->getCode()] ) ) {
1122 $reason = TranslateMetadata::get( $groupId, 'priorityreason' );
1123 if ( $reason ) {
1124 return [ 'tpt-translation-restricted', $reason ];
1125 }
1126
1127 return [ 'tpt-translation-restricted-no-reason' ];
1128 }
1129
1130 return [];
1131 }
1132
1142 public static function preventDirectEditing( Title $title, User $user, $action, &$result ) {
1143 if ( self::$allowTargetEdit ) {
1144 return true;
1145 }
1146
1147 $inclusionList = [
1148 'read', 'deletedtext', 'deletedhistory',
1149 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884
1150 'review', // FlaggedRevs
1151 'patrol', // T151172
1152 ];
1153 $needsPageTranslationRight = in_array( $action, [ 'delete', 'undelete' ] );
1154 if ( in_array( $action, $inclusionList ) ||
1155 $needsPageTranslationRight && $user->isAllowed( 'pagetranslation' )
1156 ) {
1157 return true;
1158 }
1159
1160 $page = TranslatablePage::isTranslationPage( $title );
1161 if ( $page !== false && $page->getMarkedTag() ) {
1162 if ( $needsPageTranslationRight ) {
1163 $result = User::newFatalPermissionDeniedStatus( 'pagetranslation' )->getMessage();
1164 return false;
1165 }
1166
1167 [ , $code ] = Utilities::figureMessage( $title->getText() );
1168 $mwService = MediaWikiServices::getInstance();
1169
1170 $translationUrl = $mwService->getUrlUtils()->expand(
1171 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1172 );
1173
1174 $result = [
1175 'tpt-target-page',
1176 ':' . $page->getTitle()->getPrefixedText(),
1177 // This url shouldn't get cached
1178 $translationUrl
1179 ];
1180
1181 return false;
1182 }
1183
1184 return true;
1185 }
1186
1197 public static function disableDelete( $article, $out, &$reason ) {
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()
1205 );
1206 $out->redirect( $new->getFullURL() );
1207 }
1208
1209 return true;
1210 }
1211
1219 public static function translatablePageHeader( $article, &$outputDone, &$pcache ) {
1220 if ( $article->getOldID() ) {
1221 return;
1222 }
1223
1224 $transPage = TranslatablePage::isTranslationPage( $article->getTitle() );
1225 $context = $article->getContext();
1226 if ( $transPage ) {
1227 self::translationPageHeader( $context, $transPage );
1228 } else {
1229 // Check for pages that are tagged or marked
1230 self::sourcePageHeader( $context );
1231 }
1232 }
1233
1234 private static function sourcePageHeader( IContextSource $context ) {
1235 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1236
1237 $language = $context->getLanguage();
1238 $title = $context->getTitle();
1239
1240 $page = TranslatablePage::newFromTitle( $title );
1241
1242 $marked = $page->getMarkedTag();
1243 $ready = $page->getReadyTag();
1244 $latest = $title->getLatestRevID();
1245
1246 $actions = [];
1247 if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) {
1248 $actions[] = self::getTranslateLink( $context, $page, null );
1249 }
1250
1251 $hasChanges = $ready === $latest && $marked !== $latest;
1252 if ( $hasChanges ) {
1253 $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] );
1254
1255 if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) {
1256 $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' );
1257 $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ];
1258
1259 if ( $marked === null ) {
1260 // This page has never been marked
1261 $linkDesc = $context->msg( 'translate-tag-markthis' )->text();
1262 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1263 } else {
1264 $markUrl = $pageTranslation->getFullURL( $params );
1265 $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl )
1266 ->parse();
1267 }
1268 } else {
1269 $actions[] = $context->msg( 'translate-tag-hasnew', $diffUrl )->parse();
1270 }
1271 }
1272
1273 if ( !count( $actions ) ) {
1274 return;
1275 }
1276
1277 $header = Html::rawElement(
1278 'div',
1279 [
1280 'class' => 'mw-pt-translate-header noprint nomobile',
1281 'dir' => $language->getDir(),
1282 'lang' => $language->getHtmlCode(),
1283 ],
1284 $language->semicolonList( $actions )
1285 );
1286
1287 $context->getOutput()->addHTML( $header );
1288 }
1289
1290 private static function getTranslateLink(
1291 IContextSource $context,
1292 TranslatablePage $page,
1293 ?string $langCode
1294 ): string {
1295 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1296
1297 return $linker->makeKnownLink(
1298 SpecialPage::getTitleFor( 'Translate' ),
1299 $context->msg( 'translate-tag-translate-link-desc' )->text(),
1300 [],
1301 [
1302 'group' => $page->getMessageGroupId(),
1303 'language' => $langCode,
1304 'action' => 'page',
1305 'filter' => '',
1306 ]
1307 );
1308 }
1309
1310 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1311 global $wgTranslateKeepOutdatedTranslations;
1312
1313 $title = $context->getTitle();
1314 if ( !$title->exists() ) {
1315 return;
1316 }
1317
1318 [ , $code ] = Utilities::figureMessage( $title->getText() );
1319
1320 // Get the translation percentage
1321 $pers = $page->getTranslationPercentages();
1322 $per = 0;
1323 if ( isset( $pers[$code] ) ) {
1324 $per = $pers[$code] * 100;
1325 }
1326
1327 $language = $context->getLanguage();
1328 $output = $context->getOutput();
1329
1330 if ( $page->getSourceLanguageCode() === $code ) {
1331 // If we are on the source language page, link to translate for user's language
1332 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1333 } else {
1334 $mwService = MediaWikiServices::getInstance();
1335
1336 $translationUrl = $mwService->getUrlUtils()->expand(
1337 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1338 );
1339
1340 $msg = $context->msg( 'tpt-translation-intro',
1341 $translationUrl,
1342 ':' . $page->getTitle()->getPrefixedText(),
1343 $language->formatNum( $per )
1344 )->parse();
1345 }
1346
1347 $header = Html::rawElement(
1348 'div',
1349 [
1350 'class' => 'mw-pt-translate-header noprint',
1351 'dir' => $language->getDir(),
1352 'lang' => $language->getHtmlCode(),
1353 ],
1354 $msg
1355 );
1356
1357 $output->addHTML( $header );
1358
1359 if ( $wgTranslateKeepOutdatedTranslations ) {
1360 $groupId = $page->getMessageGroupId();
1361 // This is already calculated and cached by above call to getTranslationPercentages
1362 $stats = MessageGroupStats::forItem( $groupId, $code );
1363 if ( $stats[MessageGroupStats::FUZZY] ) {
1364 // Only show if there is fuzzy messages
1365 $wrap = Html::rawElement(
1366 'div',
1367 [
1368 'class' => 'mw-pt-translate-header',
1369 'dir' => $language->getDir(),
1370 'lang' => $language->getHtmlCode()
1371 ],
1372 '<span class="mw-translate-fuzzy">$1</span>'
1373 );
1374
1375 $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] );
1376 }
1377 }
1378 }
1379
1384 public static function replaceMovePage( &$list ) {
1385 $movePageSpec = $list['Movepage'] ?? null;
1386
1387 // This should never happen, but apparently is happening? See: T296568
1388 if ( $movePageSpec === null ) {
1389 return;
1390 }
1391
1392 $list['Movepage'] = [
1393 'class' => MoveTranslatableBundleSpecialPage::class,
1394 'services' => [
1395 'ObjectFactory',
1396 'PermissionManager',
1397 'Translate:TranslatableBundleMover',
1398 'Translate:TranslatableBundleFactory'
1399 ],
1400 'args' => [
1401 $movePageSpec
1402 ]
1403 ];
1404 }
1405
1414 public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) {
1415 if ( $action === 'read' ) {
1416 return true;
1417 }
1418
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' ];
1423
1424 return false;
1425 }
1426
1427 return true;
1428 }
1429
1437 public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) {
1438 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1439
1440 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1441 if ( !$isTranslationPage
1442 && !TranslatablePage::isSourcePage( $out->getTitle() )
1443 ) {
1444 return true;
1445 }
1446
1447 // Copied from Skin::subPageSubtitle()
1448 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1449 if (
1450 $out->isArticle() &&
1451 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1452 ) {
1453 $ptext = $out->getTitle()->getPrefixedText();
1454 $links = explode( '/', $ptext );
1455 if ( count( $links ) > 1 ) {
1456 array_pop( $links );
1457 if ( $isTranslationPage ) {
1458 // Also remove language code page
1459 array_pop( $links );
1460 }
1461 $c = 0;
1462 $growinglink = '';
1463 $display = '';
1464 $lang = $skin->getLanguage();
1465
1466 foreach ( $links as $link ) {
1467 $growinglink .= $link;
1468 $display .= $link;
1469 $linkObj = Title::newFromText( $growinglink );
1470
1471 if ( $linkObj && $linkObj->isKnown() ) {
1472 $getlink = $linker->makeKnownLink(
1473 SpecialPage::getTitleFor( 'MyLanguage', $growinglink ),
1474 $display
1475 );
1476
1477 $c++;
1478
1479 if ( $c > 1 ) {
1480 $subpages .= $lang->getDirMarkEntity() . $skin->msg( 'pipe-separator' )->escaped();
1481 } else {
1482 $subpages .= '&lt; ';
1483 }
1484
1485 $subpages .= $getlink;
1486 $display = '';
1487 } else {
1488 $display .= '/';
1489 }
1490
1491 $growinglink .= '/';
1492 }
1493 }
1494
1495 return false;
1496 }
1497
1498 return true;
1499 }
1500
1507 public static function translateTab( Skin $skin, array &$tabs ) {
1508 $title = $skin->getTitle();
1509 $handle = new MessageHandle( $title );
1510 $code = $handle->getCode();
1511 $page = TranslatablePage::isTranslationPage( $title );
1512 // The source language has a subpage too, but cannot be translated
1513 if ( !$page || $page->getSourceLanguageCode() === $code ) {
1514 return;
1515 }
1516
1517 $user = $skin->getUser();
1518 if ( isset( $tabs['views']['edit'] ) ) {
1519 // There is an edit tab, just replace its text and URL with ours, keeping the tooltip and access key
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();
1526 if (
1527 !$permissionManager->userHasAllRights(
1528 $user, ...(array)( $namespaceProtection[ NS_TRANSLATIONS ] ?? [] )
1529 )
1530 ) {
1531 return;
1532 }
1533
1534 $tab = [
1535 'text' => $skin->msg( 'tpt-tab-translate' )->text(),
1536 'href' => $page->getTranslationUrl( $code ),
1537 ];
1538
1539 // Get the position of the viewsource tab within the array (if any)
1540 $viewsourcePos = array_keys( array_keys( $tabs['views'] ), 'viewsource', true )[0] ?? null;
1541
1542 if ( $viewsourcePos !== null ) {
1543 // Remove the viewsource tab and insert the translate tab at its place. Showing the tooltip
1544 // of the viewsource tab for the translate tab would be confusing.
1545 array_splice( $tabs['views'], $viewsourcePos, 1, [ 'translate' => $tab ] );
1546 } else {
1547 // We have neither an edit tab nor a viewsource tab to replace with the translate tab,
1548 // put the translate tab at the end
1549 $tabs['views']['translate'] = $tab;
1550 }
1551 }
1552 }
1553
1566 public static function onMovePageTranslationUnits(
1567 LinkTarget $oldLinkTarget,
1568 LinkTarget $newLinkTarget,
1569 UserIdentity $userIdentity,
1570 int $oldid,
1571 int $newid,
1572 string $reason,
1573 RevisionRecord $revisionRecord
1574 ) {
1575 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1576 // MoveTranslatableBundleJob takes care of handling updates because it performs
1577 // a lot of moves at once. As a performance optimization, skip this hook if
1578 // we detect moves from that job. As there isn't a good way to pass information
1579 // to this hook what originated the move, we use some heuristics.
1580 if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1581 return;
1582 }
1583
1584 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1585 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1586 $groupLast = null;
1587 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1588 $handle = new MessageHandle( $title );
1589 // Documentation pages are never translation pages
1590 if ( !$handle->isValid() || $handle->isDoc() ) {
1591 continue;
1592 }
1593
1594 $group = $handle->getGroup();
1595 if ( !$group instanceof WikiPageMessageGroup ) {
1596 continue;
1597 }
1598
1599 $language = $handle->getCode();
1600
1601 // Ignore pages such as Translations:Page/unit without language code
1602 if ( (string)$language === '' ) {
1603 continue;
1604 }
1605
1606 // Update the page only once if source and destination units
1607 // belong to the same page
1608 if ( $group !== $groupLast ) {
1609 $groupLast = $group;
1610 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1611 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1612 }
1613 }
1614 }
1615
1626 public static function onDeleteTranslationUnit(
1627 WikiPage $unit,
1628 User $user,
1629 $reason,
1630 $id,
1631 $content,
1632 $logEntry
1633 ) {
1634 // Do the update. In case job queue is doing the work, the update is not done here
1635 if ( self::$jobQueueRunning ) {
1636 return;
1637 }
1638
1639 $title = $unit->getTitle();
1640 $handle = new MessageHandle( $title );
1641 if ( !$handle->isValid() ) {
1642 return;
1643 }
1644
1645 $group = $handle->getGroup();
1646 if ( !$group instanceof WikiPageMessageGroup ) {
1647 return;
1648 }
1649
1650 $target = $group->getTitle();
1651 $langCode = $handle->getCode();
1652 $fname = __METHOD__;
1653
1654 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1655 $callback = function () use (
1656 $dbw,
1657 $target,
1658 $handle,
1659 $langCode,
1660 $user,
1661 $reason,
1662 $fname
1663 ) {
1664 $translationPageTitle = $target->getSubpage( $langCode );
1665 // Do a more thorough check for the translation page in case the translation page is deleted in a
1666 // different transaction.
1667 if ( !$translationPageTitle || !$translationPageTitle->exists( Title::READ_LATEST ) ) {
1668 return;
1669 }
1670
1671 $dbw->startAtomic( $fname );
1672
1673 $page = TranslatablePage::newFromTitle( $target );
1674
1675 MessageGroupStats::forItem(
1676 $page->getMessageGroupId(),
1677 $langCode,
1678 MessageGroupStats::FLAG_NO_CACHE
1679 );
1680
1681 if ( !$handle->isDoc() ) {
1682 $unitTitle = $handle->getTitle();
1683 // Assume that $user and $reason for the first deletion is the same for all
1684 self::updateTranslationPage(
1685 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1686 );
1687 }
1688
1689 $dbw->endAtomic( $fname );
1690 };
1691
1692 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1693 }
1694
1699 public static function onReplaceTextFilterPageTitlesForEdit( array &$titles ): void {
1700 foreach ( $titles as $index => $title ) {
1701 $handle = new MessageHandle( $title );
1702 if ( Utilities::isTranslationPage( $handle ) ) {
1703 unset( $titles[ $index ] );
1704 }
1705 }
1706 }
1707
1712 public static function onReplaceTextFilterPageTitlesForRename( array &$titles ): void {
1713 foreach ( $titles as $index => $title ) {
1714 $handle = new MessageHandle( $title );
1715 if (
1716 TranslatablePage::isSourcePage( $title ) ||
1717 Utilities::isTranslationPage( $handle )
1718 ) {
1719 unset( $titles[ $index ] );
1720 }
1721 }
1722 }
1723}
static tpSyntaxCheckForEditContent( $context, $content, $status, $summary)
Display nice error when editing content.
Definition Hooks.php:828
static tpSyntaxCheck(RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus)
When attempting to save, last resort.
Definition Hooks.php:878
static onReplaceTextFilterPageTitlesForRename(array &$titles)
Removes translatable and translation pages from the list of titles to be renamed Hook: ReplaceTextFil...
Definition Hooks.php:1712
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:1566
static replaceMovePage(&$list)
Hook: SpecialPage_initList.
Definition Hooks.php:1384
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:303
static onParserGetVariableValueSwitch(Parser $parser, array &$variableCache, string $magicWordId, ?string &$ret, PPFrame $frame)
Hook: ParserGetVariableValueSwitch.
Definition Hooks.php:466
static onGetUserPermissionsErrorsExpensive(Title $title, User $user, $action, &$result)
Prevent creation of orphan translation units in Translations namespace.
Definition Hooks.php:991
static preprocessTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforePreprocess.
Definition Hooks.php:160
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:1626
static preventDirectEditing(Title $title, User $user, $action, &$result)
Prevent editing of translation pages directly.
Definition Hooks.php:1142
static renderTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforeInternalParse.
Definition Hooks.php:76
static onParserOutputPostCacheTransform(ParserOutput $out, &$text, array &$options)
Hook: ParserOutputPostCacheTransform.
Definition Hooks.php:190
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:381
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:211
static onBeforePageDisplay(OutputPage $out, Skin $skin)
Hook: BeforePageDisplay.
Definition Hooks.php:334
static translatablePageHeader( $article, &$outputDone, &$pcache)
Hook: ArticleViewHeader.
Definition Hooks.php:1219
static onPageContentLanguage(Title $title, &$pageLang)
Set the right page content language for translated pages ("Page/xx").
Definition Hooks.php:287
static lockedPagesCheck(Title $title, User $user, $action, &$result)
Hook: getUserPermissionsErrorsExpensive.
Definition Hooks.php:1414
static updateTranstagOnNullRevisions(RevisionRecord $rev)
Page moving and page protection (and possibly other things) creates null revisions.
Definition Hooks.php:955
static onVisualEditorBeforeEditor(OutputPage $out, Skin $skin)
Hook: onVisualEditorBeforeEditor.
Definition Hooks.php:367
static replaceSubtitle(&$subpages, ?Skin $skin, OutputPage $out)
Hook: SkinSubPageSubtitle.
Definition Hooks.php:1437
static formatLanguageLink(array &$link, Title $linkTitle, Title $pageTitle, OutputPage $out)
Hooks: SkinTemplateGetLanguageLink.
Definition Hooks.php:795
static onGetMagicVariableIDs(&$variableIDs)
Hook: GetMagicVariableIDs.
Definition Hooks.php:459
static languages( $data, $params, $parser)
Definition Hooks.php:489
static addLanguageLinks(Title $title, array &$languageLinks)
Hooks: LanguageLinks.
Definition Hooks.php:706
static disableDelete( $article, $out, &$reason)
Redirects the delete action to our own for translatable pages.
Definition Hooks.php:1197
static onReplaceTextFilterPageTitlesForEdit(array &$titles)
Removes translation pages from the list of page titles to be edited Hook: ReplaceTextFilterPageTitles...
Definition Hooks.php:1699
static translateTab(Skin $skin, array &$tabs)
Converts the edit tab (if exists) for translation pages to translate tab.
Definition Hooks.php:1507
static addTranstagAfterSave(WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revisionRecord, EditResult $editResult)
Hook: PageSaveComplete.
Definition Hooks.php:910
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.
getMessageGroup()
Returns MessageGroup used for translating this page.
getPageDisplayTitle(string $languageCode)
Get translated page title.
This class represents one translation unit in a translatable page.
Minimal service container.
Definition Services.php:44
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
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.
static get(string $group, string $key)
Get a metadata value for the given group and key.
Wraps the translatable page sections into a message group.