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 20ebb94406..1ef4414fb7 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 @@ -73,7 +73,10 @@ public enum Status { MT_REVIEW, /** A string that doesn't need any work to be performed on it. */ - APPROVED; + APPROVED, + + /** It was translated in Mojito, so it won't be updated during third-party sync */ + TRANSLATED_IN_MOJITO; }; @Column(name = "content", length = Integer.MAX_VALUE) diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java index a8a6b6e7d3..1a22628738 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/TMService.java @@ -568,7 +568,8 @@ public AddTMTextUnitCurrentVariantResult addTMTextUnitCurrentVariantWithResult( TMTextUnitVariant.Status status, boolean includedInLocalizedFile, ZonedDateTime createdDate, - User createdBy) { + User createdBy, + boolean checkTranslatedInMojito) { boolean noUpdate = false; @@ -604,16 +605,21 @@ public AddTMTextUnitCurrentVariantResult addTMTextUnitCurrentVariantWithResult( } else { logger.debug("There is a current text unit variant, check if an update is needed"); TMTextUnitVariant currentTmTextUnitVariant = tmTextUnitCurrentVariant.getTmTextUnitVariant(); + boolean translatedInMojito = + checkTranslatedInMojito + && currentTmTextUnitVariant.getStatus() + == TMTextUnitVariant.Status.TRANSLATED_IN_MOJITO; boolean updateNeeded = - isUpdateNeededForTmTextUnitVariant( - currentTmTextUnitVariant.getStatus(), - currentTmTextUnitVariant.getContentMD5(), - currentTmTextUnitVariant.isIncludedInLocalizedFile(), - currentTmTextUnitVariant.getComment(), - status, - DigestUtils.md5Hex(content), - includedInLocalizedFile, - comment); + !translatedInMojito + && isUpdateNeededForTmTextUnitVariant( + currentTmTextUnitVariant.getStatus(), + currentTmTextUnitVariant.getContentMD5(), + currentTmTextUnitVariant.isIncludedInLocalizedFile(), + currentTmTextUnitVariant.getComment(), + status, + DigestUtils.md5Hex(content), + includedInLocalizedFile, + comment); if (updateNeeded) { logger.debug( @@ -638,7 +644,9 @@ public AddTMTextUnitCurrentVariantResult addTMTextUnitCurrentVariantWithResult( tmTextUnitCurrentVariantRepository.save(tmTextUnitCurrentVariant); } else { logger.debug( - "The current text unit variant has same content, comment and review status, don't add entities and return it instead"); + translatedInMojito + ? "The current text unit variant is kept because it has the TRANSLATED_IN_MOJITO status" + : "The current text unit variant has same content, comment and review status, don't add entities and return it instead"); noUpdate = true; } } @@ -646,6 +654,33 @@ public AddTMTextUnitCurrentVariantResult addTMTextUnitCurrentVariantWithResult( return new AddTMTextUnitCurrentVariantResult(!noUpdate, tmTextUnitCurrentVariant); } + public AddTMTextUnitCurrentVariantResult addTMTextUnitCurrentVariantWithResult( + TMTextUnitCurrentVariant tmTextUnitCurrentVariant, + Long tmId, + Long assetId, + Long tmTextUnitId, + Long localeId, + String content, + String comment, + TMTextUnitVariant.Status status, + boolean includedInLocalizedFile, + ZonedDateTime createdDate, + User createdBy) { + return this.addTMTextUnitCurrentVariantWithResult( + tmTextUnitCurrentVariant, + tmId, + assetId, + tmTextUnitId, + localeId, + content, + comment, + status, + includedInLocalizedFile, + createdDate, + createdBy, + false); + } + /** * Indicates if a {@link TMTextUnitVariant} should be updated by looking at new/old content, * status, comments, etc diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterService.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterService.java index a54cb4caf6..76928ae2d5 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterService.java @@ -263,7 +263,8 @@ void importTextUnitsOfLocaleAndAsset( textUnitForBatchImport.getStatus(), textUnitForBatchImport.isIncludedInLocalizedFile(), importTime, - importedBy); + importedBy, + true); if (addTMTextUnitCurrentVariantResult.isTmTextUnitCurrentVariantUpdated()) { diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/StatusFilter.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/StatusFilter.java index 35065c5b8f..82827fb6c0 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/StatusFilter.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/StatusFilter.java @@ -52,4 +52,6 @@ public enum StatusFilter { * TextUnits that are not rejected, ie {@link TMTextUnitVariant#includedInLocalizedFile} is true. */ NOT_REJECTED, + /** TextUnits with status ({@link TMTextUnitVariant.Status#TRANSLATED_IN_MOJITO}). */ + TRANSLATED_IN_MOJITO, } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/TextUnitSearcher.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/TextUnitSearcher.java index 728da69b9b..5e55067270 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/TextUnitSearcher.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/search/TextUnitSearcher.java @@ -454,6 +454,11 @@ NativeCriteria getCriteriaForSearch(TextUnitSearcherParameters searchParameters) "tuv.status", TMTextUnitVariant.Status.TRANSLATION_NEEDED.toString()), new NativeEqExpFix("tuv.included_in_localized_file", Boolean.FALSE)))); break; + case TRANSLATED_IN_MOJITO: + conjunction.add( + new NativeEqExpFix( + "tuv.status", TMTextUnitVariant.Status.TRANSLATED_IN_MOJITO.toString())); + break; default: throw new RuntimeException("Filter type not implemented"); } diff --git a/webapp/src/main/resources/config/application.properties b/webapp/src/main/resources/config/application.properties index f4a83c583f..f3e79fab1f 100644 --- a/webapp/src/main/resources/config/application.properties +++ b/webapp/src/main/resources/config/application.properties @@ -265,4 +265,3 @@ spring.session.jdbc.table-name=SPRING_SESSION_V2 # l10n.pagerduty.retry.minBackOffDelay=500 # Maximum back off delay in milliseconds # l10n.pagerduty.retry.maxBackOffDelay=5000 - diff --git a/webapp/src/main/resources/properties/en.properties b/webapp/src/main/resources/properties/en.properties index c267fe1882..88ba572c99 100644 --- a/webapp/src/main/resources/properties/en.properties +++ b/webapp/src/main/resources/properties/en.properties @@ -213,6 +213,9 @@ search.statusDropdown.needsReview=Needs Review # Status filter option to search for text units that need to be translated search.statusDropdown.forTranslation=Needs Translation +# Status filter option to search for text units that are translated in Mojito +search.statusDropdown.translatedInMojito=Translated in Mojito + # Status filter option to search for text units that are rejected (won't be added in localized file) search.statusDropdown.rejected=Rejected @@ -325,6 +328,9 @@ textUnit.reviewModal.rejected=Rejected # Button label used for primary action "removeReview" on modal dialog textUnit.reviewModal.accepted=Accepted +# Label for Translated in Mojito button on modal dialog +textUnit.reviewModal.translatedInMojito=Translated in Mojito + # Button label used for the Needs Review button on the textunit review modal textUnit.reviewModal.needsReview=Needs Review diff --git a/webapp/src/main/resources/public/js/components/workbench/StatusDropdown.js b/webapp/src/main/resources/public/js/components/workbench/StatusDropdown.js index a33f732ebb..faa4d1a0d3 100644 --- a/webapp/src/main/resources/public/js/components/workbench/StatusDropdown.js +++ b/webapp/src/main/resources/public/js/components/workbench/StatusDropdown.js @@ -103,8 +103,8 @@ let StatusDropdown = createReactClass({ }, setStateAndCallSearchParamChanged(searchFilterParam, searchFilterParamValue) { - let state = {}; + state[searchFilterParam] = searchFilterParamValue; this.setState(state, function () { @@ -170,6 +170,8 @@ let StatusDropdown = createReactClass({ return this.props.intl.formatMessage({ id: "search.statusDropdown.needsReview" }); case SearchParamsStore.STATUS.REJECTED: return this.props.intl.formatMessage({ id: "search.statusDropdown.rejected" }); + case SearchParamsStore.STATUS.TRANSLATED_IN_MOJITO: + return this.props.intl.formatMessage({ id: "search.statusDropdown.translatedInMojito" }); } }, @@ -263,6 +265,7 @@ let StatusDropdown = createReactClass({ {this.renderStatusMenuItem(SearchParamsStore.STATUS.FOR_TRANSLATION)} {this.renderStatusMenuItem(SearchParamsStore.STATUS.REVIEW_NEEDED)} {this.renderStatusMenuItem(SearchParamsStore.STATUS.REJECTED)} + {this.renderStatusMenuItem(SearchParamsStore.STATUS.TRANSLATED_IN_MOJITO)} diff --git a/webapp/src/main/resources/public/js/components/workbench/TextUnit.js b/webapp/src/main/resources/public/js/components/workbench/TextUnit.js index 58057b16b3..00f3654b55 100644 --- a/webapp/src/main/resources/public/js/components/workbench/TextUnit.js +++ b/webapp/src/main/resources/public/js/components/workbench/TextUnit.js @@ -363,6 +363,10 @@ let TextUnit = createReactClass({ textUnit.setIncludedInLocalizedFile(true); textUnit.setStatus(TextUnitSDK.STATUS.TRANSLATION_NEEDED); break; + case "translated_in_mojito": + textUnit.setIncludedInLocalizedFile(true); + textUnit.setStatus(TextUnitSDK.STATUS.TRANSLATED_IN_MOJITO); + break; } WorkbenchActions.saveTextUnit(textUnit); diff --git a/webapp/src/main/resources/public/js/components/workbench/TextUnitsReviewModal.js b/webapp/src/main/resources/public/js/components/workbench/TextUnitsReviewModal.js index 750134eb4a..c56b1737bc 100644 --- a/webapp/src/main/resources/public/js/components/workbench/TextUnitsReviewModal.js +++ b/webapp/src/main/resources/public/js/components/workbench/TextUnitsReviewModal.js @@ -20,7 +20,8 @@ class TextUnitsreviewModal extends React.Component { this.REVIEW = "review"; this.REJECT = "reject"; this.ACCEPT = "accept"; - this.TRANSLATE = "translate"; + this.TRANSLATE = "translate" + this.TRANSLATED_IN_MOJITO = "translated_in_mojito"; this.state = { "currentReviewState": this.getInitialReviewStateOfTextUnits(), @@ -99,6 +100,15 @@ class TextUnitsreviewModal extends React.Component { ); }; + getTranslatedInMojitoButton = () => { + return ( + + ); + }; + /** * @returns {JSX} The JSX for the translate button with class active set according to the current component state */ @@ -166,6 +176,8 @@ class TextUnitsreviewModal extends React.Component { currentReviewState = this.REVIEW; } else if (textUnit.getStatus() === TextUnit.STATUS.TRANSLATION_NEEDED) { currentReviewState = this.TRANSLATE; + } else if (textUnit.getStatus() === TextUnit.STATUS.TRANSLATED_IN_MOJITO) { + currentReviewState = this.TRANSLATED_IN_MOJITO; } } @@ -195,7 +207,7 @@ class TextUnitsreviewModal extends React.Component { return ( { e.stopPropagation() - }}> + }} bsSize="large"> @@ -216,6 +228,7 @@ class TextUnitsreviewModal extends React.Component { {this.getTranslateButton()} {this.getReviewButton()} {this.getAcceptButton()} + {this.getTranslatedInMojitoButton()} diff --git a/webapp/src/main/resources/public/js/sdk/TextUnit.js b/webapp/src/main/resources/public/js/sdk/TextUnit.js index 8a6172b3a5..fa01acd04e 100644 --- a/webapp/src/main/resources/public/js/sdk/TextUnit.js +++ b/webapp/src/main/resources/public/js/sdk/TextUnit.js @@ -308,5 +308,6 @@ TextUnit.STATUS = { "TRANSLATION_NEEDED": "TRANSLATION_NEEDED", "REVIEW_NEEDED": "REVIEW_NEEDED", "APPROVED": "APPROVED", - "REJECTED": "REJECTED" + "REJECTED": "REJECTED", + "TRANSLATED_IN_MOJITO": "TRANSLATED_IN_MOJITO", }; diff --git a/webapp/src/main/resources/public/js/stores/workbench/SearchParamsStore.js b/webapp/src/main/resources/public/js/stores/workbench/SearchParamsStore.js index 812d0af9b8..ddaaee6fa4 100644 --- a/webapp/src/main/resources/public/js/stores/workbench/SearchParamsStore.js +++ b/webapp/src/main/resources/public/js/stores/workbench/SearchParamsStore.js @@ -446,7 +446,10 @@ SearchParamsStore.STATUS = { * TextUnits that are not rejected, ie includedInLocalizedFile is true. */ "NOT_REJECTED": "NOT_REJECTED", - + /** + * TextUnits with status TRANSLATED_IN_MOJITO. + */ + "TRANSLATED_IN_MOJITO": "TRANSLATED_IN_MOJITO", }; export default alt.createStore(SearchParamsStore, 'SearchParamsStore'); diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/tm/TMServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/tm/TMServiceTest.java index 19baddd47f..4f380a3181 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/tm/TMServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/tm/TMServiceTest.java @@ -43,6 +43,7 @@ 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.box.l10n.mojito.service.tm.search.TextUnitSearcherParametersForTesting; import com.box.l10n.mojito.test.TestIdWatcher; import com.google.common.base.Function; import com.google.common.collect.FluentIterable; @@ -4756,4 +4757,56 @@ public void testLocalizeHtmlFilter() throws Exception { "", ""); assertEquals(assetContent, localizedAsset); } + + @Test + public void testAddTMTextUnitWithTranslatedInMojitoStatus() + throws RepositoryNameAlreadyUsedException { + createTestData(); + + Long textUnitId = + addTextUnitAndCheck( + this.tmId, + this.assetId, + "name", + "this is the content", + "some comment", + "3063c39d3cf8ab69bcabbbc5d7187dc9", + "cf8ea6b6848f23345648038bc3abf324"); + + Locale targetLocale = this.localeService.findByBcp47Tag("fr-FR"); + + this.tmService.addTMTextUnitCurrentVariant( + textUnitId, + targetLocale.getId(), + "this is the new content", + "some comment", + TMTextUnitVariant.Status.TRANSLATED_IN_MOJITO, + true); + + TextUnitSearcherParameters textUnitSearcherParameters = + new TextUnitSearcherParametersForTesting(); + textUnitSearcherParameters.setRepositoryNames( + Collections.singletonList(this.repository.getName())); + textUnitSearcherParameters.setAssetPath(this.asset.getPath()); + textUnitSearcherParameters.setLocaleTags(List.of(targetLocale.getBcp47Tag())); + + TextUnitDTO textUnitDTOFromSearch = + this.textUnitSearcher.search(textUnitSearcherParameters).getFirst(); + + assertEquals("this is the new content", textUnitDTOFromSearch.getTarget()); + assertEquals(TMTextUnitVariant.Status.TRANSLATED_IN_MOJITO, textUnitDTOFromSearch.getStatus()); + + this.tmService.addTMTextUnitCurrentVariant( + textUnitId, + targetLocale.getId(), + "this is the newest content", + "some comment", + TMTextUnitVariant.Status.APPROVED, + true); + + textUnitDTOFromSearch = this.textUnitSearcher.search(textUnitSearcherParameters).getFirst(); + + assertEquals("this is the newest content", textUnitDTOFromSearch.getTarget()); + assertEquals(TMTextUnitVariant.Status.APPROVED, textUnitDTOFromSearch.getStatus()); + } } diff --git a/webapp/src/test/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterServiceTest.java b/webapp/src/test/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterServiceTest.java index 970fe254fe..af02e1cd78 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterServiceTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/service/tm/importer/TextUnitBatchImporterServiceTest.java @@ -12,6 +12,7 @@ import com.box.l10n.mojito.entity.PollableTask; import com.box.l10n.mojito.entity.Repository; import com.box.l10n.mojito.entity.RepositoryLocale; +import com.box.l10n.mojito.entity.TMTextUnitVariant; import com.box.l10n.mojito.service.asset.VirtualAsset; import com.box.l10n.mojito.service.asset.VirtualAssetBadRequestException; import com.box.l10n.mojito.service.asset.VirtualAssetService; @@ -36,6 +37,7 @@ import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import org.junit.Rule; @@ -502,4 +504,90 @@ public void testIntegirtyChecker() throws Exception { "should be included with proper placeholder", textUnitDTOs.get(0).isIncludedInLocalizedFile()); } + + @Test + public void testImportTextUnits() throws InterruptedException { + TMTestData tmTestData = new TMTestData(this.testIdWatcher); + + TextUnitDTO textUnitDTO = new TextUnitDTO(); + textUnitDTO.setRepositoryName(tmTestData.repository.getName()); + textUnitDTO.setTargetLocale(tmTestData.frFR.getBcp47Tag()); + textUnitDTO.setAssetPath(tmTestData.asset.getPath()); + textUnitDTO.setName("TEST2"); + textUnitDTO.setTarget("New TEST2 translation for fr"); + textUnitDTO.setStatus(TMTextUnitVariant.Status.TRANSLATED_IN_MOJITO); + + TextUnitDTO textUnitDTO2 = new TextUnitDTO(); + textUnitDTO2.setRepositoryName(tmTestData.repository.getName()); + textUnitDTO2.setTargetLocale(tmTestData.frFR.getBcp47Tag()); + textUnitDTO2.setAssetPath(tmTestData.asset.getPath()); + textUnitDTO2.setName("TEST3"); + textUnitDTO2.setTarget("New TEST3 translation for fr"); + + List textUnitDTOsForImport = Arrays.asList(textUnitDTO, textUnitDTO2); + + this.textUnitBatchImporterService.importTextUnits(textUnitDTOsForImport, true, false); + + TextUnitSearcherParameters textUnitSearcherParameters = + new TextUnitSearcherParametersForTesting(); + textUnitSearcherParameters.setRepositoryNames( + Collections.singletonList(tmTestData.repository.getName())); + textUnitSearcherParameters.setAssetPath(tmTestData.asset.getPath()); + textUnitSearcherParameters.setLocaleTags(List.of("fr-FR")); + + List textUnitDTOsFromSearch = + this.textUnitSearcher.search(textUnitSearcherParameters); + + int i = 1; + assertEquals("TEST2", textUnitDTOsFromSearch.get(i).getName()); + assertEquals("New TEST2 translation for fr", textUnitDTOsFromSearch.get(i).getTarget()); + i++; + assertEquals("TEST3", textUnitDTOsFromSearch.get(i).getName()); + assertEquals("New TEST3 translation for fr", textUnitDTOsFromSearch.get(i).getTarget()); + + textUnitDTO = new TextUnitDTO(); + textUnitDTO.setRepositoryName(tmTestData.repository.getName()); + textUnitDTO.setTargetLocale(tmTestData.frFR.getBcp47Tag()); + textUnitDTO.setAssetPath(tmTestData.asset.getPath()); + textUnitDTO.setName("TEST2"); + textUnitDTO.setTarget("The newest TEST2 translation for fr"); + + textUnitDTO2 = new TextUnitDTO(); + textUnitDTO2.setRepositoryName(tmTestData.repository.getName()); + textUnitDTO2.setTargetLocale(tmTestData.frFR.getBcp47Tag()); + textUnitDTO2.setAssetPath(tmTestData.asset.getPath()); + textUnitDTO2.setName("TEST3"); + textUnitDTO2.setTarget("The newest TEST3 translation for fr"); + + textUnitDTOsForImport = Arrays.asList(textUnitDTO, textUnitDTO2); + + this.textUnitBatchImporterService.importTextUnits(textUnitDTOsForImport, false, false); + + textUnitDTOsFromSearch = this.textUnitSearcher.search(textUnitSearcherParameters); + + i = 1; + assertEquals("TEST2", textUnitDTOsFromSearch.get(i).getName()); + assertEquals("New TEST2 translation for fr", textUnitDTOsFromSearch.get(i).getTarget()); + i++; + assertEquals("TEST3", textUnitDTOsFromSearch.get(i).getName()); + assertEquals("The newest TEST3 translation for fr", textUnitDTOsFromSearch.get(i).getTarget()); + + // Set status to APPROVED + this.tmService.addTMTextUnitCurrentVariant( + textUnitDTOsFromSearch.get(1).getTmTextUnitId(), + textUnitDTOsFromSearch.get(1).getLocaleId(), + "Reverted TEST2 translation for fr", + textUnitDTOsFromSearch.get(1).getComment(), + TMTextUnitVariant.Status.APPROVED, + true); + + textUnitDTOsFromSearch = this.textUnitSearcher.search(textUnitSearcherParameters); + + i = 1; + assertEquals("TEST2", textUnitDTOsFromSearch.get(i).getName()); + assertEquals("Reverted TEST2 translation for fr", textUnitDTOsFromSearch.get(i).getTarget()); + i++; + assertEquals("TEST3", textUnitDTOsFromSearch.get(i).getName()); + assertEquals("The newest TEST3 translation for fr", textUnitDTOsFromSearch.get(i).getTarget()); + } }