Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TranslateHooks.php
Go to the documentation of this file.
1<?php
10use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
11use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
12use MediaWiki\Config\ServiceOptions;
13use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
34use MediaWiki\MediaWikiServices;
35use MediaWiki\Revision\Hook\RevisionRecordInsertedHook;
36use MediaWiki\Revision\RevisionLookup;
37use MediaWiki\Settings\SettingsBuilder;
38use Wikimedia\Rdbms\ILoadBalancer;
39
48class TranslateHooks implements RevisionRecordInsertedHook, ListDefinedTagsHook, ChangeTagsListActiveHook {
55 private static $userMergeTables = [
56 'translate_stash' => 'ts_user',
57 'translate_reviews' => 'trr_user',
58 ];
60 private $revisionLookup;
62 private $loadBalancer;
64 private $config;
65
66 public function __construct(
67 RevisionLookup $revisionLookup,
68 ILoadBalancer $loadBalancer,
69 Config $config
70 ) {
71 $this->revisionLookup = $revisionLookup;
72 $this->loadBalancer = $loadBalancer;
73 $this->config = $config;
74 }
75
79 public static function setupTranslate() {
80 global $wgTranslateYamlLibrary;
81 $hooks = [];
82
83 /*
84 * Text that will be shown in translations if the translation is outdated.
85 * Must be something that does not conflict with actual content.
86 */
87 if ( !defined( 'TRANSLATE_FUZZY' ) ) {
88 define( 'TRANSLATE_FUZZY', '!!FUZZY!!' );
89 }
90
91 if ( $wgTranslateYamlLibrary === null ) {
92 $wgTranslateYamlLibrary = function_exists( 'yaml_parse' ) ? 'phpyaml' : 'spyc';
93 }
94
95 $hooks['PageSaveComplete'][] = [ TranslateEditAddons::class, 'onSaveComplete' ];
96
97 // Page translation setup check and init if enabled.
98 global $wgEnablePageTranslation;
99 if ( $wgEnablePageTranslation ) {
100 // Special page and the right to use it
101 global $wgSpecialPages, $wgAvailableRights;
102 $wgSpecialPages['PageTranslation'] = [
103 'class' => PageTranslationSpecialPage::class,
104 'services' => [
105 'LanguageNameUtils',
106 'LanguageFactory',
107 'Translate:TranslationUnitStoreFactory',
108 'Translate:TranslatablePageParser',
109 'LinkBatchFactory',
110 'JobQueueGroup',
111 'DBLoadBalancer',
112 'Translate:MessageIndex'
113 ]
114 ];
115 $wgSpecialPages['PageTranslationDeletePage'] = [
116 'class' => DeleteTranslatableBundleSpecialPage::class,
117 'services' => [
118 'MainObjectStash',
119 'PermissionManager',
120 'Translate:TranslatableBundleFactory',
121 'Translate:SubpageListBuilder',
122 'JobQueueGroup',
123 ]
124 ];
125
126 // right-pagetranslation action-pagetranslation
127 $wgAvailableRights[] = 'pagetranslation';
128
129 $wgSpecialPages['PageMigration'] = MigrateTranslatablePageSpecialPage::class;
130 $wgSpecialPages['PagePreparation'] = PrepareTranslatablePageSpecialPage::class;
131
132 global $wgActionFilteredLogs, $wgLogActionsHandlers, $wgLogTypes;
133
134 // log-description-pagetranslation log-name-pagetranslation logentry-pagetranslation-mark
135 // logentry-pagetranslation-unmark logentry-pagetranslation-moveok
136 // logentry-pagetranslation-movenok logentry-pagetranslation-deletefok
137 // logentry-pagetranslation-deletefnok logentry-pagetranslation-deletelok
138 // logentry-pagetranslation-deletelnok logentry-pagetranslation-encourage
139 // logentry-pagetranslation-discourage logentry-pagetranslation-prioritylanguages
140 // logentry-pagetranslation-associate logentry-pagetranslation-dissociate
141 $wgLogTypes[] = 'pagetranslation';
142 $wgLogActionsHandlers['pagetranslation/mark'] = TranslatableBundleLogFormatter::class;
143 $wgLogActionsHandlers['pagetranslation/unmark'] = TranslatableBundleLogFormatter::class;
144 $wgLogActionsHandlers['pagetranslation/moveok'] = TranslatableBundleLogFormatter::class;
145 $wgLogActionsHandlers['pagetranslation/movenok'] = TranslatableBundleLogFormatter::class;
146 $wgLogActionsHandlers['pagetranslation/deletelok'] = TranslatableBundleLogFormatter::class;
147 $wgLogActionsHandlers['pagetranslation/deletefok'] = TranslatableBundleLogFormatter::class;
148 $wgLogActionsHandlers['pagetranslation/deletelnok'] = TranslatableBundleLogFormatter::class;
149 $wgLogActionsHandlers['pagetranslation/deletefnok'] = TranslatableBundleLogFormatter::class;
150 $wgLogActionsHandlers['pagetranslation/encourage'] = TranslatableBundleLogFormatter::class;
151 $wgLogActionsHandlers['pagetranslation/discourage'] = TranslatableBundleLogFormatter::class;
152 $wgLogActionsHandlers['pagetranslation/prioritylanguages'] = TranslatableBundleLogFormatter::class;
153 $wgLogActionsHandlers['pagetranslation/associate'] = TranslatableBundleLogFormatter::class;
154 $wgLogActionsHandlers['pagetranslation/dissociate'] = TranslatableBundleLogFormatter::class;
155 $wgActionFilteredLogs['pagetranslation'] = [
156 'mark' => [ 'mark' ],
157 'unmark' => [ 'unmark' ],
158 'move' => [ 'moveok', 'movenok' ],
159 'delete' => [ 'deletefok', 'deletefnok', 'deletelok', 'deletelnok' ],
160 'encourage' => [ 'encourage' ],
161 'discourage' => [ 'discourage' ],
162 'prioritylanguages' => [ 'prioritylanguages' ],
163 'aggregategroups' => [ 'associate', 'dissociate' ],
164 ];
165
166 $wgLogTypes[] = 'messagebundle';
167 $wgLogActionsHandlers['messagebundle/moveok'] = TranslatableBundleLogFormatter::class;
168 $wgLogActionsHandlers['messagebundle/movenok'] = TranslatableBundleLogFormatter::class;
169 $wgLogActionsHandlers['messagebundle/deletefok'] = TranslatableBundleLogFormatter::class;
170 $wgLogActionsHandlers['messagebundle/deletefnok'] = TranslatableBundleLogFormatter::class;
171 $wgActionFilteredLogs['messagebundle'] = [
172 'move' => [ 'moveok', 'movenok' ],
173 'delete' => [ 'deletefok', 'deletefnok' ],
174 ];
175
176 global $wgJobClasses;
177 $wgJobClasses['RenderTranslationPageJob'] = RenderTranslationPageJob::class;
178 // Remove after MLEB 2022.10 release
179 $wgJobClasses['TranslateRenderJob'] = RenderTranslationPageJob::class;
180 // Remove after MLEB 2022.07 release
181 $wgJobClasses['TranslatableBundleMoveJob'] = MoveTranslatableBundleJob::class;
182 $wgJobClasses['MoveTranslatableBundleJob'] = MoveTranslatableBundleJob::class;
183 // Remove after MLEB 2022.07 release
184 $wgJobClasses['TranslatableBundleDeleteJob'] = DeleteTranslatableBundleJob::class;
185 $wgJobClasses['DeleteTranslatableBundleJob'] = DeleteTranslatableBundleJob::class;
186
187 $wgJobClasses['UpdateTranslatablePageJob'] = UpdateTranslatablePageJob::class;
188 // Remove after MLEB 2022.10 release
189 $wgJobClasses['TranslationsUpdateJob'] = UpdateTranslatablePageJob::class;
190
191 // Namespaces
192 global $wgNamespacesWithSubpages, $wgNamespaceProtection;
193 global $wgTranslateMessageNamespaces;
194
195 $wgNamespacesWithSubpages[NS_TRANSLATIONS] = true;
196 $wgNamespacesWithSubpages[NS_TRANSLATIONS_TALK] = true;
197
198 // Standard protection and register it for filtering
199 $wgNamespaceProtection[NS_TRANSLATIONS] = [ 'translate' ];
200 $wgTranslateMessageNamespaces[] = NS_TRANSLATIONS;
201
203
205 $hooks['BeforePageDisplay'][] = [ Hooks::class, 'onBeforePageDisplay' ];
206
207 // Disable VE
208 $hooks['VisualEditorBeforeEditor'][] = [ Hooks::class, 'onVisualEditorBeforeEditor' ];
209
210 // Check syntax for <translate>
211 $hooks['MultiContentSave'][] = [ Hooks::class, 'tpSyntaxCheck' ];
212 $hooks['EditFilterMergedContent'][] =
213 [ Hooks::class, 'tpSyntaxCheckForEditContent' ];
214
215 // Add transtag to page props for discovery
216 $hooks['PageSaveComplete'][] = [ Hooks::class, 'addTranstagAfterSave' ];
217
218 $hooks['RevisionRecordInserted'][] = [ Hooks::class, 'updateTranstagOnNullRevisions' ];
219
220 // Register different ways to show language links
221 $hooks['ParserFirstCallInit'][] = [ self::class, 'setupParserHooks' ];
222 $hooks['LanguageLinks'][] = [ Hooks::class, 'addLanguageLinks' ];
223 $hooks['SkinTemplateGetLanguageLink'][] = [ Hooks::class, 'formatLanguageLink' ];
224
225 // Strip <translate> tags etc. from source pages when rendering
226 $hooks['ParserBeforeInternalParse'][] = [ Hooks::class, 'renderTagPage' ];
227 // Strip <translate> tags etc. from source pages when preprocessing
228 $hooks['ParserBeforePreprocess'][] = [ Hooks::class, 'preprocessTagPage' ];
229 $hooks['ParserOutputPostCacheTransform'][] =
230 [ Hooks::class, 'onParserOutputPostCacheTransform' ];
231
232 $hooks['BeforeParserFetchTemplateRevisionRecord'][] =
233 [ Hooks::class, 'fetchTranslatableTemplateAndTitle' ];
234
235 // Set the page content language
236 $hooks['PageContentLanguage'][] = [ Hooks::class, 'onPageContentLanguage' ];
237
238 // Prevent editing of certain pages in translations namespace
239 $hooks['getUserPermissionsErrorsExpensive'][] =
240 [ Hooks::class, 'onGetUserPermissionsErrorsExpensive' ];
241 // Prevent editing of translation pages directly
242 $hooks['getUserPermissionsErrorsExpensive'][] =
243 [ Hooks::class, 'preventDirectEditing' ];
244
245 // Our custom header for translation pages
246 $hooks['ArticleViewHeader'][] = [ Hooks::class, 'translatablePageHeader' ];
247
248 // Edit notice shown on translatable pages
249 $hooks['TitleGetEditNotices'][] = [ Hooks::class, 'onTitleGetEditNotices' ];
250
251 // Custom move page that can move all the associated pages too
252 $hooks['SpecialPage_initList'][] = [ Hooks::class, 'replaceMovePage' ];
253 // Locking during page moves
254 $hooks['getUserPermissionsErrorsExpensive'][] =
255 [ Hooks::class, 'lockedPagesCheck' ];
256 // Disable action=delete
257 $hooks['ArticleConfirmDelete'][] = [ Hooks::class, 'disableDelete' ];
258
259 // Replace subpage logic behavior
260 $hooks['SkinSubPageSubtitle'][] = [ Hooks::class, 'replaceSubtitle' ];
261
262 // Replaced edit tab with translation tab for translation pages
263 $hooks['SkinTemplateNavigation::Universal'][] = [ Hooks::class, 'translateTab' ];
264
265 // Update translated page when translation unit is moved
266 $hooks['PageMoveComplete'][] = [ Hooks::class, 'onMovePageTranslationUnits' ];
267
268 // Update translated page when translation unit is deleted
269 $hooks['ArticleDeleteComplete'][] = [ Hooks::class, 'onDeleteTranslationUnit' ];
270 }
271
272 global $wgTranslateUseSandbox;
273 if ( $wgTranslateUseSandbox ) {
274 global $wgSpecialPages, $wgAvailableRights, $wgDefaultUserOptions;
275
276 $wgSpecialPages['ManageTranslatorSandbox'] = [
277 'class' => ManageTranslatorSandboxSpecialPage::class,
278 'services' => [
279 'Translate:TranslationStashReader',
280 'UserOptionsLookup'
281 ],
282 'args' => [
283 static function () {
284 return new ServiceOptions(
285 ManageTranslatorSandboxSpecialPage::CONSTRUCTOR_OPTIONS,
286 MediaWikiServices::getInstance()->getMainConfig()
287 );
288 }
289 ]
290 ];
291 $wgSpecialPages['TranslationStash'] = [
292 'class' => TranslationStashSpecialPage::class,
293 'services' => [
294 'LanguageNameUtils',
295 'Translate:TranslationStashReader',
296 'UserOptionsLookup',
297 'LanguageFactory',
298 ],
299 'args' => [
300 static function () {
301 return new ServiceOptions(
302 TranslationStashSpecialPage::CONSTRUCTOR_OPTIONS,
303 MediaWikiServices::getInstance()->getMainConfig()
304 );
305 }
306 ]
307 ];
308 $wgDefaultUserOptions['translate-sandbox'] = '';
309 // right-translate-sandboxmanage action-translate-sandboxmanage
310 $wgAvailableRights[] = 'translate-sandboxmanage';
311
312 $hooks['GetPreferences'][] = [ TranslateSandbox::class, 'onGetPreferences' ];
313 $hooks['UserGetRights'][] = [ TranslateSandbox::class, 'enforcePermissions' ];
314 $hooks['ApiCheckCanExecute'][] = [ TranslateSandbox::class, 'onApiCheckCanExecute' ];
315
316 global $wgLogTypes, $wgLogActionsHandlers;
317 // log-name-translatorsandbox log-description-translatorsandbox
318 $wgLogTypes[] = 'translatorsandbox';
319 // logentry-translatorsandbox-promoted logentry-translatorsandbox-rejected
320 $wgLogActionsHandlers['translatorsandbox/promoted'] = 'TranslateLogFormatter';
321 $wgLogActionsHandlers['translatorsandbox/rejected'] = 'TranslateLogFormatter';
322
323 // This is no longer used for new entries since 2016.07.
324 // logentry-newusers-tsbpromoted
325 $wgLogActionsHandlers['newusers/tsbpromoted'] = 'LogFormatter';
326
327 global $wgJobClasses;
328 $wgJobClasses['TranslateSandboxEmailJob'] = 'TranslateSandboxEmailJob';
329
330 global $wgAPIModules;
331 $wgAPIModules['translationstash'] = [
332 'class' => TranslationStashActionApi::class,
333 'services' => [
334 'DBLoadBalancer',
335 'UserFactory'
336 ]
337 ];
338 $wgAPIModules['translatesandbox'] = [
339 'class' => TranslatorSandboxActionApi::class,
340 'services' => [
341 'UserFactory',
342 'UserNameUtils',
343 'UserOptionsManager',
344 'WikiPageFactory',
345 'UserOptionsLookup'
346 ],
347 'args' => [
348 static function () {
349 return new ServiceOptions(
350 TranslatorSandboxActionApi::CONSTRUCTOR_OPTIONS,
351 MediaWikiServices::getInstance()->getMainConfig()
352 );
353 }
354 ]
355 ];
356 }
357
358 global $wgNamespaceRobotPolicies;
359 $wgNamespaceRobotPolicies[NS_TRANSLATIONS] = 'noindex';
360
361 // If no service has been configured, we use a built-in fallback.
362 global $wgTranslateTranslationDefaultService,
363 $wgTranslateTranslationServices;
364 if ( $wgTranslateTranslationDefaultService === true ) {
365 $wgTranslateTranslationDefaultService = 'TTMServer';
366 if ( !isset( $wgTranslateTranslationServices['TTMServer'] ) ) {
367 $wgTranslateTranslationServices['TTMServer'] = [
368 'database' => false, // Passed to wfGetDB
369 'cutoff' => 0.75,
370 'type' => 'ttmserver',
371 'public' => false,
372 ];
373 }
374 }
375
376 $hooks['SidebarBeforeOutput'][] = [ TranslateToolbox::class, 'toolboxAllTranslations' ];
377
378 static::registerHookHandlers( $hooks );
379 }
380
381 private static function registerHookHandlers( array $hooks ) {
382 if ( defined( 'MW_PHPUNIT_TEST' ) && MediaWikiServices::hasInstance() ) {
383 // When called from a test case's setUp() method,
384 // we can use HookContainer, but we cannot use SettingsBuilder.
385 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
386 foreach ( $hooks as $name => $handlers ) {
387 foreach ( $handlers as $h ) {
388 $hookContainer->register( $name, $h );
389 }
390 }
391 } elseif ( method_exists( SettingsBuilder::class, 'registerHookHandlers' ) ) {
392 // Since 1.40: Use SettingsBuilder to register hooks during initialization.
393 // HookContainer is not available at this time.
394 $settingsBuilder = SettingsBuilder::getInstance();
395 $settingsBuilder->registerHookHandlers( $hooks );
396 } else {
397 // For MW < 1.40: Directly manipulate $wgHooks during initialization.
398 foreach ( $hooks as $name => $handlers ) {
399 $GLOBALS['wgHooks'][$name] = array_merge(
400 $GLOBALS['wgHooks'][$name],
401 $handlers
402 );
403 }
404 }
405 }
406
413 public static function onUserGetReservedNames( array &$names ) {
414 $names[] = FuzzyBot::getName();
415 $names[] = TranslateUserManager::getName();
416 }
417
425 public static function onAbuseFilterAlterVariables(
426 &$vars, Title $title, User $user
427 ) {
428 $handle = new MessageHandle( $title );
429
430 // Only set this variable if we are in a proper namespace to avoid
431 // unnecessary overhead in non-translation pages
432 if ( $handle->isMessageNamespace() ) {
433 $vars->setLazyLoadVar(
434 'translate_source_text',
435 'translate-get-source',
436 [ 'handle' => $handle ]
437 );
438 $vars->setLazyLoadVar(
439 'translate_target_language',
440 'translate-get-target-language',
441 [ 'handle' => $handle ]
442 );
443 }
444 }
445
454 public static function onAbuseFilterComputeVariable( $method, $vars, $parameters, &$result ) {
455 if ( $method !== 'translate-get-source' && $method !== 'translate-get-target-language' ) {
456 return true;
457 }
458
459 $handle = $parameters['handle'];
460 $value = '';
461 if ( $handle->isValid() ) {
462 if ( $method === 'translate-get-source' ) {
463 $group = $handle->getGroup();
464 $value = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
465 } else {
466 $value = $handle->getCode();
467 }
468 }
469
470 $result = $value;
471
472 return false;
473 }
474
479 public static function onAbuseFilterBuilder( array &$builderValues ) {
480 // Uses: 'abusefilter-edit-builder-vars-translate-source-text'
481 // and 'abusefilter-edit-builder-vars-translate-target-language'
482 $builderValues['vars']['translate_source_text'] = 'translate-source-text';
483 $builderValues['vars']['translate_target_language'] = 'translate-target-language';
484 }
485
492 public static function setupParserHooks( Parser $parser ) {
493 // For nice language list in-page
494 $parser->setHook( 'languages', [ Hooks::class, 'languages' ] );
495 }
496
502 public static function schemaUpdates( DatabaseUpdater $updater ) {
503 $dir = __DIR__ . '/sql';
504 $dbType = $updater->getDB()->getType();
505
506 if ( $dbType === 'mysql' || $dbType === 'sqlite' ) {
507 $updater->addExtensionTable(
508 'translate_sections',
509 "{$dir}/{$dbType}/translate_sections.sql"
510 );
511 $updater->addExtensionTable(
512 'revtag',
513 "{$dir}/{$dbType}/revtag.sql"
514 );
515 $updater->addExtensionTable(
516 'translate_groupstats',
517 "{$dir}/{$dbType}/translate_groupstats.sql"
518 );
519 $updater->addExtensionTable(
520 'translate_reviews',
521 "{$dir}/{$dbType}/translate_reviews.sql"
522 );
523 $updater->addExtensionTable(
524 'translate_groupreviews',
525 "{$dir}/{$dbType}/translate_groupreviews.sql"
526 );
527 $updater->addExtensionTable(
528 'translate_tms',
529 "{$dir}/{$dbType}/translate_tm.sql"
530 );
531 $updater->addExtensionTable(
532 'translate_metadata',
533 "{$dir}/{$dbType}/translate_metadata.sql"
534 );
535 $updater->addExtensionTable(
536 'translate_messageindex',
537 "{$dir}/{$dbType}/translate_messageindex.sql"
538 );
539 $updater->addExtensionTable(
540 'translate_stash',
541 "{$dir}/{$dbType}/translate_stash.sql"
542 );
543 $updater->addExtensionTable(
544 'translate_translatable_bundles',
545 "{$dir}/{$dbType}/translate_translatable_bundles.sql"
546 );
547
548 // 1.32 - This also adds a PRIMARY KEY
549 $updater->addExtensionUpdate( [
550 'renameIndex',
551 'translate_reviews',
552 'trr_user_page_revision',
553 'PRIMARY',
554 false,
555 "$dir/translate_reviews-patch-01-primary-key.sql",
556 true
557 ] );
558
559 $updater->addExtensionTable(
560 'translate_cache',
561 "{$dir}/{$dbType}/translate_cache.sql"
562 );
563
564 if ( $dbType === 'mysql' ) {
565 // 1.38
566 $updater->modifyExtensionField(
567 'translate_cache',
568 'tc_key',
569 "{$dir}/{$dbType}/translate_cache-alter-varbinary.sql"
570 );
571 }
572 } elseif ( $dbType === 'postgres' ) {
573 $updater->addExtensionTable(
574 'translate_sections',
575 "{$dir}/{$dbType}/tables-generated.sql"
576 );
577 $updater->addExtensionUpdate( [
578 'changeField', 'translate_cache', 'tc_exptime', 'TIMESTAMPTZ', 'th_timestamp::timestamp with time zone'
579 ] );
580 }
581
582 // 1.39
583 $updater->dropExtensionIndex(
584 'translate_messageindex',
585 'tmi_key',
586 "{$dir}/{$dbType}/patch-translate_messageindex-unique-to-pk.sql"
587 );
588 $updater->dropExtensionIndex(
589 'translate_tmt',
590 'tms_sid_lang',
591 "{$dir}/{$dbType}/patch-translate_tmt-unique-to-pk.sql"
592 );
593 $updater->dropExtensionIndex(
594 'revtag',
595 'rt_type_page_revision',
596 "{$dir}/{$dbType}/patch-revtag-unique-to-pk.sql"
597 );
598
599 $updater->addPostDatabaseUpdateMaintenance( SyncTranslatableBundleStatusMaintenanceScript::class );
600 }
601
606 public static function parserTestTables( array &$tables ) {
607 $tables[] = 'revtag';
608 $tables[] = 'translate_groupstats';
609 $tables[] = 'translate_messageindex';
610 $tables[] = 'translate_stash';
611 }
612
620 public static function onPageContentLanguage( Title $title, &$pageLang ) {
621 $handle = new MessageHandle( $title );
622 if ( $handle->isMessageNamespace() ) {
623 $pageLang = $handle->getEffectiveLanguage();
624 }
625 }
626
633 public static function translateMessageDocumentationLanguage( array &$names, $code ) {
634 global $wgTranslateDocumentationLanguageCode;
635 if ( $wgTranslateDocumentationLanguageCode ) {
636 // Special case the autonyms
637 if (
638 $wgTranslateDocumentationLanguageCode === $code ||
639 $code === null
640 ) {
641 $code = 'en';
642 }
643
644 $names[$wgTranslateDocumentationLanguageCode] =
645 wfMessage( 'translate-documentation-language' )->inLanguage( $code )->plain();
646 }
647 }
648
653 public static function searchProfile( array &$profiles ) {
654 global $wgTranslateMessageNamespaces;
655 $insert = [];
656 $insert['translation'] = [
657 'message' => 'translate-searchprofile',
658 'tooltip' => 'translate-searchprofile-tooltip',
659 'namespaces' => $wgTranslateMessageNamespaces,
660 ];
661
662 // Insert translations before 'all'
663 $index = array_search( 'all', array_keys( $profiles ) );
664
665 // Or just at the end if all is not found
666 if ( $index === false ) {
667 wfWarn( '"all" not found in search profiles' );
668 $index = count( $profiles );
669 }
670
671 $profiles = array_merge(
672 array_slice( $profiles, 0, $index ),
673 $insert,
674 array_slice( $profiles, $index )
675 );
676 }
677
687 public static function searchProfileForm(
688 SpecialSearch $search,
689 &$form,
690 $profile,
691 $term,
692 array $opts
693 ) {
694 if ( $profile !== 'translation' ) {
695 return true;
696 }
697
698 if ( TTMServer::primary() instanceof SearchableTTMServer ) {
699 $href = SpecialPage::getTitleFor( 'SearchTranslations' )
700 ->getFullUrl( [ 'query' => $term ] );
701 $form = Html::successBox(
702 $search->msg( 'translate-searchprofile-note', $href )->parse(),
703 'plainlinks'
704 );
705
706 return false;
707 }
708
709 if ( !$search->getSearchEngine()->supports( 'title-suffix-filter' ) ) {
710 return false;
711 }
712
713 $hidden = '';
714 foreach ( $opts as $key => $value ) {
715 $hidden .= Html::hidden( $key, $value );
716 }
717
718 $context = $search->getContext();
719 $code = $context->getLanguage()->getCode();
720 $selected = $context->getRequest()->getVal( 'languagefilter' );
721
722 $languages = Utilities::getLanguageNames( $code );
723 ksort( $languages );
724
725 $selector = new XmlSelect( 'languagefilter', 'languagefilter' );
726 $selector->setDefault( $selected );
727 $selector->addOption( wfMessage( 'translate-search-nofilter' )->text(), '-' );
728 foreach ( $languages as $code => $name ) {
729 $selector->addOption( "$code - $name", $code );
730 }
731
732 $selector = $selector->getHTML();
733
734 $label = Xml::label(
735 wfMessage( 'translate-search-languagefilter' )->text(),
736 'languagefilter'
737 ) . '&#160;';
738 $params = [ 'id' => 'mw-searchoptions' ];
739
740 $form = Xml::fieldset( false, false, $params ) .
741 $hidden . $label . $selector .
742 Html::closeElement( 'fieldset' );
743
744 return false;
745 }
746
753 public static function searchProfileSetupEngine(
754 SpecialSearch $search,
755 $profile,
756 SearchEngine $engine
757 ) {
758 if ( $profile !== 'translation' ) {
759 return;
760 }
761
762 $context = $search->getContext();
763 $selected = $context->getRequest()->getVal( 'languagefilter' );
764 if ( $selected !== '-' && $selected ) {
765 $engine->setFeatureData( 'title-suffix-filter', "/$selected" );
766 $search->setExtraParam( 'languagefilter', $selected );
767 }
768 }
769
775 public static function preventCategorization( Parser $parser, &$html ) {
776 $handle = new MessageHandle( $parser->getTitle() );
777 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
778 $parserOutput = $parser->getOutput();
779 $parserOutput->setExtensionData( 'translate-fake-categories',
780 $parserOutput->getCategories() );
781 $parserOutput->setCategories( [] );
782 }
783 }
784
790 public static function showFakeCategories( OutputPage $outputPage, ParserOutput $parserOutput ) {
791 $fakeCategories = $parserOutput->getExtensionData( 'translate-fake-categories' );
792 if ( $fakeCategories ) {
793 $outputPage->setCategoryLinks( $fakeCategories );
794 }
795 }
796
805 public static function addConfig( array &$vars, OutputPage $out ) {
806 $title = $out->getTitle();
807 [ $alias, ] = MediaWikiServices::getInstance()
808 ->getSpecialPageFactory()->resolveAlias( $title->getText() );
809
810 if ( $title->isSpecialPage()
811 && ( $alias === 'Translate'
812 || $alias === 'TranslationStash'
813 || $alias === 'SearchTranslations' )
814 ) {
815 global $wgTranslateDocumentationLanguageCode, $wgTranslatePermissionUrl,
816 $wgTranslateUseSandbox;
817 $vars['TranslateRight'] = $out->getUser()->isAllowed( 'translate' );
818 $vars['TranslateMessageReviewRight'] =
819 $out->getUser()->isAllowed( 'translate-messagereview' );
820 $vars['DeleteRight'] = $out->getUser()->isAllowed( 'delete' );
821 $vars['TranslateManageRight'] = $out->getUser()->isAllowed( 'translate-manage' );
822 $vars['wgTranslateDocumentationLanguageCode'] = $wgTranslateDocumentationLanguageCode;
823 $vars['wgTranslatePermissionUrl'] = $wgTranslatePermissionUrl;
824 $vars['wgTranslateUseSandbox'] = $wgTranslateUseSandbox;
825 }
826 }
827
832 public static function onAdminLinks( ALTree $tree ) {
833 global $wgTranslateUseSandbox;
834
835 if ( $wgTranslateUseSandbox ) {
836 $sectionLabel = wfMessage( 'adminlinks_users' )->text();
837 $row = $tree->getSection( $sectionLabel )->getRow( 'main' );
838 $row->addItem( ALItem::newFromSpecialPage( 'TranslateSandbox' ) );
839 }
840 }
841
849 public static function onMergeAccountFromTo( User $oldUser, User $newUser ) {
850 $dbw = wfGetDB( DB_PRIMARY );
851
852 // Update the non-duplicate rows, we'll just delete
853 // the duplicate ones later
854 foreach ( self::$userMergeTables as $table => $field ) {
855 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
856 $dbw->update(
857 $table,
858 [ $field => $newUser->getId() ],
859 [ $field => $oldUser->getId() ],
860 __METHOD__,
861 [ 'IGNORE' ]
862 );
863 }
864 }
865 }
866
873 public static function onDeleteAccount( User $oldUser ) {
874 $dbw = wfGetDB( DB_PRIMARY );
875
876 // Delete any remaining rows that didn't get merged
877 foreach ( self::$userMergeTables as $table => $field ) {
878 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
879 $dbw->delete(
880 $table,
881 [ $field => $oldUser->getId() ],
882 __METHOD__
883 );
884 }
885 }
886 }
887
897 public static function onAbortEmailNotificationReview(
898 User $editor,
899 Title $title,
900 RecentChange $rc
901 ) {
902 if ( $rc->getAttribute( 'rc_log_type' ) === 'translationreview' ) {
903 return false;
904 }
905 }
906
916 public static function onTitleIsAlwaysKnown( Title $target, &$isKnown ) {
917 if ( !$target->inNamespace( NS_SPECIAL ) ) {
918 return true;
919 }
920
921 [ $name, $subpage ] = MediaWikiServices::getInstance()
922 ->getSpecialPageFactory()->resolveAlias( $target->getDBkey() );
923 if ( $name !== 'MyLanguage' ) {
924 return true;
925 }
926
927 if ( (string)$subpage === '' ) {
928 return true;
929 }
930
931 $realTarget = Title::newFromText( $subpage );
932 if ( !$realTarget || !$realTarget->exists() ) {
933 $isKnown = false;
934
935 return false;
936 }
937
938 return true;
939 }
940
945 public static function setupTranslateParserFunction( Parser $parser ) {
946 $parser->setFunctionHook( 'translation', [ self::class, 'translateRenderParserFunction' ] );
947 }
948
953 public static function translateRenderParserFunction( Parser $parser ) {
954 $pageTitle = $parser->getTitle();
955
956 $handle = new MessageHandle( $pageTitle );
957 $code = $handle->getCode();
958 if ( MediaWikiServices::getInstance()->getLanguageNameUtils()->isKnownLanguageTag( $code ) ) {
959 return '/' . $code;
960 }
961 return '';
962 }
963
974 public static function validateMessage( IContextSource $context, Content $content,
975 Status $status, $summary, User $user
976 ) {
977 if ( !$content instanceof TextContent ) {
978 // Not interested
979 return true;
980 }
981
982 $text = $content->getText();
983 $title = $context->getTitle();
984 $handle = new MessageHandle( $title );
985
986 if ( !$handle->isValid() ) {
987 return true;
988 }
989
990 // Don't bother validating if FuzzyBot or translation admin are saving.
991 if ( $user->isAllowed( 'translate-manage' ) || $user->equals( FuzzyBot::getUser() ) ) {
992 return true;
993 }
994
995 // Check the namespace, and perform validations for all messages excluding documentation.
996 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
997 $group = $handle->getGroup();
998
999 if ( method_exists( $group, 'getMessageContent' ) ) {
1000 // @phan-suppress-next-line PhanUndeclaredMethod
1001 $definition = $group->getMessageContent( $handle );
1002 } else {
1003 $definition = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
1004 }
1005
1006 $message = new FatMessage( $handle->getKey(), $definition );
1007 $message->setTranslation( $text );
1008
1009 $messageValidator = $group->getValidator();
1010 if ( !$messageValidator ) {
1011 return true;
1012 }
1013
1014 $validationResponse = $messageValidator->validateMessage( $message, $handle->getCode() );
1015 if ( $validationResponse->hasErrors() ) {
1016 $status->fatal( new ApiRawMessage(
1017 $context->msg( 'translate-syntax-error' )->parse(),
1018 'translate-validation-failed',
1019 [
1020 'validation' => [
1021 'errors' => $validationResponse->getDescriptiveErrors( $context ),
1022 'warnings' => $validationResponse->getDescriptiveWarnings( $context )
1023 ]
1024 ]
1025 ) );
1026 return false;
1027 }
1028 }
1029
1030 return true;
1031 }
1032
1034 public function onRevisionRecordInserted( $revisionRecord ): void {
1035 $parentId = $revisionRecord->getParentId();
1036 if ( $parentId === 0 || $parentId === null ) {
1037 // No parent, bail out.
1038 return;
1039 }
1040
1041 $prevRev = $this->revisionLookup->getRevisionById( $parentId );
1042 if ( !$prevRev || !$revisionRecord->hasSameContent( $prevRev ) ) {
1043 // Not a null revision, bail out.
1044 return;
1045 }
1046
1047 // List of tags that should be copied over when updating
1048 // tp:tag and tp:mark handling is in Hooks::updateTranstagOnNullRevisions.
1049 $tagsToCopy = [ RevTagStore::FUZZY_TAG, RevTagStore::TRANSVER_PROP ];
1050
1051 $db = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
1052 $db->insertSelect(
1053 'revtag',
1054 'revtag',
1055 [
1056 'rt_type' => 'rt_type',
1057 'rt_page' => 'rt_page',
1058 'rt_revision' => $revisionRecord->getId(),
1059 'rt_value' => 'rt_value',
1060
1061 ],
1062 [
1063 'rt_type' => $tagsToCopy,
1064 'rt_revision' => $parentId,
1065 ],
1066 __METHOD__
1067 );
1068 }
1069
1071 public function onListDefinedTags( &$tags ) {
1072 $tags[] = 'translate-translation-pages';
1073 }
1074
1076 public function onChangeTagsListActive( &$tags ) {
1077 if ( $this->config->get( 'EnablePageTranslation' ) ) {
1078 $tags[] = 'translate-translation-pages';
1079 }
1080 }
1081}
Message object where you can directly set the translation.
Definition Message.php:189
Script to identify the status of the translatable bundles in the rev_tag table and update them in the...
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
Various editing enhancements to the edit page interface.
WebAPI module for storing translations for users who are in a sandbox.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:30
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.
onChangeTagsListActive(&$tags)
@inheritDoc
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.
onListDefinedTags(&$tags)
@inheritDoc
Interface for TTMServer that can act as backend for translation search.