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