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\MediaWikiServices;
23use MediaWiki\Page\PageIdentity;
24use MediaWiki\Revision\MutableRevisionRecord;
25use MediaWiki\Revision\RenderedRevision;
26use MediaWiki\Revision\RevisionRecord;
27use MediaWiki\Revision\SlotRecord;
28use MediaWiki\Storage\EditResult;
29use MediaWiki\User\UserIdentity;
32use ObjectCache;
33use OutputPage;
34use Parser;
35use ParserOutput;
36use RequestContext;
37use Skin;
38use SpecialPage;
39use Status;
40use StubUserLang;
41use TextContent;
42use Title;
44use User;
45use UserBlockedError;
46use Wikimedia\ScopedCallback;
47use WikiPage;
49use WikitextContent;
50
57class Hooks {
58 // Uuugly hacks
59 public static $allowTargetEdit = false;
60 // Check if job queue is running
61 public static $jobQueueRunning = false;
62 // Check if we are just rendering tags or such
63 public static $renderingContext = false;
64 // Used to communicate data between LanguageLinks and SkinTemplateGetLanguageLink hooks.
65 private static $languageLinkData = [];
66
74 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
75 if ( $text === null ) {
76 // SMW is unhelpfully sending null text if source contains section tags. Do not explode.
77 return;
78 }
79
80 self::preprocessTagPage( $wikitextParser, $text, $state );
81
82 // Skip further interface message parsing
83 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
84 return;
85 }
86
87 // For section previews, perform additional clean-up, given tags are often
88 // unbalanced when we preview one section only.
89 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
90 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
91 $text = $translatablePageParser->cleanupTags( $text );
92 }
93
94 // Set display title
95 $title = MediaWikiServices::getInstance()
96 ->getTitleFactory()
97 ->castFromPageReference( $wikitextParser->getPage() );
98
99 if ( !$title ) {
100 return;
101 }
102
103 $page = TranslatablePage::isTranslationPage( $title );
104 if ( !$page ) {
105 return;
106 }
107
108 try {
109 self::$renderingContext = true;
110 [ , $code ] = Utilities::figureMessage( $title->getText() );
111 $name = $page->getPageDisplayTitle( $code );
112 if ( $name ) {
113 $name = $wikitextParser->recursivePreprocess( $name );
114
115 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
116 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
117 $name = $langConv->convert( $name );
118 $wikitextParser->getOutput()->setDisplayTitle( $name );
119 }
120 self::$renderingContext = false;
121 } catch ( Exception $e ) {
122 LoggerFactory::getInstance( 'Translate' )->error(
123 'T302754 Failed to set display title for page {title}',
124 [
125 'title' => $title->getPrefixedDBkey(),
126 'text' => $text,
127 'pageid' => $title->getId(),
128 ]
129 );
130
131 // Re-throw to preserve behavior
132 throw $e;
133 }
134
135 $extensionData = [
136 'languagecode' => $code,
137 'messagegroupid' => $page->getMessageGroupId(),
138 'sourcepagetitle' => [
139 'namespace' => $page->getTitle()->getNamespace(),
140 'dbkey' => $page->getTitle()->getDBkey()
141 ]
142 ];
143
144 $wikitextParser->getOutput()->setExtensionData(
145 'translate-translation-page', $extensionData
146 );
147
148 // Disable edit section links
149 $wikitextParser->getOutput()->setExtensionData( 'Translate-noeditsection', true );
150 }
151
159 public static function preprocessTagPage( $wikitextParser, &$text, $state ): void {
160 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
161
162 if ( $translatablePageParser->containsMarkup( $text ) ) {
163 try {
164 $parserOutput = $translatablePageParser->parse( $text );
165 // If parsing succeeds, replace text and add styles
166 $text = $parserOutput->sourcePageTextForRendering(
167 $wikitextParser->getTargetLanguage()
168 );
169 $wikitextParser->getOutput()->addModuleStyles( [
170 'ext.translate',
171 ] );
172 } catch ( ParsingFailure $e ) {
173 wfDebug( 'ParsingFailure caught; expected' );
174 }
175 } else {
176 // If the text doesn't contain <translate> markup, it can still contain <tvar> in the
177 // context of a Parsoid template expansion sub-pipeline. We strip these as well.
178 $unit = new TranslationUnit( $text );
179 $text = $unit->getTextForTrans();
180 }
181 }
182
189 public static function onParserOutputPostCacheTransform(
190 ParserOutput $out,
191 &$text,
192 array &$options
193 ) {
194 if ( $out->getExtensionData( 'Translate-noeditsection' ) ) {
195 $options['enableSectionEditLinks'] = false;
196 }
197 }
198
210 public static function fetchTranslatableTemplateAndTitle(
211 ?LinkTarget $contextLink,
212 ?LinkTarget $templateLink,
213 bool &$skip,
214 ?RevisionRecord &$revRecord
215 ): void {
216 if ( !$templateLink ) {
217 return;
218 }
219
220 $templateTitle = Title::castFromLinkTarget( $templateLink );
221
222 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
223 if ( $templateTranslationPage ) {
224 // Template is referring to a translation page, fetch it and incase it doesn't
225 // exist, fetch the source fallback.
226 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
227 if ( !$revRecord ) {
228 // In very rare cases, fetching of the source fallback also seems to be failing in
229 // which case null will be returned. See: T323863
230 // This should not happen because TranslatablePage::isTranslationPage already checks
231 // the message index, and checks if the translatable page has the marked tag.
232 LoggerFactory::getInstance( 'Translate' )->warning(
233 "T323863: Did not find message group for '{groupid}'",
234 [ 'groupid' => $templateTranslationPage->getMessageGroupId() ]
235 );
236 return;
237
238 }
239 return;
240 }
241
242 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
243 return;
244 }
245
246 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
247
248 if ( !( $translatableTemplatePage->supportsTransclusion() ?? false ) ) {
249 // Page being transcluded does not support language aware transclusion
250 return;
251 }
252
253 $store = MediaWikiServices::getInstance()->getRevisionStore();
254
255 if ( $contextLink ) {
256 // Fetch the context page language, and then check if template is present in that language
257 $templateTranslationTitle = $templateTitle->getSubpage(
258 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
259 );
260
261 if ( $templateTranslationTitle ) {
262 if ( $templateTranslationTitle->exists() ) {
263 // Template is present in the context page language, fetch the revision record and return
264 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
265 } else {
266 // In case the template has not been translated to the context page language,
267 // we assign a MutableRevisionRecord in order to add a dependency, so that when
268 // it is created, the newly created page is loaded rather than the fallback
269 $revRecord = new MutableRevisionRecord( $templateTranslationTitle );
270 }
271 return;
272 }
273 }
274
275 // Context page information not available OR the template translation title could not be determined.
276 // Fetch and return the RevisionRecord of the template in the source language
277 $sourceTemplateTitle = $templateTitle->getSubpage(
278 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
279 );
280 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
281 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
282 }
283 }
284
291 public static function onPageContentLanguage( Title $title, &$pageLang ) {
292 // For translation pages, parse plural, grammar etc. with correct language,
293 // and set the right direction
294 if ( TranslatablePage::isTranslationPage( $title ) ) {
295 [ , $code ] = Utilities::figureMessage( $title->getText() );
296 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
297 }
298 }
299
307 public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) {
308 if ( TranslatablePage::isSourcePage( $title ) ) {
309 $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
310 if ( !$msg->isDisabled() ) {
311 $notices['translate-tag'] = $msg->parseAsBlock();
312 }
313
314 $notices[] = Html::warningBox(
315 wfMessage( 'tps-edit-sourcepage-text' )->parse(),
316 'translate-edit-documentation'
317 );
318
319 // The check is "we're using visual editor for WYSIWYG" (as opposed to "for wikitext
320 // edition") - the message will not be displayed in that case.
321 $request = RequestContext::getMain()->getRequest();
322 if ( $request->getVal( 'action' ) === 'visualeditor' &&
323 $request->getVal( 'paction' ) !== 'wikitext'
324 ) {
325 $notices[] = Html::warningBox(
326 wfMessage( 'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
327 'translate-edit-documentation'
328 );
329 }
330 }
331 }
332
339 public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
340 global $wgTranslatePageTranslationULS;
341
342 $title = $out->getTitle();
343 $isSource = TranslatablePage::isSourcePage( $title );
344 $isTranslation = TranslatablePage::isTranslationPage( $title );
345
346 if ( $isSource || $isTranslation ) {
347 if ( $wgTranslatePageTranslationULS ) {
348 $out->addModules( 'ext.translate.pagetranslation.uls' );
349 }
350
351 if ( $isSource ) {
352 // Adding a help notice
353 $out->addModuleStyles( 'ext.translate.edit.documentation.styles' );
354 }
355
356 if ( $isTranslation ) {
357 // Source pages get this module via <translate>, but for translation
358 // pages we need to add it manually.
359 $out->addModuleStyles( 'ext.translate' );
360 $out->addJsConfigVars( 'wgTranslatePageTranslation', 'translation' );
361 } else {
362 $out->addJsConfigVars( 'wgTranslatePageTranslation', 'source' );
363 }
364 }
365
366 return true;
367 }
368
375 public static function onVisualEditorBeforeEditor( OutputPage $out, Skin $skin ) {
376 return !TranslatablePage::isTranslationPage( $out->getTitle() );
377 }
378
390 public static function onSectionSave(
391 WikiPage $wikiPage,
392 User $user,
393 TextContent $content,
394 $summary,
395 $minor,
396 $flags,
397 MessageHandle $handle
398 ) {
399 // FuzzyBot may do some duplicate work already worked on by other jobs
400 if ( $user->equals( FuzzyBot::getUser() ) ) {
401 return true;
402 }
403
404 $group = $handle->getGroup();
405 if ( !$group instanceof WikiPageMessageGroup ) {
406 return true;
407 }
408
409 // Finally we know the title and can construct a Translatable page
410 $page = TranslatablePage::newFromTitle( $group->getTitle() );
411
412 // Update the target translation page
413 if ( !$handle->isDoc() ) {
414 $code = $handle->getCode();
415 DeferredUpdates::addCallableUpdate(
416 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
417 $unitTitle = $handle->getTitle();
418 self::updateTranslationPage( $page, $code, $user, $flags, $summary, null, $unitTitle );
419 }
420 );
421 }
422
423 return true;
424 }
425
426 private static function updateTranslationPage(
427 TranslatablePage $page,
428 string $code,
429 User $user,
430 int $flags,
431 string $summary,
432 ?string $triggerAction = null,
433 ?Title $unitTitle = null
434 ): void {
435 $source = $page->getTitle();
436 $target = $source->getSubpage( $code );
437 $mwInstance = MediaWikiServices::getInstance();
438
439 // We don't know and don't care
440 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
441
442 // Update the target page
443 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() : null;
444 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
445 $job->setUser( $user );
446 $job->setSummary( $summary );
447 $job->setFlags( $flags );
448 $mwInstance->getJobQueueGroup()->push( $job );
449
450 // Invalidate caches so that language bar is up-to-date
451 $pages = $page->getTranslationPages();
452 $wikiPageFactory = $mwInstance->getWikiPageFactory();
453 foreach ( $pages as $title ) {
454 if ( $title->equals( $target ) ) {
455 // Handled by the RenderTranslationPageJob
456 continue;
457 }
458
459 $wikiPage = $wikiPageFactory->newFromTitle( $title );
460 $wikiPage->doPurge();
461 }
462 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
463 $sourceWikiPage->doPurge();
464 }
465
472 public static function languages( $data, $params, $parser ) {
473 global $wgPageTranslationLanguageList;
474
475 if ( $wgPageTranslationLanguageList === 'sidebar-only' ) {
476 return '';
477 }
478
479 self::$renderingContext = true;
480 $context = new ScopedCallback( static function () {
481 self::$renderingContext = false;
482 } );
483
484 // Add a dummy language link that is removed in self::addLanguageLinks.
485 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) {
486 $parser->getOutput()->addLanguageLink( 'x-pagetranslation-tag' );
487 }
488
489 $currentTitle = $parser->getTitle();
490 $pageStatus = self::getTranslatablePageStatus( $currentTitle );
491 if ( !$pageStatus ) {
492 return '';
493 }
494
495 $page = $pageStatus[ 'page' ];
496 $status = $pageStatus[ 'languages' ];
497 $pageTitle = $page->getTitle();
498
499 // Sort by language code, which seems to be the only sane method
500 ksort( $status );
501
502 // This way the parser knows to fragment the parser cache by language code
503 $userLang = $parser->getOptions()->getUserLangObj();
504 $userLangCode = $userLang->getCode();
505 // Should call $page->getMessageGroup()->getSourceLanguage(), but
506 // group is sometimes null on WMF during page moves, reason unknown.
507 // This should do the same thing for now.
508 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
509
510 $languages = [];
511 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
512 foreach ( $status as $code => $percent ) {
513 // Get autonyms (null)
514 $name = Utilities::getLanguageName( $code, LanguageNameUtils::AUTONYMS );
515
516 // Add links to other languages
517 $suffix = ( $code === $sourceLanguage ) ? '' : "/$code";
518 $targetTitleString = $pageTitle->getDBkey() . $suffix;
519 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
520
521 $classes = [];
522 if ( $code === $userLangCode ) {
523 $classes[] = 'mw-pt-languages-ui';
524 }
525
526 $linker = $parser->getLinkRenderer();
527 $lang = $langFactory->getLanguage( $code );
528 if ( $currentTitle->equals( $subpage ) ) {
529 $classes[] = 'mw-pt-languages-selected';
530 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
531 $attribs = [
532 'class' => $classes,
533 'lang' => $lang->getHtmlCode(),
534 'dir' => $lang->getDir(),
535 ];
536
537 $contents = Html::element( 'span', $attribs, $name );
538 } elseif ( $subpage->isKnown() ) {
539 $pagename = $page->getPageDisplayTitle( $code );
540 if ( !is_string( $pagename ) ) {
541 $pagename = $subpage->getPrefixedText();
542 }
543
544 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
545
546 $title = wfMessage( 'tpt-languages-nonzero' )
547 ->inLanguage( $userLang )
548 ->params( $pagename )
549 ->numParams( 100 * $percent )
550 ->text();
551 $attribs = [
552 'title' => $title,
553 'class' => $classes,
554 'lang' => $lang->getHtmlCode(),
555 'dir' => $lang->getDir(),
556 ];
557
558 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
559 } else {
560 /* When language is included because it is a priority language,
561 * but translations don't exist link directly to the
562 * translation view. */
563 $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' );
564 $params = [
565 'group' => $page->getMessageGroupId(),
566 'language' => $code,
567 'task' => 'view'
568 ];
569
570 $classes[] = 'new'; // For red link color
571
572 $attribs = [
573 'title' => wfMessage( 'tpt-languages-zero' )->inLanguage( $userLang )->text(),
574 'class' => $classes,
575 'lang' => $lang->getHtmlCode(),
576 'dir' => $lang->getDir(),
577 ];
578 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
579 }
580 $languages[ $name ] = Html::rawElement( 'li', [], $contents );
581 }
582
583 // Sort languages by autonym
584 ksort( $languages );
585 $languages = array_values( $languages );
586 $languages = implode( "\n", $languages );
587
588 $out = Html::openElement( 'div', [
589 'class' => 'mw-pt-languages noprint',
590 'lang' => $userLang->getHtmlCode(),
591 'dir' => $userLang->getDir()
592 ] );
593 $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ],
594 wfMessage( 'tpt-languages-legend' )->inLanguage( $userLang )->escaped()
595 );
596 $out .= Html::rawElement(
597 'ul',
598 [ 'class' => 'mw-pt-languages-list' ],
599 $languages
600 );
601 $out .= Html::closeElement( 'div' );
602
603 $parser->getOutput()->addModuleStyles( [
604 'ext.translate.tag.languages',
605 ] );
606
607 return $out;
608 }
609
616 private static function tpProgressIcon( float $percent ) {
617 $classes = [ 'mw-pt-progress' ];
618 $percent *= 100;
619 if ( $percent < 20 ) {
620 $classes[] = 'mw-pt-progress--stub';
621 } elseif ( $percent < 40 ) {
622 $classes[] = 'mw-pt-progress--low';
623 } elseif ( $percent < 60 ) {
624 $classes[] = 'mw-pt-progress--med';
625 } elseif ( $percent < 80 ) {
626 $classes[] = 'mw-pt-progress--high';
627 } else {
628 $classes[] = 'mw-pt-progress--complete';
629 }
630 return $classes;
631 }
632
638 private static function getTranslatablePageStatus( Title $title ) {
639 // Check if this is a source page or a translation page
640 $page = TranslatablePage::newFromTitle( $title );
641 if ( $page->getMarkedTag() === null ) {
642 $page = TranslatablePage::isTranslationPage( $title );
643 }
644
645 if ( $page === false || $page->getMarkedTag() === null ) {
646 return null;
647 }
648
649 $status = $page->getTranslationPercentages();
650 if ( !$status ) {
651 return null;
652 }
653
654 // If priority languages have been set, always show those languages
655 $priorityLangs = TranslateMetadata::get( $page->getMessageGroupId(), 'prioritylangs' );
656 $priorityForce = TranslateMetadata::get( $page->getMessageGroupId(), 'priorityforce' );
657 $filter = null;
658 if ( (string)$priorityLangs !== '' ) {
659 $filter = array_flip( explode( ',', $priorityLangs ) );
660 }
661 if ( $filter !== null ) {
662 // If translation is restricted to some languages, only show them
663 if ( $priorityForce === 'on' ) {
664 // Do not filter the source language link
665 $filter[$page->getMessageGroup()->getSourceLanguage()] = true;
666 $status = array_intersect_key( $status, $filter );
667 }
668 foreach ( $filter as $langCode => $value ) {
669 if ( !isset( $status[$langCode] ) ) {
670 // We need to show all priority languages even if no translation started
671 $status[$langCode] = 0;
672 }
673 }
674 }
675
676 return [
677 'page' => $page,
678 'languages' => $status
679 ];
680 }
681
687 public static function addLanguageLinks( Title $title, array &$languageLinks ) {
688 global $wgPageTranslationLanguageList;
689
690 $hasLanguagesTag = false;
691 foreach ( $languageLinks as $index => $name ) {
692 if ( $name === 'x-pagetranslation-tag' ) {
693 $hasLanguagesTag = true;
694 unset( $languageLinks[ $index ] );
695 }
696 }
697
698 if ( $wgPageTranslationLanguageList === 'tag-only' ) {
699 return;
700 }
701
702 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' && $hasLanguagesTag ) {
703 return;
704 }
705
706 // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only'
707
708 $status = self::getTranslatablePageStatus( $title );
709 if ( !$status ) {
710 return;
711 }
712
713 self::$renderingContext = true;
714 $context = new ScopedCallback( static function () {
715 self::$renderingContext = false;
716 } );
717
718 $page = $status[ 'page' ];
719 $languages = $status[ 'languages' ];
720 $mwServices = MediaWikiServices::getInstance();
721 $en = $mwServices->getLanguageFactory()->getLanguage( 'en' );
722
723 $newLanguageLinks = [];
724
725 // Batch the Title::exists queries used below
726 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
727 foreach ( array_keys( $languages ) as $code ) {
728 $title = $page->getTitle()->getSubpage( $code );
729 $lb->addObj( $title );
730 }
731 $lb->execute();
732 $languageNameUtils = $mwServices->getLanguageNameUtils();
733 foreach ( $languages as $code => $percentage ) {
734 $title = $page->getTitle()->getSubpage( $code );
735 $key = "x-pagetranslation:{$title->getPrefixedText()}";
736 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
737
738 if ( $title->exists() ) {
739 $href = $title->getLocalURL();
740 $classes = self::tpProgressIcon( (float)$percentage );
741 $title = wfMessage( 'tpt-languages-nonzero' )
742 ->params( $translatedName )
743 ->numParams( 100 * $percentage );
744 } else {
745 $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [
746 'group' => $page->getMessageGroupId(),
747 'language' => $code,
748 ] );
749 $classes = [ 'mw-pt-progress--none' ];
750 $title = wfMessage( 'tpt-languages-zero' );
751 }
752
753 self::$languageLinkData[ $key ] = [
754 'href' => $href,
755 'language' => $code,
756 'percentage' => $percentage,
757 'classes' => $classes,
758 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
759 'title' => $title,
760 ];
761
762 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][ 'autonym' ];
763 }
764
765 asort( $newLanguageLinks );
766 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
767 }
768
776 public static function formatLanguageLink(
777 array &$link,
778 Title $linkTitle,
779 Title $pageTitle,
780 OutputPage $out
781 ) {
782 if ( substr( $link[ 'text' ], 0, 18 ) !== 'x-pagetranslation:' ) {
783 return;
784 }
785
786 if ( !isset( self::$languageLinkData[ $link[ 'text' ] ] ) ) {
787 return;
788 }
789
790 $data = self::$languageLinkData[ $link[ 'text' ] ];
791
792 $link[ 'class' ] .= ' ' . implode( ' ', $data[ 'classes' ] );
793 $link[ 'href' ] = $data[ 'href' ];
794 $link[ 'text' ] = $data[ 'autonym' ];
795 $link[ 'title' ] = $data[ 'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
796 $link[ 'lang'] = LanguageCode::bcp47( $data[ 'language' ] );
797 $link[ 'hreflang'] = LanguageCode::bcp47( $data[ 'language' ] );
798
799 $out->addModuleStyles( 'ext.translate.tag.languages' );
800 }
801
811 public static function tpSyntaxCheckForEditContent(
812 $context,
813 $content,
814 $status,
815 $summary
816 ) {
817 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
818
819 if ( $syntaxErrorStatus ) {
820 $status->merge( $syntaxErrorStatus );
821 return $syntaxErrorStatus->isGood();
822 }
823
824 return true;
825 }
826
827 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
828 // T163254: Ignore translation markup on non-wikitext pages
829 if ( !$content instanceof WikitextContent || !$page ) {
830 return null;
831 }
832
833 $text = $content->getText();
834
835 // See T154500
836 $text = TextContent::normalizeLineEndings( $text );
837 $status = Status::newGood();
838 $parser = Services::getInstance()->getTranslatablePageParser();
839 if ( $parser->containsMarkup( $text ) ) {
840 try {
841 $parser->parse( $text );
842 } catch ( ParsingFailure $e ) {
843 $status->fatal( ...( $e->getMessageSpecification() ) );
844 }
845 }
846
847 return $status;
848 }
849
861 public static function tpSyntaxCheck(
862 RenderedRevision $renderedRevision,
863 UserIdentity $user,
864 CommentStoreComment $summary,
865 $flags,
866 Status $hookStatus
867 ) {
868 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
869
870 $status = self::tpSyntaxError(
871 $renderedRevision->getRevision()->getPage(),
872 $content
873 );
874
875 if ( $status ) {
876 $hookStatus->merge( $status );
877 return $status->isGood();
878 }
879
880 return true;
881 }
882
894 public static function addTranstagAfterSave(
895 WikiPage $wikiPage,
896 UserIdentity $userIdentity,
897 string $summary,
898 int $flags,
899 RevisionRecord $revisionRecord,
900 EditResult $editResult
901 ) {
902 $content = $wikiPage->getContent();
903
904 // T163254: Disable page translation on non-wikitext pages
905 if ( $content instanceof WikitextContent ) {
906 $text = $content->getText();
907 } else {
908 // Not applicable
909 return true;
910 }
911
912 $parser = Services::getInstance()->getTranslatablePageParser();
913 if ( $parser->containsMarkup( $text ) ) {
914 // Add the ready tag
915 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
916 $page->addReadyTag( $revisionRecord->getId() );
917 }
918
919 // Schedule a deferred status update for the translatable page.
920 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
921 $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() );
922
923 return true;
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 // Check if anything is prevented for the group in the first place
1099 $force = TranslateMetadata::get( $groupId, 'priorityforce' );
1100 if ( $force !== 'on' ) {
1101 return [];
1102 }
1103
1104 // And finally check whether the language is in the inclusion list
1105 $languages = TranslateMetadata::get( $groupId, 'prioritylangs' );
1106 $filter = array_flip( explode( ',', $languages ) );
1107 if ( !isset( $filter[$handle->getCode()] ) ) {
1108 $reason = TranslateMetadata::get( $groupId, 'priorityreason' );
1109 if ( $reason ) {
1110 return [ 'tpt-translation-restricted', $reason ];
1111 }
1112
1113 return [ 'tpt-translation-restricted-no-reason' ];
1114 }
1115
1116 return [];
1117 }
1118
1128 public static function preventDirectEditing( Title $title, User $user, $action, &$result ) {
1129 if ( self::$allowTargetEdit ) {
1130 return true;
1131 }
1132
1133 $inclusionList = [
1134 'read', 'deletedtext', 'deletedhistory',
1135 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884
1136 'review', // FlaggedRevs
1137 'patrol', // T151172
1138 ];
1139 $needsPageTranslationRight = in_array( $action, [ 'delete', 'undelete' ] );
1140 if ( in_array( $action, $inclusionList ) ||
1141 $needsPageTranslationRight && $user->isAllowed( 'pagetranslation' )
1142 ) {
1143 return true;
1144 }
1145
1146 $page = TranslatablePage::isTranslationPage( $title );
1147 if ( $page !== false && $page->getMarkedTag() ) {
1148 if ( $needsPageTranslationRight ) {
1149 $result = User::newFatalPermissionDeniedStatus( 'pagetranslation' )->getMessage();
1150 return false;
1151 }
1152
1153 [ , $code ] = Utilities::figureMessage( $title->getText() );
1154 $mwService = MediaWikiServices::getInstance();
1155
1156 if ( method_exists( $mwService, 'getUrlUtils' ) ) {
1157 $translationUrl = $mwService->getUrlUtils()->expand(
1158 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1159 );
1160 } else {
1161 // < MW 1.39
1162 $translationUrl = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE );
1163 }
1164
1165 $result = [
1166 'tpt-target-page',
1167 ':' . $page->getTitle()->getPrefixedText(),
1168 // This url shouldn't get cached
1169 $translationUrl
1170 ];
1171
1172 return false;
1173 }
1174
1175 return true;
1176 }
1177
1188 public static function disableDelete( $article, $out, &$reason ) {
1189 $title = $article->getTitle();
1190 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1191 $isDeletableBundle = $bundle && $bundle->isDeletable();
1192 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1193 $new = SpecialPage::getTitleFor(
1194 'PageTranslationDeletePage',
1195 $title->getPrefixedText()
1196 );
1197 $out->redirect( $new->getFullURL() );
1198 }
1199
1200 return true;
1201 }
1202
1211 public static function translatablePageHeader( $article, &$outputDone, &$pcache ) {
1212 if ( $article->getOldID() ) {
1213 return true;
1214 }
1215
1216 $transPage = TranslatablePage::isTranslationPage( $article->getTitle() );
1217 $context = $article->getContext();
1218 if ( $transPage ) {
1219 self::translationPageHeader( $context, $transPage );
1220 } else {
1221 // Check for pages that are tagged or marked
1222 self::sourcePageHeader( $context );
1223 }
1224
1225 return true;
1226 }
1227
1228 private static function sourcePageHeader( IContextSource $context ) {
1229 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1230
1231 $language = $context->getLanguage();
1232 $title = $context->getTitle();
1233
1234 $page = TranslatablePage::newFromTitle( $title );
1235
1236 $marked = $page->getMarkedTag();
1237 $ready = $page->getReadyTag();
1238 $latest = $title->getLatestRevID();
1239
1240 $actions = [];
1241 if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) {
1242 $actions[] = self::getTranslateLink( $context, $page, null );
1243 }
1244
1245 $hasChanges = $ready === $latest && $marked !== $latest;
1246 if ( $hasChanges ) {
1247 $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] );
1248
1249 if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) {
1250 $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' );
1251 $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ];
1252
1253 if ( $marked === null ) {
1254 // This page has never been marked
1255 $linkDesc = $context->msg( 'translate-tag-markthis' )->text();
1256 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1257 } else {
1258 $markUrl = $pageTranslation->getFullURL( $params );
1259 $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl )
1260 ->parse();
1261 }
1262 } else {
1263 $actions[] = $context->msg( 'translate-tag-hasnew', $diffUrl )->parse();
1264 }
1265 }
1266
1267 if ( !count( $actions ) ) {
1268 return;
1269 }
1270
1271 $header = Html::rawElement(
1272 'div',
1273 [
1274 'class' => 'mw-pt-translate-header noprint nomobile',
1275 'dir' => $language->getDir(),
1276 'lang' => $language->getHtmlCode(),
1277 ],
1278 $language->semicolonList( $actions )
1279 );
1280
1281 $context->getOutput()->addHTML( $header );
1282 }
1283
1284 private static function getTranslateLink(
1285 IContextSource $context,
1286 TranslatablePage $page,
1287 ?string $langCode
1288 ): string {
1289 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1290
1291 return $linker->makeKnownLink(
1292 SpecialPage::getTitleFor( 'Translate' ),
1293 $context->msg( 'translate-tag-translate-link-desc' )->text(),
1294 [],
1295 [
1296 'group' => $page->getMessageGroupId(),
1297 'language' => $langCode,
1298 'action' => 'page',
1299 'filter' => '',
1300 ]
1301 );
1302 }
1303
1304 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1305 global $wgTranslateKeepOutdatedTranslations;
1306
1307 $title = $context->getTitle();
1308 if ( !$title->exists() ) {
1309 return;
1310 }
1311
1312 [ , $code ] = Utilities::figureMessage( $title->getText() );
1313
1314 // Get the translation percentage
1315 $pers = $page->getTranslationPercentages();
1316 $per = 0;
1317 if ( isset( $pers[$code] ) ) {
1318 $per = $pers[$code] * 100;
1319 }
1320
1321 $language = $context->getLanguage();
1322 $output = $context->getOutput();
1323
1324 if ( $page->getSourceLanguageCode() === $code ) {
1325 // If we are on the source language page, link to translate for user's language
1326 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1327 } else {
1328 $mwService = MediaWikiServices::getInstance();
1329
1330 if ( method_exists( $mwService, 'getUrlUtils' ) ) {
1331 $translationUrl = $mwService->getUrlUtils()->expand(
1332 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1333 );
1334 } else {
1335 // < MW 1.39
1336 $translationUrl = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE );
1337 }
1338
1339 $msg = $context->msg( 'tpt-translation-intro',
1340 $translationUrl,
1341 ':' . $page->getTitle()->getPrefixedText(),
1342 $language->formatNum( $per )
1343 )->parse();
1344 }
1345
1346 $header = Html::rawElement(
1347 'div',
1348 [
1349 'class' => 'mw-pt-translate-header noprint',
1350 'dir' => $language->getDir(),
1351 'lang' => $language->getHtmlCode(),
1352 ],
1353 $msg
1354 );
1355
1356 $output->addHTML( $header );
1357
1358 if ( $wgTranslateKeepOutdatedTranslations ) {
1359 $groupId = $page->getMessageGroupId();
1360 // This is already calculated and cached by above call to getTranslationPercentages
1361 $stats = MessageGroupStats::forItem( $groupId, $code );
1362 if ( $stats[MessageGroupStats::FUZZY] ) {
1363 // Only show if there is fuzzy messages
1364 $wrap = Html::rawElement(
1365 'div',
1366 [
1367 'class' => 'mw-pt-translate-header',
1368 'dir' => $language->getDir(),
1369 'lang' => $language->getHtmlCode()
1370 ],
1371 '<span class="mw-translate-fuzzy">$1</span>'
1372 );
1373
1374 $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] );
1375 }
1376 }
1377 }
1378
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 true;
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 return true;
1406 }
1407
1416 public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) {
1417 if ( $action === 'read' ) {
1418 return true;
1419 }
1420
1421 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1422 $key = $cache->makeKey( 'pt-lock', sha1( $title->getPrefixedText() ) );
1423 if ( $cache->get( $key ) === 'locked' ) {
1424 $result = [ 'pt-locked-page' ];
1425
1426 return false;
1427 }
1428
1429 return true;
1430 }
1431
1439 public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) {
1440 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1441
1442 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1443 if ( !$isTranslationPage
1444 && !TranslatablePage::isSourcePage( $out->getTitle() )
1445 ) {
1446 return true;
1447 }
1448
1449 // Copied from Skin::subPageSubtitle()
1450 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1451 if (
1452 $out->isArticle() &&
1453 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1454 ) {
1455 $ptext = $out->getTitle()->getPrefixedText();
1456 if ( strpos( $ptext, '/' ) !== false ) {
1457 $links = explode( '/', $ptext );
1458 array_pop( $links );
1459 if ( $isTranslationPage ) {
1460 // Also remove language code page
1461 array_pop( $links );
1462 }
1463 $c = 0;
1464 $growinglink = '';
1465 $display = '';
1466 $lang = $skin->getLanguage();
1467
1468 foreach ( $links as $link ) {
1469 $growinglink .= $link;
1470 $display .= $link;
1471 $linkObj = Title::newFromText( $growinglink );
1472
1473 if ( is_object( $linkObj ) && $linkObj->isKnown() ) {
1474 $getlink = $linker->makeKnownLink(
1475 SpecialPage::getTitleFor( 'MyLanguage', $growinglink ),
1476 $display
1477 );
1478
1479 $c++;
1480
1481 if ( $c > 1 ) {
1482 $subpages .= $lang->getDirMarkEntity() . $skin->msg( 'pipe-separator' )->escaped();
1483 } else {
1484 $subpages .= '&lt; ';
1485 }
1486
1487 $subpages .= $getlink;
1488 $display = '';
1489 } else {
1490 $display .= '/';
1491 }
1492
1493 $growinglink .= '/';
1494 }
1495 }
1496
1497 return false;
1498 }
1499
1500 return true;
1501 }
1502
1510 public static function translateTab( Skin $skin, array &$tabs ) {
1511 $title = $skin->getTitle();
1512 $handle = new MessageHandle( $title );
1513 $code = $handle->getCode();
1514 $page = TranslatablePage::isTranslationPage( $title );
1515 if ( !$page ) {
1516 return true;
1517 }
1518 // The source language has a subpage too, but cannot be translated
1519 if ( $page->getSourceLanguageCode() === $code ) {
1520 return true;
1521 }
1522
1523 if ( isset( $tabs['views']['edit'] ) ) {
1524 $tabs['views']['edit']['text'] = $skin->msg( 'tpt-tab-translate' )->text();
1525 $tabs['views']['edit']['href'] = $page->getTranslationUrl( $code );
1526 }
1527
1528 return true;
1529 }
1530
1543 public static function onMovePageTranslationUnits(
1544 LinkTarget $oldLinkTarget,
1545 LinkTarget $newLinkTarget,
1546 UserIdentity $userIdentity,
1547 int $oldid,
1548 int $newid,
1549 string $reason,
1550 RevisionRecord $revisionRecord
1551 ) {
1552 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1553 // MoveTranslatableBundleJob takes care of handling updates because it performs
1554 // a lot of moves at once. As a performance optimization, skip this hook if
1555 // we detect moves from that job. As there isn't a good way to pass information
1556 // to this hook what originated the move, we use some heuristics.
1557 if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1558 return;
1559 }
1560
1561 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1562 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1563 $groupLast = null;
1564 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1565 $handle = new MessageHandle( $title );
1566 if ( !$handle->isValid() ) {
1567 continue;
1568 }
1569
1570 // Documentation pages are never translation pages
1571 if ( $handle->isDoc() ) {
1572 continue;
1573 }
1574
1575 $group = $handle->getGroup();
1576 if ( !$group instanceof WikiPageMessageGroup ) {
1577 continue;
1578 }
1579
1580 $language = $handle->getCode();
1581
1582 // Ignore pages such as Translations:Page/unit without language code
1583 if ( (string)$language === '' ) {
1584 continue;
1585 }
1586
1587 // Update the page only once if source and destination units
1588 // belong to the same page
1589 if ( $group !== $groupLast ) {
1590 $groupLast = $group;
1591 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1592 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1593 }
1594 }
1595 }
1596
1607 public static function onDeleteTranslationUnit(
1608 WikiPage $unit,
1609 User $user,
1610 $reason,
1611 $id,
1612 $content,
1613 $logEntry
1614 ) {
1615 // Do the update. In case job queue is doing the work, the update is not done here
1616 if ( self::$jobQueueRunning ) {
1617 return;
1618 }
1619
1620 $title = $unit->getTitle();
1621
1622 $handle = new MessageHandle( $title );
1623 if ( !$handle->isValid() ) {
1624 return;
1625 }
1626
1627 $group = $handle->getGroup();
1628 if ( !$group instanceof WikiPageMessageGroup ) {
1629 return;
1630 }
1631
1632 $target = $group->getTitle();
1633 $langCode = $handle->getCode();
1634 $fname = __METHOD__;
1635
1636 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1637 $callback = function () use (
1638 $dbw,
1639 $target,
1640 $handle,
1641 $langCode,
1642 $user,
1643 $reason,
1644 $fname
1645 ) {
1646 $translationPageTitle = $target->getSubpage( $langCode );
1647 // Do a more thorough check for the translation page in case the translation page is deleted in a
1648 // different transaction.
1649 if ( !$translationPageTitle || !$translationPageTitle->exists( Title::READ_LATEST ) ) {
1650 return;
1651 }
1652
1653 $dbw->startAtomic( $fname );
1654
1655 $page = TranslatablePage::newFromTitle( $target );
1656
1657 MessageGroupStats::forItem(
1658 $page->getMessageGroupId(),
1659 $langCode,
1660 MessageGroupStats::FLAG_NO_CACHE
1661 );
1662
1663 if ( !$handle->isDoc() ) {
1664 $unitTitle = $handle->getTitle();
1665 // Assume that $user and $reason for the first deletion is the same for all
1666 self::updateTranslationPage(
1667 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1668 );
1669 }
1670
1671 $dbw->endAtomic( $fname );
1672 };
1673
1674 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1675 }
1676}
static tpSyntaxCheckForEditContent( $context, $content, $status, $summary)
Display nice error when editing content.
Definition Hooks.php:811
static tpSyntaxCheck(RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus)
When attempting to save, last resort.
Definition Hooks.php:861
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:1543
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:307
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:159
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:1607
static preventDirectEditing(Title $title, User $user, $action, &$result)
Prevent editing of translation pages directly.
Definition Hooks.php:1128
static renderTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforeInternalParse.
Definition Hooks.php:74
static onParserOutputPostCacheTransform(ParserOutput $out, &$text, array &$options)
Hook: ParserOutputPostCacheTransform.
Definition Hooks.php:189
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:390
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:210
static onBeforePageDisplay(OutputPage $out, Skin $skin)
Hook: BeforePageDisplay.
Definition Hooks.php:339
static translatablePageHeader( $article, &$outputDone, &$pcache)
Hook: ArticleViewHeader.
Definition Hooks.php:1211
static onPageContentLanguage(Title $title, &$pageLang)
Set the right page content language for translated pages ("Page/xx").
Definition Hooks.php:291
static lockedPagesCheck(Title $title, User $user, $action, &$result)
Hook: getUserPermissionsErrorsExpensive.
Definition Hooks.php:1416
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:375
static replaceSubtitle(&$subpages, ?Skin $skin, OutputPage $out)
Hook: SkinSubPageSubtitle.
Definition Hooks.php:1439
static formatLanguageLink(array &$link, Title $linkTitle, Title $pageTitle, OutputPage $out)
Hooks: SkinTemplateGetLanguageLink.
Definition Hooks.php:776
static languages( $data, $params, $parser)
Definition Hooks.php:472
static addLanguageLinks(Title $title, array &$languageLinks)
Hooks: LanguageLinks.
Definition Hooks.php:687
static disableDelete( $article, $out, &$reason)
Redirects the delete action to our own for translatable pages.
Definition Hooks.php:1188
static translateTab(Skin $skin, array &$tabs)
Converts the edit tab (if exists) for translation pages to translate tab.
Definition Hooks.php:1510
static addTranstagAfterSave(WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revisionRecord, EditResult $editResult)
Hook: PageSaveComplete.
Definition Hooks.php:894
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:40
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:30
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( $group, $key)
Get a metadata value for the given group and key.
Wraps the translatable page sections into a message group.