Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
Hooks.php
1<?php
2
3// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
4
5namespace MediaWiki\Extension\Translate\PageTranslation;
6
7use Article;
8use Exception;
9use ManualLogEntry;
10use MediaWiki\Category\Category;
11use MediaWiki\CommentStore\CommentStoreComment;
12use MediaWiki\Config\Config;
13use MediaWiki\Content\Content;
14use MediaWiki\Content\TextContent;
15use MediaWiki\Context\IContextSource;
16use MediaWiki\Context\RequestContext;
17use MediaWiki\Deferred\DeferredUpdates;
18use MediaWiki\Deferred\LinksUpdate\LinksUpdate;
27use MediaWiki\Html\Html;
28use MediaWiki\Language\Language;
29use MediaWiki\Language\LanguageCode;
30use MediaWiki\Languages\LanguageNameUtils;
31use MediaWiki\Linker\LinkTarget;
32use MediaWiki\Logger\LoggerFactory;
33use MediaWiki\MainConfigNames;
34use MediaWiki\MediaWikiServices;
35use MediaWiki\Output\OutputPage;
36use MediaWiki\Page\PageIdentity;
37use MediaWiki\Page\PageReference;
38use MediaWiki\Parser\Parser;
39use MediaWiki\Parser\ParserOutput;
40use MediaWiki\Parser\ParserOutputFlags;
41use MediaWiki\Parser\PPFrame;
42use MediaWiki\ResourceLoader\Context;
43use MediaWiki\Revision\MutableRevisionRecord;
44use MediaWiki\Revision\RenderedRevision;
45use MediaWiki\Revision\RevisionRecord;
46use MediaWiki\Revision\SlotRecord;
47use MediaWiki\SpecialPage\SpecialPage;
48use MediaWiki\Status\Status;
49use MediaWiki\Storage\EditResult;
50use MediaWiki\StubObject\StubUserLang;
51use MediaWiki\Title\Title;
52use MediaWiki\User\User;
53use MediaWiki\User\UserIdentity;
54use Skin;
55use StatusValue;
56use UserBlockedError;
57use Wikimedia\Rdbms\IDBAccessObject;
58use Wikimedia\ScopedCallback;
59use WikiPage;
61
68class Hooks {
69 private const PAGEPROP_HAS_LANGUAGES_TAG = 'translate-has-languages-tag';
71 public static $allowTargetEdit = false;
73 public static bool $isDeleteTranslatableBundleJobRunning = false;
75 public static $renderingContext = false;
77 private static $languageLinkData = [];
78
86 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
87 if ( $text === null ) {
88 // SMW is unhelpfully sending null text if the source contains section tags. Do not explode.
89 return;
90 }
91
92 self::preprocessTagPage( $wikitextParser, $text, $state );
93
94 // Skip further interface message parsing
95 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
96 return;
97 }
98
99 // For section previews, perform additional clean-up, given tags are often
100 // unbalanced when we preview one section only.
101 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
102 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
103 $text = $translatablePageParser->cleanupTags( $text );
104 }
105
106 // Set display title
107 $title = MediaWikiServices::getInstance()
108 ->getTitleFactory()
109 ->castFromPageReference( $wikitextParser->getPage() );
110 if ( !$title ) {
111 return;
112 }
113
114 $page = TranslatablePage::isTranslationPage( $title );
115 if ( !$page ) {
116 return;
117 }
118
119 $wikitextParser->getOutput()->setUnsortedPageProperty( 'translate-is-translation' );
120
121 try {
122 self::$renderingContext = true;
123 [ , $code ] = Utilities::figureMessage( $title->getText() );
124 $name = $page->getPageDisplayTitle( $code );
125 if ( $name ) {
126 $name = $wikitextParser->recursivePreprocess( $name );
127
128 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
129 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
130 $name = $langConv->convert( $name );
131 $wikitextParser->getOutput()->setDisplayTitle( $name );
132 }
133 self::$renderingContext = false;
134 } catch ( Exception $e ) {
135 LoggerFactory::getInstance( LogNames::MAIN )->error(
136 'T302754 Failed to set display title for page {title}',
137 [
138 'title' => $title->getPrefixedDBkey(),
139 'text' => $text,
140 'pageid' => $title->getId(),
141 ]
142 );
143
144 // Re-throw to preserve behavior
145 throw $e;
146 }
147
148 $extensionData = [
149 'languagecode' => $code,
150 'messagegroupid' => $page->getMessageGroupId(),
151 'sourcepagetitle' => [
152 'namespace' => $page->getTitle()->getNamespace(),
153 'dbkey' => $page->getTitle()->getDBkey()
154 ]
155 ];
156
157 $wikitextParser->getOutput()->setExtensionData( 'translate-translation-page', $extensionData );
158 // Disable edit section links
159 $wikitextParser->getOutput()->setOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS );
160 }
161
169 public static function preprocessTagPage( $wikitextParser, &$text, $state ): void {
170 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
171
172 if ( $translatablePageParser->containsMarkup( $text ) ) {
173 try {
174 $parserOutput = $translatablePageParser->parse( $text );
175 // If parsing succeeds, replace text
176 $text = $parserOutput->sourcePageTextForRendering(
177 $wikitextParser->getTargetLanguage()
178 );
179 } catch ( ParsingFailure ) {
180 wfDebug( 'ParsingFailure caught; expected' );
181 }
182 } else {
183 // If the text doesn't contain <translate> markup, it can still contain <tvar> in the
184 // context of a Parsoid template expansion sub-pipeline. We strip these as well.
185 $unit = new TranslationUnit( $text );
186 $text = $unit->getTextForTrans();
187 }
188 }
189
201 public static function fetchTranslatableTemplateAndTitle(
202 ?LinkTarget $contextLink,
203 ?LinkTarget $templateLink,
204 bool &$skip,
205 ?RevisionRecord &$revRecord
206 ): void {
207 if ( !$templateLink ) {
208 return;
209 }
210
211 $templateTitle = Title::newFromLinkTarget( $templateLink );
212
213 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
214 if ( $templateTranslationPage ) {
215 // Template is referring to a translation page, fetch it and incase it doesn't
216 // exist, fetch the source fallback.
217 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
218 if ( !$revRecord ) {
219 // In rare cases fetching of the source fallback might fail. See: T323863
220 LoggerFactory::getInstance( LogNames::MAIN )->warning(
221 "T323863: Could not fetch any revision record for '{groupid}'",
222 [ 'groupid' => $templateTranslationPage->getMessageGroupId() ]
223 );
224 }
225 return;
226 }
227
228 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
229 return;
230 }
231
232 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
233
234 if ( !( $translatableTemplatePage->supportsTransclusion() ?? false ) ) {
235 // Page being transcluded does not support language aware transclusion
236 return;
237 }
238
239 $store = MediaWikiServices::getInstance()->getRevisionStore();
240
241 if ( $contextLink ) {
242 // Fetch the context page language, and then check if template is present in that language
243 $templateTranslationTitle = $templateTitle->getSubpage(
244 Title::newFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
245 );
246
247 if ( $templateTranslationTitle ) {
248 if ( $templateTranslationTitle->exists() ) {
249 // Template is present in the context page language, fetch the revision record and return
250 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
251 } else {
252 // In case the template has not been translated to the context page language,
253 // we assign a MutableRevisionRecord in order to add a dependency, so that when
254 // it is created, the newly created page is loaded rather than the fallback
255 $revRecord = new MutableRevisionRecord( $templateTranslationTitle );
256 }
257 return;
258 }
259 }
260
261 // Context page information not available OR the template translation title could not be determined.
262 // Fetch and return the RevisionRecord of the template in the source language
263 $sourceTemplateTitle = $templateTitle->getSubpage(
264 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
265 );
266 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
267 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
268 }
269 }
270
277 public static function onPageContentLanguage( Title $title, &$pageLang ) {
278 // For translation pages, parse plural, grammar etc. with correct language,
279 // and set the right direction
280 if ( TranslatablePage::isTranslationPage( $title ) ) {
281 [ , $code ] = Utilities::figureMessage( $title->getText() );
282 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
283 }
284 }
285
293 public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) {
294 if ( TranslatablePage::isSourcePage( $title ) ) {
295 $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
296 if ( !$msg->isDisabled() ) {
297 $notices['translate-tag'] = $msg->parseAsBlock();
298 }
299
300 $notices[] = Html::warningBox(
301 wfMessage( 'tps-edit-sourcepage-text' )->parse(),
302 'translate-edit-documentation'
303 );
304
305 // The check is "we're using visual editor for WYSIWYG" (as opposed to "for wikitext
306 // edition") - the message will not be displayed in that case.
307 $request = RequestContext::getMain()->getRequest();
308 if ( $request->getVal( 'action' ) === 'visualeditor' &&
309 $request->getVal( 'paction' ) !== 'wikitext'
310 ) {
311 $notices[] = Html::warningBox(
312 wfMessage( 'tps-edit-sourcepage-ve-warning-limited-text' )->parse(),
313 'translate-edit-documentation'
314 );
315 }
316 }
317 }
318
324 public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
325 global $wgTranslatePageTranslationULS;
326
327 $title = $out->getTitle();
328 $isSource = TranslatablePage::isSourcePage( $title );
329 $isTranslation = TranslatablePage::isTranslationPage( $title );
330
331 if ( $isSource || $isTranslation ) {
332 if ( $wgTranslatePageTranslationULS ) {
333 $out->addModules( 'ext.translate.pagetranslation.uls' );
334 }
335
336 if ( $isSource ) {
337 // Adding a help notice
338 $out->addModuleStyles( 'ext.translate.edit.documentation.styles' );
339 }
340
341 $out->addModuleStyles( 'ext.translate' );
342
343 $out->addJsConfigVars( 'wgTranslatePageTranslation', $isTranslation ? 'translation' : 'source' );
344 }
345 }
346
353 public static function onVisualEditorBeforeEditor( OutputPage $out, Skin $skin ) {
354 return !TranslatablePage::isTranslationPage( $out->getTitle() );
355 }
356
367 public static function onSectionSave(
368 WikiPage $wikiPage,
369 User $user,
370 TextContent $content,
371 $summary,
372 $minor,
373 $flags,
374 MessageHandle $handle
375 ) {
376 // FuzzyBot may do some duplicate work already worked on by other jobs
377 if ( $user->equals( FuzzyBot::getUser() ) ) {
378 return;
379 }
380
381 $group = $handle->getGroup();
382 if ( !$group instanceof WikiPageMessageGroup ) {
383 return;
384 }
385
386 // Finally we know the title and can construct a Translatable page
387 $page = TranslatablePage::newFromTitle( $group->getTitle() );
388
389 // Update the target translation page
390 if ( !$handle->isDoc() ) {
391 $code = $handle->getCode();
392 DeferredUpdates::addCallableUpdate(
393 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
394 $unitTitle = $handle->getTitle();
395 self::updateTranslationPage( $page, $code, $user, $flags, $summary, null, $unitTitle );
396 }
397 );
398 }
399 }
400
401 private static function updateTranslationPage(
402 TranslatablePage $page,
403 string $code,
404 User $user,
405 int $flags,
406 string $summary,
407 ?string $triggerAction = null,
408 ?Title $unitTitle = null
409 ): void {
410 $source = $page->getTitle();
411 $target = $source->getSubpage( $code );
412 $mwInstance = MediaWikiServices::getInstance();
413
414 // We don't know and don't care
415 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
416
417 // Update the target page
418 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() : null;
419 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
420 $session = null;
421 if ( !$user->equals( FuzzyBot::getUser() ) ) {
422 $session = RequestContext::getMain()->exportSession();
423 }
424 $job->setUser( $user, $session );
425 $job->setSummary( $summary );
426 $job->setFlags( $flags );
427 $mwInstance->getJobQueueGroup()->push( $job );
428
429 // Invalidate caches so that language bar is up-to-date
430 $pages = $page->getTranslationPages();
431 $wikiPageFactory = $mwInstance->getWikiPageFactory();
432 foreach ( $pages as $title ) {
433 if ( $title->equals( $target ) ) {
434 // Handled by the RenderTranslationPageJob
435 continue;
436 }
437
438 $wikiPage = $wikiPageFactory->newFromTitle( $title );
439 $wikiPage->doPurge();
440 }
441 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
442 $sourceWikiPage->doPurge();
443 }
444
449 public static function onGetMagicVariableIDs( &$variableIDs ): void {
450 $variableIDs[] = 'translatablepage';
451 }
452
456 public static function onParserGetVariableValueSwitch(
457 Parser $parser,
458 array &$variableCache,
459 string $magicWordId,
460 ?string &$ret,
461 PPFrame $frame
462 ): void {
463 switch ( $magicWordId ) {
464 case 'translatablepage':
465 $pageStatus = self::getTranslatablePageStatus( $parser->getPage() );
466 $ret = $pageStatus !== null ? $pageStatus['page']->getTitle()->getPrefixedText() : '';
467 $variableCache[$magicWordId] = $ret;
468 break;
469 }
470 }
471
478 public static function languages( $data, $params, $parser ) {
479 global $wgPageTranslationLanguageList;
480
481 if ( $wgPageTranslationLanguageList === 'sidebar-only' ) {
482 return '';
483 }
484
485 self::$renderingContext = true;
486 $context = new ScopedCallback( static function () {
487 self::$renderingContext = false;
488 } );
489
490 // Store a property that we can avoid adding language links when
491 // $wgPageTranslationLanguageList === 'sidebar-fallback'
492 $parser->getOutput()->setUnsortedPageProperty( self::PAGEPROP_HAS_LANGUAGES_TAG );
493
494 $currentPage = $parser->getPage();
495 $pageStatus = self::getTranslatablePageStatus( $currentPage );
496 if ( !$pageStatus ) {
497 return '';
498 }
499
500 $page = $pageStatus[ 'page' ];
501 $status = $pageStatus[ 'languages' ];
502 $pageTitle = $page->getTitle();
503
504 // Sort by language code, which seems to be the only sane method
505 ksort( $status );
506
507 // This way the parser knows to fragment the parser cache by language code
508 $userLang = $parser->getOptions()->getUserLangObj();
509 $userLangCode = $userLang->getCode();
510 // Should call $page->getMessageGroup()->getSourceLanguage(), but
511 // group is sometimes null on WMF during page moves, reason unknown.
512 // This should do the same thing for now.
513 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
514
515 $languages = [];
516 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
517 foreach ( $status as $code => $percent ) {
518 // Get autonyms (null)
519 $name = Utilities::getLanguageName( $code, LanguageNameUtils::AUTONYMS );
520
521 // Add links to other languages
522 $suffix = ( $code === $sourceLanguage ) ? '' : "/$code";
523 $targetTitleString = $pageTitle->getDBkey() . $suffix;
524 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
525
526 $classes = [];
527 if ( $code === $userLangCode ) {
528 $classes[] = 'mw-pt-languages-ui';
529 }
530
531 $linker = $parser->getLinkRenderer();
532 $lang = $langFactory->getLanguage( $code );
533 if ( $currentPage->isSamePageAs( $subpage ) ) {
534 $classes[] = 'mw-pt-languages-selected';
535 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
536 $attribs = [
537 'class' => $classes,
538 'lang' => $lang->getHtmlCode(),
539 'dir' => $lang->getDir(),
540 ];
541
542 $contents = Html::element( 'span', $attribs, $name );
543 } elseif ( $subpage->isKnown() ) {
544 $pagename = $page->getPageDisplayTitle( $code );
545 if ( !is_string( $pagename ) ) {
546 $pagename = $subpage->getPrefixedText();
547 }
548
549 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
550
551 $title = wfMessage( 'tpt-languages-nonzero' )
552 ->page( $parser->getPage() )
553 ->inLanguage( $userLang )
554 ->params( $pagename )
555 ->numParams( 100 * $percent )
556 ->text();
557 $attribs = [
558 'title' => $title,
559 'class' => $classes,
560 'lang' => $lang->getHtmlCode(),
561 'dir' => $lang->getDir(),
562 ];
563
564 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
565 } else {
566 /* When language is included because it is a priority language,
567 * but translations don't exist link directly to the
568 * translation view. */
569 $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' );
570 $params = [
571 'group' => $page->getMessageGroupId(),
572 'language' => $code,
573 'task' => 'view'
574 ];
575
576 $classes[] = 'new'; // For red link color
577
578 $attribs = [
579 'title' => wfMessage( 'tpt-languages-zero' )
580 ->page( $parser->getPage() )
581 ->inLanguage( $userLang )
582 ->text(),
583 'class' => $classes,
584 'lang' => $lang->getHtmlCode(),
585 'dir' => $lang->getDir(),
586 ];
587 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
588 }
589 $languages[ $name ] = Html::rawElement( 'li', [], $contents );
590 }
591
592 // Sort languages by autonym
593 ksort( $languages );
594 $languages = array_values( $languages );
595 $languages = implode( "\n", $languages );
596
597 $out = Html::openElement( 'div', [
598 'class' => 'mw-pt-languages noprint navigation-not-searchable',
599 'lang' => $userLang->getHtmlCode(),
600 'dir' => $userLang->getDir()
601 ] );
602 $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ],
603 wfMessage( 'tpt-languages-legend' )
604 ->page( $parser->getPage() )
605 ->inLanguage( $userLang )
606 ->escaped()
607 );
608 $out .= Html::rawElement(
609 'ul',
610 [ 'class' => 'mw-pt-languages-list' ],
611 $languages
612 );
613 $out .= Html::closeElement( 'div' );
614
615 $parser->getOutput()->addModuleStyles( [
616 'ext.translate.tag.languages',
617 ] );
618
619 return $out;
620 }
621
628 private static function tpProgressIcon( float $percent ) {
629 $classes = [ 'mw-pt-progress' ];
630 $percent *= 100;
631 if ( $percent < 15 ) {
632 $classes[] = 'mw-pt-progress--low';
633 } elseif ( $percent < 70 ) {
634 $classes[] = 'mw-pt-progress--med';
635 } elseif ( $percent < 100 ) {
636 $classes[] = 'mw-pt-progress--high';
637 } else {
638 $classes[] = 'mw-pt-progress--complete';
639 }
640 return $classes;
641 }
642
647 private static function getTranslatablePageStatus( ?PageReference $pageReference ): ?array {
648 if ( $pageReference === null ) {
649 return null;
650 }
651 $title = Title::newFromPageReference( $pageReference );
652 // Check if this is a source page or a translation page
653 $page = TranslatablePage::newFromTitle( $title );
654 if ( $page->getMarkedTag() === null ) {
655 $page = TranslatablePage::isTranslationPage( $title );
656 }
657
658 if ( $page === false || $page->getMarkedTag() === null ) {
659 return null;
660 }
661
662 $status = $page->getTranslationPercentages();
663 if ( !$status ) {
664 return null;
665 }
666
667 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
668 // If priority languages have been set, always show those languages
669 $priorityLanguages = $messageGroupMetadata->get( $page->getMessageGroupId(), 'prioritylangs' );
670 if ( $priorityLanguages !== false && $priorityLanguages !== '' ) {
671 $status += array_fill_keys( explode( ',', $priorityLanguages ), 0 );
672 }
673
674 return [
675 'page' => $page,
676 'languages' => $status
677 ];
678 }
679
685 public static function addLanguageLinks( Title $title, array &$languageLinks ) {
686 global $wgPageTranslationLanguageList;
687
688 if ( $wgPageTranslationLanguageList === 'tag-only' ) {
689 return;
690 }
691
692 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) {
693 $pageProps = MediaWikiServices::getInstance()->getPageProps();
694 $languageProp = $pageProps->getProperties( $title, self::PAGEPROP_HAS_LANGUAGES_TAG );
695 if ( $languageProp !== [] ) {
696 return;
697 }
698 }
699
700 // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only'
701
702 $status = self::getTranslatablePageStatus( $title );
703 if ( !$status ) {
704 return;
705 }
706
707 self::$renderingContext = true;
708 $context = new ScopedCallback( static function () {
709 self::$renderingContext = false;
710 } );
711
712 $page = $status[ 'page' ];
713 $languages = $status[ 'languages' ];
714 $mwServices = MediaWikiServices::getInstance();
715 $en = $mwServices->getLanguageFactory()->getLanguage( 'en' );
716
717 // Batch the Title::exists queries used below
718 $lb = $mwServices->getLinkBatchFactory()->newLinkBatch();
719 foreach ( array_keys( $languages ) as $code ) {
720 $title = $page->getTitle()->getSubpage( $code );
721 $lb->addObj( $title );
722 }
723 $lb->execute();
724 $languageNameUtils = $mwServices->getLanguageNameUtils();
725 foreach ( $languages as $code => $percentage ) {
726 $title = $page->getTitle()->getSubpage( $code );
727 $placeholderValue = "$code-x-pagetranslation:{$title->getPrefixedText()}";
728 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
729
730 if ( $title->exists() ) {
731 $href = $title->getLocalURL();
732 $classes = self::tpProgressIcon( (float)$percentage );
733 $titleAttribute = wfMessage( 'tpt-languages-nonzero' )
734 ->params( $translatedName )
735 ->numParams( 100 * $percentage );
736 } else {
737 $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [
738 'group' => $page->getMessageGroupId(),
739 'language' => $code,
740 ] );
741 $classes = [ 'mw-pt-progress--none' ];
742 $titleAttribute = wfMessage( 'tpt-languages-zero' );
743 }
744
745 self::$languageLinkData[ $placeholderValue ] = [
746 'href' => $href,
747 'language' => $code,
748 'classes' => $classes,
749 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
750 'title' => $titleAttribute,
751 ];
752
753 // Insert a placeholder which we will then fix up in SkinTemplateGetLanguageLink hook handler
754 $languageLinks[] = $placeholderValue;
755 }
756 }
757
765 public static function formatLanguageLink(
766 array &$link,
767 Title $linkTitle,
768 Title $pageTitle,
769 OutputPage $out
770 ) {
771 $data = self::$languageLinkData[$link['text']] ?? null;
772 if ( !$data ) {
773 return;
774 }
775
776 $link['class'] .= ' interwiki-pagetranslation ' . implode( ' ', $data['classes'] );
777 $link['href'] = $data['href'];
778 $link['text'] = $data['autonym'];
779 $link['title'] = $data['title']->inLanguage( $out->getLanguage()->getCode() )->text();
780 $link['lang'] = LanguageCode::bcp47( $data['language'] );
781 $link['hreflang'] = LanguageCode::bcp47( $data['language'] );
782
783 $out->addModuleStyles( 'ext.translate.tag.languages' );
784 }
785
795 public static function tpSyntaxCheckForEditContent(
796 $context,
797 $content,
798 $status,
799 $summary
800 ) {
801 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
802
803 if ( $syntaxErrorStatus ) {
804 $status->merge( $syntaxErrorStatus );
805 return $syntaxErrorStatus->isGood();
806 }
807
808 return true;
809 }
810
811 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
812 if ( !$page || !self::isAllowedContentModel( $content, $page ) ) {
813 return null;
814 }
815
816 '@phan-var TextContent $content';
817 $text = $content->getText();
818
819 // See T154500
820 $text = TextContent::normalizeLineEndings( $text );
821 $status = Status::newGood();
822 $parser = Services::getInstance()->getTranslatablePageParser();
823 if ( $parser->containsMarkup( $text ) ) {
824 try {
825 $parser->parse( $text );
826 } catch ( ParsingFailure $e ) {
827 $status->fatal( $e->getMessageSpecification() );
828 }
829 }
830
831 return $status;
832 }
833
845 public static function tpSyntaxCheck(
846 RenderedRevision $renderedRevision,
847 UserIdentity $user,
848 CommentStoreComment $summary,
849 $flags,
850 Status $hookStatus
851 ) {
852 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
853
854 $status = self::tpSyntaxError(
855 $renderedRevision->getRevision()->getPage(),
856 $content
857 );
858
859 if ( $status ) {
860 $hookStatus->merge( $status );
861 return $status->isGood();
862 }
863
864 return true;
865 }
866
877 public static function addTranstagAfterSave(
878 WikiPage $wikiPage,
879 UserIdentity $userIdentity,
880 string $summary,
881 int $flags,
882 RevisionRecord $revisionRecord,
883 EditResult $editResult
884 ) {
885 $content = $wikiPage->getContent();
886
887 // Only allow translating configured content models (T360544)
888 if ( !self::isAllowedContentModel( $content, $wikiPage ) ) {
889 return;
890 }
891
892 '@phan-var TextContent $content';
893 $text = $content->getText();
894
895 $parser = Services::getInstance()->getTranslatablePageParser();
896 if ( $parser->containsMarkup( $text ) ) {
897 // Add the ready tag
898 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
899 $page->addReadyTag( $revisionRecord->getId() );
900 }
901
902 // Schedule a deferred status update for the translatable page.
903 $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore();
904 $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() );
905 }
906
922 public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) {
923 $parentId = $rev->getParentId();
924 if ( $parentId === 0 || $parentId === null ) {
925 // No parent, bail out.
926 return;
927 }
928
929 $prevRev = MediaWikiServices::getInstance()
930 ->getRevisionLookup()
931 ->getRevisionById( $parentId );
932
933 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
934 // Not a null revision, bail out.
935 return;
936 }
937
938 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
939 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
940 $bundle = $bundleFactory->getBundle( $title );
941
942 if ( $bundle ) {
943 $bundleStore = $bundleFactory->getStore( $bundle );
944 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
945 }
946 }
947
963 Title $title,
964 User $user,
965 $action,
966 &$result
967 ) {
968 $handle = new MessageHandle( $title );
969
970 if ( !$handle->isPageTranslation() || $action === 'read' ) {
971 return true;
972 }
973
974 $isValid = true;
975 $groupId = null;
976
977 if ( $handle->isValid() ) {
978 $group = $handle->getGroup();
979 $groupId = $group->getId();
980 $permissionTitleCheck = null;
981
982 if ( $group instanceof WikiPageMessageGroup ) {
983 $permissionTitleCheck = $group->getTitle();
984 } elseif ( $group instanceof MessageBundleMessageGroup ) {
985 // TODO: This check for MessageBundle related permission should be in
986 // the MessageBundleTranslation/Hook
987 $permissionTitleCheck = Title::newFromID( $group->getBundlePageId() );
988 }
989
990 if ( $permissionTitleCheck ) {
991 if ( $handle->getCode() === $group->getSourceLanguage() && !$user->equals( FuzzyBot::getUser() ) ) {
992 // Allow the same set of actions allowed for translation pages - in particular
993 // if something bad somehow gets marked for translation, deleting
994 // revisions everywhere should be possible without deliberately
995 // invalidating the unit
996 $allowedActionList = [
997 'read', 'deletedtext', 'deletedhistory',
998 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884
999 'review', // FlaggedRevs
1000 'patrol', // T151172
1001 ];
1002 if ( !in_array( $action, $allowedActionList ) ) {
1003 $result = [ 'tpt-cant-edit-source-language', $permissionTitleCheck ];
1004 return false;
1005 }
1006 }
1007 // Check for blocks
1008 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1009 if ( $permissionManager->isBlockedFrom( $user, $permissionTitleCheck ) ) {
1010 $block = $user->getBlock();
1011 if ( $block ) {
1012 $error = new UserBlockedError( $block, $user );
1013 $errorMessage = $error->getMessageObject();
1014 $result = array_merge( [ $errorMessage->getKey() ], $errorMessage->getParams() );
1015 return false;
1016 }
1017 }
1018 if ( $action === 'create' && $permissionTitleCheck->inNamespace( NS_CATEGORY ) ) {
1019 $renderedPage = $permissionTitleCheck->getSubpage( $handle->getCode() );
1020 if ( !$renderedPage->exists() ) {
1021 $cat = Category::newFromTitle( $renderedPage );
1022 if ( $cat->getMemberCount() === 0 ) {
1023 if ( !$permissionManager->userCan( 'translate-empty-category', $user, $renderedPage ) ) {
1024 $result = [ 'tpt-create-empty-category', $renderedPage ];
1025 return false;
1026 }
1027 }
1028 }
1029 }
1030 }
1031 }
1032
1033 // Allow editing units that become orphaned in regular use, so that
1034 // people can delete them or fix links or other issues in them.
1035 if ( $action !== 'create' ) {
1036 return true;
1037 }
1038
1039 if ( !$handle->isValid() ) {
1040 // TODO: These checks may no longer be needed
1041 // Sometimes the message index can be out of date. Either the rebuild job failed or
1042 // it just hasn't finished yet. Do a secondary check to make sure we are not
1043 // inconveniencing translators for no good reason.
1044 // See https://phabricator.wikimedia.org/T221119
1045 $translatablePage = self::checkTranslatablePageSlow( $title );
1046 MediaWikiServices::getInstance()->getStatsFactory()
1047 ->withComponent( 'Translate' )
1048 ->getCounter( 'slow_translatable_page_check' )
1049 ->setLabel( 'valid', $translatablePage ? 'yes' : 'no' )
1050 ->increment();
1051
1052 if ( $translatablePage ) {
1053 $groupId = $translatablePage->getMessageGroupId();
1054 } else {
1055 $isValid = false;
1056 }
1057 }
1058
1059 if ( $isValid ) {
1060 $error = self::getTranslationRestrictions( $handle, $groupId );
1061 $result = $error ?: $result;
1062 return $error === [];
1063 }
1064
1065 // Don't allow editing invalid messages that do not belong to any translatable page
1066 LoggerFactory::getInstance( LogNames::MAIN )->info(
1067 'Unknown translation page: {title}',
1068 [ 'title' => $title->getPrefixedDBkey() ]
1069 );
1070 $result = [ 'tpt-unknown-page' ];
1071 return false;
1072 }
1073
1074 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?TranslatablePage {
1075 $parts = TranslatablePage::parseTranslationUnit( $unit );
1076 $translationPageTitle = Title::newFromText(
1077 $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ]
1078 );
1079 if ( !$translationPageTitle ) {
1080 return null;
1081 }
1082
1083 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1084 if ( !$translatablePage ) {
1085 return null;
1086 }
1087
1088 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1089 $store = $factory->getReader( $translatablePage->getTitle() );
1090 $units = $store->getNames();
1091
1092 if ( !in_array( $parts[ 'section' ], $units ) ) {
1093 return null;
1094 }
1095
1096 return $translatablePage;
1097 }
1098
1106 private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) {
1107 global $wgTranslateDocumentationLanguageCode;
1108
1109 // Allow adding message documentation even when translation is restricted
1110 if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) {
1111 return [];
1112 }
1113
1114 $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata();
1115 // Check if anything is prevented for the group in the first place
1116 $force = $messageGroupMetadata->get( $groupId, 'priorityforce' );
1117 if ( $force !== 'on' ) {
1118 return [];
1119 }
1120
1121 // And finally check whether the language is in the inclusion list
1122 $languages = $messageGroupMetadata->get( $groupId, 'prioritylangs' );
1123 $reason = $messageGroupMetadata->get( $groupId, 'priorityreason' );
1124 if ( !$languages ) {
1125 if ( $reason ) {
1126 return [ 'tpt-translation-restricted-no-priority-languages', $reason ];
1127 }
1128 return [ 'tpt-translation-restricted-no-priority-languages-no-reason' ];
1129 }
1130
1131 $filter = array_flip( explode( ',', $languages ) );
1132 if ( !isset( $filter[$handle->getCode()] ) ) {
1133 if ( $reason ) {
1134 return [ 'tpt-translation-restricted', $reason ];
1135 }
1136
1137 return [ 'tpt-translation-restricted-no-reason' ];
1138 }
1139
1140 return [];
1141 }
1142
1152 public static function preventDirectEditing( Title $title, User $user, $action, &$result ) {
1153 if ( self::$allowTargetEdit ) {
1154 return true;
1155 }
1156
1157 $inclusionList = [
1158 'read', 'deletedtext', 'deletedhistory',
1159 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884
1160 'review', // FlaggedRevs
1161 'patrol', // T151172
1162 'translate-empty-category' // This is checked below on the page that would be created
1163 ];
1164 $needsPageTranslationRight = in_array( $action, [ 'delete', 'undelete' ] );
1165 if ( in_array( $action, $inclusionList ) ||
1166 ( $needsPageTranslationRight && $user->isAllowed( 'pagetranslation' ) )
1167 ) {
1168 return true;
1169 }
1170
1171 $page = TranslatablePage::isTranslationPage( $title );
1172 if ( $page !== false && $page->getMarkedTag() ) {
1173 $mwService = MediaWikiServices::getInstance();
1174 if ( $needsPageTranslationRight ) {
1175 $context = RequestContext::getMain();
1176 $statusFormatter = $mwService->getFormatterFactory()->getStatusFormatter( $context );
1177 $permissionError = $mwService->getPermissionManager()
1178 ->newFatalPermissionDeniedStatus( 'pagetranslation', $context );
1179 $result = $statusFormatter->getMessage( $permissionError );
1180 return false;
1181 }
1182
1183 [ , $code ] = Utilities::figureMessage( $title->getText() );
1184
1185 if ( $code === $page->getMessageGroup()->getSourceLanguage() ) {
1186 $result = [
1187 'tpt-source-mirror',
1188 ':' . $page->getTitle()->getPrefixedText()
1189 ];
1190 return false;
1191 }
1192
1193 $translationUrl = $mwService->getUrlUtils()->expand(
1194 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1195 );
1196
1197 $result = [
1198 'tpt-target-page',
1199 ':' . $page->getTitle()->getPrefixedText(),
1200 // This url shouldn't get cached
1201 $translationUrl
1202 ];
1203
1204 return false;
1205 }
1206
1207 return true;
1208 }
1209
1214 public static function preventMoves( Title $oldTitle, Title $newTitle, StatusValue $status ) {
1215 if ( self::$allowTargetEdit ) {
1216 return;
1217 }
1218 // Don’t use TranslatablePage::isSourcePage() as it uses a cache that might be stale
1219 if ( TranslatablePage::newFromTitle( $oldTitle )->getMarkedTag() !== null ) {
1220 $status->fatal( 'tpt-manual-move-source', $oldTitle->getPrefixedText() );
1221 }
1222 $tp = TranslatablePage::isTranslationPage( $oldTitle );
1223 if ( $tp ) {
1224 // Somewhat confusingly $tp->getTitle actually returns the source page here
1225 $status->fatal( 'tpt-manual-move-translation', $tp->getTitle() );
1226 }
1227 }
1228
1239 public static function disableDelete( $article, $out, &$reason ) {
1240 $title = $article->getTitle();
1241 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1242 $isDeletableBundle = $bundle && $bundle->isDeletable();
1243 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1244 $new = SpecialPage::getTitleFor(
1245 'PageTranslationDeletePage',
1246 $title->getPrefixedText()
1247 );
1248 $out->redirect( $new->getFullURL() );
1249 }
1250
1251 return true;
1252 }
1253
1261 public static function translatablePageHeader( $article, &$outputDone, &$pcache ) {
1262 if ( $article->getOldID() ) {
1263 return;
1264 }
1265
1266 $articleTitle = $article->getTitle();
1267 $transPage = TranslatablePage::isTranslationPage( $articleTitle );
1268 $context = $article->getContext();
1269 if ( $transPage ) {
1270 self::translationPageHeader( $context, $transPage );
1271 } else {
1272 $viewTranslatablePage = Services::getInstance()->getTranslatablePageView();
1273 $user = $context->getUser();
1274 if ( $viewTranslatablePage->canDisplayTranslationSettingsBanner( $articleTitle, $user ) ) {
1275 $output = $context->getOutput();
1276 $pageUrl = SpecialPage::getTitleFor( 'PageTranslation' )->getFullURL( [
1277 'do' => 'settings',
1278 'target' => $articleTitle->getPrefixedDBkey(),
1279 ] );
1280 $output->addHTML(
1281 Html::noticeBox(
1282 $context->msg( 'pt-cta-mark-translation', $pageUrl )->parse(),
1283 'translate-cta-pt-mark'
1284 )
1285 );
1286 } else {
1287 self::sourcePageHeader( $context );
1288 }
1289 }
1290 }
1291
1292 private static function sourcePageHeader( IContextSource $context ) {
1293 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1294
1295 $language = $context->getLanguage();
1296 $title = $context->getTitle();
1297
1298 $page = TranslatablePage::newFromTitle( $title );
1299
1300 $marked = $page->getMarkedTag();
1301 $ready = $page->getReadyTag();
1302 $latest = $title->getLatestRevID();
1303
1304 $actions = [];
1305 if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) {
1306 $actions[] = self::getTranslateLink( $context, $page, null );
1307 }
1308
1309 $hasChanges = $ready === $latest && $marked !== $latest;
1310 if ( $hasChanges ) {
1311 $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] );
1312
1313 if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) {
1314 $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' );
1315 $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ];
1316
1317 if ( $marked === null ) {
1318 // This page has never been marked
1319 $linkDesc = $context->msg( 'translate-tag-markthis' )->text();
1320 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1321 } else {
1322 $markUrl = $pageTranslation->getFullURL( $params );
1323 $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl )
1324 ->parse();
1325 }
1326 } else {
1327 $actions[] = $context->msg( 'translate-tag-hasnew', $diffUrl )->parse();
1328 }
1329 }
1330
1331 if ( !count( $actions ) ) {
1332 return;
1333 }
1334
1335 $header = Html::rawElement(
1336 'div',
1337 [
1338 'class' => 'mw-pt-translate-header noprint nomobile',
1339 'dir' => $language->getDir(),
1340 'lang' => $language->getHtmlCode(),
1341 ],
1342 $language->semicolonList( $actions )
1343 );
1344
1345 $context->getOutput()->addHTML( $header );
1346 }
1347
1348 private static function getTranslateLink(
1349 IContextSource $context,
1350 TranslatablePage $page,
1351 ?string $langCode
1352 ): string {
1353 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1354
1355 return $linker->makeKnownLink(
1356 SpecialPage::getTitleFor( 'Translate' ),
1357 $context->msg( 'translate-tag-translate-link-desc' )->text(),
1358 [],
1359 [
1360 'group' => $page->getMessageGroupId(),
1361 'language' => $langCode,
1362 'action' => 'page',
1363 'filter' => '',
1364 'action_source' => 'translate_page'
1365 ]
1366 );
1367 }
1368
1369 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1370 global $wgTranslateKeepOutdatedTranslations;
1371
1372 $title = $context->getTitle();
1373 if ( !$title->exists() ) {
1374 return;
1375 }
1376
1377 [ , $code ] = Utilities::figureMessage( $title->getText() );
1378
1379 // Get the translation percentage
1380 $pers = $page->getTranslationPercentages();
1381 $per = 0;
1382 if ( isset( $pers[$code] ) ) {
1383 $per = $pers[$code] * 100;
1384 }
1385
1386 $language = $context->getLanguage();
1387 $output = $context->getOutput();
1388
1389 if ( $page->getSourceLanguageCode() === $code ) {
1390 // If we are on the source language page, link to translate for user's language
1391 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1392 } else {
1393 $mwService = MediaWikiServices::getInstance();
1394
1395 $translationUrl = $mwService->getUrlUtils()->expand(
1396 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1397 );
1398
1399 $msg = $context->msg( 'tpt-translation-intro',
1400 $translationUrl,
1401 ':' . $page->getTitle()->getPrefixedText(),
1402 $language->formatNum( $per )
1403 )->parse();
1404 }
1405
1406 $header = Html::rawElement(
1407 'div',
1408 [
1409 'class' => 'mw-pt-translate-header noprint',
1410 'dir' => $language->getDir(),
1411 'lang' => $language->getHtmlCode(),
1412 ],
1413 $msg
1414 );
1415
1416 $output->addHTML( $header );
1417
1418 if ( $wgTranslateKeepOutdatedTranslations ) {
1419 $groupId = $page->getMessageGroupId();
1420 // This is already calculated and cached by above call to getTranslationPercentages
1421 $stats = MessageGroupStats::forItem( $groupId, $code );
1422 if ( $stats[MessageGroupStats::FUZZY] ) {
1423 // Only show if there is fuzzy messages
1424 $wrap = Html::rawElement(
1425 'div',
1426 [
1427 'class' => 'mw-pt-translate-header',
1428 'dir' => $language->getDir(),
1429 'lang' => $language->getHtmlCode()
1430 ],
1431 '<span class="mw-translate-fuzzy">$1</span>'
1432 );
1433
1434 $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] );
1435 }
1436 }
1437 }
1438
1439 private static function isAllowedContentModel( Content $content, PageReference $page ): bool {
1440 $config = MediaWikiServices::getInstance()->getMainConfig();
1441 $allowedModels = $config->get( 'PageTranslationAllowedContentModels' );
1442 $contentModel = $content->getModel();
1443 $allowed = (bool)( $allowedModels[$contentModel] ?? false );
1444
1445 // T163254: Disable page translation on non-text pages
1446 if ( $allowed && !$content instanceof TextContent ) {
1447 LoggerFactory::getInstance( LogNames::MAIN )->error(
1448 'Expected {title} to have content of type TextContent, got {contentType}. ' .
1449 '$wgPageTranslationAllowedContentModels is incorrectly configured with a non-text content model.',
1450 [
1451 'title' => (string)$page,
1452 'contentType' => get_class( $content )
1453 ]
1454 );
1455 return false;
1456 }
1457
1458 return $allowed;
1459 }
1460
1465 public static function replaceMovePage( &$list ) {
1466 $movePageSpec = $list['Movepage'] ?? null;
1467
1468 // This should never happen, but apparently is happening? See: T296568
1469 if ( $movePageSpec === null ) {
1470 return;
1471 }
1472
1473 $list['Movepage'] = [
1474 'class' => MoveTranslatableBundleSpecialPage::class,
1475 'services' => [
1476 'ObjectFactory',
1477 'PermissionManager',
1478 'Translate:TranslatableBundleMover',
1479 'Translate:TranslatableBundleFactory',
1480 'FormatterFactory'
1481 ],
1482 'args' => [
1483 $movePageSpec
1484 ]
1485 ];
1486 }
1487
1496 public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) {
1497 if ( $action === 'read' ) {
1498 return true;
1499 }
1500
1501 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
1502 $key = $cache->makeKey( 'pt-lock', sha1( $title->getPrefixedText() ) );
1503 if ( $cache->get( $key ) === 'locked' ) {
1504 $result = [ 'pt-locked-page' ];
1505
1506 return false;
1507 }
1508
1509 return true;
1510 }
1511
1519 public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) {
1520 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1521
1522 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1523 if ( !$isTranslationPage
1524 && !TranslatablePage::isSourcePage( $out->getTitle() )
1525 ) {
1526 return true;
1527 }
1528
1529 // Copied from Skin::subPageSubtitle()
1530 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1531 if (
1532 $out->isArticle() &&
1533 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1534 ) {
1535 $ptext = $out->getTitle()->getPrefixedText();
1536 $links = explode( '/', $ptext );
1537 if ( count( $links ) > 1 ) {
1538 array_pop( $links );
1539 if ( $isTranslationPage ) {
1540 // Also remove language code page
1541 array_pop( $links );
1542 }
1543 $c = 0;
1544 $growinglink = '';
1545 $display = '';
1546 $sitedir = $skin->getLanguage()->getDir();
1547
1548 foreach ( $links as $link ) {
1549 $growinglink .= $link;
1550 $display .= $link;
1551 $linkObj = Title::newFromText( $growinglink );
1552
1553 if ( $linkObj && $linkObj->isKnown() ) {
1554 $getlink = $linker->makeKnownLink(
1555 SpecialPage::getTitleFor( 'MyLanguage', $growinglink ),
1556 $display
1557 );
1558
1559 $c++;
1560
1561 if ( $c > 1 ) {
1562 $subpages .= $skin->msg( 'pipe-separator' )->escaped();
1563 } else {
1564 $subpages .= '&lt; ';
1565 }
1566
1567 $subpages .= Html::rawElement( 'bdi', [ 'dir' => $sitedir ], $getlink );
1568 $display = '';
1569 } else {
1570 $display .= '/';
1571 }
1572
1573 $growinglink .= '/';
1574 }
1575 }
1576
1577 return false;
1578 }
1579
1580 return true;
1581 }
1582
1590 public static function onSkinTemplateNavigation__Universal( Skin $skin, array &$tabs ) {
1591 $title = $skin->getTitle();
1592 $handle = new MessageHandle( $title );
1593 $code = $handle->getCode();
1594 $user = $skin->getUser();
1595
1596 if ( TranslatablePage::isSourcePage( $title ) ) {
1597 if ( $user->isAllowed( 'pagetranslation' ) ) {
1598 $tabs['actions']['marktranslation'] = [
1599 'text' => $skin->msg( 'translate-ca-marktranslation' )->text(),
1600 'href' => SpecialPage::getTitleFor( 'PageTranslation' )->getLocalURL( [
1601 'target' => $title->getPrefixedText(),
1602 'do' => 'mark',
1603 ] ),
1604 ];
1605 }
1606 return;
1607 }
1608
1609 $page = TranslatablePage::isTranslationPage( $title );
1610 // The source language has a subpage too, but cannot be translated
1611 if ( !$page || $page->getSourceLanguageCode() === $code ) {
1612 return;
1613 }
1614
1615 if ( isset( $tabs['views']['edit'] ) ) {
1616 // There is an edit tab, just replace its text and URL with ours, keeping the tooltip and access key
1617 $tabs['views']['edit']['text'] = $skin->msg( 'tpt-tab-translate' )->text();
1618 $tabs['views']['edit']['href'] = $page->getTranslationUrl( $code );
1619 } elseif ( $user->isAllowed( 'translate' ) ) {
1620 $mwInstance = MediaWikiServices::getInstance();
1621 $namespaceProtection = $mwInstance->getMainConfig()->get( MainConfigNames::NamespaceProtection );
1622 $permissionManager = $mwInstance->getPermissionManager();
1623 if (
1624 !$permissionManager->userHasAllRights(
1625 $user, ...(array)( $namespaceProtection[ NS_TRANSLATIONS ] ?? [] )
1626 )
1627 ) {
1628 return;
1629 }
1630
1631 $tab = [
1632 'text' => $skin->msg( 'tpt-tab-translate' )->text(),
1633 'href' => $page->getTranslationUrl( $code ),
1634 ];
1635
1636 // Get the position of the viewsource tab within the array (if any)
1637 $viewsourcePos = array_keys( array_keys( $tabs['views'] ), 'viewsource', true )[0] ?? null;
1638
1639 if ( $viewsourcePos !== null ) {
1640 // Remove the viewsource tab and insert the translate tab at its place. Showing the tooltip
1641 // of the viewsource tab for the translate tab would be confusing.
1642 array_splice( $tabs['views'], $viewsourcePos, 1, [ 'translate' => $tab ] );
1643 } else {
1644 // We have neither an edit tab nor a viewsource tab to replace with the translate tab,
1645 // put the translate tab at the end
1646 $tabs['views']['translate'] = $tab;
1647 }
1648 }
1649 }
1650
1663 public static function onMovePageTranslationUnits(
1664 LinkTarget $oldLinkTarget,
1665 LinkTarget $newLinkTarget,
1666 UserIdentity $userIdentity,
1667 int $oldid,
1668 int $newid,
1669 string $reason,
1670 RevisionRecord $revisionRecord
1671 ) {
1672 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1673 // MoveTranslatableBundleJob takes care of handling updates because it performs
1674 // a lot of moves at once. As a performance optimization, skip this hook if
1675 // we detect moves from that job. As there isn't a good way to pass information
1676 // to this hook what originated the move, we use some heuristics.
1677 if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1678 return;
1679 }
1680
1681 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1682 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1683 $groupLast = null;
1684 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1685 $handle = new MessageHandle( $title );
1686 // Documentation pages are never translation pages
1687 if ( !$handle->isValid() || $handle->isDoc() ) {
1688 continue;
1689 }
1690
1691 $group = $handle->getGroup();
1692 if ( !$group instanceof WikiPageMessageGroup ) {
1693 continue;
1694 }
1695
1696 $language = $handle->getCode();
1697
1698 // Ignore pages such as Translations:Page/unit without language code
1699 if ( $language === '' ) {
1700 continue;
1701 }
1702
1703 // Update the page only once if source and destination units
1704 // belong to the same page
1705 if ( $group !== $groupLast ) {
1706 $groupLast = $group;
1707 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1708 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1709 }
1710 }
1711 }
1712
1723 public static function onDeleteTranslationUnit(
1724 WikiPage $unit,
1725 User $user,
1726 $reason,
1727 $id,
1728 $content,
1729 $logEntry
1730 ) {
1731 $title = $unit->getTitle();
1732 $handle = new MessageHandle( $title );
1733 if ( !$handle->isValid() ) {
1734 return;
1735 }
1736
1737 $group = $handle->getGroup();
1738 if ( !$group instanceof WikiPageMessageGroup ) {
1739 return;
1740 }
1741
1742 $mwServices = MediaWikiServices::getInstance();
1743 // During deletions this may cause creation of a lot of duplicate jobs. It is expected that
1744 // job queue will deduplicate them to reduce the number of jobs actually run.
1745 $mwServices->getJobQueueGroup()->push(
1746 RebuildMessageGroupStatsJob::newRefreshGroupsJob( [ $group->getId() ] )
1747 );
1748
1749 // Logic to update translation pages, skipped if we are in a middle of a deletion
1750 if ( self::$isDeleteTranslatableBundleJobRunning ) {
1751 return;
1752 }
1753
1754 $target = $group->getTitle();
1755 $langCode = $handle->getCode();
1756 $fname = __METHOD__;
1757
1758 $dbw = $mwServices->getConnectionProvider()->getPrimaryDatabase();
1759 $callback = function () use (
1760 $dbw,
1761 $target,
1762 $handle,
1763 $langCode,
1764 $user,
1765 $reason,
1766 $fname
1767 ) {
1768 $translationPageTitle = $target->getSubpage( $langCode );
1769 // Do a more thorough check for the translation page in case the translation page is deleted in a
1770 // different transaction.
1771 if ( !$translationPageTitle || !$translationPageTitle->exists( IDBAccessObject::READ_LATEST ) ) {
1772 return;
1773 }
1774
1775 $dbw->startAtomic( $fname );
1776
1777 $page = TranslatablePage::newFromTitle( $target );
1778
1779 if ( !$handle->isDoc() ) {
1780 $unitTitle = $handle->getTitle();
1781 // Assume that $user and $reason for the first deletion is the same for all
1782 self::updateTranslationPage(
1783 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1784 );
1785 }
1786
1787 $dbw->endAtomic( $fname );
1788 };
1789
1790 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1791 }
1792
1797 public static function onReplaceTextFilterPageTitlesForEdit( array &$titles ): void {
1798 foreach ( $titles as $index => $title ) {
1799 $handle = new MessageHandle( $title );
1800 if ( Utilities::isTranslationPage( $handle ) ) {
1801 unset( $titles[ $index ] );
1802 }
1803 }
1804 }
1805
1810 public static function onReplaceTextFilterPageTitlesForRename( array &$titles ): void {
1811 foreach ( $titles as $index => $title ) {
1812 $handle = new MessageHandle( $title );
1813 if (
1814 TranslatablePage::isSourcePage( $title ) ||
1815 Utilities::isTranslationPage( $handle )
1816 ) {
1817 unset( $titles[ $index ] );
1818 }
1819 }
1820 }
1821
1822 public static function getSpecialManageMessageGroupSubscriptionsLink(
1823 Context $context,
1824 Config $config
1825 ): array {
1826 return [
1827 'pagelink' => SpecialPage::getTitleFor( 'ManageMessageGroupSubscriptions' )->getPrefixedText()
1828 ];
1829 }
1830
1835 public static function onLinksUpdateComplete( LinksUpdate $linksUpdate ) {
1836 $handle = new MessageHandle( $linksUpdate->getTitle() );
1837 if ( !Utilities::isTranslationPage( $handle ) ) {
1838 return;
1839 }
1840 $code = $handle->getCode();
1841 $categories = $linksUpdate->getParserOutput()->getCategoryNames();
1842 $editSummary = wfMessage(
1843 'translate-category-summary',
1844 $linksUpdate->getTitle()->getPrefixedText()
1845 )->inContentLanguage()->text();
1846 foreach ( $categories as $category ) {
1847 $categoryTitle = Title::makeTitle( NS_CATEGORY, $category );
1848 $categoryHandle = new MessageHandle( $categoryTitle );
1849 // Only create categories for the same language code to reduce
1850 // the potential for very deep recursion if a category is
1851 // a member of itself in a different language
1852 $categoryTranslationPage = TranslatablePage::isTranslationPage( $categoryTitle );
1853 if (
1854 $categoryTranslationPage
1855 && $categoryHandle->getCode() == $code
1856 && !$categoryTitle->exists()
1857 ) {
1858 self::updateTranslationPage(
1859 $categoryTranslationPage,
1860 $code,
1861 FuzzyBot::getUser(),
1862 EDIT_FORCE_BOT,
1863 $editSummary,
1864 RenderTranslationPageJob::ACTION_CATEGORIZATION
1865 );
1866 }
1867 }
1868 }
1869}
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(), $services->getContentLanguageCode() ->toString(), 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 { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} 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(), $services->get( 'Translate:HookRunner'),);}, '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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@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:795
static tpSyntaxCheck(RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus)
When attempting to save, last resort.
Definition Hooks.php:845
static onReplaceTextFilterPageTitlesForRename(array &$titles)
Removes translatable and translation pages from the list of titles to be renamed Hook: ReplaceTextFil...
Definition Hooks.php:1810
static bool $isDeleteTranslatableBundleJobRunning
State flag used by DeleteTranslatableBundleJob for performance optimizations.
Definition Hooks.php:73
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:1663
static replaceMovePage(&$list)
Hook: SpecialPage_initList.
Definition Hooks.php:1465
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:293
static onParserGetVariableValueSwitch(Parser $parser, array &$variableCache, string $magicWordId, ?string &$ret, PPFrame $frame)
Hook: ParserGetVariableValueSwitch.
Definition Hooks.php:456
static onGetUserPermissionsErrorsExpensive(Title $title, User $user, $action, &$result)
Prevent creation of orphan translation units in Translations namespace.
Definition Hooks.php:962
static preprocessTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforePreprocess.
Definition Hooks.php:169
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:1723
static preventDirectEditing(Title $title, User $user, $action, &$result)
Prevent editing of translation pages directly.
Definition Hooks.php:1152
static renderTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforeInternalParse.
Definition Hooks.php:86
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:367
static onSkinTemplateNavigation__Universal(Skin $skin, array &$tabs)
Converts the edit tab (if exists) for translation pages to translate tab, and adds a "mark for transl...
Definition Hooks.php:1590
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:201
static onBeforePageDisplay(OutputPage $out, Skin $skin)
Hook: BeforePageDisplay.
Definition Hooks.php:324
static translatablePageHeader( $article, &$outputDone, &$pcache)
Hook: ArticleViewHeader.
Definition Hooks.php:1261
static onPageContentLanguage(Title $title, &$pageLang)
Set the right page content language for translated pages ("Page/xx").
Definition Hooks.php:277
static lockedPagesCheck(Title $title, User $user, $action, &$result)
Hook: getUserPermissionsErrorsExpensive.
Definition Hooks.php:1496
static updateTranstagOnNullRevisions(RevisionRecord $rev)
Page moving and page protection (and possibly other things) creates null revisions.
Definition Hooks.php:922
static onVisualEditorBeforeEditor(OutputPage $out, Skin $skin)
Hook: onVisualEditorBeforeEditor.
Definition Hooks.php:353
static replaceSubtitle(&$subpages, ?Skin $skin, OutputPage $out)
Hook: SkinSubPageSubtitle.
Definition Hooks.php:1519
static formatLanguageLink(array &$link, Title $linkTitle, Title $pageTitle, OutputPage $out)
Hooks: SkinTemplateGetLanguageLink.
Definition Hooks.php:765
static onLinksUpdateComplete(LinksUpdate $linksUpdate)
Create any redlinked categories marked for translation Hook: LinksUpdateComplete.
Definition Hooks.php:1835
static onGetMagicVariableIDs(&$variableIDs)
Hook: GetMagicVariableIDs.
Definition Hooks.php:449
static languages( $data, $params, $parser)
Definition Hooks.php:478
static addLanguageLinks(Title $title, array &$languageLinks)
Hooks: LanguageLinks.
Definition Hooks.php:685
static disableDelete( $article, $out, &$reason)
Redirects the delete action to our own for translatable pages.
Definition Hooks.php:1239
static onReplaceTextFilterPageTitlesForEdit(array &$titles)
Removes translation pages from the list of page titles to be edited Hook: ReplaceTextFilterPageTitles...
Definition Hooks.php:1797
static preventMoves(Title $oldTitle, Title $newTitle, StatusValue $status)
Prevent moving translatable or translation pages by any means other than our own move tools.
Definition Hooks.php:1214
static addTranstagAfterSave(WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revisionRecord, EditResult $editResult)
Hook: PageSaveComplete.
Definition Hooks.php:877
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:60
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:29
Wraps the translatable page sections into a message group.