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