From 0875f80a747d02894ccbfa2614ceabd876b0cd0d Mon Sep 17 00:00:00 2001 From: zshs000 <2862877643@qq.com> Date: Tue, 3 Mar 2026 08:39:03 +0800 Subject: [PATCH 1/2] feat: add file translations APIs --- .../java/com/crowdin/client/ai/AIApi.java | 73 +++++++++++++++++++ .../client/ai/model/AiFileTranslation.java | 35 +++++++++ .../ai/model/AiFileTranslationAddRequest.java | 21 ++++++ .../AiFileTranslationResponseObject.java | 8 ++ .../java/com/crowdin/client/ai/AIApiTest.java | 66 ++++++++++++++++- .../api/ai/addAiFileTranslationRequest.json | 22 ++++++ .../api/ai/aiFileTranslationResponse.json | 24 ++++++ .../ai/downloadAiFileTranslationResponse.json | 6 ++ ...nloadAiFileTranslationStringsResponse.json | 6 ++ 9 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java create mode 100644 src/main/java/com/crowdin/client/ai/model/AiFileTranslationAddRequest.java create mode 100644 src/main/java/com/crowdin/client/ai/model/AiFileTranslationResponseObject.java create mode 100644 src/test/resources/api/ai/addAiFileTranslationRequest.json create mode 100644 src/test/resources/api/ai/aiFileTranslationResponse.json create mode 100644 src/test/resources/api/ai/downloadAiFileTranslationResponse.json create mode 100644 src/test/resources/api/ai/downloadAiFileTranslationStringsResponse.json diff --git a/src/main/java/com/crowdin/client/ai/AIApi.java b/src/main/java/com/crowdin/client/ai/AIApi.java index 8403d2ca..ea59f5d7 100644 --- a/src/main/java/com/crowdin/client/ai/AIApi.java +++ b/src/main/java/com/crowdin/client/ai/AIApi.java @@ -603,6 +603,79 @@ public ResponseObject aiTranslateStrings(Long userId, AiTranslateRe return ResponseObject.of(response.getData()); } + /** + * @param userId user identifier + * @param request request body + * @return file translation job + * @see + */ + public ResponseObject addAiFileTranslation(Long userId, AiFileTranslationAddRequest request) { + String url = getAIPath(userId, "ai/file-translations"); + AiFileTranslationResponseObject response = this.httpClient.post(url, request, new HttpRequestConfig(), AiFileTranslationResponseObject.class); + return ResponseObject.of(response.getData()); + } + + /** + * @param userId user identifier + * @param jobIdentifier AI file translation job identifier + * @return file translation job status + * @see + */ + public ResponseObject getAiFileTranslationStatus(Long userId, String jobIdentifier) { + String url = getAIPath(userId, "ai/file-translations/" + jobIdentifier); + AiFileTranslationResponseObject response = this.httpClient.get(url, new HttpRequestConfig(), AiFileTranslationResponseObject.class); + return ResponseObject.of(response.getData()); + } + + /** + * @param userId user identifier + * @param jobIdentifier AI file translation job identifier + * @see + */ + public void cancelAiFileTranslation(Long userId, String jobIdentifier) { + String url = getAIPath(userId, "ai/file-translations/" + jobIdentifier); + this.httpClient.delete(url, new HttpRequestConfig(), Void.class); + } + + /** + * @param userId user identifier + * @param jobIdentifier AI file translation job identifier + * @return translated file download link + * @see + */ + public ResponseObject downloadAiFileTranslation(Long userId, String jobIdentifier) { + String url = getAIPath(userId, "ai/file-translations/" + jobIdentifier + "/download"); + DownloadLinkResponseObject response = this.httpClient.get(url, new HttpRequestConfig(), DownloadLinkResponseObject.class); + return ResponseObject.of(response.getData()); + } + + /** + * @param userId user identifier + * @param jobIdentifier AI file translation job identifier + * @return translated strings download link + * @see + */ + public ResponseObject downloadAiFileTranslationStrings(Long userId, String jobIdentifier) { + String url = getAIPath(userId, "ai/file-translations/" + jobIdentifier + "/translations"); + DownloadLinkResponseObject response = this.httpClient.get(url, new HttpRequestConfig(), DownloadLinkResponseObject.class); + return ResponseObject.of(response.getData()); + } + private String getAIPath(Long userId, String path) { return userId != null ? String.format("%s/users/%d/%s", this.url, userId, path) : String.format("%s/%s", this.url, path); } diff --git a/src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java b/src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java new file mode 100644 index 00000000..82965553 --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java @@ -0,0 +1,35 @@ +package com.crowdin.client.ai.model; + +import lombok.Data; + +import java.util.Date; + +@Data +public class AiFileTranslation { + private String identifier; + private String status; + private Long progress; + private Attributes attributes; + private Date createdAt; + private Date updatedAt; + private Date startedAt; + private Date finishedAt; + + @Data + public static class Attributes { + private String stage; + private Error error; + private String downloadName; + private String sourceLanguageId; + private String targetLanguageId; + private String originalFileName; + private String detectedType; + private Integer parserVersion; + } + + @Data + public static class Error { + private String stage; + private String message; + } +} diff --git a/src/main/java/com/crowdin/client/ai/model/AiFileTranslationAddRequest.java b/src/main/java/com/crowdin/client/ai/model/AiFileTranslationAddRequest.java new file mode 100644 index 00000000..99cf4a36 --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiFileTranslationAddRequest.java @@ -0,0 +1,21 @@ +package com.crowdin.client.ai.model; + +import lombok.Data; + +import java.util.List; + +@Data +public class AiFileTranslationAddRequest { + private Long storageId; + private String sourceLanguageId; + private String targetLanguageId; + private String type; + private Integer parserVersion; + private List tmIds; + private List glossaryIds; + private Long aiPromptId; + private Long aiProviderId; + private String aiModelId; + private List instructions; + private List attachmentIds; +} diff --git a/src/main/java/com/crowdin/client/ai/model/AiFileTranslationResponseObject.java b/src/main/java/com/crowdin/client/ai/model/AiFileTranslationResponseObject.java new file mode 100644 index 00000000..ba28ce78 --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiFileTranslationResponseObject.java @@ -0,0 +1,8 @@ +package com.crowdin.client.ai.model; + +import lombok.Data; + +@Data +public class AiFileTranslationResponseObject { + private AiFileTranslation data; +} diff --git a/src/test/java/com/crowdin/client/ai/AIApiTest.java b/src/test/java/com/crowdin/client/ai/AIApiTest.java index 2a6e7299..d516532d 100644 --- a/src/test/java/com/crowdin/client/ai/AIApiTest.java +++ b/src/test/java/com/crowdin/client/ai/AIApiTest.java @@ -67,6 +67,10 @@ public class AIApiTest extends TestClient { private static final String PROXY_CHAT = "%s/users/%d/ai/providers/%d/chat/completions"; private static final String LIST_SUPPORTED_AI_PROVIDER_MODELS = "%s/users/%d/ai/providers/supported-models"; private static final String AI_TRANSLATE_STRING = "%s/users/%d/ai/translate"; + private static final String AI_FILE_TRANSLATIONS = "%s/users/%d/ai/file-translations"; + private static final String AI_FILE_TRANSLATION = "%s/users/%d/ai/file-translations/%s"; + private static final String AI_FILE_TRANSLATION_DOWNLOAD = "%s/users/%d/ai/file-translations/%s/download"; + private static final String AI_FILE_TRANSLATION_STRINGS = "%s/users/%d/ai/file-translations/%s/translations"; @Override public List getMocks() { @@ -108,7 +112,12 @@ public List getMocks() { RequestMock.build(String.format(AI_PROMPT, this.url, userId, aiPromptId), HttpPatch.METHOD_NAME, "api/ai/editPromptRequest.json", "api/ai/promptResponse.json"), RequestMock.build(String.format(PROXY_CHAT, this.url, userId, aiPromptId), HttpPost.METHOD_NAME, "api/ai/proxyChatCompletionRequest.json", "api/ai/proxyChatCompletionResponse.json"), RequestMock.build(String.format(LIST_SUPPORTED_AI_PROVIDER_MODELS, this.url, userId), HttpGet.METHOD_NAME, "api/ai/listSupportedAiProviderModels.json"), - RequestMock.build(String.format(AI_TRANSLATE_STRING, this.url, userId), HttpPost.METHOD_NAME, "api/ai/aiTranslateRequest.json", "api/ai/aiTranslateResponse.json") + RequestMock.build(String.format(AI_TRANSLATE_STRING, this.url, userId), HttpPost.METHOD_NAME, "api/ai/aiTranslateRequest.json", "api/ai/aiTranslateResponse.json"), + RequestMock.build(String.format(AI_FILE_TRANSLATIONS, this.url, userId), HttpPost.METHOD_NAME, "api/ai/addAiFileTranslationRequest.json", "api/ai/aiFileTranslationResponse.json"), + RequestMock.build(String.format(AI_FILE_TRANSLATION, this.url, userId, jobIdentifier), HttpGet.METHOD_NAME, "api/ai/aiFileTranslationResponse.json"), + RequestMock.build(String.format(AI_FILE_TRANSLATION, this.url, userId, jobIdentifier), HttpDelete.METHOD_NAME), + RequestMock.build(String.format(AI_FILE_TRANSLATION_DOWNLOAD, this.url, userId, jobIdentifier), HttpGet.METHOD_NAME, "api/ai/downloadAiFileTranslationResponse.json"), + RequestMock.build(String.format(AI_FILE_TRANSLATION_STRINGS, this.url, userId, jobIdentifier), HttpGet.METHOD_NAME, "api/ai/downloadAiFileTranslationStringsResponse.json") ); } @@ -554,4 +563,59 @@ public void aiTranslateStringsTest() { assertEquals(2, response.getData().getTranslations().size()); assertEquals("Перекладений текст 1", response.getData().getTranslations().get(0)); } + + @Test + public void addAiFileTranslationTest() { + AiFileTranslationAddRequest request = new AiFileTranslationAddRequest(); + request.setStorageId(123L); + request.setSourceLanguageId("en"); + request.setTargetLanguageId("uk"); + request.setType("xliff"); + request.setParserVersion(1); + request.setTmIds(Collections.singletonList(123L)); + request.setGlossaryIds(Collections.singletonList(456L)); + request.setAiPromptId(789L); + request.setAiProviderId(12L); + request.setAiModelId("gpt-4.1"); + request.setInstructions(Collections.singletonList("Keep a formal tone")); + request.setAttachmentIds(Collections.singletonList(123L)); + + ResponseObject response = this.getAiApi().addAiFileTranslation(userId, request); + assertEquals(jobIdentifier, response.getData().getIdentifier()); + assertEquals(status, response.getData().getStatus()); + assertEquals(progress, response.getData().getProgress()); + assertEquals("translate", response.getData().getAttributes().getStage()); + assertEquals("en", response.getData().getAttributes().getSourceLanguageId()); + assertEquals("uk", response.getData().getAttributes().getTargetLanguageId()); + } + + @Test + public void getAiFileTranslationStatusTest() { + ResponseObject response = this.getAiApi().getAiFileTranslationStatus(userId, jobIdentifier); + assertEquals(jobIdentifier, response.getData().getIdentifier()); + assertEquals(status, response.getData().getStatus()); + assertEquals("file.pdf", response.getData().getAttributes().getDownloadName()); + assertEquals("chrome", response.getData().getAttributes().getDetectedType()); + } + + @Test + public void cancelAiFileTranslationTest() { + this.getAiApi().cancelAiFileTranslation(userId, jobIdentifier); + } + + @Test + public void downloadAiFileTranslationTest() { + ResponseObject response = this.getAiApi().downloadAiFileTranslation(userId, jobIdentifier); + assertNotNull(response.getData()); + assertNotNull(response.getData().getUrl()); + assertFalse(response.getData().getUrl().isEmpty()); + } + + @Test + public void downloadAiFileTranslationStringsTest() { + ResponseObject response = this.getAiApi().downloadAiFileTranslationStrings(userId, jobIdentifier); + assertNotNull(response.getData()); + assertNotNull(response.getData().getUrl()); + assertFalse(response.getData().getUrl().isEmpty()); + } } diff --git a/src/test/resources/api/ai/addAiFileTranslationRequest.json b/src/test/resources/api/ai/addAiFileTranslationRequest.json new file mode 100644 index 00000000..59f07555 --- /dev/null +++ b/src/test/resources/api/ai/addAiFileTranslationRequest.json @@ -0,0 +1,22 @@ +{ + "storageId": 123, + "sourceLanguageId": "en", + "targetLanguageId": "uk", + "type": "xliff", + "parserVersion": 1, + "tmIds": [ + 123 + ], + "glossaryIds": [ + 456 + ], + "aiPromptId": 789, + "aiProviderId": 12, + "aiModelId": "gpt-4.1", + "instructions": [ + "Keep a formal tone" + ], + "attachmentIds": [ + 123 + ] +} diff --git a/src/test/resources/api/ai/aiFileTranslationResponse.json b/src/test/resources/api/ai/aiFileTranslationResponse.json new file mode 100644 index 00000000..40f882e2 --- /dev/null +++ b/src/test/resources/api/ai/aiFileTranslationResponse.json @@ -0,0 +1,24 @@ +{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "stage": "translate", + "error": { + "stage": "import", + "message": "Failed to parse file" + }, + "downloadName": "file.pdf", + "sourceLanguageId": "en", + "targetLanguageId": "uk", + "originalFileName": "Sample_Chrome.json", + "detectedType": "chrome", + "parserVersion": 2 + }, + "createdAt": "2026-01-23T11:26:54+00:00", + "updatedAt": "2026-01-23T11:26:54+00:00", + "startedAt": "2026-01-23T11:26:54+00:00", + "finishedAt": "2026-01-23T11:26:54+00:00" + } +} diff --git a/src/test/resources/api/ai/downloadAiFileTranslationResponse.json b/src/test/resources/api/ai/downloadAiFileTranslationResponse.json new file mode 100644 index 00000000..5ef3cde7 --- /dev/null +++ b/src/test/resources/api/ai/downloadAiFileTranslationResponse.json @@ -0,0 +1,6 @@ +{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff?response-content-disposition={disposition}&X-Amz-Content-Sha256={sha}&X-Amz-Algorithm={algorithm}&X-Amz-Credential={credentials}&X-Amz-Date={date}&X-Amz-SignedHeaders={headers}&X-Amz-Expires={expires}&X-Amz-Signature={signature}", + "expireIn": "2019-09-20T10:31:21+00:00" + } +} diff --git a/src/test/resources/api/ai/downloadAiFileTranslationStringsResponse.json b/src/test/resources/api/ai/downloadAiFileTranslationStringsResponse.json new file mode 100644 index 00000000..5ef3cde7 --- /dev/null +++ b/src/test/resources/api/ai/downloadAiFileTranslationStringsResponse.json @@ -0,0 +1,6 @@ +{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff?response-content-disposition={disposition}&X-Amz-Content-Sha256={sha}&X-Amz-Algorithm={algorithm}&X-Amz-Credential={credentials}&X-Amz-Date={date}&X-Amz-SignedHeaders={headers}&X-Amz-Expires={expires}&X-Amz-Signature={signature}", + "expireIn": "2019-09-20T10:31:21+00:00" + } +} From 273f918c96fa2c22b329ea7fc3586422811807bb Mon Sep 17 00:00:00 2001 From: zshs000 <2862877643@qq.com> Date: Tue, 3 Mar 2026 17:34:34 +0800 Subject: [PATCH 2/2] refactor: rename AiFileTranslation error type for clarity --- .../java/com/crowdin/client/ai/model/AiFileTranslation.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java b/src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java index 82965553..da4e3c56 100644 --- a/src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java +++ b/src/main/java/com/crowdin/client/ai/model/AiFileTranslation.java @@ -18,7 +18,7 @@ public class AiFileTranslation { @Data public static class Attributes { private String stage; - private Error error; + private FileTranslationError error; private String downloadName; private String sourceLanguageId; private String targetLanguageId; @@ -28,7 +28,7 @@ public static class Attributes { } @Data - public static class Error { + private static class FileTranslationError { private String stage; private String message; }