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