From 42578687d9a9afeabca7d00861246a5cd4bfee16 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Tue, 24 Mar 2026 15:25:12 +0100 Subject: [PATCH 01/17] Implement leveraging by name --- .../mojito/cli/command/LeveragingCommand.java | 3 +- .../cli/command/LeveragingCommandTest.java | 88 ++++++++++++++ .../input/source/source-xliff.xliff | 29 +++++ .../input/source3/source-xliff.xliff | 26 +++++ .../translations/source-xliff_fr-FR.xliff | 35 ++++++ .../translations/source-xliff_ja-JP.xliff | 35 ++++++ .../l10n/mojito/rest/entity/CopyTmConfig.java | 4 + .../mojito/rest/leveraging/CopyTmConfig.java | 2 + .../service/leveraging/LeveragerByName.java | 46 ++++++++ .../service/leveraging/LeveragingService.java | 13 ++- .../leveraging/LeveragingServiceTest.java | 110 ++++++++++++++++++ 11 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source/source-xliff.xliff create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source3/source-xliff.xliff create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_fr-FR.xliff create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_ja-JP.xliff create mode 100644 webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragerByName.java diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index 06159647f1..a025927dd2 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -86,7 +86,8 @@ public class LeveragingCommand extends Command { description = "Matching mode. " + "MD5 will perform matching based on the ID, content and comment. " - + "EXACT match is only using the content.", + + "EXACT match is only using the content. " + + "NAME match is only using the resource name.", converter = CopyTmConfigModeConverter.class) CopyTmConfig.Mode mode = CopyTmConfig.Mode.MD5; diff --git a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java index 9619622667..7170cec90e 100644 --- a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java +++ b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java @@ -197,6 +197,94 @@ public void copyTMModeExact() throws Exception { Assert.assertFalse(itTargetTranslations.hasNext()); } + @Test + public void copyTMModeName() throws Exception { + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("source-repoisotry")); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + repositoryService.addRepositoryLocale(sourceRepository, "fr-CA", "fr-FR", false); + repositoryService.addRepositoryLocale(sourceRepository, "ja-JP"); + + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + repositoryService.addRepositoryLocale(targetRepository, "fr-CA", "fr-FR", false); + repositoryService.addRepositoryLocale(targetRepository, "ja-JP"); + + getL10nJCommander() + .run( + "push", + "-r", + sourceRepository.getName(), + "-s", + getInputResourcesTestDir("source").getAbsolutePath()); + + getL10nJCommander() + .run( + "push", + "-r", + targetRepository.getName(), + "-s", + getInputResourcesTestDir("source3").getAbsolutePath()); + + Asset asset = + assetClient.getAssetByPathAndRepositoryId("source-xliff.xliff", sourceRepository.getId()); + importTranslations(asset.getId(), "source-xliff_", "fr-FR"); + importTranslations(asset.getId(), "source-xliff_", "ja-JP"); + + List initialTargetTranslations = + tmTextUnitVariantRepository.findByTmTextUnitTmRepositoriesOrderByContent(targetRepository); + + assertEquals("There must be only english for now", 5, initialTargetTranslations.size()); + + getL10nJCommander() + .run( + "leveraging-copy-tm", + "-s", + sourceRepository.getName(), + "-t", + targetRepository.getName(), + "-m", + "NAME"); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesOrderByContent(targetRepository) + .stream() + .sorted( + Comparator.comparing( + TMTextUnitVariant::getLocale, Comparator.comparing(Locale::getBcp47Tag)) + .thenComparing(TMTextUnitVariant::getContent)) + .map(TMTextUnitVariant::getContent) + .peek(t -> logger.info("target translation: {}", t)) + .collect(Collectors.toList()); + + List expectedTargetTranslations = + Arrays.asList( + "1 hour", + "1 month", + "100 char limit:", + "15 min", + "one day", + "1 heure", + "1 jour", + "1 mois", + "15 min", + "Description de 100 caract\u00e8res\u00a0:", + "100\u6587\u5b57\u306e\u8aac\u660e\uff1a", + "15\u5206", + "1\u304b\u6708", + "1\u65e5", + "1\u6642\u9593"); + + Assert.assertEquals( + "All target text units should have translations leveraged by name", + expectedTargetTranslations, + targetTranslations); + } + @Test public void copyTMModeTUIDs() throws Exception { diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source/source-xliff.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source/source-xliff.xliff new file mode 100644 index 0000000000..d95c81a0de --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source/source-xliff.xliff @@ -0,0 +1,29 @@ + + + + + + 100 character description: + + + 15 min + File lock dialog duration + + + 15 min + + + 1 day + File lock dialog duration + + + 1 hour + File lock dialog duration + + + 1 month + File lock dialog duration + + + + diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source3/source-xliff.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source3/source-xliff.xliff new file mode 100644 index 0000000000..6d520326d9 --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/source3/source-xliff.xliff @@ -0,0 +1,26 @@ + + + + + + 100 char limit: + + + 15 min + File lock dialog duration + + + one day + File lock dialog duration + + + 1 hour + File lock dialog duration + + + 1 month + File lock dialog duration + + + + diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_fr-FR.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_fr-FR.xliff new file mode 100644 index 0000000000..bc91a0cc47 --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_fr-FR.xliff @@ -0,0 +1,35 @@ + + + + + + 100 character description: + Description de 100 caractères : + + + 15 min + 15 min + File lock dialog duration + + + 15 min + 15 min duplicated + + + 1 day + 1 jour + File lock dialog duration + + + 1 hour + 1 heure + File lock dialog duration + + + 1 month + 1 mois + File lock dialog duration + + + + \ No newline at end of file diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_ja-JP.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_ja-JP.xliff new file mode 100644 index 0000000000..0661fde134 --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeName/input/translations/source-xliff_ja-JP.xliff @@ -0,0 +1,35 @@ + + + + + + 100 character description: + 100文字の説明: + + + 15 min + 15分 + File lock dialog duration + + + 15 min + 15分 duplicate + + + 1 day + 1日 + File lock dialog duration + + + 1 hour + 1時間 + File lock dialog duration + + + 1 month + 1か月 + File lock dialog duration + + + + diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index 3e05e088b3..a593661c78 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -107,6 +107,10 @@ public enum Mode { MD5, /** Exact match means the content must be the same (message id and comment are not checked). */ EXACT, + /** + * Name match means the resource name must be the same (content and comment are not checked). + */ + NAME, /** Copy based on a map of source to target tmTextUnitId */ TUIDS } diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index 7be7193b66..ff2491cbcc 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -110,6 +110,8 @@ public enum Mode { MD5, /** Exact match means the content must be the same (message id and comment are not checked) */ EXACT, + /** Name match means the resource name must be the same (content and comment are not checked) */ + NAME, /** Copy based on a Map source to target tmTextUnitId */ TUIDS } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragerByName.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragerByName.java new file mode 100644 index 0000000000..e871e2c44e --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragerByName.java @@ -0,0 +1,46 @@ +package com.box.l10n.mojito.service.leveraging; + +import com.box.l10n.mojito.entity.TMTextUnit; +import com.box.l10n.mojito.service.tm.search.StatusFilter; +import com.box.l10n.mojito.service.tm.search.TextUnitDTO; +import com.box.l10n.mojito.service.tm.search.TextUnitSearcherParameters; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Performs leveraging based on the name of the text units. The leveraged translations will need to + * be re-translated since the content may differ. + * + * @author wwawrzenczak + */ +@Component +public class LeveragerByName extends AbstractLeverager { + + static Logger logger = LoggerFactory.getLogger(LeveragerByName.class); + + @Override + public List getLeveragingMatches( + TMTextUnit tmTextUnit, Long sourceTmId, Long sourceAssetId) { + logger.debug("Get TextUnitDTOs for leveraging by name"); + + TextUnitSearcherParameters textUnitSearcherParameters = new TextUnitSearcherParameters(); + textUnitSearcherParameters.setTmId(sourceTmId); + textUnitSearcherParameters.setAssetId(sourceAssetId); + textUnitSearcherParameters.setName(tmTextUnit.getName()); + textUnitSearcherParameters.setStatusFilter(StatusFilter.TRANSLATED); + + return textUnitSearcher.search(textUnitSearcherParameters); + } + + @Override + public boolean isTranslationNeededIfUniqueMatch() { + return true; + } + + @Override + public String getType() { + return "Leverage by name"; + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java index 3ec39fbe4f..16fdda5252 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java @@ -57,6 +57,8 @@ public class LeveragingService { @Autowired LeveragerByNameAndContent leveragerByNameAndContent; + @Autowired LeveragerByName leveragerByName; + @Autowired RepositoryRepository repositoryRepository; @Autowired AssetRepository assetRepository; @@ -158,13 +160,22 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) } else if (CopyTmConfig.Mode.MD5.equals(copyTmConfig.getMode())) { leveragerByMd5.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + } else if (CopyTmConfig.Mode.NAME.equals(copyTmConfig.getMode())) { + logger.debug( + "First perform leveraging by name and content (to give priority to strings with same content)"); + leveragerByNameAndContent.performLeveragingFor( + textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + + logger.debug("Now, perform leveraging only on the name"); + leveragerByName.performLeveragingFor( + textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); } else { logger.debug( "First perform leveraging by name and content (to give priority to string with same tags"); leveragerByNameAndContent.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); - logger.debug("Now, perform leveraging only on the name"); + logger.debug("Now, perform leveraging only on the content"); leveragerByContent.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 0062301f0d..57fcfcffc1 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -264,6 +264,116 @@ public void copyTranslationForTmTextUnitMapping() targetTmTextUnit1.getId(), targetTranslations.get(2).getTmTextUnit().getId()); } + @Test + public void copyAllTranslationsWithNameMatchBetweenRepositories() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + TMTestData tmTestDataSource = new TMTestData(testIdWatcher); + + Repository sourceRepository = tmTestDataSource.repository; + + logger.debug("Create the target repository"); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + + TM tm = targetRepository.getTm(); + + Asset asset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + Long assetId = asset.getId(); + + tmService.addTMTextUnit( + tm.getId(), + assetId, + "zuora_error_message_verify_state_province", + "Different source content", + "DifferentComment"); + tmService.addTMTextUnit( + tm.getId(), assetId, "TEST2", "Different Content2", "DifferentComment2"); + tmService.addTMTextUnit(tm.getId(), assetId, "TEST3", "Content3", "Comment3"); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.NAME); + + leveragingService.copyTm(copyTmConfig).get(); + + List sourceTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + sourceRepository, "en"); + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Iterator itSource = sourceTranslations.iterator(); + Iterator itTarget = targetTranslations.iterator(); + + while (itTarget.hasNext()) { + TMTextUnitVariant next = itTarget.next(); + Assert.assertEquals( + "translation in source and target must be the same", + itSource.next().getContent(), + next.getContent()); + } + + Assert.assertFalse(itSource.hasNext()); + } + + @Test + public void copyTranslationsWithNameMatchDoesNotMatchDifferentNames() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + TMTestData tmTestDataSource = new TMTestData(testIdWatcher); + + Repository sourceRepository = tmTestDataSource.repository; + + logger.debug("Create the target repository"); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + + TM tm = targetRepository.getTm(); + + Asset asset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + Long assetId = asset.getId(); + + tmService.addTMTextUnit( + tm.getId(), + assetId, + "completely_different_name", + "Please enter a valid state, region or province", + "Comment1"); + tmService.addTMTextUnit(tm.getId(), assetId, "another_different_name", "Content2", "Comment2"); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.NAME); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertEquals( + "No translations should be copied when names don't match", 0, targetTranslations.size()); + } + @Test public void copyAllTranslationsWithExactMatchBetweenRepositories() throws InterruptedException, From a23714ae265ae66ef20bbb57f97c675395e55a86 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Wed, 25 Mar 2026 00:28:50 +0100 Subject: [PATCH 02/17] Implement option to force preservation of leveraged unit status --- .../mojito/cli/command/LeveragingCommand.java | 10 ++ .../cli/command/LeveragingCommandTest.java | 70 ++++++++++++ .../input/source/source-xliff.xliff | 29 +++++ .../input/source3/source-xliff.xliff | 26 +++++ .../translations/source-xliff_fr-FR.xliff | 35 ++++++ .../translations/source-xliff_ja-JP.xliff | 35 ++++++ .../l10n/mojito/rest/entity/CopyTmConfig.java | 10 ++ .../mojito/rest/leveraging/CopyTmConfig.java | 10 ++ .../service/leveraging/AbstractLeverager.java | 14 ++- .../service/leveraging/LeveragingService.java | 27 ++++- .../leveraging/LeveragingServiceTest.java | 103 ++++++++++++++++++ 11 files changed, 362 insertions(+), 7 deletions(-) create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source/source-xliff.xliff create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source3/source-xliff.xliff create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_fr-FR.xliff create mode 100644 cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_ja-JP.xliff diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index a025927dd2..f1cad001dd 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -91,6 +91,15 @@ public class LeveragingCommand extends Command { converter = CopyTmConfigModeConverter.class) CopyTmConfig.Mode mode = CopyTmConfig.Mode.MD5; + @Parameter( + names = {"--preserve-status", "-ps"}, + arity = 1, + required = false, + description = + "When true, keep the source translation's status (e.g. APPROVED) " + + "even if the matching mode would normally require re-translation") + boolean preserveStatus = false; + @Parameter( names = {"--tuids-mapping"}, required = false, @@ -145,6 +154,7 @@ void copyTmBetweenRepositories() throws CommandException { copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setNameRegex(nameRegexParam); copyTmConfig.setTargetBranchName(targetBranchNameParam); + copyTmConfig.setPreserveStatus(preserveStatus); if (mode != null) { copyTmConfig.setMode(mode); diff --git a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java index 7170cec90e..bcaaff1b4c 100644 --- a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java +++ b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java @@ -285,6 +285,76 @@ public void copyTMModeName() throws Exception { targetTranslations); } + @Test + public void copyTMModeNamePreserveStatus() throws Exception { + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("source-repoisotry")); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + repositoryService.addRepositoryLocale(sourceRepository, "fr-CA", "fr-FR", false); + repositoryService.addRepositoryLocale(sourceRepository, "ja-JP"); + + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + repositoryService.addRepositoryLocale(targetRepository, "fr-CA", "fr-FR", false); + repositoryService.addRepositoryLocale(targetRepository, "ja-JP"); + + getL10nJCommander() + .run( + "push", + "-r", + sourceRepository.getName(), + "-s", + getInputResourcesTestDir("source").getAbsolutePath()); + + getL10nJCommander() + .run( + "push", + "-r", + targetRepository.getName(), + "-s", + getInputResourcesTestDir("source3").getAbsolutePath()); + + Asset asset = + assetClient.getAssetByPathAndRepositoryId("source-xliff.xliff", sourceRepository.getId()); + importTranslations(asset.getId(), "source-xliff_", "fr-FR"); + importTranslations(asset.getId(), "source-xliff_", "ja-JP"); + + getL10nJCommander() + .run( + "leveraging-copy-tm", + "-s", + sourceRepository.getName(), + "-t", + targetRepository.getName(), + "-m", + "NAME", + "--preserve-status", + "true"); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesOrderByContent(targetRepository) + .stream() + .filter(v -> !v.getLocale().getBcp47Tag().equals("en")) + .sorted( + Comparator.comparing( + TMTextUnitVariant::getLocale, Comparator.comparing(Locale::getBcp47Tag)) + .thenComparing(TMTextUnitVariant::getContent)) + .collect(Collectors.toList()); + + Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); + + for (TMTextUnitVariant variant : targetTranslations) { + Assert.assertEquals( + "Status should be preserved as APPROVED when --preserve-status is true", + TMTextUnitVariant.Status.APPROVED, + variant.getStatus()); + } + } + @Test public void copyTMModeTUIDs() throws Exception { diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source/source-xliff.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source/source-xliff.xliff new file mode 100644 index 0000000000..d95c81a0de --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source/source-xliff.xliff @@ -0,0 +1,29 @@ + + + + + + 100 character description: + + + 15 min + File lock dialog duration + + + 15 min + + + 1 day + File lock dialog duration + + + 1 hour + File lock dialog duration + + + 1 month + File lock dialog duration + + + + diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source3/source-xliff.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source3/source-xliff.xliff new file mode 100644 index 0000000000..6d520326d9 --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/source3/source-xliff.xliff @@ -0,0 +1,26 @@ + + + + + + 100 char limit: + + + 15 min + File lock dialog duration + + + one day + File lock dialog duration + + + 1 hour + File lock dialog duration + + + 1 month + File lock dialog duration + + + + diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_fr-FR.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_fr-FR.xliff new file mode 100644 index 0000000000..bc91a0cc47 --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_fr-FR.xliff @@ -0,0 +1,35 @@ + + + + + + 100 character description: + Description de 100 caractères : + + + 15 min + 15 min + File lock dialog duration + + + 15 min + 15 min duplicated + + + 1 day + 1 jour + File lock dialog duration + + + 1 hour + 1 heure + File lock dialog duration + + + 1 month + 1 mois + File lock dialog duration + + + + \ No newline at end of file diff --git a/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_ja-JP.xliff b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_ja-JP.xliff new file mode 100644 index 0000000000..0661fde134 --- /dev/null +++ b/cli/src/test/resources/com/box/l10n/mojito/cli/command/LeveragingCommandTest_IO/copyTMModeNamePreserveStatus/input/translations/source-xliff_ja-JP.xliff @@ -0,0 +1,35 @@ + + + + + + 100 character description: + 100文字の説明: + + + 15 min + 15分 + File lock dialog duration + + + 15 min + 15分 duplicate + + + 1 day + 1日 + File lock dialog duration + + + 1 hour + 1時間 + File lock dialog duration + + + 1 month + 1か月 + File lock dialog duration + + + + diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index a593661c78..d82323743d 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -21,6 +21,8 @@ public class CopyTmConfig { Mode mode = Mode.MD5; + boolean preserveStatus = false; + PollableTask pollableTask; @JsonProperty @@ -101,6 +103,14 @@ public void setTargetBranchName(String targetBranchName) { this.targetBranchName = targetBranchName; } + public boolean isPreserveStatus() { + return preserveStatus; + } + + public void setPreserveStatus(boolean preserveStatus) { + this.preserveStatus = preserveStatus; + } + /** Matching mode for leveraging */ public enum Mode { /** MD5 match means the message id, comment and content must be the same */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index ff2491cbcc..029d4238c1 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -22,6 +22,8 @@ public class CopyTmConfig { Mode mode = Mode.MD5; + boolean preserveStatus = false; + PollableTask pollableTask; @JsonProperty @@ -104,6 +106,14 @@ public void setTargetBranchName(String targetBranchName) { this.targetBranchName = targetBranchName; } + public boolean isPreserveStatus() { + return preserveStatus; + } + + public void setPreserveStatus(boolean preserveStatus) { + this.preserveStatus = preserveStatus; + } + /** Matching mode for leveraging */ public enum Mode { /** MD5 match means the message id, comment and content must be the same */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index 8d0de99fa5..e51eb6ff7b 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -82,8 +82,17 @@ public abstract List getLeveragingMatches( * @param assetId */ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, Long assetId) { + performLeveragingFor(tmTextUnits, sourceTmId, assetId, false); + } + + /** + * @param preserveStatus if {@code true}, the source translation's status is always preserved + * regardless of the match precision + */ + public void performLeveragingFor( + List tmTextUnits, Long sourceTmId, Long assetId, boolean preserveStatus) { - logger.debug("Perform leveraging: {}", getType()); + logger.debug("Perform leveraging: {}, preserveStatus: {}", getType(), preserveStatus); for (Iterator tmTextUnitsIterator = tmTextUnits.iterator(); tmTextUnitsIterator.hasNext(); ) { @@ -110,7 +119,8 @@ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, textUnitDTOsForLeveragingSize == textUnitDTOsForLeveraging.size(); logger.debug("Determine if re-translation is needed for the strings that will be copied"); - boolean translationNeeded = isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched; + boolean translationNeeded = + !preserveStatus && (isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched); addLeveragedTranslations( tmTextUnit, textUnitDTOsForLeveraging, translationNeeded, uniqueTMTextUnitMatched); diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java index 16fdda5252..0843be9e00 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java @@ -155,29 +155,46 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) copyTmConfig.getNameRegex(), copyTmConfig.getTargetBranchName()); + boolean preserveStatus = copyTmConfig.isPreserveStatus(); + if (CopyTmConfig.Mode.TUIDS.equals(copyTmConfig.getMode())) { copyTranslationBetweenTextUnits(copyTmConfig.getSourceToTargetTmTextUnitIds()); } else if (CopyTmConfig.Mode.MD5.equals(copyTmConfig.getMode())) { leveragerByMd5.performLeveragingFor( - textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatus); } else if (CopyTmConfig.Mode.NAME.equals(copyTmConfig.getMode())) { logger.debug( "First perform leveraging by name and content (to give priority to strings with same content)"); leveragerByNameAndContent.performLeveragingFor( - textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatus); logger.debug("Now, perform leveraging only on the name"); leveragerByName.performLeveragingFor( - textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatus); } else { logger.debug( "First perform leveraging by name and content (to give priority to string with same tags"); leveragerByNameAndContent.performLeveragingFor( - textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatus); logger.debug("Now, perform leveraging only on the content"); leveragerByContent.performLeveragingFor( - textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatus); } } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 57fcfcffc1..e7ce4ebbbb 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -327,6 +327,109 @@ public void copyAllTranslationsWithNameMatchBetweenRepositories() Assert.assertFalse(itSource.hasNext()); } + @Test + public void copyWithNameMatchPreservesStatusWhenFlagSet() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + TMTestData tmTestDataSource = new TMTestData(testIdWatcher); + + Repository sourceRepository = tmTestDataSource.repository; + + logger.debug("Create the target repository"); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + + TM tm = targetRepository.getTm(); + + Asset asset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + Long assetId = asset.getId(); + + tmService.addTMTextUnit( + tm.getId(), + assetId, + "zuora_error_message_verify_state_province", + "Different source content", + "DifferentComment"); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.NAME); + copyTmConfig.setPreserveStatus(true); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); + for (TMTextUnitVariant variant : targetTranslations) { + Assert.assertEquals( + "Status should be preserved as APPROVED when preserveStatus is true", + TMTextUnitVariant.Status.APPROVED, + variant.getStatus()); + } + } + + @Test + public void copyWithNameMatchSetsTranslationNeededWithoutPreserveStatus() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + TMTestData tmTestDataSource = new TMTestData(testIdWatcher); + + Repository sourceRepository = tmTestDataSource.repository; + + logger.debug("Create the target repository"); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + + TM tm = targetRepository.getTm(); + + Asset asset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + Long assetId = asset.getId(); + + tmService.addTMTextUnit( + tm.getId(), + assetId, + "zuora_error_message_verify_state_province", + "Different source content", + "DifferentComment"); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.NAME); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); + for (TMTextUnitVariant variant : targetTranslations) { + Assert.assertEquals( + "Status should be TRANSLATION_NEEDED when preserveStatus is false for NAME mode", + TMTextUnitVariant.Status.TRANSLATION_NEEDED, + variant.getStatus()); + } + } + @Test public void copyTranslationsWithNameMatchDoesNotMatchDifferentNames() throws InterruptedException, From 2514d9af330741fc3c982e8f5d573b638b634cfc Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Wed, 25 Mar 2026 00:48:00 +0100 Subject: [PATCH 03/17] Allow choosing between just a bit unsafe and even more unsafe status preservation modes --- .../mojito/cli/command/LeveragingCommand.java | 11 ++-- .../command/PreserveStatusModeConverter.java | 11 ++++ .../cli/command/LeveragingCommandTest.java | 4 +- .../l10n/mojito/rest/entity/CopyTmConfig.java | 20 +++++-- .../mojito/rest/leveraging/CopyTmConfig.java | 20 +++++-- .../service/leveraging/AbstractLeverager.java | 25 +++++--- .../service/leveraging/LeveragingService.java | 12 ++-- .../leveraging/LeveragingServiceTest.java | 60 +++++++++++++++++-- 8 files changed, 129 insertions(+), 34 deletions(-) create mode 100644 cli/src/main/java/com/box/l10n/mojito/cli/command/PreserveStatusModeConverter.java diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index f1cad001dd..0a8ec4cac3 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -96,9 +96,12 @@ public class LeveragingCommand extends Command { arity = 1, required = false, description = - "When true, keep the source translation's status (e.g. APPROVED) " - + "even if the matching mode would normally require re-translation") - boolean preserveStatus = false; + "Controls whether to keep the source translation's status. " + + "NONE (default): the leverager decides based on match mode precision. " + + "UNIQUE: preserve the status only when the match is unique. Just a bit unsafe." + + "ANY: always preserve the status, even for non-unique matches. Unsafe!", + converter = PreserveStatusModeConverter.class) + CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.NONE; @Parameter( names = {"--tuids-mapping"}, @@ -154,7 +157,7 @@ void copyTmBetweenRepositories() throws CommandException { copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setNameRegex(nameRegexParam); copyTmConfig.setTargetBranchName(targetBranchNameParam); - copyTmConfig.setPreserveStatus(preserveStatus); + copyTmConfig.setPreserveStatusMode(preserveStatusMode); if (mode != null) { copyTmConfig.setMode(mode); diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/PreserveStatusModeConverter.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/PreserveStatusModeConverter.java new file mode 100644 index 0000000000..4012978211 --- /dev/null +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/PreserveStatusModeConverter.java @@ -0,0 +1,11 @@ +package com.box.l10n.mojito.cli.command; + +import com.box.l10n.mojito.rest.entity.CopyTmConfig; + +public class PreserveStatusModeConverter extends EnumConverter { + + @Override + protected Class getGenericClass() { + return CopyTmConfig.PreserveStatusMode.class; + } +} diff --git a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java index bcaaff1b4c..ca91d2a263 100644 --- a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java +++ b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java @@ -332,7 +332,7 @@ public void copyTMModeNamePreserveStatus() throws Exception { "-m", "NAME", "--preserve-status", - "true"); + "ANY"); List targetTranslations = tmTextUnitVariantRepository @@ -349,7 +349,7 @@ public void copyTMModeNamePreserveStatus() throws Exception { for (TMTextUnitVariant variant : targetTranslations) { Assert.assertEquals( - "Status should be preserved as APPROVED when --preserve-status is true", + "Status should be preserved as APPROVED with ANY", TMTextUnitVariant.Status.APPROVED, variant.getStatus()); } diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index d82323743d..dd37cc1aee 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -21,7 +21,7 @@ public class CopyTmConfig { Mode mode = Mode.MD5; - boolean preserveStatus = false; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.NONE; PollableTask pollableTask; @@ -103,12 +103,12 @@ public void setTargetBranchName(String targetBranchName) { this.targetBranchName = targetBranchName; } - public boolean isPreserveStatus() { - return preserveStatus; + public PreserveStatusMode getPreserveStatusMode() { + return preserveStatusMode; } - public void setPreserveStatus(boolean preserveStatus) { - this.preserveStatus = preserveStatus; + public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { + this.preserveStatusMode = preserveStatusMode; } /** Matching mode for leveraging */ @@ -124,4 +124,14 @@ public enum Mode { /** Copy based on a map of source to target tmTextUnitId */ TUIDS } + + /** Controls whether the source translation's status is preserved during leveraging */ + public enum PreserveStatusMode { + /** Default: the leverager decides based on match precision */ + NONE, + /** Preserve the source status only when the match is unique (single TMTextUnit matched) */ + UNIQUE, + /** Always preserve the source status, even for non-unique matches */ + ANY + } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index 029d4238c1..5e5a603b37 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -22,7 +22,7 @@ public class CopyTmConfig { Mode mode = Mode.MD5; - boolean preserveStatus = false; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.NONE; PollableTask pollableTask; @@ -106,12 +106,12 @@ public void setTargetBranchName(String targetBranchName) { this.targetBranchName = targetBranchName; } - public boolean isPreserveStatus() { - return preserveStatus; + public PreserveStatusMode getPreserveStatusMode() { + return preserveStatusMode; } - public void setPreserveStatus(boolean preserveStatus) { - this.preserveStatus = preserveStatus; + public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { + this.preserveStatusMode = preserveStatusMode; } /** Matching mode for leveraging */ @@ -125,4 +125,14 @@ public enum Mode { /** Copy based on a Map source to target tmTextUnitId */ TUIDS } + + /** Controls whether the source translation's status is preserved during leveraging */ + public enum PreserveStatusMode { + /** Default: the leverager decides based on match precision */ + NONE, + /** Preserve the source status only when the match is unique (single TMTextUnit matched) */ + UNIQUE, + /** Always preserve the source status, even for non-unique matches */ + ANY + } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index e51eb6ff7b..b42c134d67 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -5,6 +5,7 @@ import com.box.l10n.mojito.entity.TMTextUnitCurrentVariant; import com.box.l10n.mojito.entity.TMTextUnitVariant; import com.box.l10n.mojito.entity.TMTextUnitVariantComment; +import com.box.l10n.mojito.rest.leveraging.CopyTmConfig.PreserveStatusMode; import com.box.l10n.mojito.service.assetExtraction.AssetMappingService; import com.box.l10n.mojito.service.tm.AddTMTextUnitCurrentVariantResult; import com.box.l10n.mojito.service.tm.TMService; @@ -82,17 +83,16 @@ public abstract List getLeveragingMatches( * @param assetId */ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, Long assetId) { - performLeveragingFor(tmTextUnits, sourceTmId, assetId, false); + performLeveragingFor(tmTextUnits, sourceTmId, assetId, PreserveStatusMode.NONE); } - /** - * @param preserveStatus if {@code true}, the source translation's status is always preserved - * regardless of the match precision - */ public void performLeveragingFor( - List tmTextUnits, Long sourceTmId, Long assetId, boolean preserveStatus) { + List tmTextUnits, + Long sourceTmId, + Long assetId, + PreserveStatusMode preserveStatusMode) { - logger.debug("Perform leveraging: {}, preserveStatus: {}", getType(), preserveStatus); + logger.debug("Perform leveraging: {}, preserveStatusMode: {}", getType(), preserveStatusMode); for (Iterator tmTextUnitsIterator = tmTextUnits.iterator(); tmTextUnitsIterator.hasNext(); ) { @@ -120,7 +120,7 @@ public void performLeveragingFor( logger.debug("Determine if re-translation is needed for the strings that will be copied"); boolean translationNeeded = - !preserveStatus && (isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched); + computeTranslationNeeded(preserveStatusMode, uniqueTMTextUnitMatched); addLeveragedTranslations( tmTextUnit, textUnitDTOsForLeveraging, translationNeeded, uniqueTMTextUnitMatched); @@ -130,6 +130,15 @@ public void performLeveragingFor( } } + boolean computeTranslationNeeded( + PreserveStatusMode preserveStatusMode, boolean uniqueTMTextUnitMatched) { + return switch (preserveStatusMode) { + case ANY -> false; + case UNIQUE -> !uniqueTMTextUnitMatched; + case NONE -> isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched; + }; + } + /** * Adds translations (potentially to be re-translated) into the {@link TMTextUnit}. * diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java index 0843be9e00..1da54d9d42 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java @@ -155,7 +155,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) copyTmConfig.getNameRegex(), copyTmConfig.getTargetBranchName()); - boolean preserveStatus = copyTmConfig.isPreserveStatus(); + CopyTmConfig.PreserveStatusMode preserveStatusMode = copyTmConfig.getPreserveStatusMode(); if (CopyTmConfig.Mode.TUIDS.equals(copyTmConfig.getMode())) { copyTranslationBetweenTextUnits(copyTmConfig.getSourceToTargetTmTextUnitIds()); @@ -164,7 +164,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatus); + preserveStatusMode); } else if (CopyTmConfig.Mode.NAME.equals(copyTmConfig.getMode())) { logger.debug( "First perform leveraging by name and content (to give priority to strings with same content)"); @@ -172,14 +172,14 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatus); + preserveStatusMode); logger.debug("Now, perform leveraging only on the name"); leveragerByName.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatus); + preserveStatusMode); } else { logger.debug( "First perform leveraging by name and content (to give priority to string with same tags"); @@ -187,14 +187,14 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatus); + preserveStatusMode); logger.debug("Now, perform leveraging only on the content"); leveragerByContent.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatus); + preserveStatusMode); } } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index e7ce4ebbbb..38b82b6be6 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -328,7 +328,7 @@ public void copyAllTranslationsWithNameMatchBetweenRepositories() } @Test - public void copyWithNameMatchPreservesStatusWhenFlagSet() + public void copyWithNameMatchPreservesStatusForAnyMatch() throws InterruptedException, ExecutionException, RepositoryNameAlreadyUsedException, @@ -361,7 +361,7 @@ public void copyWithNameMatchPreservesStatusWhenFlagSet() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.NAME); - copyTmConfig.setPreserveStatus(true); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); leveragingService.copyTm(copyTmConfig).get(); @@ -373,7 +373,59 @@ public void copyWithNameMatchPreservesStatusWhenFlagSet() Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); for (TMTextUnitVariant variant : targetTranslations) { Assert.assertEquals( - "Status should be preserved as APPROVED when preserveStatus is true", + "Status should be preserved as APPROVED with ANY", + TMTextUnitVariant.Status.APPROVED, + variant.getStatus()); + } + } + + @Test + public void copyWithNameMatchPreservesStatusForUniqueMatchOnly() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + TMTestData tmTestDataSource = new TMTestData(testIdWatcher); + + Repository sourceRepository = tmTestDataSource.repository; + + logger.debug("Create the target repository"); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + + TM tm = targetRepository.getTm(); + + Asset asset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + Long assetId = asset.getId(); + + tmService.addTMTextUnit( + tm.getId(), + assetId, + "zuora_error_message_verify_state_province", + "Different source content", + "DifferentComment"); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.NAME); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.UNIQUE); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); + for (TMTextUnitVariant variant : targetTranslations) { + Assert.assertEquals( + "Status should be preserved as APPROVED for unique match with UNIQUE mode", TMTextUnitVariant.Status.APPROVED, variant.getStatus()); } @@ -424,7 +476,7 @@ public void copyWithNameMatchSetsTranslationNeededWithoutPreserveStatus() Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); for (TMTextUnitVariant variant : targetTranslations) { Assert.assertEquals( - "Status should be TRANSLATION_NEEDED when preserveStatus is false for NAME mode", + "Status should be TRANSLATION_NEEDED with default NONE mode for NAME leveraging", TMTextUnitVariant.Status.TRANSLATION_NEEDED, variant.getStatus()); } From 520f6116bad6610df27f6beebf263f27f4b1d31b Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Wed, 25 Mar 2026 12:36:24 +0100 Subject: [PATCH 04/17] Allow filtering target text units for leveraging based on their status --- .../mojito/cli/command/LeveragingCommand.java | 21 ++++- .../command/TargetStatusFilterConverter.java | 11 +++ .../l10n/mojito/rest/entity/CopyTmConfig.java | 23 +++++ .../mojito/rest/leveraging/CopyTmConfig.java | 24 +++++ .../service/leveraging/AbstractLeverager.java | 68 +++++++++++++-- .../service/leveraging/LeveragingService.java | 55 ++++++------ .../TMTextUnitCurrentVariantRepository.java | 7 ++ .../leveraging/LeveragingServiceTest.java | 87 +++++++++++++++++++ 8 files changed, 258 insertions(+), 38 deletions(-) create mode 100644 cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index 0a8ec4cac3..3ea170c2cf 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -97,12 +97,26 @@ public class LeveragingCommand extends Command { required = false, description = "Controls whether to keep the source translation's status. " - + "NONE (default): the leverager decides based on match mode precision. " - + "UNIQUE: preserve the status only when the match is unique. Just a bit unsafe." - + "ANY: always preserve the status, even for non-unique matches. Unsafe!", + + "NONE (default): the leverager decides based on match precision. " + + "UNIQUE: preserve the status only when the match is unique. " + + "ANY: always preserve the status, even for non-unique matches.", converter = PreserveStatusModeConverter.class) CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.NONE; + @Parameter( + names = {"--target-status", "-ts"}, + arity = 1, + required = false, + description = + "Only leverage into target text units whose current translation matches this status. " + + "When not set, all target text units are eligible. " + + "UNTRANSLATED: no translation in any locale. " + + "TRANSLATION_NEEDED: at least one locale has TRANSLATION_NEEDED status. " + + "REVIEW_NEEDED: at least one locale has REVIEW_NEEDED status. " + + "APPROVED: at least one locale has APPROVED status.", + converter = TargetStatusFilterConverter.class) + CopyTmConfig.TargetStatusFilter targetStatusFilter; + @Parameter( names = {"--tuids-mapping"}, required = false, @@ -158,6 +172,7 @@ void copyTmBetweenRepositories() throws CommandException { copyTmConfig.setNameRegex(nameRegexParam); copyTmConfig.setTargetBranchName(targetBranchNameParam); copyTmConfig.setPreserveStatusMode(preserveStatusMode); + copyTmConfig.setTargetStatusFilter(targetStatusFilter); if (mode != null) { copyTmConfig.setMode(mode); diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java new file mode 100644 index 0000000000..863bfc3664 --- /dev/null +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java @@ -0,0 +1,11 @@ +package com.box.l10n.mojito.cli.command; + +import com.box.l10n.mojito.rest.entity.CopyTmConfig; + +public class TargetStatusFilterConverter extends EnumConverter { + + @Override + protected Class getGenericClass() { + return CopyTmConfig.TargetStatusFilter.class; + } +} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index dd37cc1aee..eec6186eff 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -23,6 +23,8 @@ public class CopyTmConfig { PreserveStatusMode preserveStatusMode = PreserveStatusMode.NONE; + TargetStatusFilter targetStatusFilter; + PollableTask pollableTask; @JsonProperty @@ -111,6 +113,14 @@ public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { this.preserveStatusMode = preserveStatusMode; } + public TargetStatusFilter getTargetStatusFilter() { + return targetStatusFilter; + } + + public void setTargetStatusFilter(TargetStatusFilter targetStatusFilter) { + this.targetStatusFilter = targetStatusFilter; + } + /** Matching mode for leveraging */ public enum Mode { /** MD5 match means the message id, comment and content must be the same */ @@ -134,4 +144,17 @@ public enum PreserveStatusMode { /** Always preserve the source status, even for non-unique matches */ ANY } + + /** + * Filters target text units by their current translation status before leveraging. + * + *

UNTRANSLATED selects text units with no translation in any locale. The other values select + * text units that have at least one translation with the given status. + */ + public enum TargetStatusFilter { + UNTRANSLATED, + APPROVED, + REVIEW_NEEDED, + TRANSLATION_NEEDED + } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index 5e5a603b37..8f95c5c27a 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -24,6 +24,8 @@ public class CopyTmConfig { PreserveStatusMode preserveStatusMode = PreserveStatusMode.NONE; + TargetStatusFilter targetStatusFilter; + PollableTask pollableTask; @JsonProperty @@ -114,6 +116,14 @@ public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { this.preserveStatusMode = preserveStatusMode; } + public TargetStatusFilter getTargetStatusFilter() { + return targetStatusFilter; + } + + public void setTargetStatusFilter(TargetStatusFilter targetStatusFilter) { + this.targetStatusFilter = targetStatusFilter; + } + /** Matching mode for leveraging */ public enum Mode { /** MD5 match means the message id, comment and content must be the same */ @@ -135,4 +145,18 @@ public enum PreserveStatusMode { /** Always preserve the source status, even for non-unique matches */ ANY } + + /** + * Filters target text units by their current translation status before leveraging. + * + *

UNTRANSLATED selects text units with no translation in any locale. The other values + * correspond to {@link com.box.l10n.mojito.entity.TMTextUnitVariant.Status} and select text units + * that have at least one translation with the given status. + */ + public enum TargetStatusFilter { + UNTRANSLATED, + APPROVED, + REVIEW_NEEDED, + TRANSLATION_NEEDED + } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index b42c134d67..485a982017 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -6,14 +6,18 @@ import com.box.l10n.mojito.entity.TMTextUnitVariant; import com.box.l10n.mojito.entity.TMTextUnitVariantComment; import com.box.l10n.mojito.rest.leveraging.CopyTmConfig.PreserveStatusMode; +import com.box.l10n.mojito.rest.leveraging.CopyTmConfig.TargetStatusFilter; import com.box.l10n.mojito.service.assetExtraction.AssetMappingService; import com.box.l10n.mojito.service.tm.AddTMTextUnitCurrentVariantResult; import com.box.l10n.mojito.service.tm.TMService; +import com.box.l10n.mojito.service.tm.TMTextUnitCurrentVariantRepository; import com.box.l10n.mojito.service.tm.TMTextUnitVariantCommentService; import com.box.l10n.mojito.service.tm.search.TextUnitDTO; import com.box.l10n.mojito.service.tm.search.TextUnitSearcher; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +41,8 @@ public abstract class AbstractLeverager { @Autowired TMTextUnitVariantCommentService tmTextUnitVariantCommentService; + @Autowired TMTextUnitCurrentVariantRepository tmTextUnitCurrentVariantRepository; + /** * Gets {@link TextUnitDTO}s that matches the {@link TMTextUnit} based on different criteria * defined by the implementing class. @@ -83,16 +89,21 @@ public abstract List getLeveragingMatches( * @param assetId */ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, Long assetId) { - performLeveragingFor(tmTextUnits, sourceTmId, assetId, PreserveStatusMode.NONE); + performLeveragingFor(tmTextUnits, sourceTmId, assetId, PreserveStatusMode.NONE, null); } public void performLeveragingFor( List tmTextUnits, Long sourceTmId, Long assetId, - PreserveStatusMode preserveStatusMode) { + PreserveStatusMode preserveStatusMode, + TargetStatusFilter targetStatusFilter) { - logger.debug("Perform leveraging: {}, preserveStatusMode: {}", getType(), preserveStatusMode); + logger.debug( + "Perform leveraging: {}, preserveStatusMode: {}, targetStatusFilter: {}", + getType(), + preserveStatusMode, + targetStatusFilter); for (Iterator tmTextUnitsIterator = tmTextUnits.iterator(); tmTextUnitsIterator.hasNext(); ) { @@ -123,7 +134,11 @@ public void performLeveragingFor( computeTranslationNeeded(preserveStatusMode, uniqueTMTextUnitMatched); addLeveragedTranslations( - tmTextUnit, textUnitDTOsForLeveraging, translationNeeded, uniqueTMTextUnitMatched); + tmTextUnit, + textUnitDTOsForLeveraging, + translationNeeded, + uniqueTMTextUnitMatched, + targetStatusFilter); } else { logger.debug("No Match found for this TMTextUnit with name: {}", tmTextUnit.getName()); } @@ -154,12 +169,25 @@ private void addLeveragedTranslations( TMTextUnit tmTextUnit, List translations, boolean translationNeeded, - boolean uniqueTMTextUnitMatched) { + boolean uniqueTMTextUnitMatched, + TargetStatusFilter targetStatusFilter) { logger.debug("Add leveraged translations in tmTextUnit, id: {}", tmTextUnit.getId()); + Map currentStatusByLocaleId = + buildCurrentStatusByLocaleId(tmTextUnit, targetStatusFilter); + for (TextUnitDTO translation : translations) { + if (!shouldLeverageLocale( + currentStatusByLocaleId, translation.getLocaleId(), targetStatusFilter)) { + logger.debug( + "Skipping locale {} for tmTextUnit {} due to target status filter", + translation.getLocaleId(), + tmTextUnit.getId()); + continue; + } + AddTMTextUnitCurrentVariantResult addTMTextUnitCurrentVariantWithResult = tmService.addTMTextUnitCurrentVariantWithResult( tmTextUnit.getId(), @@ -193,6 +221,36 @@ private void addLeveragedTranslations( } } + private Map buildCurrentStatusByLocaleId( + TMTextUnit tmTextUnit, TargetStatusFilter targetStatusFilter) { + if (targetStatusFilter == null) { + return Map.of(); + } + return tmTextUnitCurrentVariantRepository.findByTmTextUnit_Id(tmTextUnit.getId()).stream() + .collect( + Collectors.toMap( + cv -> cv.getLocale().getId(), + cv -> cv.getTmTextUnitVariant().getStatus(), + (s1, s2) -> s1)); + } + + private boolean shouldLeverageLocale( + Map currentStatusByLocaleId, + Long localeId, + TargetStatusFilter targetStatusFilter) { + if (targetStatusFilter == null) { + return true; + } + TMTextUnitVariant.Status currentStatus = currentStatusByLocaleId.get(localeId); + return switch (targetStatusFilter) { + case UNTRANSLATED -> currentStatus == null; + case APPROVED -> currentStatus == TMTextUnitVariant.Status.APPROVED; + case REVIEW_NEEDED -> currentStatus == TMTextUnitVariant.Status.REVIEW_NEEDED; + case TRANSLATION_NEEDED -> + currentStatus == null || currentStatus == TMTextUnitVariant.Status.TRANSLATION_NEEDED; + }; + } + private String getLeverageComment(TextUnitDTO translation, boolean uniqueTMTextUnitMatched) { return getType() + " - leveraging from tmTextUnitId: " diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java index 1da54d9d42..f72bb4cfe0 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java @@ -19,8 +19,6 @@ import com.box.l10n.mojito.service.tm.search.TextUnitDTO; import com.box.l10n.mojito.service.tm.search.TextUnitSearcher; import com.box.l10n.mojito.service.tm.search.TextUnitSearcherParameters; -import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -150,12 +148,12 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) List textUnitsForCopyTM = getTextUnitsForCopyTM( - targetRepository, - copyTmConfig.getTargetAssetId(), - copyTmConfig.getNameRegex(), - copyTmConfig.getTargetBranchName()); + targetRepository, copyTmConfig.getTargetAssetId(), copyTmConfig.getTargetBranchName()); + + filterTextUnitsByNameRegex(textUnitsForCopyTM, copyTmConfig.getNameRegex()); CopyTmConfig.PreserveStatusMode preserveStatusMode = copyTmConfig.getPreserveStatusMode(); + CopyTmConfig.TargetStatusFilter targetStatusFilter = copyTmConfig.getTargetStatusFilter(); if (CopyTmConfig.Mode.TUIDS.equals(copyTmConfig.getMode())) { copyTranslationBetweenTextUnits(copyTmConfig.getSourceToTargetTmTextUnitIds()); @@ -164,7 +162,8 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatusMode); + preserveStatusMode, + targetStatusFilter); } else if (CopyTmConfig.Mode.NAME.equals(copyTmConfig.getMode())) { logger.debug( "First perform leveraging by name and content (to give priority to strings with same content)"); @@ -172,14 +171,16 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatusMode); + preserveStatusMode, + targetStatusFilter); logger.debug("Now, perform leveraging only on the name"); leveragerByName.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatusMode); + preserveStatusMode, + targetStatusFilter); } else { logger.debug( "First perform leveraging by name and content (to give priority to string with same tags"); @@ -187,14 +188,16 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatusMode); + preserveStatusMode, + targetStatusFilter); logger.debug("Now, perform leveraging only on the content"); leveragerByContent.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), - preserveStatusMode); + preserveStatusMode, + targetStatusFilter); } } @@ -250,39 +253,31 @@ Repository getRepositoryForCopy(Long repositoryId, Long assetId) } List getTextUnitsForCopyTM( - Repository targetRepository, Long targetAssetId, String nameRegex, String branchName) { + Repository targetRepository, Long targetAssetId, String branchName) { logger.debug("Get TmTextUnit that must be processed"); - List tmTextUnits; if (targetAssetId != null) { logger.debug("Process a single asset"); - tmTextUnits = tmTextUnitRepository.findByAssetId(targetAssetId); + return tmTextUnitRepository.findByAssetId(targetAssetId); } else if (branchName != null) { logger.debug("Process a branch"); List tmTextUnitIdsInBranch = assetTextUnitToTMTextUnitRepository.findByBranchName(branchName); - tmTextUnits = tmTextUnitRepository.findByIdIn(tmTextUnitIdsInBranch); + return tmTextUnitRepository.findByIdIn(tmTextUnitIdsInBranch); } else { logger.debug("Process the whole TM"); - tmTextUnits = tmTextUnitRepository.findByTm_id(targetRepository.getTm().getId()); + return tmTextUnitRepository.findByTm_id(targetRepository.getTm().getId()); } - removeTmTextUnitsIfNameMatches(tmTextUnits, nameRegex); - return tmTextUnits; } - void removeTmTextUnitsIfNameMatches(List tmTextUnits, String tmTextUnitNameRegex) { + void filterTextUnitsByNameRegex(List tmTextUnits, String nameRegex) { + if (nameRegex == null) { + return; + } - if (tmTextUnitNameRegex != null) { - final Pattern pattern = Pattern.compile(tmTextUnitNameRegex); + logger.debug("Filtering target text units by name regex: {}", nameRegex); - Iterables.removeIf( - tmTextUnits, - new Predicate() { - @Override - public boolean apply(TMTextUnit tmTextUnit) { - return !pattern.matcher(tmTextUnit.getName()).matches(); - } - }); - } + Pattern pattern = Pattern.compile(nameRegex); + tmTextUnits.removeIf(tu -> !pattern.matcher(tu.getName()).matches()); } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java index 483b5546a0..5e4a6a1816 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java @@ -1,6 +1,8 @@ package com.box.l10n.mojito.service.tm; import com.box.l10n.mojito.entity.TMTextUnitCurrentVariant; +import com.box.l10n.mojito.entity.TMTextUnitVariant; +import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; @@ -29,4 +31,9 @@ public interface TMTextUnitCurrentVariantRepository where ttucv.asset.id = ?1 and ttucv.locale.id = ?2 """) List findByAsset_idAndLocale_Id(Long assetId, Long localeId); + + List findByTmTextUnit_IdIn(Collection tmTextUnitIds); + + List findByTmTextUnit_IdInAndTmTextUnitVariant_Status( + Collection tmTextUnitIds, TMTextUnitVariant.Status status); } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 38b82b6be6..71baacf821 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -735,4 +735,91 @@ public void copyBetweenAssets() Assert.assertEquals(2, targetTranslations2.size()); } + + @Test + public void targetStatusFilterSkipsApprovedLocaleAndLeveragesOthers() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale koKR = localeService.findByBcp47Tag("ko-KR"); + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "ko-KR"); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant(sourceTu.getId(), koKR.getId(), "안녕하세요"); + tmService.addCurrentTMTextUnitVariant(sourceTu.getId(), frFR.getId(), "Bonjour"); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "ko-KR"); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + koKR.getId(), + "existing ko-KR approved", + TMTextUnitVariant.Status.APPROVED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setTargetStatusFilter(CopyTmConfig.TargetStatusFilter.TRANSLATION_NEEDED); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertEquals(2, targetTranslations.size()); + + Map byLocale = new HashMap<>(); + for (TMTextUnitVariant v : targetTranslations) { + byLocale.put(v.getLocale().getBcp47Tag(), v); + } + + Assert.assertEquals( + "ko-KR should keep existing approved translation", + "existing ko-KR approved", + byLocale.get("ko-KR").getContent()); + Assert.assertEquals(TMTextUnitVariant.Status.APPROVED, byLocale.get("ko-KR").getStatus()); + + Assert.assertEquals( + "fr-FR should be leveraged since it had no translation", + "Bonjour", + byLocale.get("fr-FR").getContent()); + } } From 20a7a47e3e30d2343f68a58f7b7ac1610c331cf1 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 26 Mar 2026 02:47:16 +0100 Subject: [PATCH 05/17] Rename preserve status default opt to be less misleading --- .../com/box/l10n/mojito/cli/command/LeveragingCommand.java | 4 ++-- .../java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java | 4 ++-- .../com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java | 4 ++-- .../box/l10n/mojito/service/leveraging/AbstractLeverager.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index 3ea170c2cf..fb6c336a98 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -97,11 +97,11 @@ public class LeveragingCommand extends Command { required = false, description = "Controls whether to keep the source translation's status. " - + "NONE (default): the leverager decides based on match precision. " + + "DEFAULT: the leverager decides based on match precision. " + "UNIQUE: preserve the status only when the match is unique. " + "ANY: always preserve the status, even for non-unique matches.", converter = PreserveStatusModeConverter.class) - CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.NONE; + CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.DEFAULT; @Parameter( names = {"--target-status", "-ts"}, diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index eec6186eff..341b8cae4f 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -21,7 +21,7 @@ public class CopyTmConfig { Mode mode = Mode.MD5; - PreserveStatusMode preserveStatusMode = PreserveStatusMode.NONE; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; TargetStatusFilter targetStatusFilter; @@ -138,7 +138,7 @@ public enum Mode { /** Controls whether the source translation's status is preserved during leveraging */ public enum PreserveStatusMode { /** Default: the leverager decides based on match precision */ - NONE, + DEFAULT, /** Preserve the source status only when the match is unique (single TMTextUnit matched) */ UNIQUE, /** Always preserve the source status, even for non-unique matches */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index 8f95c5c27a..93166ee4a7 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -22,7 +22,7 @@ public class CopyTmConfig { Mode mode = Mode.MD5; - PreserveStatusMode preserveStatusMode = PreserveStatusMode.NONE; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; TargetStatusFilter targetStatusFilter; @@ -139,7 +139,7 @@ public enum Mode { /** Controls whether the source translation's status is preserved during leveraging */ public enum PreserveStatusMode { /** Default: the leverager decides based on match precision */ - NONE, + DEFAULT, /** Preserve the source status only when the match is unique (single TMTextUnit matched) */ UNIQUE, /** Always preserve the source status, even for non-unique matches */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index 485a982017..75ee8ff45d 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -89,7 +89,7 @@ public abstract List getLeveragingMatches( * @param assetId */ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, Long assetId) { - performLeveragingFor(tmTextUnits, sourceTmId, assetId, PreserveStatusMode.NONE, null); + performLeveragingFor(tmTextUnits, sourceTmId, assetId, PreserveStatusMode.DEFAULT, null); } public void performLeveragingFor( @@ -150,7 +150,7 @@ boolean computeTranslationNeeded( return switch (preserveStatusMode) { case ANY -> false; case UNIQUE -> !uniqueTMTextUnitMatched; - case NONE -> isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched; + case DEFAULT -> isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched; }; } From ef4b8298084d58f166c6dccb8211bbb13ee28659 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 26 Mar 2026 02:59:04 +0100 Subject: [PATCH 06/17] Allow specifying multiple statuses --- .../mojito/cli/command/LeveragingCommand.java | 19 +++++++------ .../l10n/mojito/rest/entity/CopyTmConfig.java | 11 ++++---- .../mojito/rest/leveraging/CopyTmConfig.java | 11 ++++---- .../service/leveraging/AbstractLeverager.java | 28 +++++++++++-------- .../service/leveraging/LeveragingService.java | 13 +++++---- .../leveraging/LeveragingServiceTest.java | 3 +- 6 files changed, 47 insertions(+), 38 deletions(-) diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index fb6c336a98..eb6196215d 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -11,6 +11,7 @@ import com.box.l10n.mojito.rest.entity.CopyTmConfig; import com.box.l10n.mojito.rest.entity.PollableTask; import com.box.l10n.mojito.rest.entity.Repository; +import java.util.List; import java.util.Map; import org.fusesource.jansi.Ansi.Color; import org.slf4j.Logger; @@ -105,17 +106,17 @@ public class LeveragingCommand extends Command { @Parameter( names = {"--target-status", "-ts"}, - arity = 1, + variableArity = true, required = false, description = - "Only leverage into target text units whose current translation matches this status. " - + "When not set, all target text units are eligible. " - + "UNTRANSLATED: no translation in any locale. " - + "TRANSLATION_NEEDED: at least one locale has TRANSLATION_NEEDED status. " - + "REVIEW_NEEDED: at least one locale has REVIEW_NEEDED status. " - + "APPROVED: at least one locale has APPROVED status.", + "Only leverage into locales whose current translation matches one of the given statuses. " + + "Multiple values can be space-separated. When not set, all locales are eligible. " + + "UNTRANSLATED: locale has no translation. " + + "TRANSLATION_NEEDED: locale has TRANSLATION_NEEDED status (or no translation). " + + "REVIEW_NEEDED: locale has REVIEW_NEEDED status. " + + "APPROVED: locale has APPROVED status.", converter = TargetStatusFilterConverter.class) - CopyTmConfig.TargetStatusFilter targetStatusFilter; + List targetStatusFilters; @Parameter( names = {"--tuids-mapping"}, @@ -172,7 +173,7 @@ void copyTmBetweenRepositories() throws CommandException { copyTmConfig.setNameRegex(nameRegexParam); copyTmConfig.setTargetBranchName(targetBranchNameParam); copyTmConfig.setPreserveStatusMode(preserveStatusMode); - copyTmConfig.setTargetStatusFilter(targetStatusFilter); + copyTmConfig.setTargetStatusFilters(targetStatusFilters); if (mode != null) { copyTmConfig.setMode(mode); diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index 341b8cae4f..68bdcff3c8 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import java.util.Map; /** @@ -23,7 +24,7 @@ public class CopyTmConfig { PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; - TargetStatusFilter targetStatusFilter; + List targetStatusFilters; PollableTask pollableTask; @@ -113,12 +114,12 @@ public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { this.preserveStatusMode = preserveStatusMode; } - public TargetStatusFilter getTargetStatusFilter() { - return targetStatusFilter; + public List getTargetStatusFilters() { + return targetStatusFilters; } - public void setTargetStatusFilter(TargetStatusFilter targetStatusFilter) { - this.targetStatusFilter = targetStatusFilter; + public void setTargetStatusFilters(List targetStatusFilters) { + this.targetStatusFilters = targetStatusFilters; } /** Matching mode for leveraging */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index 93166ee4a7..f2b342494f 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -3,6 +3,7 @@ import com.box.l10n.mojito.entity.PollableTask; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import java.util.Map; /** @@ -24,7 +25,7 @@ public class CopyTmConfig { PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; - TargetStatusFilter targetStatusFilter; + List targetStatusFilters; PollableTask pollableTask; @@ -116,12 +117,12 @@ public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { this.preserveStatusMode = preserveStatusMode; } - public TargetStatusFilter getTargetStatusFilter() { - return targetStatusFilter; + public List getTargetStatusFilters() { + return targetStatusFilters; } - public void setTargetStatusFilter(TargetStatusFilter targetStatusFilter) { - this.targetStatusFilter = targetStatusFilter; + public void setTargetStatusFilters(List targetStatusFilters) { + this.targetStatusFilters = targetStatusFilters; } /** Matching mode for leveraging */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index 75ee8ff45d..780fb7d2c7 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -97,13 +97,13 @@ public void performLeveragingFor( Long sourceTmId, Long assetId, PreserveStatusMode preserveStatusMode, - TargetStatusFilter targetStatusFilter) { + List targetStatusFilters) { logger.debug( - "Perform leveraging: {}, preserveStatusMode: {}, targetStatusFilter: {}", + "Perform leveraging: {}, preserveStatusMode: {}, targetStatusFilters: {}", getType(), preserveStatusMode, - targetStatusFilter); + targetStatusFilters); for (Iterator tmTextUnitsIterator = tmTextUnits.iterator(); tmTextUnitsIterator.hasNext(); ) { @@ -138,7 +138,7 @@ public void performLeveragingFor( textUnitDTOsForLeveraging, translationNeeded, uniqueTMTextUnitMatched, - targetStatusFilter); + targetStatusFilters); } else { logger.debug("No Match found for this TMTextUnit with name: {}", tmTextUnit.getName()); } @@ -170,17 +170,17 @@ private void addLeveragedTranslations( List translations, boolean translationNeeded, boolean uniqueTMTextUnitMatched, - TargetStatusFilter targetStatusFilter) { + List targetStatusFilters) { logger.debug("Add leveraged translations in tmTextUnit, id: {}", tmTextUnit.getId()); Map currentStatusByLocaleId = - buildCurrentStatusByLocaleId(tmTextUnit, targetStatusFilter); + buildCurrentStatusByLocaleId(tmTextUnit, targetStatusFilters); for (TextUnitDTO translation : translations) { if (!shouldLeverageLocale( - currentStatusByLocaleId, translation.getLocaleId(), targetStatusFilter)) { + currentStatusByLocaleId, translation.getLocaleId(), targetStatusFilters)) { logger.debug( "Skipping locale {} for tmTextUnit {} due to target status filter", translation.getLocaleId(), @@ -222,8 +222,8 @@ private void addLeveragedTranslations( } private Map buildCurrentStatusByLocaleId( - TMTextUnit tmTextUnit, TargetStatusFilter targetStatusFilter) { - if (targetStatusFilter == null) { + TMTextUnit tmTextUnit, List targetStatusFilters) { + if (targetStatusFilters == null || targetStatusFilters.isEmpty()) { return Map.of(); } return tmTextUnitCurrentVariantRepository.findByTmTextUnit_Id(tmTextUnit.getId()).stream() @@ -237,12 +237,16 @@ private Map buildCurrentStatusByLocaleId( private boolean shouldLeverageLocale( Map currentStatusByLocaleId, Long localeId, - TargetStatusFilter targetStatusFilter) { - if (targetStatusFilter == null) { + List targetStatusFilters) { + if (targetStatusFilters == null || targetStatusFilters.isEmpty()) { return true; } TMTextUnitVariant.Status currentStatus = currentStatusByLocaleId.get(localeId); - return switch (targetStatusFilter) { + return targetStatusFilters.stream().anyMatch(f -> matchesFilter(currentStatus, f)); + } + + private boolean matchesFilter(TMTextUnitVariant.Status currentStatus, TargetStatusFilter filter) { + return switch (filter) { case UNTRANSLATED -> currentStatus == null; case APPROVED -> currentStatus == TMTextUnitVariant.Status.APPROVED; case REVIEW_NEEDED -> currentStatus == TMTextUnitVariant.Status.REVIEW_NEEDED; diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java index f72bb4cfe0..45d1a743ac 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java @@ -153,7 +153,8 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) filterTextUnitsByNameRegex(textUnitsForCopyTM, copyTmConfig.getNameRegex()); CopyTmConfig.PreserveStatusMode preserveStatusMode = copyTmConfig.getPreserveStatusMode(); - CopyTmConfig.TargetStatusFilter targetStatusFilter = copyTmConfig.getTargetStatusFilter(); + List targetStatusFilters = + copyTmConfig.getTargetStatusFilters(); if (CopyTmConfig.Mode.TUIDS.equals(copyTmConfig.getMode())) { copyTranslationBetweenTextUnits(copyTmConfig.getSourceToTargetTmTextUnitIds()); @@ -163,7 +164,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilter); + targetStatusFilters); } else if (CopyTmConfig.Mode.NAME.equals(copyTmConfig.getMode())) { logger.debug( "First perform leveraging by name and content (to give priority to strings with same content)"); @@ -172,7 +173,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilter); + targetStatusFilters); logger.debug("Now, perform leveraging only on the name"); leveragerByName.performLeveragingFor( @@ -180,7 +181,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilter); + targetStatusFilters); } else { logger.debug( "First perform leveraging by name and content (to give priority to string with same tags"); @@ -189,7 +190,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilter); + targetStatusFilters); logger.debug("Now, perform leveraging only on the content"); leveragerByContent.performLeveragingFor( @@ -197,7 +198,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilter); + targetStatusFilters); } } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 71baacf821..6f15c79c61 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -795,7 +795,8 @@ public void targetStatusFilterSkipsApprovedLocaleAndLeveragesOthers() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setTargetStatusFilter(CopyTmConfig.TargetStatusFilter.TRANSLATION_NEEDED); + copyTmConfig.setTargetStatusFilters( + List.of(CopyTmConfig.TargetStatusFilter.TRANSLATION_NEEDED)); leveragingService.copyTm(copyTmConfig).get(); From f280efc01e4c4ff2fa43664fc6f1de05cb85d2ac Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 16 Apr 2026 01:34:04 +0200 Subject: [PATCH 07/17] Make status filtering relative --- .../mojito/cli/command/LeveragingCommand.java | 23 +- .../cli/command/OverwriteModeConverter.java | 11 + .../command/TargetStatusFilterConverter.java | 11 - .../l10n/mojito/rest/entity/CopyTmConfig.java | 31 +- .../mojito/rest/leveraging/CopyTmConfig.java | 32 +- .../service/leveraging/AbstractLeverager.java | 55 +- .../service/leveraging/LeveragingService.java | 13 +- .../leveraging/LeveragingServiceTest.java | 564 +++++++++++++++++- 8 files changed, 649 insertions(+), 91 deletions(-) create mode 100644 cli/src/main/java/com/box/l10n/mojito/cli/command/OverwriteModeConverter.java delete mode 100644 cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index eb6196215d..0f15042aa7 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -11,7 +11,6 @@ import com.box.l10n.mojito.rest.entity.CopyTmConfig; import com.box.l10n.mojito.rest.entity.PollableTask; import com.box.l10n.mojito.rest.entity.Repository; -import java.util.List; import java.util.Map; import org.fusesource.jansi.Ansi.Color; import org.slf4j.Logger; @@ -105,18 +104,18 @@ public class LeveragingCommand extends Command { CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.DEFAULT; @Parameter( - names = {"--target-status", "-ts"}, - variableArity = true, + names = {"--overwrite-mode", "-om"}, + arity = 1, required = false, description = - "Only leverage into locales whose current translation matches one of the given statuses. " - + "Multiple values can be space-separated. When not set, all locales are eligible. " - + "UNTRANSLATED: locale has no translation. " - + "TRANSLATION_NEEDED: locale has TRANSLATION_NEEDED status (or no translation). " - + "REVIEW_NEEDED: locale has REVIEW_NEEDED status. " - + "APPROVED: locale has APPROVED status.", - converter = TargetStatusFilterConverter.class) - List targetStatusFilters; + "Controls when existing translations may be overwritten based on status comparison. " + + "UNTRANSLATED_ONLY: only leverage into locales that have no translation. " + + "HIGHER_STATUS: overwrite only when the candidate status is strictly higher " + + "(e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). " + + "HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. " + + "ALL: overwrite regardless of status (default).", + converter = OverwriteModeConverter.class) + CopyTmConfig.OverwriteMode overwriteMode = CopyTmConfig.OverwriteMode.ALL; @Parameter( names = {"--tuids-mapping"}, @@ -173,7 +172,7 @@ void copyTmBetweenRepositories() throws CommandException { copyTmConfig.setNameRegex(nameRegexParam); copyTmConfig.setTargetBranchName(targetBranchNameParam); copyTmConfig.setPreserveStatusMode(preserveStatusMode); - copyTmConfig.setTargetStatusFilters(targetStatusFilters); + copyTmConfig.setOverwriteMode(overwriteMode); if (mode != null) { copyTmConfig.setMode(mode); diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/OverwriteModeConverter.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/OverwriteModeConverter.java new file mode 100644 index 0000000000..f587e8f154 --- /dev/null +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/OverwriteModeConverter.java @@ -0,0 +1,11 @@ +package com.box.l10n.mojito.cli.command; + +import com.box.l10n.mojito.rest.entity.CopyTmConfig; + +public class OverwriteModeConverter extends EnumConverter { + + @Override + protected Class getGenericClass() { + return CopyTmConfig.OverwriteMode.class; + } +} diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java deleted file mode 100644 index 863bfc3664..0000000000 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/TargetStatusFilterConverter.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.box.l10n.mojito.cli.command; - -import com.box.l10n.mojito.rest.entity.CopyTmConfig; - -public class TargetStatusFilterConverter extends EnumConverter { - - @Override - protected Class getGenericClass() { - return CopyTmConfig.TargetStatusFilter.class; - } -} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index 68bdcff3c8..f3d6f05753 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; import java.util.Map; /** @@ -24,7 +23,7 @@ public class CopyTmConfig { PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; - List targetStatusFilters; + OverwriteMode overwriteMode = OverwriteMode.ALL; PollableTask pollableTask; @@ -114,12 +113,12 @@ public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { this.preserveStatusMode = preserveStatusMode; } - public List getTargetStatusFilters() { - return targetStatusFilters; + public OverwriteMode getOverwriteMode() { + return overwriteMode; } - public void setTargetStatusFilters(List targetStatusFilters) { - this.targetStatusFilters = targetStatusFilters; + public void setOverwriteMode(OverwriteMode overwriteMode) { + this.overwriteMode = overwriteMode; } /** Matching mode for leveraging */ @@ -147,15 +146,19 @@ public enum PreserveStatusMode { } /** - * Filters target text units by their current translation status before leveraging. + * Controls when existing translations may be overwritten during leveraging, based on a comparison + * of the candidate's effective status against the target locale's current status. * - *

UNTRANSLATED selects text units with no translation in any locale. The other values select - * text units that have at least one translation with the given status. + *

Status hierarchy (lowest to highest): TRANSLATION_NEEDED < REVIEW_NEEDED < APPROVED. */ - public enum TargetStatusFilter { - UNTRANSLATED, - APPROVED, - REVIEW_NEEDED, - TRANSLATION_NEEDED + public enum OverwriteMode { + /** Only leverage into locales that have no translation at all. */ + UNTRANSLATED_ONLY, + /** Overwrite only when the candidate's effective status is strictly higher. */ + HIGHER_STATUS, + /** Overwrite when the candidate's effective status is higher or equal. */ + HIGHER_OR_EQUAL_STATUS, + /** Always overwrite regardless of the current status. */ + ALL } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index f2b342494f..b0cb016858 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -3,7 +3,6 @@ import com.box.l10n.mojito.entity.PollableTask; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; import java.util.Map; /** @@ -25,7 +24,7 @@ public class CopyTmConfig { PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; - List targetStatusFilters; + OverwriteMode overwriteMode = OverwriteMode.ALL; PollableTask pollableTask; @@ -117,12 +116,12 @@ public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { this.preserveStatusMode = preserveStatusMode; } - public List getTargetStatusFilters() { - return targetStatusFilters; + public OverwriteMode getOverwriteMode() { + return overwriteMode; } - public void setTargetStatusFilters(List targetStatusFilters) { - this.targetStatusFilters = targetStatusFilters; + public void setOverwriteMode(OverwriteMode overwriteMode) { + this.overwriteMode = overwriteMode; } /** Matching mode for leveraging */ @@ -148,16 +147,19 @@ public enum PreserveStatusMode { } /** - * Filters target text units by their current translation status before leveraging. + * Controls when existing translations may be overwritten during leveraging, based on a comparison + * of the candidate's effective status against the target locale's current status. * - *

UNTRANSLATED selects text units with no translation in any locale. The other values - * correspond to {@link com.box.l10n.mojito.entity.TMTextUnitVariant.Status} and select text units - * that have at least one translation with the given status. + *

Status hierarchy (lowest to highest): TRANSLATION_NEEDED < REVIEW_NEEDED < APPROVED. */ - public enum TargetStatusFilter { - UNTRANSLATED, - APPROVED, - REVIEW_NEEDED, - TRANSLATION_NEEDED + public enum OverwriteMode { + /** Only leverage into locales that have no translation at all. */ + UNTRANSLATED_ONLY, + /** Overwrite only when the candidate's effective status is strictly higher. */ + HIGHER_STATUS, + /** Overwrite when the candidate's effective status is higher or equal. */ + HIGHER_OR_EQUAL_STATUS, + /** Always overwrite regardless of the current status. */ + ALL } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index 780fb7d2c7..76f15f2384 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -5,8 +5,8 @@ import com.box.l10n.mojito.entity.TMTextUnitCurrentVariant; import com.box.l10n.mojito.entity.TMTextUnitVariant; import com.box.l10n.mojito.entity.TMTextUnitVariantComment; +import com.box.l10n.mojito.rest.leveraging.CopyTmConfig.OverwriteMode; import com.box.l10n.mojito.rest.leveraging.CopyTmConfig.PreserveStatusMode; -import com.box.l10n.mojito.rest.leveraging.CopyTmConfig.TargetStatusFilter; import com.box.l10n.mojito.service.assetExtraction.AssetMappingService; import com.box.l10n.mojito.service.tm.AddTMTextUnitCurrentVariantResult; import com.box.l10n.mojito.service.tm.TMService; @@ -89,7 +89,8 @@ public abstract List getLeveragingMatches( * @param assetId */ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, Long assetId) { - performLeveragingFor(tmTextUnits, sourceTmId, assetId, PreserveStatusMode.DEFAULT, null); + performLeveragingFor( + tmTextUnits, sourceTmId, assetId, PreserveStatusMode.DEFAULT, OverwriteMode.ALL); } public void performLeveragingFor( @@ -97,13 +98,13 @@ public void performLeveragingFor( Long sourceTmId, Long assetId, PreserveStatusMode preserveStatusMode, - List targetStatusFilters) { + OverwriteMode overwriteMode) { logger.debug( - "Perform leveraging: {}, preserveStatusMode: {}, targetStatusFilters: {}", + "Perform leveraging: {}, preserveStatusMode: {}, overwriteMode: {}", getType(), preserveStatusMode, - targetStatusFilters); + overwriteMode); for (Iterator tmTextUnitsIterator = tmTextUnits.iterator(); tmTextUnitsIterator.hasNext(); ) { @@ -138,7 +139,7 @@ public void performLeveragingFor( textUnitDTOsForLeveraging, translationNeeded, uniqueTMTextUnitMatched, - targetStatusFilters); + overwriteMode); } else { logger.debug("No Match found for this TMTextUnit with name: {}", tmTextUnit.getName()); } @@ -170,19 +171,22 @@ private void addLeveragedTranslations( List translations, boolean translationNeeded, boolean uniqueTMTextUnitMatched, - List targetStatusFilters) { + OverwriteMode overwriteMode) { logger.debug("Add leveraged translations in tmTextUnit, id: {}", tmTextUnit.getId()); Map currentStatusByLocaleId = - buildCurrentStatusByLocaleId(tmTextUnit, targetStatusFilters); + buildCurrentStatusByLocaleId(tmTextUnit, overwriteMode); for (TextUnitDTO translation : translations) { + TMTextUnitVariant.Status effectiveStatus = + translationNeeded ? TMTextUnitVariant.Status.TRANSLATION_NEEDED : translation.getStatus(); + if (!shouldLeverageLocale( - currentStatusByLocaleId, translation.getLocaleId(), targetStatusFilters)) { + currentStatusByLocaleId, translation.getLocaleId(), effectiveStatus, overwriteMode)) { logger.debug( - "Skipping locale {} for tmTextUnit {} due to target status filter", + "Skipping locale {} for tmTextUnit {} due to status overwrite mode", translation.getLocaleId(), tmTextUnit.getId()); continue; @@ -194,9 +198,7 @@ private void addLeveragedTranslations( translation.getLocaleId(), translation.getTarget(), translation.getTargetComment(), - translationNeeded - ? TMTextUnitVariant.Status.TRANSLATION_NEEDED - : translation.getStatus(), + effectiveStatus, translation.isIncludedInLocalizedFile(), null); @@ -222,8 +224,8 @@ private void addLeveragedTranslations( } private Map buildCurrentStatusByLocaleId( - TMTextUnit tmTextUnit, List targetStatusFilters) { - if (targetStatusFilters == null || targetStatusFilters.isEmpty()) { + TMTextUnit tmTextUnit, OverwriteMode overwriteMode) { + if (overwriteMode == OverwriteMode.ALL) { return Map.of(); } return tmTextUnitCurrentVariantRepository.findByTmTextUnit_Id(tmTextUnit.getId()).stream() @@ -237,21 +239,16 @@ private Map buildCurrentStatusByLocaleId( private boolean shouldLeverageLocale( Map currentStatusByLocaleId, Long localeId, - List targetStatusFilters) { - if (targetStatusFilters == null || targetStatusFilters.isEmpty()) { - return true; - } + TMTextUnitVariant.Status effectiveStatus, + OverwriteMode overwriteMode) { TMTextUnitVariant.Status currentStatus = currentStatusByLocaleId.get(localeId); - return targetStatusFilters.stream().anyMatch(f -> matchesFilter(currentStatus, f)); - } - - private boolean matchesFilter(TMTextUnitVariant.Status currentStatus, TargetStatusFilter filter) { - return switch (filter) { - case UNTRANSLATED -> currentStatus == null; - case APPROVED -> currentStatus == TMTextUnitVariant.Status.APPROVED; - case REVIEW_NEEDED -> currentStatus == TMTextUnitVariant.Status.REVIEW_NEEDED; - case TRANSLATION_NEEDED -> - currentStatus == null || currentStatus == TMTextUnitVariant.Status.TRANSLATION_NEEDED; + return switch (overwriteMode) { + case UNTRANSLATED_ONLY -> currentStatus == null; + case HIGHER_STATUS -> + currentStatus == null || effectiveStatus.ordinal() > currentStatus.ordinal(); + case HIGHER_OR_EQUAL_STATUS -> + currentStatus == null || effectiveStatus.ordinal() >= currentStatus.ordinal(); + case ALL -> true; }; } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java index 45d1a743ac..2917a24c15 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java @@ -153,8 +153,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) filterTextUnitsByNameRegex(textUnitsForCopyTM, copyTmConfig.getNameRegex()); CopyTmConfig.PreserveStatusMode preserveStatusMode = copyTmConfig.getPreserveStatusMode(); - List targetStatusFilters = - copyTmConfig.getTargetStatusFilters(); + CopyTmConfig.OverwriteMode overwriteMode = copyTmConfig.getOverwriteMode(); if (CopyTmConfig.Mode.TUIDS.equals(copyTmConfig.getMode())) { copyTranslationBetweenTextUnits(copyTmConfig.getSourceToTargetTmTextUnitIds()); @@ -164,7 +163,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilters); + overwriteMode); } else if (CopyTmConfig.Mode.NAME.equals(copyTmConfig.getMode())) { logger.debug( "First perform leveraging by name and content (to give priority to strings with same content)"); @@ -173,7 +172,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilters); + overwriteMode); logger.debug("Now, perform leveraging only on the name"); leveragerByName.performLeveragingFor( @@ -181,7 +180,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilters); + overwriteMode); } else { logger.debug( "First perform leveraging by name and content (to give priority to string with same tags"); @@ -190,7 +189,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilters); + overwriteMode); logger.debug("Now, perform leveraging only on the content"); leveragerByContent.performLeveragingFor( @@ -198,7 +197,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId(), preserveStatusMode, - targetStatusFilters); + overwriteMode); } } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 6f15c79c61..9cdbab6673 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -5,6 +5,7 @@ import com.box.l10n.mojito.entity.Repository; import com.box.l10n.mojito.entity.TM; import com.box.l10n.mojito.entity.TMTextUnit; +import com.box.l10n.mojito.entity.TMTextUnitCurrentVariant; import com.box.l10n.mojito.entity.TMTextUnitVariant; import com.box.l10n.mojito.rest.asset.AssetWithIdNotFoundException; import com.box.l10n.mojito.rest.leveraging.CopyTmConfig; @@ -17,6 +18,7 @@ import com.box.l10n.mojito.service.repository.RepositoryService; import com.box.l10n.mojito.service.tm.TMService; import com.box.l10n.mojito.service.tm.TMTestData; +import com.box.l10n.mojito.service.tm.TMTextUnitCurrentVariantRepository; import com.box.l10n.mojito.service.tm.TMTextUnitVariantRepository; import com.box.l10n.mojito.test.TestIdWatcher; import com.google.common.base.Predicate; @@ -49,6 +51,8 @@ public class LeveragingServiceTest extends ServiceTestBase { @Autowired TMTextUnitVariantRepository tmTextUnitVariantRepository; + @Autowired TMTextUnitCurrentVariantRepository tmTextUnitCurrentVariantRepository; + @Autowired AssetService assetService; @Autowired TMService tmService; @@ -737,7 +741,7 @@ public void copyBetweenAssets() } @Test - public void targetStatusFilterSkipsApprovedLocaleAndLeveragesOthers() + public void untranslatedOnlyModeSkipsApprovedLocaleAndLeveragesOthers() throws InterruptedException, ExecutionException, RepositoryNameAlreadyUsedException, @@ -795,8 +799,7 @@ public void targetStatusFilterSkipsApprovedLocaleAndLeveragesOthers() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setTargetStatusFilters( - List.of(CopyTmConfig.TargetStatusFilter.TRANSLATION_NEEDED)); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.UNTRANSLATED_ONLY); leveragingService.copyTm(copyTmConfig).get(); @@ -823,4 +826,559 @@ public void targetStatusFilterSkipsApprovedLocaleAndLeveragesOthers() "Bonjour", byLocale.get("fr-FR").getContent()); } + + @Test + public void untranslatedOnlyModeSkipsTranslationNeededLocale() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), frFR.getId(), "Bonjour", TMTextUnitVariant.Status.APPROVED, true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + frFR.getId(), + "existing translation needed", + TMTextUnitVariant.Status.TRANSLATION_NEEDED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.UNTRANSLATED_ONLY); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertEquals(1, targetTranslations.size()); + Assert.assertEquals( + "Should keep existing TRANSLATION_NEEDED translation", + "existing translation needed", + targetTranslations.get(0).getContent()); + Assert.assertEquals( + TMTextUnitVariant.Status.TRANSLATION_NEEDED, targetTranslations.get(0).getStatus()); + } + + @Test + public void higherStatusModeOverwritesLowerStatus() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + Locale koKR = localeService.findByBcp47Tag("ko-KR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + repositoryService.addRepositoryLocale(sourceRepository, "ko-KR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), + frFR.getId(), + "Bonjour approved", + TMTextUnitVariant.Status.APPROVED, + true); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), koKR.getId(), "안녕 approved", TMTextUnitVariant.Status.APPROVED, true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + repositoryService.addRepositoryLocale(targetRepository, "ko-KR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + frFR.getId(), + "existing translation needed", + TMTextUnitVariant.Status.TRANSLATION_NEEDED, + true); + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + koKR.getId(), + "existing approved ko", + TMTextUnitVariant.Status.APPROVED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); + + leveragingService.copyTm(copyTmConfig).get(); + + TMTextUnitCurrentVariant frCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + frFR.getId(), targetTu.getId()); + TMTextUnitCurrentVariant koCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + koKR.getId(), targetTu.getId()); + + Assert.assertEquals( + "fr-FR should be overwritten because APPROVED > TRANSLATION_NEEDED", + "Bonjour approved", + frCV.getTmTextUnitVariant().getContent()); + + Assert.assertEquals( + "ko-KR should NOT be overwritten because APPROVED is not > APPROVED", + "existing approved ko", + koCV.getTmTextUnitVariant().getContent()); + } + + @Test + public void higherStatusModeSkipsWhenCandidateStatusIsLower() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), + frFR.getId(), + "Bonjour review", + TMTextUnitVariant.Status.REVIEW_NEEDED, + true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + frFR.getId(), + "existing approved", + TMTextUnitVariant.Status.APPROVED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertEquals(1, targetTranslations.size()); + Assert.assertEquals( + "Should keep existing APPROVED because REVIEW_NEEDED is not higher", + "existing approved", + targetTranslations.get(0).getContent()); + Assert.assertEquals(TMTextUnitVariant.Status.APPROVED, targetTranslations.get(0).getStatus()); + } + + @Test + public void higherOrEqualStatusModeOverwritesEqualStatus() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + Locale koKR = localeService.findByBcp47Tag("ko-KR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + repositoryService.addRepositoryLocale(sourceRepository, "ko-KR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), + frFR.getId(), + "Bonjour review", + TMTextUnitVariant.Status.REVIEW_NEEDED, + true); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), koKR.getId(), "안녕 review", TMTextUnitVariant.Status.REVIEW_NEEDED, true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + repositoryService.addRepositoryLocale(targetRepository, "ko-KR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + frFR.getId(), + "existing review fr", + TMTextUnitVariant.Status.REVIEW_NEEDED, + true); + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + koKR.getId(), + "existing approved ko", + TMTextUnitVariant.Status.APPROVED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_OR_EQUAL_STATUS); + + leveragingService.copyTm(copyTmConfig).get(); + + TMTextUnitCurrentVariant frCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + frFR.getId(), targetTu.getId()); + TMTextUnitCurrentVariant koCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + koKR.getId(), targetTu.getId()); + + Assert.assertEquals( + "fr-FR should be overwritten because REVIEW_NEEDED == REVIEW_NEEDED", + "Bonjour review", + frCV.getTmTextUnitVariant().getContent()); + + Assert.assertEquals( + "ko-KR should NOT be overwritten because REVIEW_NEEDED < APPROVED", + "existing approved ko", + koCV.getTmTextUnitVariant().getContent()); + } + + @Test + public void allModeOverwritesEverything() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), + frFR.getId(), + "Bonjour translation needed", + TMTextUnitVariant.Status.TRANSLATION_NEEDED, + true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + frFR.getId(), + "existing approved", + TMTextUnitVariant.Status.APPROVED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.ALL); + + leveragingService.copyTm(copyTmConfig).get(); + + TMTextUnitCurrentVariant frCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + frFR.getId(), targetTu.getId()); + + Assert.assertNotNull("fr-FR should have a current variant", frCV); + Assert.assertEquals( + "Should overwrite even though TRANSLATION_NEEDED < APPROVED", + "Bonjour translation needed", + frCV.getTmTextUnitVariant().getContent()); + } + + @Test + public void higherStatusModeUsesEffectiveStatusNotCandidateStatus() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), + frFR.getId(), + "Bonjour approved", + TMTextUnitVariant.Status.APPROVED, + true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + frFR.getId(), + "existing review needed", + TMTextUnitVariant.Status.REVIEW_NEEDED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.NAME); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.DEFAULT); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertEquals(1, targetTranslations.size()); + Assert.assertEquals( + "Should NOT overwrite: candidate is APPROVED but effective status is TRANSLATION_NEEDED " + + "(downgraded by NAME leveraging default), which is lower than existing REVIEW_NEEDED", + "existing review needed", + targetTranslations.get(0).getContent()); + Assert.assertEquals( + TMTextUnitVariant.Status.REVIEW_NEEDED, targetTranslations.get(0).getStatus()); + } + + @Test + public void higherStatusModeAllowsLeveragingIntoUntranslatedLocale() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), frFR.getId(), "Bonjour", TMTextUnitVariant.Status.APPROVED, true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + tmService.addTMTextUnit( + targetRepository.getTm().getId(), targetAsset.getId(), "greeting", "Hello", "A greeting"); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + Assert.assertEquals(1, targetTranslations.size()); + Assert.assertEquals("Bonjour", targetTranslations.get(0).getContent()); + Assert.assertEquals(TMTextUnitVariant.Status.APPROVED, targetTranslations.get(0).getStatus()); + } } From bc2949c08b6a2335724eb15052a0596f7c68fa4f Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 16 Apr 2026 01:51:29 +0200 Subject: [PATCH 08/17] Clarify preserve status modes --- .../mojito/cli/command/LeveragingCommand.java | 17 +++++---- .../cli/command/LeveragingCommandTest.java | 2 +- .../l10n/mojito/rest/entity/CopyTmConfig.java | 36 ++++++++++++------- .../mojito/rest/leveraging/CopyTmConfig.java | 26 ++++++++++---- .../service/leveraging/AbstractLeverager.java | 6 ++-- .../leveraging/LeveragingServiceTest.java | 20 +++++------ 6 files changed, 66 insertions(+), 41 deletions(-) diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index 0f15042aa7..d160643324 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -96,12 +96,15 @@ public class LeveragingCommand extends Command { arity = 1, required = false, description = - "Controls whether to keep the source translation's status. " - + "DEFAULT: the leverager decides based on match precision. " - + "UNIQUE: preserve the status only when the match is unique. " - + "ANY: always preserve the status, even for non-unique matches.", + "Controls whether to keep the leveraged translation's status or downgrade it to TRANSLATION_NEEDED. " + + "PRECISION (default): preserve status based on the match precision (ID, content)." + + " Lowest risk of carrying over incorrect statuses. " + + "UNIQUE: preserve status when the match is unambiguous (single source text unit matched)." + + " Medium risk — trusts all unique matches regardless of their precition. " + + "ALL: always preserve the source status. Highest risk — ambiguous matches may " + + "copy an arbitrarily chosen translation at its original (possibly APPROVED) status.", converter = PreserveStatusModeConverter.class) - CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.DEFAULT; + CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.PRECISION; @Parameter( names = {"--overwrite-mode", "-om"}, @@ -109,11 +112,11 @@ public class LeveragingCommand extends Command { required = false, description = "Controls when existing translations may be overwritten based on status comparison. " + + "ALL (default): overwrite regardless of status." + "UNTRANSLATED_ONLY: only leverage into locales that have no translation. " + "HIGHER_STATUS: overwrite only when the candidate status is strictly higher " + "(e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). " - + "HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. " - + "ALL: overwrite regardless of status (default).", + + "HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. ", converter = OverwriteModeConverter.class) CopyTmConfig.OverwriteMode overwriteMode = CopyTmConfig.OverwriteMode.ALL; diff --git a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java index ca91d2a263..3b2a6f8dcc 100644 --- a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java +++ b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java @@ -332,7 +332,7 @@ public void copyTMModeNamePreserveStatus() throws Exception { "-m", "NAME", "--preserve-status", - "ANY"); + "ALL"); List targetTranslations = tmTextUnitVariantRepository diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index f3d6f05753..98b636c32b 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -21,7 +21,7 @@ public class CopyTmConfig { Mode mode = Mode.MD5; - PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.PRECISION; OverwriteMode overwriteMode = OverwriteMode.ALL; @@ -135,30 +135,40 @@ public enum Mode { TUIDS } - /** Controls whether the source translation's status is preserved during leveraging */ + /** + * Controls whether to keep the leveraged translation's status or downgrade it to + * TRANSLATION_NEEDED. + */ public enum PreserveStatusMode { - /** Default: the leverager decides based on match precision */ - DEFAULT, - /** Preserve the source status only when the match is unique (single TMTextUnit matched) */ + /** + * Preserve status based on the match precision (ID, content). Lowest risk of carrying over + * incorrect statuses. + */ + PRECISION, + /** + * Preserve status when the match is unambiguous (single source text unit matched). Medium risk + * — trusts all unique matches regardless of their precition. + */ UNIQUE, - /** Always preserve the source status, even for non-unique matches */ - ANY + /** + * Always preserve the source status. Highest risk — ambiguous matches may copy an arbitrarily + * chosen translation at its original (possibly APPROVED) status. + */ + ALL } /** * Controls when existing translations may be overwritten during leveraging, based on a comparison * of the candidate's effective status against the target locale's current status. - * - *

Status hierarchy (lowest to highest): TRANSLATION_NEEDED < REVIEW_NEEDED < APPROVED. */ public enum OverwriteMode { - /** Only leverage into locales that have no translation at all. */ + /** Overwrite regardless of the current status. */ + ALL, + /** Only leverage into locales that have no translation. */ UNTRANSLATED_ONLY, /** Overwrite only when the candidate's effective status is strictly higher. */ HIGHER_STATUS, /** Overwrite when the candidate's effective status is higher or equal. */ - HIGHER_OR_EQUAL_STATUS, - /** Always overwrite regardless of the current status. */ - ALL + HIGHER_OR_EQUAL_STATUS } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index b0cb016858..850bb67e68 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -22,7 +22,7 @@ public class CopyTmConfig { Mode mode = Mode.MD5; - PreserveStatusMode preserveStatusMode = PreserveStatusMode.DEFAULT; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.PRECISION; OverwriteMode overwriteMode = OverwriteMode.ALL; @@ -136,14 +136,26 @@ public enum Mode { TUIDS } - /** Controls whether the source translation's status is preserved during leveraging */ + /** + * Controls whether to keep the leveraged translation's status or downgrade it to + * TRANSLATION_NEEDED. + */ public enum PreserveStatusMode { - /** Default: the leverager decides based on match precision */ - DEFAULT, - /** Preserve the source status only when the match is unique (single TMTextUnit matched) */ + /** + * Preserve status based on the match precision (ID, content). Lowest risk of carrying over + * incorrect statuses. + */ + PRECISION, + /** + * Preserve status when the match is unambiguous (single source text unit matched). Medium risk + * — trusts all unique matches regardless of their precition. + */ UNIQUE, - /** Always preserve the source status, even for non-unique matches */ - ANY + /** + * Always preserve the source status. Highest risk — ambiguous matches may copy an arbitrarily + * chosen translation at its original (possibly APPROVED) status. + */ + ALL } /** diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index 76f15f2384..0aec46fdcf 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -90,7 +90,7 @@ public abstract List getLeveragingMatches( */ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, Long assetId) { performLeveragingFor( - tmTextUnits, sourceTmId, assetId, PreserveStatusMode.DEFAULT, OverwriteMode.ALL); + tmTextUnits, sourceTmId, assetId, PreserveStatusMode.PRECISION, OverwriteMode.ALL); } public void performLeveragingFor( @@ -149,9 +149,9 @@ public void performLeveragingFor( boolean computeTranslationNeeded( PreserveStatusMode preserveStatusMode, boolean uniqueTMTextUnitMatched) { return switch (preserveStatusMode) { - case ANY -> false; + case ALL -> false; case UNIQUE -> !uniqueTMTextUnitMatched; - case DEFAULT -> isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched; + case PRECISION -> isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched; }; } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 9cdbab6673..6f1f91bdef 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -365,7 +365,7 @@ public void copyWithNameMatchPreservesStatusForAnyMatch() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.NAME); - copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ALL); leveragingService.copyTm(copyTmConfig).get(); @@ -377,7 +377,7 @@ public void copyWithNameMatchPreservesStatusForAnyMatch() Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); for (TMTextUnitVariant variant : targetTranslations) { Assert.assertEquals( - "Status should be preserved as APPROVED with ANY", + "Status should be preserved as APPROVED with ALL", TMTextUnitVariant.Status.APPROVED, variant.getStatus()); } @@ -429,7 +429,7 @@ public void copyWithNameMatchPreservesStatusForUniqueMatchOnly() Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); for (TMTextUnitVariant variant : targetTranslations) { Assert.assertEquals( - "Status should be preserved as APPROVED for unique match with UNIQUE mode", + "Status should be preserved as APPROVED for unambiguous match with UNIQUE mode", TMTextUnitVariant.Status.APPROVED, variant.getStatus()); } @@ -480,7 +480,7 @@ public void copyWithNameMatchSetsTranslationNeededWithoutPreserveStatus() Assert.assertFalse("Should have copied translations", targetTranslations.isEmpty()); for (TMTextUnitVariant variant : targetTranslations) { Assert.assertEquals( - "Status should be TRANSLATION_NEEDED with default NONE mode for NAME leveraging", + "Status should be TRANSLATION_NEEDED with PRECISION mode for NAME leveraging", TMTextUnitVariant.Status.TRANSLATION_NEEDED, variant.getStatus()); } @@ -972,7 +972,7 @@ public void higherStatusModeOverwritesLowerStatus() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ALL); copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); leveragingService.copyTm(copyTmConfig).get(); @@ -1055,7 +1055,7 @@ public void higherStatusModeSkipsWhenCandidateStatusIsLower() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ALL); copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); leveragingService.copyTm(copyTmConfig).get(); @@ -1144,7 +1144,7 @@ public void higherOrEqualStatusModeOverwritesEqualStatus() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ALL); copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_OR_EQUAL_STATUS); leveragingService.copyTm(copyTmConfig).get(); @@ -1227,7 +1227,7 @@ public void allModeOverwritesEverything() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ALL); copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.ALL); leveragingService.copyTm(copyTmConfig).get(); @@ -1303,7 +1303,7 @@ public void higherStatusModeUsesEffectiveStatusNotCandidateStatus() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.NAME); - copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.DEFAULT); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.PRECISION); copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); leveragingService.copyTm(copyTmConfig).get(); @@ -1367,7 +1367,7 @@ public void higherStatusModeAllowsLeveragingIntoUntranslatedLocale() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ANY); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ALL); copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); leveragingService.copyTm(copyTmConfig).get(); From d3d24a80d9d6e29cd94f283de17291a08b8bbac2 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 16 Apr 2026 11:42:20 +0200 Subject: [PATCH 09/17] Use leveraging candidate original status during comparison --- .../l10n/mojito/rest/entity/CopyTmConfig.java | 6 +++--- .../mojito/rest/leveraging/CopyTmConfig.java | 6 +++--- .../service/leveraging/AbstractLeverager.java | 19 +++++++++-------- .../leveraging/LeveragingServiceTest.java | 21 +++++++++++-------- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index 98b636c32b..44de06369e 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -159,16 +159,16 @@ public enum PreserveStatusMode { /** * Controls when existing translations may be overwritten during leveraging, based on a comparison - * of the candidate's effective status against the target locale's current status. + * of the candidate's original status against the target locale's current status. */ public enum OverwriteMode { /** Overwrite regardless of the current status. */ ALL, /** Only leverage into locales that have no translation. */ UNTRANSLATED_ONLY, - /** Overwrite only when the candidate's effective status is strictly higher. */ + /** Overwrite only when the candidate's original status is strictly higher. */ HIGHER_STATUS, - /** Overwrite when the candidate's effective status is higher or equal. */ + /** Overwrite when the candidate's original status is higher or equal. */ HIGHER_OR_EQUAL_STATUS } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index 850bb67e68..be030bc43a 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -160,16 +160,16 @@ public enum PreserveStatusMode { /** * Controls when existing translations may be overwritten during leveraging, based on a comparison - * of the candidate's effective status against the target locale's current status. + * of the candidate's original status against the target locale's current status. * *

Status hierarchy (lowest to highest): TRANSLATION_NEEDED < REVIEW_NEEDED < APPROVED. */ public enum OverwriteMode { /** Only leverage into locales that have no translation at all. */ UNTRANSLATED_ONLY, - /** Overwrite only when the candidate's effective status is strictly higher. */ + /** Overwrite only when the candidate's original status is strictly higher. */ HIGHER_STATUS, - /** Overwrite when the candidate's effective status is higher or equal. */ + /** Overwrite when the candidate's original status is higher or equal. */ HIGHER_OR_EQUAL_STATUS, /** Always overwrite regardless of the current status. */ ALL diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index 0aec46fdcf..b32fa22a57 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -179,12 +179,11 @@ private void addLeveragedTranslations( buildCurrentStatusByLocaleId(tmTextUnit, overwriteMode); for (TextUnitDTO translation : translations) { - - TMTextUnitVariant.Status effectiveStatus = - translationNeeded ? TMTextUnitVariant.Status.TRANSLATION_NEEDED : translation.getStatus(); - if (!shouldLeverageLocale( - currentStatusByLocaleId, translation.getLocaleId(), effectiveStatus, overwriteMode)) { + currentStatusByLocaleId, + translation.getLocaleId(), + translation.getStatus(), + overwriteMode)) { logger.debug( "Skipping locale {} for tmTextUnit {} due to status overwrite mode", translation.getLocaleId(), @@ -198,7 +197,9 @@ private void addLeveragedTranslations( translation.getLocaleId(), translation.getTarget(), translation.getTargetComment(), - effectiveStatus, + translationNeeded + ? TMTextUnitVariant.Status.TRANSLATION_NEEDED + : translation.getStatus(), translation.isIncludedInLocalizedFile(), null); @@ -239,15 +240,15 @@ private Map buildCurrentStatusByLocaleId( private boolean shouldLeverageLocale( Map currentStatusByLocaleId, Long localeId, - TMTextUnitVariant.Status effectiveStatus, + TMTextUnitVariant.Status candidateStatus, OverwriteMode overwriteMode) { TMTextUnitVariant.Status currentStatus = currentStatusByLocaleId.get(localeId); return switch (overwriteMode) { case UNTRANSLATED_ONLY -> currentStatus == null; case HIGHER_STATUS -> - currentStatus == null || effectiveStatus.ordinal() > currentStatus.ordinal(); + currentStatus == null || candidateStatus.ordinal() > currentStatus.ordinal(); case HIGHER_OR_EQUAL_STATUS -> - currentStatus == null || effectiveStatus.ordinal() >= currentStatus.ordinal(); + currentStatus == null || candidateStatus.ordinal() >= currentStatus.ordinal(); case ALL -> true; }; } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 6f1f91bdef..2c327b615d 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -1244,7 +1244,7 @@ public void allModeOverwritesEverything() } @Test - public void higherStatusModeUsesEffectiveStatusNotCandidateStatus() + public void higherStatusModeUsesCandidateStatusNotEffectiveStatus() throws InterruptedException, ExecutionException, RepositoryNameAlreadyUsedException, @@ -1313,14 +1313,17 @@ public void higherStatusModeUsesEffectiveStatusNotCandidateStatus() .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( targetRepository, "en"); - Assert.assertEquals(1, targetTranslations.size()); - Assert.assertEquals( - "Should NOT overwrite: candidate is APPROVED but effective status is TRANSLATION_NEEDED " - + "(downgraded by NAME leveraging default), which is lower than existing REVIEW_NEEDED", - "existing review needed", - targetTranslations.get(0).getContent()); - Assert.assertEquals( - TMTextUnitVariant.Status.REVIEW_NEEDED, targetTranslations.get(0).getStatus()); + Assert.assertEquals(2, targetTranslations.size()); + TMTextUnitVariant leveraged = + targetTranslations.stream() + .filter(v -> v.getContent().equals("Bonjour approved")) + .findFirst() + .orElse(null); + Assert.assertNotNull( + "Should overwrite: candidate's original status is APPROVED which is higher than " + + "existing REVIEW_NEEDED (effective status downgrade does not affect the decision)", + leveraged); + Assert.assertEquals(TMTextUnitVariant.Status.TRANSLATION_NEEDED, leveraged.getStatus()); } @Test From cfed09e92729f8640f80f54cc00ef5ed4b202543 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 16 Apr 2026 11:47:16 +0200 Subject: [PATCH 10/17] Fix dead code in leveraging mode selection --- .../l10n/mojito/service/leveraging/LeveragingService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java index 2917a24c15..839a1af2dd 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/LeveragingService.java @@ -155,9 +155,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) CopyTmConfig.PreserveStatusMode preserveStatusMode = copyTmConfig.getPreserveStatusMode(); CopyTmConfig.OverwriteMode overwriteMode = copyTmConfig.getOverwriteMode(); - if (CopyTmConfig.Mode.TUIDS.equals(copyTmConfig.getMode())) { - copyTranslationBetweenTextUnits(copyTmConfig.getSourceToTargetTmTextUnitIds()); - } else if (CopyTmConfig.Mode.MD5.equals(copyTmConfig.getMode())) { + if (CopyTmConfig.Mode.MD5.equals(copyTmConfig.getMode())) { leveragerByMd5.performLeveragingFor( textUnitsForCopyTM, sourceRepository.getTm().getId(), @@ -181,7 +179,7 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) copyTmConfig.getSourceAssetId(), preserveStatusMode, overwriteMode); - } else { + } else if (CopyTmConfig.Mode.EXACT.equals(copyTmConfig.getMode())) { logger.debug( "First perform leveraging by name and content (to give priority to string with same tags"); leveragerByNameAndContent.performLeveragingFor( @@ -198,6 +196,8 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) copyTmConfig.getSourceAssetId(), preserveStatusMode, overwriteMode); + } else { + throw new UnsupportedOperationException("Unexpected mode " + copyTmConfig.getMode()); } } From 4eab219fae6f8cb6a917ed064f3b905bf12526c7 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 16 Apr 2026 11:49:44 +0200 Subject: [PATCH 11/17] Remove leftover unused methods --- .../service/tm/TMTextUnitCurrentVariantRepository.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java index 5e4a6a1816..483b5546a0 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMTextUnitCurrentVariantRepository.java @@ -1,8 +1,6 @@ package com.box.l10n.mojito.service.tm; import com.box.l10n.mojito.entity.TMTextUnitCurrentVariant; -import com.box.l10n.mojito.entity.TMTextUnitVariant; -import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; @@ -31,9 +29,4 @@ public interface TMTextUnitCurrentVariantRepository where ttucv.asset.id = ?1 and ttucv.locale.id = ?2 """) List findByAsset_idAndLocale_Id(Long assetId, Long localeId); - - List findByTmTextUnit_IdIn(Collection tmTextUnitIds); - - List findByTmTextUnit_IdInAndTmTextUnitVariant_Status( - Collection tmTextUnitIds, TMTextUnitVariant.Status status); } From 7fb190242c35eac66a9fc1b30964711457912ea1 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 16 Apr 2026 11:55:53 +0200 Subject: [PATCH 12/17] Typo and documentation fixes --- .../mojito/cli/command/LeveragingCommand.java | 4 ++-- .../cli/command/LeveragingCommandTest.java | 24 +++++++++---------- .../l10n/mojito/rest/entity/CopyTmConfig.java | 2 +- .../mojito/rest/leveraging/CopyTmConfig.java | 12 ++++------ 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index d160643324..74749fb2e9 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -100,7 +100,7 @@ public class LeveragingCommand extends Command { + "PRECISION (default): preserve status based on the match precision (ID, content)." + " Lowest risk of carrying over incorrect statuses. " + "UNIQUE: preserve status when the match is unambiguous (single source text unit matched)." - + " Medium risk — trusts all unique matches regardless of their precition. " + + " Medium risk — trusts all unique matches regardless of their precision. " + "ALL: always preserve the source status. Highest risk — ambiguous matches may " + "copy an arbitrarily chosen translation at its original (possibly APPROVED) status.", converter = PreserveStatusModeConverter.class) @@ -112,7 +112,7 @@ public class LeveragingCommand extends Command { required = false, description = "Controls when existing translations may be overwritten based on status comparison. " - + "ALL (default): overwrite regardless of status." + + "ALL (default): overwrite regardless of status. " + "UNTRANSLATED_ONLY: only leverage into locales that have no translation. " + "HIGHER_STATUS: overwrite only when the candidate status is strictly higher " + "(e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). " diff --git a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java index 3b2a6f8dcc..68cde15179 100644 --- a/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java +++ b/cli/src/test/java/com/box/l10n/mojito/cli/command/LeveragingCommandTest.java @@ -44,7 +44,7 @@ public void copyTMModeMD5() throws Exception { Repository sourceRepository = createTestRepoUsingRepoService(); Repository targetRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); repositoryService.addRepositoryLocale(targetRepository, "fr-CA", "fr-FR", false); @@ -108,9 +108,9 @@ public void copyTMModeMD5() throws Exception { public void copyTMModeExact() throws Exception { Repository sourceRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("source-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("source-repository")); Repository targetRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); repositoryService.addRepositoryLocale(sourceRepository, "fr-CA", "fr-FR", false); @@ -201,9 +201,9 @@ public void copyTMModeExact() throws Exception { public void copyTMModeName() throws Exception { Repository sourceRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("source-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("source-repository")); Repository targetRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); repositoryService.addRepositoryLocale(sourceRepository, "fr-CA", "fr-FR", false); @@ -289,9 +289,9 @@ public void copyTMModeName() throws Exception { public void copyTMModeNamePreserveStatus() throws Exception { Repository sourceRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("source-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("source-repository")); Repository targetRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); repositoryService.addRepositoryLocale(sourceRepository, "fr-CA", "fr-FR", false); @@ -349,7 +349,7 @@ public void copyTMModeNamePreserveStatus() throws Exception { for (TMTextUnitVariant variant : targetTranslations) { Assert.assertEquals( - "Status should be preserved as APPROVED with ANY", + "Status should be preserved as APPROVED with ALL", TMTextUnitVariant.Status.APPROVED, variant.getStatus()); } @@ -359,9 +359,9 @@ public void copyTMModeNamePreserveStatus() throws Exception { public void copyTMModeTUIDs() throws Exception { Repository sourceRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("source-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("source-repository")); Repository targetRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); repositoryService.addRepositoryLocale(sourceRepository, "fr-CA", "fr-FR", false); @@ -456,9 +456,9 @@ public void copyTMModeTUIDs() throws Exception { public void copyTMModeTargetBranchName() throws Exception { Repository sourceRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("source-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("source-repository")); Repository targetRepository = - repositoryService.createRepository(testIdWatcher.getEntityName("target-repoisotry")); + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); repositoryService.addRepositoryLocale(sourceRepository, "fr-CA", "fr-FR", false); diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index 44de06369e..a9fbcd150f 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -147,7 +147,7 @@ public enum PreserveStatusMode { PRECISION, /** * Preserve status when the match is unambiguous (single source text unit matched). Medium risk - * — trusts all unique matches regardless of their precition. + * — trusts all unique matches regardless of their precision. */ UNIQUE, /** diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index be030bc43a..ae0b6180e7 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -148,7 +148,7 @@ public enum PreserveStatusMode { PRECISION, /** * Preserve status when the match is unambiguous (single source text unit matched). Medium risk - * — trusts all unique matches regardless of their precition. + * — trusts all unique matches regardless of their precision. */ UNIQUE, /** @@ -161,17 +161,15 @@ public enum PreserveStatusMode { /** * Controls when existing translations may be overwritten during leveraging, based on a comparison * of the candidate's original status against the target locale's current status. - * - *

Status hierarchy (lowest to highest): TRANSLATION_NEEDED < REVIEW_NEEDED < APPROVED. */ public enum OverwriteMode { - /** Only leverage into locales that have no translation at all. */ + /** Overwrite regardless of the current status. */ + ALL, + /** Only leverage into locales that have no translation. */ UNTRANSLATED_ONLY, /** Overwrite only when the candidate's original status is strictly higher. */ HIGHER_STATUS, /** Overwrite when the candidate's original status is higher or equal. */ - HIGHER_OR_EQUAL_STATUS, - /** Always overwrite regardless of the current status. */ - ALL + HIGHER_OR_EQUAL_STATUS } } From 96b8f0d9f4c536d6db7d2bfaa45c410ce776a4f1 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Thu, 16 Apr 2026 14:57:02 +0200 Subject: [PATCH 13/17] Refactor status comparison to be less fragile --- .../com/box/l10n/mojito/entity/TMTextUnitVariant.java | 8 ++++++++ .../l10n/mojito/service/leveraging/AbstractLeverager.java | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/webapp/src/main/java/com/box/l10n/mojito/entity/TMTextUnitVariant.java b/webapp/src/main/java/com/box/l10n/mojito/entity/TMTextUnitVariant.java index 16d832b0d2..e2d960f31e 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/entity/TMTextUnitVariant.java +++ b/webapp/src/main/java/com/box/l10n/mojito/entity/TMTextUnitVariant.java @@ -76,6 +76,14 @@ public enum Status { REVIEW_NEEDED, /** A string that doesn't need any work to be performed on it. */ APPROVED; + + public boolean isHigherThan(Status other) { + return this.ordinal() > other.ordinal(); + } + + public boolean isHigherOrEqualTo(Status other) { + return this.ordinal() >= other.ordinal(); + } }; @Column(name = "content", length = Integer.MAX_VALUE) diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index b32fa22a57..68c9756cf6 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -245,10 +245,9 @@ private boolean shouldLeverageLocale( TMTextUnitVariant.Status currentStatus = currentStatusByLocaleId.get(localeId); return switch (overwriteMode) { case UNTRANSLATED_ONLY -> currentStatus == null; - case HIGHER_STATUS -> - currentStatus == null || candidateStatus.ordinal() > currentStatus.ordinal(); + case HIGHER_STATUS -> currentStatus == null || candidateStatus.isHigherThan(currentStatus); case HIGHER_OR_EQUAL_STATUS -> - currentStatus == null || candidateStatus.ordinal() >= currentStatus.ordinal(); + currentStatus == null || candidateStatus.isHigherOrEqualTo(currentStatus); case ALL -> true; }; } From 1e299be8e20f43a64b7b6555701aba0beb3fa727 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Fri, 17 Apr 2026 14:31:32 +0200 Subject: [PATCH 14/17] Improve CLI descriptions and formatting --- .../mojito/cli/command/LeveragingCommand.java | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index 74749fb2e9..2c9ea6a586 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -84,10 +84,12 @@ public class LeveragingCommand extends Command { arity = 1, required = false, description = - "Matching mode. " - + "MD5 will perform matching based on the ID, content and comment. " - + "EXACT match is only using the content. " - + "NAME match is only using the resource name.", + """ + Matching mode. + - MD5: match on all of the resource name, source content and comment. + - EXACT: match on the source (!) content. + - NAME: match on the resource name. + - TUIDS: only leverage between tmTextUnitIds specified in explicit mapping.""", converter = CopyTmConfigModeConverter.class) CopyTmConfig.Mode mode = CopyTmConfig.Mode.MD5; @@ -96,13 +98,18 @@ public class LeveragingCommand extends Command { arity = 1, required = false, description = - "Controls whether to keep the leveraged translation's status or downgrade it to TRANSLATION_NEEDED. " - + "PRECISION (default): preserve status based on the match precision (ID, content)." - + " Lowest risk of carrying over incorrect statuses. " - + "UNIQUE: preserve status when the match is unambiguous (single source text unit matched)." - + " Medium risk — trusts all unique matches regardless of their precision. " - + "ALL: always preserve the source status. Highest risk — ambiguous matches may " - + "copy an arbitrarily chosen translation at its original (possibly APPROVED) status.", + """ + Controls whether to keep the leveraged translation's original status or downgrade \ + it to TRANSLATION_NEEDED. + A match is 'unique' when exactly one candidate text unit matched (no ambiguity). \ + A match is 'high-precision' when matched on both name and content (or full MD5). + - PRECISION (default): preserve status only when the match is both unique and \ + high-precision. Matches on name-only or content-only are always downgraded, \ + even if unique. Low risk. + - UNIQUE: preserve status for any unique match, regardless of its precision. \ + Useful when e.g. migrating between naming schemes. Medium risk. + - ALL: always preserve the original status, even for ambiguous matches. HIGH RISK - \ + this will arbitrarily pick one of several candidates.""", converter = PreserveStatusModeConverter.class) CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.PRECISION; @@ -111,12 +118,13 @@ public class LeveragingCommand extends Command { arity = 1, required = false, description = - "Controls when existing translations may be overwritten based on status comparison. " - + "ALL (default): overwrite regardless of status. " - + "UNTRANSLATED_ONLY: only leverage into locales that have no translation. " - + "HIGHER_STATUS: overwrite only when the candidate status is strictly higher " - + "(e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). " - + "HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. ", + """ + Controls when existing translations may be overwritten based on status comparison. + - ALL (default): overwrite regardless of status. + - HIGHER_STATUS: overwrite only when the candidate status is strictly higher \ + (e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). + - HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. + - UNTRANSLATED_ONLY: only leverage into locales that have no translation.""", converter = OverwriteModeConverter.class) CopyTmConfig.OverwriteMode overwriteMode = CopyTmConfig.OverwriteMode.ALL; @@ -124,9 +132,10 @@ public class LeveragingCommand extends Command { names = {"--tuids-mapping"}, required = false, description = - "Text unit mapping (by tmTextUnitId) for TUIDS mode, format: \"1001:2001;1002:2002\" " - + "(\"source_tm_text_unit_id:target_tm_text_unit_id;...\" with source_tm_text_unit_id unique. " - + "Use multiple calls to copy the same source to multiple targets)", + """ + Text unit mapping (by tmTextUnitId) for TUIDS mode. + Format: "1001:2001;1002:2002" ("source_tm_text_unit_id:target_tm_text_unit_id;..."). + Note: source_tm_text_unit_id should be unique. Use multiple calls to copy the same source to multiple targets.""", converter = TmTextUnitMappingConverter.class) Map sourceToTargetTmTextUnitMapping; From b03c19a9a00880e11fa884cb8d3667282ead5254 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Fri, 17 Apr 2026 14:42:11 +0200 Subject: [PATCH 15/17] Add separate mode to overwrite only strings marked for translation --- .../mojito/cli/command/LeveragingCommand.java | 3 +- .../l10n/mojito/rest/entity/CopyTmConfig.java | 9 +- .../mojito/rest/leveraging/CopyTmConfig.java | 8 +- .../service/leveraging/AbstractLeverager.java | 4 +- .../leveraging/LeveragingServiceTest.java | 109 +++++++++++++++++- 5 files changed, 123 insertions(+), 10 deletions(-) diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index 2c9ea6a586..649adb7605 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -124,7 +124,8 @@ public class LeveragingCommand extends Command { - HIGHER_STATUS: overwrite only when the candidate status is strictly higher \ (e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). - HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. - - UNTRANSLATED_ONLY: only leverage into locales that have no translation.""", + - FOR_TRANSLATION: leverage into locales with no translation or with TRANSLATION_NEEDED status. + - NONE: never overwrite; only leverage into locales that have no translation at all.""", converter = OverwriteModeConverter.class) CopyTmConfig.OverwriteMode overwriteMode = CopyTmConfig.OverwriteMode.ALL; diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index a9fbcd150f..706c7a1b82 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -164,8 +164,13 @@ public enum PreserveStatusMode { public enum OverwriteMode { /** Overwrite regardless of the current status. */ ALL, - /** Only leverage into locales that have no translation. */ - UNTRANSLATED_ONLY, + /** Never overwrite; only leverage into locales that have no translation at all. */ + NONE, + /** + * Leverage into locales that have no translation or whose current status is + * TRANSLATION_NEEDED. + */ + FOR_TRANSLATION, /** Overwrite only when the candidate's original status is strictly higher. */ HIGHER_STATUS, /** Overwrite when the candidate's original status is higher or equal. */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index ae0b6180e7..ca2f4e0d5e 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java @@ -165,8 +165,12 @@ public enum PreserveStatusMode { public enum OverwriteMode { /** Overwrite regardless of the current status. */ ALL, - /** Only leverage into locales that have no translation. */ - UNTRANSLATED_ONLY, + /** Never overwrite; only leverage into locales that have no translation at all. */ + NONE, + /** + * Leverage into locales that have no translation or whose current status is TRANSLATION_NEEDED. + */ + FOR_TRANSLATION, /** Overwrite only when the candidate's original status is strictly higher. */ HIGHER_STATUS, /** Overwrite when the candidate's original status is higher or equal. */ diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java index 68c9756cf6..d540d662d0 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/leveraging/AbstractLeverager.java @@ -244,7 +244,9 @@ private boolean shouldLeverageLocale( OverwriteMode overwriteMode) { TMTextUnitVariant.Status currentStatus = currentStatusByLocaleId.get(localeId); return switch (overwriteMode) { - case UNTRANSLATED_ONLY -> currentStatus == null; + case NONE -> currentStatus == null; + case FOR_TRANSLATION -> + currentStatus == null || currentStatus == TMTextUnitVariant.Status.TRANSLATION_NEEDED; case HIGHER_STATUS -> currentStatus == null || candidateStatus.isHigherThan(currentStatus); case HIGHER_OR_EQUAL_STATUS -> currentStatus == null || candidateStatus.isHigherOrEqualTo(currentStatus); diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java index 2c327b615d..8a35707398 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/leveraging/LeveragingServiceTest.java @@ -741,7 +741,7 @@ public void copyBetweenAssets() } @Test - public void untranslatedOnlyModeSkipsApprovedLocaleAndLeveragesOthers() + public void noneModeSkipsApprovedLocaleAndLeveragesOthers() throws InterruptedException, ExecutionException, RepositoryNameAlreadyUsedException, @@ -799,7 +799,7 @@ public void untranslatedOnlyModeSkipsApprovedLocaleAndLeveragesOthers() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.UNTRANSLATED_ONLY); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.NONE); leveragingService.copyTm(copyTmConfig).get(); @@ -828,7 +828,7 @@ public void untranslatedOnlyModeSkipsApprovedLocaleAndLeveragesOthers() } @Test - public void untranslatedOnlyModeSkipsTranslationNeededLocale() + public void noneModeSkipsTranslationNeededLocale() throws InterruptedException, ExecutionException, RepositoryNameAlreadyUsedException, @@ -883,7 +883,7 @@ public void untranslatedOnlyModeSkipsTranslationNeededLocale() copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setMode(CopyTmConfig.Mode.MD5); - copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.UNTRANSLATED_ONLY); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.NONE); leveragingService.copyTm(copyTmConfig).get(); @@ -901,6 +901,107 @@ public void untranslatedOnlyModeSkipsTranslationNeededLocale() TMTextUnitVariant.Status.TRANSLATION_NEEDED, targetTranslations.get(0).getStatus()); } + @Test + public void forTranslationModeOverwritesTranslationNeededAndLeveragesUntranslated() + throws InterruptedException, + ExecutionException, + RepositoryNameAlreadyUsedException, + RepositoryLocaleCreationException, + AssetWithIdNotFoundException, + RepositoryWithIdNotFoundException { + + Locale frFR = localeService.findByBcp47Tag("fr-FR"); + Locale koKR = localeService.findByBcp47Tag("ko-KR"); + Locale jaJP = localeService.findByBcp47Tag("ja-JP"); + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("sourceRepository")); + repositoryService.addRepositoryLocale(sourceRepository, "fr-FR"); + repositoryService.addRepositoryLocale(sourceRepository, "ko-KR"); + repositoryService.addRepositoryLocale(sourceRepository, "ja-JP"); + + Asset sourceAsset = + assetService.createAssetWithContent( + sourceRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit sourceTu = + tmService.addTMTextUnit( + sourceRepository.getTm().getId(), + sourceAsset.getId(), + "greeting", + "Hello", + "A greeting"); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), frFR.getId(), "Bonjour", TMTextUnitVariant.Status.APPROVED, true); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), koKR.getId(), "안녕하세요", TMTextUnitVariant.Status.APPROVED, true); + tmService.addCurrentTMTextUnitVariant( + sourceTu.getId(), jaJP.getId(), "こんにちは", TMTextUnitVariant.Status.APPROVED, true); + + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("targetRepository")); + repositoryService.addRepositoryLocale(targetRepository, "fr-FR"); + repositoryService.addRepositoryLocale(targetRepository, "ko-KR"); + repositoryService.addRepositoryLocale(targetRepository, "ja-JP"); + + Asset targetAsset = + assetService.createAssetWithContent( + targetRepository.getId(), "fake_for_test", "fake for test"); + + TMTextUnit targetTu = + tmService.addTMTextUnit( + targetRepository.getTm().getId(), + targetAsset.getId(), + "greeting", + "Hello", + "A greeting"); + + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + frFR.getId(), + "existing translation needed", + TMTextUnitVariant.Status.TRANSLATION_NEEDED, + true); + tmService.addCurrentTMTextUnitVariant( + targetTu.getId(), + koKR.getId(), + "existing approved ko", + TMTextUnitVariant.Status.APPROVED, + true); + + CopyTmConfig copyTmConfig = new CopyTmConfig(); + copyTmConfig.setSourceRepositoryId(sourceRepository.getId()); + copyTmConfig.setTargetRepositoryId(targetRepository.getId()); + copyTmConfig.setMode(CopyTmConfig.Mode.MD5); + copyTmConfig.setPreserveStatusMode(CopyTmConfig.PreserveStatusMode.ALL); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.FOR_TRANSLATION); + + leveragingService.copyTm(copyTmConfig).get(); + + TMTextUnitCurrentVariant frCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + frFR.getId(), targetTu.getId()); + TMTextUnitCurrentVariant koCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + koKR.getId(), targetTu.getId()); + TMTextUnitCurrentVariant jaCV = + tmTextUnitCurrentVariantRepository.findByLocale_IdAndTmTextUnit_Id( + jaJP.getId(), targetTu.getId()); + + Assert.assertEquals( + "fr-FR should be overwritten because current status is TRANSLATION_NEEDED", + "Bonjour", + frCV.getTmTextUnitVariant().getContent()); + + Assert.assertEquals( + "ko-KR should NOT be overwritten because current status is APPROVED", + "existing approved ko", + koCV.getTmTextUnitVariant().getContent()); + + Assert.assertNotNull("ja-JP should be leveraged since it had no translation", jaCV); + Assert.assertEquals("こんにちは", jaCV.getTmTextUnitVariant().getContent()); + } + @Test public void higherStatusModeOverwritesLowerStatus() throws InterruptedException, From d9f0b606d1530b0cb67c3471aec718019a6167f1 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Fri, 17 Apr 2026 15:23:17 +0200 Subject: [PATCH 16/17] Fix spotless --- .../java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java index 706c7a1b82..9e9b128eb4 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CopyTmConfig.java @@ -167,8 +167,7 @@ public enum OverwriteMode { /** Never overwrite; only leverage into locales that have no translation at all. */ NONE, /** - * Leverage into locales that have no translation or whose current status is - * TRANSLATION_NEEDED. + * Leverage into locales that have no translation or whose current status is TRANSLATION_NEEDED. */ FOR_TRANSLATION, /** Overwrite only when the candidate's original status is strictly higher. */ From bd3e8b68bd62887fc791d0eb2dd05d0285fe4199 Mon Sep 17 00:00:00 2001 From: Wadim Wawrzenczak Date: Fri, 17 Apr 2026 15:25:31 +0200 Subject: [PATCH 17/17] Remove newlines from CLI opt descriptions --- .../mojito/cli/command/LeveragingCommand.java | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java index 649adb7605..37fc505fc3 100644 --- a/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/LeveragingCommand.java @@ -85,11 +85,11 @@ public class LeveragingCommand extends Command { required = false, description = """ - Matching mode. - - MD5: match on all of the resource name, source content and comment. - - EXACT: match on the source (!) content. - - NAME: match on the resource name. - - TUIDS: only leverage between tmTextUnitIds specified in explicit mapping.""", + Matching mode. \ + MD5: match on all of the resource name, source content and comment. \ + EXACT: match on the source (!) content. \ + NAME: match on the resource name. \ + TUIDS: only leverage between tmTextUnitIds specified in explicit mapping.""", converter = CopyTmConfigModeConverter.class) CopyTmConfig.Mode mode = CopyTmConfig.Mode.MD5; @@ -100,16 +100,16 @@ public class LeveragingCommand extends Command { description = """ Controls whether to keep the leveraged translation's original status or downgrade \ - it to TRANSLATION_NEEDED. + it to TRANSLATION_NEEDED. \ A match is 'unique' when exactly one candidate text unit matched (no ambiguity). \ - A match is 'high-precision' when matched on both name and content (or full MD5). - - PRECISION (default): preserve status only when the match is both unique and \ - high-precision. Matches on name-only or content-only are always downgraded, \ - even if unique. Low risk. - - UNIQUE: preserve status for any unique match, regardless of its precision. \ - Useful when e.g. migrating between naming schemes. Medium risk. - - ALL: always preserve the original status, even for ambiguous matches. HIGH RISK - \ - this will arbitrarily pick one of several candidates.""", + A match is 'high-precision' when matched on both name and content (or full MD5). \ + PRECISION (default): preserve status only when the match is both unique and \ + high-precision. Matches on name-only or content-only are always downgraded, \ + even if unique. Low risk. \ + UNIQUE: preserve status for any unique match, regardless of its precision. \ + Useful when e.g. migrating between naming schemes. Medium risk. \ + ALL: always preserve the original status, even for ambiguous matches. High risk - \ + this will arbitrarily pick one of several candidates.""", converter = PreserveStatusModeConverter.class) CopyTmConfig.PreserveStatusMode preserveStatusMode = CopyTmConfig.PreserveStatusMode.PRECISION; @@ -119,13 +119,13 @@ public class LeveragingCommand extends Command { required = false, description = """ - Controls when existing translations may be overwritten based on status comparison. - - ALL (default): overwrite regardless of status. - - HIGHER_STATUS: overwrite only when the candidate status is strictly higher \ - (e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). - - HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. - - FOR_TRANSLATION: leverage into locales with no translation or with TRANSLATION_NEEDED status. - - NONE: never overwrite; only leverage into locales that have no translation at all.""", + Controls when existing translations may be overwritten based on status comparison. \ + ALL (default): overwrite regardless of status. \ + HIGHER_STATUS: overwrite only when the candidate status is strictly higher \ + (e.g. TRANSLATION_NEEDED -> REVIEW_NEEDED, REVIEW_NEEDED -> APPROVED). \ + HIGHER_OR_EQUAL_STATUS: same as HIGHER_STATUS but also overwrite when statuses are equal. \ + FOR_TRANSLATION: leverage into locales with no translation or with TRANSLATION_NEEDED status. \ + NONE: never overwrite; only leverage into locales that have no translation at all.""", converter = OverwriteModeConverter.class) CopyTmConfig.OverwriteMode overwriteMode = CopyTmConfig.OverwriteMode.ALL; @@ -134,8 +134,8 @@ public class LeveragingCommand extends Command { required = false, description = """ - Text unit mapping (by tmTextUnitId) for TUIDS mode. - Format: "1001:2001;1002:2002" ("source_tm_text_unit_id:target_tm_text_unit_id;..."). + Text unit mapping (by tmTextUnitId) for TUIDS mode. \ + Format: "1001:2001;1002:2002" ("source_tm_text_unit_id:target_tm_text_unit_id;..."). \ Note: source_tm_text_unit_id should be unique. Use multiple calls to copy the same source to multiple targets.""", converter = TmTextUnitMappingConverter.class) Map sourceToTargetTmTextUnitMapping;