Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TranslateHooks.php
Go to the documentation of this file.
1<?php
10use MediaWiki\Config\ServiceOptions;
11use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Revision\Hook\RevisionRecordInsertedHook;
31use MediaWiki\Revision\RevisionLookup;
32use Wikimedia\Rdbms\ILoadBalancer;
33
42class TranslateHooks implements RevisionRecordInsertedHook {
49 private static $userMergeTables = [
50 'translate_stash' => 'ts_user',
51 'translate_reviews' => 'trr_user',
52 ];
54 private $revisionLookup;
56 private $loadBalancer;
57
58 public function __construct( RevisionLookup $revisionLookup, ILoadBalancer $loadBalancer ) {
59 $this->revisionLookup = $revisionLookup;
60 $this->loadBalancer = $loadBalancer;
61 }
62
66 public static function setupTranslate() {
67 global $wgHooks, $wgTranslateYamlLibrary;
68
69 /*
70 * Text that will be shown in translations if the translation is outdated.
71 * Must be something that does not conflict with actual content.
72 */
73 if ( !defined( 'TRANSLATE_FUZZY' ) ) {
74 define( 'TRANSLATE_FUZZY', '!!FUZZY!!' );
75 }
76
77 if ( $wgTranslateYamlLibrary === null ) {
78 $wgTranslateYamlLibrary = function_exists( 'yaml_parse' ) ? 'phpyaml' : 'spyc';
79 }
80
81 $wgHooks['PageSaveComplete'][] = 'TranslateEditAddons::onSaveComplete';
82
83 // Page translation setup check and init if enabled.
84 global $wgEnablePageTranslation;
85 if ( $wgEnablePageTranslation ) {
86 // Special page and the right to use it
87 global $wgSpecialPages, $wgAvailableRights;
88 $wgSpecialPages['PageTranslation'] = [
89 'class' => PageTranslationSpecialPage::class,
90 'services' => [
91 'LanguageNameUtils',
92 'LanguageFactory',
93 'Translate:TranslationUnitStoreFactory',
94 'Translate:TranslatablePageParser',
95 'LinkBatchFactory',
96 'JobQueueGroup',
97 ]
98 ];
99 $wgSpecialPages['PageTranslationDeletePage'] = [
100 'class' => DeleteTranslatableBundleSpecialPage::class,
101 'services' => [
102 'MainObjectStash',
103 'PermissionManager',
104 'Translate:TranslatableBundleFactory',
105 'Translate:SubpageListBuilder',
106 'JobQueueGroup',
107 ]
108 ];
109
110 // right-pagetranslation action-pagetranslation
111 $wgAvailableRights[] = 'pagetranslation';
112
113 $wgSpecialPages['PageMigration'] = MigrateTranslatablePageSpecialPage::class;
114 $wgSpecialPages['PagePreparation'] = PrepareTranslatablePageSpecialPage::class;
115
116 global $wgActionFilteredLogs, $wgLogActionsHandlers, $wgLogTypes;
117
118 // log-description-pagetranslation log-name-pagetranslation logentry-pagetranslation-mark
119 // logentry-pagetranslation-unmark logentry-pagetranslation-moveok
120 // logentry-pagetranslation-movenok logentry-pagetranslation-deletefok
121 // logentry-pagetranslation-deletefnok logentry-pagetranslation-deletelok
122 // logentry-pagetranslation-deletelnok logentry-pagetranslation-encourage
123 // logentry-pagetranslation-discourage logentry-pagetranslation-prioritylanguages
124 // logentry-pagetranslation-associate logentry-pagetranslation-dissociate
125 $wgLogTypes[] = 'pagetranslation';
126 $wgLogActionsHandlers['pagetranslation/mark'] = TranslatableBundleLogFormatter::class;
127 $wgLogActionsHandlers['pagetranslation/unmark'] = TranslatableBundleLogFormatter::class;
128 $wgLogActionsHandlers['pagetranslation/moveok'] = TranslatableBundleLogFormatter::class;
129 $wgLogActionsHandlers['pagetranslation/movenok'] = TranslatableBundleLogFormatter::class;
130 $wgLogActionsHandlers['pagetranslation/deletelok'] = TranslatableBundleLogFormatter::class;
131 $wgLogActionsHandlers['pagetranslation/deletefok'] = TranslatableBundleLogFormatter::class;
132 $wgLogActionsHandlers['pagetranslation/deletelnok'] = TranslatableBundleLogFormatter::class;
133 $wgLogActionsHandlers['pagetranslation/deletefnok'] = TranslatableBundleLogFormatter::class;
134 $wgLogActionsHandlers['pagetranslation/encourage'] = TranslatableBundleLogFormatter::class;
135 $wgLogActionsHandlers['pagetranslation/discourage'] = TranslatableBundleLogFormatter::class;
136 $wgLogActionsHandlers['pagetranslation/prioritylanguages'] = TranslatableBundleLogFormatter::class;
137 $wgLogActionsHandlers['pagetranslation/associate'] = TranslatableBundleLogFormatter::class;
138 $wgLogActionsHandlers['pagetranslation/dissociate'] = TranslatableBundleLogFormatter::class;
139 $wgActionFilteredLogs['pagetranslation'] = [
140 'mark' => [ 'mark' ],
141 'unmark' => [ 'unmark' ],
142 'move' => [ 'moveok', 'movenok' ],
143 'delete' => [ 'deletefok', 'deletefnok', 'deletelok', 'deletelnok' ],
144 'encourage' => [ 'encourage' ],
145 'discourage' => [ 'discourage' ],
146 'prioritylanguages' => [ 'prioritylanguages' ],
147 'aggregategroups' => [ 'associate', 'dissociate' ],
148 ];
149
150 $wgLogTypes[] = 'messagebundle';
151 $wgLogActionsHandlers['messagebundle/moveok'] = TranslatableBundleLogFormatter::class;
152 $wgLogActionsHandlers['messagebundle/movenok'] = TranslatableBundleLogFormatter::class;
153 $wgLogActionsHandlers['messagebundle/deletefok'] = TranslatableBundleLogFormatter::class;
154 $wgLogActionsHandlers['messagebundle/deletefnok'] = TranslatableBundleLogFormatter::class;
155 $wgActionFilteredLogs['messagebundle'] = [
156 'move' => [ 'moveok', 'movenok' ],
157 'delete' => [ 'deletefok', 'deletefnok' ],
158 ];
159
160 global $wgJobClasses;
161 $wgJobClasses['RenderTranslationPageJob'] = RenderTranslationPageJob::class;
162 // Remove after MLEB 2022.10 release
163 $wgJobClasses['TranslateRenderJob'] = RenderTranslationPageJob::class;
164 // Remove after MLEB 2022.07 release
165 $wgJobClasses['TranslatableBundleMoveJob'] = MoveTranslatableBundleJob::class;
166 $wgJobClasses['MoveTranslatableBundleJob'] = MoveTranslatableBundleJob::class;
167 // Remove after MLEB 2022.07 release
168 $wgJobClasses['TranslatableBundleDeleteJob'] = DeleteTranslatableBundleJob::class;
169 $wgJobClasses['DeleteTranslatableBundleJob'] = DeleteTranslatableBundleJob::class;
170
171 $wgJobClasses['UpdateTranslatablePageJob'] = UpdateTranslatablePageJob::class;
172 // Remove after MLEB 2022.10 release
173 $wgJobClasses['TranslationsUpdateJob'] = UpdateTranslatablePageJob::class;
174
175 // Namespaces
176 global $wgNamespacesWithSubpages, $wgNamespaceProtection;
177 global $wgTranslateMessageNamespaces;
178
179 $wgNamespacesWithSubpages[NS_TRANSLATIONS] = true;
180 $wgNamespacesWithSubpages[NS_TRANSLATIONS_TALK] = true;
181
182 // Standard protection and register it for filtering
183 $wgNamespaceProtection[NS_TRANSLATIONS] = [ 'translate' ];
184 $wgTranslateMessageNamespaces[] = NS_TRANSLATIONS;
185
187
189 $wgHooks['BeforePageDisplay'][] = [ Hooks::class, 'onBeforePageDisplay' ];
190
191 // Disable VE
192 $wgHooks['VisualEditorBeforeEditor'][] = [ Hooks::class, 'onVisualEditorBeforeEditor' ];
193
194 // Check syntax for <translate>
195 $wgHooks['MultiContentSave'][] = [ Hooks::class, 'tpSyntaxCheck' ];
196 $wgHooks['EditFilterMergedContent'][] =
197 [ Hooks::class, 'tpSyntaxCheckForEditContent' ];
198
199 // Add transtag to page props for discovery
200 $wgHooks['PageSaveComplete'][] = [ Hooks::class, 'addTranstagAfterSave' ];
201
202 $wgHooks['RevisionRecordInserted'][] = [ Hooks::class, 'updateTranstagOnNullRevisions' ];
203
204 // Register different ways to show language links
205 $wgHooks['ParserFirstCallInit'][] = 'TranslateHooks::setupParserHooks';
206 $wgHooks['LanguageLinks'][] = [ Hooks::class, 'addLanguageLinks' ];
207 $wgHooks['SkinTemplateGetLanguageLink'][] = [ Hooks::class, 'formatLanguageLink' ];
208
209 // Strip <translate> tags etc. from source pages when rendering
210 $wgHooks['ParserBeforeInternalParse'][] = [ Hooks::class, 'renderTagPage' ];
211 // Strip <translate> tags etc. from source pages when preprocessing
212 $wgHooks['ParserBeforePreprocess'][] = [ Hooks::class, 'preprocessTagPage' ];
213 $wgHooks['ParserOutputPostCacheTransform'][] =
214 [ Hooks::class, 'onParserOutputPostCacheTransform' ];
215
216 $wgHooks['BeforeParserFetchTemplateRevisionRecord'][] =
217 [ Hooks::class, 'fetchTranslatableTemplateAndTitle' ];
218
219 // Set the page content language
220 $wgHooks['PageContentLanguage'][] = [ Hooks::class, 'onPageContentLanguage' ];
221
222 // Prevent editing of certain pages in translations namespace
223 $wgHooks['getUserPermissionsErrorsExpensive'][] =
224 [ Hooks::class, 'onGetUserPermissionsErrorsExpensive' ];
225 // Prevent editing of translation pages directly
226 $wgHooks['getUserPermissionsErrorsExpensive'][] =
227 [ Hooks::class, 'preventDirectEditing' ];
228
229 // Our custom header for translation pages
230 $wgHooks['ArticleViewHeader'][] = [ Hooks::class, 'translatablePageHeader' ];
231
232 // Edit notice shown on translatable pages
233 $wgHooks['TitleGetEditNotices'][] = [ Hooks::class, 'onTitleGetEditNotices' ];
234
235 // Custom move page that can move all the associated pages too
236 $wgHooks['SpecialPage_initList'][] = [ Hooks::class, 'replaceMovePage' ];
237 // Locking during page moves
238 $wgHooks['getUserPermissionsErrorsExpensive'][] =
239 [ Hooks::class, 'lockedPagesCheck' ];
240 // Disable action=delete
241 $wgHooks['ArticleConfirmDelete'][] = [ Hooks::class, 'disableDelete' ];
242
243 // Replace subpage logic behavior
244 $wgHooks['SkinSubPageSubtitle'][] = [ Hooks::class, 'replaceSubtitle' ];
245
246 // Replaced edit tab with translation tab for translation pages
247 $wgHooks['SkinTemplateNavigation::Universal'][] = [ Hooks::class, 'translateTab' ];
248
249 // Update translated page when translation unit is moved
250 $wgHooks['PageMoveComplete'][] = [ Hooks::class, 'onMovePageTranslationUnits' ];
251
252 // Update translated page when translation unit is deleted
253 $wgHooks['ArticleDeleteComplete'][] = [ Hooks::class, 'onDeleteTranslationUnit' ];
254 }
255
256 global $wgTranslateUseSandbox;
257 if ( $wgTranslateUseSandbox ) {
258 global $wgSpecialPages, $wgAvailableRights, $wgDefaultUserOptions;
259
260 $wgSpecialPages['ManageTranslatorSandbox'] = [
261 'class' => ManageTranslatorSandboxSpecialPage::class,
262 'services' => [
263 'Translate:TranslationStashReader',
264 'UserOptionsLookup'
265 ],
266 'args' => [
267 static function () {
268 return new ServiceOptions(
269 ManageTranslatorSandboxSpecialPage::CONSTRUCTOR_OPTIONS,
270 MediaWikiServices::getInstance()->getMainConfig()
271 );
272 }
273 ]
274 ];
275 $wgSpecialPages['TranslationStash'] = [
276 'class' => TranslationStashSpecialPage::class,
277 'services' => [
278 'LanguageNameUtils',
279 'Translate:TranslationStashReader',
280 'UserOptionsLookup'
281 ],
282 'args' => [
283 static function () {
284 return new ServiceOptions(
285 TranslationStashSpecialPage::CONSTRUCTOR_OPTIONS,
286 MediaWikiServices::getInstance()->getMainConfig()
287 );
288 }
289 ]
290 ];
291 $wgDefaultUserOptions['translate-sandbox'] = '';
292 // right-translate-sandboxmanage action-translate-sandboxmanage
293 $wgAvailableRights[] = 'translate-sandboxmanage';
294
295 $wgHooks['GetPreferences'][] = 'TranslateSandbox::onGetPreferences';
296 $wgHooks['UserGetRights'][] = 'TranslateSandbox::enforcePermissions';
297 $wgHooks['ApiCheckCanExecute'][] = 'TranslateSandbox::onApiCheckCanExecute';
298
299 global $wgLogTypes, $wgLogActionsHandlers;
300 // log-name-translatorsandbox log-description-translatorsandbox
301 $wgLogTypes[] = 'translatorsandbox';
302 // logentry-translatorsandbox-promoted logentry-translatorsandbox-rejected
303 $wgLogActionsHandlers['translatorsandbox/promoted'] = 'TranslateLogFormatter';
304 $wgLogActionsHandlers['translatorsandbox/rejected'] = 'TranslateLogFormatter';
305
306 // This is no longer used for new entries since 2016.07.
307 // logentry-newusers-tsbpromoted
308 $wgLogActionsHandlers['newusers/tsbpromoted'] = 'LogFormatter';
309
310 global $wgJobClasses;
311 $wgJobClasses['TranslateSandboxEmailJob'] = 'TranslateSandboxEmailJob';
312
313 global $wgAPIModules;
314 $wgAPIModules['translationstash'] = [
315 'class' => TranslationStashActionApi::class,
316 'services' => [
317 'DBLoadBalancer',
318 'UserFactory'
319 ]
320 ];
321 $wgAPIModules['translatesandbox'] = [
322 'class' => TranslatorSandboxActionApi::class,
323 'services' => [
324 'UserFactory',
325 'UserNameUtils',
326 'UserOptionsManager',
327 'WikiPageFactory',
328 'UserOptionsLookup'
329 ],
330 'args' => [
331 static function () {
332 return new ServiceOptions(
333 TranslatorSandboxActionApi::CONSTRUCTOR_OPTIONS,
334 MediaWikiServices::getInstance()->getMainConfig()
335 );
336 }
337 ]
338 ];
339 }
340
341 global $wgNamespaceRobotPolicies;
342 $wgNamespaceRobotPolicies[NS_TRANSLATIONS] = 'noindex';
343
344 // If no service has been configured, we use a built-in fallback.
345 global $wgTranslateTranslationDefaultService,
346 $wgTranslateTranslationServices;
347 if ( $wgTranslateTranslationDefaultService === true ) {
348 $wgTranslateTranslationDefaultService = 'TTMServer';
349 if ( !isset( $wgTranslateTranslationServices['TTMServer'] ) ) {
350 $wgTranslateTranslationServices['TTMServer'] = [
351 'database' => false, // Passed to wfGetDB
352 'cutoff' => 0.75,
353 'type' => 'ttmserver',
354 'public' => false,
355 ];
356 }
357 }
358
359 $wgHooks['SidebarBeforeOutput'][] = 'TranslateToolbox::toolboxAllTranslations';
360 }
361
368 public static function onUserGetReservedNames( array &$names ) {
369 $names[] = FuzzyBot::getName();
370 $names[] = TranslateUserManager::getName();
371 }
372
380 public static function onAbuseFilterAlterVariables(
381 &$vars, Title $title, User $user
382 ) {
383 $handle = new MessageHandle( $title );
384
385 // Only set this variable if we are in a proper namespace to avoid
386 // unnecessary overhead in non-translation pages
387 if ( $handle->isMessageNamespace() ) {
388 $vars->setLazyLoadVar(
389 'translate_source_text',
390 'translate-get-source',
391 [ 'handle' => $handle ]
392 );
393 $vars->setLazyLoadVar(
394 'translate_target_language',
395 'translate-get-target-language',
396 [ 'handle' => $handle ]
397 );
398 }
399 }
400
409 public static function onAbuseFilterComputeVariable( $method, $vars, $parameters, &$result ) {
410 if ( $method !== 'translate-get-source' && $method !== 'translate-get-target-language' ) {
411 return true;
412 }
413
414 $handle = $parameters['handle'];
415 $value = '';
416 if ( $handle->isValid() ) {
417 if ( $method === 'translate-get-source' ) {
418 $group = $handle->getGroup();
419 $value = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
420 } else {
421 $value = $handle->getCode();
422 }
423 }
424
425 $result = $value;
426
427 return false;
428 }
429
434 public static function onAbuseFilterBuilder( array &$builderValues ) {
435 // Uses: 'abusefilter-edit-builder-vars-translate-source-text'
436 // and 'abusefilter-edit-builder-vars-translate-target-language'
437 $builderValues['vars']['translate_source_text'] = 'translate-source-text';
438 $builderValues['vars']['translate_target_language'] = 'translate-target-language';
439 }
440
447 public static function setupParserHooks( Parser $parser ) {
448 // For nice language list in-page
449 $parser->setHook( 'languages', [ Hooks::class, 'languages' ] );
450 }
451
457 public static function schemaUpdates( DatabaseUpdater $updater ) {
458 $dir = __DIR__ . '/sql';
459 $dbType = $updater->getDB()->getType();
460
461 if ( $dbType === 'mysql' || $dbType === 'sqlite' ) {
462 $updater->addExtensionTable(
463 'translate_sections',
464 "{$dir}/{$dbType}/translate_sections.sql"
465 );
466 $updater->addExtensionTable(
467 'revtag',
468 "{$dir}/{$dbType}/revtag.sql"
469 );
470 $updater->addExtensionTable(
471 'translate_groupstats',
472 "{$dir}/{$dbType}/translate_groupstats.sql"
473 );
474 $updater->addExtensionTable(
475 'translate_reviews',
476 "{$dir}/{$dbType}/translate_reviews.sql"
477 );
478 $updater->addExtensionTable(
479 'translate_groupreviews',
480 "{$dir}/{$dbType}/translate_groupreviews.sql"
481 );
482 $updater->addExtensionTable(
483 'translate_tms',
484 "{$dir}/{$dbType}/translate_tm.sql"
485 );
486 $updater->addExtensionTable(
487 'translate_metadata',
488 "{$dir}/{$dbType}/translate_metadata.sql"
489 );
490 $updater->addExtensionTable(
491 'translate_messageindex',
492 "{$dir}/{$dbType}/translate_messageindex.sql"
493 );
494 $updater->addExtensionTable(
495 'translate_stash',
496 "{$dir}/{$dbType}/translate_stash.sql"
497 );
498
499 // 1.32 - This also adds a PRIMARY KEY
500 $updater->addExtensionUpdate( [
501 'renameIndex',
502 'translate_reviews',
503 'trr_user_page_revision',
504 'PRIMARY',
505 false,
506 "$dir/translate_reviews-patch-01-primary-key.sql",
507 true
508 ] );
509
510 $updater->addExtensionTable(
511 'translate_cache',
512 "{$dir}/{$dbType}/translate_cache.sql"
513 );
514
515 if ( $dbType === 'mysql' ) {
516 // 1.38
517 $updater->modifyExtensionField(
518 'translate_cache',
519 'tc_key',
520 "{$dir}/{$dbType}/translate_cache-alter-varbinary.sql"
521 );
522 }
523 } elseif ( $dbType === 'postgres' ) {
524 $updater->addExtensionTable(
525 'translate_sections',
526 "{$dir}/{$dbType}/tables-generated.sql"
527 );
528 $updater->addExtensionUpdate( [
529 'changeField', 'translate_cache', 'tc_exptime', 'TIMESTAMPTZ', 'th_timestamp::timestamp with time zone'
530 ] );
531 }
532
533 // 1.39
534 $updater->dropExtensionIndex(
535 'translate_messageindex',
536 'tmi_key',
537 "{$dir}/{$dbType}/patch-translate_messageindex-unique-to-pk.sql"
538 );
539 $updater->dropExtensionIndex(
540 'translate_tmt',
541 'tms_sid_lang',
542 "{$dir}/{$dbType}/patch-translate_tmt-unique-to-pk.sql"
543 );
544 $updater->dropExtensionIndex(
545 'revtag',
546 'rt_type_page_revision',
547 "{$dir}/{$dbType}/patch-revtag-unique-to-pk.sql"
548 );
549 }
550
555 public static function parserTestTables( array &$tables ) {
556 $tables[] = 'revtag';
557 $tables[] = 'translate_groupstats';
558 $tables[] = 'translate_messageindex';
559 $tables[] = 'translate_stash';
560 }
561
569 public static function onPageContentLanguage( Title $title, &$pageLang ) {
570 $handle = new MessageHandle( $title );
571 if ( $handle->isMessageNamespace() ) {
572 $pageLang = $handle->getEffectiveLanguage();
573 }
574 }
575
582 public static function translateMessageDocumentationLanguage( array &$names, $code ) {
583 global $wgTranslateDocumentationLanguageCode;
584 if ( $wgTranslateDocumentationLanguageCode ) {
585 // Special case the autonyms
586 if (
587 $wgTranslateDocumentationLanguageCode === $code ||
588 $code === null
589 ) {
590 $code = 'en';
591 }
592
593 $names[$wgTranslateDocumentationLanguageCode] =
594 wfMessage( 'translate-documentation-language' )->inLanguage( $code )->plain();
595 }
596 }
597
602 public static function searchProfile( array &$profiles ) {
603 global $wgTranslateMessageNamespaces;
604 $insert = [];
605 $insert['translation'] = [
606 'message' => 'translate-searchprofile',
607 'tooltip' => 'translate-searchprofile-tooltip',
608 'namespaces' => $wgTranslateMessageNamespaces,
609 ];
610
611 // Insert translations before 'all'
612 $index = array_search( 'all', array_keys( $profiles ) );
613
614 // Or just at the end if all is not found
615 if ( $index === false ) {
616 wfWarn( '"all" not found in search profiles' );
617 $index = count( $profiles );
618 }
619
620 $profiles = array_merge(
621 array_slice( $profiles, 0, $index ),
622 $insert,
623 array_slice( $profiles, $index )
624 );
625 }
626
636 public static function searchProfileForm(
637 SpecialSearch $search,
638 &$form,
639 $profile,
640 $term,
641 array $opts
642 ) {
643 if ( $profile !== 'translation' ) {
644 return true;
645 }
646
647 if ( TTMServer::primary() instanceof SearchableTTMServer ) {
648 $href = SpecialPage::getTitleFor( 'SearchTranslations' )
649 ->getFullUrl( [ 'query' => $term ] );
650 $form = Html::successBox(
651 $search->msg( 'translate-searchprofile-note', $href )->parse(),
652 'plainlinks'
653 );
654
655 return false;
656 }
657
658 if ( !$search->getSearchEngine()->supports( 'title-suffix-filter' ) ) {
659 return false;
660 }
661
662 $hidden = '';
663 foreach ( $opts as $key => $value ) {
664 $hidden .= Html::hidden( $key, $value );
665 }
666
667 $context = $search->getContext();
668 $code = $context->getLanguage()->getCode();
669 $selected = $context->getRequest()->getVal( 'languagefilter' );
670
671 $languages = TranslateUtils::getLanguageNames( $code );
672 ksort( $languages );
673
674 $selector = new XmlSelect( 'languagefilter', 'languagefilter' );
675 $selector->setDefault( $selected );
676 $selector->addOption( wfMessage( 'translate-search-nofilter' )->text(), '-' );
677 foreach ( $languages as $code => $name ) {
678 $selector->addOption( "$code - $name", $code );
679 }
680
681 $selector = $selector->getHTML();
682
683 $label = Xml::label(
684 wfMessage( 'translate-search-languagefilter' )->text(),
685 'languagefilter'
686 ) . '&#160;';
687 $params = [ 'id' => 'mw-searchoptions' ];
688
689 $form = Xml::fieldset( false, false, $params ) .
690 $hidden . $label . $selector .
691 Html::closeElement( 'fieldset' );
692
693 return false;
694 }
695
702 public static function searchProfileSetupEngine(
703 SpecialSearch $search,
704 $profile,
705 SearchEngine $engine
706 ) {
707 if ( $profile !== 'translation' ) {
708 return;
709 }
710
711 $context = $search->getContext();
712 $selected = $context->getRequest()->getVal( 'languagefilter' );
713 if ( $selected !== '-' && $selected ) {
714 $engine->setFeatureData( 'title-suffix-filter', "/$selected" );
715 $search->setExtraParam( 'languagefilter', $selected );
716 }
717 }
718
724 public static function preventCategorization( Parser $parser, &$html ) {
725 $handle = new MessageHandle( $parser->getTitle() );
726 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
727 $parserOutput = $parser->getOutput();
728 $parserOutput->setExtensionData( 'translate-fake-categories',
729 $parserOutput->getCategories() );
730 if ( method_exists( $parserOutput, 'setCategories' ) ) { // 1.38+
731 $parserOutput->setCategories( [] );
732 } else {
733 $parserOutput->setCategoryLinks( [] );
734 }
735 }
736 }
737
743 public static function showFakeCategories( OutputPage $outputPage, ParserOutput $parserOutput ) {
744 $fakeCategories = $parserOutput->getExtensionData( 'translate-fake-categories' );
745 if ( $fakeCategories ) {
746 $outputPage->setCategoryLinks( $fakeCategories );
747 }
748 }
749
758 public static function addConfig( array &$vars, OutputPage $out ) {
759 $title = $out->getTitle();
760 [ $alias, ] = MediaWikiServices::getInstance()
761 ->getSpecialPageFactory()->resolveAlias( $title->getText() );
762
763 if ( $title->isSpecialPage()
764 && ( $alias === 'Translate'
765 || $alias === 'TranslationStash'
766 || $alias === 'SearchTranslations' )
767 ) {
768 global $wgTranslateDocumentationLanguageCode, $wgTranslatePermissionUrl,
769 $wgTranslateUseSandbox;
770 $vars['TranslateRight'] = $out->getUser()->isAllowed( 'translate' );
771 $vars['TranslateMessageReviewRight'] =
772 $out->getUser()->isAllowed( 'translate-messagereview' );
773 $vars['DeleteRight'] = $out->getUser()->isAllowed( 'delete' );
774 $vars['TranslateManageRight'] = $out->getUser()->isAllowed( 'translate-manage' );
775 $vars['wgTranslateDocumentationLanguageCode'] = $wgTranslateDocumentationLanguageCode;
776 $vars['wgTranslatePermissionUrl'] = $wgTranslatePermissionUrl;
777 $vars['wgTranslateUseSandbox'] = $wgTranslateUseSandbox;
778 }
779 }
780
785 public static function onAdminLinks( ALTree $tree ) {
786 global $wgTranslateUseSandbox;
787
788 if ( $wgTranslateUseSandbox ) {
789 $sectionLabel = wfMessage( 'adminlinks_users' )->text();
790 $row = $tree->getSection( $sectionLabel )->getRow( 'main' );
791 $row->addItem( ALItem::newFromSpecialPage( 'TranslateSandbox' ) );
792 }
793 }
794
802 public static function onMergeAccountFromTo( User $oldUser, User $newUser ) {
803 $dbw = wfGetDB( DB_PRIMARY );
804
805 // Update the non-duplicate rows, we'll just delete
806 // the duplicate ones later
807 foreach ( self::$userMergeTables as $table => $field ) {
808 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
809 $dbw->update(
810 $table,
811 [ $field => $newUser->getId() ],
812 [ $field => $oldUser->getId() ],
813 __METHOD__,
814 [ 'IGNORE' ]
815 );
816 }
817 }
818 }
819
826 public static function onDeleteAccount( User $oldUser ) {
827 $dbw = wfGetDB( DB_PRIMARY );
828
829 // Delete any remaining rows that didn't get merged
830 foreach ( self::$userMergeTables as $table => $field ) {
831 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
832 $dbw->delete(
833 $table,
834 [ $field => $oldUser->getId() ],
835 __METHOD__
836 );
837 }
838 }
839 }
840
850 public static function onAbortEmailNotificationReview(
851 User $editor,
852 Title $title,
853 RecentChange $rc
854 ) {
855 if ( $rc->getAttribute( 'rc_log_type' ) === 'translationreview' ) {
856 return false;
857 }
858 }
859
869 public static function onTitleIsAlwaysKnown( Title $target, &$isKnown ) {
870 if ( !$target->inNamespace( NS_SPECIAL ) ) {
871 return true;
872 }
873
874 [ $name, $subpage ] = MediaWikiServices::getInstance()
875 ->getSpecialPageFactory()->resolveAlias( $target->getDBkey() );
876 if ( $name !== 'MyLanguage' ) {
877 return true;
878 }
879
880 if ( (string)$subpage === '' ) {
881 return true;
882 }
883
884 $realTarget = Title::newFromText( $subpage );
885 if ( !$realTarget || !$realTarget->exists() ) {
886 $isKnown = false;
887
888 return false;
889 }
890
891 return true;
892 }
893
898 public static function setupTranslateParserFunction( Parser $parser ) {
899 $parser->setFunctionHook( 'translation', 'TranslateHooks::translateRenderParserFunction' );
900 }
901
906 public static function translateRenderParserFunction( Parser $parser ) {
907 $pageTitle = $parser->getTitle();
908
909 $handle = new MessageHandle( $pageTitle );
910 $code = $handle->getCode();
911 if ( Language::isKnownLanguageTag( $code ) ) {
912 return '/' . $code;
913 }
914 return '';
915 }
916
927 public static function validateMessage( IContextSource $context, Content $content,
928 Status $status, $summary, User $user
929 ) {
930 if ( !$content instanceof TextContent ) {
931 // Not interested
932 return true;
933 }
934
935 $text = $content->getText();
936 $title = $context->getTitle();
937 $handle = new MessageHandle( $title );
938
939 if ( !$handle->isValid() ) {
940 return true;
941 }
942
943 // Don't bother validating if FuzzyBot or translation admin are saving.
944 if ( $user->isAllowed( 'translate-manage' ) || $user->equals( FuzzyBot::getUser() ) ) {
945 return true;
946 }
947
948 // Check the namespace, and perform validations for all messages excluding documentation.
949 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
950 $group = $handle->getGroup();
951
952 if ( is_callable( [ $group, 'getMessageContent' ] ) ) {
953 // @phan-suppress-next-line PhanUndeclaredMethod
954 $definition = $group->getMessageContent( $handle );
955 } else {
956 $definition = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
957 }
958
959 $message = new FatMessage( $handle->getKey(), $definition );
960 $message->setTranslation( $text );
961
962 $messageValidator = $group->getValidator();
963 if ( !$messageValidator ) {
964 return true;
965 }
966
967 $validationResponse = $messageValidator->validateMessage( $message, $handle->getCode() );
968 if ( $validationResponse->hasErrors() ) {
969 $status->fatal( new ApiRawMessage(
970 $context->msg( 'translate-syntax-error' )->parse(),
971 'translate-validation-failed',
972 [
973 'validation' => [
974 'errors' => $validationResponse->getDescriptiveErrors( $context ),
975 'warnings' => $validationResponse->getDescriptiveWarnings( $context )
976 ]
977 ]
978 ) );
979 return false;
980 }
981 }
982
983 return true;
984 }
985
987 public function onRevisionRecordInserted( $revisionRecord ): void {
988 $parentId = $revisionRecord->getParentId();
989 if ( $parentId === 0 || $parentId === null ) {
990 // No parent, bail out.
991 return;
992 }
993
994 $prevRev = $this->revisionLookup->getRevisionById( $parentId );
995 if ( !$prevRev || !$revisionRecord->hasSameContent( $prevRev ) ) {
996 // Not a null revision, bail out.
997 return;
998 }
999
1000 // List of tags that should be copied over when updating
1001 // tp:tag and tp:mark handling is in Hooks::updateTranstagOnNullRevisions.
1002 $tagsToCopy = [ RevTagStore::FUZZY_TAG, RevTagStore::TRANSVER_PROP ];
1003
1004 $db = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
1005 $db->insertSelect(
1006 'revtag',
1007 'revtag',
1008 [
1009 'rt_type' => 'rt_type',
1010 'rt_page' => 'rt_page',
1011 'rt_revision' => $revisionRecord->getId(),
1012 'rt_value' => 'rt_value',
1013
1014 ],
1015 [
1016 'rt_type' => $tagsToCopy,
1017 'rt_revision' => $parentId,
1018 ],
1019 __METHOD__
1020 );
1021 }
1022}
Message object where you can directly set the translation.
Definition Message.php:189
Class to manage revision tags for translatable bundles.
Special page which enables deleting translations of translatable bundles and translation pages.
Contains code for Special:PageMigration to migrate to page transation.
A special page for marking revisions of pages for translation.
Job for updating translation pages when translation or template changes.
Job for updating translation units and translation pages when a translatable page is marked for trans...
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
WebAPI module for storing translations for users who are in a sandbox.
Class for pointing to messages, like Title class is for titles.
Hooks for Translate extension.
static addConfig(array &$vars, OutputPage $out)
Hook: MakeGlobalVariablesScript.
static setupTranslate()
Do late setup that depends on configuration.
static parserTestTables(array &$tables)
Hook: ParserTestTables.
static translateMessageDocumentationLanguage(array &$names, $code)
Hook: LanguageGetTranslatedLanguageNames Hook: TranslateSupportedLanguages.
static onUserGetReservedNames(array &$names)
Hook: UserGetReservedNames Prevents anyone from registering or logging in as FuzzyBot.
static onPageContentLanguage(Title $title, &$pageLang)
Hook: PageContentLanguage Set the correct page content language for translation units.
static setupParserHooks(Parser $parser)
Hook: ParserFirstCallInit Registers <languages> tag with the parser.
static schemaUpdates(DatabaseUpdater $updater)
Hook: LoadExtensionSchemaUpdates.
static searchProfileSetupEngine(SpecialSearch $search, $profile, SearchEngine $engine)
Hook: SpecialSearchSetupEngine.
static onAbuseFilterBuilder(array &$builderValues)
Register AbuseFilter variables provided by Translate.
onRevisionRecordInserted( $revisionRecord)
@inheritDoc
static onTitleIsAlwaysKnown(Title $target, &$isKnown)
Hook: TitleIsAlwaysKnown Make Special:MyLanguage links red if the target page doesn't exist.
static onDeleteAccount(User $oldUser)
Hook: DeleteAccount For UserMerge extension.
static onAdminLinks(ALTree $tree)
Hook: AdminLinks.
static setupTranslateParserFunction(Parser $parser)
Hook: ParserFirstCallInit.
static searchProfile(array &$profiles)
Hook: SpecialSearchProfiles.
static translateRenderParserFunction(Parser $parser)
static searchProfileForm(SpecialSearch $search, &$form, $profile, $term, array $opts)
Hook: SpecialSearchProfileForm.
static onMergeAccountFromTo(User $oldUser, User $newUser)
Hook: MergeAccountFromTo For UserMerge extension.
static preventCategorization(Parser $parser, &$html)
Hook: ParserAfterTidy.
static onAbuseFilterAlterVariables(&$vars, Title $title, User $user)
Used for setting an AbuseFilter variable.
static onAbuseFilterComputeVariable( $method, $vars, $parameters, &$result)
Computes the translate_source_text and translate_target_language AbuseFilter variables.
static showFakeCategories(OutputPage $outputPage, ParserOutput $parserOutput)
Hook: OutputPageParserOutput.
static validateMessage(IContextSource $context, Content $content, Status $status, $summary, User $user)
Runs the configured validator to ensure that the message meets the required criteria.
static onAbortEmailNotificationReview(User $editor, Title $title, RecentChange $rc)
Hook: AbortEmailNotification.
Interface for TTMServer that can act as backend for translation search.