Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
HookHandler.php
1<?php
2declare( strict_types=1 );
3
4namespace MediaWiki\Extension\Translate;
5
6use ALItem;
7use ALTree;
8use ApiRawMessage;
9use Config;
10use Content;
11use DatabaseUpdater;
12use IContextSource;
13use Language;
14use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
15use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
16use MediaWiki\Config\ServiceOptions;
17use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
40use MediaWiki\Html\Html;
41use MediaWiki\MediaWikiServices;
42use MediaWiki\Revision\Hook\RevisionRecordInsertedHook;
43use MediaWiki\Revision\RevisionLookup;
44use MediaWiki\Settings\SettingsBuilder;
45use MediaWiki\StubObject\StubUserLang;
47use OutputPage;
48use Parser;
49use ParserOutput;
50use RecentChange;
51use SearchEngine;
52use SpecialPage;
53use SpecialSearch;
54use Status;
55use TextContent;
56use Title;
57use TitleValue;
60use User;
61use Wikimedia\Rdbms\ILoadBalancer;
62use Xml;
63use XmlSelect;
64
75class HookHandler implements RevisionRecordInsertedHook, ListDefinedTagsHook, ChangeTagsListActiveHook {
80 private const USER_MERGE_TABLES = [
81 'translate_stash' => 'ts_user',
82 'translate_reviews' => 'trr_user',
83 ];
84 private RevisionLookup $revisionLookup;
85 private ILoadBalancer $loadBalancer;
86 private Config $config;
87
88 public function __construct(
89 RevisionLookup $revisionLookup,
90 ILoadBalancer $loadBalancer,
91 Config $config
92 ) {
93 $this->revisionLookup = $revisionLookup;
94 $this->loadBalancer = $loadBalancer;
95 $this->config = $config;
96 }
97
99 public static function setupTranslate(): void {
100 global $wgTranslateYamlLibrary;
101 $hooks = [];
102
103 /*
104 * Text that will be shown in translations if the translation is outdated.
105 * Must be something that does not conflict with actual content.
106 */
107 if ( !defined( 'TRANSLATE_FUZZY' ) ) {
108 define( 'TRANSLATE_FUZZY', '!!FUZZY!!' );
109 }
110
111 if ( $wgTranslateYamlLibrary === null ) {
112 $wgTranslateYamlLibrary = function_exists( 'yaml_parse' ) ? 'phpyaml' : 'spyc';
113 }
114
115 $hooks['PageSaveComplete'][] = [ TranslateEditAddons::class, 'onSaveComplete' ];
116
117 // Page translation setup check and init if enabled.
118 global $wgEnablePageTranslation;
119 if ( $wgEnablePageTranslation ) {
120 // Special page and the right to use it
121 global $wgSpecialPages, $wgAvailableRights;
122 $wgSpecialPages['PageTranslation'] = [
123 'class' => PageTranslationSpecialPage::class,
124 'services' => [
125 'LanguageNameUtils',
126 'LanguageFactory',
127 'Translate:TranslationUnitStoreFactory',
128 'Translate:TranslatablePageParser',
129 'LinkBatchFactory',
130 'JobQueueGroup',
131 'DBLoadBalancer',
132 'Translate:MessageIndex',
133 'TitleParser',
134 ]
135 ];
136 $wgSpecialPages['PageTranslationDeletePage'] = [
137 'class' => DeleteTranslatableBundleSpecialPage::class,
138 'services' => [
139 'MainObjectStash',
140 'PermissionManager',
141 'Translate:TranslatableBundleFactory',
142 'Translate:SubpageListBuilder',
143 'JobQueueGroup',
144 ]
145 ];
146
147 // right-pagetranslation action-pagetranslation
148 $wgAvailableRights[] = 'pagetranslation';
149
150 $wgSpecialPages['PageMigration'] = MigrateTranslatablePageSpecialPage::class;
151 $wgSpecialPages['PagePreparation'] = PrepareTranslatablePageSpecialPage::class;
152
153 global $wgActionFilteredLogs, $wgLogActionsHandlers, $wgLogTypes;
154
155 // log-description-pagetranslation log-name-pagetranslation logentry-pagetranslation-mark
156 // logentry-pagetranslation-unmark logentry-pagetranslation-moveok
157 // logentry-pagetranslation-movenok logentry-pagetranslation-deletefok
158 // logentry-pagetranslation-deletefnok logentry-pagetranslation-deletelok
159 // logentry-pagetranslation-deletelnok logentry-pagetranslation-encourage
160 // logentry-pagetranslation-discourage logentry-pagetranslation-prioritylanguages
161 // logentry-pagetranslation-associate logentry-pagetranslation-dissociate
162 $wgLogTypes[] = 'pagetranslation';
163 $wgLogActionsHandlers['pagetranslation/mark'] = TranslatableBundleLogFormatter::class;
164 $wgLogActionsHandlers['pagetranslation/unmark'] = TranslatableBundleLogFormatter::class;
165 $wgLogActionsHandlers['pagetranslation/moveok'] = TranslatableBundleLogFormatter::class;
166 $wgLogActionsHandlers['pagetranslation/movenok'] = TranslatableBundleLogFormatter::class;
167 $wgLogActionsHandlers['pagetranslation/deletelok'] = TranslatableBundleLogFormatter::class;
168 $wgLogActionsHandlers['pagetranslation/deletefok'] = TranslatableBundleLogFormatter::class;
169 $wgLogActionsHandlers['pagetranslation/deletelnok'] = TranslatableBundleLogFormatter::class;
170 $wgLogActionsHandlers['pagetranslation/deletefnok'] = TranslatableBundleLogFormatter::class;
171 $wgLogActionsHandlers['pagetranslation/encourage'] = TranslatableBundleLogFormatter::class;
172 $wgLogActionsHandlers['pagetranslation/discourage'] = TranslatableBundleLogFormatter::class;
173 $wgLogActionsHandlers['pagetranslation/prioritylanguages'] = TranslatableBundleLogFormatter::class;
174 $wgLogActionsHandlers['pagetranslation/associate'] = TranslatableBundleLogFormatter::class;
175 $wgLogActionsHandlers['pagetranslation/dissociate'] = TranslatableBundleLogFormatter::class;
176 $wgActionFilteredLogs['pagetranslation'] = [
177 'mark' => [ 'mark' ],
178 'unmark' => [ 'unmark' ],
179 'move' => [ 'moveok', 'movenok' ],
180 'delete' => [ 'deletefok', 'deletefnok', 'deletelok', 'deletelnok' ],
181 'encourage' => [ 'encourage' ],
182 'discourage' => [ 'discourage' ],
183 'prioritylanguages' => [ 'prioritylanguages' ],
184 'aggregategroups' => [ 'associate', 'dissociate' ],
185 ];
186
187 $wgLogTypes[] = 'messagebundle';
188 $wgLogActionsHandlers['messagebundle/moveok'] = TranslatableBundleLogFormatter::class;
189 $wgLogActionsHandlers['messagebundle/movenok'] = TranslatableBundleLogFormatter::class;
190 $wgLogActionsHandlers['messagebundle/deletefok'] = TranslatableBundleLogFormatter::class;
191 $wgLogActionsHandlers['messagebundle/deletefnok'] = TranslatableBundleLogFormatter::class;
192 $wgActionFilteredLogs['messagebundle'] = [
193 'move' => [ 'moveok', 'movenok' ],
194 'delete' => [ 'deletefok', 'deletefnok' ],
195 ];
196
197 $wgLogActionsHandlers['import/translatable-bundle'] = TranslatableBundleLogFormatter::class;
198
199 global $wgJobClasses;
200 $wgJobClasses['RenderTranslationPageJob'] = RenderTranslationPageJob::class;
201 $wgJobClasses['MoveTranslatableBundleJob'] = MoveTranslatableBundleJob::class;
202 $wgJobClasses['DeleteTranslatableBundleJob'] = DeleteTranslatableBundleJob::class;
203 $wgJobClasses['UpdateTranslatablePageJob'] = UpdateTranslatablePageJob::class;
204
205 // Namespaces
206 global $wgNamespacesWithSubpages, $wgNamespaceProtection;
207 global $wgTranslateMessageNamespaces;
208
209 $wgNamespacesWithSubpages[NS_TRANSLATIONS] = true;
210 $wgNamespacesWithSubpages[NS_TRANSLATIONS_TALK] = true;
211
212 // Standard protection and register it for filtering
213 $wgNamespaceProtection[NS_TRANSLATIONS] = [ 'translate' ];
214 $wgTranslateMessageNamespaces[] = NS_TRANSLATIONS;
215
217
219 $hooks['BeforePageDisplay'][] = [ Hooks::class, 'onBeforePageDisplay' ];
220
221 // Disable VE
222 $hooks['VisualEditorBeforeEditor'][] = [ Hooks::class, 'onVisualEditorBeforeEditor' ];
223
224 // Check syntax for <translate>
225 $hooks['MultiContentSave'][] = [ Hooks::class, 'tpSyntaxCheck' ];
226 $hooks['EditFilterMergedContent'][] =
227 [ Hooks::class, 'tpSyntaxCheckForEditContent' ];
228
229 // Add transtag to page props for discovery
230 $hooks['PageSaveComplete'][] = [ Hooks::class, 'addTranstagAfterSave' ];
231
232 $hooks['RevisionRecordInserted'][] = [ Hooks::class, 'updateTranstagOnNullRevisions' ];
233
234 // Register different ways to show language links
235 $hooks['ParserFirstCallInit'][] = [ self::class, 'setupParserHooks' ];
236 $hooks['LanguageLinks'][] = [ Hooks::class, 'addLanguageLinks' ];
237 $hooks['SkinTemplateGetLanguageLink'][] = [ Hooks::class, 'formatLanguageLink' ];
238
239 // Allow templates to query whether they are transcluded in a translatable/translated page
240 $hooks['GetMagicVariableIDs'][] = [ Hooks::class, 'onGetMagicVariableIDs' ];
241 $hooks['ParserGetVariableValueSwitch'][] = [ Hooks::class, 'onParserGetVariableValueSwitch' ];
242
243 // Strip <translate> tags etc. from source pages when rendering
244 $hooks['ParserBeforeInternalParse'][] = [ Hooks::class, 'renderTagPage' ];
245 // Strip <translate> tags etc. from source pages when preprocessing
246 $hooks['ParserBeforePreprocess'][] = [ Hooks::class, 'preprocessTagPage' ];
247 $hooks['ParserOutputPostCacheTransform'][] =
248 [ Hooks::class, 'onParserOutputPostCacheTransform' ];
249
250 $hooks['BeforeParserFetchTemplateRevisionRecord'][] =
251 [ Hooks::class, 'fetchTranslatableTemplateAndTitle' ];
252
253 // Set the page content language
254 $hooks['PageContentLanguage'][] = [ Hooks::class, 'onPageContentLanguage' ];
255
256 // Prevent editing of certain pages in translations namespace
257 $hooks['getUserPermissionsErrorsExpensive'][] =
258 [ Hooks::class, 'onGetUserPermissionsErrorsExpensive' ];
259 // Prevent editing of translation pages directly
260 $hooks['getUserPermissionsErrorsExpensive'][] =
261 [ Hooks::class, 'preventDirectEditing' ];
262
263 // Our custom header for translation pages
264 $hooks['ArticleViewHeader'][] = [ Hooks::class, 'translatablePageHeader' ];
265
266 // Edit notice shown on translatable pages
267 $hooks['TitleGetEditNotices'][] = [ Hooks::class, 'onTitleGetEditNotices' ];
268
269 // Custom move page that can move all the associated pages too
270 $hooks['SpecialPage_initList'][] = [ Hooks::class, 'replaceMovePage' ];
271 // Locking during page moves
272 $hooks['getUserPermissionsErrorsExpensive'][] =
273 [ Hooks::class, 'lockedPagesCheck' ];
274 // Disable action=delete
275 $hooks['ArticleConfirmDelete'][] = [ Hooks::class, 'disableDelete' ];
276
277 // Replace subpage logic behavior
278 $hooks['SkinSubPageSubtitle'][] = [ Hooks::class, 'replaceSubtitle' ];
279
280 // Replaced edit tab with translation tab for translation pages
281 $hooks['SkinTemplateNavigation::Universal'][] = [ Hooks::class, 'translateTab' ];
282
283 // Update translated page when translation unit is moved
284 $hooks['PageMoveComplete'][] = [ Hooks::class, 'onMovePageTranslationUnits' ];
285
286 // Update translated page when translation unit is deleted
287 $hooks['ArticleDeleteComplete'][] = [ Hooks::class, 'onDeleteTranslationUnit' ];
288
289 // Prevent editing of translation pages.
290 $hooks['ReplaceTextFilterPageTitlesForEdit'][] = [ Hooks::class, 'onReplaceTextFilterPageTitlesForEdit' ];
291 // Prevent renaming of translatable pages and their translation and translation units
292 $hooks['ReplaceTextFilterPageTitlesForRename'][] =
293 [ Hooks::class, 'onReplaceTextFilterPageTitlesForRename' ];
294 }
295
296 global $wgTranslateUseSandbox;
297 if ( $wgTranslateUseSandbox ) {
298 global $wgSpecialPages, $wgAvailableRights, $wgDefaultUserOptions;
299
300 $wgSpecialPages['ManageTranslatorSandbox'] = [
301 'class' => ManageTranslatorSandboxSpecialPage::class,
302 'services' => [
303 'Translate:TranslationStashReader',
304 'UserOptionsLookup'
305 ],
306 'args' => [
307 static function () {
308 return new ServiceOptions(
309 ManageTranslatorSandboxSpecialPage::CONSTRUCTOR_OPTIONS,
310 MediaWikiServices::getInstance()->getMainConfig()
311 );
312 }
313 ]
314 ];
315 $wgSpecialPages['TranslationStash'] = [
316 'class' => TranslationStashSpecialPage::class,
317 'services' => [
318 'LanguageNameUtils',
319 'Translate:TranslationStashReader',
320 'UserOptionsLookup',
321 'LanguageFactory',
322 ],
323 'args' => [
324 static function () {
325 return new ServiceOptions(
326 TranslationStashSpecialPage::CONSTRUCTOR_OPTIONS,
327 MediaWikiServices::getInstance()->getMainConfig()
328 );
329 }
330 ]
331 ];
332 $wgDefaultUserOptions['translate-sandbox'] = '';
333 // right-translate-sandboxmanage action-translate-sandboxmanage
334 $wgAvailableRights[] = 'translate-sandboxmanage';
335
336 $hooks['GetPreferences'][] = [ TranslateSandbox::class, 'onGetPreferences' ];
337 $hooks['UserGetRights'][] = [ TranslateSandbox::class, 'enforcePermissions' ];
338 $hooks['ApiCheckCanExecute'][] = [ TranslateSandbox::class, 'onApiCheckCanExecute' ];
339
340 global $wgLogTypes, $wgLogActionsHandlers;
341 // log-name-translatorsandbox log-description-translatorsandbox
342 $wgLogTypes[] = 'translatorsandbox';
343 // logentry-translatorsandbox-promoted logentry-translatorsandbox-rejected
344 $wgLogActionsHandlers['translatorsandbox/promoted'] = 'TranslateLogFormatter';
345 $wgLogActionsHandlers['translatorsandbox/rejected'] = 'TranslateLogFormatter';
346
347 // This is no longer used for new entries since 2016.07.
348 // logentry-newusers-tsbpromoted
349 $wgLogActionsHandlers['newusers/tsbpromoted'] = 'LogFormatter';
350
351 global $wgJobClasses;
352 $wgJobClasses['TranslateSandboxEmailJob'] = 'TranslateSandboxEmailJob';
353
354 global $wgAPIModules;
355 $wgAPIModules['translationstash'] = [
356 'class' => TranslationStashActionApi::class,
357 'services' => [
358 'DBLoadBalancer',
359 'UserFactory'
360 ]
361 ];
362 $wgAPIModules['translatesandbox'] = [
363 'class' => TranslatorSandboxActionApi::class,
364 'services' => [
365 'UserFactory',
366 'UserNameUtils',
367 'UserOptionsManager',
368 'WikiPageFactory',
369 'UserOptionsLookup'
370 ],
371 'args' => [
372 static function () {
373 return new ServiceOptions(
374 TranslatorSandboxActionApi::CONSTRUCTOR_OPTIONS,
375 MediaWikiServices::getInstance()->getMainConfig()
376 );
377 }
378 ]
379 ];
380 }
381
382 global $wgNamespaceRobotPolicies;
383 $wgNamespaceRobotPolicies[NS_TRANSLATIONS] = 'noindex';
384
385 // If no service has been configured, we use a built-in fallback.
386 global $wgTranslateTranslationDefaultService,
387 $wgTranslateTranslationServices;
388 if ( $wgTranslateTranslationDefaultService === true ) {
389 $wgTranslateTranslationDefaultService = 'TTMServer';
390 if ( !isset( $wgTranslateTranslationServices['TTMServer'] ) ) {
391 $wgTranslateTranslationServices['TTMServer'] = [
392 'database' => false, // Passed to wfGetDB
393 'cutoff' => 0.75,
394 'type' => 'ttmserver',
395 'public' => false,
396 ];
397 }
398 }
399
400 $hooks['SidebarBeforeOutput'][] = [ TranslateToolbox::class, 'toolboxAllTranslations' ];
401
402 static::registerHookHandlers( $hooks );
403 }
404
405 private static function registerHookHandlers( array $hooks ): void {
406 if ( defined( 'MW_PHPUNIT_TEST' ) && MediaWikiServices::hasInstance() ) {
407 // When called from a test case's setUp() method,
408 // we can use HookContainer, but we cannot use SettingsBuilder.
409 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
410 foreach ( $hooks as $name => $handlers ) {
411 foreach ( $handlers as $h ) {
412 $hookContainer->register( $name, $h );
413 }
414 }
415 } elseif ( method_exists( SettingsBuilder::class, 'registerHookHandlers' ) ) {
416 // Since 1.40: Use SettingsBuilder to register hooks during initialization.
417 // HookContainer is not available at this time.
418 $settingsBuilder = SettingsBuilder::getInstance();
419 $settingsBuilder->registerHookHandlers( $hooks );
420 } else {
421 // For MW < 1.40: Directly manipulate $wgHooks during initialization.
422 foreach ( $hooks as $name => $handlers ) {
423 $GLOBALS['wgHooks'][$name] = array_merge(
424 $GLOBALS['wgHooks'][$name] ?? [],
425 $handlers
426 );
427 }
428 }
429 }
430
435 public static function onUserGetReservedNames( array &$names ): void {
436 $names[] = FuzzyBot::getName();
437 $names[] = TranslateUserManager::getName();
438 }
439
441 public static function onAbuseFilterAlterVariables(
442 VariableHolder &$vars, Title $title, User $user
443 ): void {
444 $handle = new MessageHandle( $title );
445
446 // Only set this variable if we are in a proper namespace to avoid
447 // unnecessary overhead in non-translation pages
448 if ( $handle->isMessageNamespace() ) {
449 $vars->setLazyLoadVar(
450 'translate_source_text',
451 'translate-get-source',
452 [ 'handle' => $handle ]
453 );
454 $vars->setLazyLoadVar(
455 'translate_target_language',
456 'translate-get-target-language',
457 [ 'handle' => $handle ]
458 );
459 }
460 }
461
463 public static function onAbuseFilterComputeVariable(
464 string $method,
465 VariableHolder $vars,
466 array $parameters,
467 ?string &$result
468 ): bool {
469 if ( $method !== 'translate-get-source' && $method !== 'translate-get-target-language' ) {
470 return true;
471 }
472
473 $handle = $parameters['handle'];
474 $value = '';
475 if ( $handle->isValid() ) {
476 if ( $method === 'translate-get-source' ) {
477 $group = $handle->getGroup();
478 $value = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
479 } else {
480 $value = $handle->getCode();
481 }
482 }
483
484 $result = $value;
485
486 return false;
487 }
488
490 public static function onAbuseFilterBuilder( array &$builderValues ): void {
491 // Uses: 'abusefilter-edit-builder-vars-translate-source-text'
492 // and 'abusefilter-edit-builder-vars-translate-target-language'
493 $builderValues['vars']['translate_source_text'] = 'translate-source-text';
494 $builderValues['vars']['translate_target_language'] = 'translate-target-language';
495 }
496
501 public static function setupParserHooks( Parser $parser ): void {
502 // For nice language list in-page
503 $parser->setHook( 'languages', [ Hooks::class, 'languages' ] );
504 }
505
507 public static function schemaUpdates( DatabaseUpdater $updater ): void {
508 $dir = dirname( __DIR__, 1 ) . '/sql';
509 $dbType = $updater->getDB()->getType();
510
511 if ( $dbType === 'mysql' || $dbType === 'sqlite' ) {
512 $updater->addExtensionTable(
513 'translate_sections',
514 "{$dir}/{$dbType}/translate_sections.sql"
515 );
516 $updater->addExtensionTable(
517 'revtag',
518 "{$dir}/{$dbType}/revtag.sql"
519 );
520 $updater->addExtensionTable(
521 'translate_groupstats',
522 "{$dir}/{$dbType}/translate_groupstats.sql"
523 );
524 $updater->addExtensionTable(
525 'translate_reviews',
526 "{$dir}/{$dbType}/translate_reviews.sql"
527 );
528 $updater->addExtensionTable(
529 'translate_groupreviews',
530 "{$dir}/{$dbType}/translate_groupreviews.sql"
531 );
532 $updater->addExtensionTable(
533 'translate_tms',
534 "{$dir}/{$dbType}/translate_tm.sql"
535 );
536 $updater->addExtensionTable(
537 'translate_metadata',
538 "{$dir}/{$dbType}/translate_metadata.sql"
539 );
540 $updater->addExtensionTable(
541 'translate_messageindex',
542 "{$dir}/{$dbType}/translate_messageindex.sql"
543 );
544 $updater->addExtensionTable(
545 'translate_stash',
546 "{$dir}/{$dbType}/translate_stash.sql"
547 );
548 $updater->addExtensionTable(
549 'translate_translatable_bundles',
550 "{$dir}/{$dbType}/translate_translatable_bundles.sql"
551 );
552
553 // 1.32 - This also adds a PRIMARY KEY
554 $updater->addExtensionUpdate( [
555 'renameIndex',
556 'translate_reviews',
557 'trr_user_page_revision',
558 'PRIMARY',
559 false,
560 "$dir/translate_reviews-patch-01-primary-key.sql",
561 true
562 ] );
563
564 $updater->addExtensionTable(
565 'translate_cache',
566 "{$dir}/{$dbType}/translate_cache.sql"
567 );
568
569 if ( $dbType === 'mysql' ) {
570 // 1.38
571 $updater->modifyExtensionField(
572 'translate_cache',
573 'tc_key',
574 "{$dir}/{$dbType}/translate_cache-alter-varbinary.sql"
575 );
576 }
577 } elseif ( $dbType === 'postgres' ) {
578 $updater->addExtensionTable(
579 'translate_sections',
580 "{$dir}/{$dbType}/tables-generated.sql"
581 );
582 $updater->addExtensionUpdate( [
583 'changeField', 'translate_cache', 'tc_exptime', 'TIMESTAMPTZ', 'th_timestamp::timestamp with time zone'
584 ] );
585 }
586
587 // 1.39
588 $updater->dropExtensionIndex(
589 'translate_messageindex',
590 'tmi_key',
591 "{$dir}/{$dbType}/patch-translate_messageindex-unique-to-pk.sql"
592 );
593 $updater->dropExtensionIndex(
594 'translate_tmt',
595 'tms_sid_lang',
596 "{$dir}/{$dbType}/patch-translate_tmt-unique-to-pk.sql"
597 );
598 $updater->dropExtensionIndex(
599 'revtag',
600 'rt_type_page_revision',
601 "{$dir}/{$dbType}/patch-revtag-unique-to-pk.sql"
602 );
603
604 $updater->addPostDatabaseUpdateMaintenance( SyncTranslatableBundleStatusMaintenanceScript::class );
605 }
606
613 public static function onPageContentLanguage( Title $title, &$pageLang ): void {
614 $handle = new MessageHandle( $title );
615 if ( $handle->isMessageNamespace() ) {
616 $pageLang = $handle->getEffectiveLanguage();
617 }
618 }
619
624 public static function translateMessageDocumentationLanguage( array &$names, ?string $code ): void {
625 global $wgTranslateDocumentationLanguageCode;
626 if ( $wgTranslateDocumentationLanguageCode ) {
627 // Special case the autonyms
628 if (
629 $wgTranslateDocumentationLanguageCode === $code ||
630 $code === null
631 ) {
632 $code = 'en';
633 }
634
635 $names[$wgTranslateDocumentationLanguageCode] =
636 wfMessage( 'translate-documentation-language' )->inLanguage( $code )->plain();
637 }
638 }
639
641 public static function searchProfile( array &$profiles ): void {
642 global $wgTranslateMessageNamespaces;
643 $insert = [];
644 $insert['translation'] = [
645 'message' => 'translate-searchprofile',
646 'tooltip' => 'translate-searchprofile-tooltip',
647 'namespaces' => $wgTranslateMessageNamespaces,
648 ];
649
650 // Insert translations before 'all'
651 $index = array_search( 'all', array_keys( $profiles ) );
652
653 // Or just at the end if all is not found
654 if ( $index === false ) {
655 wfWarn( '"all" not found in search profiles' );
656 $index = count( $profiles );
657 }
658
659 $profiles = array_merge(
660 array_slice( $profiles, 0, $index ),
661 $insert,
662 array_slice( $profiles, $index )
663 );
664 }
665
667 public static function searchProfileForm(
668 SpecialSearch $search,
669 string &$form,
670 string $profile,
671 string $term,
672 array $opts
673 ): bool {
674 if ( $profile !== 'translation' ) {
675 return true;
676 }
677
678 if ( Services::getInstance()->getTtmServerFactory()->getDefaultForQuerying() instanceof SearchableTtmServer ) {
679 $href = SpecialPage::getTitleFor( 'SearchTranslations' )
680 ->getFullUrl( [ 'query' => $term ] );
681 $form = Html::successBox(
682 $search->msg( 'translate-searchprofile-note', $href )->parse(),
683 'plainlinks'
684 );
685
686 return false;
687 }
688
689 if ( !$search->getSearchEngine()->supports( 'title-suffix-filter' ) ) {
690 return false;
691 }
692
693 $hidden = '';
694 foreach ( $opts as $key => $value ) {
695 $hidden .= Html::hidden( $key, $value );
696 }
697
698 $context = $search->getContext();
699 $code = $context->getLanguage()->getCode();
700 $selected = $context->getRequest()->getVal( 'languagefilter' );
701
702 $languages = Utilities::getLanguageNames( $code );
703 ksort( $languages );
704
705 $selector = new XmlSelect( 'languagefilter', 'languagefilter' );
706 $selector->setDefault( $selected );
707 $selector->addOption( wfMessage( 'translate-search-nofilter' )->text(), '-' );
708 foreach ( $languages as $code => $name ) {
709 $selector->addOption( "$code - $name", $code );
710 }
711
712 $selector = $selector->getHTML();
713
714 $label = Xml::label(
715 wfMessage( 'translate-search-languagefilter' )->text(),
716 'languagefilter'
717 ) . '&#160;';
718 $params = [ 'id' => 'mw-searchoptions' ];
719
720 $form = Xml::fieldset( false, false, $params ) .
721 $hidden . $label . $selector .
722 Html::closeElement( 'fieldset' );
723
724 return false;
725 }
726
728 public static function searchProfileSetupEngine(
729 SpecialSearch $search,
730 string $profile,
731 SearchEngine $engine
732 ): void {
733 if ( $profile !== 'translation' ) {
734 return;
735 }
736
737 $context = $search->getContext();
738 $selected = $context->getRequest()->getVal( 'languagefilter' );
739 if ( $selected !== '-' && $selected ) {
740 $engine->setFeatureData( 'title-suffix-filter', "/$selected" );
741 $search->setExtraParam( 'languagefilter', $selected );
742 }
743 }
744
746 public static function preventCategorization( Parser $parser, string &$html ): void {
747 $pageReference = $parser->getPage();
748 if ( !$pageReference ) {
749 return;
750 }
751
752 $linkTarget = TitleValue::newFromPage( $pageReference );
753 $handle = new MessageHandle( $linkTarget );
754 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
755 $parserOutput = $parser->getOutput();
756 // MW >= 1.40
757 if ( method_exists( $parserOutput, 'getCategorySortKey' ) ) {
758 $names = $parserOutput->getCategoryNames();
759 $parserCategories = [];
760 foreach ( $names as $name ) {
761 $parserCategories[$name] = $parserOutput->getCategorySortKey( $name );
762 }
763 $parserOutput->setExtensionData( 'translate-fake-categories', $parserCategories );
764 } else {
765 $parserOutput->setExtensionData( 'translate-fake-categories',
766 $parserOutput->getCategories() );
767 }
768 $parserOutput->setCategories( [] );
769 }
770 }
771
773 public static function showFakeCategories( OutputPage $outputPage, ParserOutput $parserOutput ): void {
774 $fakeCategories = $parserOutput->getExtensionData( 'translate-fake-categories' );
775 if ( $fakeCategories ) {
776 $outputPage->setCategoryLinks( $fakeCategories );
777 }
778 }
779
785 public static function addConfig( array &$vars, OutputPage $out ): void {
786 global $wgTranslateDocumentationLanguageCode,
787 $wgTranslatePermissionUrl,
788 $wgTranslateUseSandbox;
789
790 $title = $out->getTitle();
791 if ( $title->isSpecial( 'Translate' ) ||
792 $title->isSpecial( 'TranslationStash' ) ||
793 $title->isSpecial( 'SearchTranslations' )
794 ) {
795 $user = $out->getUser();
796 $vars['TranslateRight'] = $user->isAllowed( 'translate' );
797 $vars['TranslateMessageReviewRight'] = $user->isAllowed( 'translate-messagereview' );
798 $vars['DeleteRight'] = $user->isAllowed( 'delete' );
799 $vars['TranslateManageRight'] = $user->isAllowed( 'translate-manage' );
800 $vars['wgTranslateDocumentationLanguageCode'] = $wgTranslateDocumentationLanguageCode;
801 $vars['wgTranslatePermissionUrl'] = $wgTranslatePermissionUrl;
802 $vars['wgTranslateUseSandbox'] = $wgTranslateUseSandbox;
803 }
804 }
805
807 public static function onAdminLinks( ALTree $tree ): void {
808 global $wgTranslateUseSandbox;
809
810 if ( $wgTranslateUseSandbox ) {
811 $sectionLabel = wfMessage( 'adminlinks_users' )->text();
812 $row = $tree->getSection( $sectionLabel )->getRow( 'main' );
813 $row->addItem( ALItem::newFromSpecialPage( 'TranslateSandbox' ) );
814 }
815 }
816
821 public static function onMergeAccountFromTo( User $oldUser, User $newUser ): void {
822 $dbw = MediawikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
823
824 // Update the non-duplicate rows, we'll just delete
825 // the duplicate ones later
826 foreach ( self::USER_MERGE_TABLES as $table => $field ) {
827 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
828 $dbw->update(
829 $table,
830 [ $field => $newUser->getId() ],
831 [ $field => $oldUser->getId() ],
832 __METHOD__,
833 [ 'IGNORE' ]
834 );
835 }
836 }
837 }
838
843 public static function onDeleteAccount( User $oldUser ): void {
844 $dbw = MediawikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
845
846 // Delete any remaining rows that didn't get merged
847 foreach ( self::USER_MERGE_TABLES as $table => $field ) {
848 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
849 $dbw->delete(
850 $table,
851 [ $field => $oldUser->getId() ],
852 __METHOD__
853 );
854 }
855 }
856 }
857
859 public static function onAbortEmailNotificationReview(
860 User $editor,
861 Title $title,
862 RecentChange $rc
863 ): bool {
864 return $rc->getAttribute( 'rc_log_type' ) !== 'translationreview';
865 }
866
875 public static function onTitleIsAlwaysKnown( $target, &$isKnown ): bool {
876 if ( !$target->inNamespace( NS_SPECIAL ) ) {
877 return true;
878 }
879
880 [ $name, $subpage ] = MediaWikiServices::getInstance()
881 ->getSpecialPageFactory()->resolveAlias( $target->getDBkey() );
882 if ( $name !== 'MyLanguage' || (string)$subpage === '' ) {
883 return true;
884 }
885
886 $realTarget = Title::newFromText( $subpage );
887 if ( !$realTarget || !$realTarget->exists() ) {
888 $isKnown = false;
889
890 return false;
891 }
892
893 return true;
894 }
895
897 public static function setupTranslateParserFunction( Parser $parser ): void {
898 $parser->setFunctionHook( 'translation', [ self::class, 'translateRenderParserFunction' ] );
899 }
900
901 public static function translateRenderParserFunction( Parser $parser ): string {
902 $pageReference = $parser->getPage();
903 if ( !$pageReference ) {
904 return '';
905 }
906 $linkTarget = TitleValue::newFromPage( $pageReference );
907 $handle = new MessageHandle( $linkTarget );
908 $code = $handle->getCode();
909 if ( MediaWikiServices::getInstance()->getLanguageNameUtils()->isKnownLanguageTag( $code ) ) {
910 return '/' . $code;
911 }
912 return '';
913 }
914
920 public static function validateMessage(
921 IContextSource $context,
922 Content $content,
923 Status $status,
924 string $summary,
925 User $user
926 ): bool {
927 if ( !$content instanceof TextContent ) {
928 // Not interested
929 return true;
930 }
931
932 $text = $content->getText();
933 $title = $context->getTitle();
934 $handle = new MessageHandle( $title );
935
936 if ( !$handle->isValid() ) {
937 return true;
938 }
939
940 // Don't bother validating if FuzzyBot or translation admin are saving.
941 if ( $user->isAllowed( 'translate-manage' ) || $user->equals( FuzzyBot::getUser() ) ) {
942 return true;
943 }
944
945 // Check the namespace, and perform validations for all messages excluding documentation.
946 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
947 $group = $handle->getGroup();
948
949 if ( method_exists( $group, 'getMessageContent' ) ) {
950 // @phan-suppress-next-line PhanUndeclaredMethod
951 $definition = $group->getMessageContent( $handle );
952 } else {
953 $definition = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
954 }
955
956 $message = new FatMessage( $handle->getKey(), $definition );
957 $message->setTranslation( $text );
958
959 $messageValidator = $group->getValidator();
960 if ( !$messageValidator ) {
961 return true;
962 }
963
964 $validationResponse = $messageValidator->validateMessage( $message, $handle->getCode() );
965 if ( $validationResponse->hasErrors() ) {
966 $status->fatal( new ApiRawMessage(
967 $context->msg( 'translate-syntax-error' )->parse(),
968 'translate-validation-failed',
969 [
970 'validation' => [
971 'errors' => $validationResponse->getDescriptiveErrors( $context ),
972 'warnings' => $validationResponse->getDescriptiveWarnings( $context )
973 ]
974 ]
975 ) );
976 return false;
977 }
978 }
979
980 return true;
981 }
982
984 public function onRevisionRecordInserted( $revisionRecord ): void {
985 $parentId = $revisionRecord->getParentId();
986 if ( $parentId === 0 || $parentId === null ) {
987 // No parent, bail out.
988 return;
989 }
990
991 $prevRev = $this->revisionLookup->getRevisionById( $parentId );
992 if ( !$prevRev || !$revisionRecord->hasSameContent( $prevRev ) ) {
993 // Not a null revision, bail out.
994 return;
995 }
996
997 // List of tags that should be copied over when updating
998 // tp:tag and tp:mark handling is in Hooks::updateTranstagOnNullRevisions.
999 $tagsToCopy = [ RevTagStore::FUZZY_TAG, RevTagStore::TRANSVER_PROP ];
1000
1001 $db = $this->loadBalancer->getConnection( DB_PRIMARY );
1002 $db->insertSelect(
1003 'revtag',
1004 'revtag',
1005 [
1006 'rt_type' => 'rt_type',
1007 'rt_page' => 'rt_page',
1008 'rt_revision' => $revisionRecord->getId(),
1009 'rt_value' => 'rt_value',
1010
1011 ],
1012 [
1013 'rt_type' => $tagsToCopy,
1014 'rt_revision' => $parentId,
1015 ],
1016 __METHOD__
1017 );
1018 }
1019
1021 public function onListDefinedTags( &$tags ): void {
1022 $tags[] = 'translate-translation-pages';
1023 }
1024
1026 public function onChangeTagsListActive( &$tags ): void {
1027 if ( $this->config->get( 'EnablePageTranslation' ) ) {
1028 $tags[] = 'translate-translation-pages';
1029 }
1030 }
1031}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Script to identify the status of the translatable bundles in the rev_tag table and update them in the...
Hooks for Translate extension.
static schemaUpdates(DatabaseUpdater $updater)
Hook: LoadExtensionSchemaUpdates.
static onAbuseFilterAlterVariables(VariableHolder &$vars, Title $title, User $user)
Used for setting an AbuseFilter variable.
static onUserGetReservedNames(array &$names)
Hook: UserGetReservedNames Prevents anyone from registering or logging in as FuzzyBot.
static setupTranslateParserFunction(Parser $parser)
Hook: ParserFirstCallInit.
static onPageContentLanguage(Title $title, &$pageLang)
Hook: PageContentLanguage Set the correct page content language for translation units.
static onAbuseFilterBuilder(array &$builderValues)
Register AbuseFilter variables provided by Translate.
static validateMessage(IContextSource $context, Content $content, Status $status, string $summary, User $user)
Runs the configured validator to ensure that the message meets the required criteria.
static searchProfileForm(SpecialSearch $search, string &$form, string $profile, string $term, array $opts)
Hook: SpecialSearchProfileForm.
static setupTranslate()
Do late setup that depends on configuration.
static onAbortEmailNotificationReview(User $editor, Title $title, RecentChange $rc)
Hook: AbortEmailNotification.
static onAbuseFilterComputeVariable(string $method, VariableHolder $vars, array $parameters, ?string &$result)
Computes the translate_source_text and translate_target_language AbuseFilter variables.
onRevisionRecordInserted( $revisionRecord)
@inheritDoc
static addConfig(array &$vars, OutputPage $out)
Hook: MakeGlobalVariablesScript Adds $wgTranslateDocumentationLanguageCode to ResourceLoader configur...
static onAdminLinks(ALTree $tree)
Hook: AdminLinks.
static preventCategorization(Parser $parser, string &$html)
Hook: ParserAfterTidy.
static setupParserHooks(Parser $parser)
Hook: ParserFirstCallInit Registers <languages> tag with the parser.
static searchProfile(array &$profiles)
Hook: SpecialSearchProfiles.
static showFakeCategories(OutputPage $outputPage, ParserOutput $parserOutput)
Hook: OutputPageParserOutput.
static onTitleIsAlwaysKnown( $target, &$isKnown)
Hook: TitleIsAlwaysKnown Make Special:MyLanguage links red if the target page doesn't exist.
static searchProfileSetupEngine(SpecialSearch $search, string $profile, SearchEngine $engine)
Hook: SpecialSearchSetupEngine.
static onMergeAccountFromTo(User $oldUser, User $newUser)
Hook: MergeAccountFromTo For UserMerge extension.
static onDeleteAccount(User $oldUser)
Hook: DeleteAccount For UserMerge extension.
static translateMessageDocumentationLanguage(array &$names, ?string $code)
Hook: LanguageGetTranslatedLanguageNames Hook: TranslateSupportedLanguages.
Class to manage revision tags for translatable bundles.
Message object where you can directly set the translation.
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:31
Class for pointing to messages, like Title class is for titles.
getEffectiveLanguage()
Return the Language object for the assumed language of the content, which might be different from the...
Utility class for the sandbox feature of Translate.
Adds toolbox menu item to Special:Prefixindex to show all other available translations for a message.
Interface for TTMServer that can act as backend for translation search.