Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
Hooks.php
1<?php
2
3namespace MediaWiki\Extension\Translate\PageTranslation;
4
5use Article;
6use CommentStoreComment;
7use Content;
8use DeferredUpdates;
9use Exception;
10use Html;
11use IContextSource;
12use Language;
13use LanguageCode;
14use LinkBatch;
15use ManualLogEntry;
18use MediaWiki\Linker\LinkTarget;
19use MediaWiki\Logger\LoggerFactory;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Page\PageIdentity;
22use MediaWiki\Revision\MutableRevisionRecord;
23use MediaWiki\Revision\RenderedRevision;
24use MediaWiki\Revision\RevisionRecord;
25use MediaWiki\Revision\SlotRecord;
26use MediaWiki\Storage\EditResult;
27use MediaWiki\User\UserIdentity;
30use ObjectCache;
31use OutputPage;
32use Parser;
33use ParserOutput;
34use RequestContext;
35use Skin;
36use SpecialPage;
37use Status;
38use StubUserLang;
39use TextContent;
40use Title;
43use User;
44use Wikimedia\ScopedCallback;
45use WikiPage;
47use WikitextContent;
48
55class Hooks {
56 // Uuugly hacks
57 public static $allowTargetEdit = false;
58 // Check if job queue is running
59 public static $jobQueueRunning = false;
60 // Check if we are just rendering tags or such
61 public static $renderingContext = false;
62 // Used to communicate data between LanguageLinks and SkinTemplateGetLanguageLink hooks.
63 private static $languageLinkData = [];
64
72 public static function renderTagPage( $wikitextParser, &$text, $state ): void {
73 if ( $text === null ) {
74 // SMW is unhelpfully sending null text if source contains section tags. Do not explode.
75 return;
76 }
77
78 self::preprocessTagPage( $wikitextParser, $text, $state );
79
80 // Skip further interface message parsing
81 if ( $wikitextParser->getOptions()->getInterfaceMessage() ) {
82 return;
83 }
84
85 // For section previews, perform additional clean-up, given tags are often
86 // unbalanced when we preview one section only.
87 if ( $wikitextParser->getOptions()->getIsSectionPreview() ) {
88 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
89 $text = $translatablePageParser->cleanupTags( $text );
90 }
91
92 // Set display title
93 $title = MediaWikiServices::getInstance()
94 ->getTitleFactory()
95 ->castFromPageReference( $wikitextParser->getPage() );
96
97 if ( !$title ) {
98 return;
99 }
100
101 $page = TranslatablePage::isTranslationPage( $title );
102 if ( !$page ) {
103 return;
104 }
105
106 try {
107 self::$renderingContext = true;
108 [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
109 $name = $page->getPageDisplayTitle( $code );
110 if ( $name ) {
111 $name = $wikitextParser->recursivePreprocess( $name );
112
113 $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
114 ->getLanguageConverter( $wikitextParser->getTargetLanguage() );
115 $name = $langConv->convert( $name );
116 $wikitextParser->getOutput()->setDisplayTitle( $name );
117 }
118 self::$renderingContext = false;
119 } catch ( Exception $e ) {
120 LoggerFactory::getInstance( 'Translate' )->error(
121 'T302754 Failed to set display title for page {title}',
122 [
123 'title' => $title->getPrefixedDBkey(),
124 'text' => $text,
125 'pageid' => $title->getId(),
126 ]
127 );
128
129 // Re-throw to preserve behavior
130 throw $e;
131 }
132
133 $extensionData = [
134 'languagecode' => $code,
135 'messagegroupid' => $page->getMessageGroupId(),
136 'sourcepagetitle' => [
137 'namespace' => $page->getTitle()->getNamespace(),
138 'dbkey' => $page->getTitle()->getDBkey()
139 ]
140 ];
141
142 $wikitextParser->getOutput()->setExtensionData(
143 'translate-translation-page', $extensionData
144 );
145
146 // Disable edit section links
147 $wikitextParser->getOutput()->setExtensionData( 'Translate-noeditsection', true );
148 }
149
157 public static function preprocessTagPage( $wikitextParser, &$text, $state ): void {
158 $translatablePageParser = Services::getInstance()->getTranslatablePageParser();
159
160 if ( $translatablePageParser->containsMarkup( $text ) ) {
161 try {
162 $parserOutput = $translatablePageParser->parse( $text );
163 // If parsing succeeds, replace text and add styles
164 $text = $parserOutput->sourcePageTextForRendering(
165 $wikitextParser->getTargetLanguage()
166 );
167 $wikitextParser->getOutput()->addModuleStyles( [
168 'ext.translate',
169 ] );
170 } catch ( ParsingFailure $e ) {
171 wfDebug( 'ParsingFailure caught; expected' );
172 }
173 } else {
174 // If the text doesn't contain <translate> markup, it can still contain <tvar> in the
175 // context of a Parsoid template expansion sub-pipeline. We strip these as well.
176 $unit = new TranslationUnit( $text );
177 $text = $unit->getTextForTrans();
178 }
179 }
180
187 public static function onParserOutputPostCacheTransform(
188 ParserOutput $out,
189 &$text,
190 array &$options
191 ) {
192 if ( $out->getExtensionData( 'Translate-noeditsection' ) ) {
193 $options['enableSectionEditLinks'] = false;
194 }
195 }
196
208 public static function fetchTranslatableTemplateAndTitle(
209 ?LinkTarget $contextLink,
210 ?LinkTarget $templateLink,
211 bool &$skip,
212 ?RevisionRecord &$revRecord
213 ): void {
214 if ( !$templateLink ) {
215 return;
216 }
217
218 $templateTitle = Title::castFromLinkTarget( $templateLink );
219
220 $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle );
221 if ( $templateTranslationPage ) {
222 // Template is referring to a translation page, fetch it and incase it doesn't
223 // exist, fetch the source fallback
224 $revRecord = $templateTranslationPage->getRevisionRecordWithFallback();
225 return;
226 }
227
228 if ( !TranslatablePage::isSourcePage( $templateTitle ) ) {
229 return;
230 }
231
232 $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle );
233
234 if ( !( $translatableTemplatePage->supportsTransclusion() ?? false ) ) {
235 // Page being transcluded does not support language aware transclusion
236 return;
237 }
238
239 $store = MediaWikiServices::getInstance()->getRevisionStore();
240
241 if ( $contextLink ) {
242 // Fetch the context page language, and then check if template is present in that language
243 $templateTranslationTitle = $templateTitle->getSubpage(
244 Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode()
245 );
246
247 if ( $templateTranslationTitle ) {
248 if ( $templateTranslationTitle->exists() ) {
249 // Template is present in the context page language, fetch the revision record and return
250 $revRecord = $store->getRevisionByTitle( $templateTranslationTitle );
251 } else {
252 // In case the template has not been translated to the context page language,
253 // we assign a MutableRevisionRecord in order to add a dependency, so that when
254 // it is created, the newly created page is loaded rather than the fallback
255 $revRecord = new MutableRevisionRecord( $templateTranslationTitle );
256 }
257 return;
258 }
259 }
260
261 // Context page information not available OR the template translation title could not be determined.
262 // Fetch and return the RevisionRecord of the template in the source language
263 $sourceTemplateTitle = $templateTitle->getSubpage(
264 $translatableTemplatePage->getMessageGroup()->getSourceLanguage()
265 );
266 if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) {
267 $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle );
268 }
269 }
270
277 public static function onPageContentLanguage( Title $title, &$pageLang ) {
278 // For translation pages, parse plural, grammar etc with correct language,
279 // and set the right direction
280 if ( TranslatablePage::isTranslationPage( $title ) ) {
281 [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
282 $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code );
283 }
284 }
285
293 public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) {
294 if ( TranslatablePage::isSourcePage( $title ) ) {
295 $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage();
296 if ( !$msg->isDisabled() ) {
297 $notices['translate-tag'] = $msg->parseAsBlock();
298 }
299
300 $notices[] = Html::warningBox(
301 wfMessage( 'tps-edit-sourcepage-text' )->parse(),
302 'translate-edit-documentation'
303 );
304 // TECHNICALLY this would only be necessary for 1.38+, but the way the message is
305 // written is also true for 1.37-; therefore it's reasonable to not add another version
306 // check here.
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
327 public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
328 global $wgTranslatePageTranslationULS;
329
330 $title = $out->getTitle();
331 $isSource = TranslatablePage::isSourcePage( $title );
332 $isTranslation = TranslatablePage::isTranslationPage( $title );
333
334 if ( $isSource || $isTranslation ) {
335 if ( $wgTranslatePageTranslationULS ) {
336 $out->addModules( 'ext.translate.pagetranslation.uls' );
337 }
338
339 if ( $isSource ) {
340 // Adding a help notice
341 $out->addModuleStyles( 'ext.translate.edit.documentation.styles' );
342 $out->addModules( 'ext.translate.edit.documentation' );
343 }
344
345 if ( $isTranslation ) {
346 // Source pages get this module via <translate>, but for translation
347 // pages we need to add it manually.
348 $out->addModuleStyles( 'ext.translate' );
349 $out->addJsConfigVars( 'wgTranslatePageTranslation', 'translation' );
350 } else {
351 $out->addJsConfigVars( 'wgTranslatePageTranslation', 'source' );
352 }
353 }
354
355 return true;
356 }
357
364 public static function onVisualEditorBeforeEditor( OutputPage $out, Skin $skin ) {
365 return !TranslatablePage::isTranslationPage( $out->getTitle() );
366 }
367
379 public static function onSectionSave(
380 WikiPage $wikiPage,
381 User $user,
382 TextContent $content,
383 $summary,
384 $minor,
385 $flags,
386 MessageHandle $handle
387 ) {
388 // FuzzyBot may do some duplicate work already worked on by other jobs
389 if ( $user->equals( FuzzyBot::getUser() ) ) {
390 return true;
391 }
392
393 $group = $handle->getGroup();
394 if ( !$group instanceof WikiPageMessageGroup ) {
395 return true;
396 }
397
398 // Finally we know the title and can construct a Translatable page
399 $page = TranslatablePage::newFromTitle( $group->getTitle() );
400
401 // Update the target translation page
402 if ( !$handle->isDoc() ) {
403 $code = $handle->getCode();
404 DeferredUpdates::addCallableUpdate(
405 function () use ( $page, $code, $user, $flags, $summary, $handle ) {
406 $unitTitle = $handle->getTitle();
407 self::updateTranslationPage( $page, $code, $user, $flags, $summary, null, $unitTitle );
408 }
409 );
410 }
411
412 return true;
413 }
414
415 private static function updateTranslationPage(
416 TranslatablePage $page,
417 string $code,
418 User $user,
419 int $flags,
420 string $summary,
421 ?string $triggerAction = null,
422 ?Title $unitTitle = null
423 ): void {
424 $source = $page->getTitle();
425 $target = $source->getSubpage( $code );
426 $mwInstance = MediaWikiServices::getInstance();
427
428 // We don't know and don't care
429 $flags &= ~EDIT_NEW & ~EDIT_UPDATE;
430
431 // Update the target page
432 $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() : null;
433 $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText );
434 $job->setUser( $user );
435 $job->setSummary( $summary );
436 $job->setFlags( $flags );
437 $mwInstance->getJobQueueGroup()->push( $job );
438
439 // Invalidate caches so that language bar is up-to-date
440 $pages = $page->getTranslationPages();
441 $wikiPageFactory = $mwInstance->getWikiPageFactory();
442 foreach ( $pages as $title ) {
443 if ( $title->equals( $target ) ) {
444 // Handled by the RenderTranslationPageJob
445 continue;
446 }
447
448 $wikiPage = $wikiPageFactory->newFromTitle( $title );
449 $wikiPage->doPurge();
450 }
451 $sourceWikiPage = $wikiPageFactory->newFromTitle( $source );
452 $sourceWikiPage->doPurge();
453 }
454
461 public static function languages( $data, $params, $parser ) {
462 global $wgPageTranslationLanguageList;
463
464 if ( $wgPageTranslationLanguageList === 'sidebar-only' ) {
465 return '';
466 }
467
468 self::$renderingContext = true;
469 $context = new ScopedCallback( static function () {
470 self::$renderingContext = false;
471 } );
472
473 // Add a dummy language link that is removed in self::addLanguageLinks.
474 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) {
475 $parser->getOutput()->addLanguageLink( 'x-pagetranslation-tag' );
476 }
477
478 $currentTitle = $parser->getTitle();
479 $pageStatus = self::getTranslatablePageStatus( $currentTitle );
480 if ( !$pageStatus ) {
481 return '';
482 }
483
484 $page = $pageStatus[ 'page' ];
485 $status = $pageStatus[ 'languages' ];
486 $pageTitle = $page->getTitle();
487
488 // Sort by language code, which seems to be the only sane method
489 ksort( $status );
490
491 // This way the parser knows to fragment the parser cache by language code
492 $userLang = $parser->getOptions()->getUserLangObj();
493 $userLangCode = $userLang->getCode();
494 // Should call $page->getMessageGroup()->getSourceLanguage(), but
495 // group is sometimes null on WMF during page moves, reason unknown.
496 // This should do the same thing for now.
497 $sourceLanguage = $pageTitle->getPageLanguage()->getCode();
498
499 $languages = [];
500 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
501 foreach ( $status as $code => $percent ) {
502 // Get autonyms (null)
503 $name = TranslateUtils::getLanguageName( $code, null );
504
505 // Add links to other languages
506 $suffix = ( $code === $sourceLanguage ) ? '' : "/$code";
507 $targetTitleString = $pageTitle->getDBkey() . $suffix;
508 $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString );
509
510 $classes = [];
511 if ( $code === $userLangCode ) {
512 $classes[] = 'mw-pt-languages-ui';
513 }
514
515 $linker = $parser->getLinkRenderer();
516 $lang = $langFactory->getLanguage( $code );
517 if ( $currentTitle->equals( $subpage ) ) {
518 $classes[] = 'mw-pt-languages-selected';
519 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
520 $attribs = [
521 'class' => $classes,
522 'lang' => $lang->getHtmlCode(),
523 'dir' => $lang->getDir(),
524 ];
525
526 $contents = Html::Element( 'span', $attribs, $name );
527 } elseif ( $subpage->isKnown() ) {
528 $pagename = $page->getPageDisplayTitle( $code );
529 if ( !is_string( $pagename ) ) {
530 $pagename = $subpage->getPrefixedText();
531 }
532
533 $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) );
534
535 $title = wfMessage( 'tpt-languages-nonzero' )
536 ->inLanguage( $userLang )
537 ->params( $pagename )
538 ->numParams( 100 * $percent )
539 ->text();
540 $attribs = [
541 'title' => $title,
542 'class' => $classes,
543 'lang' => $lang->getHtmlCode(),
544 'dir' => $lang->getDir(),
545 ];
546
547 $contents = $linker->makeKnownLink( $subpage, $name, $attribs );
548 } else {
549 /* When language is included because it is a priority language,
550 * but translation does not yet exists, link directly to the
551 * translation view. */
552 $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' );
553 $params = [
554 'group' => $page->getMessageGroupId(),
555 'language' => $code,
556 'task' => 'view'
557 ];
558
559 $classes[] = 'new'; // For red link color
560
561 $attribs = [
562 'title' => wfMessage( 'tpt-languages-zero' )->inLanguage( $userLang )->text(),
563 'class' => $classes,
564 'lang' => $lang->getHtmlCode(),
565 'dir' => $lang->getDir(),
566 ];
567 $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params );
568 }
569 $languages[ $name ] = Html::rawElement( 'li', [], $contents );
570 }
571
572 // Sort languages by autonym
573 ksort( $languages );
574 $languages = array_values( $languages );
575 $languages = implode( "\n", $languages );
576
577 $out = Html::openElement( 'div', [
578 'class' => 'mw-pt-languages noprint',
579 'lang' => $userLang->getHtmlCode(),
580 'dir' => $userLang->getDir()
581 ] );
582 $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ],
583 wfMessage( 'tpt-languages-legend' )->inLanguage( $userLang )->escaped()
584 );
585 $out .= Html::rawElement(
586 'ul',
587 [ 'class' => 'mw-pt-languages-list' ],
588 $languages
589 );
590 $out .= Html::closeElement( 'div' );
591
592 $parser->getOutput()->addModuleStyles( [
593 'ext.translate.tag.languages',
594 ] );
595
596 return $out;
597 }
598
605 private static function tpProgressIcon( float $percent ) {
606 $classes = [ 'mw-pt-progress' ];
607 $percent *= 100;
608 if ( $percent < 20 ) {
609 $classes[] = 'mw-pt-progress--stub';
610 } elseif ( $percent < 40 ) {
611 $classes[] = 'mw-pt-progress--low';
612 } elseif ( $percent < 60 ) {
613 $classes[] = 'mw-pt-progress--med';
614 } elseif ( $percent < 80 ) {
615 $classes[] = 'mw-pt-progress--high';
616 } else {
617 $classes[] = 'mw-pt-progress--complete';
618 }
619 return $classes;
620 }
621
627 private static function getTranslatablePageStatus( Title $title ) {
628 // Check if this is a source page or a translation page
629 $page = TranslatablePage::newFromTitle( $title );
630 if ( $page->getMarkedTag() === null ) {
631 $page = TranslatablePage::isTranslationPage( $title );
632 }
633
634 if ( $page === false || $page->getMarkedTag() === null ) {
635 return null;
636 }
637
638 $status = $page->getTranslationPercentages();
639 if ( !$status ) {
640 return null;
641 }
642
643 // If priority languages have been set always show those languages
644 $priorityLangs = TranslateMetadata::get( $page->getMessageGroupId(), 'prioritylangs' );
645 $priorityForce = TranslateMetadata::get( $page->getMessageGroupId(), 'priorityforce' );
646 $filter = null;
647 if ( (string)$priorityLangs !== '' ) {
648 $filter = array_flip( explode( ',', $priorityLangs ) );
649 }
650 if ( $filter !== null ) {
651 // If translation is restricted to some languages, only show them
652 if ( $priorityForce === 'on' ) {
653 // Do not filter the source language link
654 $filter[$page->getMessageGroup()->getSourceLanguage()] = true;
655 $status = array_intersect_key( $status, $filter );
656 }
657 foreach ( $filter as $langCode => $value ) {
658 if ( !isset( $status[$langCode] ) ) {
659 // We need to show all priority languages even if no translation started
660 $status[$langCode] = 0;
661 }
662 }
663 }
664
665 return [
666 'page' => $page,
667 'languages' => $status
668 ];
669 }
670
676 public static function addLanguageLinks( Title $title, array &$languageLinks ) {
677 global $wgPageTranslationLanguageList;
678
679 $hasLanguagesTag = false;
680 foreach ( $languageLinks as $index => $name ) {
681 if ( $name === 'x-pagetranslation-tag' ) {
682 $hasLanguagesTag = true;
683 unset( $languageLinks[ $index ] );
684 }
685 }
686
687 if ( $wgPageTranslationLanguageList === 'tag-only' ) {
688 return;
689 }
690
691 if ( $wgPageTranslationLanguageList === 'sidebar-fallback' && $hasLanguagesTag ) {
692 return;
693 }
694
695 // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only'
696
697 $status = self::getTranslatablePageStatus( $title );
698 if ( !$status ) {
699 return;
700 }
701
702 self::$renderingContext = true;
703 $context = new ScopedCallback( static function () {
704 self::$renderingContext = false;
705 } );
706
707 $page = $status[ 'page' ];
708 $languages = $status[ 'languages' ];
709 $mwServices = MediaWikiServices::getInstance();
710 $en = $mwServices->getLanguageFactory()->getLanguage( 'en' );
711
712 $newLanguageLinks = [];
713
714 // Batch the Title::exists queries used below
715 $lb = new LinkBatch();
716 foreach ( array_keys( $languages ) as $code ) {
717 $title = $page->getTitle()->getSubpage( $code );
718 $lb->addObj( $title );
719 }
720 $lb->execute();
721 $languageNameUtils = $mwServices->getLanguageNameUtils();
722 foreach ( $languages as $code => $percentage ) {
723 $title = $page->getTitle()->getSubpage( $code );
724 $key = "x-pagetranslation:{$title->getPrefixedText()}";
725 $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText();
726
727 if ( $title->exists() ) {
728 $href = $title->getLocalURL();
729 $classes = self::tpProgressIcon( (float)$percentage );
730 $title = wfMessage( 'tpt-languages-nonzero' )
731 ->params( $translatedName )
732 ->numParams( 100 * $percentage );
733 } else {
734 $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [
735 'group' => $page->getMessageGroupId(),
736 'language' => $code,
737 ] );
738 $classes = [ 'mw-pt-progress--none' ];
739 $title = wfMessage( 'tpt-languages-zero' );
740 }
741
742 self::$languageLinkData[ $key ] = [
743 'href' => $href,
744 'language' => $code,
745 'percentage' => $percentage,
746 'classes' => $classes,
747 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ),
748 'title' => $title,
749 ];
750
751 $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][ 'autonym' ];
752 }
753
754 asort( $newLanguageLinks );
755 $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks );
756 }
757
765 public static function formatLanguageLink(
766 array &$link,
767 Title $linkTitle,
768 Title $pageTitle,
769 OutputPage $out
770 ) {
771 if ( substr( $link[ 'text' ], 0, 18 ) !== 'x-pagetranslation:' ) {
772 return;
773 }
774
775 if ( !isset( self::$languageLinkData[ $link[ 'text' ] ] ) ) {
776 return;
777 }
778
779 $data = self::$languageLinkData[ $link[ 'text' ] ];
780
781 $link[ 'class' ] .= ' ' . implode( ' ', $data[ 'classes' ] );
782 $link[ 'href' ] = $data[ 'href' ];
783 $link[ 'text' ] = $data[ 'autonym' ];
784 $link[ 'title' ] = $data[ 'title' ]->inLanguage( $out->getLanguage()->getCode() )->text();
785 $link[ 'lang'] = LanguageCode::bcp47( $data[ 'language' ] );
786 $link[ 'hreflang'] = LanguageCode::bcp47( $data[ 'language' ] );
787
788 $out->addModuleStyles( 'ext.translate.tag.languages' );
789 }
790
800 public static function tpSyntaxCheckForEditContent(
801 $context,
802 $content,
803 $status,
804 $summary
805 ) {
806 $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content );
807
808 if ( $syntaxErrorStatus ) {
809 $status->merge( $syntaxErrorStatus );
810 return $syntaxErrorStatus->isGood();
811 }
812
813 return true;
814 }
815
816 private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status {
817 // T163254: Ignore translation markup on non-wikitext pages
818 if ( !$content instanceof WikitextContent || !$page ) {
819 return null;
820 }
821
822 $text = $content->getText();
823
824 // See T154500
825 $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
826 $status = Status::newGood();
827 $parser = Services::getInstance()->getTranslatablePageParser();
828 if ( $parser->containsMarkup( $text ) ) {
829 try {
830 $parser->parse( $text );
831 } catch ( ParsingFailure $e ) {
832 $status->fatal( ...( $e->getMessageSpecification() ) );
833 }
834 }
835
836 return $status;
837 }
838
850 public static function tpSyntaxCheck(
851 RenderedRevision $renderedRevision,
852 UserIdentity $user,
853 CommentStoreComment $summary,
854 $flags,
855 Status $hookStatus
856 ) {
857 $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN );
858
859 $status = self::tpSyntaxError(
860 $renderedRevision->getRevision()->getPage(),
861 $content
862 );
863
864 if ( $status ) {
865 $hookStatus->merge( $status );
866 return $status->isGood();
867 }
868
869 return true;
870 }
871
883 public static function addTranstagAfterSave(
884 WikiPage $wikiPage,
885 UserIdentity $userIdentity,
886 string $summary,
887 int $flags,
888 RevisionRecord $revisionRecord,
889 EditResult $editResult
890 ) {
891 $content = $wikiPage->getContent();
892
893 // T163254: Disable page translation on non-wikitext pages
894 if ( $content instanceof WikitextContent ) {
895 $text = $content->getText();
896 } else {
897 // Not applicable
898 return true;
899 }
900
901 $parser = Services::getInstance()->getTranslatablePageParser();
902 if ( $parser->containsMarkup( $text ) ) {
903 // Add the ready tag
904 $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() );
905 $page->addReadyTag( $revisionRecord->getId() );
906 }
907
908 return true;
909 }
910
926 public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) {
927 $parentId = $rev->getParentId();
928 if ( $parentId === 0 || $parentId === null ) {
929 // No parent, bail out.
930 return;
931 }
932
933 $prevRev = MediaWikiServices::getInstance()
934 ->getRevisionLookup()
935 ->getRevisionById( $parentId );
936
937 if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) {
938 // Not a null revision, bail out.
939 return;
940 }
941
942 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
943 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
944 $bundle = $bundleFactory->getBundle( $title );
945
946 if ( $bundle ) {
947 $bundleStore = $bundleFactory->getStore( $bundle );
948 $bundleStore->handleNullRevisionInsert( $bundle, $rev );
949 }
950 }
951
963 Title $title,
964 User $user,
965 $action,
966 &$result
967 ) {
968 $handle = new MessageHandle( $title );
969
970 // Check only when someone tries to create translation units.
971 // Allow editing units that become orphaned in regular use, so that
972 // people can delete them or fix links or other issues in them.
973 if ( $action !== 'create' || !$handle->isPageTranslation() ) {
974 return true;
975 }
976
977 $isValid = true;
978 $groupId = null;
979
980 if ( $handle->isValid() ) {
981 $groupId = $handle->getGroup()->getId();
982 } else {
983 // Sometimes the message index can be out of date. Either the rebuild job failed or
984 // it just hasn't finished yet. Do a secondary check to make sure we are not
985 // inconveniencing translators for no good reason.
986 // See https://phabricator.wikimedia.org/T221119
987 MediaWikiServices::getInstance()->getStatsdDataFactory()
988 ->increment( 'translate.slow_translatable_page_check' );
989 $translatablePage = self::checkTranslatablePageSlow( $title );
990 if ( $translatablePage ) {
991 $groupId = $translatablePage->getMessageGroupId();
992 } else {
993 $isValid = false;
994 }
995 }
996
997 if ( $isValid ) {
998 $error = self::getTranslationRestrictions( $handle, $groupId );
999 $result = $error ?: $result;
1000 return $error === [];
1001 }
1002
1003 // Don't allow editing invalid messages that do not belong to any translatable page
1004 LoggerFactory::getInstance( 'Translate' )->info(
1005 'Unknown translation page: {title}',
1006 [ 'title' => $title->getPrefixedDBkey() ]
1007 );
1008 $result = [ 'tpt-unknown-page' ];
1009 return false;
1010 }
1011
1012 private static function checkTranslatablePageSlow( LinkTarget $unit ): ?TranslatablePage {
1013 $parts = TranslatablePage::parseTranslationUnit( $unit );
1014 $translationPageTitle = Title::newFromText(
1015 $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ]
1016 );
1017 if ( !$translationPageTitle ) {
1018 return null;
1019 }
1020
1021 $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle );
1022 if ( !$translatablePage ) {
1023 return null;
1024 }
1025
1026 $factory = Services::getInstance()->getTranslationUnitStoreFactory();
1027 $store = $factory->getReader( $translatablePage->getTitle() );
1028 $units = $store->getNames();
1029
1030 if ( !in_array( $parts[ 'section' ], $units ) ) {
1031 return null;
1032 }
1033
1034 return $translatablePage;
1035 }
1036
1044 private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) {
1045 global $wgTranslateDocumentationLanguageCode;
1046
1047 // Allow adding message documentation even when translation is restricted
1048 if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) {
1049 return [];
1050 }
1051
1052 // Check if anything is prevented for the group in the first place
1053 $force = TranslateMetadata::get( $groupId, 'priorityforce' );
1054 if ( $force !== 'on' ) {
1055 return [];
1056 }
1057
1058 // And finally check whether the language is in the inclusion list
1059 $languages = TranslateMetadata::get( $groupId, 'prioritylangs' );
1060 $filter = array_flip( explode( ',', $languages ) );
1061 if ( !isset( $filter[$handle->getCode()] ) ) {
1062 $reason = TranslateMetadata::get( $groupId, 'priorityreason' );
1063 if ( $reason ) {
1064 return [ 'tpt-translation-restricted', $reason ];
1065 }
1066
1067 return [ 'tpt-translation-restricted-no-reason' ];
1068 }
1069
1070 return [];
1071 }
1072
1082 public static function preventDirectEditing( Title $title, User $user, $action, &$result ) {
1083 if ( self::$allowTargetEdit ) {
1084 return true;
1085 }
1086
1087 $inclusionList = [
1088 'read', 'delete', 'undelete', 'deletedtext', 'deletedhistory',
1089 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884
1090 'review', // FlaggedRevs
1091 'patrol', // T151172
1092 ];
1093 if ( in_array( $action, $inclusionList ) ) {
1094 return true;
1095 }
1096
1097 $page = TranslatablePage::isTranslationPage( $title );
1098 if ( $page !== false && $page->getMarkedTag() ) {
1099 [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
1100 $mwService = MediaWikiServices::getInstance();
1101
1102 if ( method_exists( $mwService, 'getUrlUtils' ) ) {
1103 $translationUrl = $mwService->getUrlUtils()->expand(
1104 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1105 );
1106 } else {
1107 // < MW 1.39
1108 $translationUrl = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE );
1109 }
1110
1111 $result = [
1112 'tpt-target-page',
1113 ':' . $page->getTitle()->getPrefixedText(),
1114 // This url shouldn't get cached
1115 $translationUrl
1116 ];
1117
1118 return false;
1119 }
1120
1121 return true;
1122 }
1123
1134 public static function disableDelete( $article, $out, &$reason ) {
1135 $title = $article->getTitle();
1136 $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title );
1137 $isDeletableBundle = $bundle && $bundle->isDeletable();
1138 if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) {
1139 $new = SpecialPage::getTitleFor(
1140 'PageTranslationDeletePage',
1141 $title->getPrefixedText()
1142 );
1143 $out->redirect( $new->getFullURL() );
1144 }
1145
1146 return true;
1147 }
1148
1157 public static function translatablePageHeader( $article, &$outputDone, &$pcache ) {
1158 if ( $article->getOldID() ) {
1159 return true;
1160 }
1161
1162 $transPage = TranslatablePage::isTranslationPage( $article->getTitle() );
1163 $context = $article->getContext();
1164 if ( $transPage ) {
1165 self::translationPageHeader( $context, $transPage );
1166 } else {
1167 // Check for pages that are tagged or marked
1168 self::sourcePageHeader( $context );
1169 }
1170
1171 return true;
1172 }
1173
1174 private static function sourcePageHeader( IContextSource $context ) {
1175 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1176
1177 $language = $context->getLanguage();
1178 $title = $context->getTitle();
1179
1180 $page = TranslatablePage::newFromTitle( $title );
1181
1182 $marked = $page->getMarkedTag();
1183 $ready = $page->getReadyTag();
1184 $latest = $title->getLatestRevID();
1185
1186 $actions = [];
1187 if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) {
1188 $actions[] = self::getTranslateLink( $context, $page, null );
1189 }
1190
1191 $hasChanges = $ready === $latest && $marked !== $latest;
1192 if ( $hasChanges ) {
1193 $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] );
1194
1195 if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) {
1196 $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' );
1197 $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ];
1198
1199 if ( $marked === null ) {
1200 // This page has never been marked
1201 $linkDesc = $context->msg( 'translate-tag-markthis' )->text();
1202 $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params );
1203 } else {
1204 $markUrl = $pageTranslation->getFullURL( $params );
1205 $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl )
1206 ->parse();
1207 }
1208 } else {
1209 $actions[] = $context->msg( 'translate-tag-hasnew', $diffUrl )->parse();
1210 }
1211 }
1212
1213 if ( !count( $actions ) ) {
1214 return;
1215 }
1216
1217 $header = Html::rawElement(
1218 'div',
1219 [
1220 'class' => 'mw-pt-translate-header noprint nomobile',
1221 'dir' => $language->getDir(),
1222 'lang' => $language->getHtmlCode(),
1223 ],
1224 $language->semicolonList( $actions )
1225 );
1226
1227 $context->getOutput()->addHTML( $header );
1228 }
1229
1230 private static function getTranslateLink(
1231 IContextSource $context,
1232 TranslatablePage $page,
1233 ?string $langCode
1234 ): string {
1235 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1236
1237 return $linker->makeKnownLink(
1238 SpecialPage::getTitleFor( 'Translate' ),
1239 $context->msg( 'translate-tag-translate-link-desc' )->text(),
1240 [],
1241 [
1242 'group' => $page->getMessageGroupId(),
1243 'language' => $langCode,
1244 'action' => 'page',
1245 'filter' => '',
1246 ]
1247 );
1248 }
1249
1250 private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) {
1251 global $wgTranslateKeepOutdatedTranslations;
1252
1253 $title = $context->getTitle();
1254 if ( !$title->exists() ) {
1255 return;
1256 }
1257
1258 [ , $code ] = TranslateUtils::figureMessage( $title->getText() );
1259
1260 // Get the translation percentage
1261 $pers = $page->getTranslationPercentages();
1262 $per = 0;
1263 if ( isset( $pers[$code] ) ) {
1264 $per = $pers[$code] * 100;
1265 }
1266
1267 $language = $context->getLanguage();
1268 $output = $context->getOutput();
1269
1270 if ( $page->getSourceLanguageCode() === $code ) {
1271 // If we are on the source language page, link to translate for user's language
1272 $msg = self::getTranslateLink( $context, $page, $language->getCode() );
1273 } else {
1274 $mwService = MediaWikiServices::getInstance();
1275
1276 if ( method_exists( $mwService, 'getUrlUtils' ) ) {
1277 $translationUrl = $mwService->getUrlUtils()->expand(
1278 $page->getTranslationUrl( $code ), PROTO_RELATIVE
1279 );
1280 } else {
1281 // < MW 1.39
1282 $translationUrl = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE );
1283 }
1284
1285 $msg = $context->msg( 'tpt-translation-intro',
1286 $translationUrl,
1287 ':' . $page->getTitle()->getPrefixedText(),
1288 $language->formatNum( $per )
1289 )->parse();
1290 }
1291
1292 $header = Html::rawElement(
1293 'div',
1294 [
1295 'class' => 'mw-pt-translate-header noprint',
1296 'dir' => $language->getDir(),
1297 'lang' => $language->getHtmlCode(),
1298 ],
1299 $msg
1300 );
1301
1302 $output->addHTML( $header );
1303
1304 if ( $wgTranslateKeepOutdatedTranslations ) {
1305 $groupId = $page->getMessageGroupId();
1306 // This is already calculated and cached by above call to getTranslationPercentages
1307 $stats = MessageGroupStats::forItem( $groupId, $code );
1308 if ( $stats[MessageGroupStats::FUZZY] ) {
1309 // Only show if there is fuzzy messages
1310 $wrap = Html::rawElement(
1311 'div',
1312 [
1313 'class' => 'mw-pt-translate-header',
1314 'dir' => $language->getDir(),
1315 'lang' => $language->getHtmlCode()
1316 ],
1317 '<span class="mw-translate-fuzzy">$1</span>'
1318 );
1319
1320 $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] );
1321 }
1322 }
1323 }
1324
1330 public static function replaceMovePage( &$list ) {
1331 $movePageSpec = $list['Movepage'] ?? null;
1332
1333 // This should never happen, but apparently is happening? See: T296568
1334 if ( $movePageSpec === null ) {
1335 return true;
1336 }
1337
1338 $list['Movepage'] = [
1339 'class' => MoveTranslatableBundleSpecialPage::class,
1340 'services' => [
1341 'ObjectFactory',
1342 'PermissionManager',
1343 'Translate:TranslatableBundleMover',
1344 'Translate:TranslatableBundleFactory'
1345 ],
1346 'args' => [
1347 $movePageSpec
1348 ]
1349 ];
1350
1351 return true;
1352 }
1353
1362 public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) {
1363 if ( $action === 'read' ) {
1364 return true;
1365 }
1366
1367 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1368 $key = $cache->makeKey( 'pt-lock', sha1( $title->getPrefixedText() ) );
1369 if ( $cache->get( $key ) === 'locked' ) {
1370 $result = [ 'pt-locked-page' ];
1371
1372 return false;
1373 }
1374
1375 return true;
1376 }
1377
1385 public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) {
1386 $linker = MediaWikiServices::getInstance()->getLinkRenderer();
1387
1388 $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() );
1389 if ( !$isTranslationPage
1390 && !TranslatablePage::isSourcePage( $out->getTitle() )
1391 ) {
1392 return true;
1393 }
1394
1395 // Copied from Skin::subPageSubtitle()
1396 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1397 if (
1398 $out->isArticle() &&
1399 $nsInfo->hasSubpages( $out->getTitle()->getNamespace() )
1400 ) {
1401 $ptext = $out->getTitle()->getPrefixedText();
1402 if ( strpos( $ptext, '/' ) !== false ) {
1403 $links = explode( '/', $ptext );
1404 array_pop( $links );
1405 if ( $isTranslationPage ) {
1406 // Also remove language code page
1407 array_pop( $links );
1408 }
1409 $c = 0;
1410 $growinglink = '';
1411 $display = '';
1412 $lang = $skin->getLanguage();
1413
1414 foreach ( $links as $link ) {
1415 $growinglink .= $link;
1416 $display .= $link;
1417 $linkObj = Title::newFromText( $growinglink );
1418
1419 if ( is_object( $linkObj ) && $linkObj->isKnown() ) {
1420 $getlink = $linker->makeKnownLink(
1421 SpecialPage::getTitleFor( 'MyLanguage', $growinglink ),
1422 $display
1423 );
1424
1425 $c++;
1426
1427 if ( $c > 1 ) {
1428 $subpages .= $lang->getDirMarkEntity() . $skin->msg( 'pipe-separator' )->escaped();
1429 } else {
1430 $subpages .= '&lt; ';
1431 }
1432
1433 $subpages .= $getlink;
1434 $display = '';
1435 } else {
1436 $display .= '/';
1437 }
1438
1439 $growinglink .= '/';
1440 }
1441 }
1442
1443 return false;
1444 }
1445
1446 return true;
1447 }
1448
1456 public static function translateTab( Skin $skin, array &$tabs ) {
1457 $title = $skin->getTitle();
1458 $handle = new MessageHandle( $title );
1459 $code = $handle->getCode();
1460 $page = TranslatablePage::isTranslationPage( $title );
1461 if ( !$page ) {
1462 return true;
1463 }
1464 // The source language has a subpage too, but cannot be translated
1465 if ( $page->getSourceLanguageCode() === $code ) {
1466 return true;
1467 }
1468
1469 if ( isset( $tabs['views']['edit'] ) ) {
1470 $tabs['views']['edit']['text'] = $skin->msg( 'tpt-tab-translate' )->text();
1471 $tabs['views']['edit']['href'] = $page->getTranslationUrl( $code );
1472 }
1473
1474 return true;
1475 }
1476
1489 public static function onMovePageTranslationUnits(
1490 LinkTarget $oldLinkTarget,
1491 LinkTarget $newLinkTarget,
1492 UserIdentity $userIdentity,
1493 int $oldid,
1494 int $newid,
1495 string $reason,
1496 RevisionRecord $revisionRecord
1497 ) {
1498 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
1499 // MoveTranslatableBundleJob takes care of handling updates because it performs
1500 // a lot of moves at once. As a performance optimization, skip this hook if
1501 // we detect moves from that job. As there isn't a good way to pass information
1502 // to this hook what originated the move, we use some heuristics.
1503 if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) {
1504 return;
1505 }
1506
1507 $oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
1508 $newTitle = Title::newFromLinkTarget( $newLinkTarget );
1509 $groupLast = null;
1510 foreach ( [ $oldTitle, $newTitle ] as $title ) {
1511 $handle = new MessageHandle( $title );
1512 if ( !$handle->isValid() ) {
1513 continue;
1514 }
1515
1516 // Documentation pages are never translation pages
1517 if ( $handle->isDoc() ) {
1518 continue;
1519 }
1520
1521 $group = $handle->getGroup();
1522 if ( !$group instanceof WikiPageMessageGroup ) {
1523 continue;
1524 }
1525
1526 $language = $handle->getCode();
1527
1528 // Ignore pages such as Translations:Page/unit without language code
1529 if ( (string)$language === '' ) {
1530 continue;
1531 }
1532
1533 // Update the page only once if source and destination units
1534 // belong to the same page
1535 if ( $group !== $groupLast ) {
1536 $groupLast = $group;
1537 $page = TranslatablePage::newFromTitle( $group->getTitle() );
1538 self::updateTranslationPage( $page, $language, $user, 0, $reason );
1539 }
1540 }
1541 }
1542
1553 public static function onDeleteTranslationUnit(
1554 WikiPage $unit,
1555 User $user,
1556 $reason,
1557 $id,
1558 $content,
1559 $logEntry
1560 ) {
1561 // Do the update. In case job queue is doing the work, the update is not done here
1562 if ( self::$jobQueueRunning ) {
1563 return;
1564 }
1565
1566 $title = $unit->getTitle();
1567
1568 $handle = new MessageHandle( $title );
1569 if ( !$handle->isValid() ) {
1570 return;
1571 }
1572
1573 $group = $handle->getGroup();
1574 if ( !$group instanceof WikiPageMessageGroup ) {
1575 return;
1576 }
1577
1578 $target = $group->getTitle();
1579 $langCode = $handle->getCode();
1580 $fname = __METHOD__;
1581
1582 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
1583 $callback = function () use (
1584 $dbw,
1585 $target,
1586 $handle,
1587 $langCode,
1588 $user,
1589 $reason,
1590 $fname
1591 ) {
1592 $translationPageTitle = $target->getSubpage( $langCode );
1593 // Do a more thorough check for the translation page in case the translation page is deleted in a
1594 // different transaction.
1595 if ( !$translationPageTitle || !$translationPageTitle->exists( Title::READ_LATEST ) ) {
1596 return;
1597 }
1598
1599 $dbw->startAtomic( $fname );
1600
1601 $page = TranslatablePage::newFromTitle( $target );
1602
1603 MessageGroupStats::forItem(
1604 $page->getMessageGroupId(),
1605 $langCode,
1606 MessageGroupStats::FLAG_NO_CACHE
1607 );
1608
1609 if ( !$handle->isDoc() ) {
1610 $unitTitle = $handle->getTitle();
1611 // Assume that $user and $reason for the first deletion is the same for all
1612 self::updateTranslationPage(
1613 $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle
1614 );
1615 }
1616
1617 $dbw->endAtomic( $fname );
1618 };
1619
1620 $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ );
1621 }
1622}
static tpSyntaxCheckForEditContent( $context, $content, $status, $summary)
Display nice error when editing content.
Definition Hooks.php:800
static tpSyntaxCheck(RenderedRevision $renderedRevision, UserIdentity $user, CommentStoreComment $summary, $flags, Status $hookStatus)
When attempting to save, last resort.
Definition Hooks.php:850
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:1489
static replaceMovePage(&$list)
Hook: SpecialPage_initList.
Definition Hooks.php:1330
static onTitleGetEditNotices(Title $title, int $oldid, array &$notices)
Display an edit notice for translatable source pages if it's enabled Hook: TitleGetEditNotices.
Definition Hooks.php:293
static onGetUserPermissionsErrorsExpensive(Title $title, User $user, $action, &$result)
Prevent creation of orphan translation units in Translations namespace.
Definition Hooks.php:962
static preprocessTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforePreprocess.
Definition Hooks.php:157
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:1553
static preventDirectEditing(Title $title, User $user, $action, &$result)
Prevent editing of translation pages directly.
Definition Hooks.php:1082
static renderTagPage( $wikitextParser, &$text, $state)
Hook: ParserBeforeInternalParse.
Definition Hooks.php:72
static onParserOutputPostCacheTransform(ParserOutput $out, &$text, array &$options)
Hook: ParserOutputPostCacheTransform.
Definition Hooks.php:187
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:379
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:208
static onBeforePageDisplay(OutputPage $out, Skin $skin)
Hook: BeforePageDisplay.
Definition Hooks.php:327
static translatablePageHeader( $article, &$outputDone, &$pcache)
Hook: ArticleViewHeader.
Definition Hooks.php:1157
static onPageContentLanguage(Title $title, &$pageLang)
Set the right page content language for translated pages ("Page/xx").
Definition Hooks.php:277
static lockedPagesCheck(Title $title, User $user, $action, &$result)
Hook: getUserPermissionsErrorsExpensive.
Definition Hooks.php:1362
static updateTranstagOnNullRevisions(RevisionRecord $rev)
Page moving and page protection (and possibly other things) creates null revisions.
Definition Hooks.php:926
static onVisualEditorBeforeEditor(OutputPage $out, Skin $skin)
Hook: onVisualEditorBeforeEditor.
Definition Hooks.php:364
static replaceSubtitle(&$subpages, ?Skin $skin, OutputPage $out)
Hook: SkinSubPageSubtitle.
Definition Hooks.php:1385
static formatLanguageLink(array &$link, Title $linkTitle, Title $pageTitle, OutputPage $out)
Hooks: SkinTemplateGetLanguageLink.
Definition Hooks.php:765
static languages( $data, $params, $parser)
Definition Hooks.php:461
static addLanguageLinks(Title $title, array &$languageLinks)
Hooks: LanguageLinks.
Definition Hooks.php:676
static disableDelete( $article, $out, &$reason)
Redirects the delete action to our own for translatable pages.
Definition Hooks.php:1134
static translateTab(Skin $skin, array &$tabs)
Converts the edit tab (if exists) for translation pages to translate tab.
Definition Hooks.php:1456
static addTranstagAfterSave(WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revisionRecord, EditResult $editResult)
Hook: PageSaveComplete.
Definition Hooks.php:883
Represents a parsing output produced by TranslatablePageParser.
Represents any kind of failure to parse a translatable page source code.
Mixed bag of methods related to translatable pages.
addReadyTag(int $revision)
Adds a tag which indicates that this page source is ready for marking for translation.
getMarkedTag()
Returns the latest revision which has marked tag, if any.
getReadyTag()
Returns the latest revision which has ready tag, if any.
getTranslationUrl( $code=false)
Produces a link to translation view of a translation page.
getMessageGroup()
Returns MessageGroup used for translating this page.
getPageDisplayTitle(string $languageCode)
Get translated page title.
This class represents one translation unit in a translatable page.
Minimal service container.
Definition Services.php:38
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
This class abstract MessageGroup statistics calculation and storing.
static forItem( $id, $code, $flags=0)
Returns stats for given group in given language.
Class for pointing to messages, like Title class is for titles.
isDoc()
Determine whether the current handle is for message documentation.
getGroup()
Get the primary MessageGroup this message belongs to.
isValid()
Checks if the handle corresponds to a known message.
getTitle()
Get the original title.
getCode()
Returns the language code.
static get( $group, $key)
Get a metadata value for the given group and key.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
static figureMessage( $text)
Splits page name into message key and language code.
Wraps the translatable page sections into a message group.