31 private bool $isGroupSyncCacheEnabled;
33 public const IMPORT_NONE = 1;
35 public const IMPORT_SAFE = 2;
38 public const IMPORT_NON_RENAMES = 3;
39 public const CONSTRUCTOR_OPTIONS = [
'TranslateGroupSynchronizationCache' ];
41 public function __construct(
43 private readonly JobQueueGroup $jobQueueGroup,
44 private readonly LoggerInterface $logger,
46 private readonly TitleFactory $titleFactory,
48 ServiceOptions $options
50 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
51 $this->isGroupSyncCacheEnabled = $options->get(
'TranslateGroupSynchronizationCache' );
60 public function import( array $changeData,
string $name,
int $importStrategy ): array {
65 foreach ( $changeData as $groupId => $changesForGroup ) {
68 unset( $changeData[$groupId] );
73 $this->logger->warning(
74 '[ExternalMessageSourceStateImporter] Expected FileBasedMessageGroup, ' .
75 'but got {class} for group {groupId}',
77 'class' => get_class( $group ),
81 unset( $changeData[$groupId] );
85 $processed[$groupId] = [];
86 $languages = $changesForGroup->getLanguages();
90 $groupSafeLanguages = $this->identifySafeLanguages( $group, $changesForGroup, $importStrategy );
92 foreach ( $languages as $language ) {
93 if ( !$groupSafeLanguages[ $language ] ) {
94 $skipped[$groupId] =
true;
98 $additions = $changesForGroup->getAdditions( $language );
99 if ( $additions === [] ) {
103 [ $groupLanguageJobs, $groupProcessed ] = $this->createUpdateMessageJobs(
104 $group, $additions, $language
107 $groupJobs = array_merge( $groupJobs, $groupLanguageJobs );
108 $processed[$groupId][$language] = $groupProcessed;
112 $changesForGroup->removeAdditions( $language,
null );
113 $group->getMessageGroupCache( $language )->create();
115 }
catch ( Throwable $e ) {
116 $this->logger->warning(
117 '[ExternalMessageSourceStateImporter] Error during import. Group: {group}, language: {language}',
120 'language' => $language ??
'N/A',
124 $skipped[ $groupId ] =
true;
129 if ( $this->isGroupSyncCacheEnabled && isset( $skipped[$groupId] ) ) {
130 $this->groupSynchronizationCache->markGroupAsInReview( $groupId );
133 if ( $groupJobs !== [] ) {
134 if ( $this->isGroupSyncCacheEnabled ) {
135 $this->updateGroupSyncInfo( $groupId, $groupJobs );
137 $jobs = array_merge( $jobs, $groupJobs );
141 $this->messageGroupSubscription->queueNotificationJob();
149 $processed = array_filter( $processed );
153 $this->jobQueueGroup->push( $jobs );
156 'processed' => $processed,
157 'skipped' => $skipped,
162 public function canImportGroup( MessageGroup $group,
bool $skipGroupSyncCache ): Status {
163 $groupId = $group->getId();
164 if ( !$group instanceof FileBasedMessageGroup ) {
165 $error =
"Group $groupId expected to be FileBasedMessageGroup, got " . get_class( $group ) .
" instead.";
166 return Status::newFatal(
new RawMessage( $error ) );
169 if ( $this->isGroupSyncCacheEnabled && !$skipGroupSyncCache ) {
170 if ( $this->groupSynchronizationCache->isGroupBeingProcessed( $groupId ) ) {
171 $error =
"Group $groupId is currently being synchronized; skipping processing of changes\n";
172 return Status::newFatal(
new RawMessage( $error ) );
175 if ( $this->groupSynchronizationCache->groupHasErrors( $groupId ) ) {
176 $error =
"Skipping $groupId due to an error during synchronization\n";
177 return Status::newFatal(
new RawMessage( $error ) );
181 return Status::newGood();
188 private function createUpdateMessageJobs(
189 FileBasedMessageGroup $group,
193 $groupId = $group->getId();
194 $isSourceLanguage = $group->getSourceLanguage() === $language;
197 foreach ( $additions as $addition ) {
198 $namespace = $group->getNamespace();
199 $name =
"{$addition['key']}/$language";
201 $title = $this->titleFactory->makeTitleSafe( $namespace, $name );
203 wfWarn(
"Invalid title for group $groupId key {$addition['key']}" );
207 $jobs[] = UpdateMessageJob::newJob( $title, $addition[
'content'] );
210 if ( $isSourceLanguage ) {
211 $this->messageGroupSubscription->queueMessage(
213 MessageGroupSubscription::STATE_ADDED,
219 return [ $jobs, $processed ];
226 private function updateGroupSyncInfo(
string $groupId, array $groupJobs ): void {
228 $groupMessageKeys = [];
229 foreach ( $groupJobs as $job ) {
230 $messageParams[] = MessageUpdateParameter::createFromJob( $job );
233 $groupMessageKeys[(
new MessageHandle( $job->getTitle() ) )->getKey()] =
true;
236 $group = MessageGroups::getGroup( $groupId );
237 if ( $group ===
null ) {
239 throw new RuntimeException(
"Did not find group $groupId" );
242 $this->messageIndex->storeInterim( $group, array_keys( $groupMessageKeys ) );
244 $this->groupSynchronizationCache->addMessages( $groupId, ...$messageParams );
245 $this->groupSynchronizationCache->markGroupForSync( $groupId );
248 '[ExternalMessageSourceStateImporter] Synchronization started for {groupId}',
249 [
'groupId' => $groupId ]
257 private function identifySafeLanguages(
258 FileBasedMessageGroup $group,
259 MessageSourceChange $changesForGroup,
262 $sourceLanguage = $group->getSourceLanguage();
263 $safeLanguagesMap = [];
264 $modifiedLanguages = $changesForGroup->getLanguages();
267 $safeLanguagesMap[ $sourceLanguage ] =
false;
268 foreach ( $modifiedLanguages as $language ) {
269 $safeLanguagesMap[ $language ] =
false;
272 if ( !self::isLanguageSafe( $changesForGroup, $sourceLanguage, $importStrategy ) ) {
273 return $safeLanguagesMap;
276 $sourceLanguageKeyCache = [];
277 foreach ( $changesForGroup->getAdditions( $sourceLanguage ) as $change ) {
278 if ( $change[
'content'] ===
'' ) {
279 return $safeLanguagesMap;
282 $sourceLanguageKeyCache[ $change[
'key'] ] =
true;
285 $safeLanguagesMap[ $sourceLanguage ] =
true;
287 $groupNamespace = $group->getNamespace();
291 if ( $sourceLanguageKeyCache ) {
292 array_splice( $modifiedLanguages, array_search( $sourceLanguage, $modifiedLanguages ), 1 );
295 foreach ( $modifiedLanguages as $language ) {
296 if ( !self::isLanguageSafe( $changesForGroup, $sourceLanguage, $importStrategy ) ) {
300 foreach ( $changesForGroup->getAdditions( $language ) as $change ) {
301 if ( $change[
'content'] ===
'' ) {
305 $msgKey = $change[
'key'];
307 if ( !isset( $sourceLanguageKeyCache[ $msgKey ] ) ) {
311 $sourceHandle =
new MessageHandle( $this->titleFactory->makeTitle( $groupNamespace, $msgKey ) );
312 $sourceLanguageKeyCache[ $msgKey ] = $sourceHandle->isValid();
315 if ( !$sourceLanguageKeyCache[ $msgKey ] ) {
320 $safeLanguagesMap[ $language ] =
true;
323 return $safeLanguagesMap;
326 private static function isLanguageSafe(
327 MessageSourceChange $changesForGroup,
328 string $languageCode,
331 if ( $importStrategy === self::IMPORT_NONE ) {
336 if ( $importStrategy === self::IMPORT_SAFE ) {
337 return $changesForGroup->hasOnly( $languageCode, MessageSourceChange::ADDITION );
342 return $changesForGroup->getDeletions( $languageCode ) === [];