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..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 @@ -84,19 +84,59 @@ 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.", + """ + 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; + @Parameter( + names = {"--preserve-status", "-ps"}, + arity = 1, + required = false, + description = + """ + 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; + + @Parameter( + names = {"--overwrite-mode", "-om"}, + arity = 1, + 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.""", + converter = OverwriteModeConverter.class) + CopyTmConfig.OverwriteMode overwriteMode = CopyTmConfig.OverwriteMode.ALL; + @Parameter( 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; @@ -144,6 +184,8 @@ void copyTmBetweenRepositories() throws CommandException { copyTmConfig.setTargetRepositoryId(targetRepository.getId()); copyTmConfig.setNameRegex(nameRegexParam); copyTmConfig.setTargetBranchName(targetBranchNameParam); + copyTmConfig.setPreserveStatusMode(preserveStatusMode); + 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/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 9619622667..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); @@ -197,13 +197,171 @@ public void copyTMModeExact() throws Exception { Assert.assertFalse(itTargetTranslations.hasNext()); } + @Test + public void copyTMModeName() throws Exception { + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("source-repository")); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); + + 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 copyTMModeNamePreserveStatus() throws Exception { + + Repository sourceRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("source-repository")); + Repository targetRepository = + repositoryService.createRepository(testIdWatcher.getEntityName("target-repository")); + + 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", + "ALL"); + + 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 with ALL", + TMTextUnitVariant.Status.APPROVED, + variant.getStatus()); + } + } + @Test 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); @@ -298,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/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/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 3e05e088b3..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 @@ -21,6 +21,10 @@ public class CopyTmConfig { Mode mode = Mode.MD5; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.PRECISION; + + OverwriteMode overwriteMode = OverwriteMode.ALL; + PollableTask pollableTask; @JsonProperty @@ -101,13 +105,74 @@ public void setTargetBranchName(String targetBranchName) { this.targetBranchName = targetBranchName; } + public PreserveStatusMode getPreserveStatusMode() { + return preserveStatusMode; + } + + public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { + this.preserveStatusMode = preserveStatusMode; + } + + public OverwriteMode getOverwriteMode() { + return overwriteMode; + } + + public void setOverwriteMode(OverwriteMode overwriteMode) { + this.overwriteMode = overwriteMode; + } + /** Matching mode for leveraging */ public enum Mode { /** MD5 match means the message id, comment and content must be the same */ 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 } + + /** + * Controls whether to keep the leveraged translation's status or downgrade it to + * TRANSLATION_NEEDED. + */ + public enum PreserveStatusMode { + /** + * 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 precision. + */ + UNIQUE, + /** + * 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 original status against the target locale's current status. + */ + public enum OverwriteMode { + /** Overwrite regardless of the current status. */ + ALL, + /** 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. */ + HIGHER_OR_EQUAL_STATUS + } } 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/rest/leveraging/CopyTmConfig.java b/webapp/src/main/java/com/box/l10n/mojito/rest/leveraging/CopyTmConfig.java index 7be7193b66..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 @@ -22,6 +22,10 @@ public class CopyTmConfig { Mode mode = Mode.MD5; + PreserveStatusMode preserveStatusMode = PreserveStatusMode.PRECISION; + + OverwriteMode overwriteMode = OverwriteMode.ALL; + PollableTask pollableTask; @JsonProperty @@ -104,13 +108,72 @@ public void setTargetBranchName(String targetBranchName) { this.targetBranchName = targetBranchName; } + public PreserveStatusMode getPreserveStatusMode() { + return preserveStatusMode; + } + + public void setPreserveStatusMode(PreserveStatusMode preserveStatusMode) { + this.preserveStatusMode = preserveStatusMode; + } + + public OverwriteMode getOverwriteMode() { + return overwriteMode; + } + + public void setOverwriteMode(OverwriteMode overwriteMode) { + this.overwriteMode = overwriteMode; + } + /** Matching mode for leveraging */ public enum Mode { /** MD5 match means the message id, comment and content must be the same */ 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 } + + /** + * Controls whether to keep the leveraged translation's status or downgrade it to + * TRANSLATION_NEEDED. + */ + public enum PreserveStatusMode { + /** + * 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 precision. + */ + UNIQUE, + /** + * 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 original status against the target locale's current status. + */ + public enum OverwriteMode { + /** Overwrite regardless of the current status. */ + ALL, + /** 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. */ + HIGHER_OR_EQUAL_STATUS + } } 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..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 @@ -5,14 +5,19 @@ 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.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; @@ -36,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. @@ -82,8 +89,22 @@ public abstract List getLeveragingMatches( * @param assetId */ public void performLeveragingFor(List tmTextUnits, Long sourceTmId, Long assetId) { + performLeveragingFor( + tmTextUnits, sourceTmId, assetId, PreserveStatusMode.PRECISION, OverwriteMode.ALL); + } - logger.debug("Perform leveraging: {}", getType()); + public void performLeveragingFor( + List tmTextUnits, + Long sourceTmId, + Long assetId, + PreserveStatusMode preserveStatusMode, + OverwriteMode overwriteMode) { + + logger.debug( + "Perform leveraging: {}, preserveStatusMode: {}, overwriteMode: {}", + getType(), + preserveStatusMode, + overwriteMode); for (Iterator tmTextUnitsIterator = tmTextUnits.iterator(); tmTextUnitsIterator.hasNext(); ) { @@ -110,16 +131,30 @@ 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 = + computeTranslationNeeded(preserveStatusMode, uniqueTMTextUnitMatched); addLeveragedTranslations( - tmTextUnit, textUnitDTOsForLeveraging, translationNeeded, uniqueTMTextUnitMatched); + tmTextUnit, + textUnitDTOsForLeveraging, + translationNeeded, + uniqueTMTextUnitMatched, + overwriteMode); } else { logger.debug("No Match found for this TMTextUnit with name: {}", tmTextUnit.getName()); } } } + boolean computeTranslationNeeded( + PreserveStatusMode preserveStatusMode, boolean uniqueTMTextUnitMatched) { + return switch (preserveStatusMode) { + case ALL -> false; + case UNIQUE -> !uniqueTMTextUnitMatched; + case PRECISION -> isTranslationNeededIfUniqueMatch() || !uniqueTMTextUnitMatched; + }; + } + /** * Adds translations (potentially to be re-translated) into the {@link TMTextUnit}. * @@ -135,11 +170,26 @@ private void addLeveragedTranslations( TMTextUnit tmTextUnit, List translations, boolean translationNeeded, - boolean uniqueTMTextUnitMatched) { + boolean uniqueTMTextUnitMatched, + OverwriteMode overwriteMode) { logger.debug("Add leveraged translations in tmTextUnit, id: {}", tmTextUnit.getId()); + Map currentStatusByLocaleId = + buildCurrentStatusByLocaleId(tmTextUnit, overwriteMode); + for (TextUnitDTO translation : translations) { + if (!shouldLeverageLocale( + currentStatusByLocaleId, + translation.getLocaleId(), + translation.getStatus(), + overwriteMode)) { + logger.debug( + "Skipping locale {} for tmTextUnit {} due to status overwrite mode", + translation.getLocaleId(), + tmTextUnit.getId()); + continue; + } AddTMTextUnitCurrentVariantResult addTMTextUnitCurrentVariantWithResult = tmService.addTMTextUnitCurrentVariantWithResult( @@ -174,6 +224,36 @@ private void addLeveragedTranslations( } } + private Map buildCurrentStatusByLocaleId( + TMTextUnit tmTextUnit, OverwriteMode overwriteMode) { + if (overwriteMode == OverwriteMode.ALL) { + 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, + TMTextUnitVariant.Status candidateStatus, + OverwriteMode overwriteMode) { + TMTextUnitVariant.Status currentStatus = currentStatusByLocaleId.get(localeId); + return switch (overwriteMode) { + 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); + case ALL -> true; + }; + } + 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/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..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 @@ -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; @@ -57,6 +55,8 @@ public class LeveragingService { @Autowired LeveragerByNameAndContent leveragerByNameAndContent; + @Autowired LeveragerByName leveragerByName; + @Autowired RepositoryRepository repositoryRepository; @Autowired AssetRepository assetRepository; @@ -148,25 +148,56 @@ void copyTmBetweenRepositories(CopyTmConfig copyTmConfig) List textUnitsForCopyTM = getTextUnitsForCopyTM( - targetRepository, - copyTmConfig.getTargetAssetId(), - copyTmConfig.getNameRegex(), - copyTmConfig.getTargetBranchName()); + targetRepository, copyTmConfig.getTargetAssetId(), copyTmConfig.getTargetBranchName()); - if (CopyTmConfig.Mode.TUIDS.equals(copyTmConfig.getMode())) { - copyTranslationBetweenTextUnits(copyTmConfig.getSourceToTargetTmTextUnitIds()); - } else if (CopyTmConfig.Mode.MD5.equals(copyTmConfig.getMode())) { + filterTextUnitsByNameRegex(textUnitsForCopyTM, copyTmConfig.getNameRegex()); + + CopyTmConfig.PreserveStatusMode preserveStatusMode = copyTmConfig.getPreserveStatusMode(); + CopyTmConfig.OverwriteMode overwriteMode = copyTmConfig.getOverwriteMode(); + + if (CopyTmConfig.Mode.MD5.equals(copyTmConfig.getMode())) { leveragerByMd5.performLeveragingFor( - textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); - } else { + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatusMode, + overwriteMode); + } else if (CopyTmConfig.Mode.NAME.equals(copyTmConfig.getMode())) { logger.debug( - "First perform leveraging by name and content (to give priority to string with same tags"); + "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(), + preserveStatusMode, + overwriteMode); logger.debug("Now, perform leveraging only on the name"); + leveragerByName.performLeveragingFor( + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatusMode, + overwriteMode); + } 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( + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatusMode, + overwriteMode); + + logger.debug("Now, perform leveraging only on the content"); leveragerByContent.performLeveragingFor( - textUnitsForCopyTM, sourceRepository.getTm().getId(), copyTmConfig.getSourceAssetId()); + textUnitsForCopyTM, + sourceRepository.getTm().getId(), + copyTmConfig.getSourceAssetId(), + preserveStatusMode, + overwriteMode); + } else { + throw new UnsupportedOperationException("Unexpected mode " + copyTmConfig.getMode()); } } @@ -222,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/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..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 @@ -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; @@ -264,6 +268,271 @@ 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 copyWithNameMatchPreservesStatusForAnyMatch() + 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.ALL); + + 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 with ALL", + 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 unambiguous match with UNIQUE mode", + 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 with PRECISION mode for NAME leveraging", + TMTextUnitVariant.Status.TRANSLATION_NEEDED, + variant.getStatus()); + } + } + + @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, @@ -470,4 +739,750 @@ public void copyBetweenAssets() Assert.assertEquals(2, targetTranslations2.size()); } + + @Test + public void noneModeSkipsApprovedLocaleAndLeveragesOthers() + 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.setOverwriteMode(CopyTmConfig.OverwriteMode.NONE); + + 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()); + } + + @Test + public void noneModeSkipsTranslationNeededLocale() + 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.NONE); + + 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 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, + 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.ALL); + 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.ALL); + 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.ALL); + 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.ALL); + 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 higherStatusModeUsesCandidateStatusNotEffectiveStatus() + 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.PRECISION); + copyTmConfig.setOverwriteMode(CopyTmConfig.OverwriteMode.HIGHER_STATUS); + + leveragingService.copyTm(copyTmConfig).get(); + + List targetTranslations = + tmTextUnitVariantRepository + .findByTmTextUnitTmRepositoriesAndLocale_Bcp47TagNotOrderByContent( + targetRepository, "en"); + + 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 + 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.ALL); + 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()); + } }