54 private const GROUP_SYNC_INFO_WRAPPER_CLASS =
'smg-group-sync-cache-info';
55 private const RIGHT =
'translate-manage';
56 protected DifferenceEngine $diff;
63 private Language $contLang;
64 private NamespaceInfo $nsInfo;
65 private RevisionLookup $revLookup;
68 private JobQueueGroup $jobQueueGroup;
70 private LinkBatchFactory $linkBatchFactory;
73 public function __construct(
75 NamespaceInfo $nsInfo,
76 RevisionLookup $revLookup,
78 JobQueueGroup $jobQueueGroup,
80 LinkBatchFactory $linkBatchFactory,
84 parent::__construct(
'ManageMessageGroups' );
85 $this->contLang = $contLang;
86 $this->nsInfo = $nsInfo;
87 $this->revLookup = $revLookup;
88 $this->synchronizationCache = $synchronizationCache;
90 $this->jobQueueGroup = $jobQueueGroup;
91 $this->messageIndex = $messageIndex;
92 $this->linkBatchFactory = $linkBatchFactory;
93 $this->messageGroupSubscription = $messageGroupSubscription;
96 public function doesWrites() {
100 protected function getGroupName() {
101 return 'translation';
104 public function getDescription() {
105 return $this->msg(
'managemessagegroups' );
108 public function execute( $par ) {
111 $out = $this->getOutput();
112 $out->addModuleStyles( [
113 'ext.translate.specialpages.styles',
114 'mediawiki.codex.messagebox.styles',
116 $out->addModules(
'ext.translate.special.managegroups' );
117 $out->addHelpLink(
'Help:Extension:Translate/Group_management' );
119 $this->name = $par ?: MessageChangeStorage::DEFAULT_NAME;
123 if ( $this->getConfig()->
get(
'TranslateGroupSynchronizationCache' ) ) {
125 $this->displayGroupSyncInfo->getGroupsInSyncHtml(
126 $this->synchronizationCache->getGroupsInSync(),
127 self::GROUP_SYNC_INFO_WRAPPER_CLASS
132 $this->displayGroupSyncInfo->getHtmlForGroupsWithError(
133 $this->synchronizationCache,
134 self::GROUP_SYNC_INFO_WRAPPER_CLASS,
142 $out->addWikiMsg(
'translate-smg-nochanges' );
147 $user = $this->getUser();
148 $this->hasRight = $user->isAllowed( self::RIGHT );
150 $req = $this->getRequest();
151 if ( !$req->wasPosted() ) {
152 $this->showChanges( $this->
getLimit() );
157 $block = $user->getBlock();
158 if ( $block && $block->isSitewide() ) {
159 throw new UserBlockedError(
162 $this->getLanguage(),
167 $csrfTokenSet = $this->getContext()->getCsrfTokenSet();
168 if ( !$this->hasRight || !$csrfTokenSet->matchTokenField(
'token' ) ) {
169 throw new PermissionsError( self::RIGHT );
172 $this->processSubmit();
179 ini_get(
'max_input_vars' ),
180 ini_get(
'suhosin.post.max_vars' ),
181 ini_get(
'suhosin.request.max_vars' )
184 $limits = array_filter( $limits );
185 return (
int)min( $limits );
188 protected function getLegend(): string {
189 $text = $this->diff->addHeader(
191 $this->msg(
'translate-smg-left' )->escaped(),
192 $this->msg(
'translate-smg-right' )->escaped()
195 return Html::rawElement(
'div', [
'class' =>
'mw-translate-smg-header' ], $text );
198 protected function showChanges(
int $limit ): void {
199 $diff = new DifferenceEngine( $this->getContext() );
200 $diff->showDiffStyle();
201 $diff->setReducedLineNumbers();
204 $out = $this->getOutput();
206 Html::openElement(
'form', [
'method' =>
'post',
'id' =>
'smgForm',
'data-name' => $this->name ] ) .
207 Html::hidden(
'token', $this->getContext()->getCsrfTokenSet()->getToken() ) .
208 Html::hidden(
'changesetModifiedTime',
209 MessageChangeStorage::getLastModifiedTime( $this->cdb ) ) .
216 $groupSyncCacheEnabled = $this->getConfig()->get(
'TranslateGroupSynchronizationCache' );
217 if ( $groupSyncCacheEnabled ) {
219 $this->displayGroupSyncInfo->getGroupsInSyncHtml(
220 $this->synchronizationCache->getGroupsInSync(),
221 self::GROUP_SYNC_INFO_WRAPPER_CLASS
226 $this->displayGroupSyncInfo->getHtmlForGroupsWithError(
227 $this->synchronizationCache,
228 self::GROUP_SYNC_INFO_WRAPPER_CLASS,
234 $reader = Reader::open( $this->cdb );
235 $groups = $this->getGroupsFromCdb( $reader );
236 foreach ( $groups as $id => $group ) {
237 $sourceChanges = MessageSourceChange::loadModifications(
238 Utilities::deserialize( $reader->get( $id ) )
240 $out->addHTML( Html::element(
'h2', [], $group->getLabel() ) );
242 if ( $groupSyncCacheEnabled && $this->synchronizationCache->groupHasErrors( $id ) ) {
244 Html::warningBox( $this->msg(
'translate-smg-group-sync-error-warn' )->escaped(),
'center' )
249 $lb = $this->linkBatchFactory->newLinkBatch();
250 $ns = $group->getNamespace();
251 $isCap = $this->nsInfo->isCapitalized( $ns );
252 $languages = $sourceChanges->getLanguages();
254 foreach ( $languages as $language ) {
255 $languageChanges = $sourceChanges->getModificationsForLanguage( $language );
256 foreach ( $languageChanges as $changes ) {
257 foreach ( $changes as $params ) {
259 $key = $params[
'key'];
261 $key = $this->contLang->ucfirst( $key );
263 $lb->add( $ns,
"$key/$language" );
269 foreach ( $languages as $language ) {
272 $changes[ MessageSourceChange::ADDITION ] = $sourceChanges->getAdditions( $language );
273 $changes[ MessageSourceChange::DELETION ] = $sourceChanges->getDeletions( $language );
274 $changes[ MessageSourceChange::CHANGE ] = $sourceChanges->getChanges( $language );
276 foreach ( $changes as $type => $messages ) {
277 foreach ( $messages as $params ) {
278 $change = $this->formatChange( $group, $sourceChanges, $language, $type, $params, $limit );
279 $out->addHTML( $change );
284 $out->wrapWikiMsg(
"<div class=warning>\n$1\n</div>",
'translate-smg-more' );
291 $this->showRenames( $group, $sourceChanges, $out, $language, $limit );
296 $button =
new ButtonInputWidget( [
298 'label' => $this->msg(
'translate-smg-submit' )->plain(),
299 'disabled' => !$this->hasRight ?
'disabled' : null,
300 'classes' => [
'mw-translate-smg-submit' ],
301 'title' => !$this->hasRight ? $this->msg(
'translate-smg-notallowed' )->plain() : null,
302 'flags' => [
'primary',
'progressive' ],
304 $out->addHTML( $button );
305 $out->addHTML( Html::closeElement(
'form' ) );
308 protected function formatChange(
310 MessageSourceChange $changes,
316 $key = $params[
'key'];
317 $title = Title::makeTitleSafe( $group->
getNamespace(),
"$key/$language" );
318 $id = self::changeId( $group->
getId(), $language, $type, $key );
320 $isReusedKey =
false;
322 if ( $title && $type ===
'addition' && $title->exists() ) {
331 $noticeHtml .= Html::warningBox( $this->msg(
'translate-manage-key-reused' )->parse() );
333 } elseif ( $title && ( $type ===
'deletion' || $type ===
'change' ) && !$title->exists() ) {
340 $titleLink = $this->getLinkRenderer()->makeLink( $title );
342 if ( $type ===
'deletion' ) {
343 $revTitle = $this->revLookup->getRevisionByTitle( $title );
345 wfWarn(
"[ManageGroupSpecialPage] No revision associated with {$title->getPrefixedText()}" );
347 $content = $revTitle ? $revTitle->getContent( SlotRecord::MAIN ) :
null;
348 $wiki = ( $content instanceof TextContent ) ? $content->getText() :
'';
350 if ( $wiki ===
'' ) {
351 $noticeHtml .= Html::warningBox(
352 $this->msg(
'translate-manage-empty-content' )->parse()
356 $newRevision =
new MutableRevisionRecord( $title );
357 $newRevision->setContent( SlotRecord::MAIN, ContentHandler::makeContent(
'', $title ) );
359 $this->diff->setRevisions( $revTitle, $newRevision );
360 $text = $this->diff->getDiff( $titleLink,
'', $noticeHtml );
361 } elseif ( $type ===
'addition' ) {
364 if ( $sourceLanguage === $language ) {
365 if ( $this->hasRight ) {
366 $menu = Html::rawElement(
369 'class' =>
'smg-rename-actions',
371 'data-group-id' => $group->
getId(),
372 'data-lang' => $language,
373 'data-msgkey' => $key,
374 'data-msgtitle' => $title->getFullText()
378 } elseif ( !self::isMessageDefinitionPresent( $group, $changes, $key ) ) {
379 $noticeHtml .= Html::warningBox(
380 $this->msg(
'translate-manage-source-message-not-found' )->parse(),
381 'mw-translate-smg-notice-important'
385 $menu = Html::hidden(
"msg/$id",
'ignore', [
'id' =>
"i/$id" ] );
389 if ( $params[
'content'] ===
'' ) {
390 $noticeHtml .= Html::warningBox(
391 $this->msg(
'translate-manage-empty-content' )->parse()
395 $oldRevision =
new MutableRevisionRecord( $title );
396 $oldRevision->setContent( SlotRecord::MAIN, ContentHandler::makeContent(
'', $title ) );
398 $newRevision =
new MutableRevisionRecord( $title );
399 $newRevision->setContent(
401 ContentHandler::makeContent( (
string)$params[
'content'], $title )
404 $this->diff->setRevisions( $oldRevision, $newRevision );
405 $text = $this->diff->getDiff(
'', $titleLink . $menu, $noticeHtml );
406 } elseif ( $type ===
'change' ) {
407 $wiki = Utilities::getContentForTitle( $title,
true );
414 $shouldFuzzy = $sourceLanguage === $language && $wiki !== $params[
'content'];
416 if ( $sourceLanguage === $language ) {
417 $label = $this->msg(
'translate-manage-action-fuzzy' )->text();
418 $actions .= $this->radioLabel( $label,
"msg/$id",
"fuzzy", $shouldFuzzy );
422 $sourceLanguage !== $language &&
424 !self::isMessageDefinitionPresent( $group, $changes, $key )
426 $noticeHtml .= Html::warningBox(
427 $this->msg(
'translate-manage-source-message-not-found' )->parse(),
428 'mw-translate-smg-notice-important'
432 $actions .= Html::hidden(
"msg/$id",
'ignore', [
'id' =>
"i/$id" ] );
435 $label = $this->msg(
'translate-manage-action-import' )->text();
436 $actions .= $this->radioLabel( $label,
"msg/$id",
"import", !$shouldFuzzy );
438 $label = $this->msg(
'translate-manage-action-ignore' )->text();
439 $actions .= $this->radioLabel( $label,
"msg/$id",
"ignore" );
443 $oldRevision =
new MutableRevisionRecord( $title );
444 $oldRevision->setContent( SlotRecord::MAIN, ContentHandler::makeContent( (
string)$wiki, $title ) );
446 $newRevision =
new MutableRevisionRecord( $title );
447 $newRevision->setContent(
449 ContentHandler::makeContent( (
string)$params[
'content'], $title )
452 $this->diff->setRevisions( $oldRevision, $newRevision );
453 $text .= $this->diff->getDiff( $titleLink, $actions, $noticeHtml );
456 $hidden = Html::hidden( $id, 1 );
459 $classes =
"mw-translate-smg-change smg-change-$type";
466 return Html::rawElement(
'div', [
'class' => $classes ], $text );
469 protected function processSubmit(): void {
470 $req = $this->getRequest();
471 $out = $this->getOutput();
474 $modificationJobs = $renameJobData = [];
475 $lastModifiedTime = intval( $req->getVal(
'changesetModifiedTime' ) );
477 if ( !MessageChangeStorage::isModifiedSince( $this->cdb, $lastModifiedTime ) ) {
478 $out->addWikiMsg(
'translate-smg-changeset-modified' );
482 $reader = Reader::open( $this->cdb );
483 $groups = $this->getGroupsFromCdb( $reader );
484 $groupSyncCacheEnabled = $this->getConfig()->get(
'TranslateGroupSynchronizationCache' );
487 foreach ( $groups as $groupId => $group ) {
490 throw new RuntimeException(
"Expected $groupId to be FileBasedMessageGroup, got "
491 . get_class( $group )
495 $changes = Utilities::deserialize( $reader->get( $groupId ) );
496 if ( $groupSyncCacheEnabled && $this->synchronizationCache->groupHasErrors( $groupId ) ) {
497 $postponed[$groupId] = $changes;
501 $sourceChanges = MessageSourceChange::loadModifications( $changes );
502 $groupModificationJobs = [];
503 $groupRenameJobData = [];
504 $languages = $sourceChanges->getLanguages();
505 foreach ( $languages as $language ) {
507 $this->handleModificationsSubmit(
513 $groupModificationJobs
517 $this->handleRenameSubmit(
524 $groupModificationJobs
527 if ( !isset( $postponed[$groupId][$language] ) ) {
528 $group->getMessageGroupCache( $language )->create();
532 if ( $groupSyncCacheEnabled && !isset( $postponed[ $groupId ] ) ) {
533 $this->synchronizationCache->markGroupAsReviewed( $groupId );
536 $modificationJobs[$groupId] = $groupModificationJobs;
537 $renameJobData[$groupId] = $groupRenameJobData;
538 }
catch ( Exception $e ) {
540 "ManageGroupsSpecialPage: Error in processSubmit. Group: $groupId\n" .
544 $errorGroups[] = $group->
getLabel();
547 $this->messageGroupSubscription->queueNotificationJob();
549 $renameJobs = $this->createRenameJobs( $renameJobData );
550 $this->startSync( $modificationJobs, $renameJobs );
553 rename( $this->cdb, $this->cdb .
'-' . wfTimestamp() );
555 if ( $errorGroups ) {
556 $errorMsg = $this->getProcessingErrorMessage( $errorGroups, count( $groups ) );
560 'mw-translate-smg-submitted'
565 if ( count( $postponed ) ) {
566 $postponedSourceChanges = [];
567 foreach ( $postponed as $groupId => $changes ) {
568 $postponedSourceChanges[$groupId] = MessageSourceChange::loadModifications( $changes );
570 MessageChangeStorage::writeChanges( $postponedSourceChanges, $this->cdb );
572 $this->showChanges( $this->getLimit() );
573 } elseif ( $errorGroups === [] ) {
574 $out->addWikiMsg(
'translate-smg-submitted' );
578 protected static function changeId(
584 return 'smg/' . substr( sha1(
"$groupId/$language/$type/$key" ), 0, 7 );
591 public static function tabify( Skin $skin, array &$tabs ): void {
592 $title = $skin->getTitle();
593 if ( !$title->isSpecialPage() ) {
596 $specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
597 [ $alias, ] = $specialPageFactory->resolveAlias( $title->getText() );
600 'ManageMessageGroups' =>
'namespaces',
601 'AggregateGroups' =>
'namespaces',
602 'SupportedLanguages' =>
'views',
603 'TranslationStats' =>
'views',
605 if ( !isset( $pagesInGroup[$alias] ) ) {
609 $tabs[
'namespaces'] = [];
610 foreach ( $pagesInGroup as $spName => $section ) {
611 $spClass = $specialPageFactory->getPage( $spName );
613 if ( $spClass ===
null || $spClass instanceof DisabledSpecialPage ) {
616 $spTitle = $spClass->getPageTitle();
618 $tabs[$section][strtolower( $spName )] = [
619 'text' => $spClass->getDescription(),
620 'href' => $spTitle->getLocalURL(),
621 'class' => $alias === $spName ?
'selected' :
'',
630 private static function isMessageDefinitionPresent(
632 MessageSourceChange $changes,
635 $sourceLanguage = $group->getSourceLanguage();
636 if ( $changes->findMessage( $sourceLanguage, $msgKey, [ MessageSourceChange::ADDITION ] ) ) {
641 $sourceHandle =
new MessageHandle( Title::makeTitle( $namespace, $msgKey ) );
642 return $sourceHandle->isValid();
645 private function showRenames(
647 MessageSourceChange $sourceChanges,
652 $changes = $sourceChanges->getRenames( $language );
653 foreach ( $changes as $key => $params ) {
656 if ( !isset( $changes[ $key ] ) ) {
661 $sourceChanges->isEqual( $language, $key ) ) {
669 $secondKey = $sourceChanges->getMatchedKey( $language, $key ) ??
'';
670 $secondMsg = $sourceChanges->getMatchedMessage( $language, $key );
671 if ( $secondMsg ===
null ) {
672 throw new RuntimeException(
"Could not find matched message for $key" );
676 $sourceChanges->isPreviousState(
679 [ MessageSourceChange::ADDITION, MessageSourceChange::CHANGE ]
682 $addedMsg = $firstMsg;
683 $deletedMsg = $secondMsg;
685 $addedMsg = $secondMsg;
686 $deletedMsg = $firstMsg;
689 $change = $this->formatRename(
694 $sourceChanges->isEqual( $language, $key ),
697 $out->addHTML( $change );
700 unset( $changes[$secondKey] );
705 $out->wrapWikiMsg(
"<div class=warning>\n$1\n</div>",
'translate-smg-more' );
711 private function formatRename(
719 $addedKey = $addedMsg[
'key'];
720 $deletedKey = $deletedMsg[
'key'];
723 $addedTitle = Title::makeTitleSafe( $group->
getNamespace(),
"$addedKey/$language" );
724 $deletedTitle = Title::makeTitleSafe( $group->
getNamespace(),
"$deletedKey/$language" );
725 $id = self::changeId( $group->
getId(), $language, MessageSourceChange::RENAME, $addedKey );
727 $addedTitleLink = $this->getLinkRenderer()->makeLink( $addedTitle );
728 $deletedTitleLink = $this->getLinkRenderer()->makeLink( $deletedTitle );
730 $renameSelected =
true;
733 $renameSelected =
false;
734 $label = $this->msg(
'translate-manage-action-rename-fuzzy' )->text();
735 $actions .= $this->radioLabel( $label,
"msg/$id",
"renamefuzzy",
true );
738 $label = $this->msg(
'translate-manage-action-rename' )->text();
739 $actions .= $this->radioLabel( $label,
"msg/$id",
"rename", $renameSelected );
741 $label = $this->msg(
'translate-manage-action-import' )->text();
742 $actions .= $this->radioLabel( $label,
"msg/$id",
"import",
true );
747 $label = $this->msg(
'translate-manage-action-ignore-change' )->text();
748 $actions .= $this->radioLabel( $label,
"msg/$id",
"ignore" );
752 $addedContent = ContentHandler::makeContent( (
string)$addedMsg[
'content'], $addedTitle );
753 $addedRevision =
new MutableRevisionRecord( $addedTitle );
754 $addedRevision->setContent( SlotRecord::MAIN, $addedContent );
756 $deletedContent = ContentHandler::makeContent( (
string)$deletedMsg[
'content'], $deletedTitle );
757 $deletedRevision =
new MutableRevisionRecord( $deletedTitle );
758 $deletedRevision->setContent( SlotRecord::MAIN, $deletedContent );
760 $this->diff->setRevisions( $deletedRevision, $addedRevision );
765 $menu = Html::rawElement(
768 'class' =>
'smg-rename-actions',
770 'data-group-id' => $group->
getId(),
771 'data-msgkey' => $addedKey,
772 'data-msgtitle' => $addedTitle->getFullText()
777 $actions = Html::rawElement(
'div', [
'class' =>
'smg-change-import-options' ], $actions );
779 $text = $this->diff->getDiff(
781 $addedTitleLink . $menu . $actions,
782 $isEqual ? htmlspecialchars( $addedMsg[
'content'] ) :
''
785 $hidden = Html::hidden( $id, 1 );
789 return Html::rawElement(
791 [
'class' =>
'mw-translate-smg-change smg-change-rename' ],
796 private function getRenameJobParams(
798 MessageSourceChange $sourceChanges,
799 string $languageCode,
802 bool $isSourceLang =
true
804 if ( $selectedVal ===
'ignore' ) {
809 $currentMsgKey = $currentMsg[
'key'];
810 $matchedMsg = $sourceChanges->getMatchedMessage( $languageCode, $currentMsgKey );
811 if ( $matchedMsg ===
null ) {
812 throw new RuntimeException(
"Could not find matched message for $currentMsgKey." );
814 $matchedMsgKey = $matchedMsg[
'key'];
817 $sourceChanges->isPreviousState(
820 [ MessageSourceChange::ADDITION, MessageSourceChange::CHANGE ]
823 $params[
'target'] = $matchedMsgKey;
824 $params[
'replacement'] = $currentMsgKey;
825 $replacementContent = $currentMsg[
'content'];
827 $params[
'target'] = $currentMsgKey;
828 $params[
'replacement'] = $matchedMsgKey;
829 $replacementContent = $matchedMsg[
'content'];
832 $params[
'fuzzy'] = $selectedVal ===
'renamefuzzy';
834 $params[
'content'] = $replacementContent;
836 if ( $isSourceLang ) {
837 $params[
'targetTitle'] = Title::newFromText(
838 Utilities::title( $params[
'target'], $languageCode, $groupNamespace ),
841 $params[
'others'] = [];
847 private function handleRenameSubmit(
849 MessageSourceChange $sourceChanges,
854 array &$modificationJobs
856 $groupId = $group->getId();
857 $renames = $sourceChanges->getRenames( $language );
861 foreach ( $renames as $key => $params ) {
864 if ( !isset( $renames[$key] ) ) {
868 $id = self::changeId( $groupId, $language, MessageSourceChange::RENAME, $key );
870 [ $renameMissing, $isCurrentKeyPresent ] = $this->isRenameMissing(
880 if ( $renameMissing ) {
883 $postponed[$groupId][$language][MessageSourceChange::RENAME][$key] = $params;
887 if ( !$isCurrentKeyPresent ) {
892 $selectedVal = $req->getVal(
"msg/$id" );
893 $jobParams = $this->getRenameJobParams(
902 if ( $jobParams ===
null ) {
906 $targetStr = $jobParams[
'target' ];
907 if ( $isSourceLang ) {
908 $jobData[ $targetStr ] = $jobParams;
910 if ( isset( $jobParams[
'targetTitle' ] ) && ( $jobParams[
'fuzzy' ] ??
false ) ) {
911 $this->messageGroupSubscription->queueMessage(
912 $jobParams[
'targetTitle' ],
913 MessageGroupSubscription::STATE_UPDATED,
917 } elseif ( isset( $jobData[ $targetStr ] ) ) {
921 $jobData[ $targetStr ][
'others' ][ $language ] = $jobParams[
'content' ];
925 $title = Title::newFromText(
926 Utilities::title( $targetStr, $language, $groupNamespace ),
929 $modificationJobs[] = UpdateMessageJob::newJob( $title, $jobParams[
'content'] );
933 $matchedKey = $sourceChanges->getMatchedKey( $language, $key );
934 unset( $renames[$matchedKey] );
938 private function handleModificationsSubmit(
940 MessageSourceChange $sourceChanges,
944 array &$messageUpdateJob
946 $groupId = $group->getId();
947 $subChanges = $sourceChanges->getModificationsForLanguage( $language );
951 unset( $subChanges[ MessageSourceChange::RENAME ] );
954 foreach ( $subChanges as $type => $messages ) {
955 foreach ( $messages as $index => $params ) {
956 $key = $params[
'key'];
957 $id = self::changeId( $groupId, $language, $type, $key );
958 $title = Title::makeTitleSafe( $group->
getNamespace(),
"$key/$language" );
960 if ( !$this->isTitlePresent( $title, $type ) ) {
964 if ( !$req->getCheck( $id ) ) {
966 $postponed[$groupId][$language][$type][$index] = $params;
970 $selectedVal = $req->getVal(
"msg/$id" );
971 if ( $type === MessageSourceChange::DELETION || $selectedVal ===
'ignore' ) {
975 $fuzzy = $selectedVal ===
'fuzzy';
976 $messageUpdateJob[] = UpdateMessageJob::newJob( $title, $params[
'content'], $fuzzy );
978 if ( $isSourceLanguage ) {
979 $this->sendNotificationsForChangedMessages( $groupId, $title, $type, $fuzzy );
986 private function createRenameJobs( array $jobParams ): array {
988 foreach ( $jobParams as $groupId => $groupJobParams ) {
989 $jobs[$groupId] ??= [];
990 foreach ( $groupJobParams as $params ) {
991 $jobs[$groupId][] = UpdateMessageJob::newRenameJob(
992 $params[
'targetTitle'],
994 $params[
'replacement'],
1006 private function isTitlePresent( Title $title,
string $type ): bool {
1009 ( $type === MessageSourceChange::DELETION || $type === MessageSourceChange::CHANGE ) &&
1030 private function isRenameMissing(
1032 MessageSourceChange $sourceChanges,
1039 if ( $req->getCheck( $id ) ) {
1040 return [
false, true ];
1043 $isCurrentKeyPresent =
false;
1046 $matchedKey = $sourceChanges->getMatchedKey( $language, $key );
1047 $matchedId = self::changeId( $groupId, $language, MessageSourceChange::RENAME, $matchedKey );
1048 if ( $req->getCheck( $matchedId ) ) {
1049 return [
false, $isCurrentKeyPresent ];
1055 $isSourceLang || !$sourceChanges->isEqual( $language, $matchedKey ),
1056 $isCurrentKeyPresent
1060 private function getProcessingErrorMessage( array $errorGroups,
int $totalGroupCount ): string {
1062 if ( count( $errorGroups ) < $totalGroupCount ) {
1063 $errorMsg = $this->msg(
'translate-smg-submitted-with-failure' )
1064 ->numParams( count( $errorGroups ) )
1066 $this->getLanguage()->commaList( $errorGroups ),
1067 $this->msg(
'translate-smg-submitted-others-processing' )
1071 $this->msg(
'translate-smg-submitted-with-failure' )
1072 ->numParams( count( $errorGroups ) )
1073 ->params( $this->getLanguage()->commaList( $errorGroups ),
'' )
1082 private function getGroupsFromCdb( Reader $reader ): array {
1084 $groupIds = Utilities::deserialize( $reader->get(
'#keys' ) );
1085 foreach ( $groupIds as $id ) {
1086 $groups[$id] = MessageGroups::getGroup( $id );
1088 return array_filter( $groups );
1096 private function startSync( array $modificationJobs, array $renameJobs ): void {
1099 $modificationGroupIds = array_keys( array_filter( $modificationJobs ) );
1100 $renameGroupIds = array_keys( array_filter( $renameJobs ) );
1101 $uniqueGroupIds = array_unique( array_merge( $modificationGroupIds, $renameGroupIds ) );
1102 $jobQueueInstance = $this->jobQueueGroup;
1104 foreach ( $uniqueGroupIds as $groupId ) {
1109 $groupRenameJobs = $renameJobs[$groupId] ?? [];
1111 foreach ( $groupRenameJobs as $job ) {
1112 $groupJobs[] = $job;
1113 $messageUpdateParam = MessageUpdateParameter::createFromJob( $job );
1114 $messages[] = $messageUpdateParam;
1117 $replacement = $messageUpdateParam->getReplacementValue();
1118 $targetTitle = Title::makeTitle( $job->getTitle()->getNamespace(), $replacement );
1119 $messageKeys[] = (
new MessageHandle( $targetTitle ) )->getKey();
1122 $groupModificationJobs = $modificationJobs[$groupId] ?? [];
1124 foreach ( $groupModificationJobs as $job ) {
1125 $groupJobs[] = $job;
1126 $messageUpdateParam = MessageUpdateParameter::createFromJob( $job );
1127 $messages[] = $messageUpdateParam;
1129 $messageKeys[] = (
new MessageHandle( $job->getTitle() ) )->getKey();
1134 $group = MessageGroups::getGroup( $groupId );
1135 $this->messageIndex->storeInterim( $group, $messageKeys );
1137 if ( $this->getConfig()->
get(
'TranslateGroupSynchronizationCache' ) ) {
1138 $this->synchronizationCache->addMessages( $groupId, ...$messages );
1139 $this->synchronizationCache->markGroupForSync( $groupId );
1141 LoggerFactory::getInstance( LogNames::GROUP_SYNCHRONIZATION )->info(
1142 '[' . __CLASS__ .
'] Synchronization started for {groupId} by {user}',
1144 'groupId' => $groupId,
1145 'user' => $this->getUser()->getName()
1154 DeferredUpdates::addCallableUpdate(
1155 static function () use ( $jobQueueInstance, $groupJobs ) {
1156 $jobQueueInstance->push( $groupJobs );
1163 private function radioLabel(
1167 bool $checked =
false
1175 [
'value' => $value ]
1176 ) .
"\u{00A0}" . $label
1180 private function sendNotificationsForChangedMessages(
string $groupId, Title $title, $type,
bool $fuzzy ): void {
1181 $subscriptionState = $type === MessageSourceChange::ADDITION ?
1182 MessageGroupSubscription::STATE_ADDED :
1183 MessageGroupSubscription::STATE_UPDATED;
1185 if ( $subscriptionState === MessageGroupSubscription::STATE_UPDATED && !$fuzzy ) {
1188 $subscriptionState =
null;
1191 if ( $subscriptionState ) {
1192 $this->messageGroupSubscription->queueMessage( $title, $subscriptionState, $groupId );
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(), 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 { 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());}, '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);}]
@phpcs-require-sorted-array