28 private RevisionLookup $revisionLookup;
29 private PageStore $pageStore;
31 public function __construct(
33 RevisionLookup $revisionLookup,
36 $this->stringComparator = $stringComparator;
37 $this->revisionLookup = $revisionLookup;
38 $this->pageStore = $pageStore;
67 $this->processLanguage( $group, $sourceLanguage, $changes );
68 unset( $languages[ $sourceLanguage] );
70 foreach ( array_keys( $languages ) as $language ) {
71 $this->processLanguage( $group, $language, $changes );
77 private function processLanguage(
80 MessageSourceChange $changes
82 $cache = $group->getMessageGroupCache( $language );
84 if ( !$cache->isValid( $reason ) ) {
85 $this->addMessageUpdateChanges( $group, $language, $changes, $reason, $cache );
87 if ( $changes->getModificationsForLanguage( $language ) === [] ) {
120 $wiki = $group->initCollection( $language );
121 $wiki->filter( MessageCollection::FILTER_HAS_TRANSLATION, MessageCollection::INCLUDE_MATCHING );
122 $wiki->loadTranslations();
123 $wikiKeys = $wiki->getMessageKeys();
127 $ffs = $group->getFFS();
128 if ( $language === $sourceLanguage && !$ffs->exists( $language ) ) {
130 throw new RuntimeException(
"Source message file for {$group->getId()} does not exist: $path" );
133 $file = $ffs->read( $language );
136 if ( $file ===
false ) {
141 if ( !isset( $file[
'MESSAGES'] ) ) {
142 $id = $group->
getId();
143 $ffsClass = get_class( $ffs );
145 error_log(
"$id has an FFS ($ffsClass) - it didn't return cake for $language" );
150 $fileKeys = array_keys( $file[
'MESSAGES'] );
152 $common = array_intersect( $fileKeys, $wikiKeys );
154 $supportsFuzzy = $ffs->supportsFuzzy();
155 $changesToRemove = [];
157 foreach ( $common as $key ) {
158 $sourceContent = $file[
'MESSAGES'][$key];
160 $wikiMessage = $wiki[$key];
161 $wikiContent = $wikiMessage->translation();
165 $wikiContent = str_replace( TRANSLATE_FUZZY,
'', $wikiContent );
168 if ( $supportsFuzzy ===
'yes' && $wikiMessage->hasTag(
'fuzzy' ) ) {
169 $wikiContent = TRANSLATE_FUZZY . $wikiContent;
172 if ( $ffs->isContentEqual( $sourceContent, $wikiContent ) ) {
180 if ( $reason !== MessageGroupCache::NO_CACHE ) {
181 $cacheContent = $cache->
get( $key );
190 $this->hasCacheEntry( $cache, $wiki, $key ) &&
191 !$ffs->isContentEqual( $wikiContent, $cacheContent ) &&
192 $ffs->isContentEqual( $sourceContent, $cacheContent )
198 if ( $language !== $sourceLanguage ) {
203 if ( $renameMsg !==
null ) {
208 $this->addNonSourceRenames(
209 $changes, $key, $renameMsg[
'key'], $sourceContent, $wikiContent, $language
211 $changesToRemove[] = $key;
215 $changes->
addChange( $language, $key, $sourceContent );
220 $added = array_diff( $fileKeys, $wikiKeys );
221 foreach ( $added as $key ) {
222 $sourceContent = $file[
'MESSAGES'][$key];
223 $changes->
addAddition( $language, $key, $sourceContent );
230 if ( $reason !== MessageGroupCache::NO_CACHE ) {
231 $deleted = array_diff( $wikiKeys, $fileKeys );
232 foreach ( $deleted as $key ) {
233 if ( $cache->
get( $key ) ===
false ) {
238 $changes->
addDeletion( $language, $key, $wiki[$key]->translation() );
242 if ( $language === $sourceLanguage ) {
243 $this->findAndMarkSourceRenames( $changes, $language );
246 $this->checkNonSourceAdditionsForRename(
247 $changes, $sourceLanguage, $language, $wiki, $wikiKeys
261 private function checkNonSourceAdditionsForRename(
262 MessageSourceChange $changes,
263 string $sourceLanguage,
264 string $targetLanguage,
265 MessageCollection $wiki,
268 $additions = $changes->getAdditions( $targetLanguage );
269 if ( $additions === [] ) {
273 $additionsToRemove = [];
274 $deletionsToRemove = [];
275 foreach ( $additions as $addedMsg ) {
276 $addedMsgKey = $addedMsg[
'key'];
279 $renamedSourceMsg = $changes->findMessage(
280 $sourceLanguage, $addedMsgKey, [ MessageSourceChange::RENAME ]
283 if ( $renamedSourceMsg ===
null ) {
289 $deletedSource = $changes->getMatchedMessage( $sourceLanguage, $renamedSourceMsg[
'key'] );
290 if ( $deletedSource ===
null ) {
293 $deletedMsgKey = $deletedSource[
'key'];
294 $deletedMsg = $changes->findMessage(
295 $targetLanguage, $deletedMsgKey, [ MessageSourceChange::DELETION ]
301 if ( $deletedMsg ===
null ) {
303 if ( in_array( $deletedMsgKey, $wikiKeys ) ) {
304 $content = $wiki[ $deletedMsgKey ]->translation();
307 'key' => $deletedMsgKey,
308 'content' => $content
312 $similarityPercent = $this->stringComparator->getSimilarity(
313 $addedMsg[
'content'], $deletedMsg[
'content']
316 $changes->addRename( $targetLanguage, [
317 'key' => $addedMsgKey,
318 'content' => $addedMsg[
'content']
320 'key' => $deletedMsgKey,
321 'content' => $deletedMsg[
'content']
322 ], $similarityPercent );
324 $deletionsToRemove[] = $deletedMsgKey;
325 $additionsToRemove[] = $addedMsgKey;
328 $changes->removeAdditions( $targetLanguage, $additionsToRemove );
329 $changes->removeDeletions( $targetLanguage, $deletionsToRemove );
337 private function findAndMarkSourceRenames( MessageSourceChange $changes,
string $sourceLanguage ): void {
341 $deletions = $changes->getDeletions( $sourceLanguage );
342 $additions = $changes->getAdditions( $sourceLanguage );
343 if ( $deletions === [] || $additions === [] ) {
349 $potentialRenames = [];
350 foreach ( $additions as $addedMsg ) {
351 $addedMsgKey = $addedMsg[
'key'];
353 foreach ( $deletions as $deletedMsg ) {
354 $similarityPercent = $this->stringComparator->getSimilarity(
355 $addedMsg[
'content'], $deletedMsg[
'content']
358 if ( $changes->areStringsSimilar( $similarityPercent ) ) {
359 $potentialRenames[ $addedMsgKey .
'|' . $deletedMsg[
'key'] ] = $similarityPercent;
364 $this->matchRenames( $changes, $potentialRenames, $sourceLanguage );
368 private function addNonSourceRenames(
369 MessageSourceChange $changes,
372 string $sourceContent,
378 'content' => $sourceContent
383 'content' => $wikiContent
386 $similarityPercent = $this->stringComparator->getSimilarity(
387 $sourceContent, $wikiContent
389 $changes->addRename( $language, $addedMsg, $removedMsg, $similarityPercent );
399 private function matchRenames( MessageSourceChange $changes, array $trackRename,
string $language ): void {
400 arsort( $trackRename, SORT_NUMERIC );
402 $alreadyRenamed = $additionsToRemove = $deletionsToRemove = [];
403 foreach ( $trackRename as $key => $similarityPercent ) {
404 [ $addKey, $deleteKey ] = explode(
'|', $key, 2 );
405 if ( isset( $alreadyRenamed[ $addKey ] ) || isset( $alreadyRenamed[ $deleteKey ] ) ) {
411 $alreadyRenamed[ $addKey ] = 1;
412 $alreadyRenamed[ $deleteKey ] = 1;
414 $addMsg = $changes->findMessage( $language, $addKey, [ MessageSourceChange::ADDITION ] );
415 $deleteMsg = $changes->findMessage( $language, $deleteKey, [ MessageSourceChange::DELETION ] );
417 $changes->addRename( $language, $addMsg, $deleteMsg, $similarityPercent );
420 $additionsToRemove[] = $addMsg[
'key'];
422 $deletionsToRemove[] = $deleteMsg[
'key'];
425 $changes->removeAdditions( $language, $additionsToRemove );
426 $changes->removeDeletions( $language, $deletionsToRemove );
434 private function hasCacheEntry(
435 MessageGroupCache $cache,
436 MessageCollection $collection,
439 $cacheContent = $cache->get( $messageKey );
440 if ( $cacheContent !==
false ) {
444 $cacheUpdateTime = $cache->getUpdateTimestamp();
445 $cacheUpdateTime = $cacheUpdateTime !==
false ? MWTimestamp::convert( TS_MW, $cacheUpdateTime ) : false;
447 $pageIdentity = $this->pageStore->getPageForLink( $collection->keys()[ $messageKey ] );
448 $oldestRevision = $this->revisionLookup->getFirstRevision( $pageIdentity );
449 $latestRevision = $this->revisionLookup->getRevisionByTitle( $pageIdentity );
451 $logger = LoggerFactory::getInstance( LogNames::GROUP_SYNCHRONIZATION );
458 $cacheUpdateTime !==
false &&
459 ( $oldestRevision && $oldestRevision->getTimestamp() < $cacheUpdateTime ) &&
460 ( $latestRevision && $cacheUpdateTime < $latestRevision->getTimestamp() )
463 'Expected cache miss for {messageKey} in language: {language}. Cache update time: {cacheUpdateTime}',
465 'messageKey' => $messageKey,
466 'language' => $collection->getLanguage(),
467 'cacheUpdateTime' => $cacheUpdateTime,
468 'oldestRevisionTs' => $oldestRevision->getTimestamp(),
469 'latestRevisionTs' => $latestRevision->getTimestamp()
476 'Unexpected cache miss for {messageKey} in language: {language}. Cache update time: {cacheUpdateTime}',
478 'messageKey' => $messageKey,
479 'language' => $collection->getLanguage(),
480 'cacheUpdateTime' => $cacheUpdateTime,
481 'oldestRevisionTs' => $oldestRevision ? $oldestRevision->getTimestamp() :
'N/A',
482 'latestRevisionTs' => $latestRevision ? $latestRevision->getTimestamp() :
'N/A'