Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ManageGroupsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
5
6use Cdb\Reader;
7use DifferenceEngine;
8use Exception;
10use JobQueueGroup;
11use MediaWiki\Cache\LinkBatchFactory;
12use MediaWiki\Content\ContentHandler;
13use MediaWiki\Content\TextContent;
14use MediaWiki\Deferred\DeferredUpdates;
22use MediaWiki\Html\Html;
23use MediaWiki\Language\Language;
24use MediaWiki\Logger\LoggerFactory;
25use MediaWiki\MediaWikiServices;
26use MediaWiki\Output\OutputPage;
27use MediaWiki\Request\WebRequest;
28use MediaWiki\Revision\MutableRevisionRecord;
29use MediaWiki\Revision\RevisionLookup;
30use MediaWiki\Revision\SlotRecord;
31use MediaWiki\SpecialPage\DisabledSpecialPage;
32use MediaWiki\SpecialPage\SpecialPage;
33use MediaWiki\Title\NamespaceInfo;
34use MediaWiki\Title\Title;
35use MessageGroup;
36use OOUI\ButtonInputWidget;
37use PermissionsError;
38use RuntimeException;
39use Skin;
40use UserBlockedError;
41
53class ManageGroupsSpecialPage extends SpecialPage {
54 private const GROUP_SYNC_INFO_WRAPPER_CLASS = 'smg-group-sync-cache-info';
55 private const RIGHT = 'translate-manage';
56 protected DifferenceEngine $diff;
58 private string $name;
60 protected string $cdb;
62 protected bool $hasRight = false;
63 private Language $contLang;
64 private NamespaceInfo $nsInfo;
65 private RevisionLookup $revLookup;
66 private GroupSynchronizationCache $synchronizationCache;
67 private DisplayGroupSynchronizationInfo $displayGroupSyncInfo;
68 private JobQueueGroup $jobQueueGroup;
69 private MessageIndex $messageIndex;
70 private LinkBatchFactory $linkBatchFactory;
71 private MessageGroupSubscription $messageGroupSubscription;
72
73 public function __construct(
74 Language $contLang,
75 NamespaceInfo $nsInfo,
76 RevisionLookup $revLookup,
77 GroupSynchronizationCache $synchronizationCache,
78 JobQueueGroup $jobQueueGroup,
79 MessageIndex $messageIndex,
80 LinkBatchFactory $linkBatchFactory,
81 MessageGroupSubscription $messageGroupSubscription
82 ) {
83 // Anyone is allowed to see, but actions are restricted
84 parent::__construct( 'ManageMessageGroups' );
85 $this->contLang = $contLang;
86 $this->nsInfo = $nsInfo;
87 $this->revLookup = $revLookup;
88 $this->synchronizationCache = $synchronizationCache;
89 $this->displayGroupSyncInfo = new DisplayGroupSynchronizationInfo( $this, $this->getLinkRenderer() );
90 $this->jobQueueGroup = $jobQueueGroup;
91 $this->messageIndex = $messageIndex;
92 $this->linkBatchFactory = $linkBatchFactory;
93 $this->messageGroupSubscription = $messageGroupSubscription;
94 }
95
96 public function doesWrites() {
97 return true;
98 }
99
100 protected function getGroupName() {
101 return 'translation';
102 }
103
104 public function getDescription() {
105 return $this->msg( 'managemessagegroups' );
106 }
107
108 public function execute( $par ) {
109 $this->setHeaders();
110
111 $out = $this->getOutput();
112 $out->addModuleStyles( [
113 'ext.translate.specialpages.styles',
114 'mediawiki.codex.messagebox.styles',
115 ] );
116 $out->addModules( 'ext.translate.special.managegroups' );
117 $out->addHelpLink( 'Help:Extension:Translate/Group_management' );
118
119 $this->name = $par ?: MessageChangeStorage::DEFAULT_NAME;
120
121 $this->cdb = MessageChangeStorage::getCdbPath( $this->name );
122 if ( !MessageChangeStorage::isValidCdbName( $this->name ) || !file_exists( $this->cdb ) ) {
123 if ( $this->getConfig()->get( 'TranslateGroupSynchronizationCache' ) ) {
124 $out->addHTML(
125 $this->displayGroupSyncInfo->getGroupsInSyncHtml(
126 $this->synchronizationCache->getGroupsInSync(),
127 self::GROUP_SYNC_INFO_WRAPPER_CLASS
128 )
129 );
130
131 $out->addHTML(
132 $this->displayGroupSyncInfo->getHtmlForGroupsWithError(
133 $this->synchronizationCache,
134 self::GROUP_SYNC_INFO_WRAPPER_CLASS,
135 $this->getLanguage()
136 )
137 );
138 }
139
140 // @todo Tell them when changes was last checked/process
141 // or how to initiate recheck.
142 $out->addWikiMsg( 'translate-smg-nochanges' );
143
144 return;
145 }
146
147 $user = $this->getUser();
148 $this->hasRight = $user->isAllowed( self::RIGHT );
149
150 $req = $this->getRequest();
151 if ( !$req->wasPosted() ) {
152 $this->showChanges( $this->getLimit() );
153
154 return;
155 }
156
157 $block = $user->getBlock();
158 if ( $block && $block->isSitewide() ) {
159 throw new UserBlockedError(
160 $block,
161 $user,
162 $this->getLanguage(),
163 $req->getIP()
164 );
165 }
166
167 $csrfTokenSet = $this->getContext()->getCsrfTokenSet();
168 if ( !$this->hasRight || !$csrfTokenSet->matchTokenField( 'token' ) ) {
169 throw new PermissionsError( self::RIGHT );
170 }
171
172 $this->processSubmit();
173 }
174
176 protected function getLimit(): int {
177 $limits = [
178 1000, // Default max
179 ini_get( 'max_input_vars' ),
180 ini_get( 'suhosin.post.max_vars' ),
181 ini_get( 'suhosin.request.max_vars' )
182 ];
183 // Ignore things not set
184 $limits = array_filter( $limits );
185 return (int)min( $limits );
186 }
187
188 protected function getLegend(): string {
189 $text = $this->diff->addHeader(
190 '',
191 $this->msg( 'translate-smg-left' )->escaped(),
192 $this->msg( 'translate-smg-right' )->escaped()
193 );
194
195 return Html::rawElement( 'div', [ 'class' => 'mw-translate-smg-header' ], $text );
196 }
197
198 protected function showChanges( int $limit ): void {
199 $diff = new DifferenceEngine( $this->getContext() );
200 $diff->showDiffStyle();
201 $diff->setReducedLineNumbers();
202 $this->diff = $diff;
203
204 $out = $this->getOutput();
205 $out->addHTML(
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 ) ) .
210 $this->getLegend()
211 );
212
213 // The above count as three
214 $limit -= 3;
215
216 $groupSyncCacheEnabled = $this->getConfig()->get( 'TranslateGroupSynchronizationCache' );
217 if ( $groupSyncCacheEnabled ) {
218 $out->addHTML(
219 $this->displayGroupSyncInfo->getGroupsInSyncHtml(
220 $this->synchronizationCache->getGroupsInSync(),
221 self::GROUP_SYNC_INFO_WRAPPER_CLASS
222 )
223 );
224
225 $out->addHTML(
226 $this->displayGroupSyncInfo->getHtmlForGroupsWithError(
227 $this->synchronizationCache,
228 self::GROUP_SYNC_INFO_WRAPPER_CLASS,
229 $this->getLanguage()
230 )
231 );
232 }
233
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 ) )
239 );
240 $out->addHTML( Html::element( 'h2', [], $group->getLabel() ) );
241
242 if ( $groupSyncCacheEnabled && $this->synchronizationCache->groupHasErrors( $id ) ) {
243 $out->addHTML(
244 Html::warningBox( $this->msg( 'translate-smg-group-sync-error-warn' )->escaped(), 'center' )
245 );
246 }
247
248 // Reduce page existence queries to one per group
249 $lb = $this->linkBatchFactory->newLinkBatch();
250 $ns = $group->getNamespace();
251 $isCap = $this->nsInfo->isCapitalized( $ns );
252 $languages = $sourceChanges->getLanguages();
253
254 foreach ( $languages as $language ) {
255 $languageChanges = $sourceChanges->getModificationsForLanguage( $language );
256 foreach ( $languageChanges as $changes ) {
257 foreach ( $changes as $params ) {
258 // Constructing title objects is way slower
259 $key = $params['key'];
260 if ( $isCap ) {
261 $key = $this->contLang->ucfirst( $key );
262 }
263 $lb->add( $ns, "$key/$language" );
264 }
265 }
266 }
267 $lb->execute();
268
269 foreach ( $languages as $language ) {
270 // Handle and generate UI for additions, deletions, change
271 $changes = [];
272 $changes[ MessageSourceChange::ADDITION ] = $sourceChanges->getAdditions( $language );
273 $changes[ MessageSourceChange::DELETION ] = $sourceChanges->getDeletions( $language );
274 $changes[ MessageSourceChange::CHANGE ] = $sourceChanges->getChanges( $language );
275
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 );
280
281 if ( $limit <= 0 ) {
282 // We need to restrict the changes per page per form submission
283 // limitations as well as performance.
284 $out->wrapWikiMsg( "<div class=warning>\n$1\n</div>", 'translate-smg-more' );
285 break 4;
286 }
287 }
288 }
289
290 // Handle and generate UI for renames
291 $this->showRenames( $group, $sourceChanges, $out, $language, $limit );
292 }
293 }
294
295 $out->enableOOUI();
296 $button = new ButtonInputWidget( [
297 'type' => 'submit',
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' ],
303 ] );
304 $out->addHTML( $button );
305 $out->addHTML( Html::closeElement( 'form' ) );
306 }
307
308 protected function formatChange(
309 MessageGroup $group,
310 MessageSourceChange $changes,
311 string $language,
312 string $type,
313 array $params,
314 int &$limit
315 ): string {
316 $key = $params['key'];
317 $title = Title::makeTitleSafe( $group->getNamespace(), "$key/$language" );
318 $id = self::changeId( $group->getId(), $language, $type, $key );
319 $noticeHtml = '';
320 $isReusedKey = false;
321
322 if ( $title && $type === 'addition' && $title->exists() ) {
323 // The message has for some reason dropped out from cache
324 // or, perhaps it is being reused. In any case treat it
325 // as a change for display, so the admin can see if
326 // action is needed and let the message be processed.
327 // Otherwise, it will end up in the postponed category
328 // forever and will prevent rebuilding the cache, which
329 // leads to many other annoying problems.
330 $type = 'change';
331 $noticeHtml .= Html::warningBox( $this->msg( 'translate-manage-key-reused' )->parse() );
332 $isReusedKey = true;
333 } elseif ( $title && ( $type === 'deletion' || $type === 'change' ) && !$title->exists() ) {
334 // This happens if a message key has been renamed
335 // The change can be ignored.
336 return '';
337 }
338
339 $text = '';
340 $titleLink = $this->getLinkRenderer()->makeLink( $title );
341
342 if ( $type === 'deletion' ) {
343 $revTitle = $this->revLookup->getRevisionByTitle( $title );
344 if ( !$revTitle ) {
345 wfWarn( "[ManageGroupSpecialPage] No revision associated with {$title->getPrefixedText()}" );
346 }
347 $content = $revTitle ? $revTitle->getContent( SlotRecord::MAIN ) : null;
348 $wiki = ( $content instanceof TextContent ) ? $content->getText() : '';
349
350 if ( $wiki === '' ) {
351 $noticeHtml .= Html::warningBox(
352 $this->msg( 'translate-manage-empty-content' )->parse()
353 );
354 }
355
356 $newRevision = new MutableRevisionRecord( $title );
357 $newRevision->setContent( SlotRecord::MAIN, ContentHandler::makeContent( '', $title ) );
358
359 $this->diff->setRevisions( $revTitle, $newRevision );
360 $text = $this->diff->getDiff( $titleLink, '', $noticeHtml );
361 } elseif ( $type === 'addition' ) {
362 $menu = '';
363 $sourceLanguage = $group->getSourceLanguage();
364 if ( $sourceLanguage === $language ) {
365 if ( $this->hasRight ) {
366 $menu = Html::rawElement(
367 'button',
368 [
369 'class' => 'smg-rename-actions',
370 'type' => 'button',
371 'data-group-id' => $group->getId(),
372 'data-lang' => $language,
373 'data-msgkey' => $key,
374 'data-msgtitle' => $title->getFullText()
375 ]
376 );
377 }
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'
382 );
383
384 // Automatically ignore messages that don't have a definitions
385 $menu = Html::hidden( "msg/$id", 'ignore', [ 'id' => "i/$id" ] );
386 $limit--;
387 }
388
389 if ( $params['content'] === '' ) {
390 $noticeHtml .= Html::warningBox(
391 $this->msg( 'translate-manage-empty-content' )->parse()
392 );
393 }
394
395 $oldRevision = new MutableRevisionRecord( $title );
396 $oldRevision->setContent( SlotRecord::MAIN, ContentHandler::makeContent( '', $title ) );
397
398 $newRevision = new MutableRevisionRecord( $title );
399 $newRevision->setContent(
400 SlotRecord::MAIN,
401 ContentHandler::makeContent( (string)$params['content'], $title )
402 );
403
404 $this->diff->setRevisions( $oldRevision, $newRevision );
405 $text = $this->diff->getDiff( '', $titleLink . $menu, $noticeHtml );
406 } elseif ( $type === 'change' ) {
407 $wiki = Utilities::getContentForTitle( $title, true );
408
409 $actions = '';
410 $sourceLanguage = $group->getSourceLanguage();
411
412 // Option to fuzzy is only available for source languages, and should be used
413 // if content has changed.
414 $shouldFuzzy = $sourceLanguage === $language && $wiki !== $params['content'];
415
416 if ( $sourceLanguage === $language ) {
417 $label = $this->msg( 'translate-manage-action-fuzzy' )->text();
418 $actions .= $this->radioLabel( $label, "msg/$id", "fuzzy", $shouldFuzzy );
419 }
420
421 if (
422 $sourceLanguage !== $language &&
423 $isReusedKey &&
424 !self::isMessageDefinitionPresent( $group, $changes, $key )
425 ) {
426 $noticeHtml .= Html::warningBox(
427 $this->msg( 'translate-manage-source-message-not-found' )->parse(),
428 'mw-translate-smg-notice-important'
429 );
430
431 // Automatically ignore messages that don't have a definitions
432 $actions .= Html::hidden( "msg/$id", 'ignore', [ 'id' => "i/$id" ] );
433 $limit--;
434 } else {
435 $label = $this->msg( 'translate-manage-action-import' )->text();
436 $actions .= $this->radioLabel( $label, "msg/$id", "import", !$shouldFuzzy );
437
438 $label = $this->msg( 'translate-manage-action-ignore' )->text();
439 $actions .= $this->radioLabel( $label, "msg/$id", "ignore" );
440 $limit--;
441 }
442
443 $oldRevision = new MutableRevisionRecord( $title );
444 $oldRevision->setContent( SlotRecord::MAIN, ContentHandler::makeContent( (string)$wiki, $title ) );
445
446 $newRevision = new MutableRevisionRecord( $title );
447 $newRevision->setContent(
448 SlotRecord::MAIN,
449 ContentHandler::makeContent( (string)$params['content'], $title )
450 );
451
452 $this->diff->setRevisions( $oldRevision, $newRevision );
453 $text .= $this->diff->getDiff( $titleLink, $actions, $noticeHtml );
454 }
455
456 $hidden = Html::hidden( $id, 1 );
457 $limit--;
458 $text .= $hidden;
459 $classes = "mw-translate-smg-change smg-change-$type";
460
461 if ( $limit < 0 ) {
462 // Don't add if one of the fields might get dropped of at submission
463 return '';
464 }
465
466 return Html::rawElement( 'div', [ 'class' => $classes ], $text );
467 }
468
469 protected function processSubmit(): void {
470 $req = $this->getRequest();
471 $out = $this->getOutput();
472 $errorGroups = [];
473
474 $modificationJobs = $renameJobData = [];
475 $lastModifiedTime = intval( $req->getVal( 'changesetModifiedTime' ) );
476
477 if ( !MessageChangeStorage::isModifiedSince( $this->cdb, $lastModifiedTime ) ) {
478 $out->addWikiMsg( 'translate-smg-changeset-modified' );
479 return;
480 }
481
482 $reader = Reader::open( $this->cdb );
483 $groups = $this->getGroupsFromCdb( $reader );
484 $groupSyncCacheEnabled = $this->getConfig()->get( 'TranslateGroupSynchronizationCache' );
485 $postponed = [];
486
487 foreach ( $groups as $groupId => $group ) {
488 try {
489 if ( !$group instanceof FileBasedMessageGroup ) {
490 throw new RuntimeException( "Expected $groupId to be FileBasedMessageGroup, got "
491 . get_class( $group )
492 . " instead."
493 );
494 }
495 $changes = Utilities::deserialize( $reader->get( $groupId ) );
496 if ( $groupSyncCacheEnabled && $this->synchronizationCache->groupHasErrors( $groupId ) ) {
497 $postponed[$groupId] = $changes;
498 continue;
499 }
500
501 $sourceChanges = MessageSourceChange::loadModifications( $changes );
502 $groupModificationJobs = [];
503 $groupRenameJobData = [];
504 $languages = $sourceChanges->getLanguages();
505 foreach ( $languages as $language ) {
506 // Handle changes, additions, deletions
507 $this->handleModificationsSubmit(
508 $group,
509 $sourceChanges,
510 $req,
511 $language,
512 $postponed,
513 $groupModificationJobs
514 );
515
516 // Handle renames, this might also add modification jobs based on user selection.
517 $this->handleRenameSubmit(
518 $group,
519 $sourceChanges,
520 $req,
521 $language,
522 $postponed,
523 $groupRenameJobData,
524 $groupModificationJobs
525 );
526
527 if ( !isset( $postponed[$groupId][$language] ) ) {
528 $group->getMessageGroupCache( $language )->create();
529 }
530 }
531
532 if ( $groupSyncCacheEnabled && !isset( $postponed[ $groupId ] ) ) {
533 $this->synchronizationCache->markGroupAsReviewed( $groupId );
534 }
535
536 $modificationJobs[$groupId] = $groupModificationJobs;
537 $renameJobData[$groupId] = $groupRenameJobData;
538 } catch ( Exception $e ) {
539 error_log(
540 "ManageGroupsSpecialPage: Error in processSubmit. Group: $groupId\n" .
541 "Exception: $e"
542 );
543
544 $errorGroups[] = $group->getLabel();
545 }
546 }
547 $this->messageGroupSubscription->queueNotificationJob();
548
549 $renameJobs = $this->createRenameJobs( $renameJobData );
550 $this->startSync( $modificationJobs, $renameJobs );
551
552 $reader->close();
553 rename( $this->cdb, $this->cdb . '-' . wfTimestamp() );
554
555 if ( $errorGroups ) {
556 $errorMsg = $this->getProcessingErrorMessage( $errorGroups, count( $groups ) );
557 $out->addHTML(
558 Html::warningBox(
559 $errorMsg,
560 'mw-translate-smg-submitted'
561 )
562 );
563 }
564
565 if ( count( $postponed ) ) {
566 $postponedSourceChanges = [];
567 foreach ( $postponed as $groupId => $changes ) {
568 $postponedSourceChanges[$groupId] = MessageSourceChange::loadModifications( $changes );
569 }
570 MessageChangeStorage::writeChanges( $postponedSourceChanges, $this->cdb );
571
572 $this->showChanges( $this->getLimit() );
573 } elseif ( $errorGroups === [] ) {
574 $out->addWikiMsg( 'translate-smg-submitted' );
575 }
576 }
577
578 protected static function changeId(
579 string $groupId,
580 string $language,
581 string $type,
582 string $key
583 ): string {
584 return 'smg/' . substr( sha1( "$groupId/$language/$type/$key" ), 0, 7 );
585 }
586
591 public static function tabify( Skin $skin, array &$tabs ): void {
592 $title = $skin->getTitle();
593 if ( !$title->isSpecialPage() ) {
594 return;
595 }
596 $specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
597 [ $alias, ] = $specialPageFactory->resolveAlias( $title->getText() );
598
599 $pagesInGroup = [
600 'ManageMessageGroups' => 'namespaces',
601 'AggregateGroups' => 'namespaces',
602 'SupportedLanguages' => 'views',
603 'TranslationStats' => 'views',
604 ];
605 if ( !isset( $pagesInGroup[$alias] ) ) {
606 return;
607 }
608
609 $tabs['namespaces'] = [];
610 foreach ( $pagesInGroup as $spName => $section ) {
611 $spClass = $specialPageFactory->getPage( $spName );
612
613 if ( $spClass === null || $spClass instanceof DisabledSpecialPage ) {
614 continue; // Page explicitly disabled
615 }
616 $spTitle = $spClass->getPageTitle();
617
618 $tabs[$section][strtolower( $spName )] = [
619 'text' => $spClass->getDescription(),
620 'href' => $spTitle->getLocalURL(),
621 'class' => $alias === $spName ? 'selected' : '',
622 ];
623 }
624 }
625
630 private static function isMessageDefinitionPresent(
631 MessageGroup $group,
632 MessageSourceChange $changes,
633 string $msgKey
634 ): bool {
635 $sourceLanguage = $group->getSourceLanguage();
636 if ( $changes->findMessage( $sourceLanguage, $msgKey, [ MessageSourceChange::ADDITION ] ) ) {
637 return true;
638 }
639
640 $namespace = $group->getNamespace();
641 $sourceHandle = new MessageHandle( Title::makeTitle( $namespace, $msgKey ) );
642 return $sourceHandle->isValid();
643 }
644
645 private function showRenames(
646 MessageGroup $group,
647 MessageSourceChange $sourceChanges,
648 OutputPage $out,
649 string $language,
650 int &$limit
651 ): void {
652 $changes = $sourceChanges->getRenames( $language );
653 foreach ( $changes as $key => $params ) {
654 // Since we're removing items from the array within the loop add
655 // a check here to ensure that the current key is still set.
656 if ( !isset( $changes[ $key ] ) ) {
657 continue;
658 }
659
660 if ( $group->getSourceLanguage() !== $language &&
661 $sourceChanges->isEqual( $language, $key ) ) {
662 // This is a translation rename, that does not have any changes.
663 // We can group this along with the source rename.
664 continue;
665 }
666
667 // Determine added key, and corresponding removed key.
668 $firstMsg = $params;
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" );
673 }
674
675 if (
676 $sourceChanges->isPreviousState(
677 $language,
678 $key,
679 [ MessageSourceChange::ADDITION, MessageSourceChange::CHANGE ]
680 )
681 ) {
682 $addedMsg = $firstMsg;
683 $deletedMsg = $secondMsg;
684 } else {
685 $addedMsg = $secondMsg;
686 $deletedMsg = $firstMsg;
687 }
688
689 $change = $this->formatRename(
690 $group,
691 $addedMsg,
692 $deletedMsg,
693 $language,
694 $sourceChanges->isEqual( $language, $key ),
695 $limit
696 );
697 $out->addHTML( $change );
698
699 // no need to process the second key again.
700 unset( $changes[$secondKey] );
701
702 if ( $limit <= 0 ) {
703 // We need to restrict the changes per page per form submission
704 // limitations as well as performance.
705 $out->wrapWikiMsg( "<div class=warning>\n$1\n</div>", 'translate-smg-more' );
706 break;
707 }
708 }
709 }
710
711 private function formatRename(
712 MessageGroup $group,
713 array $addedMsg,
714 array $deletedMsg,
715 string $language,
716 bool $isEqual,
717 int &$limit
718 ): string {
719 $addedKey = $addedMsg['key'];
720 $deletedKey = $deletedMsg['key'];
721 $actions = '';
722
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 );
726
727 $addedTitleLink = $this->getLinkRenderer()->makeLink( $addedTitle );
728 $deletedTitleLink = $this->getLinkRenderer()->makeLink( $deletedTitle );
729
730 $renameSelected = true;
731 if ( $group->getSourceLanguage() === $language ) {
732 if ( !$isEqual ) {
733 $renameSelected = false;
734 $label = $this->msg( 'translate-manage-action-rename-fuzzy' )->text();
735 $actions .= $this->radioLabel( $label, "msg/$id", "renamefuzzy", true );
736 }
737
738 $label = $this->msg( 'translate-manage-action-rename' )->text();
739 $actions .= $this->radioLabel( $label, "msg/$id", "rename", $renameSelected );
740 } else {
741 $label = $this->msg( 'translate-manage-action-import' )->text();
742 $actions .= $this->radioLabel( $label, "msg/$id", "import", true );
743 }
744
745 if ( $group->getSourceLanguage() !== $language ) {
746 // Allow user to ignore changes to non-source languages.
747 $label = $this->msg( 'translate-manage-action-ignore-change' )->text();
748 $actions .= $this->radioLabel( $label, "msg/$id", "ignore" );
749 }
750 $limit--;
751
752 $addedContent = ContentHandler::makeContent( (string)$addedMsg['content'], $addedTitle );
753 $addedRevision = new MutableRevisionRecord( $addedTitle );
754 $addedRevision->setContent( SlotRecord::MAIN, $addedContent );
755
756 $deletedContent = ContentHandler::makeContent( (string)$deletedMsg['content'], $deletedTitle );
757 $deletedRevision = new MutableRevisionRecord( $deletedTitle );
758 $deletedRevision->setContent( SlotRecord::MAIN, $deletedContent );
759
760 $this->diff->setRevisions( $deletedRevision, $addedRevision );
761
762 $menu = '';
763 if ( $group->getSourceLanguage() === $language && $this->hasRight ) {
764 // Only show rename and add as new option for source language.
765 $menu = Html::rawElement(
766 'button',
767 [
768 'class' => 'smg-rename-actions',
769 'type' => 'button',
770 'data-group-id' => $group->getId(),
771 'data-msgkey' => $addedKey,
772 'data-msgtitle' => $addedTitle->getFullText()
773 ]
774 );
775 }
776
777 $actions = Html::rawElement( 'div', [ 'class' => 'smg-change-import-options' ], $actions );
778
779 $text = $this->diff->getDiff(
780 $deletedTitleLink,
781 $addedTitleLink . $menu . $actions,
782 $isEqual ? htmlspecialchars( $addedMsg['content'] ) : ''
783 );
784
785 $hidden = Html::hidden( $id, 1 );
786 $limit--;
787 $text .= $hidden;
788
789 return Html::rawElement(
790 'div',
791 [ 'class' => 'mw-translate-smg-change smg-change-rename' ],
792 $text
793 );
794 }
795
796 private function getRenameJobParams(
797 array $currentMsg,
798 MessageSourceChange $sourceChanges,
799 string $languageCode,
800 int $groupNamespace,
801 string $selectedVal,
802 bool $isSourceLang = true
803 ): ?array {
804 if ( $selectedVal === 'ignore' ) {
805 return null;
806 }
807
808 $params = [];
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." );
813 }
814 $matchedMsgKey = $matchedMsg['key'];
815
816 if (
817 $sourceChanges->isPreviousState(
818 $languageCode,
819 $currentMsgKey,
820 [ MessageSourceChange::ADDITION, MessageSourceChange::CHANGE ]
821 )
822 ) {
823 $params['target'] = $matchedMsgKey;
824 $params['replacement'] = $currentMsgKey;
825 $replacementContent = $currentMsg['content'];
826 } else {
827 $params['target'] = $currentMsgKey;
828 $params['replacement'] = $matchedMsgKey;
829 $replacementContent = $matchedMsg['content'];
830 }
831
832 $params['fuzzy'] = $selectedVal === 'renamefuzzy';
833
834 $params['content'] = $replacementContent;
835
836 if ( $isSourceLang ) {
837 $params['targetTitle'] = Title::newFromText(
838 Utilities::title( $params['target'], $languageCode, $groupNamespace ),
839 $groupNamespace
840 );
841 $params['others'] = [];
842 }
843
844 return $params;
845 }
846
847 private function handleRenameSubmit(
848 MessageGroup $group,
849 MessageSourceChange $sourceChanges,
850 WebRequest $req,
851 string $language,
852 array &$postponed,
853 array &$jobData,
854 array &$modificationJobs
855 ): void {
856 $groupId = $group->getId();
857 $renames = $sourceChanges->getRenames( $language );
858 $isSourceLang = $group->getSourceLanguage() === $language;
859 $groupNamespace = $group->getNamespace();
860
861 foreach ( $renames as $key => $params ) {
862 // Since we're removing items from the array within the loop add
863 // a check here to ensure that the current key is still set.
864 if ( !isset( $renames[$key] ) ) {
865 continue;
866 }
867
868 $id = self::changeId( $groupId, $language, MessageSourceChange::RENAME, $key );
869
870 [ $renameMissing, $isCurrentKeyPresent ] = $this->isRenameMissing(
871 $req,
872 $sourceChanges,
873 $id,
874 $key,
875 $language,
876 $groupId,
877 $isSourceLang
878 );
879
880 if ( $renameMissing ) {
881 // we probably hit the limit with number of post parameters since neither
882 // addition nor deletion key is present.
883 $postponed[$groupId][$language][MessageSourceChange::RENAME][$key] = $params;
884 continue;
885 }
886
887 if ( !$isCurrentKeyPresent ) {
888 // still don't process this key, and wait for the matched rename
889 continue;
890 }
891
892 $selectedVal = $req->getVal( "msg/$id" );
893 $jobParams = $this->getRenameJobParams(
894 $params,
895 $sourceChanges,
896 $language,
897 $groupNamespace,
898 $selectedVal,
899 $isSourceLang
900 );
901
902 if ( $jobParams === null ) {
903 continue;
904 }
905
906 $targetStr = $jobParams[ 'target' ];
907 if ( $isSourceLang ) {
908 $jobData[ $targetStr ] = $jobParams;
909 // Send notification for fuzzy items
910 if ( isset( $jobParams[ 'targetTitle' ] ) && ( $jobParams[ 'fuzzy' ] ?? false ) ) {
911 $this->messageGroupSubscription->queueMessage(
912 $jobParams[ 'targetTitle' ],
913 MessageGroupSubscription::STATE_UPDATED,
914 $groupId
915 );
916 }
917 } elseif ( isset( $jobData[ $targetStr ] ) ) {
918 // We are grouping the source rename, and content changes in other languages
919 // for the message together into a single job in order to avoid race conditions
920 // since jobs are not guaranteed to be run in order.
921 $jobData[ $targetStr ][ 'others' ][ $language ] = $jobParams[ 'content' ];
922 } else {
923 // the source was probably ignored, we should add this as a modification instead,
924 // since the source is not going to be renamed.
925 $title = Title::newFromText(
926 Utilities::title( $targetStr, $language, $groupNamespace ),
927 $groupNamespace
928 );
929 $modificationJobs[] = UpdateMessageJob::newJob( $title, $jobParams['content'] );
930 }
931
932 // remove the matched key in order to avoid double processing.
933 $matchedKey = $sourceChanges->getMatchedKey( $language, $key );
934 unset( $renames[$matchedKey] );
935 }
936 }
937
938 private function handleModificationsSubmit(
939 MessageGroup $group,
940 MessageSourceChange $sourceChanges,
941 WebRequest $req,
942 string $language,
943 array &$postponed,
944 array &$messageUpdateJob
945 ): void {
946 $groupId = $group->getId();
947 $subChanges = $sourceChanges->getModificationsForLanguage( $language );
948 $isSourceLanguage = $group->getSourceLanguage() === $language;
949
950 // Ignore renames
951 unset( $subChanges[ MessageSourceChange::RENAME ] );
952
953 // Handle additions, deletions, and changes.
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" );
959
960 if ( !$this->isTitlePresent( $title, $type ) ) {
961 continue;
962 }
963
964 if ( !$req->getCheck( $id ) ) {
965 // We probably hit the limit with number of post parameters.
966 $postponed[$groupId][$language][$type][$index] = $params;
967 continue;
968 }
969
970 $selectedVal = $req->getVal( "msg/$id" );
971 if ( $type === MessageSourceChange::DELETION || $selectedVal === 'ignore' ) {
972 continue;
973 }
974
975 $fuzzy = $selectedVal === 'fuzzy';
976 $messageUpdateJob[] = UpdateMessageJob::newJob( $title, $params['content'], $fuzzy );
977
978 if ( $isSourceLanguage ) {
979 $this->sendNotificationsForChangedMessages( $groupId, $title, $type, $fuzzy );
980 }
981 }
982 }
983 }
984
986 private function createRenameJobs( array $jobParams ): array {
987 $jobs = [];
988 foreach ( $jobParams as $groupId => $groupJobParams ) {
989 $jobs[$groupId] ??= [];
990 foreach ( $groupJobParams as $params ) {
991 $jobs[$groupId][] = UpdateMessageJob::newRenameJob(
992 $params['targetTitle'],
993 $params['target'],
994 $params['replacement'],
995 $params['fuzzy'],
996 $params['content'],
997 $params['others']
998 );
999 }
1000 }
1001
1002 return $jobs;
1003 }
1004
1006 private function isTitlePresent( Title $title, string $type ): bool {
1007 // phpcs:ignore SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn
1008 if (
1009 ( $type === MessageSourceChange::DELETION || $type === MessageSourceChange::CHANGE ) &&
1010 !$title->exists()
1011 ) {
1012 // This means that this change was probably introduced due to a rename
1013 // which removed the key. No need to process.
1014 return false;
1015 }
1016 return true;
1017 }
1018
1030 private function isRenameMissing(
1031 WebRequest $req,
1032 MessageSourceChange $sourceChanges,
1033 string $id,
1034 string $key,
1035 string $language,
1036 string $groupId,
1037 bool $isSourceLang
1038 ): array {
1039 if ( $req->getCheck( $id ) ) {
1040 return [ false, true ];
1041 }
1042
1043 $isCurrentKeyPresent = false;
1044
1045 // Checked the matched key is also missing to confirm if its truly missing
1046 $matchedKey = $sourceChanges->getMatchedKey( $language, $key );
1047 $matchedId = self::changeId( $groupId, $language, MessageSourceChange::RENAME, $matchedKey );
1048 if ( $req->getCheck( $matchedId ) ) {
1049 return [ false, $isCurrentKeyPresent ];
1050 }
1051
1052 // For non source language, if strings are equal, they are not shown on the UI
1053 // and hence not submitted.
1054 return [
1055 $isSourceLang || !$sourceChanges->isEqual( $language, $matchedKey ),
1056 $isCurrentKeyPresent
1057 ];
1058 }
1059
1060 private function getProcessingErrorMessage( array $errorGroups, int $totalGroupCount ): string {
1061 // Number of error groups, are less than the total groups processed.
1062 if ( count( $errorGroups ) < $totalGroupCount ) {
1063 $errorMsg = $this->msg( 'translate-smg-submitted-with-failure' )
1064 ->numParams( count( $errorGroups ) )
1065 ->params(
1066 $this->getLanguage()->commaList( $errorGroups ),
1067 $this->msg( 'translate-smg-submitted-others-processing' )
1068 )->parse();
1069 } else {
1070 $errorMsg = trim(
1071 $this->msg( 'translate-smg-submitted-with-failure' )
1072 ->numParams( count( $errorGroups ) )
1073 ->params( $this->getLanguage()->commaList( $errorGroups ), '' )
1074 ->parse()
1075 );
1076 }
1077
1078 return $errorMsg;
1079 }
1080
1082 private function getGroupsFromCdb( Reader $reader ): array {
1083 $groups = [];
1084 $groupIds = Utilities::deserialize( $reader->get( '#keys' ) );
1085 foreach ( $groupIds as $id ) {
1086 $groups[$id] = MessageGroups::getGroup( $id );
1087 }
1088 return array_filter( $groups );
1089 }
1090
1096 private function startSync( array $modificationJobs, array $renameJobs ): void {
1097 // We are adding an empty array for groups that have no jobs. This is mainly done to
1098 // avoid adding unnecessary checks. Remove those using array_filter
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;
1103
1104 foreach ( $uniqueGroupIds as $groupId ) {
1105 $messages = [];
1106 $messageKeys = [];
1107 $groupJobs = [];
1108
1109 $groupRenameJobs = $renameJobs[$groupId] ?? [];
1111 foreach ( $groupRenameJobs as $job ) {
1112 $groupJobs[] = $job;
1113 $messageUpdateParam = MessageUpdateParameter::createFromJob( $job );
1114 $messages[] = $messageUpdateParam;
1115
1116 // Build the handle to add the message key in interim cache
1117 $replacement = $messageUpdateParam->getReplacementValue();
1118 $targetTitle = Title::makeTitle( $job->getTitle()->getNamespace(), $replacement );
1119 $messageKeys[] = ( new MessageHandle( $targetTitle ) )->getKey();
1120 }
1121
1122 $groupModificationJobs = $modificationJobs[$groupId] ?? [];
1124 foreach ( $groupModificationJobs as $job ) {
1125 $groupJobs[] = $job;
1126 $messageUpdateParam = MessageUpdateParameter::createFromJob( $job );
1127 $messages[] = $messageUpdateParam;
1128
1129 $messageKeys[] = ( new MessageHandle( $job->getTitle() ) )->getKey();
1130 }
1131
1132 // Store all message keys in the interim cache - we're particularly interested in new
1133 // and renamed messages, but it's cleaner to just store everything.
1134 $group = MessageGroups::getGroup( $groupId );
1135 $this->messageIndex->storeInterim( $group, $messageKeys );
1136
1137 if ( $this->getConfig()->get( 'TranslateGroupSynchronizationCache' ) ) {
1138 $this->synchronizationCache->addMessages( $groupId, ...$messages );
1139 $this->synchronizationCache->markGroupForSync( $groupId );
1140
1141 LoggerFactory::getInstance( LogNames::GROUP_SYNCHRONIZATION )->info(
1142 '[' . __CLASS__ . '] Synchronization started for {groupId} by {user}',
1143 [
1144 'groupId' => $groupId,
1145 'user' => $this->getUser()->getName()
1146 ]
1147 );
1148 }
1149
1150 // There is possibility for a race condition here: the translate_cache table / group sync
1151 // cache is not yet populated with the messages to be processed, but the jobs start
1152 // running and try to remove the message from the cache. This results in a "Key not found"
1153 // error. Avoid this condition by using a DeferredUpdate.
1154 DeferredUpdates::addCallableUpdate(
1155 static function () use ( $jobQueueInstance, $groupJobs ) {
1156 $jobQueueInstance->push( $groupJobs );
1157 }
1158 );
1159
1160 }
1161 }
1162
1163 private function radioLabel(
1164 string $label,
1165 string $name,
1166 string $value,
1167 bool $checked = false
1168 ): string {
1169 return Html::rawElement(
1170 'label',
1171 [],
1172 Html::radio(
1173 $name,
1174 $checked,
1175 [ 'value' => $value ]
1176 ) . "\u{00A0}" . $label
1177 );
1178 }
1179
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;
1184
1185 if ( $subscriptionState === MessageGroupSubscription::STATE_UPDATED && !$fuzzy ) {
1186 // If the state is updated, but the change has not been marked as fuzzy,
1187 // lets not send a notification.
1188 $subscriptionState = null;
1189 }
1190
1191 if ( $subscriptionState ) {
1192 $this->messageGroupSubscription->queueMessage( $title, $subscriptionState, $groupId );
1193 }
1194 }
1195}
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
This class implements default behavior for file based message groups.
Constants for log channel names used in this extension.
Definition LogNames.php:13
Manage user subscriptions to message groups and trigger notifications.
Factory class for accessing message groups individually by id or all of them as a list.
Class for pointing to messages, like Title class is for titles.
Creates a database of keys in all groups, so that namespace and key can be used to get the groups the...
Class is used to track the changes made when importing messages from the remote sources using importE...
string $cdb
Path to the change cdb file, derived from the name.
bool $hasRight
Has the necessary right specified by the RIGHT constant.
static tabify(Skin $skin, array &$tabs)
Adds the task-based tabs on Special:Translate and few other special pages.
static getCdbPath(string $fileName)
Get a full path to file in a known location.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
Interface for message groups.
getNamespace()
Returns the namespace where messages are placed.
getSourceLanguage()
Returns language code depicting the language of source text.
getId()
Returns the unique identifier for this group.
getLabel(?IContextSource $context=null)
Returns the human readable label (as plain text).
Finds external changes for file based message groups.