2declare( strict_types=1 );
4namespace MediaWiki\Extension\Translate;
10use MediaWiki\Api\ApiRawMessage;
11use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
12use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
13use MediaWiki\Config\Config;
14use MediaWiki\Config\ConfigException;
15use MediaWiki\Config\ServiceOptions;
16use MediaWiki\Content\Content;
17use MediaWiki\Content\TextContent;
18use MediaWiki\Context\IContextSource;
19use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
48use MediaWiki\Hook\ParserFirstCallInitHook;
49use MediaWiki\Html\Html;
50use MediaWiki\Language\Language;
51use MediaWiki\Languages\LanguageNameUtils;
52use MediaWiki\MediaWikiServices;
53use MediaWiki\Output\OutputPage;
54use MediaWiki\Parser\Parser;
55use MediaWiki\Parser\ParserOutput;
56use MediaWiki\Registration\ExtensionRegistry;
57use MediaWiki\ResourceLoader\Context;
58use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
59use MediaWiki\ResourceLoader\ResourceLoader;
60use MediaWiki\Revision\Hook\RevisionRecordInsertedHook;
61use MediaWiki\Revision\RevisionLookup;
62use MediaWiki\Settings\SettingsBuilder;
63use MediaWiki\SpecialPage\SpecialPage;
64use MediaWiki\Specials\SpecialSearch;
65use MediaWiki\Status\Status;
66use MediaWiki\StubObject\StubUserLang;
67use MediaWiki\Title\Title;
68use MediaWiki\Title\TitleValue;
69use MediaWiki\User\Hook\UserGetReservedNamesHook;
70use MediaWiki\User\User;
71use MediaWiki\Xml\XmlSelect;
73use Wikimedia\Rdbms\ILoadBalancer;
86 ChangeTagsListActiveHook,
88 ParserFirstCallInitHook,
89 RevisionRecordInsertedHook,
90 UserGetReservedNamesHook,
91 ResourceLoaderRegisterModulesHook
97 private const USER_MERGE_TABLES = [
98 'translate_stash' =>
'ts_user',
99 'translate_reviews' =>
'trr_user',
101 private RevisionLookup $revisionLookup;
102 private ILoadBalancer $loadBalancer;
103 private Config $config;
104 private LanguageNameUtils $languageNameUtils;
106 public function __construct(
107 RevisionLookup $revisionLookup,
108 ILoadBalancer $loadBalancer,
110 LanguageNameUtils $languageNameUtils
112 $this->revisionLookup = $revisionLookup;
113 $this->loadBalancer = $loadBalancer;
114 $this->config = $config;
115 $this->languageNameUtils = $languageNameUtils;
120 global $wgTranslateYamlLibrary, $wgLogTypes;
127 if ( !defined(
'TRANSLATE_FUZZY' ) ) {
128 define(
'TRANSLATE_FUZZY',
'!!FUZZY!!' );
131 $wgTranslateYamlLibrary ??= function_exists(
'yaml_parse' ) ?
'phpyaml' :
'spyc';
133 $hooks[
'PageSaveComplete'][] = [ TranslateEditAddons::class,
'onSaveComplete' ];
136 global $wgEnablePageTranslation;
137 if ( $wgEnablePageTranslation ) {
139 global $wgSpecialPages, $wgAvailableRights;
140 $wgSpecialPages[
'PageTranslation'] = [
141 'class' => PageTranslationSpecialPage::class,
147 'Translate:TranslatablePageMarker',
148 'Translate:TranslatablePageParser',
149 'Translate:MessageGroupMetadata',
150 'Translate:TranslatablePageView',
151 'Translate:TranslatablePageStateStore',
155 $wgSpecialPages[
'PageTranslationDeletePage'] = [
156 'class' => DeleteTranslatableBundleSpecialPage::class,
159 'Translate:TranslatableBundleDeleter',
160 'Translate:TranslatableBundleFactory',
165 $wgAvailableRights[] =
'pagetranslation';
167 $wgSpecialPages[
'PageMigration'] = MigrateTranslatablePageSpecialPage::class;
168 $wgSpecialPages[
'PagePreparation'] = PrepareTranslatablePageSpecialPage::class;
170 global $wgActionFilteredLogs, $wgLogActionsHandlers;
179 if ( !in_array(
'pagetranslation', $wgLogTypes ) ) {
180 $wgLogTypes[] =
'pagetranslation';
182 $wgLogActionsHandlers[
'pagetranslation/mark'] = TranslatableBundleLogFormatter::class;
183 $wgLogActionsHandlers[
'pagetranslation/unmark'] = TranslatableBundleLogFormatter::class;
184 $wgLogActionsHandlers[
'pagetranslation/moveok'] = TranslatableBundleLogFormatter::class;
185 $wgLogActionsHandlers[
'pagetranslation/movenok'] = TranslatableBundleLogFormatter::class;
186 $wgLogActionsHandlers[
'pagetranslation/deletelok'] = TranslatableBundleLogFormatter::class;
187 $wgLogActionsHandlers[
'pagetranslation/deletefok'] = TranslatableBundleLogFormatter::class;
188 $wgLogActionsHandlers[
'pagetranslation/deletelnok'] = TranslatableBundleLogFormatter::class;
189 $wgLogActionsHandlers[
'pagetranslation/deletefnok'] = TranslatableBundleLogFormatter::class;
190 $wgLogActionsHandlers[
'pagetranslation/encourage'] = TranslatableBundleLogFormatter::class;
191 $wgLogActionsHandlers[
'pagetranslation/discourage'] = TranslatableBundleLogFormatter::class;
192 $wgLogActionsHandlers[
'pagetranslation/prioritylanguages'] = TranslatableBundleLogFormatter::class;
193 $wgLogActionsHandlers[
'pagetranslation/associate'] = TranslatableBundleLogFormatter::class;
194 $wgLogActionsHandlers[
'pagetranslation/dissociate'] = TranslatableBundleLogFormatter::class;
195 $wgActionFilteredLogs[
'pagetranslation'] = [
196 'mark' => [
'mark' ],
197 'unmark' => [
'unmark' ],
198 'move' => [
'moveok',
'movenok' ],
199 'delete' => [
'deletefok',
'deletefnok',
'deletelok',
'deletelnok' ],
200 'encourage' => [
'encourage' ],
201 'discourage' => [
'discourage' ],
202 'prioritylanguages' => [
'prioritylanguages' ],
203 'aggregategroups' => [
'associate',
'dissociate' ],
206 if ( !in_array(
'messagebundle', $wgLogTypes ) ) {
207 $wgLogTypes[] =
'messagebundle';
209 $wgLogActionsHandlers[
'messagebundle/moveok'] = TranslatableBundleLogFormatter::class;
210 $wgLogActionsHandlers[
'messagebundle/movenok'] = TranslatableBundleLogFormatter::class;
211 $wgLogActionsHandlers[
'messagebundle/deletefok'] = TranslatableBundleLogFormatter::class;
212 $wgLogActionsHandlers[
'messagebundle/deletefnok'] = TranslatableBundleLogFormatter::class;
213 $wgActionFilteredLogs[
'messagebundle'] = [
214 'move' => [
'moveok',
'movenok' ],
215 'delete' => [
'deletefok',
'deletefnok' ],
218 $wgLogActionsHandlers[
'import/translatable-bundle'] = TranslatableBundleLogFormatter::class;
220 global $wgJobClasses;
221 $wgJobClasses[
'RenderTranslationPageJob'] = RenderTranslationPageJob::class;
222 $wgJobClasses[
'NonPrioritizedRenderTranslationPageJob'] = RenderTranslationPageJob::class;
223 $wgJobClasses[
'MoveTranslatableBundleJob'] = MoveTranslatableBundleJob::class;
224 $wgJobClasses[
'DeleteTranslatableBundleJob'] = DeleteTranslatableBundleJob::class;
225 $wgJobClasses[
'UpdateTranslatablePageJob'] = UpdateTranslatablePageJob::class;
228 global $wgAPIModules;
229 $wgAPIModules[
'markfortranslation'] = [
230 'class' => MarkForTranslationActionApi::class,
232 'Translate:TranslatablePageMarker',
233 'Translate:MessageGroupMetadata',
238 global $wgNamespacesWithSubpages, $wgNamespaceProtection;
239 global $wgTranslateMessageNamespaces;
241 $wgNamespacesWithSubpages[NS_TRANSLATIONS] =
true;
242 $wgNamespacesWithSubpages[NS_TRANSLATIONS_TALK] =
true;
245 $wgNamespaceProtection[NS_TRANSLATIONS] = [
'translate' ];
246 $wgTranslateMessageNamespaces[] = NS_TRANSLATIONS;
251 $hooks[
'BeforePageDisplay'][] = [ Hooks::class,
'onBeforePageDisplay' ];
254 $hooks[
'VisualEditorBeforeEditor'][] = [ Hooks::class,
'onVisualEditorBeforeEditor' ];
257 $hooks[
'MultiContentSave'][] = [ Hooks::class,
'tpSyntaxCheck' ];
258 $hooks[
'EditFilterMergedContent'][] =
259 [ Hooks::class,
'tpSyntaxCheckForEditContent' ];
262 $hooks[
'PageSaveComplete'][] = [ Hooks::class,
'addTranstagAfterSave' ];
264 $hooks[
'RevisionRecordInserted'][] = [ Hooks::class,
'updateTranstagOnNullRevisions' ];
267 $hooks[
'ParserFirstCallInit'][] = [ self::class,
'setupParserHooks' ];
268 $hooks[
'LanguageLinks'][] = [ Hooks::class,
'addLanguageLinks' ];
269 $hooks[
'SkinTemplateGetLanguageLink'][] = [ Hooks::class,
'formatLanguageLink' ];
272 $hooks[
'GetMagicVariableIDs'][] = [ Hooks::class,
'onGetMagicVariableIDs' ];
273 $hooks[
'ParserGetVariableValueSwitch'][] = [ Hooks::class,
'onParserGetVariableValueSwitch' ];
276 $hooks[
'ParserBeforeInternalParse'][] = [ Hooks::class,
'renderTagPage' ];
278 $hooks[
'ParserBeforePreprocess'][] = [ Hooks::class,
'preprocessTagPage' ];
280 $hooks[
'BeforeParserFetchTemplateRevisionRecord'][] =
281 [ Hooks::class,
'fetchTranslatableTemplateAndTitle' ];
284 $hooks[
'PageContentLanguage'][] = [ Hooks::class,
'onPageContentLanguage' ];
287 $hooks[
'getUserPermissionsErrorsExpensive'][] =
288 [ Hooks::class,
'onGetUserPermissionsErrorsExpensive' ];
290 $hooks[
'getUserPermissionsErrorsExpensive'][] =
291 [ Hooks::class,
'preventDirectEditing' ];
294 $hooks[
'ArticleViewHeader'][] = [ Hooks::class,
'translatablePageHeader' ];
297 $hooks[
'TitleGetEditNotices'][] = [ Hooks::class,
'onTitleGetEditNotices' ];
300 $hooks[
'SpecialPage_initList'][] = [ Hooks::class,
'replaceMovePage' ];
302 $hooks[
'getUserPermissionsErrorsExpensive'][] =
303 [ Hooks::class,
'lockedPagesCheck' ];
305 $hooks[
'ArticleConfirmDelete'][] = [ Hooks::class,
'disableDelete' ];
308 $hooks[
'SkinSubPageSubtitle'][] = [ Hooks::class,
'replaceSubtitle' ];
312 $hooks[
'SkinTemplateNavigation::Universal'][] = [ Hooks::class,
'onSkinTemplateNavigation__Universal' ];
315 $hooks[
'PageMoveComplete'][] = [ Hooks::class,
'onMovePageTranslationUnits' ];
318 $hooks[
'ArticleDeleteComplete'][] = [ Hooks::class,
'onDeleteTranslationUnit' ];
321 $hooks[
'ReplaceTextFilterPageTitlesForEdit'][] = [ Hooks::class,
'onReplaceTextFilterPageTitlesForEdit' ];
323 $hooks[
'ReplaceTextFilterPageTitlesForRename'][] =
324 [ Hooks::class,
'onReplaceTextFilterPageTitlesForRename' ];
327 $hooks[
'LinksUpdateComplete'][] = [ Hooks::class,
'onLinksUpdateComplete' ];
330 global $wgTranslateUseSandbox;
331 if ( $wgTranslateUseSandbox ) {
332 global $wgSpecialPages, $wgAvailableRights, $wgDefaultUserOptions;
334 $wgSpecialPages[
'ManageTranslatorSandbox'] = [
335 'class' => ManageTranslatorSandboxSpecialPage::class,
337 'Translate:TranslationStashReader',
339 'Translate:TranslateSandbox',
343 return new ServiceOptions(
344 ManageTranslatorSandboxSpecialPage::CONSTRUCTOR_OPTIONS,
345 MediaWikiServices::getInstance()->getMainConfig()
350 $wgSpecialPages[
'TranslationStash'] = [
351 'class' => TranslationStashSpecialPage::class,
354 'Translate:TranslationStashReader',
360 return new ServiceOptions(
361 TranslationStashSpecialPage::CONSTRUCTOR_OPTIONS,
362 MediaWikiServices::getInstance()->getMainConfig()
367 $wgDefaultUserOptions[
'translate-sandbox'] =
'';
369 $wgAvailableRights[] =
'translate-sandboxmanage';
371 global $wgLogTypes, $wgLogActionsHandlers;
373 if ( !in_array(
'translatorsandbox', $wgLogTypes ) ) {
374 $wgLogTypes[] =
'translatorsandbox';
377 $wgLogActionsHandlers[
'translatorsandbox/promoted'] = TranslateLogFormatter::class;
378 $wgLogActionsHandlers[
'translatorsandbox/rejected'] = TranslateLogFormatter::class;
382 $wgLogActionsHandlers[
'newusers/tsbpromoted'] = LogFormatter::class;
384 $wgJobClasses[
'TranslateSandboxEmailJob'] = TranslateSandboxEmailJob::class;
386 global $wgAPIModules;
387 $wgAPIModules[
'translationstash'] = [
388 'class' => TranslationStashActionApi::class,
390 'DBLoadBalancerFactory',
392 'Translate:MessageIndex'
395 $wgAPIModules[
'translatesandbox'] = [
396 'class' => TranslatorSandboxActionApi::class,
400 'UserOptionsManager',
403 'Translate:TranslateSandbox',
407 return new ServiceOptions(
408 TranslatorSandboxActionApi::CONSTRUCTOR_OPTIONS,
409 MediaWikiServices::getInstance()->getMainConfig()
416 global $wgNamespaceRobotPolicies;
417 $wgNamespaceRobotPolicies[NS_TRANSLATIONS] =
'noindex';
420 global $wgTranslateTranslationDefaultService,
421 $wgTranslateTranslationServices;
422 if ( $wgTranslateTranslationDefaultService ===
true ) {
423 $wgTranslateTranslationDefaultService =
'TTMServer';
424 if ( !isset( $wgTranslateTranslationServices[
'TTMServer'] ) ) {
425 $wgTranslateTranslationServices[
'TTMServer'] = [
428 'type' =>
'ttmserver',
434 global $wgTranslateEnableMessageGroupSubscription;
435 if ( $wgTranslateEnableMessageGroupSubscription ) {
436 if ( !ExtensionRegistry::getInstance()->isLoaded(
'Echo' ) ) {
437 throw new ConfigException(
438 'Translate: Message group subscriptions (TranslateEnableMessageGroupSubscription) are ' .
439 'enabled but Echo extension is not installed'
442 MessageGroupSubscriptionHookHandler::registerHooks( $hooks );
443 $wgJobClasses[
'MessageGroupSubscriptionNotificationJob'] = MessageGroupSubscriptionNotificationJob::class;
446 global $wgTranslateEnableEventLogging;
447 if ( $wgTranslateEnableEventLogging ) {
448 if ( !ExtensionRegistry::getInstance()->isLoaded(
'EventLogging' ) ) {
449 throw new ConfigException(
450 'Translate: Event logging (TranslateEnableEventLogging) is ' .
451 'enabled but EventLogging extension is not installed'
456 global $wgTranslateEnableLuaIntegration;
457 if ( $wgTranslateEnableLuaIntegration ) {
458 if ( ExtensionRegistry::getInstance()->isLoaded(
'Scribunto' ) ) {
459 $hooks[
'ScribuntoExternalLibraries' ][] =
static function (
string $engine, array &$extraLibraries ) {
460 $scribuntoHookHandler =
new ScribuntoHookHandler();
461 $scribuntoHookHandler->onScribuntoExternalLibraries( $engine, $extraLibraries );
465 'Translate: Lua integration (TranslateEnableLuaIntegration) is ' .
466 'enabled but Scribunto extension is not installed'
471 static::registerHookHandlers( $hooks );
474 private static function registerHookHandlers( array $hooks ): void {
475 if ( defined(
'MW_PHPUNIT_TEST' ) && MediaWikiServices::hasInstance() ) {
478 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
479 foreach ( $hooks as $name => $handlers ) {
480 foreach ( $handlers as $h ) {
481 $hookContainer->register( $name, $h );
485 $settingsBuilder = SettingsBuilder::getInstance();
486 $settingsBuilder->registerHookHandlers( $hooks );
495 $reservedUsernames[] =
FuzzyBot::getName();
496 $reservedUsernames[] = TranslateUserManager::getName();
501 VariableHolder &$vars, Title $title, User $user
507 if ( $handle->isMessageNamespace() ) {
508 $vars->setLazyLoadVar(
509 'translate_source_text',
510 'translate-get-source',
511 [
'handle' => $handle ]
513 $vars->setLazyLoadVar(
514 'translate_target_language',
515 'translate-get-target-language',
516 [
'handle' => $handle ]
524 VariableHolder $vars,
528 if ( $method !==
'translate-get-source' && $method !==
'translate-get-target-language' ) {
532 $handle = $parameters[
'handle'];
534 if ( $handle->isValid() ) {
535 if ( $method ===
'translate-get-source' ) {
536 $group = $handle->getGroup();
537 $value = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
539 $value = $handle->getCode();
553 $builderValues[
'vars'][
'translate_source_text'] =
'translate-source-text';
554 $builderValues[
'vars'][
'translate_target_language'] =
'translate-target-language';
563 $parser->setHook(
'languages', [
Hooks::class,
'languages' ] );
574 if ( $handle->isMessageNamespace() ) {
584 global $wgTranslateDocumentationLanguageCode;
585 if ( $wgTranslateDocumentationLanguageCode ) {
588 $wgTranslateDocumentationLanguageCode === $code ||
594 $names[$wgTranslateDocumentationLanguageCode] =
595 wfMessage(
'translate-documentation-language' )->inLanguage( $code )->plain();
601 global $wgTranslateMessageNamespaces;
603 $insert[
'translation'] = [
604 'message' =>
'translate-searchprofile',
605 'tooltip' =>
'translate-searchprofile-tooltip',
606 'namespaces' => $wgTranslateMessageNamespaces,
610 $index = array_search(
'all', array_keys( $profiles ) );
613 if ( $index ===
false ) {
614 wfWarn(
'"all" not found in search profiles' );
615 $index = count( $profiles );
618 $profiles = array_merge(
619 array_slice( $profiles, 0, $index ),
621 array_slice( $profiles, $index )
627 SpecialSearch $search,
633 if ( $profile !==
'translation' ) {
637 if ( Services::getInstance()->getTtmServerFactory()->getDefaultForQuerying() instanceof
SearchableTtmServer ) {
638 $href = SpecialPage::getTitleFor(
'SearchTranslations' )
639 ->getFullUrl( [
'query' => $term ] );
640 $form = Html::successBox(
641 $search->msg(
'translate-searchprofile-note', $href )->parse(),
644 $search->getOutput()->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
648 if ( !$search->getSearchEngine()->supports(
'title-suffix-filter' ) ) {
653 foreach ( $opts as $key => $value ) {
654 $hidden .= Html::hidden( $key, $value );
657 $context = $search->getContext();
658 $code = $context->getLanguage()->getCode();
659 $selected = $context->getRequest()->getVal(
'languagefilter' );
661 $languages = Utilities::getLanguageNames( $code );
664 $selector =
new XmlSelect(
'languagefilter',
'languagefilter' );
665 $selector->setDefault( $selected );
666 $selector->addOption( wfMessage(
'translate-search-nofilter' )->text(),
'-' );
667 foreach ( $languages as $code => $name ) {
668 $selector->addOption(
"$code - $name", $code );
671 $selector = $selector->getHTML();
673 $label = Html::label(
674 wfMessage(
'translate-search-languagefilter' )->text(),
678 $form .= Html::rawElement(
680 [
'id' =>
'mw-searchoptions' ],
681 $hidden . $label . $selector
687 SpecialSearch $search,
691 if ( $profile !==
'translation' ) {
695 $context = $search->getContext();
696 $selected = $context->getRequest()->getVal(
'languagefilter' );
697 if ( $selected !==
'-' && $selected ) {
698 $engine->setFeatureData(
'title-suffix-filter',
"/$selected" );
699 $search->setExtraParam(
'languagefilter', $selected );
705 if ( $parser->getOptions()->getInterfaceMessage() ) {
708 $pageReference = $parser->getPage();
709 if ( !$pageReference ) {
713 $linkTarget = TitleValue::newFromPage( $pageReference );
714 $handle =
new MessageHandle( $linkTarget );
715 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
716 $parserOutput = $parser->getOutput();
717 $names = $parserOutput->getCategoryNames();
718 $parserCategories = [];
719 foreach ( $names as $name ) {
720 $parserCategories[$name] = $parserOutput->getCategorySortKey( $name );
722 $parserOutput->setExtensionData(
'translate-fake-categories', $parserCategories );
723 $parserOutput->setCategories( [] );
728 public static function showFakeCategories( OutputPage $outputPage, ParserOutput $parserOutput ): void {
729 $fakeCategories = $parserOutput->getExtensionData(
'translate-fake-categories' );
730 if ( $fakeCategories ) {
731 $outputPage->addCategoryLinks( $fakeCategories );
740 public static function addConfig( array &$vars, OutputPage $out ): void {
741 global $wgTranslateDocumentationLanguageCode,
742 $wgTranslatePermissionUrl,
743 $wgTranslateUseSandbox;
745 $title = $out->getTitle();
746 if ( $title->isSpecial(
'Translate' ) ||
747 $title->isSpecial(
'TranslationStash' ) ||
748 $title->isSpecial(
'SearchTranslations' )
750 $user = $out->getUser();
751 $vars[
'TranslateRight'] = $user->isAllowedAll(
'translate',
'edit' );
752 $vars[
'TranslateMessageReviewRight'] = $user->isAllowed(
'translate-messagereview' );
753 $vars[
'DeleteRight'] = $user->isAllowed(
'delete' );
754 $vars[
'TranslateManageRight'] = $user->isAllowed(
'translate-manage' );
755 $vars[
'wgTranslateDocumentationLanguageCode'] = $wgTranslateDocumentationLanguageCode;
756 $vars[
'wgTranslatePermissionUrl'] = $wgTranslatePermissionUrl;
757 $vars[
'wgTranslateUseSandbox'] = $wgTranslateUseSandbox;
763 global $wgTranslateUseSandbox;
765 if ( $wgTranslateUseSandbox ) {
766 $sectionLabel = wfMessage(
'adminlinks_users' )->text();
767 $row = $tree->getSection( $sectionLabel )->getRow(
'main' );
768 $row->addItem( ALItem::newFromSpecialPage(
'TranslateSandbox' ) );
777 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
781 foreach ( self::USER_MERGE_TABLES as $table => $field ) {
782 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
783 $dbw->newUpdateQueryBuilder()
786 ->set( [ $field => $newUser->getId() ] )
787 ->where( [ $field => $oldUser->getId() ] )
788 ->caller( __METHOD__ )
799 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
802 foreach ( self::USER_MERGE_TABLES as $table => $field ) {
803 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
804 $dbw->newDeleteQueryBuilder()
805 ->deleteFrom( $table )
806 ->where( [ $field => $oldUser->getId() ] )
807 ->caller( __METHOD__ )
822 if ( !$target->inNamespace( NS_SPECIAL ) ) {
826 [ $name, $subpage ] = MediaWikiServices::getInstance()
827 ->getSpecialPageFactory()->resolveAlias( $target->getDBkey() );
828 if ( $name !==
'MyLanguage' || $subpage ===
null || $subpage ===
'' ) {
832 $realTarget = Title::newFromText( $subpage );
833 if ( !$realTarget || !$realTarget->exists() ) {
844 $parser->setFunctionHook(
'translation', [ $this,
'translateRenderParserFunction' ] );
847 public function translateRenderParserFunction( Parser $parser ): string {
848 if ( $parser->getOptions()->getInterfaceMessage() ) {
851 $pageReference = $parser->getPage();
852 if ( !$pageReference ) {
855 $linkTarget = TitleValue::newFromPage( $pageReference );
856 $handle =
new MessageHandle( $linkTarget );
857 $code = $handle->getCode();
858 if ( $this->languageNameUtils->isKnownLanguageTag( $code ) ) {
870 IContextSource $context,
876 if ( !$content instanceof TextContent ) {
881 $text = $content->getText();
882 $title = $context->getTitle();
885 if ( !$handle->isValid() ) {
890 if ( $user->isAllowed(
'translate-manage' ) || $user->equals( FuzzyBot::getUser() ) ) {
895 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
896 $group = $handle->getGroup();
898 if ( method_exists( $group,
'getMessageContent' ) ) {
900 $definition = $group->getMessageContent( $handle );
902 $definition = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
905 $message =
new FatMessage( $handle->getKey(), $definition );
906 $message->setTranslation( $text );
908 $messageValidator = $group->getValidator();
909 if ( !$messageValidator ) {
913 $validationResponse = $messageValidator->validateMessage( $message, $handle->getCode() );
914 if ( $validationResponse->hasErrors() ) {
915 $status->fatal(
new ApiRawMessage(
916 $context->msg(
'translate-syntax-error' )->parse(),
917 'translate-validation-failed',
920 'errors' => $validationResponse->getDescriptiveErrors( $context ),
921 'warnings' => $validationResponse->getDescriptiveWarnings( $context )
934 $parentId = $revisionRecord->getParentId();
935 if ( $parentId === 0 || $parentId ===
null ) {
940 $prevRev = $this->revisionLookup->getRevisionById( $parentId );
941 if ( !$prevRev || !$revisionRecord->hasSameContent( $prevRev ) ) {
948 $tagsToCopy = [ RevTagStore::FUZZY_TAG, RevTagStore::TRANSVER_PROP ];
950 $db = $this->loadBalancer->getConnection( DB_PRIMARY );
955 'rt_type' =>
'rt_type',
956 'rt_page' =>
'rt_page',
957 'rt_revision' => $revisionRecord->getId(),
958 'rt_value' =>
'rt_value',
962 'rt_type' => $tagsToCopy,
963 'rt_revision' => $parentId,
971 $tags[] =
'translate-translation-pages';
976 if ( $this->config->get(
'EnablePageTranslation' ) ) {
977 $tags[] =
'translate-translation-pages';
981 public static function getLanguageJson( Context $context ): array {
983 'supportedLanguages' => Utilities::getLanguageNames( $context->getLanguage() ),
989 public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
990 $dir = dirname( __DIR__ );
994 if ( ExtensionRegistry::getInstance()->isLoaded(
'VisualEditor' ) ) {
995 $modules[
'ext.translate.ve' ] = [
996 'localBasePath' => $dir,
997 'remoteExtPath' =>
'Translate',
999 'resources/src/ve-translate/ve.ce.MWTranslateAnnotationNode.js',
1000 'resources/src/ve-translate/ve.dm.MWTranslateAnnotationNode.js',
1001 'resources/src/ve-translate/ve.ui.MWTranslateAnnotationContextItem.js',
1004 'ext.visualEditor.mwcore',
1007 'visualeditor-annotations-translate-start',
1008 'visualeditor-annotations-translate-end',
1009 'visualeditor-annotations-translate-description',
1010 'visualeditor-annotations-tvar-start',
1011 'visualeditor-annotations-tvar-end',
1012 'visualeditor-annotations-tvar-description',
1018 $resourceLoader->register( $modules );
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), $services->getContentLanguageCode() ->toString(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):?MessageGroupSubscriptionHookHandler { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory(), $services->get( 'Translate:HookRunner'),);}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array
Groups multiple message groups together as one group.
Special page which enables deleting translations of translatable bundles and translation pages.
Hooks for page translation.
API module to mark a page for translation.
Contains code for Special:PageMigration to migrate to page transation.
A special page for marking revisions of pages for translation.
Contains code to prepare a page 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...
Special page for managing sandboxed users.
Special page for new users to translate example messages.