Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
Hooks.php
1<?php
2
3namespace MediaWiki\Extension\Translate\PageTranslation;
4
5use Article;
6use Exception;
7use ManualLogEntry;
8use MediaWiki\CommentStore\CommentStoreComment;
9use MediaWiki\Config\Config;
10use MediaWiki\Content\Content;
11use MediaWiki\Content\TextContent;
12use MediaWiki\Context\IContextSource;
13use MediaWiki\Context\RequestContext;
14use MediaWiki\Deferred\DeferredUpdates;
15use MediaWiki\Deferred\LinksUpdate\LinksUpdate;
24use MediaWiki\Html\Html;
25use MediaWiki\Language\Language;
26use MediaWiki\Language\LanguageCode;
27use MediaWiki\Languages\LanguageNameUtils;
28use MediaWiki\Linker\LinkTarget;
29use MediaWiki\Logger\LoggerFactory;
30use MediaWiki\MainConfigNames;
31use MediaWiki\MediaWikiServices;
32use MediaWiki\Output\OutputPage;
33use MediaWiki\Page\PageIdentity;
34use MediaWiki\Page\PageReference;
35use MediaWiki\Parser\Parser;
36use MediaWiki\Parser\ParserOutput;
37use MediaWiki\Parser\PPFrame;
38use MediaWiki\ResourceLoader\Context;
39use MediaWiki\Revision\MutableRevisionRecord;
40use MediaWiki\Revision\RenderedRevision;
41use MediaWiki\Revision\RevisionRecord;
42use MediaWiki\Revision\SlotRecord;
43use MediaWiki\SpecialPage\SpecialPage;
44use MediaWiki\Status\Status;
45use MediaWiki\Storage\EditResult;
46use MediaWiki\StubObject\StubUserLang;
47use MediaWiki\Title\Title;
48use MediaWiki\User\User;
49use MediaWiki\User\UserIdentity;
50use Skin;
51use UserBlockedError;
52use Wikimedia\Rdbms\IDBAccessObject;
53use Wikimedia\ScopedCallback;
54use WikiPage;
56
63class Hooks {
64 private const PAGEPROP_HAS_LANGUAGES_TAG = 'translate-has-languages-tag';
66 public static $allowTargetEdit = false;
68 public static bool $isDeleteTranslatableBundleJobRunning = false;
70 public static $renderingContext = false;
72 private static $languageLinkData = [];
73
81 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
82 if ( $text === null ) {
83 // SMW is unhelpfully sending null text if the source contains section tags. Do not explode.
84 return;
85 }
86
87 self::preprocessTagPage( $wikitextParser, $text, $state );
88
89 // Skip further interface message parsing
90 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
91 return;
92 }
93
94 // For section previews, perform additional clean-up, given tags are often
95 // unbalanced when we preview one section only.
96 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
97 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
98 $text = $translatablePageParser->cleanupTags( $text );
99 }
100
101 // Set display title
102 $title = MediaWikiServices::getInstance()
103 ->getTitleFactory()
104 ->castFromPageReference( $wikitextParser->getPage() );
105 if ( !$title ) {
106 return;
107 }
108
109 $page = TranslatablePage::isTranslationPage( $title );
110 if ( !$page ) {
111 return;
112 }
113
114 $wikitextParser->getOutput()->setUnsortedPageProperty( 'translate-is-translation' );
115
116 try {
117 self::$renderingContext = true;
118 [ , $code ] = Utilities::figureMessage( $title->getText() );
119 $name = $page->getPageDisplayTitle( $code );
120 if ( $name ) {
121 $name = $wikitextParser->recursivePreprocess( $name );
122
123 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
124 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
125 $name = $langConv->convert( $name );
126 $wikitextParser->getOutput()->setDisplayTitle( $name );
127 }
128 self::$renderingContext = false;
129 } catch ( Exception $e ) {
130 LoggerFactory::getInstance( LogNames::MAIN )->error(
131 'T302754 Failed to set display title for page {title}',
132 [
133 'title' => $title->getPrefixedDBkey(),
134 'text' => $text,
135 'pageid' => $title->getId(),
136 ]
137 );
138
139 // Re-throw to preserve behavior
140 throw $e;
141 }
142
143 $extensionData = [
144 'languagecode' => $code,
145 'messagegroupid' => $page->getMessageGroupId(),
146 'sourcepagetitle' => [
147 'namespace' => $page->getTitle()->getNamespace(),
148 'dbkey' => $page->getTitle()->getDBkey()
149 ]
150 ];
151
152 $wikitextParser->getOutput()->setExtensionData( 'translate-translation-page', $extensionData );
153 // Disable edit section links
154 $wikitextParser->getOutput()->setExtensionData( 'Translate-noeditsection', true );
155 }
156
164 public static function preprocessTagPage( $wikitextParser, &$text, $state ): void {
165 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
166
167 if ( $translatablePageParser->containsMarkup( $text ) ) {
168 try {
169 $parserOutput = $translatablePageParser->parse( $text );
170 // If parsing succeeds, replace text
171 $text = $parserOutput->sourcePageTextForRendering(
172 $wikitextParser->getTargetLanguage()
173 );
174 } catch ( ParsingFailure $e ) {
175 wfDebug( 'ParsingFailure caught; expected' );
176 }
177 } else {
178 // If the text doesn't contain <translate> markup, it can still contain <tvar> in the
179 // context of a Parsoid template expansion sub-pipeline. We strip these as well.
180 $unit = new TranslationUnit( $text );
181 $text = $unit->getTextForTrans();
182 }
183 }
184
191 public static function onParserOutputPostCacheTransform(
192 ParserOutput $out,
193 &$text,
194 array &$options
195 ) {
196 if ( $out->getExtensionData( 'Translate-noeditsection' ) ) {
197 $options['enableSectionEditLinks'] = false;
198 }
199 }
200
212 public static function fetchTranslatableTemplateAndTitle(
213 ?LinkTarget $contextLink,
214 ?LinkTarget $templateLink,
215 bool &$skip,
216 ?RevisionRecord &$revRecord
217 ): void {
218 if ( !$templateLink ) {
219 return;
220 }
221
222 $templateTitle = Title::castFromLinkTarget( $templateLink );
223
224 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
225 if ( $templateTranslationPage ) {
226 // Template is referring to a translation page, fetch it and incase it doesn't
227 // exist, fetch the source fallback.
228 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
229 if ( !$revRecord ) {
230 // In rare cases fetching of the source fallback might fail. See: T323863
231 LoggerFactory::getInstance( LogNames::MAIN )->warning(
232 "T323863: Could not fetch any revision record for '{groupid}'",
233 [ 'groupid' => $templateTranslationPage->getMessageGroupId() ]
234 );
235 }
236 return;
237 }
238
239 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
240 return;
241 }
242
243 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
244
245 if ( !( $translatableTemplatePage->supportsTransclusion() ?? false ) ) {
246 // Page being transcluded does not support language aware transclusion
247 return;
248 }
249
250 $store = MediaWikiServices::getInstance()->getRevisionStore();
251
252 if ( $contextLink ) {
253 // Fetch the context page language, and then check if template is present in that language
254 $templateTranslationTitle = $templateTitle->getSubpage(
255 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
256 );
257
258 if ( $templateTranslationTitle ) {
259 if ( $templateTranslationTitle->exists() ) {
260 // Template is present in the context page language, fetch the revision record and return
261 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
262 } else {
263 // In case the template has not been translated to the context page language,
264 // we assign a MutableRevisionRecord in order to add a dependency, so that when
265 // it is created, the newly created page is loaded rather than the fallback
266 $revRecord = new MutableRevisionRecord( $templateTranslationTitle );
267 }
268 return;
269 }
270 }
271
272 // Context page information not available OR the template translation title could not be determined.
273 // Fetch and return the RevisionRecord of the template in the source language
274 $sourceTemplateTitle = $templateTitle->getSubpage(
275 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
276 );
277 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
278 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
279 }
280 }
281
288 public static function onPageContentLanguage( Title $title, &$pageLang ) {
289 // For translation pages, parse plural, grammar etc. with correct language,
290 // and set the right direction
291 if ( TranslatablePage::isTranslationPage( $title ) ) {
292 [ , $code ] = Utilities::figureMessage( $title->getText() );
293 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
294 }
295 }
296
304 public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) {
305 if ( TranslatablePage::isSourcePage( $title ) ) {
306 $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
307 if ( !$msg->isDisabled() ) {
308 $notices['translate-tag'] = $msg->parseAsBlock();
309 }
310
311 $notices[] = Html::warningBox(
312 wfMessage( 'tps-edit-sourcepage-text' )->parse(),
313 'translate-edit-documentation'
314 );
315
316 // The check is "we're using visual editor for WYSIWYG" (as opposed to "for wikitext
317 // edition") - the message will not be displayed in that case.
318 $request = RequestContext::getMain()->getRequest();
319 if ( $request->getVal( 'action' ) === 'visualeditor' &&
320 $request->getVal( 'paction' ) !== 'wikitext'
321 ) {
322 $notices[] = Html::warningBox(
323 wfMessage( 'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
324 'translate-edit-documentation'
325 );
326 }
327 }
328 }
329
335 public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
336 global $wgTranslatePageTranslationULS;
337
338 $title = $out->getTitle();
339 $isSource = TranslatablePage::isSourcePage( $title );
340 $isTranslation = TranslatablePage::isTranslationPage( $title );
341
342 if ( $isSource || $isTranslation ) {
343 if ( $wgTranslatePageTranslationULS ) {
344 $out->addModules( 'ext.translate.pagetranslation.uls' );
345 }
346
347 if ( $isSource ) {
348 // Adding a help notice
349 $out->addModuleStyles( 'ext.translate.edit.documentation.styles' );
350 }
351
352 $out->addModuleStyles( 'ext.translate' );
353
354 $out->addJsConfigVars( 'wgTranslatePageTranslation', $isTranslation ? 'translation' : 'source' );
355 }
356 }
357
364 public static function onVisualEditorBeforeEditor( OutputPage $out, Skin $skin ) {
365 return !TranslatablePage::isTranslationPage( $out->getTitle() );
366 }
367
378 public static function onSectionSave(
379 WikiPage $wikiPage,
380 User $user,
381 TextContent $content,
382 $summary,
383 $minor,
384 $flags,
385 MessageHandle $handle
386 ) {
387 // FuzzyBot may do some duplicate work already worked on by other jobs
388 if ( $user->equals( FuzzyBot::getUser() ) ) {
389 return;
390 }
391
392 $group = $handle->getGroup();
393 if ( !$group instanceof WikiPageMessageGroup ) {
394 return;
395 }
396
397 // Finally we know the title and can construct a Translatable page
398 $page = TranslatablePage::newFromTitle( $group->getTitle() );
399
400 // Update the target translation page
401 if ( !$handle->isDoc() ) {
402 $code = $handle->getCode();
403 DeferredUpdates::addCallableUpdate(
404 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
405 $unitTitle = $handle->getTitle();
406 self::updateTranslationPage( $page, $code, $user, $flags, $summary, null, $unitTitle );
407 }
408 );
409 }
410 }
411
412 private static function updateTranslationPage(
413 TranslatablePage $page,
414 string $code,
415 User $user,
416 int $flags,
417 string $summary,
418 ?string $triggerAction = null,
419 ?Title $unitTitle = null
420 ): void {
421 $source = $page->getTitle();
422 $target = $source->getSubpage( $code );
423 $mwInstance = MediaWikiServices::getInstance();
424
425 // We don't know and don't care
426 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
427
428 // Update the target page
429 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() : null;
430 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
431 $job->setUser( $user );
432 $job->setSummary( $summary );
433 $job->setFlags( $flags );
434 $mwInstance->getJobQueueGroup()->push( $job );
435
436 // Invalidate caches so that language bar is up-to-date
437 $pages = $page->getTranslationPages();
438 $wikiPageFactory = $mwInstance->getWikiPageFactory();
439 foreach ( $pages as $title ) {
440 if ( $title->equals( $target ) ) {
441 // Handled by the RenderTranslationPageJob
442 continue;
443 }
444
445 $wikiPage = $wikiPageFactory->newFromTitle( $title );
446 $wikiPage->doPurge();
447 }
448 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
449 $sourceWikiPage->doPurge();
450 }
451
456 public static function onGetMagicVariableIDs( &$variableIDs ): void {
457 $variableIDs[] = 'translatablepage';
458 }
459
463 public static function onParserGetVariableValueSwitch(
464 Parser $parser,
465 array &$variableCache,
466 string $magicWordId,
467 ?string &$ret,
468 PPFrame $frame
469 ): void {
470 switch ( $magicWordId ) {
471 case 'translatablepage':
472 $pageStatus = self::getTranslatablePageStatus( $parser->getPage() );
473 $ret = $pageStatus !== null ? $pageStatus['page']->getTitle()->getPrefixedText() : '';
474 $variableCache[$magicWordId] = $ret;
475 break;
476 }
477 }
478
485 public static function languages( $data, $params, $parser ) {
486 global $wgPageTranslationLanguageList;
487
488 if ( $wgPageTranslationLanguageList === 'sidebar-only' ) {
489 return '';
490 }
491
492 self::$renderingContext = true;
493 $context = new ScopedCallback( static function () {
494 self::$renderingContext = false;
495 } );
496
497 // Store a property that we can avoid adding language links when
498 // $wgPageTranslationLanguageList === 'sidebar-fallback'
499 $parser->getOutput()->setUnsortedPageProperty( self::PAGEPROP_HAS_LANGUAGES_TAG );
500
501 $currentPage = $parser->getPage();
502 $pageStatus = self::getTranslatablePageStatus( $currentPage );
503 if ( !$pageStatus ) {
504 return '';
505 }
506
507 $page = $pageStatus[ 'page' ];
508 $status = $pageStatus[ 'languages' ];
509 $pageTitle = $page->getTitle();
510
511 // Sort by language code, which seems to be the only sane method
512 ksort( $status );
513
514 // This way the parser knows to fragment the parser cache by language code
515 $userLang = $parser->getOptions()->getUserLangObj();
516 $userLangCode = $userLang->getCode();
517 // Should call $page->getMessageGroup()->getSourceLanguage(), but
518 // group is sometimes null on WMF during page moves, reason unknown.
519 // This should do the same thing for now.
520 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
521
522 $languages = [];
523 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
524 foreach ( $status as $code => $percent ) {
525 // Get autonyms (null)
526 $name = Utilities::getLanguageName( $code, LanguageNameUtils::AUTONYMS );
527
528 // Add links to other languages
529 $suffix = ( $code === $sourceLanguage ) ? '' : "/$code";
530 $targetTitleString = $pageTitle->getDBkey() . $suffix;
531 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
532
533 $classes = [];
534 if ( $code === $userLangCode ) {
535 $classes[] = 'mw-pt-languages-ui';
536 }
537
538 $linker = $parser->getLinkRenderer();
539 $lang = $langFactory->getLanguage( $code );
540 if ( $currentPage->isSamePageAs( $subpage ) ) {
541 $classes[] = 'mw-pt-languages-selected';
542 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
543 $attribs = [
544 'class' => $classes,
545 'lang' => $lang->getHtmlCode(),
546 'dir' => $lang->getDir(),
547 ];
548
549 $contents = Html::element( 'span', $attribs, $name );
550 } elseif ( $subpage->isKnown() ) {
551 $pagename = $page->getPageDisplayTitle( $code );
552 if ( !is_string( $pagename ) ) {
553 $pagename = $subpage->getPrefixedText();
554 }
555
556 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
557
558 $title = wfMessage( 'tpt-languages-nonzero' )
559 ->page( $parser->getPage() )
560 ->inLanguage( $userLang )
561 ->params( $pagename )
562 ->numParams( 100 * $percent )
563 ->text();
564 $attribs = [
565 'title' => $title,
566 'class' => $classes,
567 'lang' => $lang->getHtmlCode(),
568 'dir' => $lang->getDir(),
569 ];
570
571 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
572 } else {
573 /* When language is included because it is a priority language,
574 * but translations don't exist link directly to the
575 * translation view. */
576 $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' );
577 $params = [
578 'group' => $page->getMessageGroupId(),
579 'language' => $code,
580 'task' => 'view'
581 ];
582
583 $classes[] = 'new'; // For red link color
584
585 $attribs = [
586 'title' => wfMessage( 'tpt-languages-zero' )
587 ->page( $parser->getPage() )
588 ->inLanguage( $userLang )
589 ->text(),
590 'class' => $classes,
591 'lang' => $lang->getHtmlCode(),
592 'dir' => $lang->getDir(),
593 ];
594 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
595 }
596 $languages[ $name ] = Html::rawElement( 'li', [], $contents );
597 }
598
599 // Sort languages by autonym
600 ksort( $languages );
601 $languages = array_values( $languages );
602 $languages = implode( "\n", $languages );
603
604 $out = Html::openElement( 'div', [
605 'class' => 'mw-pt-languages noprint navigation-not-searchable',
606 'lang' => $userLang->getHtmlCode(),
607 'dir' => $userLang->getDir()
608 ] );
609 $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ],
610 wfMessage( 'tpt-languages-legend' )
611 ->page( $parser->getPage() )
612 ->inLanguage( $userLang )
613 ->escaped()
614 );
615 $out .= Html::rawElement(
616 'ul',
617 [ 'class' => 'mw-pt-languages-list' ],
618 $languages
619 );
620 $out .= Html::closeElement( 'div' );
621
622 $parser->getOutput()->addModuleStyles( [
623 'ext.translate.tag.languages',
624 ] );
625
626 return $out;
627 }
628
635 private static function tpProgressIcon( float $percent ) {
636 $classes = [ 'mw-pt-progress' ];
637 $percent *= 100;
638 if ( $percent < 15 ) {
639 $classes[] = 'mw-pt-progress--low';
640 } elseif ( $percent < 70 ) {
641 $classes[] = 'mw-pt-progress--med';
642 } elseif ( $percent < 100 ) {
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( ?PageReference $pageReference ): ?array {
655 if ( $pageReference === null ) {
656 return null;
657 }
658 $title = Title::newFromPageReference( $pageReference );
659 // Check if this is a source page or a translation page
660 $page = TranslatablePage::newFromTitle( $title );
661 if ( $page->getMarkedTag() === null ) {
662 $page = TranslatablePage::isTranslationPage( $title );
663 }
664
665 if ( $page === false || $page->getMarkedTag() === null ) {
666 return null;
667 }
668
669 $status = $page->getTranslationPercentages();
670 if ( !$status ) {
671 return null;
672 }
673
674 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
675 // If priority languages have been set, always show those languages
676 $priorityLanguages = $messageGroupMetadata->get( $page->getMessageGroupId(), 'prioritylangs' );
677 if ( (string)$priorityLanguages !== '' ) {
678 $status += array_fill_keys( explode( ',', $priorityLanguages ), 0 );
679 }
680
681 return [
682 'page' => $page,
683 'languages' => $status
684 ];
685 }
686
692 public static function addLanguageLinks( Title $title, array &$languageLinks ) {
693 global $wgPageTranslationLanguageList;
694
695 if ( $wgPageTranslationLanguageList === 'tag-only' ) {
696 return;
697 }
698
699 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) {
700 $pageProps = MediaWikiServices::getInstance()->getPageProps();
701 $languageProp = $pageProps->getProperties( $title, self::PAGEPROP_HAS_LANGUAGES_TAG );
702 if ( $languageProp !== [] ) {
703 return;
704 }
705 }
706
707 // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only'
708
709 $status = self::getTranslatablePageStatus( $title );
710 if ( !$status ) {
711 return;
712 }
713
714 self::$renderingContext = true;
715 $context = new ScopedCallback( static function () {
716 self::$renderingContext = false;
717 } );
718
719 $page = $status[ 'page' ];
720 $languages = $status[ 'languages' ];
721 $mwServices = MediaWikiServices::getInstance();
722 $en = $mwServices->getLanguageFactory()->getLanguage( 'en' );
723
724 // Batch the Title::exists queries used below
725 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
726 foreach ( array_keys( $languages ) as $code ) {
727 $title = $page->getTitle()->getSubpage( $code );
728 $lb->addObj( $title );
729 }
730 $lb->execute();
731 $languageNameUtils = $mwServices->getLanguageNameUtils();
732 foreach ( $languages as $code => $percentage ) {
733 $title = $page->getTitle()->getSubpage( $code );
734 $placeholderValue = "$code-x-pagetranslation:{$title->getPrefixedText()}";
735 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
736
737 if ( $title->exists() ) {
738 $href = $title->getLocalURL();
739 $classes = self::tpProgressIcon( (float)$percentage );
740 $titleAttribute = wfMessage( 'tpt-languages-nonzero' )
741 ->params( $translatedName )
742 ->numParams( 100 * $percentage );
743 } else {
744 $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [
745 'group' => $page->getMessageGroupId(),
746 'language' => $code,
747 ] );
748 $classes = [ 'mw-pt-progress--none' ];
749 $titleAttribute = wfMessage( 'tpt-languages-zero' );
750 }
751
752 self::$languageLinkData[ $placeholderValue ] = [
753 'href' => $href,
754 'language' => $code,
755 'classes' => $classes,
756 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
757 'title' => $titleAttribute,
758 ];
759
760 // Insert a placeholder which we will then fix up in SkinTemplateGetLanguageLink hook handler
761 $languageLinks[] = $placeholderValue;
762 }
763 }
764
772 public static function formatLanguageLink(
773 array &$link,
774 Title $linkTitle,
775 Title $pageTitle,
776 OutputPage $out
777 ) {
778 $data = self::$languageLinkData[$link['text']] ?? null;
779 if ( !$data ) {
780 return;
781 }
782
783 $link['class'] .= ' interwiki-pagetranslation ' . implode( ' ', $data['classes'] );
784 $link['href'] = $data['href'];
785 $link['text'] = $data['autonym'];
786 $link['title'] = $data['title']->inLanguage( $out->getLanguage()->getCode() )->text();
787 $link['lang'] = LanguageCode::bcp47( $data['language'] );
788 $link['hreflang'] = LanguageCode::bcp47( $data['language'] );
789
790 $out->addModuleStyles( 'ext.translate.tag.languages' );
791 }
792
802 public static function tpSyntaxCheckForEditContent(
803 $context,
804 $content,
805 $status,
806 $summary
807 ) {
808 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
809
810 if ( $syntaxErrorStatus ) {
811 $status->merge( $syntaxErrorStatus );
812 return $syntaxErrorStatus->isGood();
813 }
814
815 return true;
816 }
817
818 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
819 if ( !$page || !self::isAllowedContentModel( $content, $page ) ) {
820 return null;
821 }
822
823 '@phan-var TextContent $content';
824 $text = $content->getText();
825
826 // See T154500
827 $text = TextContent::normalizeLineEndings( $text );
828 $status = Status::newGood();
829 $parser = Services::getInstance()->getTranslatablePageParser();
830 if ( $parser->containsMarkup( $text ) ) {
831 try {
832 $parser->parse( $text );
833 } catch ( ParsingFailure $e ) {
834 $status->fatal( $e->getMessageSpecification() );
835 }
836 }
837
838 return $status;
839 }
840
852 public static function tpSyntaxCheck(
853 RenderedRevision $renderedRevision,
854 UserIdentity $user,
855 CommentStoreComment $summary,
856 $flags,
857 Status $hookStatus
858 ) {
859 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
860
861 $status = self::tpSyntaxError(
862 $renderedRevision->getRevision()->getPage(),
863 $content
864 );
865
866 if ( $status ) {
867 $hookStatus->merge( $status );
868 return $status->isGood();
869 }
870
871 return true;
872 }
873
884 public static function addTranstagAfterSave(
885 WikiPage $wikiPage,
886 UserIdentity $userIdentity,
887 string $summary,
888 int $flags,
889 RevisionRecord $revisionRecord,
890 EditResult $editResult
891 ) {
892 $content = $wikiPage->getContent();
893
894 // Only allow translating configured content models (T360544)
895 if ( !self::isAllowedContentModel( $content, $wikiPage ) ) {
896 return;
897 }
898
899 '@phan-var TextContent $content';
900 $text = $content->getText();
901
902 $parser = Services::getInstance()->getTranslatablePageParser();
903 if ( $parser->containsMarkup( $text ) ) {
904 // Add the ready tag
905 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
906 $page->addReadyTag( $revisionRecord->getId() );
907 }
908
909 // Schedule a deferred status update for the translatable page.
910 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
911 $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() );
912 }
913
929 public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) {
930 $parentId = $rev->getParentId();
931 if ( $parentId === 0 || $parentId === null ) {
932 // No parent, bail out.
933 return;
934 }
935
936 $prevRev = MediaWikiServices::getInstance()
937 ->getRevisionLookup()
938 ->getRevisionById( $parentId );
939
940 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
941 // Not a null revision, bail out.
942 return;
943 }
944
945 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
946 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
947 $bundle = $bundleFactory->getBundle( $title );
948
949 if ( $bundle ) {
950 $bundleStore = $bundleFactory->getStore( $bundle );
951 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
952 }
953 }
954
969 Title $title,
970 User $user,
971 $action,
972 &$result
973 ) {
974 $handle = new MessageHandle( $title );
975
976 if ( !$handle->isPageTranslation() || $action === 'read' ) {
977 return true;
978 }
979
980 $isValid = true;
981 $groupId = null;
982
983 if ( $handle->isValid() ) {
984 $group = $handle->getGroup();
985 $groupId = $group->getId();
986 $permissionTitleCheck = null;
987
988 if ( $group instanceof WikiPageMessageGroup ) {
989 $permissionTitleCheck = $group->getTitle();
990 } elseif ( $group instanceof MessageBundleMessageGroup ) {
991 // TODO: This check for MessageBundle related permission should be in
992 // the MessageBundleTranslation/Hook
993 $permissionTitleCheck = Title::newFromID( $group->getBundlePageId() );
994 }
995
996 if ( $permissionTitleCheck ) {
997 if ( $handle->getCode() === $group->getSourceLanguage() && !$user->equals( FuzzyBot::getUser() ) ) {
998 // Allow revision deletion actions as per T286884 since if something bad somehow gets marked for
999 // translation, deleting revisions everywhere should be possible without deliberately
1000 // invalidating the unit
1001 $allowedActionList = [
1002 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884
1003 ];
1004 if ( !in_array( $action, $allowedActionList ) ) {
1005 $result = [ 'tpt-cant-edit-source-language', $permissionTitleCheck ];
1006 return false;
1007 }
1008 }
1009 // Check for blocks
1010 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1011 if ( $permissionManager->isBlockedFrom( $user, $permissionTitleCheck ) ) {
1012 $block = $user->getBlock();
1013 if ( $block ) {
1014 $error = new UserBlockedError( $block, $user );
1015 $errorMessage = $error->getMessageObject();
1016 $result = array_merge( [ $errorMessage->getKey() ], $errorMessage->getParams() );
1017 return false;
1018 }
1019 }
1020 }
1021 }
1022
1023 // Allow editing units that become orphaned in regular use, so that
1024 // people can delete them or fix links or other issues in them.
1025 if ( $action !== 'create' ) {
1026 return true;
1027 }
1028
1029 if ( !$handle->isValid() ) {
1030 // TODO: These checks may no longer be needed
1031 // Sometimes the message index can be out of date. Either the rebuild job failed or
1032 // it just hasn't finished yet. Do a secondary check to make sure we are not
1033 // inconveniencing translators for no good reason.
1034 // See https://phabricator.wikimedia.org/T221119
1035 $translatablePage = self::checkTranslatablePageSlow( $title );
1036 MediaWikiServices::getInstance()->getStatsFactory()
1037 ->withComponent( 'Translate' )
1038 ->getCounter( 'slow_translatable_page_check' )
1039 ->setLabel( 'valid', $translatablePage ? 'yes' : 'no' )
1040 ->increment();
1041
1042 if ( $translatablePage ) {
1043 $groupId = $translatablePage->getMessageGroupId();
1044 } else {
1045 $isValid = false;
1046 }
1047 }
1048
1049 if ( $isValid ) {
1050 $error = self::getTranslationRestrictions( $handle, $groupId );
1051 $result = $error ?: $result;
1052 return $error === [];
1053 }
1054
1055 // Don't allow editing invalid messages that do not belong to any translatable page
1056 LoggerFactory::getInstance( LogNames::MAIN )->info(
1057 'Unknown translation page: {title}',
1058 [ 'title' => $title->getPrefixedDBkey() ]
1059 );
1060 $result = [ 'tpt-unknown-page' ];
1061 return false;
1062 }
1063
1064 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?TranslatablePage {
1065 $parts = TranslatablePage::parseTranslationUnit( $unit );
1066 $translationPageTitle = Title::newFromText(
1067 $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ]
1068 );
1069 if ( !$translationPageTitle ) {
1070 return null;
1071 }
1072
1073 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1074 if ( !$translatablePage ) {
1075 return null;
1076 }
1077
1078 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1079 $store = $factory->getReader( $translatablePage->getTitle() );
1080 $units = $store->getNames();
1081
1082 if ( !in_array( $parts[ 'section' ], $units ) ) {
1083 return null;
1084 }
1085
1086 return $translatablePage;
1087 }
1088
1096 private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) {
1097 global $wgTranslateDocumentationLanguageCode;
1098
1099 // Allow adding message documentation even when translation is restricted
1100 if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) {
1101 return [];
1102 }
1103
1104 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
1105 // Check if anything is prevented for the group in the first place
1106 $force = $messageGroupMetadata->get( $groupId, 'priorityforce' );
1107 if ( $force !== 'on' ) {
1108 return [];
1109 }
1110
1111 // And finally check whether the language is in the inclusion list
1112 $languages = $messageGroupMetadata->get( $groupId, 'prioritylangs' );
1113 $reason = $messageGroupMetadata->get( $groupId, 'priorityreason' );
1114 if ( !$languages ) {
1115 if ( $reason ) {
1116 return [ 'tpt-translation-restricted-no-priority-languages', $reason ];
1117 }
1118 return [ 'tpt-translation-restricted-no-priority-languages-no-reason' ];
1119 }
1120
1121 $filter = array_flip( explode( ',', $languages ) );
1122 if ( !isset( $filter[$handle->getCode()] ) ) {
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 $mwService = MediaWikiServices::getInstance();
1163 if ( $needsPageTranslationRight ) {
1164 $context = RequestContext::getMain();
1165 $statusFormatter = $mwService->getFormatterFactory()->getStatusFormatter( $context );
1166 $permissionError = $mwService->getPermissionManager()
1167 ->newFatalPermissionDeniedStatus( 'pagetranslation', $context );
1168 $result = $statusFormatter->getMessage( $permissionError );
1169 return false;
1170 }
1171
1172 [ , $code ] = Utilities::figureMessage( $title->getText() );
1173
1174 $translationUrl = $mwService->getUrlUtils()->expand(
1175 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1176 );
1177
1178 $result = [
1179 'tpt-target-page',
1180 ':' . $page->getTitle()->getPrefixedText(),
1181 // This url shouldn't get cached
1182 $translationUrl
1183 ];
1184
1185 return false;
1186 }
1187
1188 return true;
1189 }
1190
1201 public static function disableDelete( $article, $out, &$reason ) {
1202 $title = $article->getTitle();
1203 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1204 $isDeletableBundle = $bundle && $bundle->isDeletable();
1205 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1206 $new = SpecialPage::getTitleFor(
1207 'PageTranslationDeletePage',
1208 $title->getPrefixedText()
1209 );
1210 $out->redirect( $new->getFullURL() );
1211 }
1212
1213 return true;
1214 }
1215
1223 public static function translatablePageHeader( $article, &$outputDone, &$pcache ) {
1224 if ( $article->getOldID() ) {
1225 return;
1226 }
1227
1228 $articleTitle = $article->getTitle();
1229 $transPage = TranslatablePage::isTranslationPage( $articleTitle );
1230 $context = $article->getContext();
1231 if ( $transPage ) {
1232 self::translationPageHeader( $context, $transPage );
1233 } else {
1234 $viewTranslatablePage = Services::getInstance()->getTranslatablePageView();
1235 $user = $context->getUser();
1236 if ( $viewTranslatablePage->canDisplayTranslationSettingsBanner( $articleTitle, $user ) ) {
1237 $output = $context->getOutput();
1238 $pageUrl = SpecialPage::getTitleFor( 'PageTranslation' )->getFullURL( [
1239 'do' => 'settings',
1240 'target' => $articleTitle->getPrefixedDBkey(),
1241 ] );
1242 $output->addHTML(
1243 Html::noticeBox(
1244 $context->msg( 'pt-cta-mark-translation', $pageUrl )->parse(),
1245 'translate-cta-pt-mark'
1246 )
1247 );
1248 } else {
1249 self::sourcePageHeader( $context );
1250 }
1251 }
1252 }
1253
1254 private static function sourcePageHeader( IContextSource $context ) {
1255 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1256
1257 $language = $context->getLanguage();
1258 $title = $context->getTitle();
1259
1260 $page = TranslatablePage::newFromTitle( $title );
1261
1262 $marked = $page->getMarkedTag();
1263 $ready = $page->getReadyTag();
1264 $latest = $title->getLatestRevID();
1265
1266 $actions = [];
1267 if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) {
1268 $actions[] = self::getTranslateLink( $context, $page, null );
1269 }
1270
1271 $hasChanges = $ready === $latest && $marked !== $latest;
1272 if ( $hasChanges ) {
1273 $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] );
1274
1275 if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) {
1276 $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' );
1277 $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ];
1278
1279 if ( $marked === null ) {
1280 // This page has never been marked
1281 $linkDesc = $context->msg( 'translate-tag-markthis' )->text();
1282 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1283 } else {
1284 $markUrl = $pageTranslation->getFullURL( $params );
1285 $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl )
1286 ->parse();
1287 }
1288 } else {
1289 $actions[] = $context->msg( 'translate-tag-hasnew', $diffUrl )->parse();
1290 }
1291 }
1292
1293 if ( !count( $actions ) ) {
1294 return;
1295 }
1296
1297 $header = Html::rawElement(
1298 'div',
1299 [
1300 'class' => 'mw-pt-translate-header noprint nomobile',
1301 'dir' => $language->getDir(),
1302 'lang' => $language->getHtmlCode(),
1303 ],
1304 $language->semicolonList( $actions )
1305 );
1306
1307 $context->getOutput()->addHTML( $header );
1308 }
1309
1310 private static function getTranslateLink(
1311 IContextSource $context,
1312 TranslatablePage $page,
1313 ?string $langCode
1314 ): string {
1315 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1316
1317 return $linker->makeKnownLink(
1318 SpecialPage::getTitleFor( 'Translate' ),
1319 $context->msg( 'translate-tag-translate-link-desc' )->text(),
1320 [],
1321 [
1322 'group' => $page->getMessageGroupId(),
1323 'language' => $langCode,
1324 'action' => 'page',
1325 'filter' => '',
1326 'action_source' => 'translate_page'
1327 ]
1328 );
1329 }
1330
1331 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1332 global $wgTranslateKeepOutdatedTranslations;
1333
1334 $title = $context->getTitle();
1335 if ( !$title->exists() ) {
1336 return;
1337 }
1338
1339 [ , $code ] = Utilities::figureMessage( $title->getText() );
1340
1341 // Get the translation percentage
1342 $pers = $page->getTranslationPercentages();
1343 $per = 0;
1344 if ( isset( $pers[$code] ) ) {
1345 $per = $pers[$code] * 100;
1346 }
1347
1348 $language = $context->getLanguage();
1349 $output = $context->getOutput();
1350
1351 if ( $page->getSourceLanguageCode() === $code ) {
1352 // If we are on the source language page, link to translate for user's language
1353 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1354 } else {
1355 $mwService = MediaWikiServices::getInstance();
1356
1357 $translationUrl = $mwService->getUrlUtils()->expand(
1358 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1359 );
1360
1361 $msg = $context->msg( 'tpt-translation-intro',
1362 $translationUrl,
1363 ':' . $page->getTitle()->getPrefixedText(),
1364 $language->formatNum( $per )
1365 )->parse();
1366 }
1367
1368 $header = Html::rawElement(
1369 'div',
1370 [
1371 'class' => 'mw-pt-translate-header noprint',
1372 'dir' => $language->getDir(),
1373 'lang' => $language->getHtmlCode(),
1374 ],
1375 $msg
1376 );
1377
1378 $output->addHTML( $header );
1379
1380 if ( $wgTranslateKeepOutdatedTranslations ) {
1381 $groupId = $page->getMessageGroupId();
1382 // This is already calculated and cached by above call to getTranslationPercentages
1383 $stats = MessageGroupStats::forItem( $groupId, $code );
1384 if ( $stats[MessageGroupStats::FUZZY] ) {
1385 // Only show if there is fuzzy messages
1386 $wrap = Html::rawElement(
1387 'div',
1388 [
1389 'class' => 'mw-pt-translate-header',
1390 'dir' => $language->getDir(),
1391 'lang' => $language->getHtmlCode()
1392 ],
1393 '<span class="mw-translate-fuzzy">$1</span>'
1394 );
1395
1396 $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] );
1397 }
1398 }
1399 }
1400
1401 private static function isAllowedContentModel( Content $content, PageReference $page ): bool {
1402 $config = MediaWikiServices::getInstance()->getMainConfig();
1403 $allowedModels = $config->get( 'PageTranslationAllowedContentModels' );
1404 $contentModel = $content->getModel();
1405 $allowed = (bool)( $allowedModels[$contentModel] ?? false );
1406
1407 // T163254: Disable page translation on non-text pages
1408 if ( $allowed && !$content instanceof TextContent ) {
1409 LoggerFactory::getInstance( LogNames::MAIN )->error(
1410 'Expected {title} to have content of type TextContent, got {contentType}. ' .
1411 '$wgPageTranslationAllowedContentModels is incorrectly configured with a non-text content model.',
1412 [
1413 'title' => (string)$page,
1414 'contentType' => get_class( $content )
1415 ]
1416 );
1417 return false;
1418 }
1419
1420 return $allowed;
1421 }
1422
1427 public static function replaceMovePage( &$list ) {
1428 $movePageSpec = $list['Movepage'] ?? null;
1429
1430 // This should never happen, but apparently is happening? See: T296568
1431 if ( $movePageSpec === null ) {
1432 return;
1433 }
1434
1435 $list['Movepage'] = [
1436 'class' => MoveTranslatableBundleSpecialPage::class,
1437 'services' => [
1438 'ObjectFactory',
1439 'PermissionManager',
1440 'Translate:TranslatableBundleMover',
1441 'Translate:TranslatableBundleFactory',
1442 'FormatterFactory'
1443 ],
1444 'args' => [
1445 $movePageSpec
1446 ]
1447 ];
1448 }
1449
1458 public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) {
1459 if ( $action === 'read' ) {
1460 return true;
1461 }
1462
1463 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
1464 $key = $cache->makeKey( 'pt-lock', sha1( $title->getPrefixedText() ) );
1465 if ( $cache->get( $key ) === 'locked' ) {
1466 $result = [ 'pt-locked-page' ];
1467
1468 return false;
1469 }
1470
1471 return true;
1472 }
1473
1481 public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) {
1482 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1483
1484 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1485 if ( !$isTranslationPage
1486 && !TranslatablePage::isSourcePage( $out->getTitle() )
1487 ) {
1488 return true;
1489 }
1490
1491 // Copied from Skin::subPageSubtitle()
1492 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1493 if (
1494 $out->isArticle() &&
1495 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1496 ) {
1497 $ptext = $out->getTitle()->getPrefixedText();
1498 $links = explode( '/', $ptext );
1499 if ( count( $links ) > 1 ) {
1500 array_pop( $links );
1501 if ( $isTranslationPage ) {
1502 // Also remove language code page
1503 array_pop( $links );
1504 }
1505 $c = 0;
1506 $growinglink = '';
1507 $display = '';
1508 $sitedir = $skin->getLanguage()->getDir();
1509
1510 foreach ( $links as $link ) {
1511 $growinglink .= $link;
1512 $display .= $link;
1513 $linkObj = Title::newFromText( $growinglink );
1514
1515 if ( $linkObj && $linkObj->isKnown() ) {
1516 $getlink = $linker->makeKnownLink(
1517 SpecialPage::getTitleFor( 'MyLanguage', $growinglink ),
1518 $display
1519 );
1520
1521 $c++;
1522
1523 if ( $c > 1 ) {
1524 $subpages .= $skin->msg( 'pipe-separator' )->escaped();
1525 } else {
1526 $subpages .= '&lt; ';
1527 }
1528
1529 $subpages .= Html::rawElement( 'bdi', [ 'dir' => $sitedir ], $getlink );
1530 $display = '';
1531 } else {
1532 $display .= '/';
1533 }
1534
1535 $growinglink .= '/';
1536 }
1537 }
1538
1539 return false;
1540 }
1541
1542 return true;
1543 }
1544
1551 public static function translateTab( Skin $skin, array &$tabs ) {
1552 $title = $skin->getTitle();
1553 $handle = new MessageHandle( $title );
1554 $code = $handle->getCode();
1555 $page = TranslatablePage::isTranslationPage( $title );
1556 // The source language has a subpage too, but cannot be translated
1557 if ( !$page || $page->getSourceLanguageCode() === $code ) {
1558 return;
1559 }
1560
1561 $user = $skin->getUser();
1562 if ( isset( $tabs['views']['edit'] ) ) {
1563 // There is an edit tab, just replace its text and URL with ours, keeping the tooltip and access key
1564 $tabs['views']['edit']['text'] = $skin->msg( 'tpt-tab-translate' )->text();
1565 $tabs['views']['edit']['href'] = $page->getTranslationUrl( $code );
1566 } elseif ( $user->isAllowed( 'translate' ) ) {
1567 $mwInstance = MediaWikiServices::getInstance();
1568 $namespaceProtection = $mwInstance->getMainConfig()->get( MainConfigNames::NamespaceProtection );
1569 $permissionManager = $mwInstance->getPermissionManager();
1570 if (
1571 !$permissionManager->userHasAllRights(
1572 $user, ...(array)( $namespaceProtection[ NS_TRANSLATIONS ] ?? [] )
1573 )
1574 ) {
1575 return;
1576 }
1577
1578 $tab = [
1579 'text' => $skin->msg( 'tpt-tab-translate' )->text(),
1580 'href' => $page->getTranslationUrl( $code ),
1581 ];
1582
1583 // Get the position of the viewsource tab within the array (if any)
1584 $viewsourcePos = array_keys( array_keys( $tabs['views'] ), 'viewsource', true )[0] ?? null;
1585
1586 if ( $viewsourcePos !== null ) {
1587 // Remove the viewsource tab and insert the translate tab at its place. Showing the tooltip
1588 // of the viewsource tab for the translate tab would be confusing.
1589 array_splice( $tabs['views'], $viewsourcePos, 1, [ 'translate' => $tab ] );
1590 } else {
1591 // We have neither an edit tab nor a viewsource tab to replace with the translate tab,
1592 // put the translate tab at the end
1593 $tabs['views']['translate'] = $tab;
1594 }
1595 }
1596 }
1597
1610 public static function onMovePageTranslationUnits(
1611 LinkTarget $oldLinkTarget,
1612 LinkTarget $newLinkTarget,
1613 UserIdentity $userIdentity,
1614 int $oldid,
1615 int $newid,
1616 string $reason,
1617 RevisionRecord $revisionRecord
1618 ) {
1619 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1620 // MoveTranslatableBundleJob takes care of handling updates because it performs
1621 // a lot of moves at once. As a performance optimization, skip this hook if
1622 // we detect moves from that job. As there isn't a good way to pass information
1623 // to this hook what originated the move, we use some heuristics.
1624 if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1625 return;
1626 }
1627
1628 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1629 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1630 $groupLast = null;
1631 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1632 $handle = new MessageHandle( $title );
1633 // Documentation pages are never translation pages
1634 if ( !$handle->isValid() || $handle->isDoc() ) {
1635 continue;
1636 }
1637
1638 $group = $handle->getGroup();
1639 if ( !$group instanceof WikiPageMessageGroup ) {
1640 continue;
1641 }
1642
1643 $language = $handle->getCode();
1644
1645 // Ignore pages such as Translations:Page/unit without language code
1646 if ( $language === '' ) {
1647 continue;
1648 }
1649
1650 // Update the page only once if source and destination units
1651 // belong to the same page
1652 if ( $group !== $groupLast ) {
1653 $groupLast = $group;
1654 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1655 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1656 }
1657 }
1658 }
1659
1670 public static function onDeleteTranslationUnit(
1671 WikiPage $unit,
1672 User $user,
1673 $reason,
1674 $id,
1675 $content,
1676 $logEntry
1677 ) {
1678 $title = $unit->getTitle();
1679 $handle = new MessageHandle( $title );
1680 if ( !$handle->isValid() ) {
1681 return;
1682 }
1683
1684 $group = $handle->getGroup();
1685 if ( !$group instanceof WikiPageMessageGroup ) {
1686 return;
1687 }
1688
1689 $mwServices = MediaWikiServices::getInstance();
1690 // During deletions this may cause creation of a lot of duplicate jobs. It is expected that
1691 // job queue will deduplicate them to reduce the number of jobs actually run.
1692 $mwServices->getJobQueueGroup()->push(
1693 RebuildMessageGroupStatsJob::newRefreshGroupsJob( [ $group->getId() ] )
1694 );
1695
1696 // Logic to update translation pages, skipped if we are in a middle of a deletion
1697 if ( self::$isDeleteTranslatableBundleJobRunning ) {
1698 return;
1699 }
1700
1701 $target = $group->getTitle();
1702 $langCode = $handle->getCode();
1703 $fname = __METHOD__;
1704
1705 $dbw = $mwServices->getConnectionProvider()->getPrimaryDatabase();
1706 $callback = function () use (
1707 $dbw,
1708 $target,
1709 $handle,
1710 $langCode,
1711 $user,
1712 $reason,
1713 $fname
1714 ) {
1715 $translationPageTitle = $target->getSubpage( $langCode );
1716 // Do a more thorough check for the translation page in case the translation page is deleted in a
1717 // different transaction.
1718 if ( !$translationPageTitle || !$translationPageTitle->exists( IDBAccessObject::READ_LATEST ) ) {
1719 return;
1720 }
1721
1722 $dbw->startAtomic( $fname );
1723
1724 $page = TranslatablePage::newFromTitle( $target );
1725
1726 if ( !$handle->isDoc() ) {
1727 $unitTitle = $handle->getTitle();
1728 // Assume that $user and $reason for the first deletion is the same for all
1729 self::updateTranslationPage(
1730 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1731 );
1732 }
1733
1734 $dbw->endAtomic( $fname );
1735 };
1736
1737 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1738 }
1739
1744 public static function onReplaceTextFilterPageTitlesForEdit( array &$titles ): void {
1745 foreach ( $titles as $index => $title ) {
1746 $handle = new MessageHandle( $title );
1747 if ( Utilities::isTranslationPage( $handle ) ) {
1748 unset( $titles[ $index ] );
1749 }
1750 }
1751 }
1752
1757 public static function onReplaceTextFilterPageTitlesForRename( array &$titles ): void {
1758 foreach ( $titles as $index => $title ) {
1759 $handle = new MessageHandle( $title );
1760 if (
1761 TranslatablePage::isSourcePage( $title ) ||
1762 Utilities::isTranslationPage( $handle )
1763 ) {
1764 unset( $titles[ $index ] );
1765 }
1766 }
1767 }
1768
1769 public static function getSpecialManageMessageGroupSubscriptionsLink(
1770 Context $context,
1771 Config $config
1772 ): array {
1773 return [
1774 'pagelink' => SpecialPage::getTitleFor( 'ManageMessageGroupSubscriptions' )->getPrefixedText()
1775 ];
1776 }
1777
1782 public static function onLinksUpdateComplete( LinksUpdate $linksUpdate ) {
1783 $handle = new MessageHandle( $linksUpdate->getTitle() );
1784 if ( !Utilities::isTranslationPage( $handle ) ) {
1785 return;
1786 }
1787 $code = $handle->getCode();
1788 $categories = $linksUpdate->getParserOutput()->getCategoryNames();
1789 $editSummary = wfMessage(
1790 'translate-category-summary',
1791 $linksUpdate->getTitle()->getPrefixedText()
1792 )->inContentLanguage()->text();
1793 foreach ( $categories as $category ) {
1794 $categoryTitle = Title::makeTitle( NS_CATEGORY, $category );
1795 $categoryHandle = new MessageHandle( $categoryTitle );
1796 // Only create categories for the same language code to reduce
1797 // the potential for very deep recursion if a category is
1798 // a member of itself in a different language
1799 $categoryTranslationPage = TranslatablePage::isTranslationPage( $categoryTitle );
1800 if (
1801 $categoryTranslationPage
1802 && $categoryHandle->getCode() == $code
1803 && !$categoryTitle->exists()
1804 ) {
1805 self::updateTranslationPage(
1806 $categoryTranslationPage,
1807 $code,
1808 FuzzyBot::getUser(),
1809 EDIT_FORCE_BOT,
1810 $editSummary,
1811 RenderTranslationPageJob::ACTION_CATEGORIZATION
1812 );
1813 }
1814 }
1815 }
1816}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory());}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Constants for log channel names used in this extension.
Definition LogNames.php:13
const MAIN
Default log channel for the extension.
Definition LogNames.php:15
Class for pointing to messages, like Title class is for titles.
getGroup()
Get the primary MessageGroup this message belongs to.
figureMessage()
Recommended to use getCode and getKey instead.
isDoc()
Determine whether the current handle is for message documentation.
static tpSyntaxCheckForEditContent( $context, $content, $status, $summary)
Display nice error when editing content.
Definition Hooks.php:802
static tpSyntaxCheck(RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus)
When attempting to save, last resort.
Definition Hooks.php:852
static onReplaceTextFilterPageTitlesForRename(array &$titles)
Removes translatable and translation pages from the list of titles to be renamed Hook: ReplaceTextFil...
Definition Hooks.php:1757
static bool $isDeleteTranslatableBundleJobRunning
State flag used by DeleteTranslatableBundleJob for performance optimizations.
Definition Hooks.php:68
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:1610
static replaceMovePage(&$list)
Hook: SpecialPage_initList.
Definition Hooks.php:1427
static onTitleGetEditNotices(Title $title, int $oldid, array &$notices)
Display an edit notice for translatable source pages if it's enabled Hook: TitleGetEditNotices.
Definition Hooks.php:304
static onParserGetVariableValueSwitch(Parser $parser, array &$variableCache, string $magicWordId, ?string &$ret, PPFrame $frame)
Hook: ParserGetVariableValueSwitch.
Definition Hooks.php:463
static onGetUserPermissionsErrorsExpensive(Title $title, User $user, $action, &$result)
Prevent creation of orphan translation units in Translations namespace.
Definition Hooks.php:968
static preprocessTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforePreprocess.
Definition Hooks.php:164
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:1670
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:81
static onParserOutputPostCacheTransform(ParserOutput $out, &$text, array &$options)
Hook: ParserOutputPostCacheTransform.
Definition Hooks.php:191
static onSectionSave(WikiPage $wikiPage, User $user, TextContent $content, $summary, $minor, $flags, MessageHandle $handle)
This is triggered after an edit to translation unit page.
Definition Hooks.php:378
static fetchTranslatableTemplateAndTitle(?LinkTarget $contextLink, ?LinkTarget $templateLink, bool &$skip, ?RevisionRecord &$revRecord)
This sets &$revRecord to the revision of transcluded page translation if it exists,...
Definition Hooks.php:212
static onBeforePageDisplay(OutputPage $out, Skin $skin)
Hook: BeforePageDisplay.
Definition Hooks.php:335
static translatablePageHeader( $article, &$outputDone, &$pcache)
Hook: ArticleViewHeader.
Definition Hooks.php:1223
static onPageContentLanguage(Title $title, &$pageLang)
Set the right page content language for translated pages ("Page/xx").
Definition Hooks.php:288
static lockedPagesCheck(Title $title, User $user, $action, &$result)
Hook: getUserPermissionsErrorsExpensive.
Definition Hooks.php:1458
static updateTranstagOnNullRevisions(RevisionRecord $rev)
Page moving and page protection (and possibly other things) creates null revisions.
Definition Hooks.php:929
static onVisualEditorBeforeEditor(OutputPage $out, Skin $skin)
Hook: onVisualEditorBeforeEditor.
Definition Hooks.php:364
static replaceSubtitle(&$subpages, ?Skin $skin, OutputPage $out)
Hook: SkinSubPageSubtitle.
Definition Hooks.php:1481
static formatLanguageLink(array &$link, Title $linkTitle, Title $pageTitle, OutputPage $out)
Hooks: SkinTemplateGetLanguageLink.
Definition Hooks.php:772
static onLinksUpdateComplete(LinksUpdate $linksUpdate)
Create any redlinked categories marked for translation Hook: LinksUpdateComplete.
Definition Hooks.php:1782
static onGetMagicVariableIDs(&$variableIDs)
Hook: GetMagicVariableIDs.
Definition Hooks.php:456
static languages( $data, $params, $parser)
Definition Hooks.php:485
static addLanguageLinks(Title $title, array &$languageLinks)
Hooks: LanguageLinks.
Definition Hooks.php:692
static disableDelete( $article, $out, &$reason)
Redirects the delete action to our own for translatable pages.
Definition Hooks.php:1201
static onReplaceTextFilterPageTitlesForEdit(array &$titles)
Removes translation pages from the list of page titles to be edited Hook: ReplaceTextFilterPageTitles...
Definition Hooks.php:1744
static translateTab(Skin $skin, array &$tabs)
Converts the edit tab (if exists) for translation pages to translate tab.
Definition Hooks.php:1551
static addTranstagAfterSave(WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revisionRecord, EditResult $editResult)
Hook: PageSaveComplete.
Definition Hooks.php:884
Represents a parsing output produced by TranslatablePageParser.
Represents any kind of failure to parse a translatable page source code.
Mixed bag of methods related to translatable pages.
addReadyTag(int $revision)
Adds a tag which indicates that this page source is ready for marking for translation.
getMarkedTag()
Returns the latest revision which has marked tag, if any.
getReadyTag()
Returns the latest revision which has ready tag, if any.
getTranslationUrl( $code=false)
Produces a link to translation view of a translation page.
getPageDisplayTitle(string $languageCode)
Get translated page title.
This class represents one translation unit in a translatable page.
Minimal service container.
Definition Services.php:59
This class aims to provide efficient mechanism for fetching translation completion stats.
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
Wraps the translatable page sections into a message group.