Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ExternalMessageSourceStateImporter.php
1<?php
2declare( strict_types = 1 );
3
12
13use Config;
15use JobQueueGroup;
20use MessageIndex;
22use Psr\Log\LoggerInterface;
23use RuntimeException;
24use Title;
25use function wfWarn;
26
29 private $config;
31 private $groupSynchronizationCache;
33 private $jobQueueGroup;
35 private $logger;
37 private $messageIndex;
38 // Do not perform any import
39 public const IMPORT_NONE = 1;
40 // Import changes in a language for a group if it only has additions
41 public const IMPORT_SAFE = 2;
42 // Import changes in a language for a group if it only has additions or changes, but
43 // not deletions as it may be a rename of an addition
44 public const IMPORT_NON_RENAMES = 3;
45
46 public function __construct(
47 Config $config,
48 GroupSynchronizationCache $groupSynchronizationCache,
49 JobQueueGroup $jobQueueGroup,
50 LoggerInterface $logger,
51 MessageIndex $messageIndex
52 ) {
53 $this->config = $config;
54 $this->groupSynchronizationCache = $groupSynchronizationCache;
55 $this->jobQueueGroup = $jobQueueGroup;
56 $this->logger = $logger;
57 $this->messageIndex = $messageIndex;
58 }
59
66 public function import( array $changeData, string $name, int $importStrategy ): array {
67 $processed = [];
68 $skipped = [];
69 $jobs = [];
70
71 $groupSyncCacheEnabled = $this->config->get( 'TranslateGroupSynchronizationCache' );
72
73 foreach ( $changeData as $groupId => $changesForGroup ) {
74 $group = MessageGroups::getGroup( $groupId );
75 if ( !$group ) {
76 unset( $changeData[$groupId] );
77 continue;
78 }
79
80 if ( !$group instanceof FileBasedMessageGroup ) {
81 $this->logger->warning(
82 '[ExternalMessageSourceStateImporter] Expected FileBasedMessageGroup, ' .
83 'but got {class} for group {groupId}',
84 [
85 'class' => get_class( $group ),
86 'groupId' => $groupId
87 ]
88 );
89 unset( $changeData[$groupId] );
90 continue;
91 }
92
93 $processed[$groupId] = [];
94 $languages = $changesForGroup->getLanguages();
95 $groupJobs = [];
96
97 $groupSafeLanguages = self::identifySafeLanguages( $group, $changesForGroup, $importStrategy );
98
99 foreach ( $languages as $language ) {
100 if ( !$groupSafeLanguages[ $language ] ) {
101 $skipped[$groupId] = true;
102 continue;
103 }
104
105 $additions = $changesForGroup->getAdditions( $language );
106 if ( $additions === [] ) {
107 continue;
108 }
109
110 [ $groupLanguageJobs, $groupProcessed ] = $this->createMessageUpdateJobs(
111 $group, $additions, $language
112 );
113
114 $groupJobs = array_merge( $groupJobs, $groupLanguageJobs );
115 $processed[$groupId][$language] = $groupProcessed;
116
117 // We only remove additions since if less-safe-import option is used, then
118 // changes to existing messages might still need to be processed manually.
119 $changesForGroup->removeAdditions( $language, null );
120 $group->getMessageGroupCache( $language )->create();
121 }
122
123 // Mark the skipped group as in review
124 if ( $groupSyncCacheEnabled && isset( $skipped[$groupId] ) ) {
125 $this->groupSynchronizationCache->markGroupAsInReview( $groupId );
126 }
127
128 if ( $groupJobs !== [] ) {
129 if ( $groupSyncCacheEnabled ) {
130 $this->updateGroupSyncInfo( $groupId, $groupJobs );
131 }
132 $jobs = array_merge( $jobs, $groupJobs );
133 }
134 }
135
136 // Remove groups where everything was imported
137 $changeData = array_filter( $changeData, static function ( MessageSourceChange $change ) {
138 return $change->getAllModifications() !== [];
139 } );
140
141 // Remove groups with no imports
142 $processed = array_filter( $processed );
143
144 $file = MessageChangeStorage::getCdbPath( $name );
145 MessageChangeStorage::writeChanges( $changeData, $file );
146 $this->jobQueueGroup->push( $jobs );
147
148 return [
149 'processed' => $processed,
150 'skipped' => $skipped,
151 'name' => $name,
152 ];
153 }
154
156 private function createMessageUpdateJobs(
158 array $additions,
159 string $language
160 ): array {
161 $groupId = $group->getId();
162 $jobs = [];
163 $processed = 0;
164 foreach ( $additions as $addition ) {
165 $namespace = $group->getNamespace();
166 $name = "{$addition['key']}/$language";
167
168 $title = Title::makeTitleSafe( $namespace, $name );
169 if ( !$title ) {
170 wfWarn( "Invalid title for group $groupId key {$addition['key']}" );
171 continue;
172 }
173
174 $jobs[] = MessageUpdateJob::newJob( $title, $addition['content'] );
175 $processed++;
176 }
177
178 return [ $jobs, $processed ];
179 }
180
185 private function updateGroupSyncInfo( string $groupId, array $groupJobs ): void {
186 $messageParams = [];
187 $groupMessageKeys = [];
188 foreach ( $groupJobs as $job ) {
189 $messageParams[] = MessageUpdateParameter::createFromJob( $job );
190 // Ensure there are no duplicates as the same key may be present in
191 // multiple languages
192 $groupMessageKeys[( new MessageHandle( $job->getTitle() ) )->getKey()] = true;
193 }
194
195 $group = MessageGroups::getGroup( $groupId );
196 if ( $group === null ) {
197 // How did we get here? This should never happen.
198 throw new RuntimeException( "Did not find group $groupId" );
199 }
200
201 $this->messageIndex->storeInterim( $group, array_keys( $groupMessageKeys ) );
202
203 $this->groupSynchronizationCache->addMessages( $groupId, ...$messageParams );
204 $this->groupSynchronizationCache->markGroupForSync( $groupId );
205
206 $this->logger->info(
207 '[ExternalMessageSourceStateImporter] Synchronization started for {groupId}',
208 [ 'groupId' => $groupId ]
209 );
210 }
211
216 private static function identifySafeLanguages(
218 MessageSourceChange $changesForGroup,
219 int $importStrategy
220 ): array {
221 $sourceLanguage = $group->getSourceLanguage();
222 $safeLanguagesMap = [];
223 $modifiedLanguages = $changesForGroup->getLanguages();
224
225 // Set all languages to not safe to start with.
226 $safeLanguagesMap[ $sourceLanguage ] = false;
227 foreach ( $modifiedLanguages as $language ) {
228 $safeLanguagesMap[ $language ] = false;
229 }
230
231 if ( !self::isLanguageSafe( $changesForGroup, $sourceLanguage, $importStrategy ) ) {
232 return $safeLanguagesMap;
233 }
234
235 $sourceLanguageKeyCache = [];
236 foreach ( $changesForGroup->getAdditions( $sourceLanguage ) as $change ) {
237 if ( $change['content'] === '' ) {
238 return $safeLanguagesMap;
239 }
240
241 $sourceLanguageKeyCache[ $change['key'] ] = true;
242 }
243
244 $safeLanguagesMap[ $sourceLanguage ] = true;
245
246 $groupNamespace = $group->getNamespace();
247
248 // Remove source language from the modifiedLanguage list if present since it's already processed.
249 // The $sourceLanguageKeyCache will only have values if sourceLanguage has safe changes.
250 if ( $sourceLanguageKeyCache ) {
251 array_splice( $modifiedLanguages, array_search( $sourceLanguage, $modifiedLanguages ), 1 );
252 }
253
254 foreach ( $modifiedLanguages as $language ) {
255 if ( !self::isLanguageSafe( $changesForGroup, $sourceLanguage, $importStrategy ) ) {
256 continue;
257 }
258
259 foreach ( $changesForGroup->getAdditions( $language ) as $change ) {
260 if ( $change['content'] === '' ) {
261 continue 2;
262 }
263
264 $msgKey = $change['key'];
265
266 if ( !isset( $sourceLanguageKeyCache[ $msgKey ] ) ) {
267 // This is either a new external translation which is not added in the same sync
268 // as the source language key, or this translation does not have a corresponding
269 // definition. We will check the message index to determine which of the two.
270 $sourceHandle = new MessageHandle( Title::makeTitle( $groupNamespace, $msgKey ) );
271 $sourceLanguageKeyCache[ $msgKey ] = $sourceHandle->isValid();
272 }
273
274 if ( !$sourceLanguageKeyCache[ $msgKey ] ) {
275 continue 2;
276 }
277 }
278
279 $safeLanguagesMap[ $language ] = true;
280 }
281
282 return $safeLanguagesMap;
283 }
284
285 private static function isLanguageSafe(
286 MessageSourceChange $changesForGroup,
287 string $languageCode,
288 int $importStrategy
289 ): bool {
290 if ( $importStrategy === self::IMPORT_SAFE ) {
291 return $changesForGroup->hasOnly( $languageCode, MessageSourceChange::ADDITION );
292 }
293
294 // If language has deletions, we consider additions also unsafe because deletions
295 // maybe renames of messages that have been added, so they have to be reviewed.
296 return $changesForGroup->getDeletions( $languageCode ) === [];
297 }
298}
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 is used to track the changes made when importing messages from the remote sources using importE...
getNamespace()
Returns the namespace where messages are placed.
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...
Job for updating translation pages when translation or message definition changes.
Finds external changes for file based message groups.