2declare( strict_types=1 );
4namespace MediaWiki\Extension\Translate;
9use MediaWiki\Api\ApiRawMessage;
10use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
11use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
12use MediaWiki\Config\Config;
13use MediaWiki\Config\ConfigException;
14use MediaWiki\Config\ServiceOptions;
15use MediaWiki\Content\Content;
16use MediaWiki\Content\TextContent;
17use MediaWiki\Context\IContextSource;
18use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
47use MediaWiki\Html\Html;
48use MediaWiki\Language\Language;
49use MediaWiki\Language\LanguageNameUtils;
50use MediaWiki\Logging\LogFormatter;
51use MediaWiki\MediaWikiServices;
52use MediaWiki\Output\OutputPage;
53use MediaWiki\Parser\Hook\ParserFirstCallInitHook;
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\Search\SearchEngine;
63use MediaWiki\Settings\SettingsBuilder;
64use MediaWiki\SpecialPage\SpecialPage;
65use MediaWiki\Specials\SpecialSearch;
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\IConnectionProvider;
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',
102 public function __construct(
103 private readonly RevisionLookup $revisionLookup,
104 private readonly IConnectionProvider $dbProvider,
105 private readonly Config $config,
106 private readonly LanguageNameUtils $languageNameUtils,
112 global $wgTranslateYamlLibrary, $wgLogTypes;
119 if ( !defined(
'TRANSLATE_FUZZY' ) ) {
120 define(
'TRANSLATE_FUZZY',
'!!FUZZY!!' );
123 $wgTranslateYamlLibrary ??= function_exists(
'yaml_parse' ) ?
'phpyaml' :
'spyc';
125 $hooks[
'PageSaveComplete'][] = [ TranslateEditAddons::class,
'onSaveComplete' ];
128 global $wgEnablePageTranslation;
129 if ( $wgEnablePageTranslation ) {
131 global $wgSpecialPages, $wgAvailableRights;
132 $wgSpecialPages[
'PageTranslation'] = [
133 'class' => PageTranslationSpecialPage::class,
139 'Translate:TranslatablePageMarker',
140 'Translate:TranslatablePageParser',
141 'Translate:MessageGroupMetadata',
142 'Translate:TranslatablePageView',
143 'Translate:TranslatablePageStateStore',
147 $wgSpecialPages[
'PageTranslationDeletePage'] = [
148 'class' => DeleteTranslatableBundleSpecialPage::class,
151 'Translate:TranslatableBundleDeleter',
152 'Translate:TranslatableBundleFactory',
157 $wgAvailableRights[] =
'pagetranslation';
159 $wgSpecialPages[
'PageMigration'] = MigrateTranslatablePageSpecialPage::class;
160 $wgSpecialPages[
'PagePreparation'] = PrepareTranslatablePageSpecialPage::class;
162 global $wgActionFilteredLogs, $wgLogActionsHandlers;
171 if ( !in_array(
'pagetranslation', $wgLogTypes ) ) {
172 $wgLogTypes[] =
'pagetranslation';
174 $wgLogActionsHandlers[
'pagetranslation/mark'] = TranslatableBundleLogFormatter::class;
175 $wgLogActionsHandlers[
'pagetranslation/unmark'] = TranslatableBundleLogFormatter::class;
176 $wgLogActionsHandlers[
'pagetranslation/moveok'] = TranslatableBundleLogFormatter::class;
177 $wgLogActionsHandlers[
'pagetranslation/movenok'] = TranslatableBundleLogFormatter::class;
178 $wgLogActionsHandlers[
'pagetranslation/deletelok'] = TranslatableBundleLogFormatter::class;
179 $wgLogActionsHandlers[
'pagetranslation/deletefok'] = TranslatableBundleLogFormatter::class;
180 $wgLogActionsHandlers[
'pagetranslation/deletelnok'] = TranslatableBundleLogFormatter::class;
181 $wgLogActionsHandlers[
'pagetranslation/deletefnok'] = TranslatableBundleLogFormatter::class;
182 $wgLogActionsHandlers[
'pagetranslation/encourage'] = TranslatableBundleLogFormatter::class;
183 $wgLogActionsHandlers[
'pagetranslation/discourage'] = TranslatableBundleLogFormatter::class;
184 $wgLogActionsHandlers[
'pagetranslation/prioritylanguages'] = TranslatableBundleLogFormatter::class;
185 $wgLogActionsHandlers[
'pagetranslation/associate'] = TranslatableBundleLogFormatter::class;
186 $wgLogActionsHandlers[
'pagetranslation/dissociate'] = TranslatableBundleLogFormatter::class;
187 $wgActionFilteredLogs[
'pagetranslation'] = [
188 'mark' => [
'mark' ],
189 'unmark' => [
'unmark' ],
190 'move' => [
'moveok',
'movenok' ],
191 'delete' => [
'deletefok',
'deletefnok',
'deletelok',
'deletelnok' ],
192 'encourage' => [
'encourage' ],
193 'discourage' => [
'discourage' ],
194 'prioritylanguages' => [
'prioritylanguages' ],
195 'aggregategroups' => [
'associate',
'dissociate' ],
198 if ( !in_array(
'messagebundle', $wgLogTypes ) ) {
199 $wgLogTypes[] =
'messagebundle';
201 $wgLogActionsHandlers[
'messagebundle/moveok'] = TranslatableBundleLogFormatter::class;
202 $wgLogActionsHandlers[
'messagebundle/movenok'] = TranslatableBundleLogFormatter::class;
203 $wgLogActionsHandlers[
'messagebundle/deletefok'] = TranslatableBundleLogFormatter::class;
204 $wgLogActionsHandlers[
'messagebundle/deletefnok'] = TranslatableBundleLogFormatter::class;
205 $wgActionFilteredLogs[
'messagebundle'] = [
206 'move' => [
'moveok',
'movenok' ],
207 'delete' => [
'deletefok',
'deletefnok' ],
210 $wgLogActionsHandlers[
'import/translatable-bundle'] = TranslatableBundleLogFormatter::class;
212 global $wgJobClasses;
213 $wgJobClasses[
'RenderTranslationPageJob'] = RenderTranslationPageJob::class;
214 $wgJobClasses[
'NonPrioritizedRenderTranslationPageJob'] = RenderTranslationPageJob::class;
215 $wgJobClasses[
'MoveTranslatableBundleJob'] = MoveTranslatableBundleJob::class;
216 $wgJobClasses[
'DeleteTranslatableBundleJob'] = DeleteTranslatableBundleJob::class;
217 $wgJobClasses[
'UpdateTranslatablePageJob'] = UpdateTranslatablePageJob::class;
220 global $wgAPIModules;
221 $wgAPIModules[
'markfortranslation'] = [
222 'class' => MarkForTranslationActionApi::class,
224 'Translate:TranslatablePageMarker',
225 'Translate:MessageGroupMetadata',
230 global $wgNamespacesWithSubpages, $wgNamespaceProtection;
231 global $wgTranslateMessageNamespaces;
233 $wgNamespacesWithSubpages[NS_TRANSLATIONS] =
true;
234 $wgNamespacesWithSubpages[NS_TRANSLATIONS_TALK] =
true;
237 $wgNamespaceProtection[NS_TRANSLATIONS] = [
'translate' ];
238 $wgTranslateMessageNamespaces[] = NS_TRANSLATIONS;
243 $hooks[
'BeforePageDisplay'][] = [ Hooks::class,
'onBeforePageDisplay' ];
246 $hooks[
'VisualEditorBeforeEditor'][] = [ Hooks::class,
'onVisualEditorBeforeEditor' ];
249 $hooks[
'MultiContentSave'][] = [ Hooks::class,
'tpSyntaxCheck' ];
250 $hooks[
'EditFilterMergedContent'][] =
251 [ Hooks::class,
'tpSyntaxCheckForEditContent' ];
254 $hooks[
'PageSaveComplete'][] = [ Hooks::class,
'addTranstagAfterSave' ];
256 $hooks[
'RevisionRecordInserted'][] = [ Hooks::class,
'updateTranstagOnNullRevisions' ];
259 $hooks[
'ParserFirstCallInit'][] = [ self::class,
'setupParserHooks' ];
260 $hooks[
'LanguageLinks'][] = [ Hooks::class,
'addLanguageLinks' ];
261 $hooks[
'SkinTemplateGetLanguageLink'][] = [ Hooks::class,
'formatLanguageLink' ];
264 $hooks[
'GetMagicVariableIDs'][] = [ Hooks::class,
'onGetMagicVariableIDs' ];
265 $hooks[
'ParserGetVariableValueSwitch'][] = [ Hooks::class,
'onParserGetVariableValueSwitch' ];
268 $hooks[
'ParserBeforeInternalParse'][] = [ Hooks::class,
'renderTagPage' ];
270 $hooks[
'ParserBeforePreprocess'][] = [ Hooks::class,
'preprocessTagPage' ];
272 $hooks[
'BeforeParserFetchTemplateRevisionRecord'][] =
273 [ Hooks::class,
'fetchTranslatableTemplateAndTitle' ];
276 $hooks[
'PageContentLanguage'][] = [ Hooks::class,
'onPageContentLanguage' ];
279 $hooks[
'getUserPermissionsErrorsExpensive'][] =
280 [ Hooks::class,
'onGetUserPermissionsErrorsExpensive' ];
282 $hooks[
'getUserPermissionsErrorsExpensive'][] =
283 [ Hooks::class,
'preventDirectEditing' ];
285 $hooks[
'MovePageIsValidMove'][] = [ Hooks::class,
'preventMoves' ];
288 $hooks[
'ArticleViewHeader'][] = [ Hooks::class,
'translatablePageHeader' ];
291 $hooks[
'TitleGetEditNotices'][] = [ Hooks::class,
'onTitleGetEditNotices' ];
294 $hooks[
'SpecialPage_initList'][] = [ Hooks::class,
'replaceMovePage' ];
296 $hooks[
'getUserPermissionsErrorsExpensive'][] =
297 [ Hooks::class,
'lockedPagesCheck' ];
299 $hooks[
'ArticleConfirmDelete'][] = [ Hooks::class,
'disableDelete' ];
302 $hooks[
'SkinSubPageSubtitle'][] = [ Hooks::class,
'replaceSubtitle' ];
306 $hooks[
'SkinTemplateNavigation::Universal'][] = [ Hooks::class,
'onSkinTemplateNavigation__Universal' ];
309 $hooks[
'PageMoveComplete'][] = [ Hooks::class,
'onMovePageTranslationUnits' ];
312 $hooks[
'ArticleDeleteComplete'][] = [ Hooks::class,
'onDeleteTranslationUnit' ];
315 $hooks[
'ReplaceTextFilterPageTitlesForEdit'][] = [ Hooks::class,
'onReplaceTextFilterPageTitlesForEdit' ];
317 $hooks[
'ReplaceTextFilterPageTitlesForRename'][] =
318 [ Hooks::class,
'onReplaceTextFilterPageTitlesForRename' ];
321 $hooks[
'LinksUpdateComplete'][] = [ Hooks::class,
'onLinksUpdateComplete' ];
324 global $wgTranslateUseSandbox;
325 if ( $wgTranslateUseSandbox ) {
326 global $wgSpecialPages, $wgAvailableRights, $wgDefaultUserOptions;
328 $wgSpecialPages[
'ManageTranslatorSandbox'] = [
329 'class' => ManageTranslatorSandboxSpecialPage::class,
331 'Translate:TranslationStashReader',
333 'Translate:TranslateSandbox',
337 return new ServiceOptions(
338 ManageTranslatorSandboxSpecialPage::CONSTRUCTOR_OPTIONS,
339 MediaWikiServices::getInstance()->getMainConfig()
344 $wgSpecialPages[
'TranslationStash'] = [
345 'class' => TranslationStashSpecialPage::class,
348 'Translate:TranslationStashReader',
354 return new ServiceOptions(
355 TranslationStashSpecialPage::CONSTRUCTOR_OPTIONS,
356 MediaWikiServices::getInstance()->getMainConfig()
361 $wgDefaultUserOptions[
'translate-sandbox'] =
'';
363 $wgAvailableRights[] =
'translate-sandboxmanage';
365 global $wgLogTypes, $wgLogActionsHandlers;
367 if ( !in_array(
'translatorsandbox', $wgLogTypes ) ) {
368 $wgLogTypes[] =
'translatorsandbox';
371 $wgLogActionsHandlers[
'translatorsandbox/promoted'] = TranslateLogFormatter::class;
372 $wgLogActionsHandlers[
'translatorsandbox/rejected'] = TranslateLogFormatter::class;
376 $wgLogActionsHandlers[
'newusers/tsbpromoted'] = LogFormatter::class;
378 $wgJobClasses[
'TranslateSandboxEmailJob'] = TranslateSandboxEmailJob::class;
380 global $wgAPIModules;
381 $wgAPIModules[
'translationstash'] = [
382 'class' => TranslationStashActionApi::class,
384 'DBLoadBalancerFactory',
386 'Translate:MessageIndex'
389 $wgAPIModules[
'translatesandbox'] = [
390 'class' => TranslatorSandboxActionApi::class,
394 'UserOptionsManager',
397 'Translate:TranslateSandbox',
401 return new ServiceOptions(
402 TranslatorSandboxActionApi::CONSTRUCTOR_OPTIONS,
403 MediaWikiServices::getInstance()->getMainConfig()
410 global $wgNamespaceRobotPolicies;
411 $wgNamespaceRobotPolicies[NS_TRANSLATIONS] =
'noindex';
414 global $wgTranslateTranslationDefaultService,
415 $wgTranslateTranslationServices;
416 if ( $wgTranslateTranslationDefaultService ===
true ) {
417 $wgTranslateTranslationDefaultService =
'TTMServer';
418 if ( !isset( $wgTranslateTranslationServices[
'TTMServer'] ) ) {
419 $wgTranslateTranslationServices[
'TTMServer'] = [
422 'type' =>
'ttmserver',
428 global $wgTranslateEnableMessageGroupSubscription;
429 if ( $wgTranslateEnableMessageGroupSubscription ) {
430 if ( !ExtensionRegistry::getInstance()->isLoaded(
'Echo' ) ) {
431 throw new ConfigException(
432 'Translate: Message group subscriptions (TranslateEnableMessageGroupSubscription) are ' .
433 'enabled but Echo extension is not installed'
436 MessageGroupSubscriptionHookHandler::registerHooks( $hooks );
437 $wgJobClasses[
'MessageGroupSubscriptionNotificationJob'] = MessageGroupSubscriptionNotificationJob::class;
440 global $wgTranslateEnableEventLogging;
441 if ( $wgTranslateEnableEventLogging ) {
442 if ( !ExtensionRegistry::getInstance()->isLoaded(
'EventLogging' ) ) {
443 throw new ConfigException(
444 'Translate: Event logging (TranslateEnableEventLogging) is ' .
445 'enabled but EventLogging extension is not installed'
450 global $wgTranslateEnableLuaIntegration;
451 if ( $wgTranslateEnableLuaIntegration ) {
452 if ( ExtensionRegistry::getInstance()->isLoaded(
'Scribunto' ) ) {
453 $hooks[
'ScribuntoExternalLibraries' ][] =
static function (
string $engine, array &$extraLibraries ) {
454 $scribuntoHookHandler =
new ScribuntoHookHandler();
455 $scribuntoHookHandler->onScribuntoExternalLibraries( $engine, $extraLibraries );
459 'Translate: Lua integration (TranslateEnableLuaIntegration) is ' .
460 'enabled but Scribunto extension is not installed'
465 static::registerHookHandlers( $hooks );
468 private static function registerHookHandlers( array $hooks ): void {
469 if ( defined(
'MW_PHPUNIT_TEST' ) && MediaWikiServices::hasInstance() ) {
472 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
473 foreach ( $hooks as $name => $handlers ) {
474 foreach ( $handlers as $h ) {
475 $hookContainer->register( $name, $h );
479 $settingsBuilder = SettingsBuilder::getInstance();
480 $settingsBuilder->registerHookHandlers( $hooks );
489 $reservedUsernames[] =
FuzzyBot::getName();
490 $reservedUsernames[] = TranslateUserManager::getName();
495 VariableHolder &$vars, Title $title, User $user
501 if ( $handle->isMessageNamespace() ) {
502 $vars->setLazyLoadVar(
503 'translate_source_text',
504 'translate-get-source',
505 [
'handle' => $handle ]
507 $vars->setLazyLoadVar(
508 'translate_target_language',
509 'translate-get-target-language',
510 [
'handle' => $handle ]
518 VariableHolder $vars,
522 if ( $method !==
'translate-get-source' && $method !==
'translate-get-target-language' ) {
526 $handle = $parameters[
'handle'];
528 if ( $handle->isValid() ) {
529 if ( $method ===
'translate-get-source' ) {
530 $group = $handle->getGroup();
531 $value = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
533 $value = $handle->getCode();
547 $builderValues[
'vars'][
'translate_source_text'] =
'translate-source-text';
548 $builderValues[
'vars'][
'translate_target_language'] =
'translate-target-language';
557 $parser->setHook(
'languages', [
Hooks::class,
'languages' ] );
568 if ( $handle->isMessageNamespace() ) {
578 global $wgTranslateDocumentationLanguageCode;
579 if ( $wgTranslateDocumentationLanguageCode ) {
582 $wgTranslateDocumentationLanguageCode === $code ||
588 $names[$wgTranslateDocumentationLanguageCode] =
589 wfMessage(
'translate-documentation-language' )->inLanguage( $code )->plain();
595 global $wgTranslateMessageNamespaces;
597 $insert[
'translation'] = [
598 'message' =>
'translate-searchprofile',
599 'tooltip' =>
'translate-searchprofile-tooltip',
600 'namespaces' => $wgTranslateMessageNamespaces,
604 $index = array_search(
'all', array_keys( $profiles ) );
607 if ( $index ===
false ) {
608 wfWarn(
'"all" not found in search profiles' );
609 $index = count( $profiles );
612 $profiles = array_merge(
613 array_slice( $profiles, 0, $index ),
615 array_slice( $profiles, $index )
621 SpecialSearch $search,
627 if ( $profile !==
'translation' ) {
631 if ( Services::getInstance()->getTtmServerFactory()->getDefaultForQuerying() instanceof
SearchableTtmServer ) {
632 $href = SpecialPage::getTitleFor(
'SearchTranslations' )
633 ->getFullUrl( [
'query' => $term ] );
634 $form = Html::successBox(
635 $search->msg(
'translate-searchprofile-note', $href )->parse(),
638 $search->getOutput()->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
642 if ( !$search->getSearchEngine()->supports(
'title-suffix-filter' ) ) {
647 foreach ( $opts as $key => $value ) {
648 $hidden .= Html::hidden( $key, $value );
651 $context = $search->getContext();
652 $code = $context->getLanguage()->getCode();
653 $selected = $context->getRequest()->getVal(
'languagefilter' );
655 $languages = Utilities::getLanguageNames( $code );
658 $selector =
new XmlSelect(
'languagefilter',
'languagefilter' );
659 $selector->setDefault( $selected );
660 $selector->addOption( wfMessage(
'translate-search-nofilter' )->text(),
'-' );
661 foreach ( $languages as $code => $name ) {
662 $selector->addOption(
"$code - $name", $code );
665 $selector = $selector->getHTML();
667 $label = Html::label(
668 wfMessage(
'translate-search-languagefilter' )->text(),
672 $form .= Html::rawElement(
674 [
'id' =>
'mw-searchoptions' ],
675 $hidden . $label . $selector
681 SpecialSearch $search,
685 if ( $profile !==
'translation' ) {
689 $context = $search->getContext();
690 $selected = $context->getRequest()->getVal(
'languagefilter' );
691 if ( $selected !==
'-' && $selected ) {
692 $engine->setFeatureData(
'title-suffix-filter',
"/$selected" );
693 $search->setExtraParam(
'languagefilter', $selected );
699 if ( $parser->getOptions()->getInterfaceMessage() ) {
702 $pageReference = $parser->getPage();
704 $linkTarget = TitleValue::newFromPage( $pageReference );
706 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
707 $parserOutput = $parser->getOutput();
708 $names = $parserOutput->getCategoryNames();
709 $parserCategories = [];
710 foreach ( $names as $name ) {
711 $parserCategories[$name] = $parserOutput->getCategorySortKey( $name );
713 $parserOutput->setExtensionData(
'translate-fake-categories', $parserCategories );
714 $parserOutput->setCategories( [] );
719 public static function showFakeCategories( OutputPage $outputPage, ParserOutput $parserOutput ): void {
720 $fakeCategories = $parserOutput->getExtensionData(
'translate-fake-categories' );
721 if ( $fakeCategories ) {
722 $outputPage->addCategoryLinks( $fakeCategories );
731 public static function addConfig( array &$vars, OutputPage $out ): void {
732 global $wgTranslateDocumentationLanguageCode,
733 $wgTranslatePermissionUrl,
734 $wgTranslateUseSandbox;
736 $title = $out->getTitle();
737 if ( $title->isSpecial(
'Translate' ) ||
738 $title->isSpecial(
'TranslationStash' ) ||
739 $title->isSpecial(
'SearchTranslations' )
741 $user = $out->getUser();
742 $vars[
'TranslateRight'] = $user->isAllowedAll(
'translate',
'edit' );
743 $vars[
'TranslateMessageReviewRight'] = $user->isAllowed(
'translate-messagereview' );
744 $vars[
'DeleteRight'] = $user->isAllowed(
'delete' );
745 $vars[
'TranslateManageRight'] = $user->isAllowed(
'translate-manage' );
746 $vars[
'wgTranslateDocumentationLanguageCode'] = $wgTranslateDocumentationLanguageCode;
747 $vars[
'wgTranslatePermissionUrl'] = $wgTranslatePermissionUrl;
748 $vars[
'wgTranslateUseSandbox'] = $wgTranslateUseSandbox;
754 global $wgTranslateUseSandbox;
756 if ( $wgTranslateUseSandbox ) {
757 $sectionLabel = wfMessage(
'adminlinks_users' )->text();
758 $row = $tree->getSection( $sectionLabel )->getRow(
'main' );
759 $row->addItem( ALItem::newFromSpecialPage(
'TranslateSandbox' ) );
768 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
772 foreach ( self::USER_MERGE_TABLES as $table => $field ) {
773 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
774 $dbw->newUpdateQueryBuilder()
777 ->set( [ $field => $newUser->getId() ] )
778 ->where( [ $field => $oldUser->getId() ] )
779 ->caller( __METHOD__ )
790 $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
793 foreach ( self::USER_MERGE_TABLES as $table => $field ) {
794 if ( $dbw->tableExists( $table, __METHOD__ ) ) {
795 $dbw->newDeleteQueryBuilder()
796 ->deleteFrom( $table )
797 ->where( [ $field => $oldUser->getId() ] )
798 ->caller( __METHOD__ )
813 if ( !$target->inNamespace( NS_SPECIAL ) ) {
817 [ $name, $subpage ] = MediaWikiServices::getInstance()
818 ->getSpecialPageFactory()->resolveAlias( $target->getDBkey() );
819 if ( $name !==
'MyLanguage' || $subpage ===
null || $subpage ===
'' ) {
823 $realTarget = Title::newFromText( $subpage );
824 if ( !$realTarget || !$realTarget->exists() ) {
835 $parser->setFunctionHook(
'translation', [ $this,
'translateRenderParserFunction' ] );
838 public function translateRenderParserFunction( Parser $parser ): string {
839 if ( $parser->getOptions()->getInterfaceMessage() ) {
842 $pageReference = $parser->getPage();
843 $linkTarget = TitleValue::newFromPage( $pageReference );
844 $handle =
new MessageHandle( $linkTarget );
845 $code = $handle->getCode();
846 if ( $this->languageNameUtils->isKnownLanguageTag( $code ) ) {
858 IContextSource $context,
864 if ( !$content instanceof TextContent ) {
869 $text = $content->getText();
870 $title = $context->getTitle();
873 if ( !$handle->isValid() ) {
878 if ( $user->isAllowed(
'translate-manage' ) || $user->equals( FuzzyBot::getUser() ) ) {
883 if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
884 $group = $handle->getGroup();
886 if ( method_exists( $group,
'getMessageContent' ) ) {
888 $definition = $group->getMessageContent( $handle );
890 $definition = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
893 $message =
new FatMessage( $handle->getKey(), $definition );
894 $message->setTranslation( $text );
896 $messageValidator = $group->getValidator();
897 if ( !$messageValidator ) {
901 $validationResponse = $messageValidator->validateMessage( $message, $handle->getCode() );
902 if ( $validationResponse->hasErrors() ) {
903 $errors = $validationResponse->getDescriptiveErrors( $context );
904 $formattedErrors =
'';
905 foreach ( $errors as $error ) {
906 $formattedErrors .= Html::rawElement(
'li', [], $error );
908 $formattedErrors = Html::rawElement(
'ul', [], $formattedErrors );
909 $status->fatal(
new ApiRawMessage(
910 $context->msg(
'translate-syntax-errors' )->rawParams( $formattedErrors )->parse(),
911 'translate-validation-failed',
915 'warnings' => $validationResponse->getDescriptiveWarnings( $context )
928 $parentId = $revisionRecord->getParentId();
929 if ( $parentId === 0 || $parentId ===
null ) {
934 $prevRev = $this->revisionLookup->getRevisionById( $parentId );
935 if ( !$prevRev || !$revisionRecord->hasSameContent( $prevRev ) ) {
942 $tagsToCopy = [ RevTagStore::FUZZY_TAG, RevTagStore::TRANSVER_PROP ];
944 $db = $this->dbProvider->getPrimaryDatabase();
949 'rt_type' =>
'rt_type',
950 'rt_page' =>
'rt_page',
951 'rt_revision' => $revisionRecord->getId(),
952 'rt_value' =>
'rt_value',
956 'rt_type' => $tagsToCopy,
957 'rt_revision' => $parentId,
965 $tags[] =
'translate-translation-pages';
970 if ( $this->config->get(
'EnablePageTranslation' ) ) {
971 $tags[] =
'translate-translation-pages';
975 public static function getLanguageJson( Context $context ): array {
977 'supportedLanguages' => Utilities::getLanguageNames( $context->getLanguage() ),
983 public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
984 $dir = dirname( __DIR__ );
988 if ( ExtensionRegistry::getInstance()->isLoaded(
'VisualEditor' ) ) {
989 $modules[
'ext.translate.ve' ] = [
990 'localBasePath' => $dir,
991 'remoteExtPath' =>
'Translate',
993 'resources/src/ve-translate/ve.ce.MWTranslateAnnotationNode.js',
994 'resources/src/ve-translate/ve.dm.MWTranslateAnnotationNode.js',
995 'resources/src/ve-translate/ve.ui.MWTranslateAnnotationContextItem.js',
998 'ext.visualEditor.mwcore',
1001 'visualeditor-annotations-translate-start',
1002 'visualeditor-annotations-translate-end',
1003 'visualeditor-annotations-translate-description',
1004 'visualeditor-annotations-tvar-start',
1005 'visualeditor-annotations-tvar-end',
1006 'visualeditor-annotations-tvar-description',
1012 $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->getConnectionProvider());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.