From 78246ece65ca1ac690ff2f0c0caaca0232cd1813 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 20 Sep 2022 11:20:49 +0200 Subject: [PATCH 001/121] docs: add Unreleased section to changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca215c2..4a3e090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + + ## [0.1.3] - 2022-09-09 ### Fixed * Fixed examples in readme. @@ -25,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...HEAD [0.1.3]: https://github.com/DeepLcom/deepl-java/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/DeepLcom/deepl-java/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/DeepLcom/deepl-java/compare/v0.1.0...v0.1.1 From b04c45bec16a7cd5401973b95af9e6456f583acb Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 20 Sep 2022 11:17:21 +0200 Subject: [PATCH 002/121] docs: correction to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3e090..9359c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.3] - 2022-09-09 ### Fixed * Fixed examples in readme. -* `Usage.Detail` `count` and `detail` properties type changed from `int` to `long`. +* `Usage.Detail` `count` and `limit` properties type changed from `int` to `long`. ## [0.1.2] - 2022-09-08 From e1827a66f7fbd73453d5f0e1161b29fa74fa8a72 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 8 Sep 2022 20:31:42 +0200 Subject: [PATCH 003/121] docs: add Maven Central badge to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7808c80..0a80746 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # DeepL Java Library +[![Maven Central](https://img.shields.io/maven-central/v/com.deepl.api/deepl-java.svg)](https://mvnrepository.com/artifact/com.deepl.api/deepl-java) [![License: MIT](https://img.shields.io/badge/license-MIT-blueviolet.svg)](https://github.com/DeepLcom/deepl-java/blob/main/LICENSE) The [DeepL API][api-docs] is a language translation API that allows other From ed930995e3e2d3f3f4984d26ce28ccf6b9db04c5 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 20 Sep 2022 11:32:53 +0200 Subject: [PATCH 004/121] fix: Requests resulting in `503 Service Unavailable` errors are now retried. --- CHANGELOG.md | 4 ++++ .../src/main/java/com/deepl/api/HttpClientWrapper.java | 3 +-- .../test/java/com/deepl/api/TranslateDocumentTest.java | 9 --------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9359c9b..dccfcaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +* Requests resulting in `503 Service Unavailable` errors are now retried. + Attempting to download a document before translation is completed will now + wait and retry (up to 5 times by default), rather than throwing an exception. ## [0.1.3] - 2022-09-09 diff --git a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java index 2c33c20..182b4cd 100644 --- a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java +++ b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java @@ -87,8 +87,7 @@ private HttpResponseStream sendRequestWithBackoff( sendRequest(method, serverUrl + relativeUrl, backoffTimer.getTimeoutMillis(), content); if (backoffTimer.getNumRetries() >= this.maxRetries) { return response; - } else if (response.getCode() != 429 - && (response.getCode() < 500 || response.getCode() == 503)) { + } else if (response.getCode() != 429 && response.getCode() < 500) { return response; } response.close(); diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java index f39b8f8..54e789c 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java @@ -160,15 +160,6 @@ void testTranslateDocumentLowLevel() throws Exception { Assertions.assertTrue(status.ok()); Assertions.assertFalse(status.done()); - // Downloading before document is ready will fail - Assertions.assertThrows( - DocumentNotReadyException.class, - () -> { - translator.translateDocumentDownload(handle, outputFile); - }); - // Output file should not exist in case of failure - Assertions.assertFalse(outputFile.exists()); - // Test recreating a document handle from id & key String documentId = handle.getDocumentId(); String documentKey = handle.getDocumentKey(); From 6a2dc4bcff21921545778e4ea0974c27a3e586f9 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 20 Sep 2022 11:37:39 +0200 Subject: [PATCH 005/121] feat: add Formality options PreferLess and PreferMore --- CHANGELOG.md | 2 ++ .../main/java/com/deepl/api/Formality.java | 9 +++++++++ .../main/java/com/deepl/api/Translator.java | 6 ++++++ .../java/com/deepl/api/TranslateTextTest.java | 20 +++++++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dccfcaa..f4ff29c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +* Add new `Formality` options: `PreferLess` and `PreferMore`. ### Changed * Requests resulting in `503 Service Unavailable` errors are now retried. Attempting to download a document before translation is completed will now diff --git a/deepl-java/src/main/java/com/deepl/api/Formality.java b/deepl-java/src/main/java/com/deepl/api/Formality.java index c3ed4e9..6b19638 100644 --- a/deepl-java/src/main/java/com/deepl/api/Formality.java +++ b/deepl-java/src/main/java/com/deepl/api/Formality.java @@ -13,4 +13,13 @@ public enum Formality { /** Increased formality. */ More, + + /** + * Less formality, i.e. more informal, if available for the specified target language, otherwise + * default. + */ + PreferLess, + + /** Increased formality, if available for the specified target language, otherwise default. */ + PreferMore, } diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 1de8747..d8a02f2 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -684,6 +684,12 @@ private static ArrayList> createHttpParamsCommon( case Less: params.add(new KeyValuePair<>("formality", "less")); break; + case PreferMore: + params.add(new KeyValuePair<>("formality", "prefer_more")); + break; + case PreferLess: + params.add(new KeyValuePair<>("formality", "prefer_less")); + break; default: break; } diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index 57da312..f1de0a6 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -164,6 +164,26 @@ void testFormality() throws DeepLException, InterruptedException { if (!isMockServer) { Assertions.assertEquals("Wie geht es Ihnen?", result.getText()); } + + result = + translator.translateText( + "How are you?", + null, + "de", + new TextTranslationOptions().setFormality(Formality.PreferLess)); + if (!isMockServer) { + Assertions.assertEquals("Wie geht es dir?", result.getText()); + } + + result = + translator.translateText( + "How are you?", + null, + "de", + new TextTranslationOptions().setFormality(Formality.PreferMore)); + if (!isMockServer) { + Assertions.assertEquals("Wie geht es Ihnen?", result.getText()); + } } @Test From 27d8991800154ed562cd07aa469b294b7825295b Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 20 Sep 2022 11:46:34 +0200 Subject: [PATCH 006/121] fix: use Locale.ENGLISH when changing string case --- CHANGELOG.md | 3 +++ deepl-java/src/main/java/com/deepl/api/LanguageCode.java | 8 +++++--- deepl-java/src/test/java/com/deepl/api/GeneralTest.java | 2 +- .../src/test/java/com/deepl/api/TranslateTextTest.java | 8 ++++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ff29c..6d1a823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Requests resulting in `503 Service Unavailable` errors are now retried. Attempting to download a document before translation is completed will now wait and retry (up to 5 times by default), rather than throwing an exception. +### Fixed +* Use `Locale.ENGLISH` when changing string case. + * Thanks to [seratch](https://github.com/seratch). ## [0.1.3] - 2022-09-09 diff --git a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java index 686f0c4..95893df 100644 --- a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java +++ b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java @@ -3,6 +3,8 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import java.util.*; + /** * Language codes for the languages currently supported by DeepL translation. New languages may be * added in the future; to retrieve the currently supported languages use {@link @@ -113,7 +115,7 @@ public class LanguageCode { */ public static String removeRegionalVariant(String langCode) { String[] parts = langCode.split("-", 2); - return parts[0].toLowerCase(); + return parts[0].toLowerCase(Locale.ENGLISH); } /** @@ -126,9 +128,9 @@ public static String removeRegionalVariant(String langCode) { public static String standardize(String langCode) { String[] parts = langCode.split("-", 2); if (parts.length == 1) { - return parts[0].toLowerCase(); + return parts[0].toLowerCase(Locale.ENGLISH); } else { - return parts[0].toLowerCase() + "-" + parts[1].toUpperCase(); + return parts[0].toLowerCase(Locale.ENGLISH) + "-" + parts[1].toUpperCase(Locale.ENGLISH); } } } diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 9912753..9931ed4 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -36,7 +36,7 @@ void testExampleTranslation() throws DeepLException, InterruptedException { String inputText = entry.getValue(); String sourceLang = LanguageCode.removeRegionalVariant(entry.getKey()); TextResult result = translator.translateText(inputText, sourceLang, "en-US"); - Assertions.assertTrue(result.getText().toLowerCase().contains("proton")); + Assertions.assertTrue(result.getText().toLowerCase(Locale.ENGLISH).contains("proton")); } } diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index f1de0a6..adec48c 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -298,19 +298,19 @@ void testMixedCaseLanguages() throws DeepLException, InterruptedException { TextResult result; result = translator.translateText(exampleText.get("de"), null, "en-us"); - Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase()); + Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase(Locale.ENGLISH)); Assertions.assertEquals("de", result.getDetectedSourceLanguage()); result = translator.translateText(exampleText.get("de"), null, "EN-us"); - Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase()); + Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase(Locale.ENGLISH)); Assertions.assertEquals("de", result.getDetectedSourceLanguage()); result = translator.translateText(exampleText.get("de"), "de", "EN-US"); - Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase()); + Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase(Locale.ENGLISH)); Assertions.assertEquals("de", result.getDetectedSourceLanguage()); result = translator.translateText(exampleText.get("de"), "dE", "EN-US"); - Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase()); + Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase(Locale.ENGLISH)); Assertions.assertEquals("de", result.getDetectedSourceLanguage()); } } From c14102f2c46d688467fafdb2af3e9634b614b41f Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 20 Sep 2022 11:52:39 +0200 Subject: [PATCH 007/121] fix: avoid cases in HttpContent and StreamUtils where temporary objects might not be closed --- CHANGELOG.md | 3 +++ .../src/main/java/com/deepl/api/http/HttpContent.java | 4 ++-- .../src/main/java/com/deepl/api/utils/StreamUtil.java | 10 ++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d1a823..8207ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * Use `Locale.ENGLISH` when changing string case. * Thanks to [seratch](https://github.com/seratch). +* Avoid cases in `HttpContent` and `StreamUtils` where temporary objects might + not be closed. + * Thanks to [seratch](https://github.com/seratch). ## [0.1.3] - 2022-09-09 diff --git a/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java index d38ac38..992e5e7 100644 --- a/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java +++ b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java @@ -61,8 +61,8 @@ public static HttpContent buildMultipartFormDataContent( private static HttpContent buildMultipartFormDataContent( Iterable> params, String boundary) throws Exception { try (ByteArrayOutputStream stream = new ByteArrayOutputStream(); - PrintWriter writer = - new PrintWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) { + OutputStreamWriter osw = new OutputStreamWriter(stream, StandardCharsets.UTF_8); + PrintWriter writer = new PrintWriter(osw)) { if (params != null) { for (KeyValuePair entry : params) { diff --git a/deepl-java/src/main/java/com/deepl/api/utils/StreamUtil.java b/deepl-java/src/main/java/com/deepl/api/utils/StreamUtil.java index 662f606..f46b1b3 100644 --- a/deepl-java/src/main/java/com/deepl/api/utils/StreamUtil.java +++ b/deepl-java/src/main/java/com/deepl/api/utils/StreamUtil.java @@ -13,10 +13,12 @@ public static String readStream(InputStream inputStream) throws IOException { Charset charset = StandardCharsets.UTF_8; final char[] buffer = new char[DEFAULT_BUFFER_SIZE]; final StringBuilder sb = new StringBuilder(); - final Reader in = new BufferedReader(new InputStreamReader(inputStream, charset)); - int charsRead; - while ((charsRead = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) > 0) { - sb.append(buffer, 0, charsRead); + try (InputStreamReader isr = new InputStreamReader(inputStream, charset); + final Reader in = new BufferedReader(isr)) { + int charsRead; + while ((charsRead = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) > 0) { + sb.append(buffer, 0, charsRead); + } } return sb.toString(); } From ed420f8f85092e2f958c7ef4079bbc25414eafca Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 26 Sep 2022 21:45:06 +0200 Subject: [PATCH 008/121] Increase version to 0.2.0 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8207ec0..cdd5a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.2.0] - 2022-09-26 ### Added * Add new `Formality` options: `PreferLess` and `PreferMore`. ### Changed @@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...HEAD +[0.2.0]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/DeepLcom/deepl-java/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/DeepLcom/deepl-java/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/DeepLcom/deepl-java/compare/v0.1.0...v0.1.1 diff --git a/README.md b/README.md index 0a80746..292107f 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:0.1.3" +implementation "com.deepl.api:deepl-java:0.2.0" ``` ### Maven users @@ -41,7 +41,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 0.1.3 + 0.2.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index d7ee666..d32468d 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "0.1.3" +version = "0.2.0" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index d8a02f2..d727dbc 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -54,7 +54,7 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum headers.putAll(options.getHeaders()); } headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + authKey); - headers.putIfAbsent("User-Agent", "deepl-java/0.1.3"); + headers.putIfAbsent("User-Agent", "deepl-java/0.2.0"); this.httpClientWrapper = new HttpClientWrapper( From 0e89acff57efbb4e26d88f13c22a7f028166f3cd Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 13 Oct 2022 13:39:45 +0200 Subject: [PATCH 009/121] fix: handle case where HTTP response is not valid JSON --- CHANGELOG.md | 6 ++++++ .../src/main/java/com/deepl/api/Translator.java | 13 ++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd5a94..1d28f5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Fixed +* Handle case where HTTP response is not valid JSON. + + ## [0.2.0] - 2022-09-26 ### Added * Add new `Formality` options: `PreferLess` and `PreferMore`. @@ -40,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...HEAD [0.2.0]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/DeepLcom/deepl-java/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/DeepLcom/deepl-java/compare/v0.1.1...v0.1.2 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index d727dbc..a698a81 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -10,6 +10,7 @@ import com.deepl.api.http.HttpResponseStream; import com.deepl.api.parsing.Parser; import com.deepl.api.utils.*; +import com.google.gson.*; import java.io.*; import java.net.HttpURLConnection; import java.util.*; @@ -766,10 +767,16 @@ private void checkResponse(HttpResponse response, boolean inDocumentDownload) return; } - String messageSuffix = jsonParser.parseErrorMessage(response.getBody()); - if (!messageSuffix.isEmpty()) { - messageSuffix = ", " + messageSuffix; + String messageSuffix = ""; + String body = response.getBody(); + if (body != null && !body.isEmpty()) { + try { + messageSuffix = ", error message: " + jsonParser.parseErrorMessage(body); + } catch (JsonSyntaxException ignored) { + messageSuffix = ", response: " + body; + } } + switch (response.getCode()) { case HttpURLConnection.HTTP_BAD_REQUEST: throw new DeepLException("Bad request" + messageSuffix); From a4c0a351b7ec8f77d675a182ec4afff6909d9fa4 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Wed, 19 Oct 2022 14:27:15 +0200 Subject: [PATCH 010/121] Increase version to 0.2.1 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d28f5c..0fc2b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.2.1] - 2022-10-19 ### Fixed * Handle case where HTTP response is not valid JSON. @@ -45,7 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...HEAD +[0.2.1]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/DeepLcom/deepl-java/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/DeepLcom/deepl-java/compare/v0.1.1...v0.1.2 diff --git a/README.md b/README.md index 292107f..34b0719 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:0.2.0" +implementation "com.deepl.api:deepl-java:0.2.1" ``` ### Maven users @@ -41,7 +41,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 0.2.0 + 0.2.1 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index d32468d..a88273c 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "0.2.0" +version = "0.2.1" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index a698a81..dadb2ba 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -55,7 +55,7 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum headers.putAll(options.getHeaders()); } headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + authKey); - headers.putIfAbsent("User-Agent", "deepl-java/0.2.0"); + headers.putIfAbsent("User-Agent", "deepl-java/0.2.1"); this.httpClientWrapper = new HttpClientWrapper( From 9a08a234a2dd0184b2d9d997ced67aff0f8771e7 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 13 Dec 2022 09:27:35 +0100 Subject: [PATCH 011/121] docs: add notes on internal classes --- .../src/main/java/com/deepl/api/HttpClientWrapper.java | 6 +++++- .../src/main/java/com/deepl/api/parsing/ErrorResponse.java | 5 +++++ .../java/com/deepl/api/parsing/LanguageDeserializer.java | 5 +++++ deepl-java/src/main/java/com/deepl/api/parsing/Parser.java | 5 +++++ .../src/main/java/com/deepl/api/parsing/TextResponse.java | 5 +++++ .../java/com/deepl/api/parsing/TextResultDeserializer.java | 5 +++++ .../main/java/com/deepl/api/parsing/UsageDeserializer.java | 5 +++++ 7 files changed, 35 insertions(+), 1 deletion(-) diff --git a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java index 182b4cd..3d67c93 100644 --- a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java +++ b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java @@ -11,7 +11,11 @@ import java.util.*; import org.jetbrains.annotations.*; -/** Helper class providing functions to make HTTP requests and retry with exponential-backoff. */ +/** + * Helper class providing functions to make HTTP requests and retry with exponential-backoff. + * + *

This class is internal; you should not use this class directly. + */ class HttpClientWrapper { private static final String CONTENT_TYPE = "Content-Type"; private static final String POST = "POST"; diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java index 31468e4..072c73c 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java @@ -5,6 +5,11 @@ import org.jetbrains.annotations.Nullable; +/** + * Class representing error messages returned by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ class ErrorResponse { @Nullable String message; @Nullable String detail; diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/LanguageDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/LanguageDeserializer.java index da16b1c..f884e34 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/LanguageDeserializer.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/LanguageDeserializer.java @@ -7,6 +7,11 @@ import com.google.gson.*; import java.lang.reflect.Type; +/** + * Utility class for deserializing language codes returned by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ class LanguageDeserializer implements JsonDeserializer { public Language deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index b761bb3..08294c8 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -10,6 +10,11 @@ import java.util.*; import org.jetbrains.annotations.*; +/** + * Parsing functions for responses from the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ public class Parser { private final Gson gson; diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TextResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/TextResponse.java index c6515d1..0f17434 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/TextResponse.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/TextResponse.java @@ -6,6 +6,11 @@ import com.deepl.api.TextResult; import java.util.List; +/** + * Class representing text translation responses from the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ class TextResponse { public List translations; } diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java index 43a849f..15d6180 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java @@ -7,6 +7,11 @@ import com.google.gson.*; import java.lang.reflect.Type; +/** + * Utility class for deserializing text translation results returned by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ class TextResultDeserializer implements JsonDeserializer { public TextResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java index ce2d417..1f1db7d 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java @@ -8,6 +8,11 @@ import java.lang.reflect.*; import org.jetbrains.annotations.*; +/** + * Class representing usage responses from the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ class UsageDeserializer implements JsonDeserializer { public Usage deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { From 9915d6bb951757655f33402f4f52426d99a59044 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 13 Dec 2022 09:29:49 +0100 Subject: [PATCH 012/121] refactor: ignoring unused function returns --- deepl-java/src/test/java/com/deepl/api/TestBase.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deepl-java/src/test/java/com/deepl/api/TestBase.java b/deepl-java/src/test/java/com/deepl/api/TestBase.java index b0c6664..09c09bc 100644 --- a/deepl-java/src/test/java/com/deepl/api/TestBase.java +++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java @@ -162,16 +162,16 @@ protected File createInputFile() throws IOException { protected File createInputFile(String content) throws IOException { File inputFile = new File(tempDir + "/example_document.txt"); - inputFile.delete(); - inputFile.createNewFile(); + boolean ignored = inputFile.delete(); + ignored = inputFile.createNewFile(); writeToFile(inputFile, content); return inputFile; } protected File createOutputFile() { File outputFile = new File(tempDir + "/output/example_document.txt"); - new File(outputFile.getParent()).mkdir(); - outputFile.delete(); + boolean ignored = new File(outputFile.getParent()).mkdir(); + ignored = outputFile.delete(); return outputFile; } } From a082ee6b46dd1ced50237d3dd9e578a09f36b4a9 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 13 Dec 2022 09:32:14 +0100 Subject: [PATCH 013/121] refactor!: encapsulate ErrorResponse fields message and detail --- CHANGELOG.md | 7 +++++++ .../com/deepl/api/parsing/ErrorResponse.java | 19 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fc2b66..da2a1f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Changed +* `parsing.ErrorResponse` fields `message` and `detail` are now private, + encapsulated with getters. + + ## [0.2.1] - 2022-10-19 ### Fixed * Handle case where HTTP response is not valid JSON. @@ -45,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...HEAD [0.2.1]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/DeepLcom/deepl-java/compare/v0.1.2...v0.1.3 diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java index 072c73c..49c44c0 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java @@ -11,16 +11,25 @@ *

This class is internal; you should not use this class directly. */ class ErrorResponse { - @Nullable String message; - @Nullable String detail; + @Nullable private String message; + @Nullable private String detail; + /** Returns a diagnostic string including the message and detail (if available). */ public String getErrorMessage() { StringBuilder sb = new StringBuilder(); - if (message != null) sb.append("message: ").append(message); - if (detail != null) { + if (getMessage() != null) sb.append("message: ").append(getMessage()); + if (getDetail() != null) { if (sb.length() != 0) sb.append(", "); - sb.append("detail: ").append(detail); + sb.append("detail: ").append(getDetail()); } return sb.toString(); } + + public @Nullable String getMessage() { + return message; + } + + public @Nullable String getDetail() { + return detail; + } } From 4b0157140a578e3ddcfce680eb3684ed820043c3 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 13 Dec 2022 09:37:29 +0100 Subject: [PATCH 014/121] feat: add glossary management support --- CHANGELOG.md | 2 + README.md | 255 +++++++++++-- .../deepl/api/DocumentTranslationOptions.java | 17 + .../java/com/deepl/api/GlossaryEntries.java | 217 +++++++++++ .../main/java/com/deepl/api/GlossaryInfo.java | 96 +++++ .../com/deepl/api/GlossaryLanguagePair.java | 40 ++ .../deepl/api/GlossaryNotFoundException.java | 11 + .../java/com/deepl/api/HttpClientWrapper.java | 12 + .../com/deepl/api/TextTranslationOptions.java | 17 + .../main/java/com/deepl/api/Translator.java | 218 ++++++++++- .../parsing/GlossaryLanguagesResponse.java | 22 ++ .../api/parsing/GlossaryListResponse.java | 20 + .../java/com/deepl/api/parsing/Parser.java | 13 + .../test/java/com/deepl/api/GeneralTest.java | 11 + .../com/deepl/api/GlossaryCleanupUtility.java | 54 +++ .../test/java/com/deepl/api/GlossaryTest.java | 358 ++++++++++++++++++ 16 files changed, 1329 insertions(+), 34 deletions(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/GlossaryEntries.java create mode 100644 deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java create mode 100644 deepl-java/src/main/java/com/deepl/api/GlossaryLanguagePair.java create mode 100644 deepl-java/src/main/java/com/deepl/api/GlossaryNotFoundException.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/GlossaryLanguagesResponse.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/GlossaryListResponse.java create mode 100644 deepl-java/src/test/java/com/deepl/api/GlossaryCleanupUtility.java create mode 100644 deepl-java/src/test/java/com/deepl/api/GlossaryTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index da2a1f4..1c1019f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +* Add support for glossary management functions. ### Changed * `parsing.ErrorResponse` fields `message` and `detail` are now private, encapsulated with getters. diff --git a/README.md b/README.md index 34b0719..3b7dec7 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ developers: any translation product you can imagine can now be built on top of DeepL's best-in-class translation technology. The DeepL Java library offers a convenient way for applications written in Java -to interact with the DeepL API. Currently, the library only supports text and -document translation; we intend to add support for glossary management soon. +to interact with the DeepL API. We intend to support all API functions with the +library, though support for new features may be added to the library after +they’re added to the API. ## Getting an authentication key @@ -57,12 +58,14 @@ Be careful not to expose your key, for example when sharing source code. import com.deepl.api.*; class Example { - public String basicTranslationExample() throws Exception { + Translator translator; + + public Example() throws Exception { String authKey = "f63c02c5-f056-..."; // Replace with your key - Translator translator = new Translator(authKey); + translator = new Translator(authKey); TextResult result = translator.translateText("Hello, world!", null, "fr"); - return result.getText(); // "Bonjour, le monde !" + System.out.println(result.getText()); // "Bonjour, le monde !" } } ``` @@ -98,7 +101,7 @@ returns the translated text, and `getDetectedSourceLanguage()` returns the detected source language code. ```java -class Example { +class Example { // Continuing class Example from above public void textTranslationExamples() throws Exception { // Translate text into a target language, in this case, French: TextResult result = @@ -151,8 +154,11 @@ a `TextTranslationOptions`, with the following setters: [Listing available languages](#listing-available-languages). - `Formality.Less`: use informal language. - `Formality.More`: use formal, more polite language. -- `setGlossaryId()`: specifies a glossary to use with translation, as a string - containing the glossary ID. +- `setGlossary()`: specifies a glossary to use with translation, as a string + containing the glossary ID, or a `GlossaryInfo` object (this object is + returned by glossary lookup functions, for example `listGlossaries()`). + - `setGlossaryId()` is also available for backward-compatibility, accepting + a string containing the glossary ID. - `setTagHandling()`: type of tags to parse before translation, options are `"html"` and `"xml"`. @@ -184,7 +190,7 @@ There are additional optional arguments to control translation, see [Document translation options](#document-translation-options) below. ```java -class Example { +class Example { // Continuing class Example from above public void documentTranslationExamples() throws Exception { // Translate a formal document from English to German File inputFile = new File("/path/to/Instruction Manual.docx"); @@ -220,10 +226,184 @@ the following functions directly: #### Document translation options In addition to the input file, output file, `sourceLang` and `targetLang` -arguments, the available `translateDocument()` setters are: +arguments, `translateDocument()` accepts an optional +`DocumentTranslationOptions`, with the following setters: + +- `setFormality()`: same as + in [Text translation options](#text-translation-options). +- `setGlossary()`: same as + in [Text translation options](#text-translation-options). +- `setGlossaryId()`: same as + in [Text translation options](#text-translation-options). + +### Glossaries + +Glossaries allow you to customize your translations using user-defined terms. +Multiple glossaries can be stored with your account, each with a user-specified +name and a uniquely-assigned ID. + +#### Creating a glossary + +You can create a glossary with `createGlossary()` by passing your desired +glossary name, and a `GlossaryEntries` object specifying the terms to +store in the glossary. + +Each glossary applies to a single source-target language pair. Note: Glossaries +are only supported for some language pairs, see +[Listing available glossary languages](#listing-available-glossary-languages) +for more information. + +If successful, the glossary is created and stored with your DeepL account, and +a `GlossaryInfo` object is returned including the ID, name, languages and entry +count. + +```java +class Example { // Continuing class Example from above + public void createGlossaryExample() throws Exception { + // Create an English to German glossary with two terms: + GlossaryEntries entries = new GlossaryEntries() {{ + put("artist", "Maler"); + put("prize", "Gewinn"); + }}; + GlossaryInfo myGlossary = + translator.createGlossary("My glossary", "en", "de", entries); + + System.out.printf("Created '%s' (%s) %s->%s containing %d entries\n", + myGlossary.getName(), + myGlossary.getGlossaryId(), + myGlossary.getSourceLang(), + myGlossary.getTargetLang(), + myGlossary.getEntryCount()); + // Example: Created 'My glossary' (559192ed-8e23-...) en->de containing 2 entries + } +} +``` + +To construct the GlossaryEntries, you can insert entries using typical Map +functions like `put()`. The `fromTsv()` function allows creating GlossaryEntries +from TSV data. + +You can also create a glossary using a glossary downloaded from the DeepL +website by using `createGlossaryFromCsv()` with either a CSV file, or a string +containing the CSV data: + +```java +class Example { // Continuing class Example from above + public createGlossaryFromCsvExample() throws Exception { + File csvFile = new File("/path/to/glossary_file.csv"); + GlossaryInfo myGlossary = + translator.createGlossaryFromCsv("My glossary", + "en", + "de", + csvFile); + } +} +``` + +The [API documentation][api-docs-csv-format] explains the expected CSV format in +detail. + +#### Getting, listing, and deleting stored glossaries + +Functions to get, list, and delete stored glossaries are also provided: + +- `getGlossary()` takes a glossary ID and returns a `GlossaryInfo` object for a + stored glossary, or throws an exception if no such glossary is found. +- `listGlossaries()` returns a list of `GlossaryInfo` objects corresponding to + all of your stored glossaries. +- `deleteGlossary()` takes a glossary ID or `GlossaryInfo` object and deletes + the stored glossary from the server, or throws an exception if no such + glossary is found. + +```java +class Example { // Continuing class Example from above + public getListDeleteGlossaryExamples() throws Exception { + // Retrieve a stored glossary using the ID + String glossaryId = "559192ed-8e23-..."; + GlossaryInfo myGlossary = translator.getGlossary(glossaryId); + + // Find and delete glossaries named 'Old glossary' + List glossaries = translator.listGlossaries(); + for (GlossaryInfo glossary : glossaries) { + if (glossary.getName() == "Old glossary") { + translator.deleteGlossary(glossary); + } + } + } +} +``` + +#### Listing entries in a stored glossary + +The `GlossaryInfo` object does not contain the glossary entries, but instead +only the number of entries in the `entry_count` property. -- `setFormality()`: same as in [Text translation options](#text-translation-options). -- `setGlossaryId()`: same as in [Text translation options](#text-translation-options). +To list the entries contained within a stored glossary, use +`getGlossaryEntries()` providing either the `GlossaryInfo` object or glossary +ID: + +```java +class Example { // Continuing class Example from above + public getGlossaryEntriesExample() throws Exception { + GlossaryEntries entries = translator.getGlossaryEntries(myGlossary); + + for (Map.Entry entry : entries.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + // prints: + // artist:Maler + // prize:Gewinn + } +} +``` + +#### Using a stored glossary + +You can use a stored glossary for text translation by setting the `glossary` +argument to either the glossary ID or `GlossaryInfo` object. You must also +specify the `source_lang` argument (it is required when using a glossary): + +```java +class Example { // Continuing class Example from above + public usingGlossaryExample() throws Exception { + String text = "The artist was awarded a prize."; + TextTranslationOptions options = + new TextTranslationOptions().setGlossary(my_glossary); + TextResult resultWithGlossary = + translator.translateText(text, "en", "de", options); + System.out.println(resultWithGlossary.getText()); // "Der Maler wurde mit einem Gewinn ausgezeichnet." + + // For comparison, the result without a glossary: + TextResult resultWithoutGlossary = + translator.translateText(text, "en", "de"); + System.out.println(resultWithoutGlossary.getText()); // "Der Künstler wurde mit einem Preis ausgezeichnet." + } +} +``` + +Using a stored glossary for document translation is the same: set the `glossary` +argument and specify the `source_lang` argument: + +```java +class Example { // Continuing class Example from above + public getListDeleteGlossaryExamples() throws Exception { + String glossaryId = "559192ed-8e23-..."; + DocumentTranslationOptions options = + new DocumentTranslationOptions().setGlossary(glossaryId); + + File inputFile = new File("/path/to/Instruction Manual.docx"); + File outputFile = new File("/path/to/Bedienungsanleitung.docx"); + translator.translateDocument(inputFile, + outputFile, + "en", + "de", + options); + } +} +``` + +The `translateDocument()` and `translateDocumentUpload()` functions both +support the `glossary` argument. ### Checking account usage @@ -242,7 +422,7 @@ that checks if the usage has reached the limit. The top level `Usage` object has the `any_limit_reached` property to check all usage subtypes. ```java -class Example { +class Example { // Continuing class Example from above public void getUsageExample() throws Exception { Usage usage = translator.getUsage(); if (usage.anyLimitReached()) { @@ -274,15 +454,15 @@ for target languages, and indicates whether the target language supports the optional `formality` parameter. ```java -class Example { +class Example { // Continuing class Example from above public void getLanguagesExample() throws Exception { List sourceLanguages = translator.getSourceLanguages(); List targetLanguages = translator.getTargetLanguages(); System.out.println("Source languages:"); for (Language language : sourceLanguages) { - System.out.printf("%s (%s)%n", - language.getName(), - language.getCode()); // Example: "German (de)" + System.out.printf("%s (%s)%n", + language.getName(), + language.getCode()); // Example: "German (de)" } System.out.println("Target languages:"); @@ -292,16 +472,45 @@ class Example { language.getName(), language.getCode()); // Example: "Italian (it) supports formality" - } else { + } else { System.out.printf("%s (%s)%n", language.getName(), language.getCode()); // Example: "Lithuanian (lt)" - } + } } } } ``` +#### Listing available glossary languages + +Glossaries are supported for a subset of language pairs. To retrieve those +languages use the `getGlossaryLanguages()` function, which returns an array +of `GlossaryLanguagePair` objects. Use the `getSourceLanguage()` and +`getTargetLanguage()` functions to check the pair of language codes supported. + +```java +class Example { // Continuing class Example from above + public void getGlossaryLanguagesExample() throws Exception { + List glossaryLanguages = + translator.getGlossaryLanguages(); + for (GlossaryLanguagePair glossaryLanguage : glossaryLanguages) { + System.out.printf("%s to %s\n", + glossaryLanguage.getSourceLanguage(), + glossaryLanguage.getTargetLanguage()); + // Example: "en to de", "de to en", etc. + } + } +} +``` + +You can also find the list of supported glossary language pairs in the +[API documentation][api-docs-glossary-lang-list]. + +Note that glossaries work for all target regional-variants: a glossary for the +target language English (`"en"`) supports translations to both American English +(`"en-US"`) and British English (`"en-GB"`). + ### Exceptions All module functions may raise `DeepLException` or one of its subclasses. If @@ -314,7 +523,7 @@ The `Translator` constructor accepts `TranslatorOptions` as a second argument, for example: ```java -class Example { +class Example { // Continuing class Example from above public void configurationExample() throws Exception { TranslatorOptions options = new TranslatorOptions().setMaxRetries(1).setTimeout(Duration.ofSeconds( @@ -329,9 +538,9 @@ The available options setters are: - `setMaxRetries()`: maximum number of failed HTTP requests to retry, the default is 5. Note: only failures due to transient conditions are retried e.g. timeouts or temporary server overload. -- `setTimeout()`: connection timeout for each HTTP request. +- `setTimeout()`: connection timeout for each HTTP request. - `setProxy()`: provide details about a proxy to use for all HTTP requests to - DeepL. + DeepL. - `setHeaders()`: additional HTTP headers to attach to all requests. - `setServerUrl()`: base URL for DeepL API, may be overridden for testing purposes. By default, the correct DeepL API (Free or Pro) is automatically @@ -366,6 +575,8 @@ tests using `./gradlew test` with the `DEEPL_MOCK_SERVER_PORT` and [api-docs]: https://www.deepl.com/docs-api?utm_source=github&utm_medium=github-java-readme +[api-docs-csv-format]: https://www.deepl.com/docs-api/managing-glossaries/supported-glossary-formats/?utm_source=github&utm_medium=github-java-readme + [api-docs-xml-handling]: https://www.deepl.com/docs-api/handling-xml/?utm_source=github&utm_medium=github-java-readme [api-docs-lang-list]: https://www.deepl.com/docs-api/translating-text/?utm_source=github&utm_medium=github-java-readme diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java index ac5d6a6..c143a8a 100644 --- a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java @@ -39,6 +39,23 @@ public DocumentTranslationOptions setGlossaryId(String glossaryId) { return this; } + /** + * Sets the glossary to use with the translation. By default, this value is null and + * no glossary is used. + */ + public DocumentTranslationOptions setGlossary(GlossaryInfo glossary) { + return setGlossary(glossary.getGlossaryId()); + } + + /** + * Sets the glossary to use with the translation. By default, this value is null and + * no glossary is used. + */ + public DocumentTranslationOptions setGlossary(String glossaryId) { + this.glossaryId = glossaryId; + return this; + } + /** Gets the current formality setting. */ public Formality getFormality() { return formality; diff --git a/deepl-java/src/main/java/com/deepl/api/GlossaryEntries.java b/deepl-java/src/main/java/com/deepl/api/GlossaryEntries.java new file mode 100644 index 0000000..1acae38 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/GlossaryEntries.java @@ -0,0 +1,217 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.deepl.api.utils.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** Stores the entries of a glossary. */ +public class GlossaryEntries implements Map { + private final Map entries = new HashMap<>(); + + /** Construct an empty GlossaryEntries. */ + public GlossaryEntries() {} + + /** Initializes a new GlossaryEntries with the entry pairs in the given map. */ + public GlossaryEntries(Map entryPairs) { + this.putAll(entryPairs); + } + + /** + * Converts the given tab-separated-value (TSV) string of glossary entries into a new + * GlossaryEntries object. Whitespace is trimmed from the start and end of each term. + */ + public static GlossaryEntries fromTsv(String tsv) { + GlossaryEntries result = new GlossaryEntries(); + String[] lines = tsv.split("(\\r\\n|\\n|\\r)"); + int lineNumber = 0; + for (String line : lines) { + ++lineNumber; + String lineTrimmed = trimWhitespace(line); + if (lineTrimmed.isEmpty()) { + continue; + } + String[] splitLine = lineTrimmed.split("\\t"); + if (splitLine.length < 2) { + throw new IllegalArgumentException( + String.format( + "Entry on line %d does not contain a term separator: %s", lineNumber, lineTrimmed)); + } else if (splitLine.length > 2) { + throw new IllegalArgumentException( + String.format( + "Entry on line %d contains more than one term separator: %s", lineNumber, line)); + } else { + String sourceTerm = trimWhitespace(splitLine[0]); + String targetTerm = trimWhitespace(splitLine[1]); + validateGlossaryTerm(sourceTerm); + validateGlossaryTerm(targetTerm); + if (result.containsKey(sourceTerm)) { + throw new IllegalArgumentException( + String.format( + "Entry on line %d duplicates source term '%s'", lineNumber, sourceTerm)); + } + result.put(sourceTerm, targetTerm); + } + } + + if (result.entries.isEmpty()) { + throw new IllegalArgumentException("TSV string contains no valid entries"); + } + + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + GlossaryEntries glossaryEntries = (GlossaryEntries) o; + return glossaryEntries.entries.equals(entries); + } + + @Override + public int size() { + return entries.size(); + } + + @Override + public boolean isEmpty() { + return entries.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return entries.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return entries.containsValue(value); + } + + @Override + public String get(Object key) { + return entries.get(key); + } + + /** + * Adds the given source term and target term to the glossary entries. + * + * @param sourceTerm key with which the specified value is to be associated + * @param targetTerm value to be associated with the specified key + * @return The previous target term associated with this source term, or null if this source term + * was not present. + */ + public String put(String sourceTerm, String targetTerm) throws IllegalArgumentException { + validateGlossaryTerm(sourceTerm); + validateGlossaryTerm(targetTerm); + return entries.put(sourceTerm, targetTerm); + } + + @Override + public String remove(Object key) { + return entries.remove(key); + } + + @Override + public void putAll(@NotNull Map m) { + for (Map.Entry entryPair : m.entrySet()) { + put(entryPair.getKey(), entryPair.getValue()); + } + } + + @Override + public void clear() { + entries.clear(); + } + + @NotNull + @Override + public Set keySet() { + return entries.keySet(); + } + + @NotNull + @Override + public Collection values() { + return entries.values(); + } + + @NotNull + @Override + public Set> entrySet() { + return entries.entrySet(); + } + + /** + * Checks the validity of the given glossary term, for example that it contains no invalid + * characters. Whitespace at the start and end of the term is ignored. Terms are considered valid + * if they comprise at least one non-whitespace character, and contain no invalid characters: C0 + * and C1 control characters, and Unicode newlines. + * + * @param term String containing term to check. + */ + public static void validateGlossaryTerm(String term) throws IllegalArgumentException { + String termTrimmed = trimWhitespace(term); + if (termTrimmed.isEmpty()) { + throw new IllegalArgumentException( + String.format("Term '%s' contains no non-whitespace characters", term)); + } + for (int i = 0; i < termTrimmed.length(); ++i) { + char ch = termTrimmed.charAt(i); + if ((ch <= 31) || (128 <= ch && ch <= 159) || ch == '\u2028' || ch == '\u2029') { + throw new IllegalArgumentException( + String.format( + "Term '%s' contains invalid character: '%c' (U+%04d)", term, ch, (int) ch)); + } + } + } + + /** + * Converts the glossary entries to a string containing the entries in tab-separated-value (TSV) + * format. + * + * @return String containing the entries in TSV format. + */ + public String toTsv() { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entryPair : entries.entrySet()) { + if (builder.length() > 0) { + builder.append("\n"); + } + builder.append(entryPair.getKey()).append("\t").append(entryPair.getValue()); + } + return builder.toString(); + } + + /** + * Strips whitespace characters from the beginning and end of the given string. Implemented here + * because String.strip() is not available in Java 8. + * + * @param input String to have whitespace trimmed. + * @return Input string with whitespace removed from ends. + */ + private static String trimWhitespace(String input) { + int left = 0; + for (; left < input.length(); left++) { + char ch = input.charAt(left); + if (ch != ' ' && ch != '\t') { + break; + } + } + if (left >= input.length()) { + return ""; + } + int right = input.length() - 1; + for (; left < right; right--) { + char ch = input.charAt(right); + if (ch != ' ' && ch != '\t') { + break; + } + } + return input.substring(left, right + 1); + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java b/deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java new file mode 100644 index 0000000..15022b9 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java @@ -0,0 +1,96 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** Information about a glossary, excluding the entry list. */ +public class GlossaryInfo { + + @SerializedName(value = "glossary_id") + private final String glossaryId; + + @SerializedName(value = "name") + private final String name; + + @SerializedName(value = "ready") + private final boolean ready; + + @SerializedName(value = "source_lang") + private final String sourceLang; + + @SerializedName(value = "target_lang") + private final String targetLang; + + @SerializedName(value = "creation_time") + private final Date creationTime; + + @SerializedName(value = "entry_count") + private final long entryCount; + + /** + * Initializes a new {@link GlossaryInfo} containing information about a glossary. + * + * @param glossaryId ID of the associated glossary. + * @param name Name of the glossary chosen during creation. + * @param ready true if the glossary may be used for translations, otherwise false. + * @param sourceLang Language code of the source terms in the glossary. + * @param targetLang Language code of the target terms in the glossary. + * @param creationTime Time when the glossary was created. + * @param entryCount The number of source-target entry pairs in the glossary. + */ + public GlossaryInfo( + String glossaryId, + String name, + boolean ready, + String sourceLang, + String targetLang, + Date creationTime, + long entryCount) { + this.glossaryId = glossaryId; + this.name = name; + this.ready = ready; + this.sourceLang = sourceLang; + this.targetLang = targetLang; + this.creationTime = creationTime; + this.entryCount = entryCount; + } + + /** @return Unique ID assigned to the glossary. */ + public String getGlossaryId() { + return glossaryId; + } + + /** @return User-defined name assigned to the glossary. */ + public String getName() { + return name; + } + + /** @return True if the glossary may be used for translations, otherwise false. */ + public boolean isReady() { + return ready; + } + + /** @return Source language code of the glossary. */ + public String getSourceLang() { + return sourceLang; + } + + /** @return Target language code of the glossary. */ + public String getTargetLang() { + return targetLang; + } + + /** @return Timestamp when the glossary was created. */ + public Date getCreationTime() { + return creationTime; + } + + /** @return The number of entries contained in the glossary. */ + public long getEntryCount() { + return entryCount; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/GlossaryLanguagePair.java b/deepl-java/src/main/java/com/deepl/api/GlossaryLanguagePair.java new file mode 100644 index 0000000..1d818e4 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/GlossaryLanguagePair.java @@ -0,0 +1,40 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; + +/** + * Information about a language pair supported for glossaries. + * + * @see Translator#getGlossaryLanguages() + */ +public class GlossaryLanguagePair { + @SerializedName("source_lang") + private final String sourceLang; + + @SerializedName("target_lang") + private final String targetLang; + + /** + * Initializes a new GlossaryLanguagePair object. + * + * @param sourceLang Language code of the source terms in the glossary. + * @param targetLang Language code of the target terms in the glossary. + */ + public GlossaryLanguagePair(String sourceLang, String targetLang) { + this.sourceLang = LanguageCode.standardize(sourceLang); + this.targetLang = LanguageCode.standardize(targetLang); + } + + /** @return Language code of the source terms in the glossary. */ + public String getSourceLanguage() { + return sourceLang; + } + + /** @return Language code of the target terms in the glossary. */ + public String getTargetLanguage() { + return targetLang; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/GlossaryNotFoundException.java b/deepl-java/src/main/java/com/deepl/api/GlossaryNotFoundException.java new file mode 100644 index 0000000..6700ec9 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/GlossaryNotFoundException.java @@ -0,0 +1,11 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** Exception thrown when the specified glossary could not be found. */ +public class GlossaryNotFoundException extends NotFoundException { + public GlossaryNotFoundException(String message) { + super(message); + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java index 3d67c93..b589be0 100644 --- a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java +++ b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java @@ -18,7 +18,9 @@ */ class HttpClientWrapper { private static final String CONTENT_TYPE = "Content-Type"; + private static final String GET = "GET"; private static final String POST = "POST"; + private static final String DELETE = "DELETE"; private final String serverUrl; private final Map headers; private final Duration minTimeout; @@ -38,6 +40,16 @@ public HttpClientWrapper( this.maxRetries = maxRetries; } + public HttpResponse sendGetRequestWithBackoff(String relativeUrl) + throws InterruptedException, DeepLException { + return sendRequestWithBackoff(GET, relativeUrl, null).toStringResponse(); + } + + public HttpResponse sendDeleteRequestWithBackoff(String relativeUrl) + throws InterruptedException, DeepLException { + return sendRequestWithBackoff(DELETE, relativeUrl, null).toStringResponse(); + } + public HttpResponse sendRequestWithBackoff(String relativeUrl) throws InterruptedException, DeepLException { return sendRequestWithBackoff(POST, relativeUrl, null).toStringResponse(); diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index 0f721df..abe2dd2 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -46,6 +46,23 @@ public TextTranslationOptions setGlossaryId(String glossaryId) { return this; } + /** + * Sets the glossary to use with the translation. By default, this value is null and + * no glossary is used. + */ + public TextTranslationOptions setGlossary(GlossaryInfo glossary) { + return setGlossary(glossary.getGlossaryId()); + } + + /** + * Sets the glossary to use with the translation. By default, this value is null and + * no glossary is used. + */ + public TextTranslationOptions setGlossary(String glossaryId) { + this.glossaryId = glossaryId; + return this; + } + /** * Specifies how input translation text should be split into sentences. By default, this value is * null and the default sentence splitting mode is used. diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index dadb2ba..cad7db9 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -172,7 +172,7 @@ public List translateText( Iterable> params = createHttpParams(texts, sourceLang, targetLang, options); HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/translate", params); - checkResponse(response, false); + checkResponse(response, false, false); return jsonParser.parseTextResult(response.getBody()); } @@ -228,7 +228,7 @@ public List translateText( */ public Usage getUsage() throws DeepLException, InterruptedException { HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/usage"); - checkResponse(response, false); + checkResponse(response, false, false); return jsonParser.parseUsage(response.getBody()); } @@ -272,10 +272,27 @@ public List getLanguages(LanguageType languageType) params.add(new KeyValuePair<>("type", "target")); } HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/languages", params); - checkResponse(response, false); + checkResponse(response, false, false); return jsonParser.parseLanguages(response.getBody()); } + /** + * Retrieves the list of supported glossary language pairs. When creating glossaries, the source + * and target language pair must match one of the available language pairs. + * + * @return List of {@link GlossaryLanguagePair} objects representing the available glossary + * language pairs. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public List getGlossaryLanguages() + throws DeepLException, InterruptedException { + HttpResponse response = + httpClientWrapper.sendGetRequestWithBackoff("/v2/glossary-language-pairs"); + checkResponse(response, false, false); + return jsonParser.parseGlossaryLanguageList(response.getBody()); + } + /** * Translate specified document content from source language to target language and store the * translated document content to specified stream. @@ -411,7 +428,7 @@ public DocumentHandle translateDocumentUpload( HttpResponse response = httpClientWrapper.uploadWithBackoff( "/v2/document/", params, inputFile.getName(), inputStream); - checkResponse(response, false); + checkResponse(response, false, false); return jsonParser.parseDocumentHandle(response.getBody()); } } @@ -455,7 +472,7 @@ public DocumentHandle translateDocumentUpload( createHttpParams(sourceLang, targetLang, options); HttpResponse response = httpClientWrapper.uploadWithBackoff("/v2/document/", params, fileName, inputStream); - checkResponse(response, false); + checkResponse(response, false, false); return jsonParser.parseDocumentHandle(response.getBody()); } @@ -486,7 +503,7 @@ public DocumentStatus translateDocumentStatus(DocumentHandle handle) params.add(new KeyValuePair<>("document_key", handle.getDocumentKey())); String relativeUrl = String.format("/v2/document/%s", handle.getDocumentId()); HttpResponse response = httpClientWrapper.sendRequestWithBackoff(relativeUrl, params); - checkResponse(response, false); + checkResponse(response, false, false); return jsonParser.parseDocumentStatus(response.getBody()); } @@ -566,6 +583,158 @@ public void translateDocumentDownload(DocumentHandle handle, OutputStream output } } + /** + * Creates a glossary in your DeepL account with the specified details and returns a {@link + * GlossaryInfo} object with details about the newly created glossary. The glossary can be used in + * translations to override translations for specific terms (words). The glossary source and + * target languages must match the languages of translations for which it will be used. + * + * @param name User-defined name to assign to the glossary; must not be empty. + * @param sourceLang Language code of the source terms language. + * @param targetLang Language code of the target terms language. + * @param entries Glossary entries to add to the glossary. + * @return {@link GlossaryInfo} object with details about the newly created glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public GlossaryInfo createGlossary( + String name, String sourceLang, String targetLang, GlossaryEntries entries) + throws DeepLException, InterruptedException { + return createGlossaryInternal(name, sourceLang, targetLang, "tsv", entries.toTsv()); + } + + /** + * Creates a glossary in your DeepL account with the specified details and returns a {@link + * GlossaryInfo} object with details about the newly created glossary. The glossary can be used in + * translations to override translations for specific terms (words). The glossary source and + * target languages must match the languages of translations for which it will be used. + * + * @param name User-defined name to assign to the glossary; must not be empty. + * @param sourceLang Language code of the source terms language. + * @param targetLang Language code of the target terms language. + * @param csvFile File containing CSV content for glossary. + * @return {@link GlossaryInfo} object with details about the newly created glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + * @throws IOException If an I/O error occurs. + */ + public GlossaryInfo createGlossaryFromCsv( + String name, String sourceLang, String targetLang, File csvFile) + throws DeepLException, InterruptedException, IOException { + try (FileInputStream stream = new FileInputStream(csvFile)) { + String csvContent = StreamUtil.readStream(stream); + return createGlossaryFromCsv(name, sourceLang, targetLang, csvContent); + } + } + + /** + * Creates a glossary in your DeepL account with the specified details and returns a {@link + * GlossaryInfo} object with details about the newly created glossary. The glossary can be used in + * translations to override translations for specific terms (words). The glossary source and + * target languages must match the languages of translations for which it will be used. + * + * @param name User-defined name to assign to the glossary; must not be empty. + * @param sourceLang Language code of the source terms language. + * @param targetLang Language code of the target terms language. + * @param csvContent String containing CSV content. + * @return {@link GlossaryInfo} object with details about the newly created glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public GlossaryInfo createGlossaryFromCsv( + String name, String sourceLang, String targetLang, String csvContent) + throws DeepLException, InterruptedException { + return createGlossaryInternal(name, sourceLang, targetLang, "csv", csvContent); + } + + /** + * Retrieves information about the glossary with the specified ID and returns a {@link + * GlossaryInfo} object containing details. This does not retrieve the glossary entries; to + * retrieve entries use {@link Translator#getGlossaryEntries(String)} + * + * @param glossaryId ID of glossary to retrieve. + * @return {@link GlossaryInfo} object with details about the specified glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public GlossaryInfo getGlossary(String glossaryId) throws DeepLException, InterruptedException { + String relativeUrl = String.format("/v2/glossaries/%s", glossaryId); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, true); + return jsonParser.parseGlossaryInfo(response.getBody()); + } + + /** + * Retrieves information about all glossaries and returns an array of {@link GlossaryInfo} objects + * containing details. This does not retrieve the glossary entries; to retrieve entries use {@link + * Translator#getGlossaryEntries(String)} + * + * @return Array of {@link GlossaryInfo} objects with details about each glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public List listGlossaries() throws DeepLException, InterruptedException { + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff("/v2/glossaries"); + checkResponse(response, false, false); + return jsonParser.parseGlossaryInfoList(response.getBody()); + } + + /** + * Retrieves the entries containing within the glossary and returns them as a {@link + * GlossaryEntries}. + * + * @param glossary {@link GlossaryInfo} object corresponding to glossary for which to retrieve + * entries. + * @return {@link GlossaryEntries} containing entry pairs of the glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public GlossaryEntries getGlossaryEntries(GlossaryInfo glossary) + throws DeepLException, InterruptedException { + return getGlossaryEntries(glossary.getGlossaryId()); + } + + /** + * Retrieves the entries containing within the glossary with the specified ID and returns them as + * a {@link GlossaryEntries}. + * + * @param glossaryId ID of glossary for which to retrieve entries. + * @return {@link GlossaryEntries} containing entry pairs of the glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public GlossaryEntries getGlossaryEntries(String glossaryId) + throws DeepLException, InterruptedException { + String relativeUrl = String.format("/v2/glossaries/%s/entries", glossaryId); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, true); + return GlossaryEntries.fromTsv(response.getBody()); + } + + /** + * Deletes the specified glossary. + * + * @param glossary {@link GlossaryInfo} object corresponding to glossary to delete. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public void deleteGlossary(GlossaryInfo glossary) throws DeepLException, InterruptedException { + deleteGlossary(glossary.getGlossaryId()); + } + + /** + * Deletes the glossary with the specified ID. + * + * @param glossaryId ID of glossary to delete. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public void deleteGlossary(String glossaryId) throws DeepLException, InterruptedException { + String relativeUrl = String.format("/v2/glossaries/%s", glossaryId); + HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); + checkResponse(response, false, true); + } + /** * Checks the specified texts, languages and options are valid, and returns an iterable of * containing the parameters to include in HTTP request. @@ -697,6 +866,9 @@ private static ArrayList> createHttpParamsCommon( } if (glossaryId != null) { + if (sourceLang == null) { + throw new IllegalArgumentException("sourceLang is required if using a glossary"); + } params.add(new KeyValuePair<>("glossary_id", glossaryId)); } @@ -736,12 +908,27 @@ private static void checkValidLanguages(@Nullable String sourceLang, String targ } } + /** Creates a glossary with given details. */ + private GlossaryInfo createGlossaryInternal( + String name, String sourceLang, String targetLang, String entriesFormat, String entries) + throws DeepLException, InterruptedException { + ArrayList> params = new ArrayList<>(); + params.add(new KeyValuePair<>("name", name)); + params.add(new KeyValuePair<>("source_lang", sourceLang)); + params.add(new KeyValuePair<>("target_lang", targetLang)); + params.add(new KeyValuePair<>("entries_format", entriesFormat)); + params.add(new KeyValuePair<>("entries", entries)); + HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/glossaries", params); + checkResponse(response, false, false); + return jsonParser.parseGlossaryInfo(response.getBody()); + } + /** - * Functions the same as {@link Translator#checkResponse(HttpResponse, boolean)} but accepts - * response stream for document downloads. If the HTTP status code represents failure, the + * Functions the same as {@link Translator#checkResponse(HttpResponse, boolean, boolean)} but + * accepts response stream for document downloads. If the HTTP status code represents failure, the * response stream is converted to a String response to throw the appropriate exception. * - * @see Translator#checkResponse(HttpResponse, boolean) + * @see Translator#checkResponse(HttpResponse, boolean, boolean) */ private void checkResponse(HttpResponseStream response) throws DeepLException { if (response.getCode() >= HttpURLConnection.HTTP_OK @@ -751,17 +938,20 @@ private void checkResponse(HttpResponseStream response) throws DeepLException { if (response.getBody() == null) { throw new DeepLException("response stream is empty"); } - checkResponse(response.toStringResponse(), true); + checkResponse(response.toStringResponse(), true, false); } /** * Checks the response HTTP status is OK, otherwise throws corresponding exception. * * @param response Response received from DeepL API. + * @param inDocumentDownload True if document download function is used, otherwise false. + * @param usingGlossary True if a glossary function is used, otherwise false. * @throws DeepLException Throws {@link DeepLException} or a derived exception depending on the * type of error. */ - private void checkResponse(HttpResponse response, boolean inDocumentDownload) + private void checkResponse( + HttpResponse response, boolean inDocumentDownload, boolean usingGlossary) throws DeepLException { if (response.getCode() >= 200 && response.getCode() < 300) { return; @@ -783,7 +973,11 @@ private void checkResponse(HttpResponse response, boolean inDocumentDownload) case HttpURLConnection.HTTP_FORBIDDEN: throw new AuthorizationException("Authorization failure, check auth_key" + messageSuffix); case HttpURLConnection.HTTP_NOT_FOUND: - throw new NotFoundException("Not found, check serverUrl" + messageSuffix); + if (usingGlossary) { + throw new GlossaryNotFoundException("Glossary not found" + messageSuffix); + } else { + throw new NotFoundException("Not found, check serverUrl" + messageSuffix); + } case 429: throw new TooManyRequestsException( "Too many requests, DeepL servers are currently experiencing high load" diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/GlossaryLanguagesResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/GlossaryLanguagesResponse.java new file mode 100644 index 0000000..7a62744 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/GlossaryLanguagesResponse.java @@ -0,0 +1,22 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.*; +import com.google.gson.annotations.*; +import java.util.List; + +/** + * Class representing glossary-languages response from the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class GlossaryLanguagesResponse { + @SerializedName("supported_languages") + private List supportedLanguages; + + public List getSupportedLanguages() { + return supportedLanguages; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/GlossaryListResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/GlossaryListResponse.java new file mode 100644 index 0000000..dd1fbc4 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/GlossaryListResponse.java @@ -0,0 +1,20 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.*; +import java.util.List; + +/** + * Class representing list-glossaries response by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class GlossaryListResponse { + private List glossaries; + + public List getGlossaries() { + return glossaries; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index 08294c8..ffd7814 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -40,6 +40,10 @@ public List parseLanguages(String json) { return gson.fromJson(json, languageListType); } + public List parseGlossaryLanguageList(String json) { + return gson.fromJson(json, GlossaryLanguagesResponse.class).getSupportedLanguages(); + } + public DocumentStatus parseDocumentStatus(String json) { return gson.fromJson(json, DocumentStatus.class); } @@ -48,6 +52,15 @@ public DocumentHandle parseDocumentHandle(String json) { return gson.fromJson(json, DocumentHandle.class); } + public GlossaryInfo parseGlossaryInfo(String json) { + return gson.fromJson(json, GlossaryInfo.class); + } + + public List parseGlossaryInfoList(String json) { + GlossaryListResponse result = gson.fromJson(json, GlossaryListResponse.class); + return result.getGlossaries(); + } + public String parseErrorMessage(String json) { ErrorResponse response = gson.fromJson(json, ErrorResponse.class); diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 9931ed4..12718d5 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -82,6 +82,17 @@ void testGetSourceAndTargetLanguages() throws DeepLException, InterruptedExcepti Assertions.assertTrue(targetLanguages.size() > 20); } + @Test + void testGetGlossaryLanguages() throws DeepLException, InterruptedException { + Translator translator = createTranslator(); + List glossaryLanguagePairs = translator.getGlossaryLanguages(); + Assertions.assertTrue(glossaryLanguagePairs.size() > 0); + for (GlossaryLanguagePair glossaryLanguagePair : glossaryLanguagePairs) { + Assertions.assertTrue(glossaryLanguagePair.getSourceLanguage().length() > 0); + Assertions.assertTrue(glossaryLanguagePair.getTargetLanguage().length() > 0); + } + } + @Test void testAuthKeyIsFreeAccount() { Assertions.assertTrue( diff --git a/deepl-java/src/test/java/com/deepl/api/GlossaryCleanupUtility.java b/deepl-java/src/test/java/com/deepl/api/GlossaryCleanupUtility.java new file mode 100644 index 0000000..160cb89 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/GlossaryCleanupUtility.java @@ -0,0 +1,54 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import java.util.*; + +public class GlossaryCleanupUtility implements AutoCloseable { + private final String glossaryName; + private final Translator translator; + + public GlossaryCleanupUtility(Translator translator) { + this(translator, ""); + } + + public GlossaryCleanupUtility(Translator translator, String testNameSuffix) { + String callingFunc = getCallerFunction(); + String uuid = UUID.randomUUID().toString(); + + this.glossaryName = + String.format("deepl-java-test-glossary: %s%s %s", callingFunc, testNameSuffix, uuid); + this.translator = translator; + } + + public String getGlossaryName() { + return glossaryName; + } + + @Override + public void close() throws Exception { + List glossaries = translator.listGlossaries(); + for (GlossaryInfo glossary : glossaries) { + if (Objects.equals(glossary.getName(), glossaryName)) { + try { + translator.deleteGlossary(glossary); + } catch (Exception exception) { + // Ignore + } + } + } + } + + private static String getCallerFunction() { + StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace(); + // Find the first function outside this class following functions in this class + for (int i = 1; i < stacktrace.length; i++) { + if (!stacktrace[i].getClassName().equals(GlossaryCleanupUtility.class.getName()) + && stacktrace[i - 1].getClassName().equals(GlossaryCleanupUtility.class.getName())) { + return stacktrace[i].getMethodName(); + } + } + return "unknown"; + } +} diff --git a/deepl-java/src/test/java/com/deepl/api/GlossaryTest.java b/deepl-java/src/test/java/com/deepl/api/GlossaryTest.java new file mode 100644 index 0000000..906399b --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/GlossaryTest.java @@ -0,0 +1,358 @@ +// Copyright 2022 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.deepl.api.utils.*; +import java.io.*; +import java.util.*; +import org.junit.jupiter.api.*; + +public class GlossaryTest extends TestBase { + private final String invalidGlossaryId = "invalid_glossary_id"; + private final String nonexistentGlossaryId = "96ab91fd-e715-41a1-adeb-5d701f84a483"; + private final String sourceLang = "en"; + private final String targetLang = "de"; + + private final GlossaryEntries testEntries = GlossaryEntries.fromTsv("Hello\tHallo"); + + @Test + void TestGlossaryEntries() { + GlossaryEntries testEntries = new GlossaryEntries(); + testEntries.put("apple", "Apfel"); + testEntries.put("crab apple", "Holzapfel"); + Assertions.assertEquals( + testEntries, GlossaryEntries.fromTsv("apple\tApfel\n crab apple \t Holzapfel ")); + Assertions.assertThrows(Exception.class, () -> GlossaryEntries.fromTsv("")); + Assertions.assertThrows( + Exception.class, () -> GlossaryEntries.fromTsv("Küche\tKitchen\nKüche\tCuisine")); + Assertions.assertThrows(Exception.class, () -> GlossaryEntries.fromTsv("A\tB\tC")); + Assertions.assertThrows(Exception.class, () -> GlossaryEntries.fromTsv("A\t ")); + + Assertions.assertThrows( + Exception.class, () -> new GlossaryEntries(Collections.singletonMap("A", "B\tC"))); + } + + @Test + void testGlossaryCreate() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + GlossaryEntries entries = new GlossaryEntries(Collections.singletonMap("Hello", "Hallo")); + System.out.println(entries); + for (Map.Entry entry : entries.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + String glossaryName = cleanup.getGlossaryName(); + GlossaryInfo glossary = + translator.createGlossary(glossaryName, sourceLang, targetLang, entries); + + Assertions.assertEquals(glossaryName, glossary.getName()); + Assertions.assertEquals(sourceLang, glossary.getSourceLang()); + Assertions.assertEquals(targetLang, glossary.getTargetLang()); + Assertions.assertEquals(1, glossary.getEntryCount()); + + GlossaryInfo getResult = translator.getGlossary(glossary.getGlossaryId()); + Assertions.assertEquals(getResult.getName(), glossary.getName()); + Assertions.assertEquals(getResult.getSourceLang(), glossary.getSourceLang()); + Assertions.assertEquals(getResult.getTargetLang(), glossary.getTargetLang()); + Assertions.assertEquals(getResult.getCreationTime(), glossary.getCreationTime()); + Assertions.assertEquals(getResult.getEntryCount(), glossary.getEntryCount()); + } + } + + @Test + void testGlossaryCreateLarge() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + + Map entryPairs = new HashMap<>(); + for (int i = 0; i < 10000; i++) { + entryPairs.put(String.format("Source-%d", i), String.format("Target-%d", i)); + } + GlossaryEntries entries = new GlossaryEntries(entryPairs); + Assertions.assertTrue(entries.toTsv().length() > 100000); + GlossaryInfo glossary = + translator.createGlossary(glossaryName, sourceLang, targetLang, entries); + + Assertions.assertEquals(glossaryName, glossary.getName()); + Assertions.assertEquals(sourceLang, glossary.getSourceLang()); + Assertions.assertEquals(targetLang, glossary.getTargetLang()); + Assertions.assertEquals(entryPairs.size(), glossary.getEntryCount()); + } + } + + @Test + void testGlossaryCreateCsv() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + Map expectedEntries = new HashMap<>(); + expectedEntries.put("sourceEntry1", "targetEntry1"); + expectedEntries.put("source\"Entry", "target,Entry"); + + String csvContent = + "sourceEntry1,targetEntry1,en,de\n\"source\"\"Entry\",\"target,Entry\",en,de"; + + GlossaryInfo glossary = + translator.createGlossaryFromCsv(glossaryName, sourceLang, targetLang, csvContent); + + GlossaryEntries entries = translator.getGlossaryEntries(glossary); + Assertions.assertEquals(expectedEntries, entries); + } + } + + @Test + void testGlossaryCreateInvalid() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + Assertions.assertThrows( + Exception.class, + () -> translator.createGlossary("", sourceLang, targetLang, testEntries)); + Assertions.assertThrows( + Exception.class, () -> translator.createGlossary(glossaryName, "en", "xx", testEntries)); + } + } + + @Test + void testGlossaryGet() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + GlossaryInfo createdGlossary = + translator.createGlossary(glossaryName, sourceLang, targetLang, testEntries); + + GlossaryInfo glossary = translator.getGlossary(createdGlossary.getGlossaryId()); + Assertions.assertEquals(createdGlossary.getGlossaryId(), glossary.getGlossaryId()); + Assertions.assertEquals(glossaryName, glossary.getName()); + Assertions.assertEquals(sourceLang, glossary.getSourceLang()); + Assertions.assertEquals(targetLang, glossary.getTargetLang()); + Assertions.assertEquals(createdGlossary.getCreationTime(), glossary.getCreationTime()); + Assertions.assertEquals(testEntries.size(), glossary.getEntryCount()); + } + Assertions.assertThrows(DeepLException.class, () -> translator.getGlossary(invalidGlossaryId)); + Assertions.assertThrows( + GlossaryNotFoundException.class, () -> translator.getGlossary(nonexistentGlossaryId)); + } + + @Test + void testGlossaryGetEntries() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + GlossaryEntries entries = new GlossaryEntries(); + entries.put("Apple", "Apfel"); + entries.put("Banana", "Banane"); + entries.put("A%=&", "B&=%"); + entries.put("\u0394\u3041", "\u6DF1"); + entries.put("\uD83E\uDEA8", "\uD83E\uDEB5"); + + GlossaryInfo createdGlossary = + translator.createGlossary(glossaryName, sourceLang, targetLang, entries); + Assertions.assertEquals(entries, translator.getGlossaryEntries(createdGlossary)); + Assertions.assertEquals( + entries, translator.getGlossaryEntries(createdGlossary.getGlossaryId())); + } + + Assertions.assertThrows( + DeepLException.class, () -> translator.getGlossaryEntries(invalidGlossaryId)); + Assertions.assertThrows( + GlossaryNotFoundException.class, + () -> translator.getGlossaryEntries(nonexistentGlossaryId)); + } + + @Test + void testGlossaryList() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + translator.createGlossary(glossaryName, sourceLang, targetLang, testEntries); + + List glossaries = translator.listGlossaries(); + Assertions.assertTrue( + glossaries.stream() + .anyMatch((glossaryInfo -> Objects.equals(glossaryInfo.getName(), glossaryName)))); + } + } + + @Test + void testGlossaryDelete() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + GlossaryInfo glossary = + translator.createGlossary(glossaryName, sourceLang, targetLang, testEntries); + + translator.deleteGlossary(glossary); + Assertions.assertThrows( + GlossaryNotFoundException.class, () -> translator.deleteGlossary(glossary)); + + Assertions.assertThrows( + DeepLException.class, () -> translator.deleteGlossary(invalidGlossaryId)); + Assertions.assertThrows( + GlossaryNotFoundException.class, () -> translator.deleteGlossary(nonexistentGlossaryId)); + } + } + + @Test + void testGlossaryTranslateTextSentence() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + GlossaryEntries entries = + new GlossaryEntries() { + { + put("artist", "Maler"); + put("prize", "Gewinn"); + } + }; + String inputText = "The artist was awarded a prize."; + + GlossaryInfo glossary = + translator.createGlossary(glossaryName, sourceLang, targetLang, entries); + + TextResult result = + translator.translateText( + inputText, + sourceLang, + targetLang, + new TextTranslationOptions().setGlossary(glossary.getGlossaryId())); + if (!isMockServer) { + Assertions.assertTrue(result.getText().contains("Maler")); + Assertions.assertTrue(result.getText().contains("Gewinn")); + } + + // It is also possible to specify GlossaryInfo + result = + translator.translateText( + inputText, + sourceLang, + targetLang, + new TextTranslationOptions().setGlossary(glossary)); + if (!isMockServer) { + Assertions.assertTrue(result.getText().contains("Maler")); + Assertions.assertTrue(result.getText().contains("Gewinn")); + } + } + } + + @Test + void testGlossaryTranslateTextBasic() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanupEnDe = new GlossaryCleanupUtility(translator, "EnDe"); + GlossaryCleanupUtility cleanupDeEn = new GlossaryCleanupUtility(translator, "DeEn")) { + String glossaryNameEnDe = cleanupEnDe.getGlossaryName(); + String glossaryNameDeEn = cleanupDeEn.getGlossaryName(); + List textsEn = + new ArrayList() { + { + add("Apple"); + add("Banana"); + } + }; + List textsDe = + new ArrayList() { + { + add("Apfel"); + add("Banane"); + } + }; + GlossaryEntries glossaryEntriesEnDe = new GlossaryEntries(); + GlossaryEntries glossaryEntriesDeEn = new GlossaryEntries(); + for (int i = 0; i < textsEn.size(); i++) { + glossaryEntriesEnDe.put(textsEn.get(i), textsDe.get(i)); + glossaryEntriesDeEn.put(textsDe.get(i), textsEn.get(i)); + } + + GlossaryInfo glossaryEnDe = + translator.createGlossary(glossaryNameEnDe, "en", "de", glossaryEntriesEnDe); + GlossaryInfo glossaryDeEn = + translator.createGlossary(glossaryNameDeEn, "de", "en", glossaryEntriesDeEn); + + List result = + translator.translateText( + textsEn, "en", "de", new TextTranslationOptions().setGlossary(glossaryEnDe)); + Assertions.assertArrayEquals( + textsDe.toArray(), result.stream().map(TextResult::getText).toArray()); + + result = + translator.translateText( + textsDe, + "de", + "en-US", + new TextTranslationOptions().setGlossary(glossaryDeEn.getGlossaryId())); + Assertions.assertArrayEquals( + textsEn.toArray(), result.stream().map(TextResult::getText).toArray()); + } + } + + @Test + void testGlossaryTranslateDocument() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup = new GlossaryCleanupUtility(translator)) { + String glossaryName = cleanup.getGlossaryName(); + File inputFile = createInputFile("artist\nprize"); + File outputFile = createOutputFile(); + String expectedOutput = "Maler\nGewinn"; + GlossaryEntries entries = + new GlossaryEntries() { + { + put("artist", "Maler"); + put("prize", "Gewinn"); + } + }; + + GlossaryInfo glossary = + translator.createGlossary(glossaryName, sourceLang, targetLang, entries); + + translator.translateDocument( + inputFile, + outputFile, + sourceLang, + targetLang, + new DocumentTranslationOptions().setGlossary(glossary)); + Assertions.assertEquals(expectedOutput, readFromFile(outputFile)); + boolean ignored = outputFile.delete(); + + translator.translateDocument( + inputFile, + outputFile, + sourceLang, + targetLang, + new DocumentTranslationOptions().setGlossary(glossary.getGlossaryId())); + Assertions.assertEquals(expectedOutput, readFromFile(outputFile)); + } + } + + @Test + void testGlossaryTranslateTextInvalid() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanupEnDe = new GlossaryCleanupUtility(translator, "EnDe"); + GlossaryCleanupUtility cleanupDeEn = new GlossaryCleanupUtility(translator, "DeEn")) { + String glossaryNameEnDe = cleanupEnDe.getGlossaryName(); + String glossaryNameDeEn = cleanupDeEn.getGlossaryName(); + + GlossaryInfo glossaryEnDe = + translator.createGlossary(glossaryNameEnDe, "en", "de", testEntries); + GlossaryInfo glossaryDeEn = + translator.createGlossary(glossaryNameDeEn, "de", "en", testEntries); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + translator.translateText( + "test", null, "de", new TextTranslationOptions().setGlossary(glossaryEnDe))); + Assertions.assertTrue(exception.getMessage().contains("sourceLang is required")); + + exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + translator.translateText( + "test", "de", "en", new TextTranslationOptions().setGlossary(glossaryDeEn))); + Assertions.assertTrue(exception.getMessage().contains("targetLang=\"en\" is not allowed")); + } + } +} From cd8dbec986b496d739105b05e8927b16498a4784 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Wed, 14 Dec 2022 11:44:35 +0100 Subject: [PATCH 015/121] ci: openjdk docker images are deprecated --- .gitlab-ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 185430e..45031e5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,10 +6,12 @@ include: # Global -------------------------- -image: openjdk:latest +# Use 17 (LTS) as base +image: eclipse-temurin:17 variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" + JAVA_TOOL_OPTIONS: "" stages: - check @@ -44,18 +46,16 @@ test: extends: .test parallel: matrix: - - DOCKER_IMAGE: "openjdk:18" + - DOCKER_IMAGE: "eclipse-temurin:18" - DOCKER_IMAGE: "openjdk:8" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "openjdk:11" + - DOCKER_IMAGE: "eclipse-temurin:8" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "openjdk:17" + - DOCKER_IMAGE: "eclipse-temurin:11" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "openjdk:18" + - DOCKER_IMAGE: "eclipse-temurin:17" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "openjdk:19" - USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "openjdk:20" + - DOCKER_IMAGE: "eclipse-temurin:19" USE_MOCK_SERVER: "use mock server" image: ${DOCKER_IMAGE} script: From 63536dc9d0c21472c8cf20fa1acbd7c8bad1d608 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 15 Dec 2022 15:08:53 +0100 Subject: [PATCH 016/121] Increase version to 1.0.0 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c1019f..e5fc7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.0] - 2022-12-15 ### Added * Add support for glossary management functions. ### Changed @@ -53,7 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...HEAD +[1.0.0]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...v1.0.0 [0.2.1]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/DeepLcom/deepl-java/compare/v0.1.2...v0.1.3 diff --git a/README.md b/README.md index 3b7dec7..6aac1e7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:0.2.1" +implementation "com.deepl.api:deepl-java:1.0.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 0.2.1 + 1.0.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index a88273c..37b3c61 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "0.2.1" +version = "1.0.0" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index cad7db9..915a567 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -55,7 +55,7 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum headers.putAll(options.getHeaders()); } headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + authKey); - headers.putIfAbsent("User-Agent", "deepl-java/0.2.1"); + headers.putIfAbsent("User-Agent", "deepl-java/1.0.0"); this.httpClientWrapper = new HttpClientWrapper( From 31c646d0daa80b475e23c641fdbc94efc22f1abf Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 2 Jan 2023 16:40:10 +0100 Subject: [PATCH 017/121] fix: always send SentenceSplittingMode in requests --- CHANGELOG.md | 8 ++++++++ deepl-java/src/main/java/com/deepl/api/Translator.java | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5fc7a3..5a30e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Fixed +* Always send SentenceSplittingMode option in requests. + * [#3](https://github.com/DeepLcom/deepl-java/issues/3) thanks to + [nicStuff](https://github.com/nicStuff) + + ## [1.0.0] - 2022-12-15 ### Added * Add support for glossary management functions. @@ -53,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unrelased]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...HEAD [1.0.0]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...v1.0.0 [0.2.1]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...v0.2.0 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 915a567..aefff8c 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -765,8 +765,7 @@ private static ArrayList> createHttpParams( if (options != null) { // Note: formality and glossaryId are added above - if (options.getSentenceSplittingMode() != null - && options.getSentenceSplittingMode() != SentenceSplittingMode.All) { + if (options.getSentenceSplittingMode() != null) { switch (options.getSentenceSplittingMode()) { case Off: params.add(new KeyValuePair<>("split_sentences", "0")); @@ -774,6 +773,9 @@ private static ArrayList> createHttpParams( case NoNewlines: params.add(new KeyValuePair<>("split_sentences", "nonewlines")); break; + case All: + params.add(new KeyValuePair<>("split_sentences", "1")); + break; default: break; } From e59e347e57f5d15a1ac7b6b36ca8fe7d5e8210b8 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 2 Jan 2023 16:52:17 +0100 Subject: [PATCH 018/121] Increase version to 1.0.1 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a30e1f..bfb35ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.1] - 2023-01-02 ### Fixed * Always send SentenceSplittingMode option in requests. * [#3](https://github.com/DeepLcom/deepl-java/issues/3) thanks to @@ -60,7 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unrelased]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...HEAD +[1.0.1]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...v1.0.0 [0.2.1]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/DeepLcom/deepl-java/compare/v0.1.3...v0.2.0 diff --git a/README.md b/README.md index 6aac1e7..2824846 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.0.0" +implementation "com.deepl.api:deepl-java:1.0.1" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.0.0 + 1.0.1 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 37b3c61..f0d7679 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.0.0" +version = "1.0.1" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index aefff8c..13d25c8 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -55,7 +55,7 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum headers.putAll(options.getHeaders()); } headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + authKey); - headers.putIfAbsent("User-Agent", "deepl-java/1.0.0"); + headers.putIfAbsent("User-Agent", "deepl-java/1.0.1"); this.httpClientWrapper = new HttpClientWrapper( From d7c4c15e5d4e2266adc1fcbe039b16d5b333f657 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 11 Jan 2023 16:13:02 +0000 Subject: [PATCH 019/121] Add maven example projects and CI smoketest --- .gitlab-ci.yml | 22 ++++++ CHANGELOG.md | 5 ++ examples/maven/deepl-test-app/pom.xml | 70 +++++++++++++++++++ .../main/java/com/deepl/deepltestapp/App.java | 47 +++++++++++++ .../annotation/IntegrationTest.java | 9 +++ .../deepltestapp/DeepLIntegrationTest.java | 41 +++++++++++ 6 files changed, 194 insertions(+) create mode 100644 examples/maven/deepl-test-app/pom.xml create mode 100644 examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/App.java create mode 100644 examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/annotation/IntegrationTest.java create mode 100644 examples/maven/deepl-test-app/src/test/java/com/deepl/deepltestapp/DeepLIntegrationTest.java diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 45031e5..ea5c9ed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,7 @@ include: image: eclipse-temurin:17 variables: + DOCKER_IMAGE_PREFIX: ${CONTAINER_REGISTRY}/oss-client-libraries/${CI_PROJECT_NAME}-build GRADLE_OPTS: "-Dorg.gradle.daemon=false" JAVA_TOOL_OPTIONS: "" @@ -76,6 +77,27 @@ test: - deepl-java/build/reports/tests/test/index.html when: always +test_examples: + stage: test + extends: .test + parallel: + matrix: + - DOCKER_IMAGE: "maven:3.8" + - DOCKER_IMAGE: "maven:3.8-openjdk-18" + - DOCKER_IMAGE: "maven:3.8-openjdk-8" + - DOCKER_IMAGE: "maven:3.8-sapmachine-17" + - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-8" + - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-18" + image: ${DOCKER_IMAGE} + script: + - cd examples/maven/deepl-test-app + - mvn install -B -PbuildProject -l mvn_build.log + - mvn verify -PrunIntegrationTests + artifacts: + paths: + - examples/maven/deepl-test-app/mvn_build.log + when: always + # stage: publish ------------------------- publish: diff --git a/CHANGELOG.md b/CHANGELOG.md index bfb35ac..c989a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +* Add example maven project using this library. + ## [1.0.1] - 2023-01-02 ### Fixed @@ -60,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...HEAD [1.0.1]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...v1.0.0 [0.2.1]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...v0.2.1 diff --git a/examples/maven/deepl-test-app/pom.xml b/examples/maven/deepl-test-app/pom.xml new file mode 100644 index 0000000..cd0acce --- /dev/null +++ b/examples/maven/deepl-test-app/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + com.deepl.deeplTestApp + deepl-test-app + jar + 1.0-SNAPSHOT + deepl-test-app + http://maven.apache.org + + + 1.8 + 1.8 + + + + + junit + junit + 4.13.2 + test + + + com.deepl.api + deepl-java + 1.0.1 + + + + + + + buildProject + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M8 + + com.deepl.deepltestapp.annotation.IntegrationTest + + + + + + + runIntegrationTests + + + + maven-surefire-plugin + 3.0.0-M8 + + + integration-test + + + **/* + + com.deepl.deepltestapp.annotation.IntegrationTest + + + + + + + + + diff --git a/examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/App.java b/examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/App.java new file mode 100644 index 0000000..7e3a72b --- /dev/null +++ b/examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/App.java @@ -0,0 +1,47 @@ +// Copyright 2023 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.deepltestapp; +import com.deepl.api.*; +import java.lang.System.*; + +/** + * Hello world translation example + * + */ +public class App { + /** + * Hello world example - insert your API key to test the library. + */ + public static void main( String[] args ) throws InterruptedException, DeepLException { + String authKey = "f63c02c5-f056-..."; // Replace with your key + Translator translator = new Translator(authKey); + TextResult result = + translator.translateText("Hello, world!", null, "fr"); + System.out.println(result.getText()); // "Bonjour, le monde !" + } + + ///////////////////////////////////////////////////////////////////////////////////// + /// These methods are for a test using DeepLs CI pipeline, ignore. + + public static String getEnvironmentVariableValue(String envVar) { + return System.getenv(envVar); + } + + public static String getAuthKeyFromEnvironmentVariables() { + return getEnvironmentVariableValue("DEEPL_AUTH_KEY"); + } + + public static String getServerUrlFromEnvironmentVariables() { + return getEnvironmentVariableValue("DEEPL_SERVER_URL"); + } + + public static String translateHelloWorld() throws InterruptedException, DeepLException { + Translator translator = new Translator(getAuthKeyFromEnvironmentVariables(), + (new TranslatorOptions()).setServerUrl(getServerUrlFromEnvironmentVariables())); + TextResult result = + translator.translateText("Hello, world!", null, "fr"); + String translatedText = result.getText(); + return translatedText; + } +} diff --git a/examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/annotation/IntegrationTest.java b/examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/annotation/IntegrationTest.java new file mode 100644 index 0000000..a86fdcd --- /dev/null +++ b/examples/maven/deepl-test-app/src/main/java/com/deepl/deepltestapp/annotation/IntegrationTest.java @@ -0,0 +1,9 @@ +// Copyright 2023 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.deepltestapp.annotation; + +/** + * Marks tests as Integration tests, used for DeepLs internal CI pipeline. + */ +public interface IntegrationTest {} diff --git a/examples/maven/deepl-test-app/src/test/java/com/deepl/deepltestapp/DeepLIntegrationTest.java b/examples/maven/deepl-test-app/src/test/java/com/deepl/deepltestapp/DeepLIntegrationTest.java new file mode 100644 index 0000000..9a09bba --- /dev/null +++ b/examples/maven/deepl-test-app/src/test/java/com/deepl/deepltestapp/DeepLIntegrationTest.java @@ -0,0 +1,41 @@ +// Copyright 2023 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.deepltestapp; + +import com.deepl.api.*; +import com.deepl.deepltestapp.*; +import com.deepl.deepltestapp.annotation.IntegrationTest; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.junit.Assert.*; +import org.junit.experimental.categories.Category; + +/** + * Internal DeepL Integration Test + */ +@Category(IntegrationTest.class) +public class DeepLIntegrationTest extends TestCase { + public DeepLIntegrationTest(String testName) { + super(testName); + } + + public static Test suite() { + return new TestSuite(DeepLIntegrationTest.class); + } + + /** + * Runs the hello world example. Requires a DeepL auth key via the DEEPL_AUTH_KEY + * environment variable. + */ + public void testApp() throws InterruptedException, DeepLException { + String result = App.translateHelloWorld(); + String[] wordsToCheck = {"Hello", "World"}; + for (String wordToCheck : wordsToCheck) { + assertFalse(String.format("Expected translation to no longer contain the english %s, received %s", + wordToCheck, result), result.contains(wordToCheck) + ); + } + } +} From 7acecf3252251c43ed83cbe2c3e670a73a463ee1 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 18 Jan 2023 10:37:34 +0000 Subject: [PATCH 020/121] ci: Add 1 retry to the CI jobs that run integration tests --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ea5c9ed..050bbdb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,6 +45,7 @@ build: test: stage: test extends: .test + retry: 1 parallel: matrix: - DOCKER_IMAGE: "eclipse-temurin:18" @@ -80,6 +81,7 @@ test: test_examples: stage: test extends: .test + retry: 1 parallel: matrix: - DOCKER_IMAGE: "maven:3.8" From 943f04e2c9a44067392187c19e3fff4cd5c196a4 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 12 Jan 2023 22:40:46 +0100 Subject: [PATCH 021/121] docs: add Unreleased section to changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c989a39..0416137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added * Add example maven project using this library. +### Changed +### Deprecated +### Removed +### Fixed +### Security ## [1.0.1] - 2023-01-02 From 9b0f7172a69d83e03f24eaded52042c72dc2a30c Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 12 Jan 2023 22:46:26 +0100 Subject: [PATCH 022/121] fix: also send Formality in API requests if it is set to default --- CHANGELOG.md | 1 + deepl-java/src/main/java/com/deepl/api/Translator.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0416137..e6b168d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated ### Removed ### Fixed +* Send Formality options in API requests even if it is default. ### Security diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 13d25c8..712221c 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -848,7 +848,7 @@ private static ArrayList> createHttpParamsCommon( } params.add(new KeyValuePair<>("target_lang", targetLang)); - if (formality != null && formality != Formality.Default) { + if (formality != null) { switch (formality) { case More: params.add(new KeyValuePair<>("formality", "more")); @@ -862,7 +862,9 @@ private static ArrayList> createHttpParamsCommon( case PreferLess: params.add(new KeyValuePair<>("formality", "prefer_less")); break; + case Default: default: + params.add(new KeyValuePair<>("formality", "default")); break; } } From a8c2bc7c2213337c519e9cebae539c9bb345d39b Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 2 Jan 2023 15:02:37 +0100 Subject: [PATCH 023/121] =?UTF-8?q?feat:=20New=20languages=20Korean=20(ko)?= =?UTF-8?q?=20and=20Norwegian=20(bokm=C3=A5l)=20(nb):=20add=20language=20c?= =?UTF-8?q?ode=20constants=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ deepl-java/src/main/java/com/deepl/api/LanguageCode.java | 6 ++++++ deepl-java/src/test/java/com/deepl/api/GeneralTest.java | 4 ++-- deepl-java/src/test/java/com/deepl/api/TestBase.java | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b168d..93fe55c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added * Add example maven project using this library. +* New languages available: Korean (`'ko'`) and Norwegian (bokmål) (`'nb'`). Add + language code constants and tests. + + Note: older library versions also support the new languages, this update only + adds new code constants. ### Changed ### Deprecated ### Removed diff --git a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java index 95893df..bbba689 100644 --- a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java +++ b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java @@ -62,12 +62,18 @@ public class LanguageCode { /** Japanese language code, may be used as source or target language. */ public static final String Japanese = "ja"; + /** Korean language code, may be used as source or target language. */ + public static final String Korean = "ko"; + /** Lithuanian language code, may be used as source or target language. */ public static final String Lithuanian = "lt"; /** Latvian language code, may be used as source or target language. */ public static final String Latvian = "lv"; + /** Norwegian (bokmål) language code, may be used as source or target language. */ + public static final String Norwegian = "nb"; + /** Dutch language code, may be used as source or target language. */ public static final String Dutch = "nl"; diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 12718d5..c545366 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -70,7 +70,7 @@ void testGetSourceAndTargetLanguages() throws DeepLException, InterruptedExcepti } Assertions.assertNull(language.getSupportsFormality()); } - Assertions.assertTrue(sourceLanguages.size() > 20); + Assertions.assertTrue(sourceLanguages.size() >= 29); for (Language language : targetLanguages) { Assertions.assertNotNull(language.getSupportsFormality()); @@ -79,7 +79,7 @@ void testGetSourceAndTargetLanguages() throws DeepLException, InterruptedExcepti Assertions.assertEquals("German", language.getName()); } } - Assertions.assertTrue(targetLanguages.size() > 20); + Assertions.assertTrue(targetLanguages.size() >= 31); } @Test diff --git a/deepl-java/src/test/java/com/deepl/api/TestBase.java b/deepl-java/src/test/java/com/deepl/api/TestBase.java index 09c09bc..b3b6398 100644 --- a/deepl-java/src/test/java/com/deepl/api/TestBase.java +++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java @@ -64,8 +64,10 @@ public class TestBase { exampleText.put("id", "berkas proton"); exampleText.put("it", "fascio di protoni"); exampleText.put("ja", "陽子ビーム"); + exampleText.put("ko", "양성자 빔"); exampleText.put("lt", "protonų spindulys"); exampleText.put("lv", "protonu staru kūlis"); + exampleText.put("nb", "protonstråle"); exampleText.put("nl", "protonenbundel"); exampleText.put("pl", "wiązka protonów"); exampleText.put("pt", "feixe de prótons"); From 7aa1bcd75d70ceb99523aba04fcbfeb694a891be Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 27 Jan 2023 09:01:23 +0100 Subject: [PATCH 024/121] docs: Increase version to 1.1.0 --- CHANGELOG.md | 8 ++------ README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- examples/maven/deepl-test-app/pom.xml | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93fe55c..e9b4d91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.1.0] - 2023-01-26 ### Added * Add example maven project using this library. * New languages available: Korean (`'ko'`) and Norwegian (bokmål) (`'nb'`). Add @@ -12,12 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Note: older library versions also support the new languages, this update only adds new code constants. -### Changed -### Deprecated -### Removed ### Fixed * Send Formality options in API requests even if it is default. -### Security ## [1.0.1] - 2023-01-02 @@ -75,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...HEAD +[1.1.0]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...v1.0.0 [0.2.1]: https://github.com/DeepLcom/deepl-java/compare/v0.2.0...v0.2.1 diff --git a/README.md b/README.md index 2824846..4c1db47 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.0.1" +implementation "com.deepl.api:deepl-java:1.1.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.0.1 + 1.1.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index f0d7679..6f8ac54 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.0.1" +version = "1.1.0" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 712221c..6a9812f 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -55,7 +55,7 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum headers.putAll(options.getHeaders()); } headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + authKey); - headers.putIfAbsent("User-Agent", "deepl-java/1.0.1"); + headers.putIfAbsent("User-Agent", "deepl-java/1.1.0"); this.httpClientWrapper = new HttpClientWrapper( diff --git a/examples/maven/deepl-test-app/pom.xml b/examples/maven/deepl-test-app/pom.xml index cd0acce..0f3de8b 100644 --- a/examples/maven/deepl-test-app/pom.xml +++ b/examples/maven/deepl-test-app/pom.xml @@ -23,7 +23,7 @@ com.deepl.api deepl-java - 1.0.1 + [1.0,2.0) From 404d1806eb28ae3a95a30c5d4b986372864ecd96 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Mon, 30 Jan 2023 12:59:44 +0000 Subject: [PATCH 025/121] ci: Add license checker to CI pipeline. --- .gitlab-ci.yml | 6 ++++++ CHANGELOG.md | 5 +++++ license_checker.sh | 7 +++++++ 3 files changed, 18 insertions(+) create mode 100755 license_checker.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 050bbdb..efc4ec2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,6 +30,12 @@ spotless: stage: check script: ./gradlew spotlessCheck +licenseCheck: + stage: check + script: + - ./license_checker.sh '*.java' | tee license_check_output.txt + - '[ ! -s license_check_output.txt ]' + # stage: build ---------------------- build: diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b4d91..2cca3e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +* Script to check our source code for license headers and a step for them in the CI. + ## [1.1.0] - 2023-01-26 ### Added * Add example maven project using this library. @@ -71,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...HEAD [1.1.0]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...v1.0.0 diff --git a/license_checker.sh b/license_checker.sh new file mode 100755 index 0000000..df09ea1 --- /dev/null +++ b/license_checker.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Usage: ./license_checker.sh source_code_pattern +# Example: ./license_checker.sh '*.py' +# This will search all .py files, ignoring anything not tracked in your git tree + +git ls-files -z $1 | xargs -0 -I{} sh -c 'RES=$(head -n 3 "{}" | grep "Copyright 20[0-9][0-9] DeepL SE (https://www.deepl.com)"); if [ ! "${RES}" ] ; then echo "Lacking copyright header in" "{}" ; fi' From dc3c05b1daeeaf05d2e5c53c729e0372866eb855 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Tue, 7 Feb 2023 10:30:54 +0000 Subject: [PATCH 026/121] feat: Send platform information in user-agent with API requests, add opt-out for this feature --- CHANGELOG.md | 3 + README.md | 48 +++++++++ deepl-java/build.gradle.kts | 13 +++ .../src/main/java/com/deepl/api/AppInfo.java | 22 ++++ .../main/java/com/deepl/api/Translator.java | 25 ++++- .../java/com/deepl/api/TranslatorOptions.java | 32 ++++++ .../test/java/com/deepl/api/GeneralTest.java | 100 ++++++++++++++++++ 7 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/AppInfo.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cca3e9..b5487e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added * Script to check our source code for license headers and a step for them in the CI. +* Added system and java version information to the user-agent string that is sent with API calls, along with an opt-out. +* Added method for applications that use this library to identify themselves in API requests they make. + ## [1.1.0] - 2023-01-26 ### Added diff --git a/README.md b/README.md index 4c1db47..76c7baa 100644 --- a/README.md +++ b/README.md @@ -517,6 +517,26 @@ All module functions may raise `DeepLException` or one of its subclasses. If invalid arguments are provided, they may raise the standard exceptions `IllegalArgumentException`. +### Writing a Plugin + +If you use this library in an application, please identify the application with +`TranslatorOptions.setAppInfo()`, which takes the name and version of the app: + +```java +class Example { // Continuing class Example from above + public void configurationExample() throws Exception { + TranslatorOptions options = + new TranslatorOptions().setAppInfo("my-java-translation-plugin", "1.2.3"); + Translator translator = new Translator(authKey, options); + } +} +``` + +This information is passed along when the library makes calls to the DeepL API. +Both name and version are required. Please note that setting the `User-Agent` header +via `TranslatorOptions.setHeaders()` will override this setting, if you need to use this, +please manually identify your Application in the `User-Agent` header. + ### Configuration The `Translator` constructor accepts `TranslatorOptions` as a second argument, @@ -546,6 +566,34 @@ The available options setters are: purposes. By default, the correct DeepL API (Free or Pro) is automatically selected. +#### Anonymous platform information + +By default, we send some basic information about the platform the client library is running on with each request, see [here for an explanation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). This data is completely anonymous and only used to improve our product, not track any individual users. If you do not wish to send this data, you can opt-out when creating your `Translator` object by calling the `setSendPlatformInfo()` setter on the `TranslatorOptions` like so: + +```java +class Example { // Continuing class Example from above + public void configurationExample() throws Exception { + TranslatorOptions options = + new TranslatorOptions().setSendPlatformInfo(false); + Translator translator = new Translator(authKey, options); + } +} +``` + +You can also customize the `User-Agent` header by setting its value explicitly in the `TranslatorOptions` object via the header field. Example: + +```java +class Example { // Continuing class Example from above + public void configurationExample() throws Exception { + Map headers = new HashMap<>(); + headers.put("User-Agent", "my custom user agent"); + TranslatorOptions options = + new TranslatorOptions().setHeaders(headers); + Translator translator = new Translator(authKey, options); + } +} +``` + ## Issues If you experience problems using the library, or would like to request a new diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 6f8ac54..6ad0d81 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -8,6 +8,12 @@ plugins { group = "com.deepl.api" version = "1.1.0" +val sharedManifest = the().manifest { + attributes ( + "Implementation-Title" to "Gradle", + "Implementation-Version" to version + ) +} java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -20,6 +26,7 @@ repositories { dependencies { implementation("org.jetbrains:annotations:20.1.0") testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") + testImplementation("org.mockito:mockito-inline:4.11.0") api("org.apache.commons:commons-math3:3.6.1") @@ -42,11 +49,17 @@ spotless { tasks.register("sourcesJar") { archiveClassifier.set("sources") from(sourceSets.main.get().allJava) + manifest = project.the().manifest { + from(sharedManifest) + } } tasks.register("javadocJar") { archiveClassifier.set("javadoc") from(tasks.javadoc.get().destinationDir) + manifest = project.the().manifest { + from(sharedManifest) + } } publishing { diff --git a/deepl-java/src/main/java/com/deepl/api/AppInfo.java b/deepl-java/src/main/java/com/deepl/api/AppInfo.java new file mode 100644 index 0000000..a614435 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/AppInfo.java @@ -0,0 +1,22 @@ +// Copyright 2023 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +public class AppInfo { + private String appName; + private String appVersion; + + public AppInfo(String appName, String appVersion) { + this.appName = appName; + this.appVersion = appVersion; + } + + public String getAppName() { + return appName; + } + + public String getAppVersion() { + return appVersion; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 6a9812f..4047ffd 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -55,7 +55,9 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum headers.putAll(options.getHeaders()); } headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + authKey); - headers.putIfAbsent("User-Agent", "deepl-java/1.1.0"); + headers.putIfAbsent( + "User-Agent", + constructUserAgentString(options.getSendPlatformInfo(), options.getAppInfo())); this.httpClientWrapper = new HttpClientWrapper( @@ -76,6 +78,27 @@ public Translator(String authKey) throws IllegalArgumentException { this(authKey, new TranslatorOptions()); } + /** + * Builds the user-agent String which contains platform information. + * + * @return A string containing the client library version, java version and operating system. + */ + private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { + StringBuilder sb = new StringBuilder(); + sb.append("deepl-java/1.1.0"); + if (sendPlatformInfo) { + sb.append(" ("); + Properties props = System.getProperties(); + sb.append(props.get("os.name") + "-" + props.get("os.version") + "-" + props.get("os.arch")); + sb.append(") java/"); + sb.append(props.get("java.version")); + } + if (appInfo != null) { + sb.append(" " + appInfo.getAppName() + "/" + appInfo.getAppVersion()); + } + return sb.toString(); + } + /** * Determines if the given DeepL Authentication Key belongs to an API Free account. * diff --git a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java index e080ac9..7fb2773 100644 --- a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java @@ -24,6 +24,8 @@ public class TranslatorOptions { @Nullable private Proxy proxy = null; @Nullable private Map headers = null; @Nullable private String serverUrl = null; + private boolean sendPlatformInfo = true; + @Nullable private AppInfo appInfo = null; /** * Set the maximum number of failed attempts that {@link Translator} will retry, per request. By @@ -69,6 +71,26 @@ public TranslatorOptions setServerUrl(String serverUrl) { return this; } + /** + * Set whether to send basic platform information with each API call to improve DeepL products. + * Defaults to `true`, set to `false` to opt out. This option will be overriden if a + * `'User-agent'` header is present in this objects `headers`. + */ + public TranslatorOptions setSendPlatformInfo(boolean sendPlatformInfo) { + this.sendPlatformInfo = sendPlatformInfo; + return this; + } + + /** + * Set an identifier and a version for the program/plugin that uses this Client Library. Example: + * `Translator t = new Translator(myAuthKey, new TranslatorOptions() + * .setAppInfo('deepl-hadoop-plugin', '1.2.0')) + */ + public TranslatorOptions setAppInfo(String appName, String appVersion) { + this.appInfo = new AppInfo(appName, appVersion); + return this; + } + /** Gets the current maximum number of retries. */ public int getMaxRetries() { return maxRetries; @@ -93,4 +115,14 @@ public Duration getTimeout() { public @Nullable String getServerUrl() { return serverUrl; } + + /** Gets the `sendPlatformInfo` option */ + public boolean getSendPlatformInfo() { + return sendPlatformInfo; + } + + /** Gets the `appInfo` identifiers */ + public @Nullable AppInfo getAppInfo() { + return appInfo; + } } diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index c545366..1a0306a 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -3,11 +3,19 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import static org.mockito.Mockito.*; + import java.io.*; import java.net.*; import java.time.*; import java.util.*; +import java.util.stream.Stream; import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; class GeneralTest extends TestBase { @@ -237,4 +245,96 @@ void testUsageTeamDocumentLimit() throws Exception { Assertions.assertNotNull(usage.getTeamDocument()); Assertions.assertTrue(usage.getTeamDocument().limitReached()); } + + @ParameterizedTest + @MethodSource("provideUserAgentTestData") + void testUserAgent( + SessionOptions sessionOptions, + TranslatorOptions translatorOptions, + Iterable requiredStrings, + Iterable blocklistedStrings) + throws Exception { + Map headers = new HashMap<>(); + HttpURLConnection con = Mockito.mock(HttpURLConnection.class); + Mockito.doAnswer( + invocation -> { + String key = (String) invocation.getArgument(0); + String value = (String) invocation.getArgument(1); + headers.put(key, value); + return null; + }) + .when(con) + .setRequestProperty(Mockito.any(String.class), Mockito.any(String.class)); + Mockito.when(con.getResponseCode()).thenReturn(200); + try (MockedConstruction mockUrl = + Mockito.mockConstruction( + URL.class, + (mock, context) -> { + Mockito.when(mock.openConnection()).thenReturn(con); + })) { + Translator translator = createTranslator(sessionOptions, translatorOptions); + Usage usage = translator.getUsage(); + String userAgentHeader = headers.get("User-Agent"); + for (String s : requiredStrings) { + Assertions.assertTrue( + userAgentHeader.contains(s), + String.format( + "Expected User-Agent header to contain %s\nActual:\n%s", s, userAgentHeader)); + } + for (String n : blocklistedStrings) { + Assertions.assertFalse( + userAgentHeader.contains(n), + String.format( + "Expected User-Agent header not to contain %s\nActual:\n%s", n, userAgentHeader)); + } + } + } + + // Session options & Translator options: Used to construct the `Translator` + // Next arg: List of Strings that must be contained in the user agent header + // Last arg: List of Strings that must not be contained in the user agent header + private static Stream provideUserAgentTestData() { + Map testHeaders = new HashMap<>(); + testHeaders.put("User-Agent", "my custom user agent"); + Iterable lightPlatformInfo = Arrays.asList("deepl-java/"); + Iterable lightPlatformInfoWithAppInfo = + Arrays.asList("deepl", "my-java-translation-plugin/1.2.3"); + Iterable detailedPlatformInfo = Arrays.asList(" java/", "("); + Iterable detailedPlatformInfoWithAppInfo = + Arrays.asList(" java/", "(", "my-java-translation-plugin/1.2.3"); + Iterable customUserAgent = Arrays.asList("my custom user agent"); + Iterable noStrings = new ArrayList(); + return Stream.of( + Arguments.of( + new SessionOptions(), new TranslatorOptions(), detailedPlatformInfo, noStrings), + Arguments.of( + new SessionOptions(), + new TranslatorOptions().setSendPlatformInfo(false), + lightPlatformInfo, + detailedPlatformInfo), + Arguments.of( + new SessionOptions(), + new TranslatorOptions().setHeaders(testHeaders), + customUserAgent, + detailedPlatformInfo), + Arguments.of( + new SessionOptions(), + new TranslatorOptions().setAppInfo("my-java-translation-plugin", "1.2.3"), + detailedPlatformInfoWithAppInfo, + noStrings), + Arguments.of( + new SessionOptions(), + new TranslatorOptions() + .setSendPlatformInfo(false) + .setAppInfo("my-java-translation-plugin", "1.2.3"), + lightPlatformInfoWithAppInfo, + detailedPlatformInfo), + Arguments.of( + new SessionOptions(), + new TranslatorOptions() + .setHeaders(testHeaders) + .setAppInfo("my-java-translation-plugin", "1.2.3"), + customUserAgent, + detailedPlatformInfoWithAppInfo)); + } } From 6d28b667c5a4d2fdd518f0327ce26f9094195c43 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 25 Jan 2023 11:24:20 +0000 Subject: [PATCH 027/121] ci: Retry all jobs for the scheduled pipeline --- .gitlab-ci.yml | 75 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index efc4ec2..98c91ad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,6 @@ include: image: eclipse-temurin:17 variables: - DOCKER_IMAGE_PREFIX: ${CONTAINER_REGISTRY}/oss-client-libraries/${CI_PROJECT_NAME}-build GRADLE_OPTS: "-Dorg.gradle.daemon=false" JAVA_TOOL_OPTIONS: "" @@ -26,19 +25,42 @@ before_script: # stage: check ---------------------- -spotless: +spotless_base: stage: check script: ./gradlew spotlessCheck -licenseCheck: +spotless_scheduled: + extends: spotless_base + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + retry: 2 + +spotless_manual: + extends: spotless_base + rules: + - if: $CI_PIPELINE_SOURCE != "schedule" + +.license_check_base: stage: check script: - ./license_checker.sh '*.java' | tee license_check_output.txt - '[ ! -s license_check_output.txt ]' +license_check_scheduled: + extends: .license_check_base + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + retry: 2 + +license_check_manual: + extends: .license_check_base + rules: + - if: $CI_PIPELINE_SOURCE != "schedule" + + # stage: build ---------------------- -build: +build_base: stage: build script: - ./gradlew assemble @@ -46,12 +68,22 @@ build: paths: - deepl-java/build/ +build_scheduled: + extends: build_base + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + retry: 2 + +build_manual: + extends: build_base + rules: + - if: $CI_PIPELINE_SOURCE != "schedule" + # stage: test ------------------------- -test: +test_base: stage: test extends: .test - retry: 1 parallel: matrix: - DOCKER_IMAGE: "eclipse-temurin:18" @@ -84,16 +116,27 @@ test: - deepl-java/build/reports/tests/test/index.html when: always -test_examples: +test_scheduled: + extends: test_base + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + retry: 2 + +test_manual: + stage: test + extends: test_base + rules: + - if: $CI_PIPELINE_SOURCE != "schedule" + +test_examples_base: stage: test extends: .test - retry: 1 parallel: matrix: - DOCKER_IMAGE: "maven:3.8" - DOCKER_IMAGE: "maven:3.8-openjdk-18" - DOCKER_IMAGE: "maven:3.8-openjdk-8" - - DOCKER_IMAGE: "maven:3.8-sapmachine-17" + - DOCKER_IMAGE: "maven:3.9-sapmachine-17" - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-8" - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-18" image: ${DOCKER_IMAGE} @@ -106,13 +149,25 @@ test_examples: - examples/maven/deepl-test-app/mvn_build.log when: always +test_examples_scheduled: + extends: test_examples_base + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + retry: 2 + +test_examples_manual: + extends: test_examples_base + rules: + - if: $CI_PIPELINE_SOURCE != "schedule" + # stage: publish ------------------------- publish: stage: publish extends: .publish dependencies: - - build + - build_scheduled + - build_manual rules: - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' script: From 03b40828f186441e313b0b2a431e1cfb099e62d3 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 17 Feb 2023 16:24:45 +0000 Subject: [PATCH 028/121] ci: Use DeepL docker repo and lightweight alpine images --- .gitlab-ci.yml | 56 +++++++++++++++++++++++++--------------------- license_checker.sh | 2 +- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 98c91ad..04f7ea6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,12 +7,18 @@ include: # Global -------------------------- # Use 17 (LTS) as base -image: eclipse-temurin:17 +image: ${CI_REGISTRY_IMAGE}/eclipse-temurin:17-alpine variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" JAVA_TOOL_OPTIONS: "" +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_TAG + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + stages: - check - build @@ -25,18 +31,18 @@ before_script: # stage: check ---------------------- -spotless_base: +.spotless_base: stage: check script: ./gradlew spotlessCheck spotless_scheduled: - extends: spotless_base + extends: .spotless_base rules: - if: $CI_PIPELINE_SOURCE == "schedule" retry: 2 spotless_manual: - extends: spotless_base + extends: .spotless_base rules: - if: $CI_PIPELINE_SOURCE != "schedule" @@ -60,7 +66,7 @@ license_check_manual: # stage: build ---------------------- -build_base: +.build_base: stage: build script: - ./gradlew assemble @@ -69,35 +75,35 @@ build_base: - deepl-java/build/ build_scheduled: - extends: build_base + extends: .build_base rules: - if: $CI_PIPELINE_SOURCE == "schedule" retry: 2 build_manual: - extends: build_base + extends: .build_base rules: - if: $CI_PIPELINE_SOURCE != "schedule" # stage: test ------------------------- -test_base: +.test_base: stage: test extends: .test parallel: matrix: - - DOCKER_IMAGE: "eclipse-temurin:18" - - DOCKER_IMAGE: "openjdk:8" + - DOCKER_IMAGE: "eclipse-temurin:18-alpine" + - DOCKER_IMAGE: "openjdk:8-alpine" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "eclipse-temurin:8" + - DOCKER_IMAGE: "eclipse-temurin:8-alpine" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "eclipse-temurin:11" + - DOCKER_IMAGE: "eclipse-temurin:11-alpine" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "eclipse-temurin:17" + - DOCKER_IMAGE: "eclipse-temurin:17-alpine" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "eclipse-temurin:19" + - DOCKER_IMAGE: "eclipse-temurin:19-alpine" USE_MOCK_SERVER: "use mock server" - image: ${DOCKER_IMAGE} + image: ${CI_REGISTRY_IMAGE}/${DOCKER_IMAGE} script: - > if [[ ! -z "${USE_MOCK_SERVER}" ]]; then @@ -117,29 +123,29 @@ test_base: when: always test_scheduled: - extends: test_base + extends: .test_base rules: - if: $CI_PIPELINE_SOURCE == "schedule" retry: 2 test_manual: stage: test - extends: test_base + extends: .test_base rules: - if: $CI_PIPELINE_SOURCE != "schedule" -test_examples_base: +.test_examples_base: stage: test extends: .test parallel: matrix: - DOCKER_IMAGE: "maven:3.8" - - DOCKER_IMAGE: "maven:3.8-openjdk-18" - - DOCKER_IMAGE: "maven:3.8-openjdk-8" + - DOCKER_IMAGE: "maven:3.8-openjdk-18-slim" + - DOCKER_IMAGE: "maven:3.8-openjdk-8-slim" - DOCKER_IMAGE: "maven:3.9-sapmachine-17" - - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-8" - - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-18" - image: ${DOCKER_IMAGE} + - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-8-focal" + - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-18-alpine" + image: ${CI_REGISTRY_IMAGE}/${DOCKER_IMAGE} script: - cd examples/maven/deepl-test-app - mvn install -B -PbuildProject -l mvn_build.log @@ -150,13 +156,13 @@ test_examples_base: when: always test_examples_scheduled: - extends: test_examples_base + extends: .test_examples_base rules: - if: $CI_PIPELINE_SOURCE == "schedule" retry: 2 test_examples_manual: - extends: test_examples_base + extends: .test_examples_base rules: - if: $CI_PIPELINE_SOURCE != "schedule" diff --git a/license_checker.sh b/license_checker.sh index df09ea1..8f8c576 100755 --- a/license_checker.sh +++ b/license_checker.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # Usage: ./license_checker.sh source_code_pattern # Example: ./license_checker.sh '*.py' From 2e6ecc03087097b0f1e39a9edc757a723f6eae0c Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Tue, 21 Mar 2023 23:15:35 +0000 Subject: [PATCH 029/121] docs: Increase version to 1.2.0 --- .gitlab-ci.yml | 2 +- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 04f7ea6..eee29e3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -95,7 +95,7 @@ build_manual: - DOCKER_IMAGE: "eclipse-temurin:18-alpine" - DOCKER_IMAGE: "openjdk:8-alpine" USE_MOCK_SERVER: "use mock server" - - DOCKER_IMAGE: "eclipse-temurin:8-alpine" + - DOCKER_IMAGE: "eclipse-temurin:8-focal" USE_MOCK_SERVER: "use mock server" - DOCKER_IMAGE: "eclipse-temurin:11-alpine" USE_MOCK_SERVER: "use mock server" diff --git a/CHANGELOG.md b/CHANGELOG.md index b5487e2..9cf57bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.2.0] - 2023-03-22 ### Added * Script to check our source code for license headers and a step for them in the CI. * Added system and java version information to the user-agent string that is sent with API calls, along with an opt-out. @@ -78,7 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...HEAD +[1.2.0]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/DeepLcom/deepl-java/compare/v0.2.1...v1.0.0 diff --git a/README.md b/README.md index 76c7baa..66b8d0f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.1.0" +implementation "com.deepl.api:deepl-java:1.2.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.1.0 + 1.2.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 6ad0d81..ea92116 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.1.0" +version = "1.2.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 4047ffd..3b589a3 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -85,7 +85,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.1.0"); + sb.append("deepl-java/1.2.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From b11530f3247021cbac27637e8fea21e4331c0b9e Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 26 Apr 2023 13:24:40 +0100 Subject: [PATCH 030/121] fix: change polling strategy to every 5 sec for doctrans --- CHANGELOG.md | 6 ++++++ .../src/main/java/com/deepl/api/Translator.java | 11 ++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf57bb..96ed7ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Fixed +* Changed document translation to poll the server every 5 seconds. This should greatly reduce observed document translation processing time. + + ## [1.2.0] - 2023-03-22 ### Added * Script to check our source code for license headers and a step for them in the CI. @@ -78,6 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...HEAD [1.2.0]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...v1.0.1 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 3b589a3..94bf17c 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -3,9 +3,6 @@ // license that can be found in the LICENSE file. package com.deepl.api; -import static java.lang.Math.max; -import static java.lang.Math.min; - import com.deepl.api.http.HttpResponse; import com.deepl.api.http.HttpResponseStream; import com.deepl.api.parsing.Parser; @@ -1026,11 +1023,7 @@ private void checkResponse( } private int calculateDocumentWaitTimeMillis(Long secondsRemaining) { - if (secondsRemaining != null) { - double secs = ((double) secondsRemaining) / 2.0 + 1.0; - secs = max(1.0, min(secs, 60.0)); - return (int) (secs * 1000); - } - return 1000; + // secondsRemaining is currently unreliable, so just poll equidistantly + return 5000; } } From 3fa28bb0c35e848fe3096bc450f61aff8613fad4 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Tue, 25 Apr 2023 16:25:29 +0100 Subject: [PATCH 031/121] fix: Fix getUsage HTTP request --- CHANGELOG.md | 1 + deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ed7ee..0cb518d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Fixed * Changed document translation to poll the server every 5 seconds. This should greatly reduce observed document translation processing time. +* Fix getUsage request to be a HTTP GET request, not POST. ## [1.2.0] - 2023-03-22 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 94bf17c..9ac2974 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -247,7 +247,7 @@ public List translateText( * @throws DeepLException If any error occurs while communicating with the DeepL API. */ public Usage getUsage() throws DeepLException, InterruptedException { - HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/usage"); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff("/v2/usage"); checkResponse(response, false, false); return jsonParser.parseUsage(response.getBody()); } From 952f3b09985ccf254e680dffb8178ff896de00d3 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 9 Jun 2023 09:28:19 +0100 Subject: [PATCH 032/121] docs: Increase version to 1.3.0 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cb518d..39bfbb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [1.3.0] - 2023-06-09 ### Fixed * Changed document translation to poll the server every 5 seconds. This should greatly reduce observed document translation processing time. * Fix getUsage request to be a HTTP GET request, not POST. @@ -84,7 +84,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...HEAD +[1.3.0]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/DeepLcom/deepl-java/compare/v1.0.0...v1.0.1 diff --git a/README.md b/README.md index 66b8d0f..3c452c8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.2.0" +implementation "com.deepl.api:deepl-java:1.3.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.2.0 + 1.3.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index ea92116..9544fec 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.2.0" +version = "1.3.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 9ac2974..9d8f15b 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -82,7 +82,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.2.0"); + sb.append("deepl-java/1.3.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 1d97c86dcc9c6ee863ecce24c26bc874aae40c3c Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Tue, 11 Jul 2023 16:19:07 +0100 Subject: [PATCH 033/121] chore: Action to add issues to GH project --- .github/workflows/add_issues_to_kanban.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/add_issues_to_kanban.yml diff --git a/.github/workflows/add_issues_to_kanban.yml b/.github/workflows/add_issues_to_kanban.yml new file mode 100644 index 0000000..bed7595 --- /dev/null +++ b/.github/workflows/add_issues_to_kanban.yml @@ -0,0 +1,16 @@ +name: Add bugs to bugs project + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/DeepLcom/projects/1 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} From 53f25e4893f579bd9ac82f6b635296dc6f88eff6 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Tue, 25 Jul 2023 08:42:24 +0000 Subject: [PATCH 034/121] [easy] ci: Add secret detection --- .gitlab-ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eee29e3..1067bec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,9 @@ include: - project: '${CI_PROJECT_NAMESPACE}/ci-libs-for-client-libraries' file: - '/${CI_PROJECT_NAME}/.gitlab-ci.yml' + - project: 'deepl/ops/ci-cd-infrastructure/gitlab-ci-lib' + file: + - '/templates/.secret-detection.yml' # Global -------------------------- @@ -63,6 +66,16 @@ license_check_manual: rules: - if: $CI_PIPELINE_SOURCE != "schedule" +secret_detection: + extends: .secret-detection + stage: check + image: !reference [.secret-detection, image] + variables: + SECRET_DETECTION_HISTORIC_SCAN: "true" + before_script: + - echo "overriding default before_script..." + rules: + - if: $CI_MERGE_REQUEST_ID # stage: build ---------------------- From 4b592256d979551b34400dc8f38958aa70515c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Parano=C3=AFd=20User?= <5120290+ParanoidUser@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:41:29 -0400 Subject: [PATCH 035/121] chore: keep dependencies up-to-date with dependabot --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..33535cc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: gradle + directory: / + schedule: + interval: weekly + - package-ecosystem: maven + directory: /examples/maven/deepl-test-app + schedule: + interval: weekly \ No newline at end of file From cdf68de52c42b039d8263d04898d6203d5dfcc38 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 16 Aug 2023 09:30:11 +0100 Subject: [PATCH 036/121] fix: Remove unused maths dependency --- CHANGELOG.md | 6 ++++++ deepl-java/build.gradle.kts | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39bfbb3..e62042e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Fixed +* Remove unused `commons-math` dependency + + ## [1.3.0] - 2023-06-09 ### Fixed * Changed document translation to poll the server every 5 seconds. This should greatly reduce observed document translation processing time. @@ -84,6 +89,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...HEAD [1.3.0]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...v1.1.0 diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 9544fec..0db0e51 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -28,8 +28,6 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") testImplementation("org.mockito:mockito-inline:4.11.0") - api("org.apache.commons:commons-math3:3.6.1") - // implementation("com.google.guava:guava:30.1.1-jre") implementation("com.google.code.gson:gson:2.9.0") } From 070ebc50a153218b80c36ef29fd57934f3cb7521 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:40:28 +0000 Subject: [PATCH 037/121] chore(deps): bump org.mockito:mockito-inline from 4.11.0 to 5.2.0 Bumps [org.mockito:mockito-inline](https://github.com/mockito/mockito) from 4.11.0 to 5.2.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.11.0...v5.2.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- deepl-java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 0db0e51..8afd1d3 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -26,7 +26,7 @@ repositories { dependencies { implementation("org.jetbrains:annotations:20.1.0") testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") - testImplementation("org.mockito:mockito-inline:4.11.0") + testImplementation("org.mockito:mockito-inline:5.2.0") // implementation("com.google.guava:guava:30.1.1-jre") implementation("com.google.code.gson:gson:2.9.0") From 8c077a5607bf98e5cd630988606827a16485f5f1 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 24 Oct 2023 11:24:49 +0200 Subject: [PATCH 038/121] ci: use harbor image registry --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1067bec..1fea863 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ include: # Global -------------------------- # Use 17 (LTS) as base -image: ${CI_REGISTRY_IMAGE}/eclipse-temurin:17-alpine +image: eclipse-temurin:17-alpine variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" @@ -116,7 +116,7 @@ build_manual: USE_MOCK_SERVER: "use mock server" - DOCKER_IMAGE: "eclipse-temurin:19-alpine" USE_MOCK_SERVER: "use mock server" - image: ${CI_REGISTRY_IMAGE}/${DOCKER_IMAGE} + image: ${DOCKER_IMAGE} script: - > if [[ ! -z "${USE_MOCK_SERVER}" ]]; then @@ -158,7 +158,7 @@ test_manual: - DOCKER_IMAGE: "maven:3.9-sapmachine-17" - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-8-focal" - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-18-alpine" - image: ${CI_REGISTRY_IMAGE}/${DOCKER_IMAGE} + image: ${DOCKER_IMAGE} script: - cd examples/maven/deepl-test-app - mvn install -B -PbuildProject -l mvn_build.log From b49e49af37d3c178db97265015cce681ea7261bd Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 26 Oct 2023 11:04:12 +0200 Subject: [PATCH 039/121] ci: raise memory limits to prevent OOM-kills --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1fea863..db1c327 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -103,6 +103,8 @@ build_manual: .test_base: stage: test extends: .test + variables: + KUBERNETES_MEMORY_LIMIT: 8Gi parallel: matrix: - DOCKER_IMAGE: "eclipse-temurin:18-alpine" From 9997cc8470935b6988ab9dd632f6edd0a63fd4ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:40:14 +0000 Subject: [PATCH 040/121] chore(deps): bump com.google.code.gson:gson from 2.9.0 to 2.10.1 Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.9.0 to 2.10.1. - [Release notes](https://github.com/google/gson/releases) - [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md) - [Commits](https://github.com/google/gson/compare/gson-parent-2.9.0...gson-parent-2.10.1) --- updated-dependencies: - dependency-name: com.google.code.gson:gson dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- deepl-java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 0db0e51..46c7768 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -29,7 +29,7 @@ dependencies { testImplementation("org.mockito:mockito-inline:4.11.0") // implementation("com.google.guava:guava:30.1.1-jre") - implementation("com.google.code.gson:gson:2.9.0") + implementation("com.google.code.gson:gson:2.10.1") } From e9b778ec8309dd5e62f56ffc656cf0f1eb0b9ee0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:40:23 +0000 Subject: [PATCH 041/121] chore(deps): bump org.junit.jupiter:junit-jupiter from 5.8.1 to 5.10.0 Bumps [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit5) from 5.8.1 to 5.10.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.1...r5.10.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- deepl-java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 46c7768..b73f66f 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -25,7 +25,7 @@ repositories { dependencies { implementation("org.jetbrains:annotations:20.1.0") - testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("org.mockito:mockito-inline:4.11.0") // implementation("com.google.guava:guava:30.1.1-jre") From 1a067dd86ee2266b744ebc429ba9914006094d43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 07:26:50 +0000 Subject: [PATCH 042/121] chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.0.0-M8 to 3.1.2. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0-M8...surefire-3.1.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- examples/maven/deepl-test-app/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/maven/deepl-test-app/pom.xml b/examples/maven/deepl-test-app/pom.xml index 0f3de8b..1c3187e 100644 --- a/examples/maven/deepl-test-app/pom.xml +++ b/examples/maven/deepl-test-app/pom.xml @@ -36,7 +36,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M8 + 3.1.2 com.deepl.deepltestapp.annotation.IntegrationTest @@ -50,7 +50,7 @@ maven-surefire-plugin - 3.0.0-M8 + 3.1.2 integration-test From 63d033107a44cd11cc998e19305e40e57d46b60a Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 3 Nov 2023 14:54:08 +0100 Subject: [PATCH 043/121] feat: add context parameter (alpha feature) --- CHANGELOG.md | 4 ++++ README.md | 7 +++++++ .../com/deepl/api/TextTranslationOptions.java | 16 ++++++++++++++++ .../src/main/java/com/deepl/api/Translator.java | 3 +++ .../java/com/deepl/api/TranslateTextTest.java | 16 ++++++++++++++++ 5 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e62042e..f9f308c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + ## [Unreleased] +### Added +* Add optional `context` parameter for text translation, that specifies + additional context to influence translations, that is not translated itself. ### Fixed * Remove unused `commons-math` dependency diff --git a/README.md b/README.md index 3c452c8..94d6147 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,11 @@ a `TextTranslationOptions`, with the following setters: returned by glossary lookup functions, for example `listGlossaries()`). - `setGlossaryId()` is also available for backward-compatibility, accepting a string containing the glossary ID. +- `setContext()`: specifies additional context to influence translations, that is not + translated itself. Note this is an **alpha feature**: it may be deprecated at + any time, or incur charges if it becomes generally available. + See the [API documentation][api-docs-context-param] for more information and + example usage. - `setTagHandling()`: type of tags to parse before translation, options are `"html"` and `"xml"`. @@ -623,6 +628,8 @@ tests using `./gradlew test` with the `DEEPL_MOCK_SERVER_PORT` and [api-docs]: https://www.deepl.com/docs-api?utm_source=github&utm_medium=github-java-readme +[api-docs-context-param]: https://www.deepl.com/docs-api/translating-text/?utm_source=github&utm_medium=github-java-readme + [api-docs-csv-format]: https://www.deepl.com/docs-api/managing-glossaries/supported-glossary-formats/?utm_source=github&utm_medium=github-java-readme [api-docs-xml-handling]: https://www.deepl.com/docs-api/handling-xml/?utm_source=github&utm_medium=github-java-readme diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index abe2dd2..b2a8e2f 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -18,6 +18,7 @@ public class TextTranslationOptions { private String glossaryId; private SentenceSplittingMode sentenceSplittingMode; private boolean preserveFormatting = false; + private String context; private String tagHandling; private boolean outlineDetection = true; private Iterable ignoreTags; @@ -63,6 +64,16 @@ public TextTranslationOptions setGlossary(String glossaryId) { return this; } + /** + * Specifies additional context to influence translations, that is not translated itself. Note: + * this is an alpha feature: it may be deprecated at any time, or incur charges if it becomes + * generally available. See the API documentation for more information and example usage. + */ + public TextTranslationOptions setContext(String context) { + this.context = context; + return this; + } + /** * Specifies how input translation text should be split into sentences. By default, this value is * null and the default sentence splitting mode is used. @@ -151,6 +162,11 @@ public boolean isPreserveFormatting() { return preserveFormatting; } + /** Gets the current context. */ + public String getContext() { + return context; + } + /** Gets the current tag handling setting. */ public String getTagHandling() { return tagHandling; diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 9d8f15b..db1f724 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -803,6 +803,9 @@ private static ArrayList> createHttpParams( if (options.isPreserveFormatting()) { params.add(new KeyValuePair<>("preserve_formatting", "1")); } + if (options.getContext() != null) { + params.add(new KeyValuePair<>("context", options.getContext())); + } if (options.getTagHandling() != null) { params.add(new KeyValuePair<>("tag_handling", options.getTagHandling())); } diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index adec48c..666a4cf 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -186,6 +186,22 @@ void testFormality() throws DeepLException, InterruptedException { } } + @Test + void testContext() throws DeepLException, InterruptedException { + // In German, "scharf" can mean: + // - spicy/hot when referring to food, or + // - sharp when referring to other objects such as a knife (Messer). + Translator translator = createTranslator(); + String text = "Das ist scharf!"; + + translator.translateText(text, null, "de"); + // Result: "That is hot!" + + translator.translateText( + text, null, "de", new TextTranslationOptions().setContext("Das ist ein Messer.")); + // Result: "That is sharp!" + } + @Test void testSplitSentences() throws DeepLException, InterruptedException { Assumptions.assumeTrue(isMockServer); From 1b338d3c63cd57de8d0960d231b173544b3de67e Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 3 Nov 2023 14:54:12 +0100 Subject: [PATCH 044/121] docs: Increase version to 1.4.0 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f308c..037ca2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.4.0] - 2023-11-03 ### Added * Add optional `context` parameter for text translation, that specifies additional context to influence translations, that is not translated itself. @@ -93,7 +93,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...HEAD +[1.4.0]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/DeepLcom/deepl-java/compare/v1.0.1...v1.1.0 diff --git a/README.md b/README.md index 94d6147..5c013b8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.3.0" +implementation "com.deepl.api:deepl-java:1.4.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.3.0 + 1.4.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index b73f66f..a42c861 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.3.0" +version = "1.4.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index db1f724..0ca7a73 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -82,7 +82,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.3.0"); + sb.append("deepl-java/1.4.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 7e34c6780570059b9553115540bf4048ab33fdb0 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 22 Nov 2023 12:35:44 +0000 Subject: [PATCH 045/121] fix: Fix document upload API path --- CHANGELOG.md | 7 +++++++ deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 037ca2a..574a734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Fixed +* Change document upload to use the path `/v2/document` instead of `/v2/document/` (no trailing `/`). + Both paths will continue to work in the v2 version of the API, but `/v2/document` is the intended one. + + ## [1.4.0] - 2023-11-03 ### Added * Add optional `context` parameter for text translation, that specifies @@ -93,6 +99,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.4.0...HEAD [1.4.0]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...v1.2.0 diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 0ca7a73..f0bc640 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -447,7 +447,7 @@ public DocumentHandle translateDocumentUpload( try (FileInputStream inputStream = new FileInputStream(inputFile)) { HttpResponse response = httpClientWrapper.uploadWithBackoff( - "/v2/document/", params, inputFile.getName(), inputStream); + "/v2/document", params, inputFile.getName(), inputStream); checkResponse(response, false, false); return jsonParser.parseDocumentHandle(response.getBody()); } From e37b322c1d93a0a4d3fa6451f66d6c34119d8a7f Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 28 Feb 2024 15:15:09 +0000 Subject: [PATCH 046/121] feat: Add arabic language code and tests --- CHANGELOG.md | 5 +++++ deepl-java/src/main/java/com/deepl/api/LanguageCode.java | 3 +++ deepl-java/src/test/java/com/deepl/api/TestBase.java | 1 + 3 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 574a734..23721a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +* New language available: Arabic (MSA) (`'ar'`). Add language code constants and tests. + + Note: older library versions also support the new language, this update only + adds new code constants. ### Fixed * Change document upload to use the path `/v2/document` instead of `/v2/document/` (no trailing `/`). Both paths will continue to work in the v2 version of the API, but `/v2/document` is the intended one. diff --git a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java index bbba689..6fa2028 100644 --- a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java +++ b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java @@ -11,6 +11,9 @@ * Translator#getSourceLanguages()} and {@link Translator#getTargetLanguages()}. */ public class LanguageCode { + /** Arabic (MSA) language code, may be used as source or target language */ + public static final String Arabic = "ar"; + /** Bulgarian language code, may be used as source or target language. */ public static final String Bulgarian = "bg"; diff --git a/deepl-java/src/test/java/com/deepl/api/TestBase.java b/deepl-java/src/test/java/com/deepl/api/TestBase.java index b3b6398..9cbe60f 100644 --- a/deepl-java/src/test/java/com/deepl/api/TestBase.java +++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java @@ -48,6 +48,7 @@ public class TestBase { } exampleText = new HashMap<>(); + exampleText.put("ar", "شعاع البروتون"); exampleText.put("bg", "протонен лъч"); exampleText.put("cs", "protonový paprsek"); exampleText.put("da", "protonstråle"); From 526ef22f5825a2b48d0d2ce1724d325dc2357da2 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 15 Mar 2024 12:22:28 +0000 Subject: [PATCH 047/121] ci: Add manual deployment option --- .gitlab-ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db1c327..c76fba3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -194,3 +194,14 @@ publish: script: - ./gradlew publish +publish_manual: + stage: publish + extends: .publish + when: manual + dependencies: + - build_scheduled + - build_manual + rules: + - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' + script: + - ./gradlew publish From cb701b7951f7a0f774e3643cb15775f64a8654c7 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 15 Mar 2024 12:53:58 +0000 Subject: [PATCH 048/121] ci: fix maven image issue & update images --- .gitlab-ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c76fba3..2deefbb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -152,14 +152,16 @@ test_manual: .test_examples_base: stage: test extends: .test + variables: + MAVEN_OPTS: -Dmaven.repo.local=.m2/repository parallel: matrix: - - DOCKER_IMAGE: "maven:3.8" + - DOCKER_IMAGE: "maven:3.9" - DOCKER_IMAGE: "maven:3.8-openjdk-18-slim" - DOCKER_IMAGE: "maven:3.8-openjdk-8-slim" - DOCKER_IMAGE: "maven:3.9-sapmachine-17" - - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-8-focal" - - DOCKER_IMAGE: "maven:3.8-eclipse-temurin-18-alpine" + - DOCKER_IMAGE: "maven:3.9-eclipse-temurin-8" + - DOCKER_IMAGE: "maven:3.9-eclipse-temurin-21" image: ${DOCKER_IMAGE} script: - cd examples/maven/deepl-test-app From 9f781570998775f7f79523d9e4e3b1912be0ac26 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 15 Mar 2024 12:54:18 +0000 Subject: [PATCH 049/121] docs: Increase version to 1.5.0 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23721a1..7983f37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [1.5.0] - 2024-04-10 ### Added * New language available: Arabic (MSA) (`'ar'`). Add language code constants and tests. @@ -104,7 +104,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.4.0...HEAD +[1.5.0]: https://github.com/DeepLcom/deepl-java/compare/v1.4.0...v1.5.0 [1.4.0]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/DeepLcom/deepl-java/compare/v1.1.0...v1.2.0 diff --git a/README.md b/README.md index 5c013b8..eb12346 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.4.0" +implementation "com.deepl.api:deepl-java:1.5.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.4.0 + 1.5.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index a42c861..4e23fc6 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.4.0" +version = "1.5.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index f0bc640..0ddd919 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -82,7 +82,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.4.0"); + sb.append("deepl-java/1.5.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 801421fdc175fc5cade4203bdcb1f0eb50174291 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 6 May 2024 16:43:01 +0200 Subject: [PATCH 050/121] test: add mixed test-direction test --- .../test/java/com/deepl/api/GeneralTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 1a0306a..d587a0f 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -59,6 +59,26 @@ void testInvalidServerUrl() { }); } + @Test + void testMixedDirectionText() throws DeepLException, InterruptedException { + Assumptions.assumeFalse(isMockServer); + Translator translator = createTranslator(); + TextTranslationOptions options = + new TextTranslationOptions().setTagHandling("xml").setIgnoreTags(Arrays.asList("xml")); + String arIgnorePart = "يجب تجاهل هذا الجزء."; + String enSentenceWithArIgnorePart = + "

This is a short sentence. " + arIgnorePart + " This is another sentence."; + String enIgnorePart = "This part should be ignored."; + String arSentenceWithEnIgnorePart = + "

هذه جملة قصيرة. " + enIgnorePart + "هذه جملة أخرى.

"; + + TextResult enResult = + translator.translateText(enSentenceWithArIgnorePart, null, "en-US", options); + Assertions.assertTrue(enResult.getText().contains(arIgnorePart)); + TextResult arResult = translator.translateText(arSentenceWithEnIgnorePart, null, "ar", options); + Assertions.assertTrue(arResult.getText().contains(enIgnorePart)); + } + @Test void testUsage() throws DeepLException, InterruptedException { Translator translator = createTranslator(); From 8efec80222b15bc552a0ab960ff54bc667ce7879 Mon Sep 17 00:00:00 2001 From: mike-winters-deepl Date: Tue, 18 Jun 2024 17:20:59 +0200 Subject: [PATCH 051/121] Remove alpha label from context parameter in README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index eb12346..25210f0 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,7 @@ a `TextTranslationOptions`, with the following setters: - `setGlossaryId()` is also available for backward-compatibility, accepting a string containing the glossary ID. - `setContext()`: specifies additional context to influence translations, that is not - translated itself. Note this is an **alpha feature**: it may be deprecated at - any time, or incur charges if it becomes generally available. + translated itself. Characters in the `context` parameter are not counted toward billing. See the [API documentation][api-docs-context-param] for more information and example usage. - `setTagHandling()`: type of tags to parse before translation, options are From 544becbd7769cdd0dff238b5f567f3da89e6471b Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 20 Jun 2024 16:20:31 +0200 Subject: [PATCH 052/121] Remove alpha label from context parameter in TextTranslationOptions --- .../src/main/java/com/deepl/api/TextTranslationOptions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index b2a8e2f..937e879 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -65,9 +65,9 @@ public TextTranslationOptions setGlossary(String glossaryId) { } /** - * Specifies additional context to influence translations, that is not translated itself. Note: - * this is an alpha feature: it may be deprecated at any time, or incur charges if it becomes - * generally available. See the API documentation for more information and example usage. + * Specifies additional context to influence translations, that is not translated itself. + * Characters in the `context` parameter are not counted toward billing. + * See the API documentation for more information and example usage. */ public TextTranslationOptions setContext(String context) { this.context = context; From c4e51edd110fafea2f8342b0f06824edf0650a78 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 25 Jun 2024 12:10:29 +0200 Subject: [PATCH 053/121] refactor: fix Spotless error --- .../src/main/java/com/deepl/api/TextTranslationOptions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index 937e879..1baebe1 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -66,8 +66,8 @@ public TextTranslationOptions setGlossary(String glossaryId) { /** * Specifies additional context to influence translations, that is not translated itself. - * Characters in the `context` parameter are not counted toward billing. - * See the API documentation for more information and example usage. + * Characters in the `context` parameter are not counted toward billing. See the API documentation + * for more information and example usage. */ public TextTranslationOptions setContext(String context) { this.context = context; From 0be4c42bfe2fadb7ccae1c870156824f00a4d685 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 24 May 2024 15:19:40 +0100 Subject: [PATCH 054/121] ci: Add GH workflow --- .github/workflows/run_ci.yml | 159 +++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 .github/workflows/run_ci.yml diff --git a/.github/workflows/run_ci.yml b/.github/workflows/run_ci.yml new file mode 100644 index 0000000..b920fdf --- /dev/null +++ b/.github/workflows/run_ci.yml @@ -0,0 +1,159 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '20 0 * * *' + +env: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + JAVA_TOOL_OPTIONS: "" + SECRET_DETECTION_JSON_REPORT_FILE: "gitleaks.json" + +jobs: + spotless: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Spotless check + run: ./gradlew spotlessCheck + + license_check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: License check + run: | + ./license_checker.sh '*.java' | tee license_check_output.txt + [ ! -s license_check_output.txt ] + + secret_detection: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install and run secret detection + run: | + wget https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_linux_x64.tar.gz + tar -xzf gitleaks_8.18.4_linux_x64.tar.gz + EXITCODE=0 + ./gitleaks detect -r ${SECRET_DETECTION_JSON_REPORT_FILE} --source . --log-opts="--all --full-history" || EXITCODE=$? + if [[ $EXITCODE -ne 0 ]]; then + exit $EXITCODE + fi + - name: Upload secret detection artifact + uses: actions/upload-artifact@v4 + with: + name: secret-detection-results + path: gitleaks.json + + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build + run: ./gradlew assemble + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: deepl-java/build/ + + + +# Test and `gradlew publish` stage are disabled for now. Code needs to be tested + +####################################################### +# test_cl: +# runs-on: ${{ matrix.docker-image }} +# strategy: +# matrix: +# docker-image: +# - 'eclipse-temurin:8-focal' +# - 'openjdk:8-alpine' +# - 'eclipse-temurin:11-alpine' +# - 'eclipse-temurin:17-alpine' +# - 'eclipse-temurin:18-alpine' +# - 'eclipse-temurin:19-alpine' +# use-mock-server: +# - '' +# - 'use mock server' +# env: +# DEEPL_SERVER_URL: http://deepl-mock:3000 +# DEEPL_MOCK_SERVER_PORT: 3000 +# DEEPL_PROXY_URL: http://deepl-mock:3001 +# DEEPL_MOCK_PROXY_SERVER_PORT: 3001 +# steps: +# - name: Checkout +# uses: actions/checkout@v4 +# - name: Start mock server +# if: ${{ matrix.use-mock-server == 'use mock server' }} +# run: docker run --name deepl-mock -d -p 3000:3000 deepl-mock +# - name: Start mock proxy server +# if: ${{ matrix.use-mock-server == 'use mock server' }} +# run: docker run --name deepl-mock-proxy -d -p 3001:3001 deepl-mock-proxy +# - name: Test +# run: | +# if [[ ! -z "${{ matrix.use-mock-server }}" ]]; then +# echo "Using mock server" +# export DEEPL_SERVER_URL=http://deepl-mock:3000 +# export DEEPL_MOCK_SERVER_PORT=3000 +# export DEEPL_PROXY_URL=http://deepl-mock:3001 +# export DEEPL_MOCK_PROXY_SERVER_PORT=3001 +# fi +# ./gradlew test +# - name: Stop mock proxy server +# if: ${{ matrix.use-mock-server == 'use mock server' }} +# run: docker stop deepl-mock-proxy +# - name: Stop mock server +# if: ${{ matrix.use-mock-server == 'use mock server' }} +# run: docker stop deepl-mock +# - name: Upload test results +# uses: actions/upload-artifact@v4 +# with: +# name: test-results +# path: deepl-java/build/reports/tests/test + +# test_examples: +# runs-on: ${{ matrix.docker-image }} +# strategy: +# matrix: +# docker-image: +# - 'maven:3.9' +# - 'maven:3.8-openjdk-18-slim' +# - 'maven:3.8-openjdk-8-slim' +# - 'maven:3.9-sapmachine-17' +# - 'maven:3.9-eclipse-temurin-8' +# - 'maven:3.9-eclipse-temurin-21' +# steps: +# - name: Checkout +# uses: actions/checkout@v4 +# - name: Test examples +# run: | +# cd examples/maven/deepl-test-app +# mvn install -B -PbuildProject -l mvn_build.log +# mvn verify -PrunIntegrationTests +# - name: Upload test results +# uses: actions/upload-artifact@v4 +# with: +# name: test-results +# path: examples/maven/deepl-test-app/mvn_build.log + +# publish: +# runs-on: ubuntu-latest +# needs: [ build ] +# if: startsWith(github.ref, 'refs/tags/v') +# steps: +# - name: Checkout +# uses: actions/checkout@v4 +# - name: Publish +# run: ./gradlew publish From b4c1764e89e28c914f1d05a8c5b2362f44a746b9 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 15 Aug 2024 11:05:31 +0200 Subject: [PATCH 055/121] Revert "chore(deps): bump org.mockito:mockito-inline from 4.11.0 to 5.2.0" --- deepl-java/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 514ee09..4e23fc6 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -26,7 +26,8 @@ repositories { dependencies { implementation("org.jetbrains:annotations:20.1.0") testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") - testImplementation("org.mockito:mockito-inline:5.2.0") + testImplementation("org.mockito:mockito-inline:4.11.0") + // implementation("com.google.guava:guava:30.1.1-jre") implementation("com.google.code.gson:gson:2.10.1") } From bec8d3da60971dd6a0db00d82cfbd7ce742b6d50 Mon Sep 17 00:00:00 2001 From: Lukas Bolz Date: Fri, 30 Aug 2024 09:24:33 +0200 Subject: [PATCH 056/121] fix: issue#44 integer overflow in usage request - `Usage.Detail::count` and `Usage.Detail::limit` were declared as `long` but parsed as `int`. - This seems to be caused by a176b6de, where the type was changed from `int` to `long` without adjusting the parsing. - This caused an integer overflow, resulting in a negative value when e.g. the character limit exceeded `Integer::MAX_VAlUE`. --- deepl-java/src/main/java/com/deepl/api/parsing/Parser.java | 5 +++++ .../main/java/com/deepl/api/parsing/UsageDeserializer.java | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index ffd7814..04638c1 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -76,6 +76,11 @@ public String parseErrorMessage(String json) { return jsonObject.get(parameterName).getAsInt(); } + static @Nullable Long getAsLongOrNull(JsonObject jsonObject, String parameterName) { + if (!jsonObject.has(parameterName)) return null; + return jsonObject.get(parameterName).getAsLong(); + } + static @Nullable String getAsStringOrNull(JsonObject jsonObject, String parameterName) { if (!jsonObject.has(parameterName)) return null; return jsonObject.get(parameterName).getAsString(); diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java index 1f1db7d..f28d119 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java @@ -25,8 +25,8 @@ public Usage deserialize(JsonElement json, Type typeOfT, JsonDeserializationCont } public static @Nullable Usage.Detail createDetail(JsonObject jsonObject, String prefix) { - Integer count = Parser.getAsIntOrNull(jsonObject, prefix + "count"); - Integer limit = Parser.getAsIntOrNull(jsonObject, prefix + "limit"); + Long count = Parser.getAsLongOrNull(jsonObject, prefix + "count"); + Long limit = Parser.getAsLongOrNull(jsonObject, prefix + "limit"); if (count == null || limit == null) return null; return new Usage.Detail(count, limit); } From 27037ac7cfa9d2666173b3a856e1450e11bad8fb Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 5 Sep 2024 10:53:33 +0200 Subject: [PATCH 057/121] test: add test case for usage with integer overflow --- .../src/test/java/com/deepl/api/GeneralTest.java | 15 +++++++++++++++ .../test/java/com/deepl/api/SessionOptions.java | 12 ++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index d587a0f..d3f4016 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -86,6 +86,21 @@ void testUsage() throws DeepLException, InterruptedException { Assertions.assertTrue(usage.toString().contains("Usage this billing period")); } + @Test + void testUsageLarge() throws DeepLException, InterruptedException { + Assumptions.assumeTrue(isMockServer); + SessionOptions sessionOptions = new SessionOptions(); + sessionOptions.initCharacterLimit = 1000000000000L; + Map headers = sessionOptions.createSessionHeaders(); + + TranslatorOptions options = new TranslatorOptions().setHeaders(headers).setServerUrl(serverUrl); + String authKeyWithUuid = authKey + "/" + UUID.randomUUID().toString(); + Translator translator = new Translator(authKeyWithUuid, options); + Usage usage = translator.getUsage(); + Assertions.assertNotNull(usage.getCharacter()); + Assertions.assertEquals(sessionOptions.initCharacterLimit, usage.getCharacter().getLimit()); + } + @Test void testGetSourceAndTargetLanguages() throws DeepLException, InterruptedException { Translator translator = createTranslator(); diff --git a/deepl-java/src/test/java/com/deepl/api/SessionOptions.java b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java index 83d0116..5fc8d95 100644 --- a/deepl-java/src/test/java/com/deepl/api/SessionOptions.java +++ b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java @@ -12,9 +12,9 @@ public class SessionOptions { // Mock server session options public Integer noResponse; public Integer respondWith429; - public Integer initCharacterLimit; - public Integer initDocumentLimit; - public Integer initTeamDocumentLimit; + public Long initCharacterLimit; + public Long initDocumentLimit; + public Long initTeamDocumentLimit; public Integer documentFailure; public Duration documentQueueTime; public Duration documentTranslateTime; @@ -76,17 +76,17 @@ public SessionOptions setRespondWith429(int respondWith429) { return this; } - public SessionOptions setInitCharacterLimit(int initCharacterLimit) { + public SessionOptions setInitCharacterLimit(long initCharacterLimit) { this.initCharacterLimit = initCharacterLimit; return this; } - public SessionOptions setInitDocumentLimit(int initDocumentLimit) { + public SessionOptions setInitDocumentLimit(long initDocumentLimit) { this.initDocumentLimit = initDocumentLimit; return this; } - public SessionOptions setInitTeamDocumentLimit(int initTeamDocumentLimit) { + public SessionOptions setInitTeamDocumentLimit(long initTeamDocumentLimit) { this.initTeamDocumentLimit = initTeamDocumentLimit; return this; } From c2f0bd5fde30216f1c56098de942cd4fc5f1f407 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 5 Sep 2024 10:32:33 +0200 Subject: [PATCH 058/121] docs: Increase version to 1.5.1 --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7983f37..ad700ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.1] - 2024-09-05 +### Fixed +* Fixed parsing for usage count and limit for large values. + * Thanks to [lubo-dev](https://github.com/lubo-dev) in [#45](https://github.com/DeepLcom/deepl-java/pull/45). + + ## [1.5.0] - 2024-04-10 ### Added * New language available: Arabic (MSA) (`'ar'`). Add language code constants and tests. @@ -104,6 +110,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[1.5.1]: https://github.com/DeepLcom/deepl-java/compare/v1.5.0...v1.5.1 [1.5.0]: https://github.com/DeepLcom/deepl-java/compare/v1.4.0...v1.5.0 [1.4.0]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/DeepLcom/deepl-java/compare/v1.2.0...v1.3.0 diff --git a/README.md b/README.md index 25210f0..9b6441f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.5.0" +implementation "com.deepl.api:deepl-java:1.5.1" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.5.0 + 1.5.1 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 4e23fc6..13c5faf 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.5.0" +version = "1.5.1" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 0ddd919..da972d5 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -82,7 +82,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.5.0"); + sb.append("deepl-java/1.5.1"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From e79f522cea14434c267c49bc1f918997d7538f31 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 15 Sep 2024 22:16:47 +0200 Subject: [PATCH 059/121] feat: add billed characters to translate text response --- CHANGELOG.md | 6 ++++++ README.md | 9 ++++++--- deepl-java/src/main/java/com/deepl/api/TextResult.java | 9 ++++++++- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 ++ .../com/deepl/api/parsing/TextResultDeserializer.java | 3 ++- deepl-java/src/test/java/com/deepl/api/GeneralTest.java | 1 + .../src/test/java/com/deepl/api/TranslateTextTest.java | 1 + 7 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad700ba..3e66bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +* Added `getBilledCharacters()` to text translation response. + + ## [1.5.1] - 2024-09-05 ### Fixed * Fixed parsing for usage count and limit for large values. @@ -110,6 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.5.1...HEAD [1.5.1]: https://github.com/DeepLcom/deepl-java/compare/v1.5.0...v1.5.1 [1.5.0]: https://github.com/DeepLcom/deepl-java/compare/v1.4.0...v1.5.0 [1.4.0]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...v1.4.0 diff --git a/README.md b/README.md index 9b6441f..6b62507 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,10 @@ There are additional optional arguments to control translation, see [Text translation options](#text-translation-options) below. `translateText()` returns a `TextResult`, or a List of `TextResult`s -corresponding to your input text(s). `TextResult` has two accessors: `getText()` -returns the translated text, and `getDetectedSourceLanguage()` returns the -detected source language code. +corresponding to your input text(s). `TextResult` has the following accessors: +- `getText()` returns the translated text, +- `getDetectedSourceLanguage()` returns the detected source language code, and +- `getBilledCharacters()` returns the number of characters billed for the text. ```java class Example { // Continuing class Example from above @@ -115,8 +116,10 @@ class Example { // Continuing class Example from above "en-GB"); System.out.println(results.get(0).getText()); // "How are you?" System.out.println(results.get(0).getDetectedSourceLanguage()); // "ja" the language code for Japanese + System.out.println(results.get(0).getBilledCharacters()); // 7 - the number of characters in the source text "お元気ですか?" System.out.println(results.get(1).getText()); // "How are you?" System.out.println(results.get(1).getDetectedSourceLanguage()); // "es" the language code for Spanish + System.out.println(results.get(1).getBilledCharacters()); // 12 - the number of characters in the source text "¿Cómo estás?" // Translate into German with less and more Formality: System.out.println(translator.translateText("How are you?", diff --git a/deepl-java/src/main/java/com/deepl/api/TextResult.java b/deepl-java/src/main/java/com/deepl/api/TextResult.java index d7c8b6b..be4b427 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextResult.java +++ b/deepl-java/src/main/java/com/deepl/api/TextResult.java @@ -7,11 +7,13 @@ public class TextResult { private final String text; private final String detectedSourceLanguage; + private final int billedCharacters; /** Constructs a new instance. */ - public TextResult(String text, String detectedSourceLanguage) { + public TextResult(String text, String detectedSourceLanguage, int billedCharacters) { this.text = text; this.detectedSourceLanguage = LanguageCode.standardize(detectedSourceLanguage); + this.billedCharacters = billedCharacters; } /** The translated text. */ @@ -23,4 +25,9 @@ public String getText() { public String getDetectedSourceLanguage() { return detectedSourceLanguage; } + + /** Number of characters billed for this text. */ + public int getBilledCharacters() { + return billedCharacters; + } } diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index da972d5..3fdc966 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -782,6 +782,8 @@ private static ArrayList> createHttpParams( if (text.isEmpty()) throw new IllegalArgumentException("text must not be empty"); params.add(new KeyValuePair<>("text", text)); }); + // Always send show_billed_characters=1, remove when the API default is changed to true + params.add(new KeyValuePair<>("show_billed_characters", "1")); if (options != null) { // Note: formality and glossaryId are added above diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java index 15d6180..84c562d 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java @@ -18,6 +18,7 @@ public TextResult deserialize(JsonElement json, Type typeOfT, JsonDeserializatio JsonObject jsonObject = json.getAsJsonObject(); return new TextResult( jsonObject.get("text").getAsString(), - jsonObject.get("detected_source_language").getAsString()); + jsonObject.get("detected_source_language").getAsString(), + jsonObject.get("billed_characters").getAsInt()); } } diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index d3f4016..2cce7f2 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -45,6 +45,7 @@ void testExampleTranslation() throws DeepLException, InterruptedException { String sourceLang = LanguageCode.removeRegionalVariant(entry.getKey()); TextResult result = translator.translateText(inputText, sourceLang, "en-US"); Assertions.assertTrue(result.getText().toLowerCase(Locale.ENGLISH).contains("proton")); + Assertions.assertEquals(inputText.length(), result.getBilledCharacters()); } } diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index 666a4cf..7b9b222 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -17,6 +17,7 @@ void testSingleText() throws DeepLException, InterruptedException { TextResult result = translator.translateText(exampleText.get("en"), null, LanguageCode.German); Assertions.assertEquals(exampleText.get("de"), result.getText()); Assertions.assertEquals("en", result.getDetectedSourceLanguage()); + Assertions.assertEquals(exampleText.get("en").length(), result.getBilledCharacters()); } @Test From 65bf56b3481c6ea85c21a14d1eff291873623d14 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 17 Sep 2024 10:33:39 +0200 Subject: [PATCH 060/121] docs: Increase version to 1.6.0 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e66bca..a721c84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.6.0] - 2024-09-17 ### Added * Added `getBilledCharacters()` to text translation response. @@ -115,7 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.5.1...HEAD +[1.6.0]: https://github.com/DeepLcom/deepl-java/compare/v1.5.1...v1.6.0 [1.5.1]: https://github.com/DeepLcom/deepl-java/compare/v1.5.0...v1.5.1 [1.5.0]: https://github.com/DeepLcom/deepl-java/compare/v1.4.0...v1.5.0 [1.4.0]: https://github.com/DeepLcom/deepl-java/compare/v1.3.0...v1.4.0 diff --git a/README.md b/README.md index 6b62507..9f6900a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.5.1" +implementation "com.deepl.api:deepl-java:1.6.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.5.1 + 1.6.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 13c5faf..5a79e33 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.5.1" +version = "1.6.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 3fdc966..342f9ec 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -82,7 +82,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.5.1"); + sb.append("deepl-java/1.6.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 1a4bd869ac2009a21d0072a4e67a7551dd68a0ba Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Mon, 11 Nov 2024 16:44:08 +0000 Subject: [PATCH 061/121] feat: Add modelType parameter to translateText() --- CHANGELOG.md | 10 +++++++++ README.md | 12 ++++++++-- .../main/java/com/deepl/api/TextResult.java | 15 ++++++++++++- .../com/deepl/api/TextTranslationOptions.java | 20 +++++++++++++++++ .../main/java/com/deepl/api/Translator.java | 3 +++ .../api/parsing/TextResultDeserializer.java | 4 +++- .../test/java/com/deepl/api/GeneralTest.java | 22 +++++++++++++++++-- 7 files changed, 80 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a721c84..e263d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added +* Added `modelType` option to `translateText()` to use models with higher + translation quality (available for some language pairs), or better latency. + Options are `'quality_optimized'`, `'latency_optimized'`, and `'prefer_quality_optimized'` +* Added the `modelTypeUsed` field to `translateText()` response, that + indicates the translation model used when the `modelType` option is + specified. + + ## [1.6.0] - 2024-09-17 ### Added * Added `getBilledCharacters()` to text translation response. diff --git a/README.md b/README.md index 9f6900a..02d96cb 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,9 @@ There are additional optional arguments to control translation, see `translateText()` returns a `TextResult`, or a List of `TextResult`s corresponding to your input text(s). `TextResult` has the following accessors: - `getText()` returns the translated text, -- `getDetectedSourceLanguage()` returns the detected source language code, and -- `getBilledCharacters()` returns the number of characters billed for the text. +- `getDetectedSourceLanguage()` returns the detected source language code, +- `getBilledCharacters()` returns the number of characters billed for the text, and +- `getModelTypeUsed()` returns the model type used for the translation. ```java class Example { // Continuing class Example from above @@ -166,6 +167,13 @@ a `TextTranslationOptions`, with the following setters: translated itself. Characters in the `context` parameter are not counted toward billing. See the [API documentation][api-docs-context-param] for more information and example usage. +- `model_type`: specifies the type of translation model to use, options are: + - `'quality_optimized'`: use a translation model that maximizes translation quality, + at the cost of response time. This option may be unavailable for some language pairs. + - `'prefer_quality_optimized'`: use the highest-quality translation model for the given + language pair. + - `'latency_optimized'`: use a translation model that minimizes response time, at the + cost of translation quality. - `setTagHandling()`: type of tags to parse before translation, options are `"html"` and `"xml"`. diff --git a/deepl-java/src/main/java/com/deepl/api/TextResult.java b/deepl-java/src/main/java/com/deepl/api/TextResult.java index be4b427..dee38f3 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextResult.java +++ b/deepl-java/src/main/java/com/deepl/api/TextResult.java @@ -3,17 +3,25 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import org.jetbrains.annotations.Nullable; + /** The result of a text translation. */ public class TextResult { private final String text; private final String detectedSourceLanguage; private final int billedCharacters; + private final @Nullable String modelTypeUsed; /** Constructs a new instance. */ - public TextResult(String text, String detectedSourceLanguage, int billedCharacters) { + public TextResult( + String text, + String detectedSourceLanguage, + int billedCharacters, + @Nullable String modelTypeUsed) { this.text = text; this.detectedSourceLanguage = LanguageCode.standardize(detectedSourceLanguage); this.billedCharacters = billedCharacters; + this.modelTypeUsed = modelTypeUsed; } /** The translated text. */ @@ -30,4 +38,9 @@ public String getDetectedSourceLanguage() { public int getBilledCharacters() { return billedCharacters; } + + /** Model type used for the translation of this text. */ + public @Nullable String getModelTypeUsed() { + return modelTypeUsed; + } } diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index 1baebe1..7d95788 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -20,6 +20,7 @@ public class TextTranslationOptions { private boolean preserveFormatting = false; private String context; private String tagHandling; + private String modelType; private boolean outlineDetection = true; private Iterable ignoreTags; private Iterable nonSplittingTags; @@ -106,6 +107,20 @@ public TextTranslationOptions setTagHandling(String tagHandling) { return this; } + /** + * Set the type of model to use for a text translation. Currently supported values: + * "quality_optimized" use a translation model that maximizes translation quality, at the + * cost of response time. This option may be unavailable for some language pairs and the API would + * respond with an error in this case; "prefer_quality_optimized" use the + * highest-quality translation model for the given language pair; "latency_optimized" + * use a translation model that minimizes response time, at the cost of translation + * quality. + */ + public TextTranslationOptions setModelType(String modelType) { + this.modelType = modelType; + return this; + } + /** * Sets whether outline detection is used; set to false to disable automatic tag * detection, default is true. @@ -167,6 +182,11 @@ public String getContext() { return context; } + /** Gets the current model type. */ + public String getModelType() { + return modelType; + } + /** Gets the current tag handling setting. */ public String getTagHandling() { return tagHandling; diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 342f9ec..287ad9b 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -808,6 +808,9 @@ private static ArrayList> createHttpParams( if (options.getContext() != null) { params.add(new KeyValuePair<>("context", options.getContext())); } + if (options.getModelType() != null) { + params.add(new KeyValuePair<>("model_type", options.getModelType())); + } if (options.getTagHandling() != null) { params.add(new KeyValuePair<>("tag_handling", options.getTagHandling())); } diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java index 84c562d..22cd42d 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java @@ -16,9 +16,11 @@ class TextResultDeserializer implements JsonDeserializer { public TextResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject jsonObject = json.getAsJsonObject(); + JsonElement modelType = jsonObject.get("model_type_used"); return new TextResult( jsonObject.get("text").getAsString(), jsonObject.get("detected_source_language").getAsString(), - jsonObject.get("billed_characters").getAsInt()); + jsonObject.get("billed_characters").getAsInt(), + modelType != null ? (modelType.getAsString()) : null); } } diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 2cce7f2..904c8b0 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. package com.deepl.api; -import static org.mockito.Mockito.*; - import java.io.*; import java.net.*; import java.time.*; @@ -13,6 +11,7 @@ import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.MockedConstruction; import org.mockito.Mockito; @@ -49,6 +48,25 @@ void testExampleTranslation() throws DeepLException, InterruptedException { } } + @ParameterizedTest + @CsvSource({ + "quality_optimized,quality_optimized", + "prefer_quality_optimized,quality_optimized", + "latency_optimized,latency_optimized" + }) + void testModelType(String modelTypeArg, String expectedModelType) + throws DeepLException, InterruptedException { + Translator translator = createTranslator(); + String sourceLang = "de"; + TextResult result = + translator.translateText( + exampleText.get(sourceLang), + sourceLang, + "en-US", + new TextTranslationOptions().setModelType(modelTypeArg)); + Assertions.assertEquals(expectedModelType, result.getModelTypeUsed()); + } + @Test void testInvalidServerUrl() { Assertions.assertThrows( From 40997d8d0c6ad2a972cc05e322cfec6a9e9c9e30 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 15 Nov 2024 11:19:46 +0000 Subject: [PATCH 062/121] docs: Increase version to 1.7.0 --- CHANGELOG.md | 3 ++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e263d58..c95d20d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [1.7.0] - 2024-11-15 ### Added * Added `modelType` option to `translateText()` to use models with higher translation quality (available for some language pairs), or better latency. @@ -125,6 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[1.7.0]: https://github.com/DeepLcom/deepl-java/compare/v1.6.0...v1.7.0 [1.6.0]: https://github.com/DeepLcom/deepl-java/compare/v1.5.1...v1.6.0 [1.5.1]: https://github.com/DeepLcom/deepl-java/compare/v1.5.0...v1.5.1 [1.5.0]: https://github.com/DeepLcom/deepl-java/compare/v1.4.0...v1.5.0 diff --git a/README.md b/README.md index 02d96cb..60e11cd 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.6.0" +implementation "com.deepl.api:deepl-java:1.7.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.6.0 + 1.7.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 5a79e33..6941fc9 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.6.0" +version = "1.7.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 287ad9b..b9b7c85 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -82,7 +82,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.6.0"); + sb.append("deepl-java/1.7.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From d826569c134d1acd7858cef45a65962344c6f8f2 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Mon, 13 Jan 2025 14:42:52 +0000 Subject: [PATCH 063/121] feat: Add write API to java CL --- CHANGELOG.md | 11 ++ README.md | 146 ++++++++++++------ .../main/java/com/deepl/api/DeepLClient.java | 62 ++++++++ .../com/deepl/api/DeepLClientOptions.java | 7 + .../com/deepl/api/TextRephraseOptions.java | 49 ++++++ .../main/java/com/deepl/api/Translator.java | 15 +- .../java/com/deepl/api/TranslatorOptions.java | 3 + .../main/java/com/deepl/api/WriteResult.java | 33 ++++ .../main/java/com/deepl/api/WritingStyle.java | 27 ++++ .../main/java/com/deepl/api/WritingTone.java | 27 ++++ .../java/com/deepl/api/parsing/Parser.java | 6 + .../com/deepl/api/parsing/WriteResponse.java | 16 ++ .../api/parsing/WriteResultDeserializer.java | 24 +++ .../java/com/deepl/api/RephraseTextTest.java | 66 ++++++++ .../src/test/java/com/deepl/api/TestBase.java | 35 +++++ 15 files changed, 475 insertions(+), 52 deletions(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/DeepLClient.java create mode 100644 deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TextRephraseOptions.java create mode 100644 deepl-java/src/main/java/com/deepl/api/WriteResult.java create mode 100644 deepl-java/src/main/java/com/deepl/api/WritingStyle.java create mode 100644 deepl-java/src/main/java/com/deepl/api/WritingTone.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/WriteResponse.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/WriteResultDeserializer.java create mode 100644 deepl-java/src/test/java/com/deepl/api/RephraseTextTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c95d20d..1b53884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +* Added support for the Write API in the client library, the implementation + can be found in the `DeepLClient` class. Please refer to the README for usage + instructions. +### Changed +* The main functionality of the library is now also exposed via the `DeepLClient` + class. Please change your code to use this over the `Translator` class whenever + convenient. + ## [1.7.0] - 2024-11-15 ### Added * Added `modelType` option to `translateText()` to use models with higher @@ -125,6 +135,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.7.0...HEAD [1.7.0]: https://github.com/DeepLcom/deepl-java/compare/v1.6.0...v1.7.0 [1.6.0]: https://github.com/DeepLcom/deepl-java/compare/v1.5.1...v1.6.0 [1.5.1]: https://github.com/DeepLcom/deepl-java/compare/v1.5.0...v1.5.1 diff --git a/README.md b/README.md index 60e11cd..2209618 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.deepl.api/deepl-java.svg)](https://mvnrepository.com/artifact/com.deepl.api/deepl-java) [![License: MIT](https://img.shields.io/badge/license-MIT-blueviolet.svg)](https://github.com/DeepLcom/deepl-java/blob/main/LICENSE) -The [DeepL API][api-docs] is a language translation API that allows other -computer programs to send texts and documents to DeepL's servers and receive -high-quality translations. This opens a whole universe of opportunities for -developers: any translation product you can imagine can now be built on top of -DeepL's best-in-class translation technology. +The [DeepL API][api-docs] is a language AI API that allows other computer programs +to send texts and documents to DeepL's servers and receive high-quality +translations and improvements to the text. This opens a whole universe of +opportunities for developers: any translation product you can imagine can now +be built on top of DeepL's best-in-class translation technology. The DeepL Java library offers a convenient way for applications written in Java to interact with the DeepL API. We intend to support all API functions with the @@ -18,7 +18,7 @@ they’re added to the API. To use the DeepL Java Library, you'll need an API authentication key. To get a key, [please create an account here][create-account]. With a DeepL API Free -account you can translate up to 500,000 characters/month for free. +account you can consume up to 500,000 characters/month for free. ## Requirements @@ -48,7 +48,7 @@ Add this dependency to your project's POM: ## Usage -Import the package and construct a `Translator`. The first argument is a string +Import the package and construct a `DeepLClient`. The first argument is a string containing your API authentication key as found in your [DeepL Pro Account][pro-account]. @@ -58,13 +58,13 @@ Be careful not to expose your key, for example when sharing source code. import com.deepl.api.*; class Example { - Translator translator; + DeepLClient client; public Example() throws Exception { String authKey = "f63c02c5-f056-..."; // Replace with your key - translator = new Translator(authKey); + client = new DeepLClient(authKey); TextResult result = - translator.translateText("Hello, world!", null, "fr"); + client.translateText("Hello, world!", null, "fr"); System.out.println(result.getText()); // "Bonjour, le monde !" } } @@ -74,7 +74,7 @@ This example is for demonstration purposes only. In production code, the authentication key should not be hard-coded, but instead fetched from a configuration file or environment variable. -`Translator` accepts additional options, see [Configuration](#configuration) +`DeepLClient` accepts additional options, see [Configuration](#configuration) for more information. ### Translating text @@ -107,12 +107,12 @@ class Example { // Continuing class Example from above public void textTranslationExamples() throws Exception { // Translate text into a target language, in this case, French: TextResult result = - translator.translateText("Hello, world!", null, "fr"); + client.translateText("Hello, world!", null, "fr"); System.out.println(result.getText()); // "Bonjour, le monde !" // Translate multiple texts into British English List results = - translator.translateText(List.of("お元気ですか?", "¿Cómo estás?"), + client.translateText(List.of("お元気ですか?", "¿Cómo estás?"), null, "en-GB"); System.out.println(results.get(0).getText()); // "How are you?" @@ -123,12 +123,12 @@ class Example { // Continuing class Example from above System.out.println(results.get(1).getBilledCharacters()); // 12 - the number of characters in the source text "¿Cómo estás?" // Translate into German with less and more Formality: - System.out.println(translator.translateText("How are you?", + System.out.println(client.translateText("How are you?", null, "de", new TextTranslationOptions().setFormality( Formality.Less)).getText()); // 'Wie geht es dir?' - System.out.println(translator.translateText("How are you?", + System.out.println(client.translateText("How are you?", null, "de", new TextTranslationOptions().setFormality( @@ -193,6 +193,60 @@ The following options are only used if `setTagHandling()` is set to `'xml'`: For a detailed explanation of the XML handling options, see the [API documentation][api-docs-xml-handling]. +### Improving text (Write API) + +You can use the Write API to improve or rephrase text. This is implemented in +the `rephraseText()` method. The first argument is a string containing the text +you want to translate, or a list of strings if you want to translate multiple texts. + +`targetLang` optionally specifies the target language, e.g. when you want to change +the variant of a text (for example, you can send an english text to the write API and +use `targetLang` to turn it into British or American English). Please note that the +Write API itself does NOT translate. If you wish to translate and improve a text, you +will need to make multiple calls in a chain. + +Language codes are the same as for translating text. + +Example call: + +```java +WriteResult result = client.rephraseText("A rainbouw has seven colours.", "EN-US", null); +System.out.println(result.getText); +``` + +Additionally, you can optionally specify a style OR a tone (not both at once) that the +improvement should be in. The following styles are supported (`default` will be used if +nothing is selected): + +- `academic` +- `business` +- `casual` +- `default` +- `simple` + +The following tones are supported (`default` will be used if nothing is selected): + +- `confident` +- `default` +- `diplomatic` +- `enthusiastic` +- `friendly` + +You can also prefix any non-default style or tone with `prefer_` (`prefer_academic`, etc.), +in which case the style/tone will only be applied if the language supports it. If you do not +use `prefer_`, requests with `targetLang`s or detected languages that do not support +styles and tones will fail. The current list of supported languages can be found in our +[API documentation][api-docs]. We plan to also expose this information via an API endpoint +in the future. + +You can use the predefined constants in the library to use a style: + +```java +TextRephraseOptions options = (new TextRephraseOptions()).setWritingStyle(WritingStyle.Business.getValue()); +WriteResult result = client.rephraseText("A rainbouw has seven colours.", "EN-US", options); +System.out.println(result.getText); +``` + ### Translating documents To translate documents, call `translateDocument()` File objects. The first and @@ -211,7 +265,7 @@ class Example { // Continuing class Example from above File inputFile = new File("/path/to/Instruction Manual.docx"); File outputFile = new File("/path/to/Bedienungsanleitung.docx"); try { - translator.translateDocument(inputFile, outputFile, "en", "de"); + client.translateDocument(inputFile, outputFile, "en", "de"); } catch (DocumentTranslationException exception) { // If an error occurs during document translation after the document was // already uploaded, a DocumentTranslationException is thrown. The @@ -281,7 +335,7 @@ class Example { // Continuing class Example from above put("prize", "Gewinn"); }}; GlossaryInfo myGlossary = - translator.createGlossary("My glossary", "en", "de", entries); + client.createGlossary("My glossary", "en", "de", entries); System.out.printf("Created '%s' (%s) %s->%s containing %d entries\n", myGlossary.getName(), @@ -307,7 +361,7 @@ class Example { // Continuing class Example from above public createGlossaryFromCsvExample() throws Exception { File csvFile = new File("/path/to/glossary_file.csv"); GlossaryInfo myGlossary = - translator.createGlossaryFromCsv("My glossary", + client.createGlossaryFromCsv("My glossary", "en", "de", csvFile); @@ -335,13 +389,13 @@ class Example { // Continuing class Example from above public getListDeleteGlossaryExamples() throws Exception { // Retrieve a stored glossary using the ID String glossaryId = "559192ed-8e23-..."; - GlossaryInfo myGlossary = translator.getGlossary(glossaryId); + GlossaryInfo myGlossary = client.getGlossary(glossaryId); // Find and delete glossaries named 'Old glossary' - List glossaries = translator.listGlossaries(); + List glossaries = client.listGlossaries(); for (GlossaryInfo glossary : glossaries) { if (glossary.getName() == "Old glossary") { - translator.deleteGlossary(glossary); + client.deleteGlossary(glossary); } } } @@ -360,7 +414,7 @@ ID: ```java class Example { // Continuing class Example from above public getGlossaryEntriesExample() throws Exception { - GlossaryEntries entries = translator.getGlossaryEntries(myGlossary); + GlossaryEntries entries = client.getGlossaryEntries(myGlossary); for (Map.Entry entry : entries.entrySet()) { System.out.println(entry.getKey() + ":" + entry.getValue()); @@ -385,12 +439,12 @@ class Example { // Continuing class Example from above TextTranslationOptions options = new TextTranslationOptions().setGlossary(my_glossary); TextResult resultWithGlossary = - translator.translateText(text, "en", "de", options); + client.translateText(text, "en", "de", options); System.out.println(resultWithGlossary.getText()); // "Der Maler wurde mit einem Gewinn ausgezeichnet." // For comparison, the result without a glossary: TextResult resultWithoutGlossary = - translator.translateText(text, "en", "de"); + client.translateText(text, "en", "de"); System.out.println(resultWithoutGlossary.getText()); // "Der Künstler wurde mit einem Preis ausgezeichnet." } } @@ -408,7 +462,7 @@ class Example { // Continuing class Example from above File inputFile = new File("/path/to/Instruction Manual.docx"); File outputFile = new File("/path/to/Bedienungsanleitung.docx"); - translator.translateDocument(inputFile, + client.translateDocument(inputFile, outputFile, "en", "de", @@ -439,7 +493,7 @@ the `any_limit_reached` property to check all usage subtypes. ```java class Example { // Continuing class Example from above public void getUsageExample() throws Exception { - Usage usage = translator.getUsage(); + Usage usage = client.getUsage(); if (usage.anyLimitReached()) { System.out.println("Translation limit reached."); } @@ -471,8 +525,8 @@ optional `formality` parameter. ```java class Example { // Continuing class Example from above public void getLanguagesExample() throws Exception { - List sourceLanguages = translator.getSourceLanguages(); - List targetLanguages = translator.getTargetLanguages(); + List sourceLanguages = client.getSourceLanguages(); + List targetLanguages = client.getTargetLanguages(); System.out.println("Source languages:"); for (Language language : sourceLanguages) { System.out.printf("%s (%s)%n", @@ -508,7 +562,7 @@ of `GlossaryLanguagePair` objects. Use the `getSourceLanguage()` and class Example { // Continuing class Example from above public void getGlossaryLanguagesExample() throws Exception { List glossaryLanguages = - translator.getGlossaryLanguages(); + client.getGlossaryLanguages(); for (GlossaryLanguagePair glossaryLanguage : glossaryLanguages) { System.out.printf("%s to %s\n", glossaryLanguage.getSourceLanguage(), @@ -535,35 +589,35 @@ invalid arguments are provided, they may raise the standard exceptions ### Writing a Plugin If you use this library in an application, please identify the application with -`TranslatorOptions.setAppInfo()`, which takes the name and version of the app: +`DeepLClientOptions.setAppInfo()`, which takes the name and version of the app: ```java class Example { // Continuing class Example from above public void configurationExample() throws Exception { - TranslatorOptions options = - new TranslatorOptions().setAppInfo("my-java-translation-plugin", "1.2.3"); - Translator translator = new Translator(authKey, options); + DeepLClientOptions options = + new DeepLClientOptions().setAppInfo("my-java-translation-plugin", "1.2.3"); + DeepLClient client = new DeepLClient(authKey, options); } } ``` This information is passed along when the library makes calls to the DeepL API. Both name and version are required. Please note that setting the `User-Agent` header -via `TranslatorOptions.setHeaders()` will override this setting, if you need to use this, +via `DeepLClientOptions.setHeaders()` will override this setting, if you need to use this, please manually identify your Application in the `User-Agent` header. ### Configuration -The `Translator` constructor accepts `TranslatorOptions` as a second argument, +The `DeepLClient` constructor accepts `DeepLClientOptions` as a second argument, for example: ```java class Example { // Continuing class Example from above public void configurationExample() throws Exception { - TranslatorOptions options = - new TranslatorOptions().setMaxRetries(1).setTimeout(Duration.ofSeconds( + DeepLClientOptions options = + new DeepLClientOptions().setMaxRetries(1).setTimeout(Duration.ofSeconds( 1)); - Translator translator = new Translator(authKey, options); + DeepLClient client = new DeepLClient(authKey, options); } } ``` @@ -583,28 +637,28 @@ The available options setters are: #### Anonymous platform information -By default, we send some basic information about the platform the client library is running on with each request, see [here for an explanation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). This data is completely anonymous and only used to improve our product, not track any individual users. If you do not wish to send this data, you can opt-out when creating your `Translator` object by calling the `setSendPlatformInfo()` setter on the `TranslatorOptions` like so: +By default, we send some basic information about the platform the client library is running on with each request, see [here for an explanation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). This data is completely anonymous and only used to improve our product, not track any individual users. If you do not wish to send this data, you can opt-out when creating your `DeepLClient` object by calling the `setSendPlatformInfo()` setter on the `DeepLClientOptions` like so: ```java class Example { // Continuing class Example from above public void configurationExample() throws Exception { - TranslatorOptions options = - new TranslatorOptions().setSendPlatformInfo(false); - Translator translator = new Translator(authKey, options); + DeepLClientOptions options = + new DeepLClientOptions().setSendPlatformInfo(false); + DeepLClient client = new DeepLClient(authKey, options); } } ``` -You can also customize the `User-Agent` header by setting its value explicitly in the `TranslatorOptions` object via the header field. Example: +You can also customize the `User-Agent` header by setting its value explicitly in the `DeepLClientOptions` object via the header field. Example: ```java class Example { // Continuing class Example from above public void configurationExample() throws Exception { Map headers = new HashMap<>(); headers.put("User-Agent", "my custom user agent"); - TranslatorOptions options = - new TranslatorOptions().setHeaders(headers); - Translator translator = new Translator(authKey, options); + DeepLClientOptions options = + new DeepLClientOptions().setHeaders(headers); + DeepLClient client = new DeepLClient(authKey, options); } } ``` diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java new file mode 100644 index 0000000..8f4e190 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -0,0 +1,62 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. + +package com.deepl.api; + +import com.deepl.api.http.HttpResponse; +import com.deepl.api.utils.KeyValuePair; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.Nullable; + +public class DeepLClient extends Translator { + + /** {@inheritDoc} */ + public DeepLClient(String authKey, TranslatorOptions options) throws IllegalArgumentException { + super(authKey, options); + } + + public WriteResult rephraseText( + String text, @Nullable String targetLang, @Nullable TextRephraseOptions options) + throws InterruptedException, DeepLException { + ArrayList texts = new ArrayList<>(); + texts.add(text); + return this.rephraseText(texts, targetLang, options).get(0); + } + + public List rephraseText( + List texts, @Nullable String targetLang, @Nullable TextRephraseOptions options) + throws InterruptedException, DeepLException { + Iterable> params = + createWriteHttpParams(texts, targetLang, options); + HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/write/rephrase", params); + checkResponse(response, false, false); + return jsonParser.parseWriteResult(response.getBody()); + } + + protected static ArrayList> createWriteHttpParams( + List texts, @Nullable String targetLang, @Nullable TextRephraseOptions options) { + targetLang = LanguageCode.standardize(targetLang); + checkValidLanguages(null, targetLang); + + ArrayList> params = new ArrayList<>(); + if (targetLang != null) { + params.add(new KeyValuePair<>("target_lang", targetLang)); + } + if (options != null && options.getWritingStyle() != null) { + params.add(new KeyValuePair<>("writing_style", options.getWritingStyle())); + } + if (options != null && options.getTone() != null) { + params.add(new KeyValuePair<>("tone", options.getTone())); + } + + texts.forEach( + (text) -> { + if (text.isEmpty()) throw new IllegalArgumentException("text must not be empty"); + params.add(new KeyValuePair<>("text", text)); + }); + + return params; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java new file mode 100644 index 0000000..7349823 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java @@ -0,0 +1,7 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** {@inheritDoc} */ +public class DeepLClientOptions extends TranslatorOptions {} diff --git a/deepl-java/src/main/java/com/deepl/api/TextRephraseOptions.java b/deepl-java/src/main/java/com/deepl/api/TextRephraseOptions.java new file mode 100644 index 0000000..2cd37c4 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TextRephraseOptions.java @@ -0,0 +1,49 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** + * Options to control text rephrasing behaviour. These options may be provided to {@link + * DeepLClient#rephraseText} overloads. + * + *

All properties have corresponding setters in fluent-style, so the following is possible: + * + * TextRephraseOptions options = new TextRephraseOptions() + * .WritingStyle(WritingStyle.Business.getValue()); + * + */ +public class TextRephraseOptions { + private String writingStyle; + private String tone; + + /** + * Sets a style the improved text should be in. Note that only style OR tone can be set. + * + * @see WritingStyle + */ + public TextRephraseOptions setWritingStyle(String style) { + this.writingStyle = style; + return this; + } + + /** + * Sets a tone the improved text should be in. Note that only style OR tone can be set. + * + * @see WritingTone + */ + public TextRephraseOptions setTone(String tone) { + this.tone = tone; + return this; + } + + /** Gets the current style setting. */ + public String getWritingStyle() { + return writingStyle; + } + + /** Gets the current tone setting. */ + public String getTone() { + return tone; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index b9b7c85..c3e73d0 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -17,15 +17,18 @@ * Client for the DeepL API. To use the DeepL API, initialize an instance of this class using your * DeepL Authentication Key as found in your DeepL * account. + * + * @deprecated Use {@link DeepLClient} instead. */ +@Deprecated public class Translator { /** Base URL for DeepL API Free accounts. */ private static final String DEEPL_SERVER_URL_FREE = "https://api-free.deepl.com"; /** Base URL for DeepL API Pro accounts */ private static final String DEEPL_SERVER_URL_PRO = "https://api.deepl.com"; - private final Parser jsonParser = new Parser(); - private final HttpClientWrapper httpClientWrapper; + protected final Parser jsonParser = new Parser(); + protected final HttpClientWrapper httpClientWrapper; /** * Initializes a new Translator object using your Authentication Key. @@ -841,7 +844,7 @@ private static ArrayList> createHttpParams( * @param options Options influencing translation. * @return Iterable of parameters for HTTP request. */ - private static ArrayList> createHttpParams( + protected static ArrayList> createHttpParams( String sourceLang, String targetLang, DocumentTranslationOptions options) { return createHttpParamsCommon( sourceLang, @@ -861,7 +864,7 @@ private static ArrayList> createHttpParams( * @param glossaryId ID of glossary to use for translation. * @return Iterable of parameters for HTTP request. */ - private static ArrayList> createHttpParamsCommon( + protected static ArrayList> createHttpParamsCommon( @Nullable String sourceLang, String targetLang, @Nullable Formality formality, @@ -920,7 +923,7 @@ private static String joinTags(Iterable tags) { * @param targetLang Language code of the desired output language. * @throws IllegalArgumentException If either language code is invalid. */ - private static void checkValidLanguages(@Nullable String sourceLang, String targetLang) + protected static void checkValidLanguages(@Nullable String sourceLang, String targetLang) throws IllegalArgumentException { if (sourceLang != null && sourceLang.isEmpty()) { throw new IllegalArgumentException("sourceLang must be null or non-empty"); @@ -982,7 +985,7 @@ private void checkResponse(HttpResponseStream response) throws DeepLException { * @throws DeepLException Throws {@link DeepLException} or a derived exception depending on the * type of error. */ - private void checkResponse( + protected void checkResponse( HttpResponse response, boolean inDocumentDownload, boolean usingGlossary) throws DeepLException { if (response.getCode() >= 200 && response.getCode() < 300) { diff --git a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java index 7fb2773..f6b1131 100644 --- a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java @@ -17,7 +17,10 @@ * TranslatorOptions options = new TranslatorOptions() * .setTimeout(Duration.ofSeconds(1)).setMaxRetries(2); * + * + * @deprecated Use {@link DeepLClientOptions} instead. */ +@Deprecated public class TranslatorOptions { private int maxRetries = 5; private Duration timeout = Duration.ofSeconds(10); diff --git a/deepl-java/src/main/java/com/deepl/api/WriteResult.java b/deepl-java/src/main/java/com/deepl/api/WriteResult.java new file mode 100644 index 0000000..bdb19d4 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/WriteResult.java @@ -0,0 +1,33 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** The result of a text translation. */ +public class WriteResult { + private final String text; + private final String detectedSourceLanguage; + private final String targetLanguage; + + /** Constructs a new instance. */ + public WriteResult(String text, String detectedSourceLanguage, String targetLanguage) { + this.text = text; + this.detectedSourceLanguage = LanguageCode.standardize(detectedSourceLanguage); + this.targetLanguage = targetLanguage; + } + + /** The translated text. */ + public String getText() { + return text; + } + + /** The language code of the source text detected by DeepL. */ + public String getDetectedSourceLanguage() { + return detectedSourceLanguage; + } + + /** The language code of the target language set by the request. */ + public String getTargetLanguage() { + return targetLanguage; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/WritingStyle.java b/deepl-java/src/main/java/com/deepl/api/WritingStyle.java new file mode 100644 index 0000000..37ba128 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/WritingStyle.java @@ -0,0 +1,27 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** Represents the style the improved text should be in in a rephrase request. */ +public enum WritingStyle { + Academic("academic"), + Business("business"), + Casual("casual"), + Default("default"), + PreferAcademic("prefer_academic"), + PreferBusiness("prefer_business"), + PreferCasual("prefer_casual"), + PreferSimple("prefer_simple"), + Simple("simple"); + + private final String value; + + WritingStyle(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/WritingTone.java b/deepl-java/src/main/java/com/deepl/api/WritingTone.java new file mode 100644 index 0000000..d507fbb --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/WritingTone.java @@ -0,0 +1,27 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** Represents the tone the improved text should be in in a rephrase request. */ +public enum WritingTone { + Confident("confident"), + Default("default"), + Diplomatic("diplomatic"), + Enthusiastic("enthusiastic"), + Friendly("friendly"), + PreferConfident("prefer_confident"), + PreferDiplomatic("prefer_diplomatic"), + PreferEnthusiastic("prefer_enthusiastic"), + PreferFriendly("prefer_friendly"); + + private final String value; + + WritingTone(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index 04638c1..cc13c5f 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -21,6 +21,7 @@ public class Parser { public Parser() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(TextResult.class, new TextResultDeserializer()); + gsonBuilder.registerTypeAdapter(WriteResult.class, new WriteResultDeserializer()); gsonBuilder.registerTypeAdapter(Language.class, new LanguageDeserializer()); gsonBuilder.registerTypeAdapter(Usage.class, new UsageDeserializer()); gson = gsonBuilder.create(); @@ -31,6 +32,11 @@ public List parseTextResult(String json) { return result.translations; } + public List parseWriteResult(String json) { + WriteResponse result = gson.fromJson(json, WriteResponse.class); + return result.improvements; + } + public Usage parseUsage(String json) { return gson.fromJson(json, Usage.class); } diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/WriteResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/WriteResponse.java new file mode 100644 index 0000000..d7074bf --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/WriteResponse.java @@ -0,0 +1,16 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.WriteResult; +import java.util.List; + +/** + * Class representing text rephrase responses from the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class WriteResponse { + public List improvements; +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/WriteResultDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/WriteResultDeserializer.java new file mode 100644 index 0000000..e692803 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/WriteResultDeserializer.java @@ -0,0 +1,24 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.WriteResult; +import com.google.gson.*; +import java.lang.reflect.Type; + +/** + * Utility class for deserializing text rephrase results returned by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class WriteResultDeserializer implements JsonDeserializer { + public WriteResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + return new WriteResult( + jsonObject.get("text").getAsString(), + jsonObject.get("detected_source_language").getAsString(), + jsonObject.get("target_language").getAsString()); + } +} diff --git a/deepl-java/src/test/java/com/deepl/api/RephraseTextTest.java b/deepl-java/src/test/java/com/deepl/api/RephraseTextTest.java new file mode 100644 index 0000000..210afbd --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/RephraseTextTest.java @@ -0,0 +1,66 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import java.util.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class RephraseTextTest extends TestBase { + + @Test + void testSingleText() throws DeepLException, InterruptedException { + String inputText = exampleText.get("en"); + DeepLClient client = createDeepLClient(); + WriteResult result = client.rephraseText(inputText, "EN-GB", null); + this.checkSanityOfImprovements(inputText, result, "EN", "EN-GB", 0.2f); + } + + @Test + void testTextArray() throws DeepLException, InterruptedException { + DeepLClient client = createDeepLClient(); + List texts = new ArrayList<>(); + texts.add(exampleText.get("en")); + texts.add(exampleText.get("en")); + List results = client.rephraseText(texts, "EN-GB", null); + for (int i = 0; i < texts.size(); i++) { + this.checkSanityOfImprovements(texts.get(i), results.get(i), "EN", "EN-GB", 0.2f); + } + } + + @Test + void testBusinessStyle() throws DeepLException, InterruptedException { + String inputText = + "As Gregor Samsa awoke one morning from uneasy dreams he found himself transformed in his bed into a gigantic insect."; + DeepLClient client = createDeepLClient(); + TextRephraseOptions options = + (new TextRephraseOptions()).setWritingStyle(WritingStyle.Business.getValue()); + WriteResult result = client.rephraseText(inputText, "EN-GB", options); + if (!isMockServer) { + this.checkSanityOfImprovements(inputText, result, "EN", "EN-GB", 0.2f); + } + } + + protected void checkSanityOfImprovements( + String inputText, + WriteResult result, + String expectedSourceLanguageUppercase, + String expectedTargetLanguageUppercase, + float epsilon) { + Assertions.assertEquals( + expectedSourceLanguageUppercase, result.getDetectedSourceLanguage().toUpperCase()); + Assertions.assertEquals( + expectedTargetLanguageUppercase, result.getTargetLanguage().toUpperCase()); + int nImproved = result.getText().length(); + int nOriginal = inputText.length(); + Assertions.assertTrue( + 1 / (1 + epsilon) <= ((float) nImproved) / nOriginal, + "Improved text is too short compared to original, improved:\n" + + result.getText() + + "\n, original:\n" + + inputText); + Assertions.assertTrue( + nImproved / nOriginal <= (1 + epsilon), "Improved text is too long compared to original"); + } +} diff --git a/deepl-java/src/test/java/com/deepl/api/TestBase.java b/deepl-java/src/test/java/com/deepl/api/TestBase.java index 9cbe60f..8873335 100644 --- a/deepl-java/src/test/java/com/deepl/api/TestBase.java +++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java @@ -90,6 +90,7 @@ protected TestBase() { tempDir = createTempDir(); } + // TODO: Delete `createTranslator` methods, replace with `createDeepLClient` protected Translator createTranslator() { SessionOptions sessionOptions = new SessionOptions(); return createTranslator(sessionOptions); @@ -124,6 +125,40 @@ protected Translator createTranslator( } } + protected DeepLClient createDeepLClient() { + SessionOptions sessionOptions = new SessionOptions(); + return createDeepLClient(sessionOptions); + } + + protected DeepLClient createDeepLClient(SessionOptions sessionOptions) { + TranslatorOptions translatorOptions = new TranslatorOptions(); + return createDeepLClient(sessionOptions, translatorOptions); + } + + protected DeepLClient createDeepLClient( + SessionOptions sessionOptions, TranslatorOptions translatorOptions) { + Map headers = sessionOptions.createSessionHeaders(); + + if (translatorOptions.getServerUrl() == null) { + translatorOptions.setServerUrl(serverUrl); + } + + if (translatorOptions.getHeaders() != null) { + headers.putAll(translatorOptions.getHeaders()); + } + translatorOptions.setHeaders(headers); + + String authKey = sessionOptions.randomAuthKey ? UUID.randomUUID().toString() : TestBase.authKey; + + try { + return new DeepLClient(authKey, translatorOptions); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + System.exit(1); + return null; + } + } + protected String createTempDir() { String newTempDir = tempDirBase + UUID.randomUUID(); boolean created = new File(newTempDir).mkdirs(); From 9087e775804e7ac7c859661f9a1c1be4e4ced424 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Thu, 16 Jan 2025 12:22:36 +0000 Subject: [PATCH 064/121] docs: Increase version to 1.8.0 --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b53884..d935bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.8.0] - 2025-01-17 ### Added * Added support for the Write API in the client library, the implementation can be found in the `DeepLClient` class. Please refer to the README for usage @@ -135,7 +135,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.7.0...HEAD +[1.8.0]: https://github.com/DeepLcom/deepl-java/compare/v1.7.0...v1.8.0 [1.7.0]: https://github.com/DeepLcom/deepl-java/compare/v1.6.0...v1.7.0 [1.6.0]: https://github.com/DeepLcom/deepl-java/compare/v1.5.1...v1.6.0 [1.5.1]: https://github.com/DeepLcom/deepl-java/compare/v1.5.0...v1.5.1 diff --git a/README.md b/README.md index 2209618..549c089 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.7.0" +implementation "com.deepl.api:deepl-java:1.8.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.7.0 + 1.8.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 6941fc9..7022e99 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.7.0" +version = "1.8.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index c3e73d0..324c3dd 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -85,7 +85,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.7.0"); + sb.append("deepl-java/1.8.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From cee8123cd3c9d0d1de3fc01a63804ac9c72f7fd4 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 7 Feb 2025 14:01:18 +0000 Subject: [PATCH 065/121] fix: Fix README constructor example for DeepLClient by adding an overloaded constructor --- .../main/java/com/deepl/api/DeepLClient.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 8f4e190..615119c 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -12,8 +12,32 @@ public class DeepLClient extends Translator { - /** {@inheritDoc} */ - public DeepLClient(String authKey, TranslatorOptions options) throws IllegalArgumentException { + /** + * Initializes a new DeepLClient object using your Authentication Key. + * + *

Note: This function does not establish a connection to the DeepL API. To check connectivity, + * use {@link DeepLClient#getUsage()}. + * + * @param authKey DeepL Authentication Key as found in your DeepL account. + * @throws IllegalArgumentException If authKey is invalid. + */ + public DeepLClient(String authKey) throws IllegalArgumentException { + this(authKey, new DeepLClientOptions()); + } + + /** + * Initializes a new DeepLClient object using your Authentication Key. + * + *

Note: This function does not establish a connection to the DeepL API. To check connectivity, + * use {@link DeepLClient#getUsage()}. + * + * @param authKey DeepL Authentication Key as found in your DeepL account. + * @param options Additional options controlling Client behaviour. + * @throws IllegalArgumentException If authKey is invalid. + */ + public DeepLClient(String authKey, DeepLClientOptions options) throws IllegalArgumentException { super(authKey, options); } From 4e787a8d5867037ad87618105cb7c068ae5d8a02 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 7 Feb 2025 14:11:25 +0000 Subject: [PATCH 066/121] fix: Only deprecate constructor of Translator and TranslatorOptions, not all methods --- deepl-java/src/main/java/com/deepl/api/Translator.java | 7 ++++--- .../src/main/java/com/deepl/api/TranslatorOptions.java | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 324c3dd..52f7bdc 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -17,10 +17,7 @@ * Client for the DeepL API. To use the DeepL API, initialize an instance of this class using your * DeepL Authentication Key as found in your DeepL * account. - * - * @deprecated Use {@link DeepLClient} instead. */ -@Deprecated public class Translator { /** Base URL for DeepL API Free accounts. */ private static final String DEEPL_SERVER_URL_FREE = "https://api-free.deepl.com"; @@ -40,7 +37,9 @@ public class Translator { * href="https://www.deepl.com/pro-account/">DeepL account. * @param options Additional options controlling Translator behaviour. * @throws IllegalArgumentException If authKey is invalid. + * @deprecated Use {@link DeepLClient} instead. */ + @Deprecated public Translator(String authKey, TranslatorOptions options) throws IllegalArgumentException { if (authKey == null || authKey.length() == 0) { throw new IllegalArgumentException("authKey must be a non-empty string"); @@ -73,7 +72,9 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum * @param authKey DeepL Authentication Key as found in your DeepL account. * @throws IllegalArgumentException If authKey is invalid. + * @deprecated Use {@link DeepLClient} instead. */ + @Deprecated public Translator(String authKey) throws IllegalArgumentException { this(authKey, new TranslatorOptions()); } diff --git a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java index f6b1131..1e7a21f 100644 --- a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java @@ -17,10 +17,7 @@ * TranslatorOptions options = new TranslatorOptions() * .setTimeout(Duration.ofSeconds(1)).setMaxRetries(2); * - * - * @deprecated Use {@link DeepLClientOptions} instead. */ -@Deprecated public class TranslatorOptions { private int maxRetries = 5; private Duration timeout = Duration.ofSeconds(10); @@ -30,6 +27,10 @@ public class TranslatorOptions { private boolean sendPlatformInfo = true; @Nullable private AppInfo appInfo = null; + /** @deprecated Use {@link DeepLClient} instead. */ + @Deprecated + public TranslatorOptions() {} + /** * Set the maximum number of failed attempts that {@link Translator} will retry, per request. By * default, 5 retries are made. Note: only errors due to transient conditions are retried. From a4a1f84bbcab5b8591bc7498adf072f33e9c9e46 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 7 Feb 2025 14:26:21 +0000 Subject: [PATCH 067/121] chore: Suppress warnings about deprecation in library code --- deepl-java/src/main/java/com/deepl/api/DeepLClient.java | 1 + deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java | 1 + 2 files changed, 2 insertions(+) diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 615119c..7bf85d0 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -37,6 +37,7 @@ public DeepLClient(String authKey) throws IllegalArgumentException { * @param options Additional options controlling Client behaviour. * @throws IllegalArgumentException If authKey is invalid. */ + @SuppressWarnings("deprecation") public DeepLClient(String authKey, DeepLClientOptions options) throws IllegalArgumentException { super(authKey, options); } diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java index 7349823..46eafc8 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java @@ -4,4 +4,5 @@ package com.deepl.api; /** {@inheritDoc} */ +@SuppressWarnings("deprecation") public class DeepLClientOptions extends TranslatorOptions {} From 11145326ee733135303030eb3a35a367a85e6950 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 7 Feb 2025 15:33:53 +0000 Subject: [PATCH 068/121] fix: Add new constructor that takes DeepLClientOptions --- .../main/java/com/deepl/api/DeepLClient.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 7bf85d0..ea7a74b 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -26,6 +26,24 @@ public DeepLClient(String authKey) throws IllegalArgumentException { this(authKey, new DeepLClientOptions()); } + /** + * Initializes a new DeepLClient object using your Authentication Key. + * + *

Note: This function does not establish a connection to the DeepL API. To check connectivity, + * use {@link DeepLClient#getUsage()}. + * + * @param authKey DeepL Authentication Key as found in your DeepL account. + * @param options Additional options controlling Client behaviour. + * @throws IllegalArgumentException If authKey is invalid. + * @deprecated Use the constructor that takes {@link DeepLClientOptions} instead of {@link + * TranslatorOptions} + */ + @Deprecated + public DeepLClient(String authKey, TranslatorOptions options) throws IllegalArgumentException { + super(authKey, options); + } + /** * Initializes a new DeepLClient object using your Authentication Key. * From 87e78b1184180d33e9d122a0b1a928e62c807ab8 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 7 Feb 2025 14:54:16 +0000 Subject: [PATCH 069/121] docs: Increase version to 1.8.1 --- CHANGELOG.md | 9 +++++++++ README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d935bd9..4367c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.8.1] - 2025-02-07 +### Fixed +* Added a constructor for `DeepLClient` that only takes an `authKey`, to fix the + README example and be in line with `Translator`. +* Un-deprecated the `Translator` and `TranslatorOptions` class and moved it to + their constructors. The functionality in them continues to work and be supported, + user code should just use `DeepLClient` and `DeepLClientOptions`. + ## [1.8.0] - 2025-01-17 ### Added * Added support for the Write API in the client library, the implementation @@ -135,6 +143,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[1.8.1]: https://github.com/DeepLcom/deepl-java/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/DeepLcom/deepl-java/compare/v1.7.0...v1.8.0 [1.7.0]: https://github.com/DeepLcom/deepl-java/compare/v1.6.0...v1.7.0 [1.6.0]: https://github.com/DeepLcom/deepl-java/compare/v1.5.1...v1.6.0 diff --git a/README.md b/README.md index 549c089..36f780d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.8.0" +implementation "com.deepl.api:deepl-java:1.8.1" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.8.0 + 1.8.1 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 7022e99..4edacfd 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.8.0" +version = "1.8.1" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 52f7bdc..bb80a24 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -86,7 +86,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.8.0"); + sb.append("deepl-java/1.8.1"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 15b74df979436a27631581571fa7fc7b5360bb39 Mon Sep 17 00:00:00 2001 From: Tima Zhum Date: Thu, 13 Feb 2025 22:15:12 +0000 Subject: [PATCH 070/121] fix: issue#62 strip auth key Fixing the issue raised in https://github.com/DeepLcom/deepl-java/issues/62 Problem: auth key with leading / trailing whitespaces is throwing exceptions. For example, assigning the secret via `echo` leaves a trailing `\n`. Solution: strip input auth key as an input sanitization procedure Notice: Java 8 does not support `String.strip()`, while `String.trim()` does not support Unicode whitespaces. We anticipate ASCII only input and utilizing `trim()` for simplicity. --- .../src/main/java/com/deepl/api/Translator.java | 13 +++++++++---- .../src/test/java/com/deepl/api/GeneralTest.java | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index bb80a24..b556d65 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -41,19 +41,24 @@ public class Translator { */ @Deprecated public Translator(String authKey, TranslatorOptions options) throws IllegalArgumentException { - if (authKey == null || authKey.length() == 0) { - throw new IllegalArgumentException("authKey must be a non-empty string"); + if (authKey == null || authKey.isEmpty()) { + throw new IllegalArgumentException("authKey cannot be null or empty"); } + + String sanitizedAuthKey = authKey.trim(); + String serverUrl = (options.getServerUrl() != null) ? options.getServerUrl() - : (isFreeAccountAuthKey(authKey) ? DEEPL_SERVER_URL_FREE : DEEPL_SERVER_URL_PRO); + : (isFreeAccountAuthKey(sanitizedAuthKey) + ? DEEPL_SERVER_URL_FREE + : DEEPL_SERVER_URL_PRO); Map headers = new HashMap<>(); if (options.getHeaders() != null) { headers.putAll(options.getHeaders()); } - headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + authKey); + headers.putIfAbsent("Authorization", "DeepL-Auth-Key " + sanitizedAuthKey); headers.putIfAbsent( "User-Agent", constructUserAgentString(options.getSendPlatformInfo(), options.getAppInfo())); diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 904c8b0..5c897be 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -28,6 +28,16 @@ void testEmptyAuthKey() { }); } + @Test + void testNullAuthKey() { + IllegalArgumentException thrown = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> { + Translator translator = new Translator(null); + }); + } + @Test void testInvalidAuthKey() { String authKey = "invalid"; From a57b3d747e2b18f567795f9d8263a088c957ac51 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 14 Feb 2025 17:33:38 +0000 Subject: [PATCH 071/121] feat: Allow specifying API version --- .../java/com/deepl/api/DeepLApiVersion.java | 20 ++++++++++ .../main/java/com/deepl/api/DeepLClient.java | 4 +- .../com/deepl/api/DeepLClientOptions.java | 22 ++++++++++- .../main/java/com/deepl/api/Translator.java | 39 ++++++++++++------- .../java/com/deepl/api/TranslatorOptions.java | 5 ++- 5 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java b/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java new file mode 100644 index 0000000..5725fbb --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java @@ -0,0 +1,20 @@ +package com.deepl.api; + +public enum DeepLApiVersion { + VERSION_1("v1"), + VERSION_2("v2"); + + /** + * How the version is represented in the URL string. Does not include any slashes (/). Example: + * "v2" + */ + private final String urlRepresentation; + + private DeepLApiVersion(String urlRepresentation) { + this.urlRepresentation = urlRepresentation; + } + + public String toString() { + return this.urlRepresentation; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index ea7a74b..9d4b825 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -73,7 +73,9 @@ public List rephraseText( throws InterruptedException, DeepLException { Iterable> params = createWriteHttpParams(texts, targetLang, options); - HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/write/rephrase", params); + HttpResponse response = + httpClientWrapper.sendRequestWithBackoff( + String.format("/%s/write/rephrase", apiVersion), params); checkResponse(response, false, false); return jsonParser.parseWriteResult(response.getBody()); } diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java index 46eafc8..1f2a6b3 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java @@ -3,6 +3,26 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import org.jetbrains.annotations.Nullable; + /** {@inheritDoc} */ @SuppressWarnings("deprecation") -public class DeepLClientOptions extends TranslatorOptions {} +public class DeepLClientOptions extends TranslatorOptions { + /** + * Set the version of the DeepL API to use. By default, this value is + * DeepLApiVersion.VERSION_2 and the most recent DeepL API version is used. Note that older + * API versions like DeepLApiVersion.VERSION_1 might not support all features of the + * more modern API (eg. document translation is v2-only), and that not all API subscriptions have + * access to one or the other API version. If in doubt, always use the most recent API version you + * have access to. + */ + public DeepLClientOptions setApiVersion(DeepLApiVersion apiVersion) { + this.apiVersion = apiVersion; + return this; + } + + /** Gets the current API version. */ + public @Nullable DeepLApiVersion getApiVersion() { + return apiVersion; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index bb80a24..6df6b1a 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -26,6 +26,7 @@ public class Translator { protected final Parser jsonParser = new Parser(); protected final HttpClientWrapper httpClientWrapper; + protected final DeepLApiVersion apiVersion; /** * Initializes a new Translator object using your Authentication Key. @@ -44,6 +45,7 @@ public Translator(String authKey, TranslatorOptions options) throws IllegalArgum if (authKey == null || authKey.length() == 0) { throw new IllegalArgumentException("authKey must be a non-empty string"); } + this.apiVersion = options.apiVersion; String serverUrl = (options.getServerUrl() != null) ? options.getServerUrl() @@ -195,7 +197,9 @@ public List translateText( throws DeepLException, InterruptedException { Iterable> params = createHttpParams(texts, sourceLang, targetLang, options); - HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/translate", params); + HttpResponse response = + httpClientWrapper.sendRequestWithBackoff( + String.format("/%s/translate", this.apiVersion), params); checkResponse(response, false, false); return jsonParser.parseTextResult(response.getBody()); } @@ -251,7 +255,8 @@ public List translateText( * @throws DeepLException If any error occurs while communicating with the DeepL API. */ public Usage getUsage() throws DeepLException, InterruptedException { - HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff("/v2/usage"); + HttpResponse response = + httpClientWrapper.sendGetRequestWithBackoff(String.format("/%s/usage", apiVersion)); checkResponse(response, false, false); return jsonParser.parseUsage(response.getBody()); } @@ -295,7 +300,9 @@ public List getLanguages(LanguageType languageType) if (languageType == LanguageType.Target) { params.add(new KeyValuePair<>("type", "target")); } - HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/languages", params); + HttpResponse response = + httpClientWrapper.sendRequestWithBackoff( + String.format("/%s/languages", apiVersion), params); checkResponse(response, false, false); return jsonParser.parseLanguages(response.getBody()); } @@ -312,7 +319,8 @@ public List getLanguages(LanguageType languageType) public List getGlossaryLanguages() throws DeepLException, InterruptedException { HttpResponse response = - httpClientWrapper.sendGetRequestWithBackoff("/v2/glossary-language-pairs"); + httpClientWrapper.sendGetRequestWithBackoff( + String.format("/%s/glossary-language-pairs", apiVersion)); checkResponse(response, false, false); return jsonParser.parseGlossaryLanguageList(response.getBody()); } @@ -451,7 +459,7 @@ public DocumentHandle translateDocumentUpload( try (FileInputStream inputStream = new FileInputStream(inputFile)) { HttpResponse response = httpClientWrapper.uploadWithBackoff( - "/v2/document", params, inputFile.getName(), inputStream); + String.format("/%s/document", apiVersion), params, inputFile.getName(), inputStream); checkResponse(response, false, false); return jsonParser.parseDocumentHandle(response.getBody()); } @@ -495,7 +503,8 @@ public DocumentHandle translateDocumentUpload( Iterable> params = createHttpParams(sourceLang, targetLang, options); HttpResponse response = - httpClientWrapper.uploadWithBackoff("/v2/document/", params, fileName, inputStream); + httpClientWrapper.uploadWithBackoff( + String.format("/%s/document/", apiVersion), params, fileName, inputStream); checkResponse(response, false, false); return jsonParser.parseDocumentHandle(response.getBody()); } @@ -525,7 +534,7 @@ public DocumentStatus translateDocumentStatus(DocumentHandle handle) throws DeepLException, InterruptedException { ArrayList> params = new ArrayList<>(); params.add(new KeyValuePair<>("document_key", handle.getDocumentKey())); - String relativeUrl = String.format("/v2/document/%s", handle.getDocumentId()); + String relativeUrl = String.format("/%s/document/%s", apiVersion, handle.getDocumentId()); HttpResponse response = httpClientWrapper.sendRequestWithBackoff(relativeUrl, params); checkResponse(response, false, false); return jsonParser.parseDocumentStatus(response.getBody()); @@ -599,7 +608,8 @@ public void translateDocumentDownload(DocumentHandle handle, OutputStream output throws DeepLException, IOException, InterruptedException { ArrayList> params = new ArrayList<>(); params.add(new KeyValuePair<>("document_key", handle.getDocumentKey())); - String relativeUrl = String.format("/v2/document/%s/result", handle.getDocumentId()); + String relativeUrl = + String.format("/%s/document/%s/result", apiVersion, handle.getDocumentId()); try (HttpResponseStream response = httpClientWrapper.downloadWithBackoff(relativeUrl, params)) { checkResponse(response); assert response.getBody() != null; @@ -682,7 +692,7 @@ public GlossaryInfo createGlossaryFromCsv( * @throws DeepLException If any error occurs while communicating with the DeepL API. */ public GlossaryInfo getGlossary(String glossaryId) throws DeepLException, InterruptedException { - String relativeUrl = String.format("/v2/glossaries/%s", glossaryId); + String relativeUrl = String.format("/%s/glossaries/%s", apiVersion, glossaryId); HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); checkResponse(response, false, true); return jsonParser.parseGlossaryInfo(response.getBody()); @@ -698,7 +708,8 @@ public GlossaryInfo getGlossary(String glossaryId) throws DeepLException, Interr * @throws DeepLException If any error occurs while communicating with the DeepL API. */ public List listGlossaries() throws DeepLException, InterruptedException { - HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff("/v2/glossaries"); + HttpResponse response = + httpClientWrapper.sendGetRequestWithBackoff(String.format("/%s/glossaries", apiVersion)); checkResponse(response, false, false); return jsonParser.parseGlossaryInfoList(response.getBody()); } @@ -729,7 +740,7 @@ public GlossaryEntries getGlossaryEntries(GlossaryInfo glossary) */ public GlossaryEntries getGlossaryEntries(String glossaryId) throws DeepLException, InterruptedException { - String relativeUrl = String.format("/v2/glossaries/%s/entries", glossaryId); + String relativeUrl = String.format("/%s/glossaries/%s/entries", apiVersion, glossaryId); HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); checkResponse(response, false, true); return GlossaryEntries.fromTsv(response.getBody()); @@ -754,7 +765,7 @@ public void deleteGlossary(GlossaryInfo glossary) throws DeepLException, Interru * @throws DeepLException If any error occurs while communicating with the DeepL API. */ public void deleteGlossary(String glossaryId) throws DeepLException, InterruptedException { - String relativeUrl = String.format("/v2/glossaries/%s", glossaryId); + String relativeUrl = String.format("/%s/glossaries/%s", apiVersion, glossaryId); HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); checkResponse(response, false, true); } @@ -954,7 +965,9 @@ private GlossaryInfo createGlossaryInternal( params.add(new KeyValuePair<>("target_lang", targetLang)); params.add(new KeyValuePair<>("entries_format", entriesFormat)); params.add(new KeyValuePair<>("entries", entries)); - HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/glossaries", params); + HttpResponse response = + httpClientWrapper.sendRequestWithBackoff( + String.format("/%s/glossaries", apiVersion), params); checkResponse(response, false, false); return jsonParser.parseGlossaryInfo(response.getBody()); } diff --git a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java index 1e7a21f..42f4634 100644 --- a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java @@ -26,10 +26,13 @@ public class TranslatorOptions { @Nullable private String serverUrl = null; private boolean sendPlatformInfo = true; @Nullable private AppInfo appInfo = null; + @Nullable protected DeepLApiVersion apiVersion = null; /** @deprecated Use {@link DeepLClient} instead. */ @Deprecated - public TranslatorOptions() {} + public TranslatorOptions() { + apiVersion = DeepLApiVersion.VERSION_2; + } /** * Set the maximum number of failed attempts that {@link Translator} will retry, per request. By From 29cfdc7fc7a421f0cf8613105c741e0e0a447419 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 14 Feb 2025 17:43:23 +0000 Subject: [PATCH 072/121] test: Add v1 api tests --- .github/workflows/run_ci.yml | 2 +- .gitlab-ci.yml | 2 +- .../test/java/com/deepl/api/GeneralTest.java | 30 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_ci.yml b/.github/workflows/run_ci.yml index b920fdf..b25a5f9 100644 --- a/.github/workflows/run_ci.yml +++ b/.github/workflows/run_ci.yml @@ -110,7 +110,7 @@ jobs: # export DEEPL_PROXY_URL=http://deepl-mock:3001 # export DEEPL_MOCK_PROXY_SERVER_PORT=3001 # fi -# ./gradlew test +# ./gradlew test -DrunV1ApiTests=true # - name: Stop mock proxy server # if: ${{ matrix.use-mock-server == 'use mock server' }} # run: docker stop deepl-mock-proxy diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2deefbb..acf3430 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -128,7 +128,7 @@ build_manual: export DEEPL_PROXY_URL=http://deepl-mock:3001 export DEEPL_MOCK_PROXY_SERVER_PORT=3001 fi - - ./gradlew test + - ./gradlew test -DrunV1ApiTests=true artifacts: paths: - deepl-java/build/reports/tests/test diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 904c8b0..b10df25 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -9,6 +9,7 @@ import java.util.*; import java.util.stream.Stream; import org.junit.jupiter.api.*; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; @@ -344,6 +345,31 @@ void testUserAgent( } } + @Test + @EnabledIf("runV1ApiTests") + void testV1Api() throws DeepLException, InterruptedException { + SessionOptions sessionOptions = new SessionOptions(); + DeepLClientOptions clientOptions = + (new DeepLClientOptions()).setApiVersion(DeepLApiVersion.VERSION_1); + DeepLClient client = createDeepLClient(sessionOptions, clientOptions); + + for (Map.Entry entry : exampleText.entrySet()) { + String inputText = entry.getValue(); + String sourceLang = LanguageCode.removeRegionalVariant(entry.getKey()); + TextResult result = client.translateText(inputText, sourceLang, "en-US"); + Assertions.assertTrue(result.getText().toLowerCase(Locale.ENGLISH).contains("proton")); + Assertions.assertEquals(inputText.length(), result.getBilledCharacters()); + } + Usage usage = client.getUsage(); + Assertions.assertTrue(usage.toString().contains("Usage this billing period")); + + List sourceLanguages = client.getSourceLanguages(); + List targetLanguages = client.getTargetLanguages(); + Assertions.assertTrue(sourceLanguages.size() > 20); + Assertions.assertTrue(targetLanguages.size() > 20); + Assertions.assertTrue(targetLanguages.size() >= sourceLanguages.size()); + } + // Session options & Translator options: Used to construct the `Translator` // Next arg: List of Strings that must be contained in the user agent header // Last arg: List of Strings that must not be contained in the user agent header @@ -391,4 +417,8 @@ private static Stream provideUserAgentTestData() { customUserAgent, detailedPlatformInfoWithAppInfo)); } + + boolean runV1ApiTests() { + return Boolean.getBoolean("runV1ApiTests"); + } } From 63754b799de0a8df3756b82e8896c895b56305b1 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Fri, 14 Feb 2025 17:52:32 +0000 Subject: [PATCH 073/121] docs: Increase version to 1.9.0 --- CHANGELOG.md | 8 ++++++++ README.md | 8 ++++++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4367c42..49f9f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.0] - 2025-02-21 +### Added +* Allow specifying the API version to use. This is mostly for users who have an + API subscription that includes an API key for CAT tool usage, who need to use + the v1 API. + + ## [1.8.1] - 2025-02-07 ### Fixed * Added a constructor for `DeepLClient` that only takes an `authKey`, to fix the @@ -143,6 +150,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[1.9.0]: https://github.com/DeepLcom/deepl-java/compare/v1.8.1...v1.9.0 [1.8.1]: https://github.com/DeepLcom/deepl-java/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/DeepLcom/deepl-java/compare/v1.7.0...v1.8.0 [1.7.0]: https://github.com/DeepLcom/deepl-java/compare/v1.6.0...v1.7.0 diff --git a/README.md b/README.md index 36f780d..54cd504 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.8.1" +implementation "com.deepl.api:deepl-java:1.9.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.8.1 + 1.9.0 ``` @@ -634,6 +634,10 @@ The available options setters are: - `setServerUrl()`: base URL for DeepL API, may be overridden for testing purposes. By default, the correct DeepL API (Free or Pro) is automatically selected. +- `setApiVersion()`: Version of the DeepL API, may be overridden to use e.g. + the v1 API. By default, the most recent API version is automatically selected. + Please note: The v1 API does not support all features of the API, e.g. + document translation or rephrase. #### Anonymous platform information diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 4edacfd..53980bb 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.8.1" +version = "1.9.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 6df6b1a..28aac00 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -88,7 +88,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.8.1"); + sb.append("deepl-java/1.9.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 82948d70e7f114735ffa08175120c07473d04c2e Mon Sep 17 00:00:00 2001 From: Alaa Jubakhanji Date: Wed, 5 Mar 2025 10:50:25 +0100 Subject: [PATCH 074/121] ci: Add SAST testing to CI checks --- .gitlab-ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index acf3430..e948517 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ include: - project: 'deepl/ops/ci-cd-infrastructure/gitlab-ci-lib' file: - '/templates/.secret-detection.yml' + - template: Security/SAST.gitlab-ci.yml # Global -------------------------- @@ -15,6 +16,7 @@ image: eclipse-temurin:17-alpine variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" JAVA_TOOL_OPTIONS: "" + GITLAB_ADVANCED_SAST_ENABLED: 'true' workflow: rules: @@ -77,6 +79,22 @@ secret_detection: rules: - if: $CI_MERGE_REQUEST_ID +gitlab-advanced-sast: + stage: check + rules: + - when: always + variables: + SAST_EXCLUDED_PATHS: '$DEFAULT_SAST_EXCLUDED_PATHS' + GIT_STRATEGY: clone + +semgrep-sast: + stage: check + rules: + - when: always + variables: + SAST_EXCLUDED_PATHS: '$DEFAULT_SAST_EXCLUDED_PATHS' + GIT_STRATEGY: clone + # stage: build ---------------------- .build_base: From 254d9776562561b8b09cf9437f72fd1f43ce2ace Mon Sep 17 00:00:00 2001 From: Marvin Strangfeld Date: Wed, 9 Apr 2025 14:21:59 +0000 Subject: [PATCH 075/121] ci: [DEX-2415] Add GitLab release tracking --- .gitlab-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e948517..139b3a2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ include: - project: 'deepl/ops/ci-cd-infrastructure/gitlab-ci-lib' file: - '/templates/.secret-detection.yml' + - '/templates/.gitlab-release.yml' - template: Security/SAST.gitlab-ci.yml # Global -------------------------- @@ -225,3 +226,9 @@ publish_manual: - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' script: - ./gradlew publish + +gitlab release: + stage: publish + extends: .create_gitlab_release + rules: + - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' \ No newline at end of file From abf10103ed74e7f75ac999d1a73014a4696bd81b Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Mon, 28 Apr 2025 15:01:50 +0000 Subject: [PATCH 076/121] feat: Add ukrainian language code and tests --- CHANGELOG.md | 6 ++++++ deepl-java/src/main/java/com/deepl/api/LanguageCode.java | 3 +++ deepl-java/src/test/java/com/deepl/api/TestBase.java | 1 + 3 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f9f78..23d271a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +* Added Ukrainian language code + + ## [1.9.0] - 2025-02-21 ### Added * Allow specifying the API version to use. This is mostly for users who have an @@ -150,6 +155,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.9.0...HEAD [1.9.0]: https://github.com/DeepLcom/deepl-java/compare/v1.8.1...v1.9.0 [1.8.1]: https://github.com/DeepLcom/deepl-java/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/DeepLcom/deepl-java/compare/v1.7.0...v1.8.0 diff --git a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java index 6fa2028..b4c9448 100644 --- a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java +++ b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java @@ -113,6 +113,9 @@ public class LanguageCode { /** Turkish language code, may be used as source or target language. */ public static final String Turkish = "tr"; + /** Ukrainian language code, may be used as source or target language. */ + public static final String Ukrainian = "uk"; + /** Chinese language code, may be used as source or target language. */ public static final String Chinese = "zh"; diff --git a/deepl-java/src/test/java/com/deepl/api/TestBase.java b/deepl-java/src/test/java/com/deepl/api/TestBase.java index 8873335..06cc67b 100644 --- a/deepl-java/src/test/java/com/deepl/api/TestBase.java +++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java @@ -80,6 +80,7 @@ public class TestBase { exampleText.put("sl", "protonski žarek"); exampleText.put("sv", "protonstråle"); exampleText.put("tr", "proton ışını"); + exampleText.put("uk", "Протонний промінь"); exampleText.put("zh", "质子束"); String tmpdir = System.getProperty("java.io.tmpdir"); From 2a13037dde6df38e3295b226fa7481f88fbb3a44 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Tue, 1 Apr 2025 14:04:35 -0400 Subject: [PATCH 077/121] feat: Add support for multilingual glossary endpoints --- CHANGELOG.md | 6 +- README.md | 246 +++++- deepl-java/build.gradle.kts | 1 + .../main/java/com/deepl/api/DeepLClient.java | 780 +++++++++++++++++- .../deepl/api/DocumentTranslationOptions.java | 2 +- .../main/java/com/deepl/api/GlossaryInfo.java | 2 +- .../java/com/deepl/api/HttpClientWrapper.java | 66 +- .../main/java/com/deepl/api/IGlossary.java | 10 + ...MultilingualGlossaryDictionaryEntries.java | 41 + .../MultilingualGlossaryDictionaryInfo.java | 48 ++ .../deepl/api/MultilingualGlossaryInfo.java | 61 ++ .../com/deepl/api/TextTranslationOptions.java | 2 +- ...gualGlossaryDictionaryEntriesResponse.java | 52 ++ ...lingualGlossaryDictionaryListResponse.java | 19 + .../MultilingualGlossaryListResponse.java | 20 + .../java/com/deepl/api/parsing/Parser.java | 19 + .../MultilingualGlossaryCleanupUtility.java | 59 ++ .../deepl/api/MultilingualGlossaryTest.java | 697 ++++++++++++++++ upgrading_to_multilingual_glossaries.md | 412 +++++++++ 19 files changed, 2497 insertions(+), 46 deletions(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/IGlossary.java create mode 100644 deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryEntries.java create mode 100644 deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryInfo.java create mode 100644 deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryInfo.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryEntriesResponse.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryListResponse.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryListResponse.java create mode 100644 deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java create mode 100644 deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java create mode 100644 upgrading_to_multilingual_glossaries.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d271a..cc68cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - ## [Unreleased] ### Added +* Added support for the /v3 Multilingual Glossary APIs in the client library + while providing backwards compatability for the previous /v2 Glossary + endpoints. Please refer to the README or + [upgrading_to_multilingual_glossaries.md](upgrading_to_multilingual_glossaries.md) + for usage instructions. * Added Ukrainian language code diff --git a/README.md b/README.md index 54cd504..77b27ac 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,9 @@ a `TextTranslationOptions`, with the following setters: - `Formality.Less`: use informal language. - `Formality.More`: use formal, more polite language. - `setGlossary()`: specifies a glossary to use with translation, as a string - containing the glossary ID, or a `GlossaryInfo` object (this object is - returned by glossary lookup functions, for example `listGlossaries()`). + containing the glossary ID, or a `GlossaryInfo`/`MultilingualGlossaryInfo` + object (this object is returned by glossary lookup functions, for example + `listGlossaries()` or `listMultilingualGlossaries()`). - `setGlossaryId()` is also available for backward-compatibility, accepting a string containing the glossary ID. - `setContext()`: specifies additional context to influence translations, that is not @@ -311,20 +312,37 @@ Glossaries allow you to customize your translations using user-defined terms. Multiple glossaries can be stored with your account, each with a user-specified name and a uniquely-assigned ID. +### v2 versus v3 glossary APIs + +The newest version of the glossary APIs are the `/v3` endpoints, allowing both +editing functionality plus support for multilingual glossaries. New methods and +objects have been created to support interacting with these new glossaries. +Due to this new functionality, users are recommended to utilize these +multilingual glossary methods. However, to continue using the `v2` glossary API +endpoints, please continue to use the existing endpoints in the `translator.java` +(e.g. `createGlossary()`, `getGlossary()`, etc). + +To migrate to use the new multilingual glossary methods from the current +monolingual glossary methods, please refer to +[this migration guide](upgrade_to_multilingual_glossaries.md). + +The following sections describe how to interact with multilingual glossaries +using the new functionality: + #### Creating a glossary -You can create a glossary with `createGlossary()` by passing your desired -glossary name, and a `GlossaryEntries` object specifying the terms to +You can create a glossary with `createMultilingualGlossary()` by passing your +desired glossary name, and a `GlossaryEntries` object specifying the terms to store in the glossary. -Each glossary applies to a single source-target language pair. Note: Glossaries +Each glossary contains a list of dictionaries, where each dictionary applies to a single source-target language pair. Note: Glossaries are only supported for some language pairs, see [Listing available glossary languages](#listing-available-glossary-languages) for more information. If successful, the glossary is created and stored with your DeepL account, and -a `GlossaryInfo` object is returned including the ID, name, languages and entry -count. +a `MultilingualGlossaryInfo` object is returned including the ID, name, +languages and entry count. ```java class Example { // Continuing class Example from above @@ -334,16 +352,24 @@ class Example { // Continuing class Example from above put("artist", "Maler"); put("prize", "Gewinn"); }}; - GlossaryInfo myGlossary = - client.createGlossary("My glossary", "en", "de", entries); - - System.out.printf("Created '%s' (%s) %s->%s containing %d entries\n", + MultilingualGlossaryDictionaryEntries myGlossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries( + "en", + "de", + entries + )); + MultilingualGlossaryInfo myGlossary = + client.createGlossary("My glossary", myGlossaryDicts); + + System.out.printf("Created '%s' (%s) containing %d dictionary from %s->%s with %d entries\n", myGlossary.getName(), myGlossary.getGlossaryId(), + myGlossary.getDictionaries().length, myGlossary.getSourceLang(), myGlossary.getTargetLang(), myGlossary.getEntryCount()); - // Example: Created 'My glossary' (559192ed-8e23-...) en->de containing 2 entries + // Example: Created 'My glossary' (559192ed-8e23-...) containing 1 + // dictionary from en->de containing 2 entries } } ``` @@ -353,18 +379,18 @@ functions like `put()`. The `fromTsv()` function allows creating GlossaryEntries from TSV data. You can also create a glossary using a glossary downloaded from the DeepL -website by using `createGlossaryFromCsv()` with either a CSV file, or a string -containing the CSV data: +website by using `createMultilingualGlossaryFromCsv()` with either a CSV file, +or a string containing the CSV data: ```java class Example { // Continuing class Example from above public createGlossaryFromCsvExample() throws Exception { File csvFile = new File("/path/to/glossary_file.csv"); - GlossaryInfo myGlossary = - client.createGlossaryFromCsv("My glossary", - "en", - "de", - csvFile); + MultilingualGlossaryInfo myGlossary = + client.createMultilingualGlossaryFromCsv("My glossary", + "en", + "de", + csvFile); } } ``` @@ -372,30 +398,43 @@ class Example { // Continuing class Example from above The [API documentation][api-docs-csv-format] explains the expected CSV format in detail. -#### Getting, listing, and deleting stored glossaries + +#### Getting, listing and deleting stored glossaries Functions to get, list, and delete stored glossaries are also provided: -- `getGlossary()` takes a glossary ID and returns a `GlossaryInfo` object for a - stored glossary, or throws an exception if no such glossary is found. -- `listGlossaries()` returns a list of `GlossaryInfo` objects corresponding to - all of your stored glossaries. -- `deleteGlossary()` takes a glossary ID or `GlossaryInfo` object and deletes - the stored glossary from the server, or throws an exception if no such - glossary is found. +- `getMultilingualGlossary()` takes a glossary ID and returns a + `MultilingualGlossaryInfo` object for a stored glossary, or raises an + exception if no such glossary is found. +- `listMultilingualGlossaries()` returns a list of `MultilingualGlossaryInfo` + objects corresponding to all of your stored glossaries. +- `deleteMultilingualGlossary()` takes a glossary ID or + `MultilingualGlossaryInfo` object and deletes the stored glossary from the + server, or raises an exception if no such glossary is found. +- `deleteMultilingualGlossaryDictionary()` takes a glossary ID or + `MultilingualGlossaryInfo` object to identify the glossary. Additionally + takes in a source and target language or a + `MultilingualGlossaryDictionaryInfo` object and deletes the stored dictionary + from the server, or raises an exception if no such glossary dictionary is + found. ```java class Example { // Continuing class Example from above public getListDeleteGlossaryExamples() throws Exception { // Retrieve a stored glossary using the ID String glossaryId = "559192ed-8e23-..."; - GlossaryInfo myGlossary = client.getGlossary(glossaryId); + MultilingualGlossaryInfo myGlossary = + client.getMultilingualGlossary(glossaryId); + + client.deleteMultilingualGlossaryDictionary(glossaryId, + myGlossary.getDictionaries()[0]); // Find and delete glossaries named 'Old glossary' - List glossaries = client.listGlossaries(); - for (GlossaryInfo glossary : glossaries) { + List glossaries = + client.listMultilingualGlossaries(); + for (MultilingualGlossaryInfo glossary : glossaries) { if (glossary.getName() == "Old glossary") { - client.deleteGlossary(glossary); + client.deleteMultilingualGlossary(glossary); } } } @@ -404,19 +443,21 @@ class Example { // Continuing class Example from above #### Listing entries in a stored glossary -The `GlossaryInfo` object does not contain the glossary entries, but instead -only the number of entries in the `entry_count` property. +The `MultilingualGlossaryDictionaryInfo` object does not contain the glossary +entries, but instead only the number of entries in the `entry_count` property. To list the entries contained within a stored glossary, use -`getGlossaryEntries()` providing either the `GlossaryInfo` object or glossary -ID: +`getMultilingualGlossaryDictionaryEntries()` providing either the +`MultilingualGlossaryInfo` object or glossary ID and either a +`MultilingualGlossaryDictionaryInfo` or source and target language pair: ```java class Example { // Continuing class Example from above public getGlossaryEntriesExample() throws Exception { - GlossaryEntries entries = client.getGlossaryEntries(myGlossary); + List glossaryDicts = + client.getMultilingualGlossaryDictionaryEntries(myGlossary, "en", "de"); - for (Map.Entry entry : entries.entrySet()) { + for (Map.Entry entry : glossaryDicts.getDictionaries()[0].getEntries().entrySet()) { System.out.println(entry.getKey() + ":" + entry.getValue()); } // prints: @@ -426,15 +467,140 @@ class Example { // Continuing class Example from above } ``` +#### Editing a glossary + +Functions to edit stored glossaries are also provided: + +- `updateMultilingualGlossaryDictionary()` takes a glossary ID or `MultilingualGlossaryInfo` + object, plus a source language, target language, and a dictionary of entries. + It will then either update the list of entries for that dictionary (either + inserting new entires or replacing the target phrase for any existing + entries) or will insert a new glossary dictionary if that language pair is + not currently in the stored glossary. +- `replaceMultilingualGlossaryDictionary()` takes a glossary ID or `MultilingualGlossaryInfo` + object, plus a source language, target language, and a dictionary of entries. + It will then either set the entries to the parameter value, completely + replacing any pre-existing entries for that language pair. +- `updateMultilingualGlossaryName()` takes a glossary ID or `MultilingualGlossaryInfo` + object, plus the new name of the glossary. + +```java +// Update glossary dictionary +class Example { // Continuing class Example from above + public updateGlossaryEntriesExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("artist", "Maler"); + put("hello", "guten tag"); + }}; + List dictionaries = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entries)); + MultilingualGlossaryInfo myGlossary = client.createMultilingualGlossary( + "My glossary", + dictionaries + ); + + GlossaryEntries newEntries = new GlossaryEntries() {{ + put("hello", "hallo"); + put("prize", "Gewinn"); + }}; + + MultilingualGlossaryDictionaryEntries glossaryDict = + new MultilingualGlossaryDictionaryEntries("EN", "DE", newEntries); + + MultilingualGlossaryInfo updatedGlossary = + client.updateMultilingualGlossaryDictionary(myGlossary, glossaryDict); + + MultilingualGlossaryInfo entriesResponse = + client.getMultilingualGlossaryDictionaryEntries(myGlossary, "EN", "DE"); + + for (Map.Entry entry : glossaryDicts.getDictionaries()[0].getEntries().entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + + // prints: + // artist:Maler + // hello:hallo + // prize:Gewinn + } + + // Update a glossary dictionary from CSV + public updateGlossaryEntriesFromCsvExample() throws Exception { + File csvFile = new File("/path/to/glossary_file.csv"); + String glossaryId = "559192ed-8e23-..."; + MultilingualGlossaryInfo myGlossary = + client.createMultilingualGlossaryDictionaryFromCsv(glossaryId, + "en", + "de", + csvFile); + } + + + // Update a glossary name + public void updateGlossaryNameExample() throws Exception { + String glossaryId = "559192ed-8e23-..."; + MultilingualGlossaryInfo myGlossary = + client.updateMultilingualName(glossaryId, "My new glossary name"); + System.out.println(myGlossary.getName()); // 'My new glossary name' + } + + // Replace a glossary dictionary + public void replaceGlossaryEntriesExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("artist", "Maler"); + put("hello", "guten tag"); + }}; + List dictionaries = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entries)); + MultilingualGlossaryInfo myGlossary = client.createMultilingualGlossary( + "My glossary", + dictionaries + ); + + GlossaryEntries newEntries = new GlossaryEntries() {{ + put("goodbye", "Auf Weidersehen"); + }}; + + MultilingualGlossaryDictionaryEntries glossaryDict = + new MultilingualGlossaryDictionaryEntries("EN", "DE", newEntries); + + MultilingualGlossaryInfo updatedGlossary = + client.replaceMultilingualGlossaryDictionary(myGlossary, glossaryDict); + + MultilingualGlossaryInfo entriesResponse = + client.getMultilingualGlossaryDictionaryEntries(myGlossary, "EN", "DE"); + + for (Map.Entry entry : glossaryDicts.getDictionaries()[0].getEntries().entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + + // prints: + // goodbye:Auf Weidersehen + } + + // Replace a glossary dictionary from CSV + public void replaceGlossaryEntriesFromCsvExample() throws Exception { + File csvFile = new File("/path/to/glossary_file.csv"); + String glossaryId = "559192ed-8e23-..."; + MultilingualGlossaryInfo myGlossary = + client.replaceMultilingualGlossaryDictionaryFromCsv(glossaryId, + "en", + "de", + csvFile); + MultilingualGlossaryInfo entriesResponse = + client.getMultilingualGlossaryDictionaryEntries(myGlossary, "EN", "DE"); + } +} +``` + #### Using a stored glossary You can use a stored glossary for text translation by setting the `glossary` -argument to either the glossary ID or `GlossaryInfo` object. You must also +argument to either the glossary ID or `MultilingualGlossaryInfo` object. You must also specify the `source_lang` argument (it is required when using a glossary): ```java class Example { // Continuing class Example from above - public usingGlossaryExample() throws Exception { + public boid usingGlossaryExample() throws Exception { String text = "The artist was awarded a prize."; TextTranslationOptions options = new TextTranslationOptions().setGlossary(my_glossary); @@ -455,7 +621,7 @@ argument and specify the `source_lang` argument: ```java class Example { // Continuing class Example from above - public getListDeleteGlossaryExamples() throws Exception { + public boid getListDeleteGlossaryExamples() throws Exception { String glossaryId = "559192ed-8e23-..."; DocumentTranslationOptions options = new DocumentTranslationOptions().setGlossary(glossaryId); diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 53980bb..5a20656 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation("org.jetbrains:annotations:20.1.0") testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("org.mockito:mockito-inline:4.11.0") + implementation("org.apache.httpcomponents:httpclient:4.5.2") { because("java.net.HttpURLConnection does not support PATCH") } // implementation("com.google.guava:guava:30.1.1-jre") implementation("com.google.code.gson:gson:2.10.1") diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 9d4b825..a7eeacf 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -5,7 +5,10 @@ package com.deepl.api; import com.deepl.api.http.HttpResponse; -import com.deepl.api.utils.KeyValuePair; +import com.deepl.api.utils.*; +import java.io.*; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; @@ -80,6 +83,747 @@ public List rephraseText( return jsonParser.parseWriteResult(response.getBody()); } + /** + * Creates a glossary in your DeepL account with the specified details and returns a {@link + * MultilingualGlossaryInfo} object with details about the newly created glossary. The glossary + * will contain the glossary dictionaries specified in each with + * their own source language, target language and entries.The glossary can be used in translations + * to override translations for specific terms (words). The glossary must contain a glossary + * dictionary that matches the languages of translations for which it will be used. + * + * @param name User-defined name to assign to the glossary; must not be empty. + * @param glossaryDicts {@link MultilingualGlossaryDictionaryInfo} The dictionaries of the + * glossary + * @return {@link MultilingualGlossaryInfo} object with details about the newly created glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo createMultilingualGlossary( + String name, List glossaryDicts) + throws DeepLException, IllegalArgumentException, InterruptedException { + validateParameter("name", name); + if (glossaryDicts.isEmpty()) { + throw new IllegalArgumentException("Parameter dictionaries must not be empty"); + } + ArrayList> bodyParams = + createGlossaryHttpParams(name, glossaryDicts); + HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v3/glossaries", bodyParams); + checkResponse(response, false, false); + return jsonParser.parseMultilingualGlossaryInfo(response.getBody()); + } + + /** + * Creates a glossary in your DeepL account with the specified details and returns a {@link + * MultilingualGlossaryInfo} object with details about the newly created glossary. The glossary + * will contain a glossary dictionary with the source and target language codes specified and + * entries created from the . The glossary can be used in translations + * to override translations for specific terms (words). The glossary must contain a glossary + * dictionary that matches the languages of translations for which it will be used. + * + * @param name User-defined name to assign to the glossary; must not be empty. + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param csvFile String containing CSV content. + * @return {@link MultilingualGlossaryInfo} object with details about the newly created glossary. + * @throws IllegalArgumentException If any argument is invalid. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo createMultilingualGlossaryFromCsv( + String name, String sourceLanguageCode, String targetLanguageCode, String csvFile) + throws DeepLException, IllegalArgumentException, InterruptedException { + return createGlossaryFromCsvInternal(name, sourceLanguageCode, targetLanguageCode, csvFile); + } + /** + * Creates a glossary in your DeepL account with the specified details and returns a {@link + * MultilingualGlossaryInfo} object with details about the newly created glossary. The glossary + * will contain a glossary dictionary with the source and target language codes specified and + * entries created from the . The glossary can be used in translations + * to override translations for specific terms (words). The glossary must contain a glossary + * dictionary that matches the languages of translations for which it will be used. + * + * @param name User-defined name to assign to the glossary; must not be empty. + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param csvFile String containing CSV content. + * @return {@link MultilingualGlossaryInfo} object with details about the newly created glossary. + * @throws IllegalArgumentException If any argument is invalid. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + * @throws IOException If an I/O error occurs. + */ + public MultilingualGlossaryInfo createMultilingualGlossaryFromCsv( + String name, String sourceLanguageCode, String targetLanguageCode, File csvFile) + throws DeepLException, IllegalArgumentException, InterruptedException, IOException { + try (FileInputStream stream = new FileInputStream(csvFile)) { + String csvContent = StreamUtil.readStream(stream); + return createGlossaryFromCsvInternal( + name, sourceLanguageCode, targetLanguageCode, csvContent); + } + } + + /** + * Retrieves information about the glossary with the specified ID and returns a {@link + * MultilingualGlossaryInfo} object containing details. This does not retrieve the glossary + * entries; to retrieve entries use {@link + * DeepLClient#getMultilingualGlossaryDictionaryEntries(String, String, String)} + * + * @param glossaryId ID of glossary to retrieve. + * @return {@link MultilingualGlossaryInfo} object with details about the specified glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public MultilingualGlossaryInfo getMultilingualGlossary(String glossaryId) + throws DeepLException, InterruptedException { + String relativeUrl = String.format("/v3/glossaries/%s", glossaryId); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, true); + return jsonParser.parseMultilingualGlossaryInfo(response.getBody()); + } + + /** + * Retrieves information about all glossaries and returns an array of {@link + * MultilingualGlossaryInfo} objects containing details. This does not retrieve the glossary + * entries; to retrieve entries use {@link + * DeepLClient#getMultilingualGlossaryDictionaryEntries(String, String, String)} + * + * @return List of {@link MultilingualGlossaryInfo} objects with details about each glossary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public List listMultilingualGlossaries() + throws DeepLException, InterruptedException { + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff("/v3/glossaries"); + checkResponse(response, false, false); + return jsonParser.parseMultilingualGlossaryInfoList(response.getBody()); + } + + /** + * For the glossary with the specified ID, retrieves the glossary dictionary with its entries for + * the given source and target language code pair. + * + * @param glossaryId ID of glossary for which to retrieve entries. + * @param sourceLanguageCode Source language code for the requested glossary dictionary. + * @param targetLanguageCode Target language code of the requested glossary dictionary. + * @return {@link MultilingualGlossaryDictionaryEntries} object containing a glossary dictionary + * with entries. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryEntries getMultilingualGlossaryDictionaryEntries( + String glossaryId, String sourceLanguageCode, String targetLanguageCode) + throws DeepLException, IllegalArgumentException, InterruptedException { + validateParameter("glossaryId", glossaryId); + String queryString = createLanguageQueryParams(sourceLanguageCode, targetLanguageCode); + String relativeUrl = String.format("/v3/glossaries/%s/entries%s", glossaryId, queryString); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, true); + return jsonParser + .parseMultilingualGlossaryDictionaryListResponse(response.getBody()) + .getDictionaries() + .get(0) + .getDictionaryEntries(); + } + + /** + * For the glossary with the specified ID, retrieves the glossary dictionary with its entries for + * the given {@link MultilingualGlossaryDictionaryInfo} glossary dictionary. + * + * @param glossaryId ID of glossary for which to retrieve entries. + * @param glossaryDict The requested glossary dictionary. + * @return {@link MultilingualGlossaryDictionaryEntries} object containing a glossary dictionary + * with entries. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryEntries getMultilingualGlossaryDictionaryEntries( + String glossaryId, MultilingualGlossaryDictionaryInfo glossaryDict) + throws DeepLException, IllegalArgumentException, InterruptedException { + return getMultilingualGlossaryDictionaryEntries( + glossaryId, glossaryDict.getSourceLanguageCode(), glossaryDict.getTargetLanguageCode()); + } + + /** + * For the specified glossary, retrieves the glossary dictionary with its entries for the given + * source and target language code pair. + * + * @param glossary The glossary for which to retrieve entries. + * @param sourceLanguageCode Source language code for the requested glossary dictionary. + * @param targetLanguageCode Target language code of the requested glossary dictionary. + * @return {@link MultilingualGlossaryDictionaryEntries} object containing a glossary dictionary + * with entries. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryEntries getMultilingualGlossaryDictionaryEntries( + MultilingualGlossaryInfo glossary, String sourceLanguageCode, String targetLanguageCode) + throws DeepLException, IllegalArgumentException, InterruptedException { + return getMultilingualGlossaryDictionaryEntries( + glossary.getGlossaryId(), sourceLanguageCode, targetLanguageCode); + } + + /** + * For the specified glossary, retrieves the glossary dictionary with its entries for the given + * {@link MultilingualGlossaryDictionaryInfo} glossary dictionary. + * + * @param glossary The glossary for which to retrieve entries. + * @param glossaryDict The requested glossary dictionary. + * @return {@link MultilingualGlossaryDictionaryEntries} object containing a glossary dictionary + * with entries. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryEntries getMultilingualGlossaryDictionaryEntries( + MultilingualGlossaryInfo glossary, MultilingualGlossaryDictionaryInfo glossaryDict) + throws DeepLException, IllegalArgumentException, InterruptedException { + return getMultilingualGlossaryDictionaryEntries( + glossary.getGlossaryId(), + glossaryDict.getSourceLanguageCode(), + glossaryDict.getTargetLanguageCode()); + } + + /** + * Replaces a glossary dictionary with given entries for the source and target language codes. If + * no such glossary dictionary exists for that language pair, a new glossary dictionary will be + * created for that language pair and entries. + * + * @param glossaryId The specified ID of the glossary that contains the dictionary to be + * replaced/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param entries The source-target entry pairs in the new glossary dictionary. + * @return {@link MultilingualGlossaryDictionaryInfo} object with details about the newly replaced + * glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary( + String glossaryId, + String sourceLanguageCode, + String targetLanguageCode, + GlossaryEntries entries) + throws DeepLException, IllegalArgumentException, InterruptedException { + return replaceGlossaryDictionaryInternal( + glossaryId, sourceLanguageCode, targetLanguageCode, entries.toTsv(), "tsv"); + } + + /** + * Replaces a glossary dictionary with given entries for the source and target language codes. If + * no such glossary dictionary exists for that language pair, a new glossary dictionary will be + * created for that language pair and entries. + * + * @param glossaryId The specified ID of the glossary that contains the dictionary to be + * replaced/created + * @param glossaryDict The glossary dictionary to replace the existing glossary dictionary for + * that source/target language code pair or to be newly created if no such glossary dictionary + * exists. + * @return {@link MultilingualGlossaryDictionaryInfo} object with details about the newly replaced + * glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary( + String glossaryId, MultilingualGlossaryDictionaryEntries glossaryDict) + throws DeepLException, IllegalArgumentException, InterruptedException { + return replaceGlossaryDictionaryInternal( + glossaryId, + glossaryDict.getSourceLanguageCode(), + glossaryDict.getTargetLanguageCode(), + glossaryDict.getEntries().toTsv(), + "tsv"); + } + + /** + * Replaces a glossary dictionary with given entries for given glossary dictionary. If no such + * glossary dictionary exists for that language pair, a new glossary dictionary will be created + * for that language pair and entries. + * + * @param glossary The specified glossary that contains the dictionary to be replaced/created + * @param glossaryDict The glossary dictionary to replace the existing glossary dictionary for + * that source/target language code pair or to be newly created if no such glossary dictionary + * exists. + * @return {@link MultilingualGlossaryDictionaryInfo} object with details about the newly replaced + * glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary( + MultilingualGlossaryInfo glossary, MultilingualGlossaryDictionaryEntries glossaryDict) + throws DeepLException, IllegalArgumentException, InterruptedException { + return replaceGlossaryDictionaryInternal( + glossary.getGlossaryId(), + glossaryDict.getSourceLanguageCode(), + glossaryDict.getTargetLanguageCode(), + glossaryDict.getEntries().toTsv(), + "tsv"); + } + + /** + * Replaces a glossary dictionary with given entries for the source and target language codes. If + * no such glossary dictionary exists for that language pair, a new glossary dictionary will be + * created for that language pair and entries. + * + * @param glossary The specified glossary that contains the dictionary to be replaced/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param entries The source-target entry pairs in the new glossary dictionary. + * @return {@link MultilingualGlossaryDictionaryInfo} object with details about the newly replaced + * glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary( + MultilingualGlossaryInfo glossary, + String sourceLanguageCode, + String targetLanguageCode, + GlossaryEntries entries) + throws DeepLException, IllegalArgumentException, InterruptedException { + return replaceGlossaryDictionaryInternal( + glossary.getGlossaryId(), sourceLanguageCode, targetLanguageCode, entries.toTsv(), "tsv"); + } + + /** + * Replaces a glossary dictionary with given entries for the source and target language codes. If + * no such glossary dictionary exists for that language pair, a new glossary dictionary will be + * created for that language pair and entries specified in the {@code csvFile}. + * + * @param glossaryId The specified Id of the glossary that contains the dictionary to be + * replaced/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param csvFile File containing CSV content. + * @return {@link MultilingualGlossaryDictionaryInfo} object with details about the newly replaced + * glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + * @throws IOException If an I/O error occurs. + */ + public MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionaryFromCsv( + String glossaryId, String sourceLanguageCode, String targetLanguageCode, File csvFile) + throws DeepLException, IllegalArgumentException, InterruptedException, IOException { + try (FileInputStream stream = new FileInputStream(csvFile)) { + String csvContent = StreamUtil.readStream(stream); + return replaceGlossaryDictionaryInternal( + glossaryId, sourceLanguageCode, targetLanguageCode, csvContent, "csv"); + } + } + + /** + * Replaces a glossary dictionary with given entries for the source and target language codes. If + * no such glossary dictionary exists for that language pair, a new glossary dictionary will be + * created for that language pair and entries specified in the {@code csvContent}. + * + * @param glossaryId The specified ID of the glossary that contains the dictionary to be + * replaced/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param csvContent String containing CSV content. + * @return {@link MultilingualGlossaryDictionaryInfo} object with details about the newly replaced + * glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionaryFromCsv( + String glossaryId, String sourceLanguageCode, String targetLanguageCode, String csvContent) + throws DeepLException, IllegalArgumentException, InterruptedException { + return replaceGlossaryDictionaryInternal( + glossaryId, sourceLanguageCode, targetLanguageCode, csvContent, "csv"); + } + + /** + * Updates a glossary dictionary with given entries for the source and target language codes. The + * glossary dictionary must belong to the glossary with the ID specified in . If a dictionary for the provided language pair already exists, the + * dictionary entries are merged. + * + * @param glossaryId The specified ID of the glossary that contains the dictionary to be + * updated/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param entries The source-target entry pairs in the new glossary dictionary. + * @return {@link MultilingualGlossaryInfo} object with details about the glossary with the newly + * updated glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo updateMultilingualGlossaryDictionary( + String glossaryId, + String sourceLanguageCode, + String targetLanguageCode, + GlossaryEntries entries) + throws DeepLException, IllegalArgumentException, InterruptedException { + return updateGlossaryDictionaryInternal( + glossaryId, sourceLanguageCode, targetLanguageCode, entries.toTsv(), "tsv"); + } + + /** + * Updates a glossary dictionary with given entries for the source and target language codes. The + * glossary dictionary must belong to the glossary specified in . If a + * dictionary for the provided language pair already exists, the dictionary entries are merged. + * + * @param glossary The specified ID for the glossary that contains the dictionary to be + * updated/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param entries The source-target entry pairs in the new glossary dictionary. + * @return {@link MultilingualGlossaryInfo} object with details about the glossary with the newly + * updated glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo updateMultilingualGlossaryDictionary( + MultilingualGlossaryInfo glossary, + String sourceLanguageCode, + String targetLanguageCode, + GlossaryEntries entries) + throws DeepLException, IllegalArgumentException, InterruptedException { + return updateGlossaryDictionaryInternal( + glossary.getGlossaryId(), sourceLanguageCode, targetLanguageCode, entries.toTsv(), "tsv"); + } + + /** + * Updates a glossary dictionary with given glossary dictionary specified in . The glossary dictionary must belong to the glossary with the ID + * specified in . If a dictionary for the provided language pair + * already exists, the dictionary entries are merged. + * + * @param glossaryId The specified ID of the glossary that contains the dictionary to be + * updated/created + * @param glossaryDict The glossary dictionary to be created/updated + * @return {@link MultilingualGlossaryInfo} object with details about the glossary with the newly + * updated glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo updateMultilingualGlossaryDictionary( + String glossaryId, MultilingualGlossaryDictionaryEntries glossaryDict) + throws DeepLException, IllegalArgumentException, InterruptedException { + return updateGlossaryDictionaryInternal( + glossaryId, + glossaryDict.getSourceLanguageCode(), + glossaryDict.getTargetLanguageCode(), + glossaryDict.getEntries().toTsv(), + "tsv"); + } + + /** + * Updates a glossary dictionary with given entries for the source and target language codes. If a + * dictionary for the provided language pair already exists, the dictionary entries are merged. + * + * @param glossary The specified glossary that contains the dictionary to be updated/created + * @param glossaryDict The glossary dictionary to be created/updated + * @return {@link MultilingualGlossaryInfo} object with details about the glossary with the newly + * updated glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo updateMultilingualGlossaryDictionary( + MultilingualGlossaryInfo glossary, MultilingualGlossaryDictionaryEntries glossaryDict) + throws DeepLException, IllegalArgumentException, InterruptedException { + return updateGlossaryDictionaryInternal( + glossary.getGlossaryId(), + glossaryDict.getSourceLanguageCode(), + glossaryDict.getTargetLanguageCode(), + glossaryDict.getEntries().toTsv(), + "tsv"); + } + + /** + * Updates a glossary's name with the provided parameter + * + * @param glossaryId The specified ID of the glossary whose name will be updated + * @param name The new name of the glossary + * @return {@link MultilingualGlossaryInfo} object with details about the glossary with the newly + * updated glossary dictionary. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo updateMultilingualGlossaryName(String glossaryId, String name) + throws DeepLException, IllegalArgumentException { + + ArrayList> bodyParams = new ArrayList<>(); + bodyParams.add(new KeyValuePair<>("name", name)); + String relativeUrl = String.format("/v3/glossaries/%s", glossaryId); + HttpResponse response = httpClientWrapper.sendPatchRequestWithBackoff(relativeUrl, bodyParams); + checkResponse(response, false, true); + return jsonParser.parseMultilingualGlossaryInfo(response.getBody()); + } + + /** + * Updates a glossary dictionary correlating to the specified ID with given entries in the {@code + * csvFile} for the source and target language codes. If a dictionary for the provided language + * pair already exists, the dictionary entries are merged. + * + * @param glossaryId The specified ID of the glossary that contains the dictionary to be + * updated/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param csvFile {@link File} containing CSV content. + * @return {@link MultilingualGlossaryInfo} object with details about the glossary with the newly + * updated glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + * @throws IOException If an I/O error occurs. + */ + public MultilingualGlossaryInfo updateMultilingualGlossaryDictionaryFromCsv( + String glossaryId, String sourceLanguageCode, String targetLanguageCode, File csvFile) + throws DeepLException, IllegalArgumentException, InterruptedException, IOException { + try (FileInputStream stream = new FileInputStream(csvFile)) { + String csvContent = StreamUtil.readStream(stream); + return updateGlossaryDictionaryInternal( + glossaryId, sourceLanguageCode, targetLanguageCode, csvContent, "csv"); + } + } + + /** + * Updates a glossary dictionary with given entries in the {@code csvFile} for the source and + * target language codes. If a dictionary for the provided language pair already exists, the + * dictionary entries are merged. + * + * @param glossaryId The specified ID of the glossary that contains the dictionary to be + * updated/created + * @param sourceLanguageCode Language code of the source terms language. + * @param targetLanguageCode Language code of the target terms language. + * @param csvContent String containing CSV content. + * @return {@link MultilingualGlossaryInfo} object with details about the glossary with the newly + * updated glossary dictionary. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public MultilingualGlossaryInfo updateMultilingualGlossaryDictionaryFromCsv( + String glossaryId, String sourceLanguageCode, String targetLanguageCode, String csvContent) + throws DeepLException, IllegalArgumentException, InterruptedException { + return updateGlossaryDictionaryInternal( + glossaryId, sourceLanguageCode, targetLanguageCode, csvContent, "csv"); + } + + /** + * Deletes the glossary with the specified ID. + * + * @param glossaryId ID of glossary to delete. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + void deleteMultilingualGlossary(String glossaryId) throws DeepLException, InterruptedException { + String relativeUrl = String.format("/v3/glossaries/%s", glossaryId); + HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); + this.checkResponse(response, false, true); + } + + /** + * Deletes the specified glossary. + * + * @param glossary {@link MultilingualGlossaryInfo} object corresponding to glossary to delete. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public void deleteMultilingualGlossary(MultilingualGlossaryInfo glossary) + throws DeepLException, InterruptedException { + this.deleteMultilingualGlossary(glossary.getGlossaryId()); + } + + /** + * Deletes the glossary dictionary with the source and target language codes specified in the + * glossary with the specified ID. + * + * @param glossaryId ID of glossary that contains the glossary dictionary to delete. + * @param sourceLanguageCode Source language code of the glossary dictionary to be deleted. + * @param targetLanguageCode Target language code of the glossary dictionary to be deleted. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws IllegalArgumentException If any argument is invalid. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public void deleteMultilingualGlossaryDictionary( + String glossaryId, String sourceLanguageCode, String targetLanguageCode) + throws DeepLException, InterruptedException, IllegalArgumentException { + String queryString = createLanguageQueryParams(sourceLanguageCode, targetLanguageCode); + String relativeUrl = String.format("/v3/glossaries/%s/dictionaries%s", glossaryId, queryString); + HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); + this.checkResponse(response, false, true); + } + + /** + * Deletes the specified glossary dictionary in the glossary with the specified ID. + * + * @param glossaryId ID of glossary that contains the glossary dictionary to delete. + * @param glossaryDict {@link MultilingualGlossaryDictionaryInfo} object corresponding to glossary + * dictionary to delete. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public void deleteMultilingualGlossaryDictionary( + String glossaryId, MultilingualGlossaryDictionaryInfo glossaryDict) + throws DeepLException, InterruptedException, IllegalArgumentException { + deleteMultilingualGlossaryDictionary( + glossaryId, glossaryDict.getSourceLanguageCode(), glossaryDict.getTargetLanguageCode()); + } + + /** + * Deletes the specified glossary dictionary in the glossary in the specified glossary. + * + * @param glossary The glossary that contains the glossary dictionary to delete. + * @param sourceLanguageCode Source language code of the glossary dictionary to be deleted. + * @param targetLanguageCode Target language code of the glossary dictionary to be deleted. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public void deleteMultilingualGlossaryDictionary( + MultilingualGlossaryInfo glossary, String sourceLanguageCode, String targetLanguageCode) + throws DeepLException, InterruptedException, IllegalArgumentException { + deleteMultilingualGlossaryDictionary( + glossary.getGlossaryId(), sourceLanguageCode, targetLanguageCode); + } + + /** + * Deletes the specified glossary dictionary in the glossary in the specified glossary. + * + * @param glossary The glossary that contains the glossary dictionary to delete. + * @param glossaryDict {@link MultilingualGlossaryDictionaryInfo} object corresponding to glossary + * dictionary to delete. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public void deleteMultilingualGlossaryDictionary( + MultilingualGlossaryInfo glossary, MultilingualGlossaryDictionaryInfo glossaryDict) + throws DeepLException, InterruptedException, IllegalArgumentException { + deleteMultilingualGlossaryDictionary( + glossary.getGlossaryId(), + glossaryDict.getSourceLanguageCode(), + glossaryDict.getTargetLanguageCode()); + } + + /** Creates a glossary with given details. */ + private MultilingualGlossaryInfo createGlossaryFromCsvInternal( + String name, String sourceLanguageCode, String targetLanguageCode, String entries) + throws DeepLException, InterruptedException { + ArrayList> params = + createGlossaryDictionariesHttpParams( + sourceLanguageCode, targetLanguageCode, entries, "csv"); + params.add(new KeyValuePair<>("name", name)); + HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v3/glossaries", params); + checkResponse(response, false, false); + return jsonParser.parseMultilingualGlossaryInfo(response.getBody()); + } + + /** + * Gets the entries in the glossary with the specified ID for the given source and target + * languages + */ + private MultilingualGlossaryDictionaryInfo replaceGlossaryDictionaryInternal( + String glossaryId, + String sourceLanguageCode, + String targetLanguageCode, + String entries, + String entriesFormat) + throws DeepLException, IllegalArgumentException, InterruptedException { + validateParameter("glossaryId", glossaryId); + validateParameter("sourceLanguageCode", sourceLanguageCode); + validateParameter("targetLanguageCode", targetLanguageCode); + validateParameter("entries", entries); + + ArrayList> bodyParams = new ArrayList<>(); + bodyParams.add(new KeyValuePair<>("source_lang", sourceLanguageCode)); + bodyParams.add(new KeyValuePair<>("target_lang", targetLanguageCode)); + bodyParams.add(new KeyValuePair<>("entries", entries)); + bodyParams.add(new KeyValuePair<>("entries_format", entriesFormat)); + + String relativeUrl = String.format("/v3/glossaries/%s/dictionaries", glossaryId); + HttpResponse response = httpClientWrapper.sendPutRequestWithBackoff(relativeUrl, bodyParams); + checkResponse(response, false, true); + return jsonParser.parseMultilingualGlossaryDictionaryInfo(response.getBody()); + } + + /** + * Gets the entries in the glossary with the specified ID for the given source and target + * languages + */ + private MultilingualGlossaryInfo updateGlossaryDictionaryInternal( + String glossaryId, + String sourceLanguageCode, + String targetLanguageCode, + String entries, + String entriesFormat) + throws DeepLException, IllegalArgumentException, InterruptedException { + validateParameter("glossaryId", glossaryId); + validateParameter("sourceLanguageCode", sourceLanguageCode); + validateParameter("targetLanguageCode", targetLanguageCode); + validateParameter("entries", entries); + + ArrayList> bodyParams = + createGlossaryDictionariesHttpParams( + sourceLanguageCode, targetLanguageCode, entries, entriesFormat); + String relativeUrl = String.format("/v3/glossaries/%s", glossaryId); + HttpResponse response = httpClientWrapper.sendPatchRequestWithBackoff(relativeUrl, bodyParams); + checkResponse(response, false, true); + return jsonParser.parseMultilingualGlossaryInfo(response.getBody()); + } + + /** Creates query string for the source and target languages */ + private String createLanguageQueryParams(String sourceLanguageCode, String targetLanguageCode) + throws IllegalArgumentException, DeepLException { + validateParameter("sourceLanguageCode", sourceLanguageCode); + validateParameter("targetLanguageCode", targetLanguageCode); + try { + return "?" + + String.join( + "&", + String.format( + "source_lang=%s", + URLEncoder.encode(sourceLanguageCode, StandardCharsets.UTF_8.name())), + String.format( + "target_lang=%s", + URLEncoder.encode(targetLanguageCode, StandardCharsets.UTF_8.name()))); + } catch (UnsupportedEncodingException exception) { + throw new DeepLException("Error while URL-encoding request", exception); + } + } + + private void validateParameter(String paramName, String value) throws IllegalArgumentException { + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException( + String.format("Parameter %s must not be empty", paramName)); + } + } + protected static ArrayList> createWriteHttpParams( List texts, @Nullable String targetLang, @Nullable TextRephraseOptions options) { targetLang = LanguageCode.standardize(targetLang); @@ -104,4 +848,38 @@ protected static ArrayList> createWriteHttpParams( return params; } + + protected static ArrayList> createGlossaryHttpParams( + String name, List glossaryDicts) { + ArrayList> bodyParams = new ArrayList<>(); + bodyParams.add(new KeyValuePair<>("name", name)); + for (int i = 0; i < glossaryDicts.size(); i++) { + bodyParams.add( + new KeyValuePair<>( + String.format("dictionaries[%d].source_lang", i), + glossaryDicts.get(i).getSourceLanguageCode())); + bodyParams.add( + new KeyValuePair<>( + String.format("dictionaries[%d].target_lang", i), + glossaryDicts.get(i).getTargetLanguageCode())); + bodyParams.add( + new KeyValuePair<>( + String.format("dictionaries[%d].entries", i), + glossaryDicts.get(i).getEntries().toTsv())); + bodyParams.add( + new KeyValuePair<>(String.format("dictionaries[%d].entries_format", i), "tsv")); + } + return bodyParams; + } + + protected static ArrayList> createGlossaryDictionariesHttpParams( + String sourceLanguageCode, String targetLanguageCode, String entries, String entriesFormat) { + ArrayList> bodyParams = new ArrayList<>(); + bodyParams.add(new KeyValuePair<>("dictionaries[0].source_lang", sourceLanguageCode)); + bodyParams.add(new KeyValuePair<>("dictionaries[0].target_lang", targetLanguageCode)); + bodyParams.add(new KeyValuePair<>("dictionaries[0].entries", entries)); + bodyParams.add(new KeyValuePair<>("dictionaries[0].entries_format", entriesFormat)); + + return bodyParams; + } } diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java index c143a8a..562f9c8 100644 --- a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java @@ -43,7 +43,7 @@ public DocumentTranslationOptions setGlossaryId(String glossaryId) { * Sets the glossary to use with the translation. By default, this value is null and * no glossary is used. */ - public DocumentTranslationOptions setGlossary(GlossaryInfo glossary) { + public DocumentTranslationOptions setGlossary(IGlossary glossary) { return setGlossary(glossary.getGlossaryId()); } diff --git a/deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java b/deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java index 15022b9..34c0b07 100644 --- a/deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java +++ b/deepl-java/src/main/java/com/deepl/api/GlossaryInfo.java @@ -8,7 +8,7 @@ import org.jetbrains.annotations.*; /** Information about a glossary, excluding the entry list. */ -public class GlossaryInfo { +public class GlossaryInfo implements IGlossary { @SerializedName(value = "glossary_id") private final String glossaryId; diff --git a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java index b589be0..75d583d 100644 --- a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java +++ b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java @@ -9,6 +9,12 @@ import java.net.*; import java.time.*; import java.util.*; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import org.jetbrains.annotations.*; /** @@ -21,6 +27,7 @@ class HttpClientWrapper { private static final String GET = "GET"; private static final String POST = "POST"; private static final String DELETE = "DELETE"; + private static final String PUT = "PUT"; private final String serverUrl; private final Map headers; private final Duration minTimeout; @@ -45,9 +52,16 @@ public HttpResponse sendGetRequestWithBackoff(String relativeUrl) return sendRequestWithBackoff(GET, relativeUrl, null).toStringResponse(); } + public HttpResponse sendDeleteRequestWithBackoff( + String relativeUrl, @Nullable Iterable> params) + throws InterruptedException, DeepLException { + HttpContent content = HttpContent.buildFormURLEncodedContent(params); + return sendRequestWithBackoff(DELETE, relativeUrl, content).toStringResponse(); + } + public HttpResponse sendDeleteRequestWithBackoff(String relativeUrl) throws InterruptedException, DeepLException { - return sendRequestWithBackoff(DELETE, relativeUrl, null).toStringResponse(); + return sendDeleteRequestWithBackoff(relativeUrl, null); } public HttpResponse sendRequestWithBackoff(String relativeUrl) @@ -62,6 +76,56 @@ public HttpResponse sendRequestWithBackoff( return sendRequestWithBackoff(POST, relativeUrl, content).toStringResponse(); } + public HttpResponse sendPutRequestWithBackoff( + String relativeUrl, @Nullable Iterable> params) + throws InterruptedException, DeepLException { + HttpContent content = HttpContent.buildFormURLEncodedContent(params); + return sendRequestWithBackoff(PUT, relativeUrl, content).toStringResponse(); + } + + public HttpResponse sendPatchRequestWithBackoff( + String relativeUrl, @Nullable Iterable> params) + throws DeepLException { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpContent content = HttpContent.buildFormURLEncodedContent(params); + HttpPatch request = new HttpPatch(serverUrl + relativeUrl); + + // Set timeouts + BackoffTimer backoffTimer = new BackoffTimer(this.minTimeout); + RequestConfig requestConfig = + RequestConfig.custom() + .setConnectTimeout((int) backoffTimer.getTimeoutMillis()) + .setSocketTimeout((int) backoffTimer.getTimeoutMillis()) + .build(); + request.setConfig(requestConfig); + + // Set headers + for (Map.Entry entry : this.headers.entrySet()) { + request.setHeader(entry.getKey(), entry.getValue()); + } + + request.setHeader("Content-Type", content.getContentType()); + request.setEntity(new ByteArrayEntity(content.getContent())); + + // Execute the request + org.apache.http.HttpResponse response = httpClient.execute(request); + + // Get the response stream + InputStream responseStream = + (response.getStatusLine().getStatusCode() >= 200 + && response.getStatusLine().getStatusCode() < 400) + ? response.getEntity().getContent() + : new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity())); + + return new HttpResponseStream(response.getStatusLine().getStatusCode(), responseStream) + .toStringResponse(); + } catch (SocketTimeoutException e) { + throw new ConnectionException(e.getMessage(), true, e); + } catch (RuntimeException | IOException e) { + throw new ConnectionException(e.getMessage(), false, e); + } + } + public HttpResponseStream downloadWithBackoff( String relativeUrl, @Nullable Iterable> params) throws InterruptedException, DeepLException { diff --git a/deepl-java/src/main/java/com/deepl/api/IGlossary.java b/deepl-java/src/main/java/com/deepl/api/IGlossary.java new file mode 100644 index 0000000..a94d7fe --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/IGlossary.java @@ -0,0 +1,10 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** Interface representing a glossary. */ +public interface IGlossary { + /** @return Unique ID assigned to the glossary. */ + String getGlossaryId(); +} diff --git a/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryEntries.java b/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryEntries.java new file mode 100644 index 0000000..14925d2 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryEntries.java @@ -0,0 +1,41 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +/** Stores the entries of a glossary. */ +public class MultilingualGlossaryDictionaryEntries { + private final String sourceLanguageCode; + private final String targetLanguageCode; + private final GlossaryEntries entries; + + /** + * Initializes a new {@link MultilingualGlossaryDictionaryInfo} containing information about a + * glossary dictionary. + * + * @param sourceLanguageCode the source language for this dictionary + * @param targetLanguageCode the target language for this dictionary + * @param entries the entries in this dictionary + */ + public MultilingualGlossaryDictionaryEntries( + String sourceLanguageCode, String targetLanguageCode, GlossaryEntries entries) { + this.sourceLanguageCode = sourceLanguageCode; + this.targetLanguageCode = targetLanguageCode; + this.entries = entries; + } + + /** @return the source language code */ + public String getSourceLanguageCode() { + return this.sourceLanguageCode; + } + + /** @return the target language code */ + public String getTargetLanguageCode() { + return this.targetLanguageCode; + } + + /** @return the entry count */ + public GlossaryEntries getEntries() { + return this.entries; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryInfo.java b/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryInfo.java new file mode 100644 index 0000000..4c04aff --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryDictionaryInfo.java @@ -0,0 +1,48 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.SerializedName; + +/** Stores the entries of a glossary. */ +public class MultilingualGlossaryDictionaryInfo { + @SerializedName(value = "source_lang") + private final String sourceLanguageCode; + + @SerializedName(value = "target_lang") + private final String targetLanguageCode; + + @SerializedName(value = "entry_count") + private final long entryCount; + + /** + * Initializes a new {@link MultilingualGlossaryDictionaryInfo} containing information about a + * glossary dictionary. + * + * @param sourceLanguageCode the source language for this dictionary + * @param targetLanguageCode the target language for this dictionary + * @param entryCount the number of entries in this dictionary + */ + public MultilingualGlossaryDictionaryInfo( + String sourceLanguageCode, String targetLanguageCode, long entryCount) { + this.sourceLanguageCode = sourceLanguageCode; + this.targetLanguageCode = targetLanguageCode; + this.entryCount = entryCount; + } + + /** @return the source language code */ + public String getSourceLanguageCode() { + return this.sourceLanguageCode; + } + + /** @return the target language code */ + public String getTargetLanguageCode() { + return this.targetLanguageCode; + } + + /** @return the entry count */ + public long getEntryCount() { + return this.entryCount; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryInfo.java b/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryInfo.java new file mode 100644 index 0000000..528c5bc --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/MultilingualGlossaryInfo.java @@ -0,0 +1,61 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; + +/** Information about a glossary, excluding the entry list. */ +public class MultilingualGlossaryInfo implements IGlossary { + @SerializedName(value = "glossary_id") + private final String glossaryId; + + @SerializedName(value = "name") + private final String name; + + @SerializedName(value = "creation_time") + private final Date creationTime; + + @SerializedName(value = "dictionaries") + private final List dictionaries; + + /** + * Initializes a new {@link MultilingualGlossaryInfo} containing information about a glossary. + * + * @param glossaryId ID of the associated glossary. + * @param name Name of the glossary chosen during creation. + * @param creationTime Time when the glossary was created. + * @param dictionaries A list of dictionaries that are in this glossary + */ + public MultilingualGlossaryInfo( + String glossaryId, + String name, + Date creationTime, + List dictionaries) { + this.glossaryId = glossaryId; + this.name = name; + this.creationTime = creationTime; + this.dictionaries = dictionaries; + } + + /** @return Unique ID assigned to the glossary. */ + public String getGlossaryId() { + return glossaryId; + } + + /** @return User-defined name assigned to the glossary. */ + public String getName() { + return name; + } + + /** @return Timestamp when the glossary was created. */ + public Date getCreationTime() { + return creationTime; + } + + /** @return the list of dictionaries in this glossary */ + public List getDictionaries() { + return dictionaries; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index 7d95788..a4507d6 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -52,7 +52,7 @@ public TextTranslationOptions setGlossaryId(String glossaryId) { * Sets the glossary to use with the translation. By default, this value is null and * no glossary is used. */ - public TextTranslationOptions setGlossary(GlossaryInfo glossary) { + public TextTranslationOptions setGlossary(IGlossary glossary) { return setGlossary(glossary.getGlossaryId()); } diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryEntriesResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryEntriesResponse.java new file mode 100644 index 0000000..823e492 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryEntriesResponse.java @@ -0,0 +1,52 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.GlossaryEntries; +import com.deepl.api.MultilingualGlossaryDictionaryEntries; +import com.google.gson.annotations.SerializedName; + +/** + * Class representing v3 list-glossaries response by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +public class MultilingualGlossaryDictionaryEntriesResponse { + + @SerializedName(value = "source_lang") + private final String sourceLanguageCode; + + @SerializedName(value = "target_lang") + private final String targetLanguageCode; + + @SerializedName(value = "entries") + private final String entries; + + @SerializedName(value = "entries_format") + private final String entriesFormat; + + /** + * Initializes a new {@link MultilingualGlossaryDictionaryEntriesResponse} containing information + * about a glossary dictionary. + * + * @param sourceLanguageCode the source language for this dictionary + * @param targetLanguageCode the target language for this dictionary + * @param entries the entries in this dictionary + * @param entriesFormat the format of the entries in this dictionary + */ + public MultilingualGlossaryDictionaryEntriesResponse( + String sourceLanguageCode, String targetLanguageCode, String entries, String entriesFormat) { + this.sourceLanguageCode = sourceLanguageCode; + this.targetLanguageCode = targetLanguageCode; + this.entries = entries; + this.entriesFormat = entriesFormat; + } + + public MultilingualGlossaryDictionaryEntries getDictionaryEntries() { + return new MultilingualGlossaryDictionaryEntries( + this.sourceLanguageCode, + this.targetLanguageCode, + new GlossaryEntries(GlossaryEntries.fromTsv(this.entries))); + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryListResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryListResponse.java new file mode 100644 index 0000000..d573155 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryDictionaryListResponse.java @@ -0,0 +1,19 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import java.util.List; + +/** + * Class representing v3 list-glossaries response by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +public class MultilingualGlossaryDictionaryListResponse { + private List dictionaries; + + public List getDictionaries() { + return dictionaries; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryListResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryListResponse.java new file mode 100644 index 0000000..fe1da36 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/MultilingualGlossaryListResponse.java @@ -0,0 +1,20 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.MultilingualGlossaryInfo; +import java.util.List; + +/** + * Class representing v3 list-glossaries response by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class MultilingualGlossaryListResponse { + private List glossaries; + + public List getGlossaries() { + return glossaries; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index cc13c5f..c8b7d32 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -62,11 +62,30 @@ public GlossaryInfo parseGlossaryInfo(String json) { return gson.fromJson(json, GlossaryInfo.class); } + public MultilingualGlossaryInfo parseMultilingualGlossaryInfo(String json) { + return gson.fromJson(json, MultilingualGlossaryInfo.class); + } + + public MultilingualGlossaryDictionaryListResponse parseMultilingualGlossaryDictionaryListResponse( + String json) { + return gson.fromJson(json, MultilingualGlossaryDictionaryListResponse.class); + } + public List parseGlossaryInfoList(String json) { GlossaryListResponse result = gson.fromJson(json, GlossaryListResponse.class); return result.getGlossaries(); } + public List parseMultilingualGlossaryInfoList(String json) { + MultilingualGlossaryListResponse result = + gson.fromJson(json, MultilingualGlossaryListResponse.class); + return result.getGlossaries(); + } + + public MultilingualGlossaryDictionaryInfo parseMultilingualGlossaryDictionaryInfo(String json) { + return gson.fromJson(json, MultilingualGlossaryDictionaryInfo.class); + } + public String parseErrorMessage(String json) { ErrorResponse response = gson.fromJson(json, ErrorResponse.class); diff --git a/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java new file mode 100644 index 0000000..13a2712 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java @@ -0,0 +1,59 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +public class MultilingualGlossaryCleanupUtility implements AutoCloseable { + private final String glossaryName; + private final DeepLClient deepLClient; + + public MultilingualGlossaryCleanupUtility(DeepLClient deepLClient) { + this(deepLClient, ""); + } + + public MultilingualGlossaryCleanupUtility(DeepLClient deepLClient, String testNameSuffix) { + String callingFunc = getCallerFunction(); + String uuid = UUID.randomUUID().toString(); + + this.glossaryName = + String.format("deepl-java-test-glossary: %s%s %s", callingFunc, testNameSuffix, uuid); + this.deepLClient = deepLClient; + } + + public String getGlossaryName() { + return glossaryName; + } + + @Override + public void close() throws Exception { + List glossaries = deepLClient.listMultilingualGlossaries(); + for (MultilingualGlossaryInfo glossary : glossaries) { + if (Objects.equals(glossary.getName(), glossaryName)) { + try { + // TODO replace with v3 delete glossary + deepLClient.deleteGlossary(glossary.getGlossaryId()); + } catch (Exception exception) { + // Ignore + } + } + } + } + + private static String getCallerFunction() { + StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace(); + // Find the first function outside this class following functions in this class + for (int i = 1; i < stacktrace.length; i++) { + if (!stacktrace[i].getClassName().equals(MultilingualGlossaryCleanupUtility.class.getName()) + && stacktrace[i - 1] + .getClassName() + .equals(MultilingualGlossaryCleanupUtility.class.getName())) { + return stacktrace[i].getMethodName(); + } + } + return "unknown"; + } +} diff --git a/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java new file mode 100644 index 0000000..b404c62 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java @@ -0,0 +1,697 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import java.io.File; +import java.util.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class MultilingualGlossaryTest extends TestBase { + private final String invalidGlossaryId = "invalid_glossary_id"; + private final String nonexistentGlossaryId = "96ab91fd-e715-41a1-adeb-5d701f84a483"; + private final String sourceLang = "en"; + private final String targetLang = "de"; + + private final GlossaryEntries testEntries = GlossaryEntries.fromTsv("Hello\tHallo"); + private final MultilingualGlossaryDictionaryEntries testGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, testEntries); + + @Test + void testGlossaryCreate() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + GlossaryEntries entries = new GlossaryEntries(Collections.singletonMap("Hello", "Hallo")); + System.out.println(entries); + for (Map.Entry entry : entries.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + String glossaryName = cleanup.getGlossaryName(); + List glossaryDicts = + Arrays.asList( + testGlossaryDict, + new MultilingualGlossaryDictionaryEntries(targetLang, sourceLang, entries)); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + Assertions.assertEquals(glossaryName, glossary.getName()); + AssertGlossaryDictionariesEquivalent(glossaryDicts, glossary.getDictionaries()); + + MultilingualGlossaryInfo getResult = + deepLClient.getMultilingualGlossary(glossary.getGlossaryId()); + AssertGlossaryDictionariesEquivalent(glossaryDicts, getResult.getDictionaries()); + } + } + + @Test + void testGlossaryCreateLarge() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + + Map entryPairs = new HashMap<>(); + for (int i = 0; i < 10000; i++) { + entryPairs.put(String.format("Source-%d", i), String.format("Target-%d", i)); + } + GlossaryEntries entries = new GlossaryEntries(entryPairs); + Assertions.assertTrue(entries.toTsv().length() > 100000); + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries)); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + Assertions.assertEquals(glossaryName, glossary.getName()); + AssertGlossaryDictionariesEquivalent(glossaryDicts, glossary.getDictionaries()); + } + } + + @Test + void testGlossaryCreateCsv() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + Map expectedEntries = new HashMap<>(); + expectedEntries.put("sourceEntry1", "targetEntry1"); + expectedEntries.put("source\"Entry", "target,Entry"); + + String csvContent = + "sourceEntry1,targetEntry1,en,de\n\"source\"\"Entry\",\"target,Entry\",en,de"; + + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossaryFromCsv( + glossaryName, sourceLang, targetLang, csvContent); + + MultilingualGlossaryDictionaryEntries createdGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries(glossary, sourceLang, targetLang); + Assertions.assertEquals(expectedEntries, createdGlossaryDict.getEntries()); + } + } + + @Test + void testGlossaryCreateInvalid() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> deepLClient.createMultilingualGlossary("", Arrays.asList(testGlossaryDict))); + Assertions.assertThrows( + Exception.class, + () -> + deepLClient.createMultilingualGlossary( + glossaryName, + Arrays.asList( + new MultilingualGlossaryDictionaryEntries("en", "xx", testEntries)))); + } + } + + @Test + void testGlossaryGet() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + List glossaryDicts = Arrays.asList(testGlossaryDict); + MultilingualGlossaryInfo createdGlossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + MultilingualGlossaryInfo glossary = + deepLClient.getMultilingualGlossary(createdGlossary.getGlossaryId()); + Assertions.assertEquals(createdGlossary.getGlossaryId(), glossary.getGlossaryId()); + Assertions.assertEquals(glossaryName, glossary.getName()); + AssertGlossaryDictionariesEquivalent(glossaryDicts, glossary.getDictionaries()); + } + Assertions.assertThrows( + DeepLException.class, () -> deepLClient.getMultilingualGlossary(invalidGlossaryId)); + Assertions.assertThrows( + GlossaryNotFoundException.class, + () -> deepLClient.getMultilingualGlossary(nonexistentGlossaryId)); + } + + @Test + void testGlossaryGetEntries() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + GlossaryEntries entries = new GlossaryEntries(); + entries.put("Apple", "Apfel"); + entries.put("Banana", "Banane"); + entries.put("A%=&", "B&=%"); + entries.put("\u0394\u3041", "\u6DF1"); + entries.put("\uD83E\uDEA8", "\uD83E\uDEB5"); + + MultilingualGlossaryDictionaryEntries glossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries); + MultilingualGlossaryInfo createdGlossary = + deepLClient.createMultilingualGlossary(glossaryName, Arrays.asList(glossaryDict)); + Assertions.assertEquals(1, createdGlossary.getDictionaries().size()); + MultilingualGlossaryDictionaryInfo createdGlossaryDict = + createdGlossary.getDictionaries().get(0); + + MultilingualGlossaryDictionaryEntries updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries( + createdGlossary, sourceLang, targetLang); + Assertions.assertEquals(entries, updatedGlossaryDict.getEntries()); + updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries( + createdGlossary, sourceLang, targetLang); + Assertions.assertEquals(entries, updatedGlossaryDict.getEntries()); + updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries( + createdGlossary, createdGlossaryDict); + Assertions.assertEquals(entries, updatedGlossaryDict.getEntries()); + updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries( + createdGlossary.getGlossaryId(), createdGlossaryDict); + Assertions.assertEquals(entries, updatedGlossaryDict.getEntries()); + + Assertions.assertThrows( + DeepLException.class, + () -> + deepLClient.getMultilingualGlossaryDictionaryEntries( + invalidGlossaryId, sourceLang, targetLang)); + Assertions.assertThrows( + GlossaryNotFoundException.class, + () -> + deepLClient.getMultilingualGlossaryDictionaryEntries( + nonexistentGlossaryId, sourceLang, targetLang)); + Assertions.assertThrows( + Exception.class, + () -> deepLClient.getMultilingualGlossaryDictionaryEntries(createdGlossary, "en", "xx")); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + deepLClient.getMultilingualGlossaryDictionaryEntries( + createdGlossary, "", targetLang)); + } + } + + @Test + void testGlossaryList() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + deepLClient.createMultilingualGlossary(glossaryName, Arrays.asList(testGlossaryDict)); + + List glossaries = deepLClient.listMultilingualGlossaries(); + Assertions.assertTrue( + glossaries.stream() + .anyMatch((glossaryInfo -> Objects.equals(glossaryInfo.getName(), glossaryName)))); + } + } + + @Test + void testGlossaryDelete() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, Arrays.asList(testGlossaryDict)); + + deepLClient.deleteMultilingualGlossary(glossary); + Assertions.assertThrows( + GlossaryNotFoundException.class, () -> deepLClient.deleteMultilingualGlossary(glossary)); + + Assertions.assertThrows( + DeepLException.class, () -> deepLClient.deleteMultilingualGlossary(invalidGlossaryId)); + Assertions.assertThrows( + GlossaryNotFoundException.class, + () -> deepLClient.deleteMultilingualGlossary(nonexistentGlossaryId)); + } + } + + @Test + void testGlossaryDictionaryDelete() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, Arrays.asList(testGlossaryDict)); + + deepLClient.deleteMultilingualGlossaryDictionary(glossary, sourceLang, targetLang); + Assertions.assertThrows( + GlossaryNotFoundException.class, + () -> deepLClient.deleteMultilingualGlossaryDictionary(glossary, sourceLang, targetLang)); + + Assertions.assertThrows( + DeepLException.class, + () -> + deepLClient.deleteMultilingualGlossaryDictionary( + invalidGlossaryId, sourceLang, targetLang)); + Assertions.assertThrows( + GlossaryNotFoundException.class, + () -> + deepLClient.deleteMultilingualGlossaryDictionary( + nonexistentGlossaryId, sourceLang, targetLang)); + } + } + + @Test + void testGlossaryReplaceDictionary() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + List glossaryDicts = Arrays.asList(testGlossaryDict); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + GlossaryEntries newEntries = new GlossaryEntries(); + + newEntries.put("key1", "value1"); + MultilingualGlossaryDictionaryEntries newGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, newEntries); + MultilingualGlossaryDictionaryInfo updatedGlossary = + deepLClient.replaceMultilingualGlossaryDictionary(glossary, newGlossaryDict); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), Arrays.asList(updatedGlossary)); + + newEntries.put("key2", "value2"); + newGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, newEntries); + updatedGlossary = + deepLClient.replaceMultilingualGlossaryDictionary( + glossary.getGlossaryId(), newGlossaryDict); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), Arrays.asList(updatedGlossary)); + + newEntries.put("key3", "value3"); + newGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, newEntries); + updatedGlossary = + deepLClient.replaceMultilingualGlossaryDictionary( + glossary.getGlossaryId(), sourceLang, targetLang, newEntries); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), Arrays.asList(updatedGlossary)); + + newEntries.put("key4", "value4"); + newGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, newEntries); + updatedGlossary = + deepLClient.replaceMultilingualGlossaryDictionary( + glossary, sourceLang, targetLang, newEntries); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), Arrays.asList(updatedGlossary)); + } + } + + @Test + void testGlossaryReplaceDictionaryReplacesExistingEntries() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + List glossaryDicts = Arrays.asList(testGlossaryDict); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + GlossaryEntries newEntries = new GlossaryEntries(); + newEntries.put("key1", "value1"); + newEntries.put("key2", "value2"); + MultilingualGlossaryDictionaryEntries newGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, newEntries); + deepLClient.replaceMultilingualGlossaryDictionary(glossary, newGlossaryDict); + + MultilingualGlossaryDictionaryEntries updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries(glossary, sourceLang, targetLang); + Assertions.assertEquals(newEntries, updatedGlossaryDict.getEntries()); + } + } + + @Test + void testGlossaryReplaceDictionaryFromCsvReplacesExistingEntries() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + List glossaryDicts = Arrays.asList(testGlossaryDict); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + GlossaryEntries newEntries = new GlossaryEntries(); + newEntries.put("key1", "value1"); + newEntries.put("key2", "value2"); + String csvContent = "key1,value1\nkey2,value2"; + deepLClient.replaceMultilingualGlossaryDictionaryFromCsv( + glossary.getGlossaryId(), sourceLang, targetLang, csvContent); + + MultilingualGlossaryDictionaryEntries updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries(glossary, sourceLang, targetLang); + Assertions.assertEquals(newEntries, updatedGlossaryDict.getEntries()); + } + } + + @Test + void testGlossaryUpdateDictionary() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + GlossaryEntries entries = new GlossaryEntries(Collections.singletonMap("key1", "value1")); + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries)); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + entries = new GlossaryEntries(Collections.singletonMap("key1", "value2")); + MultilingualGlossaryDictionaryEntries newGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries); + MultilingualGlossaryInfo updatedGlossary = + deepLClient.updateMultilingualGlossaryDictionary(glossary, newGlossaryDict); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), updatedGlossary.getDictionaries()); + + entries = new GlossaryEntries(Collections.singletonMap("key1", "value3")); + newGlossaryDict = new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries); + updatedGlossary = + deepLClient.updateMultilingualGlossaryDictionary( + glossary.getGlossaryId(), newGlossaryDict); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), updatedGlossary.getDictionaries()); + + entries = new GlossaryEntries(Collections.singletonMap("key1", "value4")); + newGlossaryDict = new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries); + updatedGlossary = + deepLClient.updateMultilingualGlossaryDictionary( + glossary.getGlossaryId(), sourceLang, targetLang, entries); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), updatedGlossary.getDictionaries()); + + entries.put("key1", "value5"); + newGlossaryDict = new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries); + updatedGlossary = + deepLClient.updateMultilingualGlossaryDictionary( + glossary, sourceLang, targetLang, entries); + AssertGlossaryDictionariesEquivalent( + Arrays.asList(newGlossaryDict), updatedGlossary.getDictionaries()); + } + } + + @Test + void testGlossaryUpdateDictionaryUpdatesExistingEntries() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + + GlossaryEntries entries = new GlossaryEntries(); + entries.put("key1", "value1"); + entries.put("key2", "value2"); + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries)); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + GlossaryEntries newEntries = new GlossaryEntries(); + newEntries.put("key1", "updatedValue1"); + newEntries.put("newKey", "newValue"); + MultilingualGlossaryDictionaryEntries newGlossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, newEntries); + deepLClient.updateMultilingualGlossaryDictionary(glossary, newGlossaryDict); + + /* We expect the entries to be the newly updated entries plus the old key2/value2 entry that was unchanged */ + GlossaryEntries expectedEntries = newEntries; + expectedEntries.put("key2", "value2"); + + MultilingualGlossaryDictionaryEntries updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries(glossary, sourceLang, targetLang); + Assertions.assertEquals(expectedEntries, updatedGlossaryDict.getEntries()); + } + } + + @Test + void testGlossaryUpdateDictionaryFromCsvUpdatesExistingEntries() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + + GlossaryEntries entries = new GlossaryEntries(); + entries.put("key1", "value1"); + entries.put("key2", "value2"); + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries)); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, glossaryDicts); + + GlossaryEntries csvEntries = new GlossaryEntries(); + csvEntries.put("key1", "updatedValue1"); + csvEntries.put("newKey", "newValue"); + String csvContent = "key1,updatedValue1\nnewKey,newValue"; + deepLClient.updateMultilingualGlossaryDictionaryFromCsv( + glossary.getGlossaryId(), sourceLang, targetLang, csvContent); + + /* We expect the entries to be the newly updated entries plus the old key2/value2 entry that was unchanged */ + GlossaryEntries expectedEntries = csvEntries; + expectedEntries.put("key2", "value2"); + + MultilingualGlossaryDictionaryEntries updatedGlossaryDict = + deepLClient.getMultilingualGlossaryDictionaryEntries(glossary, sourceLang, targetLang); + Assertions.assertEquals(expectedEntries, updatedGlossaryDict.getEntries()); + } + } + + @Test + void testGlossaryUpdateName() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String originalGlossaryName = "original glossary name"; + + GlossaryEntries entries = new GlossaryEntries(); + entries.put("key1", "value1"); + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries)); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(originalGlossaryName, glossaryDicts); + + String glossaryName = cleanup.getGlossaryName(); + MultilingualGlossaryInfo updatedGlossary = + deepLClient.updateMultilingualGlossaryName(glossary.getGlossaryId(), glossaryName); + + Assertions.assertEquals(glossaryName, updatedGlossary.getName()); + } + } + + @Test + void testGlossaryTranslateTextSentence() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + GlossaryEntries entries = + new GlossaryEntries() { + { + put("artist", "Maler"); + put("prize", "Gewinn"); + } + }; + String inputText = "The artist was awarded a prize."; + MultilingualGlossaryDictionaryEntries glossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, Arrays.asList(glossaryDict)); + + TextResult result = + deepLClient.translateText( + inputText, + sourceLang, + targetLang, + new TextTranslationOptions().setGlossary(glossary.getGlossaryId())); + if (!isMockServer) { + Assertions.assertTrue(result.getText().contains("Maler")); + Assertions.assertTrue(result.getText().contains("Gewinn")); + } + + // It is also possible to specify GlossaryInfo + result = + deepLClient.translateText( + inputText, + sourceLang, + targetLang, + new TextTranslationOptions().setGlossary(glossary)); + if (!isMockServer) { + Assertions.assertTrue(result.getText().contains("Maler")); + Assertions.assertTrue(result.getText().contains("Gewinn")); + } + } + } + + @Test + void testGlossaryTranslateTextBasic() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + List textsEn = + new ArrayList() { + { + add("Apple"); + add("Banana"); + } + }; + List textsDe = + new ArrayList() { + { + add("Apfel"); + add("Banane"); + } + }; + GlossaryEntries glossaryEntriesEnDe = new GlossaryEntries(); + GlossaryEntries glossaryEntriesDeEn = new GlossaryEntries(); + for (int i = 0; i < textsEn.size(); i++) { + glossaryEntriesEnDe.put(textsEn.get(i), textsDe.get(i)); + glossaryEntriesDeEn.put(textsDe.get(i), textsEn.get(i)); + } + + MultilingualGlossaryDictionaryEntries glossaryDictEnDe = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, glossaryEntriesEnDe); + MultilingualGlossaryDictionaryEntries glossaryDictDeEn = + new MultilingualGlossaryDictionaryEntries(targetLang, sourceLang, glossaryEntriesDeEn); + + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary( + glossaryName, Arrays.asList(glossaryDictEnDe, glossaryDictDeEn)); + + List result = + deepLClient.translateText( + textsEn, "en", "de", new TextTranslationOptions().setGlossary(glossary)); + Assertions.assertArrayEquals( + textsDe.toArray(), result.stream().map(TextResult::getText).toArray()); + + result = + deepLClient.translateText( + textsDe, + "de", + "en-US", + new TextTranslationOptions().setGlossary(glossary.getGlossaryId())); + Assertions.assertArrayEquals( + textsEn.toArray(), result.stream().map(TextResult::getText).toArray()); + } + } + + @Test + void testGlossaryTranslateDocument() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + File inputFile = createInputFile("artist\nprize"); + File outputFile = createOutputFile(); + String expectedOutput = "Maler\nGewinn"; + GlossaryEntries entries = + new GlossaryEntries() { + { + put("artist", "Maler"); + put("prize", "Gewinn"); + } + }; + MultilingualGlossaryDictionaryEntries glossaryDict = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(glossaryName, Arrays.asList(glossaryDict)); + + deepLClient.translateDocument( + inputFile, + outputFile, + sourceLang, + targetLang, + new DocumentTranslationOptions().setGlossary(glossary)); + Assertions.assertEquals(expectedOutput, readFromFile(outputFile)); + boolean ignored = outputFile.delete(); + + deepLClient.translateDocument( + inputFile, + outputFile, + sourceLang, + targetLang, + new DocumentTranslationOptions().setGlossary(glossary.getGlossaryId())); + Assertions.assertEquals(expectedOutput, readFromFile(outputFile)); + } + } + + @Test + void testGlossaryTranslateTextInvalid() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + String glossaryName = cleanup.getGlossaryName(); + + MultilingualGlossaryDictionaryEntries glossaryDictEnDe = + new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, testEntries); + MultilingualGlossaryDictionaryEntries glossaryDictDeEn = + new MultilingualGlossaryDictionaryEntries(targetLang, sourceLang, testEntries); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary( + glossaryName, Arrays.asList(glossaryDictEnDe, glossaryDictDeEn)); + + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + deepLClient.translateText( + "test", null, "de", new TextTranslationOptions().setGlossary(glossary))); + Assertions.assertTrue(exception.getMessage().contains("sourceLang is required")); + + exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + deepLClient.translateText( + "test", "de", "en", new TextTranslationOptions().setGlossary(glossary))); + Assertions.assertTrue(exception.getMessage().contains("targetLang=\"en\" is not allowed")); + } + } + + /** + * Utility function for determining if a list of MultilingualGlossaryDictionaryEntries objects + * (that have entries) matches a list of MultilingualGlossaryDictionaryInfo (that do not contain + * entries, but just a count of the number of entries for that glossary dictionary + */ + private void AssertGlossaryDictionariesEquivalent( + List expectedDicts, + List actualDicts) { + Assertions.assertEquals(expectedDicts.size(), actualDicts.size()); + for (MultilingualGlossaryDictionaryEntries expectedDict : expectedDicts) { + MultilingualGlossaryDictionaryInfo actualDict = + findMatchingDictionary( + actualDicts, + expectedDict.getSourceLanguageCode(), + expectedDict.getTargetLanguageCode()); + + Assertions.assertEquals( + expectedDict.getSourceLanguageCode().toLowerCase(), + actualDict.getSourceLanguageCode().toLowerCase()); + Assertions.assertEquals( + expectedDict.getTargetLanguageCode().toLowerCase(), + actualDict.getTargetLanguageCode().toLowerCase()); + Assertions.assertEquals( + expectedDict.getEntries().entrySet().size(), actualDict.getEntryCount()); + } + } + + private MultilingualGlossaryDictionaryInfo findMatchingDictionary( + List glossaryDicts, + String sourceLang, + String targetLang) { + String lowerCaseSourceLang = sourceLang.toLowerCase(); + String lowerCaseTargetLang = targetLang.toLowerCase(); + for (MultilingualGlossaryDictionaryInfo glossaryDict : glossaryDicts) { + if (glossaryDict.getSourceLanguageCode().equals(lowerCaseSourceLang) + && glossaryDict.getTargetLanguageCode().equals(lowerCaseTargetLang)) { + return glossaryDict; + } + } + Assertions.fail("glossary did not contain expected language pair $sourceLang->$targetLang"); + return null; + } +} diff --git a/upgrading_to_multilingual_glossaries.md b/upgrading_to_multilingual_glossaries.md new file mode 100644 index 0000000..6a5acb6 --- /dev/null +++ b/upgrading_to_multilingual_glossaries.md @@ -0,0 +1,412 @@ +# Migration Documentation for Newest Glossary Functionality + +## 1. Overview of Changes + +The newest version of the Glossary APIs is the `/v3` endpoints, which introduce enhanced functionality: + +- **Support for Multilingual Glossaries**: The v3 endpoints allow for the creation of glossaries with multiple language + pairs, enhancing flexibility and usability. +- **Editing Capabilities**: Users can now edit existing glossaries. + +To support these new v3 APIs, we have created new methods to interact with these new multilingual glossaries. Users are +encouraged to transition to the new to take full advantage of these new features. However, for those who prefer to +continue using the existing functionality, the `v2` methods for monolingual glossaries (e.g., `createGlossary()`, +`getGlossary()`, etc.) remain available. + +## 2. Endpoint Changes + +| Monolingual glossary methods | Multilingual glossary methods | Changes Summary | +|--------------------------------|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `createGlossary()` | `createMultilingualGlossary()` | Accepts a list of `MultilingualGlossaryDictionaryEntries` for multi-lingual support and now returns a `MultilingualGlossaryInfo` object. | +| `createGlossaryFromCsv()` | `createMultilingualGlossaryFromCsv()` | Similar functionality, but now returns a `MultilingualGlossaryInfo` object | +| `getGlossary()` | `getMultilingualGlossary()` | Similar functionality, but now returns `MultilingualGlossaryInfo`. Also can accept a `MultilingualGlossaryInfo` object as the glossary parameter instead of a `GlossaryInfo` object. | +| `listGlossaries()` | `listMultilingualGlossaries()` | Similar functionality, but now returns a list of `MultilingualGlossaryInfo` objects. | +| `getGlossaryEntries()` | `getMultilingualGlossaryDictionaryEntries()` | Requires specifying source and target languages. Also returns a `MultilingualGlossaryDictionaryEntriesResponse` object as the response. | +| `deleteGlossary()` | `deleteMultilingualGlossary()` | Similar functionality, but now can accept a `MultilingualGlossaryInfo` object instead of a `GlossaryInfo` object when specifying the glossary. | + +## 3. Model Changes + +V2 glossaries are monolingual and the previous glossary objects could only have entries for one language pair ( +`SourceLanguageCode` and `TargetLanguageCode`). Now we introduce the concept of "glossary dictionaries", where a +glossary dictionary specifies its own `SourceLanguageCode`, `TargetLanguageCode`, and has its own entries. + +- **Glossary Information**: + - **v2**: `GlossaryInfo` supports only mono-lingual glossaries, containing fields such as `SourceLanguageCode`, + `TargetLanguageCode`, and `EntryCount`. + - **v3**: `MultilingualGlossaryInfo` supports multi-lingual glossaries and includes a list of + `MultilingualGlossaryDictionaryInfo`, which provides details about each glossary dictionary, each with its own + `SourceLanguageCode`, `TargetLanguageCode`, and `EntryCount`. + +- **Glossary Entries**: + - **v3**: Introduces `MultilingualGlossaryDictionaryEntries`, which encapsulates a glossary dictionary with source and + target languages along with its entries. + +## 4. Code Examples + +### Create a glossary + +```java +class Example { + // monolingual glossary example + public void createGlossaryExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("hello", "hallo"); + }}; + GlossaryInfo glossaryInfo = client.createGlossary("My Glossary", "EN", "DE", entries); + } + + // multilingual glossary example + public void createMultilingualGlossaryExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("hello", "hallo"); + }}; + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entries)); + MultilingualGlossaryInfo glossaryInfo = client.createMultilingualGlossary("My Glossary", glossaryDicts); + } +} +``` + +### Get a glossary + +```java +class Example { + // monolingual glossary example + public void getGlossaryExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("hello", "hallo"); + }}; + GlossaryInfo createdGlossary = client.createGlossary("My Glossary", "EN", "DE", entries); + GlossaryInfo glossaryInfo = client.getGlossary(createdGlossary); // GlossaryInfo object + } + + // multilingual glossary example + public void getMultilingualGlossaryExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("hello", "hallo"); + }}; + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entries)); + MultilingualGlossaryInfo createdGlossary = client.createMultilingualGlossary("My Glossary", glossaryDicts); + GlossaryInfo glossaryInfo = client.getGlossary(createdGlossary); // MultilingualGlossaryInfo object + } +} +``` + +### Get glossary entries + +```java +class Example { + // monolingual glossary example + public void getGlossaryEntriesExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("hello", "hallo"); + }}; + GlossaryInfo createdGlossary = client.createGlossary("My Glossary", "EN", "DE", entries); + GlossaryEntries entries = client.getGlossaryEntries(createdGlossary); + System.out.println(entries.toTsv()); // 'hello\thallo' + } + + // mutlilingual glossary example + public void getMultilingualGlossaryEntriesExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("hello", "hallo"); + }}; + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entries)); + MultilingualGlossaryInfo createdGlossary = client.createMultilingualGlossary("My Glossary", glossaryDicts); + MultilingualGlossaryInfo dictEntries = client.getMultilingualGlossaryDictionaryEntries(createdGlossary, "EN", "DE"); + System.out.println(dictEntries.getDictionaries()[0].getEntries().ToTsv()); // 'hello\thallo' + } +} +``` + +### List and delete glossaries + +```java +class Example { + // monolingual glossary example + public void getListDeleteGlossaryExamples() throws Exception { + List glossaries = client.listGlossaries(); + for (GlossaryInfo glossary : glossaries) { + if (glossary.getName() == "Old glossary") { + client.deleteGlossary(glossary); + } + } + } + + // multilingual glossary example + public void getListDeleteMultilingualGlossaryExamples() throws Exception { + List glossaries = client.listMultilingualGlossaries(); + for (MultilingualGlossaryInfo glossary : glossaries) { + if (glossary.getName() == "Old glossary") { + client.deleteMultilingualGlossary(glossary); + } + } + } +} +``` + +## 5. New Multilingual Glossary Methods + +In addition to introducing multilingual glossaries, we introduce several new methods that enhance the functionality for +managing glossaries. Below are the details for each new method: + +### Update Multilingual Glossary Dictionary + +- **Method Overloads**: + - `MultilingualGlossaryInfo updateMultilingualGlossaryDictionary(String glossaryId, String + sourceLanguageCode, String targetLanguageCode, GlossaryEntries entries)` + - `MultilingualGlossaryInfo updateMultilingualGlossaryDictionary(MultilingualGlossaryInfo glossary, String + sourceLanguageCode, String targetLanguageCode, GlossaryEntries entries)` + - `MultilingualGlossaryInfo updateMultilingualGlossaryDictionary(String glossaryId, + MultilingualGlossaryDictionaryEntries glossaryDict)` + - `MultilingualGlossaryInfo updateMultilingualGlossaryDictionary(MultilingualGlossaryInfo glossary, + MultilingualGlossaryDictionaryEntries glossaryDict)` +- **Description**: Updates a glossary dictionary with new entries. +- **Parameters:** + - `String glossaryId`: ID of the glossary to update. + - `String sourceLanguageCode`: Source language code for the glossary dictionary. + - `String targetLanguageCode`: Target language code for the glossary dictionary. + - `GlossaryEntries entries`: The source-target entry pairs. + - `MultilingualGlossaryDictionaryEntries glossaryDict`: The glossary dictionary to update. +- **Returns**: `MultilingualGlossaryInfo` containing details about the updated glossary. +- **Exceptions**: + - `IllegalArgumentException`: Thrown if any argument is invalid. + - `DeepLException`: Thrown if any error occurs while communicating with the DeepL API. + - `InterruptedException`: If the thread is interrupted during execution of this function. +- **Example**: + +```java +class Example { + public void updateGlossaryEntriesExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("artist", "Maler"); + put("hello", "guten tag"); + }}; + List dictionaries = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entries)); + MultilingualGlossaryInfo myGlossary = client.createMultilingualGlossary( + "My glossary", + dictionaries + ); + + GlossaryEntries newEntries = new GlossaryEntries() {{ + put("hello", "hallo"); + put("prize", "Gewinn"); + }}; + + MultilingualGlossaryDictionaryEntries glossaryDict = + new MultilingualGlossaryDictionaryEntries("EN", "DE", newEntries); + + MultilingualGlossaryInfo updatedGlossary = + client.updateMultilingualGlossaryDictionary(myGlossary, glossaryDict); + + MultilingualGlossaryInfo entriesResponse = + client.getMultilingualGlossaryDictionaryEntries(myGlossary, "EN", "DE"); + + for (Map.Entry entry : entriesResponse.getDictionaries()[0].getEntries().entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + + // prints: + // artist:Maler + // hello:hallo + // prize:Gewinn + } +} +``` + +### Update Multilingual Glossary Dictionary from CSV + +- **Method**: + - `MultilingualGlossaryInfo updateMultilingualGlossaryDictionaryFromCsv(String glossaryId, String + sourceLanguageCode, String targetLanguageCode, File csvFile)` + - `MultilingualGlossaryInfo updateMultilingualGlossaryDictionaryFromCsv(MultilingualGlossaryInfo glossary, + String sourceLanguageCode, String targetLanguageCode, File csvFile)` +- **Description**: This method allows you to update or create a glossary dictionary using entries in CSV format. +- **Parameters**: + - `String glossaryId`: The ID of the glossary to update. + - `MultilingualGlossaryInfo glossary`: The `MultilingualGlossaryInfo` object representing the glossary to update + - `String sourceLanguageCode`: Language of source entries. + - `String targetLanguageCode`: Language of target entries. + - `File csvFile`: The CSV data containing glossary entries as a stream. +- **Returns**: `MultilingualGlossaryInfo` containing information about the updated glossary. +- **Exceptions**: + - `IllegalArgumentException`: Thrown if any argument is invalid. + - `DeepLException`: Thrown if any error occurs while communicating with the DeepL API. + - `InterruptedException`: If the thread is interrupted during execution of this function. + - `IOException`: If an I/O error occurs. +- **Example**: + ```java + class Example { + public void updateGlossaryEntriesFromCsvExample() throws Exception { + File csvFile = new File("/path/to/glossary_file.csv"); + String glossaryId = "559192ed-8e23-..."; + MultilingualGlossaryInfo myGlossary = + client.updateMultilingualGlossaryDictionaryFromCsv(glossaryId, "en", "de", csvFile); + } + } + ``` + +### Update Multilingual Glossary Name + +- **Method**: + `MultilingualGlossaryInfo updateMultilingualGlossaryName(String glossaryId, String name)` +- **Description**: This method allows you to update the name of an existing glossary. +- **Parameters**: + - `String glossary`: The ID of the glossary to update. + - `String name`: The new name for the glossary. +- **Returns**: `MultilingualGlossaryInfo` containing information about the updated glossary. +- **Exceptions**: + - `IllegalArgumentException`: Thrown if any argument is invalid. + - `DeepLException`: Thrown if any error occurs while communicating with the DeepL API. +- **Example**: + ```java + class Example { + public void updateGlossaryNameExample() throws Exception { + String glossaryId = "559192ed-8e23-..."; + MultilingualGlossaryInfo myGlossary = client.updateMultilingualGlossaryName(glossaryId, "My new glossary name"); + System.out.println(myGlossary.getName()); // 'My new glossary name' + } + } + ``` + +### Replace a Multilingual Glossary Dictionary + +- **Method**: + - `MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary(String glossaryId, String + sourceLanguageCode, String targetLanguageCode, GlossaryEntries entries)` + - `MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary(MultilingualGlossaryInfo + glossary, String sourceLanguageCode, String targetLanguageCode, GlossaryEntries entries)` + - `MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary(String glossaryId, + MultilingualGlossaryDictionaryEntries glossaryDict)` + - `MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionary(MultilingualGlossaryInfo + glossary, MultilingualGlossaryDictionaryEntries glossaryDict)` +- **Description**: This method replaces the existing glossary dictionary with a new set of entries. +- **Parameters**: + - `String glossaryId`: ID of the glossary whose dictionary will be replaced. + - `String sourceLanguageCode`: Source language code for the glossary dictionary. + - `String targetLanguageCode`: Target language code for the glossary dictionary. + - `GlossaryEntries entries`: The source-target entries that will replace any existing ones for that language pair. + - `MultilingualGlossaryDictionaryEntries glossaryDict`: The glossary dictionary to update. +- **Returns**: `MultilingualGlossaryDictionaryInfo` containing information about the replaced glossary dictionary. +- **Exceptions**: + - `IllegalArgumentException`: Thrown if any argument is invalid. + - `DeepLException`: Thrown if any error occurs while communicating with the DeepL API. + - `InterruptedException`: If the thread is interrupted during execution of this function. +- **Note**: Ensure that the new dictionary entries are complete and valid, as this method will completely overwrite the + existing entries. It will also create a new glossary dictionary if one did not exist for the given language pair. +- **Example**: + ```java + class Example { + public void replaceGlossaryEntriesExample() throws Exception { + GlossaryEntries entries = new GlossaryEntries() {{ + put("artist", "Maler"); + put("hello", "guten tag"); + }}; + List dictionaries = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entries)); + MultilingualGlossaryInfo myGlossary = client.createMultilingualGlossary( + "My glossary", + dictionaries + ); + + GlossaryEntries newEntries = new GlossaryEntries() {{put("goodbye", "Auf Weidersehen");}}; + MultilingualGlossaryDictionaryEntries glossaryDict = + new MultilingualGlossaryDictionaryEntries("EN", "DE", newEntries); + + MultilingualGlossaryInfo updatedGlossary = + client.replaceMultilingualGlossaryDictionary(myGlossary, glossaryDict); + + MultilingualGlossaryInfo entriesResponse = + client.getMultilingualGlossaryDictionaryEntries(myGlossary, "EN", "DE"); + + for (Map.Entry entry : glossaryDicts.getDictionaries()[0].getEntries().entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + // prints: + // goodbye:Auf Weidersehen + } + } + ``` + +### Replace Multilingual Glossary Dictionary from CSV + +- **Method**: + - `MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionaryFromCsv(String glossaryId, String + sourceLanguageCode, String targetLanguageCode, Stream csvFile)` + - `MultilingualGlossaryDictionaryInfo replaceMultilingualGlossaryDictionaryFromCsv(MultilingualGlossaryInfo + glossary, String sourceLanguageCode, String targetLanguageCode, Stream csvFile)` +- **Description**: This method allows you to replace or create a glossary dictionary using entries in CSV format. +- **Parameters**: + - `String glossaryId`: The ID of the glossary whose dictionary will be replaced. + - `MultilingualGlossaryInfo glossary`: The `MultilingualGlossaryInfo` object representing the glossary whose + dictionary will be replaced. + - `String sourceLanguageCode`: Language of source entries. + - `String targetLanguageCode`: Language of target entries. + - `Stream csvFile`: The CSV data containing glossary entries as a stream. +- **Returns**: `MultilingualGlossaryDictionaryInfo` containing information about the replaced glossary dictionary. +- **Exceptions**: + - `IllegalArgumentException`: Thrown if any argument is invalid. + - `DeepLException`: Thrown if any error occurs while communicating with the DeepL API. + - `InterruptedException`: If the thread is interrupted during execution of this function. + - `IOException`: If an I/O error occurs. +- **Example**: + ```java + class Example { + public void replaceGlossaryEntriesFromCsvExample() throws Exception { + File csvFile = new File("/path/to/glossary_file.csv"); + String glossaryId = "559192ed-8e23-..."; + MultilingualGlossaryInfo myGlossary = + client.replaceMultilingualGlossaryDictionaryFromCsv(glossaryId, "en", "de", csvFile); + MultilingualGlossaryInfo entriesResponse = + client.getMultilingualGlossaryDictionaryEntries(myGlossary, "EN", "DE"); + } + } + ``` + +### Delete a Multilingual Glossary Dictionary + +- **Method**: + - `Task deleteMultilingualGlossaryDictionary(MultilingualGlossaryInfo glossary, String sourceLanguageCode, String + targetLanguageCode)` + - `Task deleteMultilingualGlossaryDictionary(String glossaryId, String sourceLanguageCode, String + targetLanguageCode)` +- **Description**: This method deletes a specified glossary dictionary from a given glossary. +- **Parameters**: + - `String glossaryId`: The ID of the glossary containing the dictionary to delete. + - `MultilingualGlossaryInfo glossary`: The `MultilingualGlossaryInfo` object of the glossary containing the dictionary + to delete. + - `MultilingualGlossaryDictionaryInfo dictionary`: The `MultilingualGlossaryDictionaryInfo` object that specifies the + - dictionary to delete. + - `String sourceLanguageCode`: The source language of the glossary dictionary. + - `String targetLanguageCode`: The target language of the glossary dictionary. +- **Returns**: A Task + +- **Migration Note**: Ensure that your application logic correctly identifies the dictionary to delete. If using + `sourceLanguageCode` and `targetLanguageCode`, both must be provided to specify the dictionary. + +- **Example**: + ```java + class Example { + public void deleteGlossaryDictionaryExample() throws Exception { + GlossaryEntries entriesEnde = new GlossaryEntries() {{put("hello", "hallo");}}; + GlossaryEntries entriesDeen = new GlossaryEntries() {{put("hallo", "hello");}}; + MultilingualGlossaryDictionaryEntries glossaryDictEnde = + new MultilingualGlossaryDictionaryEntries("EN", "DE", entriesEnde); + MultilingualGlossaryDictionaryEntries glossaryDictDeen = + new MultilingualGlossaryDictionaryEntries("EN", "DE", entriesDeen); + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries("EN", "DE", entriesEnde), + new MultilingualGlossaryDictionaryEntries("DE", "EN", entriesDeen)); + MultilingualGlossaryInfo glossaryInfo = client.createMultilingualGlossary("My Glossary", glossaryDicts); + + // Delete via specifying the glossary dictionary + client.deleteMultilingualGlossaryDictionary(glossaryInfo, glossaryDictEnde); + + // Delete via specifying the language pair + client.deleteMultilingualGlossaryDictionary(glossaryInfo, "DE", "EN"); + } + } + ``` From 914b84525edae2a3b08914e127c3db8604dd2ec8 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 30 Apr 2025 12:25:13 +0100 Subject: [PATCH 078/121] docs: Increase version to 1.10.0 --- CHANGELOG.md | 10 +++++++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc68cd6..d7191e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + +### Changed + + + +## [1.10.0] - 2025-04-30 +### Added * Added support for the /v3 Multilingual Glossary APIs in the client library while providing backwards compatability for the previous /v2 Glossary endpoints. Please refer to the README or @@ -159,7 +166,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.9.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...HEAD +[1.10.0]: https://github.com/DeepLcom/deepl-java/compare/v1.9.0...v1.10.0 [1.9.0]: https://github.com/DeepLcom/deepl-java/compare/v1.8.1...v1.9.0 [1.8.1]: https://github.com/DeepLcom/deepl-java/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/DeepLcom/deepl-java/compare/v1.7.0...v1.8.0 diff --git a/README.md b/README.md index 77b27ac..a2de1ec 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.9.0" +implementation "com.deepl.api:deepl-java:1.10.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.9.0 + 1.10.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 5a20656..e27c463 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.9.0" +version = "1.10.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 28aac00..ddd8386 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -88,7 +88,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.9.0"); + sb.append("deepl-java/1.10.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 383591439f65896cadd46ab8f4472dfe0bb2d182 Mon Sep 17 00:00:00 2001 From: Ben Morss Date: Thu, 29 May 2025 21:05:06 -0400 Subject: [PATCH 079/121] Corrected migration guide link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2de1ec..4cf0f49 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ endpoints, please continue to use the existing endpoints in the `translator.java To migrate to use the new multilingual glossary methods from the current monolingual glossary methods, please refer to -[this migration guide](upgrade_to_multilingual_glossaries.md). +[this migration guide](upgrading_to_multilingual_glossaries.md). The following sections describe how to interact with multilingual glossaries using the new functionality: From 852fabec7947847d4b7d277daec15cc916defb02 Mon Sep 17 00:00:00 2001 From: Mateusz Grabowski Date: Wed, 18 Jun 2025 10:19:18 +0200 Subject: [PATCH 080/121] fix: issue#68 delete method not public - this method should be public to allow deleting multilingual glossaries by only using their glossaryId. --- deepl-java/src/main/java/com/deepl/api/DeepLClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index a7eeacf..5d66d49 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -641,7 +641,7 @@ public MultilingualGlossaryInfo updateMultilingualGlossaryDictionaryFromCsv( * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link * DeepLException} or a derived class will be thrown. */ - void deleteMultilingualGlossary(String glossaryId) throws DeepLException, InterruptedException { + public void deleteMultilingualGlossary(String glossaryId) throws DeepLException, InterruptedException { String relativeUrl = String.format("/v3/glossaries/%s", glossaryId); HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); this.checkResponse(response, false, true); From 8603857672a6125a30b2fd8b495f9a627a4e2ad9 Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 18 Jun 2025 12:49:22 +0100 Subject: [PATCH 081/121] fix: Formatting + use v3 glossary delete in tests --- deepl-java/src/main/java/com/deepl/api/DeepLClient.java | 3 ++- .../com/deepl/api/MultilingualGlossaryCleanupUtility.java | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 5d66d49..f524c4d 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -641,7 +641,8 @@ public MultilingualGlossaryInfo updateMultilingualGlossaryDictionaryFromCsv( * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link * DeepLException} or a derived class will be thrown. */ - public void deleteMultilingualGlossary(String glossaryId) throws DeepLException, InterruptedException { + public void deleteMultilingualGlossary(String glossaryId) + throws DeepLException, InterruptedException { String relativeUrl = String.format("/v3/glossaries/%s", glossaryId); HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); this.checkResponse(response, false, true); diff --git a/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java index 13a2712..a2825f4 100644 --- a/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java +++ b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java @@ -34,10 +34,14 @@ public void close() throws Exception { for (MultilingualGlossaryInfo glossary : glossaries) { if (Objects.equals(glossary.getName(), glossaryName)) { try { - // TODO replace with v3 delete glossary - deepLClient.deleteGlossary(glossary.getGlossaryId()); + deepLClient.deleteMultilingualGlossary(glossary.getGlossaryId()); } catch (Exception exception) { // Ignore + System.out.println( + "Failed to delete glossary: " + + glossaryName + + "\nException: " + + exception.getMessage()); } } } From dd3b6dc4aae0a71c4a95e95167ce4514a15a047e Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 18 Jun 2025 12:50:36 +0100 Subject: [PATCH 082/121] docs: Increase version to 1.10.1 --- CHANGELOG.md | 9 ++++++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7191e5..4e6acbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [1.10.1] - 2025-06-18 +### Fixed +* Fixed `DeepLClient::deleteMultilingualGlossary(String glossaryId)` being package private, made it public instead. + * Thanks to [MTSxoff](https://github.com/MTSxoff) for the report in [#68](https://github.com/DeepLcom/deepl-java/issues/68) and the fix in [#69](https://github.com/DeepLcom/deepl-java/pull/69). + + ## [1.10.0] - 2025-04-30 ### Added * Added support for the /v3 Multilingual Glossary APIs in the client library @@ -166,7 +172,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.1...HEAD +[1.10.1]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.1 [1.10.0]: https://github.com/DeepLcom/deepl-java/compare/v1.9.0...v1.10.0 [1.9.0]: https://github.com/DeepLcom/deepl-java/compare/v1.8.1...v1.9.0 [1.8.1]: https://github.com/DeepLcom/deepl-java/compare/v1.8.0...v1.8.1 diff --git a/README.md b/README.md index 4cf0f49..7e42a5f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.10.0" +implementation "com.deepl.api:deepl-java:1.10.1" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.10.0 + 1.10.1 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index e27c463..cfc7044 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.10.0" +version = "1.10.1" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index ddd8386..b259c55 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -88,7 +88,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.10.0"); + sb.append("deepl-java/1.10.1"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 967456c9669d5745e181760657074977f41a79e3 Mon Sep 17 00:00:00 2001 From: Christina Weyher Date: Tue, 24 Jun 2025 17:42:17 +0200 Subject: [PATCH 083/121] docs: [DEX-2470] Add catalog file for deepl java library --- catalog-info.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 catalog-info.yaml diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..a983c80 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: deepl-java + title: DeepL Java Library + description: Public Java library for interacting with the DeepL API +spec: + type: library + lifecycle: production + owner: api-core-team + system: oss-client-libraries \ No newline at end of file From 938f2943f88def04bc9a026dfbe5bb2d68294db7 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 27 Jun 2025 11:52:29 +0200 Subject: [PATCH 084/121] chore: migrate to Sonatype Portal OSSRH Staging API according to https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/ --- deepl-java/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index cfc7044..dc617bd 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -67,8 +67,8 @@ publishing { val mavenUploadUsername: String? by project val mavenUploadPassword: String? by project name = "MavenCentral" - val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" - val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + val releasesRepoUrl = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" + val snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) credentials { username = mavenUploadUsername From dc87c5e0ab200f57d22ed6735de46a97e65cd9e2 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 27 Jun 2025 11:55:13 +0200 Subject: [PATCH 085/121] docs: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6acbc..a89bbfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed - +* Migrate to Sonatype Portal OSSRH Staging API due to legacy OSSRH being sunsetted. ## [1.10.1] - 2025-06-18 From 644ff12c3d631ae8f67f580815f1e459bc6c7811 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 27 Jun 2025 14:19:29 +0200 Subject: [PATCH 086/121] docs: add missing license header to DeepLApiVersion.java --- deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java b/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java index 5725fbb..19a5768 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java @@ -1,3 +1,6 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. package com.deepl.api; public enum DeepLApiVersion { From 466cfbe05a5c97b55c0f4cf6a09a79a3363abf17 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 11 Jul 2025 11:59:37 +0200 Subject: [PATCH 087/121] docs: Increase version to 1.10.2 --- CHANGELOG.md | 10 +++++++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a89bbfc..a07fe4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed + + + +## [1.10.2] - 2025-07-11 +### Changed * Migrate to Sonatype Portal OSSRH Staging API due to legacy OSSRH being sunsetted. +* Whitespace surrounding auth key is now stripped. + * Thanks to [timazhum](https://github.com/timazhum) for the fix in [#64](https://github.com/DeepLcom/deepl-java/pull/69). ## [1.10.1] - 2025-06-18 @@ -172,7 +179,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.1...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.2...HEAD +[1.10.2]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.2 [1.10.1]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.1 [1.10.0]: https://github.com/DeepLcom/deepl-java/compare/v1.9.0...v1.10.0 [1.9.0]: https://github.com/DeepLcom/deepl-java/compare/v1.8.1...v1.9.0 diff --git a/README.md b/README.md index 7e42a5f..23a5f7a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.10.1" +implementation "com.deepl.api:deepl-java:1.10.2" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.10.1 + 1.10.2 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index dc617bd..a993f17 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.10.1" +version = "1.10.2" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 97f6a1c..f17a27d 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.10.1"); + sb.append("deepl-java/1.10.2"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 147a79288ce3958e3a3b1497a4c522c65e6e6190 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 11 Jul 2025 15:23:49 +0200 Subject: [PATCH 088/121] ci: add step to trigger deployment --- .gitlab-ci.yml | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 139b3a2..e2386c9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -204,28 +204,52 @@ test_examples_manual: # stage: publish ------------------------- -publish: +.publish_base: stage: publish extends: .publish dependencies: - build_scheduled - build_manual - rules: - - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' script: - ./gradlew publish +publish: + extends: .publish_base + rules: + - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' + publish_manual: - stage: publish - extends: .publish + extends: .publish_base when: manual + +## trigger deployment + +.trigger_deployment_base: + extends: .publish + stage: publish + image: curlimages/curl:8.14.1 + script: + - NAMESPACE="com.deepl.api" + - URL="https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/$NAMESPACE" + - PARAMS="publishing_type=automatic" + - BEARER_TOKEN=$(echo "${ORG_GRADLE_PROJECT_mavenUploadUsername}:${ORG_GRADLE_PROJECT_mavenUploadPassword}" | base64) + - | + curl "$URL?$PARAMS" --header "Authorization: Bearer $BEARER_TOKEN" -d "" --write-out "%{http_code}\n" + +trigger_deployment: + extends: .trigger_deployment_base dependencies: - - build_scheduled - - build_manual + - publish rules: - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' - script: - - ./gradlew publish + +trigger_deployment_manual: + extends: .trigger_deployment_base + dependencies: + - publish_manual + when: manual + +## gitlab release gitlab release: stage: publish From eb80e32d60cc9361f8a120b9c0df3a5c4eafd56b Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 22 Aug 2025 15:59:51 +0200 Subject: [PATCH 089/121] security: update org.apache.httpcomponents:httpclient to 4.5.14 due to CVE-2020-13956 --- deepl-java/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index a993f17..404e426 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -27,7 +27,7 @@ dependencies { implementation("org.jetbrains:annotations:20.1.0") testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("org.mockito:mockito-inline:4.11.0") - implementation("org.apache.httpcomponents:httpclient:4.5.2") { because("java.net.HttpURLConnection does not support PATCH") } + implementation("org.apache.httpcomponents:httpclient:4.5.14") { because("java.net.HttpURLConnection does not support PATCH") } // implementation("com.google.guava:guava:30.1.1-jre") implementation("com.google.code.gson:gson:2.10.1") From 1423ca7ca86c63421d0337e60b09f7c0d06e9246 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 22 Aug 2025 16:04:24 +0200 Subject: [PATCH 090/121] docs: update changelog --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a07fe4b..ff54e30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Added - -### Changed - +### Security +* Updated `org.apache.httpcomponents:httpclient` to 4.5.14 due to [CVE-2020-13956](https://nvd.nist.gov/vuln/detail/CVE-2020-13956). + * Thanks to [warm-tune](https://github.com/warm-tune) for reporting in [#72](https://github.com/DeepLcom/deepl-java/issues/72). ## [1.10.2] - 2025-07-11 From c84587e74c6bd57e2814b87439d9c43af0ec7f1b Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 22 Aug 2025 16:06:45 +0200 Subject: [PATCH 091/121] docs: Increase version to 1.10.3 --- CHANGELOG.md | 10 +++++++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff54e30..b67e286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +### Changed + + + +## [1.10.3] - 2025-08-22 ### Security * Updated `org.apache.httpcomponents:httpclient` to 4.5.14 due to [CVE-2020-13956](https://nvd.nist.gov/vuln/detail/CVE-2020-13956). * Thanks to [warm-tune](https://github.com/warm-tune) for reporting in [#72](https://github.com/DeepLcom/deepl-java/issues/72). @@ -178,7 +185,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.2...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.3...HEAD +[1.10.3]: https://github.com/DeepLcom/deepl-java/compare/v1.10.2...v1.10.3 [1.10.2]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.2 [1.10.1]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.1 [1.10.0]: https://github.com/DeepLcom/deepl-java/compare/v1.9.0...v1.10.0 diff --git a/README.md b/README.md index 23a5f7a..32efc4c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.10.2" +implementation "com.deepl.api:deepl-java:1.10.3" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.10.2 + 1.10.3 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 404e426..1273570 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.10.2" +version = "1.10.3" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index f17a27d..fd704b1 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.10.2"); + sb.append("deepl-java/1.10.3"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From e3fd5b8c01dc4f463286795f79b8ded2073d7a42 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 30 Oct 2025 13:06:17 +0100 Subject: [PATCH 092/121] fix: change formality tests to look for text containing "dir" or "Ihnen" for German formality, rather than depending on exact translation --- .../test/java/com/deepl/api/TranslateDocumentTest.java | 4 ++-- .../src/test/java/com/deepl/api/TranslateTextTest.java | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java index 54e789c..fd674d6 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java @@ -94,7 +94,7 @@ void testTranslateDocumentFormality() throws Exception { "de", new DocumentTranslationOptions().setFormality(Formality.More)); if (!isMockServer) { - Assertions.assertEquals("Wie geht es Ihnen?", readFromFile(outputFile)); + Assertions.assertTrue(readFromFile(outputFile).contains("Ihnen")); } outputFile.delete(); @@ -106,7 +106,7 @@ void testTranslateDocumentFormality() throws Exception { "de", new DocumentTranslationOptions().setFormality(Formality.Less)); if (!isMockServer) { - Assertions.assertEquals("Wie geht es dir?", readFromFile(outputFile)); + Assertions.assertTrue(readFromFile(outputFile).contains("dir")); } } diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index 7b9b222..f86436e 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -146,7 +146,7 @@ void testFormality() throws DeepLException, InterruptedException { translator.translateText( "How are you?", null, "de", new TextTranslationOptions().setFormality(Formality.Less)); if (!isMockServer) { - Assertions.assertEquals("Wie geht es dir?", result.getText()); + Assertions.assertTrue(result.getText().contains("dir")); } result = @@ -156,14 +156,14 @@ void testFormality() throws DeepLException, InterruptedException { "de", new TextTranslationOptions().setFormality(Formality.Default)); if (!isMockServer) { - Assertions.assertEquals("Wie geht es Ihnen?", result.getText()); + Assertions.assertTrue(result.getText().contains("Ihnen")); } result = translator.translateText( "How are you?", null, "de", new TextTranslationOptions().setFormality(Formality.More)); if (!isMockServer) { - Assertions.assertEquals("Wie geht es Ihnen?", result.getText()); + Assertions.assertTrue(result.getText().contains("Ihnen")); } result = @@ -173,7 +173,7 @@ void testFormality() throws DeepLException, InterruptedException { "de", new TextTranslationOptions().setFormality(Formality.PreferLess)); if (!isMockServer) { - Assertions.assertEquals("Wie geht es dir?", result.getText()); + Assertions.assertTrue(result.getText().contains("dir")); } result = @@ -183,7 +183,7 @@ void testFormality() throws DeepLException, InterruptedException { "de", new TextTranslationOptions().setFormality(Formality.PreferMore)); if (!isMockServer) { - Assertions.assertEquals("Wie geht es Ihnen?", result.getText()); + Assertions.assertTrue(result.getText().contains("Ihnen")); } } From 85695f9bc020f6e4ec888fd181663fd574d54d8c Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 23 Oct 2025 09:56:49 +0200 Subject: [PATCH 093/121] feat: add support for extra request params --- CHANGELOG.md | 6 ++-- .../com/deepl/api/BaseRequestOptions.java | 29 ++++++++++++++++ .../deepl/api/DocumentTranslationOptions.java | 2 +- .../com/deepl/api/TextTranslationOptions.java | 2 +- .../main/java/com/deepl/api/Translator.java | 33 ++++++++++++++++--- .../java/com/deepl/api/TranslateTextTest.java | 19 +++++++++++ 6 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/BaseRequestOptions.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b67e286..354b539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Added - -### Changed - +### Added +* Added `extraRequestParameters` option to text and document translation methods to pass arbitrary parameters in the request body. This can be used to access beta features or override built-in parameters (such as `target_lang`, `source_lang`, etc.). ## [1.10.3] - 2025-08-22 ### Security diff --git a/deepl-java/src/main/java/com/deepl/api/BaseRequestOptions.java b/deepl-java/src/main/java/com/deepl/api/BaseRequestOptions.java new file mode 100644 index 0000000..fc76cf0 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/BaseRequestOptions.java @@ -0,0 +1,29 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import java.util.Map; + +/** Base class for request options providing common functionality for all endpoints. */ +public abstract class BaseRequestOptions { + private Map extraBodyParameters; + + /** + * Sets additional parameters to pass in the body of the HTTP request. Can be used to access beta + * features, override built-in parameters, or for testing purposes. Keys in this map will be added + * to the request body and can override existing keys. + * + * @param extraBodyParameters Map of additional parameters to include in the request. + * @return This options object for method chaining. + */ + public BaseRequestOptions setExtraBodyParameters(Map extraBodyParameters) { + this.extraBodyParameters = extraBodyParameters; + return this; + } + + /** Gets the current extra body parameters. */ + public Map getExtraBodyParameters() { + return extraBodyParameters; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java index 562f9c8..6b5c51c 100644 --- a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java @@ -13,7 +13,7 @@ * .setFormality(Formality.Less).setGlossaryId("f63c02c5-f056-.."); * */ -public class DocumentTranslationOptions { +public class DocumentTranslationOptions extends BaseRequestOptions { private Formality formality; private String glossaryId; diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index a4507d6..f67d480 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -13,7 +13,7 @@ * .setFormality(Formality.Less).setGlossaryId("f63c02c5-f056-.."); * */ -public class TextTranslationOptions { +public class TextTranslationOptions extends BaseRequestOptions { private Formality formality; private String glossaryId; private SentenceSplittingMode sentenceSplittingMode; diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index fd704b1..af8acc5 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -846,6 +846,7 @@ private static ArrayList> createHttpParams( if (options.getIgnoreTags() != null) { params.add(new KeyValuePair<>("ignore_tags", joinTags(options.getIgnoreTags()))); } + addExtraBodyParameters(params, options.getExtraBodyParameters()); } return params; } @@ -862,11 +863,16 @@ private static ArrayList> createHttpParams( */ protected static ArrayList> createHttpParams( String sourceLang, String targetLang, DocumentTranslationOptions options) { - return createHttpParamsCommon( - sourceLang, - targetLang, - options != null ? options.getFormality() : null, - options != null ? options.getGlossaryId() : null); + ArrayList> params = + createHttpParamsCommon( + sourceLang, + targetLang, + options != null ? options.getFormality() : null, + options != null ? options.getGlossaryId() : null); + + addExtraBodyParameters(params, options != null ? options.getExtraBodyParameters() : null); + + return params; } /** @@ -931,6 +937,23 @@ private static String joinTags(Iterable tags) { return String.join(",", tags); } + /** + * Adds extra body parameters to the HTTP request parameters. Extra parameters can override + * existing parameters. + * + * @param params List of HTTP parameters to add to. + * @param extraBodyParameters Map of extra parameters to add (can be null). + */ + private static void addExtraBodyParameters( + ArrayList> params, Map extraBodyParameters) { + if (extraBodyParameters != null) { + params.removeIf(pair -> extraBodyParameters.containsKey(pair.getKey())); + for (Map.Entry entry : extraBodyParameters.entrySet()) { + params.add(new KeyValuePair<>(entry.getKey(), entry.getValue())); + } + } + } + /** * Checks the specified source and target language are valid. * diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index f86436e..1a7ee31 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -330,4 +330,23 @@ void testMixedCaseLanguages() throws DeepLException, InterruptedException { Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase(Locale.ENGLISH)); Assertions.assertEquals("de", result.getDetectedSourceLanguage()); } + + @Test + void testExtraBodyParams() throws DeepLException, InterruptedException { + Translator translator = createTranslator(); + + // Verifies that extra_body_parameters can override standard parameters like target_lang + Map extraParams = new HashMap<>(); + extraParams.put("target_lang", "FR"); + extraParams.put("debug", "1"); + + TextTranslationOptions options = new TextTranslationOptions(); + options.setExtraBodyParameters(extraParams); + + TextResult result = translator.translateText(exampleText.get("en"), null, "DE", options); + + Assertions.assertEquals(exampleText.get("fr"), result.getText()); + Assertions.assertEquals("en", result.getDetectedSourceLanguage()); + Assertions.assertEquals(exampleText.get("en").length(), result.getBilledCharacters()); + } } From 5bfddcdfa2ffcf1c4c36da6a8d376a94d94fa76e Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 4 Nov 2025 11:18:18 +0100 Subject: [PATCH 094/121] ci: bump-my-version config --- .bumpversion.toml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .bumpversion.toml diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..4bce192 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,22 @@ +# Configuration file for bumpversion +# See https://github.com/callowayproject/bump-my-version +[tool.bumpversion] +current_version = "1.10.3" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +tag = false +allow_dirty = false +commit = false + +[[tool.bumpversion.files]] +filename = "README.md" + +[[tool.bumpversion.files]] +filename = "deepl-java/build.gradle.kts" + +[[tool.bumpversion.files]] +filename = "deepl-java/src/main/java/com/deepl/api/Translator.java" \ No newline at end of file From eb79c1daf0ad6c177decc631879088d012899e6d Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 4 Nov 2025 11:22:38 +0100 Subject: [PATCH 095/121] docs: format changelog --- CHANGELOG.md | 119 ++++++++++++++++++++++----------------------------- 1 file changed, 52 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 354b539..150ea1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,187 +5,172 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - ### Added -* Added `extraRequestParameters` option to text and document translation methods to pass arbitrary parameters in the request body. This can be used to access beta features or override built-in parameters (such as `target_lang`, `source_lang`, etc.). +- Added `extraRequestParameters` option to text and document translation methods to pass arbitrary parameters in the request body. This can be used to access beta features or override built-in parameters (such as `target_lang`, `source_lang`, etc.). ## [1.10.3] - 2025-08-22 ### Security -* Updated `org.apache.httpcomponents:httpclient` to 4.5.14 due to [CVE-2020-13956](https://nvd.nist.gov/vuln/detail/CVE-2020-13956). - * Thanks to [warm-tune](https://github.com/warm-tune) for reporting in [#72](https://github.com/DeepLcom/deepl-java/issues/72). - +- Updated `org.apache.httpcomponents:httpclient` to 4.5.14 due to [CVE-2020-13956](https://nvd.nist.gov/vuln/detail/CVE-2020-13956). + * Thanks to [warm-tune](https://github.com/warm-tune) for reporting in [#72](https://github.com/DeepLcom/deepl-java/issues/72). ## [1.10.2] - 2025-07-11 ### Changed -* Migrate to Sonatype Portal OSSRH Staging API due to legacy OSSRH being sunsetted. -* Whitespace surrounding auth key is now stripped. +- Migrate to Sonatype Portal OSSRH Staging API due to legacy OSSRH being sunsetted. +- Whitespace surrounding auth key is now stripped. * Thanks to [timazhum](https://github.com/timazhum) for the fix in [#64](https://github.com/DeepLcom/deepl-java/pull/69). - ## [1.10.1] - 2025-06-18 ### Fixed -* Fixed `DeepLClient::deleteMultilingualGlossary(String glossaryId)` being package private, made it public instead. +- Fixed `DeepLClient::deleteMultilingualGlossary(String glossaryId)` being package private, made it public instead. * Thanks to [MTSxoff](https://github.com/MTSxoff) for the report in [#68](https://github.com/DeepLcom/deepl-java/issues/68) and the fix in [#69](https://github.com/DeepLcom/deepl-java/pull/69). - ## [1.10.0] - 2025-04-30 ### Added -* Added support for the /v3 Multilingual Glossary APIs in the client library +- Added support for the /v3 Multilingual Glossary APIs in the client library while providing backwards compatability for the previous /v2 Glossary endpoints. Please refer to the README or [upgrading_to_multilingual_glossaries.md](upgrading_to_multilingual_glossaries.md) for usage instructions. -* Added Ukrainian language code - +- Added Ukrainian language code ## [1.9.0] - 2025-02-21 ### Added -* Allow specifying the API version to use. This is mostly for users who have an +- Allow specifying the API version to use. This is mostly for users who have an API subscription that includes an API key for CAT tool usage, who need to use the v1 API. - ## [1.8.1] - 2025-02-07 ### Fixed -* Added a constructor for `DeepLClient` that only takes an `authKey`, to fix the +- Added a constructor for `DeepLClient` that only takes an `authKey`, to fix the README example and be in line with `Translator`. -* Un-deprecated the `Translator` and `TranslatorOptions` class and moved it to +- Un-deprecated the `Translator` and `TranslatorOptions` class and moved it to their constructors. The functionality in them continues to work and be supported, user code should just use `DeepLClient` and `DeepLClientOptions`. ## [1.8.0] - 2025-01-17 ### Added -* Added support for the Write API in the client library, the implementation +- Added support for the Write API in the client library, the implementation can be found in the `DeepLClient` class. Please refer to the README for usage instructions. + ### Changed -* The main functionality of the library is now also exposed via the `DeepLClient` +- The main functionality of the library is now also exposed via the `DeepLClient` class. Please change your code to use this over the `Translator` class whenever convenient. ## [1.7.0] - 2024-11-15 ### Added -* Added `modelType` option to `translateText()` to use models with higher +- Added `modelType` option to `translateText()` to use models with higher translation quality (available for some language pairs), or better latency. Options are `'quality_optimized'`, `'latency_optimized'`, and `'prefer_quality_optimized'` -* Added the `modelTypeUsed` field to `translateText()` response, that +- Added the `modelTypeUsed` field to `translateText()` response, that indicates the translation model used when the `modelType` option is specified. - ## [1.6.0] - 2024-09-17 ### Added -* Added `getBilledCharacters()` to text translation response. - +- Added `getBilledCharacters()` to text translation response. ## [1.5.1] - 2024-09-05 ### Fixed -* Fixed parsing for usage count and limit for large values. - * Thanks to [lubo-dev](https://github.com/lubo-dev) in [#45](https://github.com/DeepLcom/deepl-java/pull/45). - +- Fixed parsing for usage count and limit for large values. + * Thanks to [lubo-dev](https://github.com/lubo-dev) in [#45](https://github.com/DeepLcom/deepl-java/pull/45). ## [1.5.0] - 2024-04-10 ### Added -* New language available: Arabic (MSA) (`'ar'`). Add language code constants and tests. +- New language available: Arabic (MSA) (`'ar'`). Add language code constants and tests. Note: older library versions also support the new language, this update only adds new code constants. + ### Fixed -* Change document upload to use the path `/v2/document` instead of `/v2/document/` (no trailing `/`). +- Change document upload to use the path `/v2/document` instead of `/v2/document/` (no trailing `/`). Both paths will continue to work in the v2 version of the API, but `/v2/document` is the intended one. - ## [1.4.0] - 2023-11-03 ### Added -* Add optional `context` parameter for text translation, that specifies +- Add optional `context` parameter for text translation, that specifies additional context to influence translations, that is not translated itself. -### Fixed -* Remove unused `commons-math` dependency - -## [1.3.0] - 2023-06-09 ### Fixed -* Changed document translation to poll the server every 5 seconds. This should greatly reduce observed document translation processing time. -* Fix getUsage request to be a HTTP GET request, not POST. +- Remove unused `commons-math` dependency +## [1.3.0] - 2023-06-09 +### Fixed +- Changed document translation to poll the server every 5 seconds. This should greatly reduce observed document translation processing time. +- Fix getUsage request to be a HTTP GET request, not POST. ## [1.2.0] - 2023-03-22 ### Added -* Script to check our source code for license headers and a step for them in the CI. -* Added system and java version information to the user-agent string that is sent with API calls, along with an opt-out. -* Added method for applications that use this library to identify themselves in API requests they make. - +- Script to check our source code for license headers and a step for them in the CI. +- Added system and java version information to the user-agent string that is sent with API calls, along with an opt-out. +- Added method for applications that use this library to identify themselves in API requests they make. ## [1.1.0] - 2023-01-26 ### Added -* Add example maven project using this library. -* New languages available: Korean (`'ko'`) and Norwegian (bokmål) (`'nb'`). Add +- Add example maven project using this library. +- New languages available: Korean (`'ko'`) and Norwegian (bokmål) (`'nb'`). Add language code constants and tests. Note: older library versions also support the new languages, this update only adds new code constants. -### Fixed -* Send Formality options in API requests even if it is default. +### Fixed +- Send Formality options in API requests even if it is default. ## [1.0.1] - 2023-01-02 ### Fixed -* Always send SentenceSplittingMode option in requests. - * [#3](https://github.com/DeepLcom/deepl-java/issues/3) thanks to +- Always send SentenceSplittingMode option in requests. + * [#3](https://github.com/DeepLcom/deepl-java/issues/3) thanks to [nicStuff](https://github.com/nicStuff) - ## [1.0.0] - 2022-12-15 ### Added -* Add support for glossary management functions. +- Add support for glossary management functions. + ### Changed -* `parsing.ErrorResponse` fields `message` and `detail` are now private, +- `parsing.ErrorResponse` fields `message` and `detail` are now private, encapsulated with getters. - ## [0.2.1] - 2022-10-19 ### Fixed -* Handle case where HTTP response is not valid JSON. - +- Handle case where HTTP response is not valid JSON. ## [0.2.0] - 2022-09-26 ### Added -* Add new `Formality` options: `PreferLess` and `PreferMore`. +- Add new `Formality` options: `PreferLess` and `PreferMore`. + ### Changed -* Requests resulting in `503 Service Unavailable` errors are now retried. +- Requests resulting in `503 Service Unavailable` errors are now retried. Attempting to download a document before translation is completed will now wait and retry (up to 5 times by default), rather than throwing an exception. + ### Fixed -* Use `Locale.ENGLISH` when changing string case. +- Use `Locale.ENGLISH` when changing string case. * Thanks to [seratch](https://github.com/seratch). -* Avoid cases in `HttpContent` and `StreamUtils` where temporary objects might +- Avoid cases in `HttpContent` and `StreamUtils` where temporary objects might not be closed. * Thanks to [seratch](https://github.com/seratch). - ## [0.1.3] - 2022-09-09 ### Fixed -* Fixed examples in readme. -* `Usage.Detail` `count` and `limit` properties type changed from `int` to `long`. - +- Fixed examples in readme. +- `Usage.Detail` `count` and `limit` properties type changed from `int` to `long`. ## [0.1.2] - 2022-09-08 ### Fixed -* Fix publishing to Maven Central by including sourcesJar and javadocJar. - +- Fix publishing to Maven Central by including sourcesJar and javadocJar. ## [0.1.1] - 2022-09-08 ### Fixed -* Fix CI publishing step. - +- Fix CI publishing step. ## [0.1.0] - 2022-09-08 Initial version. - [Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.3...HEAD [1.10.3]: https://github.com/DeepLcom/deepl-java/compare/v1.10.2...v1.10.3 -[1.10.2]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.2 +[1.10.2]: https://github.com/DeepLcom/deepl-java/compare/v1.10.1...v1.10.2 [1.10.1]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.1 [1.10.0]: https://github.com/DeepLcom/deepl-java/compare/v1.9.0...v1.10.0 [1.9.0]: https://github.com/DeepLcom/deepl-java/compare/v1.8.1...v1.9.0 From bd2599d870af8a3fc1ea4cad04b739265d73e06f Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 4 Nov 2025 11:28:25 +0100 Subject: [PATCH 096/121] docs: Increase version to 1.11.0, update Java 8 image for testing --- .bumpversion.toml | 2 +- .gitlab-ci.yml | 2 +- CHANGELOG.md | 5 ++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 4bce192..d7b79c3 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.10.3" +current_version = "1.11.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2386c9..edacc41 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -127,7 +127,7 @@ build_manual: parallel: matrix: - DOCKER_IMAGE: "eclipse-temurin:18-alpine" - - DOCKER_IMAGE: "openjdk:8-alpine" + - DOCKER_IMAGE: "amazoncorretto:8-alpine" USE_MOCK_SERVER: "use mock server" - DOCKER_IMAGE: "eclipse-temurin:8-focal" USE_MOCK_SERVER: "use mock server" diff --git a/CHANGELOG.md b/CHANGELOG.md index 150ea1f..2fdae5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [1.11.0] - 2025-11-04 ### Added - Added `extraRequestParameters` option to text and document translation methods to pass arbitrary parameters in the request body. This can be used to access beta features or override built-in parameters (such as `target_lang`, `source_lang`, etc.). @@ -168,7 +170,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2022-09-08 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.10.3...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.11.0...HEAD +[1.11.0]: https://github.com/DeepLcom/deepl-java/compare/v1.10.3...v1.11.0 [1.10.3]: https://github.com/DeepLcom/deepl-java/compare/v1.10.2...v1.10.3 [1.10.2]: https://github.com/DeepLcom/deepl-java/compare/v1.10.1...v1.10.2 [1.10.1]: https://github.com/DeepLcom/deepl-java/compare/v1.10.0...v1.10.1 diff --git a/README.md b/README.md index 32efc4c..48f0a31 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.10.3" +implementation "com.deepl.api:deepl-java:1.11.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.10.3 + 1.11.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 1273570..cbad516 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.deepl.api" -version = "1.10.3" +version = "1.11.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index af8acc5..32c3930 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.10.3"); + sb.append("deepl-java/1.11.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From a990ba9cb2fe597e40a4ace06d43989ab38f0aeb Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Wed, 5 Nov 2025 08:56:26 +0100 Subject: [PATCH 097/121] ci: consolidate CI jobs --- .gitlab-ci.yml | 29 ++--------------------------- deepl-java/build.gradle.kts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index edacc41..d3d59fe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -212,6 +212,8 @@ test_examples_manual: - build_manual script: - ./gradlew publish + # Trigger deployment from same IP to ensure Sonatype can find the staging repository + - ./gradlew triggerDeployment publish: extends: .publish_base @@ -222,33 +224,6 @@ publish_manual: extends: .publish_base when: manual -## trigger deployment - -.trigger_deployment_base: - extends: .publish - stage: publish - image: curlimages/curl:8.14.1 - script: - - NAMESPACE="com.deepl.api" - - URL="https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/$NAMESPACE" - - PARAMS="publishing_type=automatic" - - BEARER_TOKEN=$(echo "${ORG_GRADLE_PROJECT_mavenUploadUsername}:${ORG_GRADLE_PROJECT_mavenUploadPassword}" | base64) - - | - curl "$URL?$PARAMS" --header "Authorization: Bearer $BEARER_TOKEN" -d "" --write-out "%{http_code}\n" - -trigger_deployment: - extends: .trigger_deployment_base - dependencies: - - publish - rules: - - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' - -trigger_deployment_manual: - extends: .trigger_deployment_base - dependencies: - - publish_manual - when: manual - ## gitlab release gitlab release: diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index cbad516..3ffce20 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -1,3 +1,7 @@ +import java.net.HttpURLConnection +import java.net.URL +import java.util.Base64 + plugins { `java-library` `maven-publish` @@ -124,3 +128,35 @@ signing { sign(publishing.publications["mavenJava"]) } +tasks.register("triggerDeployment") { + doLast { + val mavenUploadUsername: String? by project + val mavenUploadPassword: String? by project + val namespace = "com.deepl.api" + val url = "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/$namespace?publishing_type=automatic" + + val credentials = "$mavenUploadUsername:$mavenUploadPassword" + val encodedCredentials = Base64.getEncoder().encodeToString(credentials.toByteArray()) + + val connection = URL(url).openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.setRequestProperty("Authorization", "Bearer $encodedCredentials") + connection.doOutput = true + connection.outputStream.write(byteArrayOf()) + + val responseCode = connection.responseCode + val response = if (responseCode == 200) { + connection.inputStream.bufferedReader().readText() + } else { + connection.errorStream.bufferedReader().readText() + } + + println(response) + println("HTTP Status: $responseCode") + + if (responseCode != 200) { + throw GradleException("Failed to trigger deployment: $responseCode - $response") + } + } +} + From 3c2f9a993a853a9fd9e4a7cdf3de06f3a9328ae8 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Wed, 5 Nov 2025 09:15:38 +0100 Subject: [PATCH 098/121] ci: add additional logs and info for publish step --- .gitlab-ci.yml | 8 +++++++- deepl-java/build.gradle.kts | 15 +++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d3d59fe..81b5af6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -211,7 +211,13 @@ test_examples_manual: - build_scheduled - build_manual script: - - ./gradlew publish + - echo "=== Publishing to Maven Central ===" + - echo "Runner node $(hostname)" + - echo "" + - echo "Step 1/2 Publishing artifacts to staging repository..." + - ./gradlew publish --info + - echo "" + - echo "Step 2/2 Triggering deployment from same IP..." # Trigger deployment from same IP to ensure Sonatype can find the staging repository - ./gradlew triggerDeployment diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 3ffce20..6456add 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -135,6 +135,9 @@ tasks.register("triggerDeployment") { val namespace = "com.deepl.api" val url = "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/$namespace?publishing_type=automatic" + println("Triggering deployment for namespace: $namespace") + println("Endpoint: $url") + val credentials = "$mavenUploadUsername:$mavenUploadPassword" val encodedCredentials = Base64.getEncoder().encodeToString(credentials.toByteArray()) @@ -148,15 +151,19 @@ tasks.register("triggerDeployment") { val response = if (responseCode == 200) { connection.inputStream.bufferedReader().readText() } else { - connection.errorStream.bufferedReader().readText() + connection.errorStream?.bufferedReader()?.readText() ?: "No error response" } - println(response) - println("HTTP Status: $responseCode") + println("Response code: $responseCode") + if (response.isNotBlank()) { + println("Response body: $response") + } if (responseCode != 200) { - throw GradleException("Failed to trigger deployment: $responseCode - $response") + throw GradleException("Failed to trigger deployment: HTTP $responseCode - $response") } + + println("Deployment triggered successfully") } } From bca4f824bcb44855b0bb7e6407cefe0322e51240 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Sun, 9 Nov 2025 13:49:53 -0500 Subject: [PATCH 099/121] feat: Add support for style_id and get all style rules endpoint --- README.md | 63 ++++++++++ .../java/com/deepl/api/ConfiguredRules.java | 109 +++++++++++++++++ .../java/com/deepl/api/CustomInstruction.java | 49 ++++++++ .../main/java/com/deepl/api/DeepLClient.java | 62 ++++++++++ .../java/com/deepl/api/StyleRuleInfo.java | 110 ++++++++++++++++++ .../com/deepl/api/TextTranslationOptions.java | 23 ++++ .../main/java/com/deepl/api/Translator.java | 5 +- .../java/com/deepl/api/parsing/Parser.java | 5 + .../api/parsing/StyleRuleListResponse.java | 20 ++++ .../java/com/deepl/api/StyleRuleTest.java | 71 +++++++++++ 10 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/ConfiguredRules.java create mode 100644 deepl-java/src/main/java/com/deepl/api/CustomInstruction.java create mode 100644 deepl-java/src/main/java/com/deepl/api/StyleRuleInfo.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/StyleRuleListResponse.java create mode 100644 deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java diff --git a/README.md b/README.md index 48f0a31..df79af5 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,9 @@ a `TextTranslationOptions`, with the following setters: `listGlossaries()` or `listMultilingualGlossaries()`). - `setGlossaryId()` is also available for backward-compatibility, accepting a string containing the glossary ID. +- `setStyleRule()`: specifies a style rule to use with translation, as a string + containing the ID of the style rule, or a `StyleRuleInfo` object. + - `setStyleId()` is also available, accepting a string containing the style rule ID. - `setContext()`: specifies additional context to influence translations, that is not translated itself. Characters in the `context` parameter are not counted toward billing. See the [API documentation][api-docs-context-param] for more information and @@ -640,6 +643,66 @@ class Example { // Continuing class Example from above The `translateDocument()` and `translateDocumentUpload()` functions both support the `glossary` argument. +### Style Rules + +Style rules allow you to customize your translations using a managed, shared list +of rules for style, formatting, and more. Multiple style rules can be stored with +your account, each with a user-specified name and a uniquely-assigned ID. + +#### Creating and managing style rules + +Currently style rules must be created and managed in the DeepL UI via +https://www.deepl.com/en/custom-rules. Full CRUD functionality via the APIs will +come shortly. + +#### Listing all style rules + +`getAllStyleRules()` returns a list of `StyleRuleInfo` objects +corresponding to all of your stored style rules. The method accepts optional +parameters: `page` (page number for pagination, 0-indexed), `pageSize` (number +of items per page), and `detailed` (whether to include detailed configuration +rules in the `configuredRules` property). + +```java +class Example { // Continuing class Example from above + public void styleRulesExample() throws Exception { + // Get all style rules + List styleRules = client.getAllStyleRules(); + for (StyleRuleInfo rule : styleRules) { + System.out.println(String.format("%s (%s)", rule.getName(), rule.getStyleId())); + } + + // Get style rules with detailed configuration + List styleRulesDetailed = client.getAllStyleRules(null, null, true); + for (StyleRuleInfo rule : styleRulesDetailed) { + if (rule.getConfiguredRules() != null && rule.getConfiguredRules().getNumbers() != null) { + System.out.println(String.format("Number formatting rules: %s", + String.join(", ", rule.getConfiguredRules().getNumbers().keySet()))); + } + } + } +} +``` + +#### Using a stored style rule + +You can use a stored style rule for text translation by setting the `styleRule` +argument to either the style rule ID or `StyleRuleInfo` object: + +```java +class Example { // Continuing class Example from above + public void usingStyleRuleExample() throws Exception { + String styleId = "dca2e053-8ae5-45e6-a0d2-881156e7f4e4"; + TextResult result = client.translateText( + "Hallo, Welt!", + "de", + "en-US", + new TextTranslationOptions().setStyleId(styleId)); + System.out.println(result.getText()); + } +} +``` + ### Checking account usage To check account usage, use the `getUsage()` function. diff --git a/deepl-java/src/main/java/com/deepl/api/ConfiguredRules.java b/deepl-java/src/main/java/com/deepl/api/ConfiguredRules.java new file mode 100644 index 0000000..297044f --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/ConfiguredRules.java @@ -0,0 +1,109 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** Configuration rules for a style rule list. */ +public class ConfiguredRules { + @SerializedName(value = "dates_and_times") + @Nullable + private final Map datesAndTimes; + + @SerializedName(value = "formatting") + @Nullable + private final Map formatting; + + @SerializedName(value = "numbers") + @Nullable + private final Map numbers; + + @SerializedName(value = "punctuation") + @Nullable + private final Map punctuation; + + @SerializedName(value = "spelling_and_grammar") + @Nullable + private final Map spellingAndGrammar; + + @SerializedName(value = "style_and_tone") + @Nullable + private final Map styleAndTone; + + @SerializedName(value = "vocabulary") + @Nullable + private final Map vocabulary; + + /** + * Initializes a new {@link ConfiguredRules} containing configuration rules for a style rule list. + * + * @param datesAndTimes Date and time formatting rules. + * @param formatting Text formatting rules. + * @param numbers Number formatting rules. + * @param punctuation Punctuation rules. + * @param spellingAndGrammar Spelling and grammar rules. + * @param styleAndTone Style and tone rules. + * @param vocabulary Vocabulary rules. + */ + public ConfiguredRules( + @Nullable Map datesAndTimes, + @Nullable Map formatting, + @Nullable Map numbers, + @Nullable Map punctuation, + @Nullable Map spellingAndGrammar, + @Nullable Map styleAndTone, + @Nullable Map vocabulary) { + this.datesAndTimes = datesAndTimes; + this.formatting = formatting; + this.numbers = numbers; + this.punctuation = punctuation; + this.spellingAndGrammar = spellingAndGrammar; + this.styleAndTone = styleAndTone; + this.vocabulary = vocabulary; + } + + /** @return Date and time formatting rules. */ + @Nullable + public Map getDatesAndTimes() { + return datesAndTimes; + } + + /** @return Text formatting rules. */ + @Nullable + public Map getFormatting() { + return formatting; + } + + /** @return Number formatting rules. */ + @Nullable + public Map getNumbers() { + return numbers; + } + + /** @return Punctuation rules. */ + @Nullable + public Map getPunctuation() { + return punctuation; + } + + /** @return Spelling and grammar rules. */ + @Nullable + public Map getSpellingAndGrammar() { + return spellingAndGrammar; + } + + /** @return Style and tone rules. */ + @Nullable + public Map getStyleAndTone() { + return styleAndTone; + } + + /** @return Vocabulary rules. */ + @Nullable + public Map getVocabulary() { + return vocabulary; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java b/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java new file mode 100644 index 0000000..568cbe7 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java @@ -0,0 +1,49 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import org.jetbrains.annotations.*; + +/** Custom instruction for a style rule. */ +public class CustomInstruction { + @SerializedName(value = "label") + private final String label; + + @SerializedName(value = "prompt") + private final String prompt; + + @SerializedName(value = "source_language") + @Nullable + private final String sourceLanguage; + + /** + * Initializes a new {@link CustomInstruction} containing a custom instruction for a style rule. + * + * @param label Label for the custom instruction. + * @param prompt Prompt text for the custom instruction. + * @param sourceLanguage Optional source language code for the custom instruction. + */ + public CustomInstruction(String label, String prompt, @Nullable String sourceLanguage) { + this.label = label; + this.prompt = prompt; + this.sourceLanguage = sourceLanguage; + } + + /** @return Label for the custom instruction. */ + public String getLabel() { + return label; + } + + /** @return Prompt text for the custom instruction. */ + public String getPrompt() { + return prompt; + } + + /** @return Optional source language code for the custom instruction. */ + @Nullable + public String getSourceLanguage() { + return sourceLanguage; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index f524c4d..d19d208 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -732,6 +732,68 @@ public void deleteMultilingualGlossaryDictionary( glossaryDict.getTargetLanguageCode()); } + /** + * Retrieves the list of all available style rules and returns a list of {@link StyleRuleInfo} + * objects corresponding to all of your stored style rules. + * + * @param page Optional page number for pagination, 0-indexed. + * @param pageSize Optional number of items per page. + * @param detailed Optional flag indicating whether to include detailed configuration rules + * including the configuredRules and customInstructions properties. + * @return List of {@link StyleRuleInfo} objects for all available style rules. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API, a {@link + * DeepLException} or a derived class will be thrown. + */ + public List getAllStyleRules( + @Nullable Integer page, @Nullable Integer pageSize, @Nullable Boolean detailed) + throws DeepLException, InterruptedException { + ArrayList> queryParams = new ArrayList<>(); + if (page != null) { + queryParams.add(new KeyValuePair<>("page", page.toString())); + } + if (pageSize != null) { + queryParams.add(new KeyValuePair<>("page_size", pageSize.toString())); + } + if (detailed != null) { + queryParams.add(new KeyValuePair<>("detailed", detailed.toString().toLowerCase())); + } + + String queryString = ""; + if (!queryParams.isEmpty()) { + StringBuilder sb = new StringBuilder("?"); + for (int i = 0; i < queryParams.size(); i++) { + if (i > 0) { + sb.append("&"); + } + KeyValuePair param = queryParams.get(i); + try { + sb.append(URLEncoder.encode(param.getKey(), StandardCharsets.UTF_8.name())) + .append("=") + .append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8.name())); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported", e); + } + } + queryString = sb.toString(); + } + + String relativeUrl = "/v3/style_rules" + queryString; + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + return jsonParser.parseStyleRuleInfoList(response.getBody()); + } + + /** + * Functions the same as {@link DeepLClient#getAllStyleRules(Integer, Integer, Boolean)} but with + * default parameters (all null). + * + * @see DeepLClient#getAllStyleRules(Integer, Integer, Boolean) + */ + public List getAllStyleRules() throws DeepLException, InterruptedException { + return getAllStyleRules(null, null, null); + } + /** Creates a glossary with given details. */ private MultilingualGlossaryInfo createGlossaryFromCsvInternal( String name, String sourceLanguageCode, String targetLanguageCode, String entries) diff --git a/deepl-java/src/main/java/com/deepl/api/StyleRuleInfo.java b/deepl-java/src/main/java/com/deepl/api/StyleRuleInfo.java new file mode 100644 index 0000000..e712d01 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/StyleRuleInfo.java @@ -0,0 +1,110 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** Information about a style rule list. */ +public class StyleRuleInfo { + @SerializedName(value = "style_id") + private final String styleId; + + @SerializedName(value = "name") + private final String name; + + @SerializedName(value = "creation_time") + private final Date creationTime; + + @SerializedName(value = "updated_time") + private final Date updatedTime; + + @SerializedName(value = "language") + private final String language; + + @SerializedName(value = "version") + private final int version; + + @SerializedName(value = "configured_rules") + @Nullable + private final ConfiguredRules configuredRules; + + @SerializedName(value = "custom_instructions") + @Nullable + private final List customInstructions; + + /** + * Initializes a new {@link StyleRuleInfo} containing information about a style rule list. + * + * @param styleId Unique ID assigned to the style rule list. + * @param name User-defined name assigned to the style rule list. + * @param creationTime Timestamp when the style rule list was created. + * @param updatedTime Timestamp when the style rule list was last updated. + * @param language Language code for the style rule list. + * @param version Version number of the style rule list. + * @param configuredRules The predefined rules that have been enabled. + * @param customInstructions Optional list of custom instructions. + */ + public StyleRuleInfo( + String styleId, + String name, + Date creationTime, + Date updatedTime, + String language, + int version, + @Nullable ConfiguredRules configuredRules, + @Nullable List customInstructions) { + this.styleId = styleId; + this.name = name; + this.creationTime = creationTime; + this.updatedTime = updatedTime; + this.language = language; + this.version = version; + this.configuredRules = configuredRules; + this.customInstructions = customInstructions; + } + + /** @return Unique ID assigned to the style rule list. */ + public String getStyleId() { + return styleId; + } + + /** @return User-defined name assigned to the style rule list. */ + public String getName() { + return name; + } + + /** @return Timestamp when the style rule list was created. */ + public Date getCreationTime() { + return creationTime; + } + + /** @return Timestamp when the style rule list was last updated. */ + public Date getUpdatedTime() { + return updatedTime; + } + + /** @return Language code for the style rule list. */ + public String getLanguage() { + return language; + } + + /** @return Version number of the style rule list. */ + public int getVersion() { + return version; + } + + /** @return The predefined rules that have been enabled. */ + @Nullable + public ConfiguredRules getConfiguredRules() { + return configuredRules; + } + + /** @return Optional list of custom instructions. */ + @Nullable + public List getCustomInstructions() { + return customInstructions; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index f67d480..c2d0b23 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -16,6 +16,7 @@ public class TextTranslationOptions extends BaseRequestOptions { private Formality formality; private String glossaryId; + private String styleId; private SentenceSplittingMode sentenceSplittingMode; private boolean preserveFormatting = false; private String context; @@ -65,6 +66,23 @@ public TextTranslationOptions setGlossary(String glossaryId) { return this; } + /** + * Sets the ID of a style rule to use with the translation. By default, this value is + * null and no style rule is used. + */ + public TextTranslationOptions setStyleId(String styleId) { + this.styleId = styleId; + return this; + } + + /** + * Sets the style rule to use with the translation. By default, this value is null + * and no style rule is used. + */ + public TextTranslationOptions setStyleRule(StyleRuleInfo styleRule) { + return setStyleId(styleRule.getStyleId()); + } + /** * Specifies additional context to influence translations, that is not translated itself. * Characters in the `context` parameter are not counted toward billing. See the API documentation @@ -167,6 +185,11 @@ public String getGlossaryId() { return glossaryId; } + /** Gets the current style rule ID. */ + public String getStyleId() { + return styleId; + } + /** Gets the current sentence splitting mode. */ public SentenceSplittingMode getSentenceSplittingMode() { return sentenceSplittingMode; diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 32c3930..cbb21d4 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.11.0"); + sb.append("deepl-java/1.12.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); @@ -846,6 +846,9 @@ private static ArrayList> createHttpParams( if (options.getIgnoreTags() != null) { params.add(new KeyValuePair<>("ignore_tags", joinTags(options.getIgnoreTags()))); } + if (options.getStyleId() != null) { + params.add(new KeyValuePair<>("style_id", options.getStyleId())); + } addExtraBodyParameters(params, options.getExtraBodyParameters()); } return params; diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index c8b7d32..ebe99db 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -86,6 +86,11 @@ public MultilingualGlossaryDictionaryInfo parseMultilingualGlossaryDictionaryInf return gson.fromJson(json, MultilingualGlossaryDictionaryInfo.class); } + public List parseStyleRuleInfoList(String json) { + StyleRuleListResponse result = gson.fromJson(json, StyleRuleListResponse.class); + return result.getStyleRules(); + } + public String parseErrorMessage(String json) { ErrorResponse response = gson.fromJson(json, ErrorResponse.class); diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/StyleRuleListResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/StyleRuleListResponse.java new file mode 100644 index 0000000..2b5bd06 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/StyleRuleListResponse.java @@ -0,0 +1,20 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.StyleRuleInfo; +import java.util.List; + +/** + * Class representing v3 style_rules list response by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class StyleRuleListResponse { + private List style_rules; + + public List getStyleRules() { + return style_rules; + } +} diff --git a/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java new file mode 100644 index 0000000..35f3328 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java @@ -0,0 +1,71 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import java.util.List; +import org.junit.jupiter.api.*; + +public class StyleRuleTest extends TestBase { + private static final String DEFAULT_STYLE_ID = "dca2e053-8ae5-45e6-a0d2-881156e7f4e4"; + + @Test + void testGetAllStyleRules() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + List styleRules = client.getAllStyleRules(0, 10, true); + + Assertions.assertNotNull(styleRules); + Assertions.assertFalse(styleRules.isEmpty()); + Assertions.assertEquals(DEFAULT_STYLE_ID, styleRules.get(0).getStyleId()); + Assertions.assertEquals("Default Style Rule", styleRules.get(0).getName()); + Assertions.assertNotNull(styleRules.get(0).getCreationTime()); + Assertions.assertNotNull(styleRules.get(0).getUpdatedTime()); + Assertions.assertEquals("en", styleRules.get(0).getLanguage()); + Assertions.assertEquals(1, styleRules.get(0).getVersion()); + Assertions.assertNotNull(styleRules.get(0).getConfiguredRules()); + Assertions.assertNotNull(styleRules.get(0).getCustomInstructions()); + } + + @Test + void testGetAllStyleRulesWithoutDetailed() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + List styleRules = client.getAllStyleRules(); + + Assertions.assertNotNull(styleRules); + Assertions.assertFalse(styleRules.isEmpty()); + Assertions.assertEquals(DEFAULT_STYLE_ID, styleRules.get(0).getStyleId()); + Assertions.assertNull(styleRules.get(0).getConfiguredRules()); + Assertions.assertNull(styleRules.get(0).getCustomInstructions()); + } + + @Test + void testTranslateTextWithStyleId() throws Exception { + // Note: this test may use the mock server that will not translate the text + // with a style rule, therefore we do not check the translated result. + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + String text = "Hallo, Welt!"; + + TextResult result = + client.translateText( + text, "de", "en-US", new TextTranslationOptions().setStyleId(DEFAULT_STYLE_ID)); + + Assertions.assertNotNull(result); + } + + @Test + void testTranslateTextWithStyleRuleInfo() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + List styleRules = client.getAllStyleRules(); + StyleRuleInfo rule = styleRules.get(0); + String text = "Hallo, Welt!"; + + TextResult result = + client.translateText(text, "de", "en-US", new TextTranslationOptions().setStyleRule(rule)); + + Assertions.assertNotNull(result); + } +} From ef09094e1c5ff22d940e57e309c6f2fbcc3b3336 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Sun, 9 Nov 2025 13:51:03 -0500 Subject: [PATCH 100/121] docs: Increase version to 1.12.0 --- .bumpversion.toml | 2 +- CHANGELOG.md | 11 ++++++++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index d7b79c3..1375c12 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.11.0" +current_version = "1.12.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fdae5f..1ec04a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.12.0] - 2025-11-12 +### Added +- Added support for the `GET /v3/style_rules` endpoint in the client library, the + implementation can be found in the `DeepLClient` class. Please refer to the + README for usage instructions +- Added `styleId` option to `translateText()` which allows text translation with + style rules. + ## [1.11.0] - 2025-11-04 ### Added - Added `extraRequestParameters` option to text and document translation methods to pass arbitrary parameters in the request body. This can be used to access beta features or override built-in parameters (such as `target_lang`, `source_lang`, etc.). @@ -170,7 +178,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2022-09-08 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.11.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.12.0...HEAD +[1.12.0]: https://github.com/DeepLcom/deepl-java/compare/v1.11.0...v1.12.0 [1.11.0]: https://github.com/DeepLcom/deepl-java/compare/v1.10.3...v1.11.0 [1.10.3]: https://github.com/DeepLcom/deepl-java/compare/v1.10.2...v1.10.3 [1.10.2]: https://github.com/DeepLcom/deepl-java/compare/v1.10.1...v1.10.2 diff --git a/README.md b/README.md index df79af5..9be002d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.11.0" +implementation "com.deepl.api:deepl-java:1.12.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.11.0 + 1.12.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 6456add..d6650ce 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "com.deepl.api" -version = "1.11.0" +version = "1.12.0" val sharedManifest = the().manifest { attributes ( From f00229ce5e1a5178175d2dacb1600c5fc7133670 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Wed, 3 Dec 2025 00:10:58 -0500 Subject: [PATCH 101/121] feat: Add support for custom_instructions parameter in text translation --- README.md | 9 +++++++- .../com/deepl/api/TextTranslationOptions.java | 15 ++++++++++++ .../main/java/com/deepl/api/Translator.java | 5 ++++ .../java/com/deepl/api/TranslateTextTest.java | 23 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9be002d..236fe6d 100644 --- a/README.md +++ b/README.md @@ -168,9 +168,16 @@ a `TextTranslationOptions`, with the following setters: containing the ID of the style rule, or a `StyleRuleInfo` object. - `setStyleId()` is also available, accepting a string containing the style rule ID. - `setContext()`: specifies additional context to influence translations, that is not - translated itself. Characters in the `context` parameter are not counted toward billing. + translated itself. Characters in the `context` parameter are not counted toward billing. See the [API documentation][api-docs-context-param] for more information and example usage. +- `setCustomInstructions()`: an array of instructions to customize the translation behavior. + Up to 10 custom instructions can be specified, each with a maximum of 300 characters. + Important: The target language must be `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `zh` + or any variants of these languages. + Note: Any request with the custom instructions parameter enabled will use the + `quality_optimized` model type as the default. Requests combining custom instructions + and `model_type: latency_optimized` will be rejected. - `model_type`: specifies the type of translation model to use, options are: - `'quality_optimized'`: use a translation model that maximizes translation quality, at the cost of response time. This option may be unavailable for some language pairs. diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index c2d0b23..8fe6603 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -26,6 +26,7 @@ public class TextTranslationOptions extends BaseRequestOptions { private Iterable ignoreTags; private Iterable nonSplittingTags; private Iterable splittingTags; + private Iterable customInstructions; /** * Sets whether translations should lean toward formal or informal language. This option is only @@ -175,6 +176,15 @@ public TextTranslationOptions setSplittingTags(Iterable splittingTags) { return this; } + /** + * Sets custom instructions to influence translations. By default, this value is null + * and no custom instructions are used. + */ + public TextTranslationOptions setCustomInstructions(Iterable customInstructions) { + this.customInstructions = customInstructions; + return this; + } + /** Gets the current formality setting. */ public Formality getFormality() { return formality; @@ -234,4 +244,9 @@ public Iterable getNonSplittingTags() { public Iterable getSplittingTags() { return splittingTags; } + + /** Gets the current custom instructions list. */ + public Iterable getCustomInstructions() { + return customInstructions; + } } diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index cbb21d4..9696a6c 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -849,6 +849,11 @@ private static ArrayList> createHttpParams( if (options.getStyleId() != null) { params.add(new KeyValuePair<>("style_id", options.getStyleId())); } + if (options.getCustomInstructions() != null) { + for (String instruction : options.getCustomInstructions()) { + params.add(new KeyValuePair<>("custom_instructions", instruction)); + } + } addExtraBodyParameters(params, options.getExtraBodyParameters()); } return params; diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index 1a7ee31..274ba63 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -349,4 +349,27 @@ void testExtraBodyParams() throws DeepLException, InterruptedException { Assertions.assertEquals("en", result.getDetectedSourceLanguage()); Assertions.assertEquals(exampleText.get("en").length(), result.getBilledCharacters()); } + + @Test + void testCustomInstructions() throws DeepLException, InterruptedException { + Translator translator = createTranslator(); + String text = "Hello world. I am testing if custom instructions are working correctly."; + + TextResult resultWithCustomInstructions = + translator.translateText( + text, + null, + "de", + new TextTranslationOptions() + .setCustomInstructions(Arrays.asList("Use informal language", "Be concise"))); + + TextResult resultWithoutCustomInstructions = translator.translateText(text, null, "de"); + + Assertions.assertNotNull(resultWithCustomInstructions.getText()); + Assertions.assertEquals("en", resultWithCustomInstructions.getDetectedSourceLanguage()); + if (!isMockServer) { + Assertions.assertFalse( + resultWithCustomInstructions.getText().equals(resultWithoutCustomInstructions.getText())); + } + } } From b0b941a191571143c017293a544c5e8e051c25d3 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Wed, 3 Dec 2025 00:11:49 -0500 Subject: [PATCH 102/121] docs: Bump version to 1.13.0 --- .bumpversion.toml | 2 +- CHANGELOG.md | 14 +++++++++++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- .../src/main/java/com/deepl/api/Translator.java | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 1375c12..f3dc246 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.12.0" +current_version = "1.13.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec04a0..7276a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.13.0] - 2025-12-03 +### Added +- Added `setCustomInstructions()` method to `TextTranslationOptions` to + customize translation behavior with up to 10 instructions (max 300 characters + each). Only supported for target languages: `de`, `en`, `es`, `fr`, `it`, + `ja`, `ko`, `zh` and their variants. + + Note: using the custom instructions parameter will use `quality_optimized` + model type as the default. Requests combining custom instructions and the + `latency_optimized` model type will be rejected. + ## [1.12.0] - 2025-11-12 ### Added - Added support for the `GET /v3/style_rules` endpoint in the client library, the @@ -178,7 +189,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2022-09-08 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.12.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.13.0...HEAD +[1.13.0]: https://github.com/DeepLcom/deepl-java/compare/v1.12.0...v1.13.0 [1.12.0]: https://github.com/DeepLcom/deepl-java/compare/v1.11.0...v1.12.0 [1.11.0]: https://github.com/DeepLcom/deepl-java/compare/v1.10.3...v1.11.0 [1.10.3]: https://github.com/DeepLcom/deepl-java/compare/v1.10.2...v1.10.3 diff --git a/README.md b/README.md index 236fe6d..1a84f34 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.12.0" +implementation "com.deepl.api:deepl-java:1.13.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.12.0 + 1.13.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index d6650ce..1986c50 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "com.deepl.api" -version = "1.12.0" +version = "1.13.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 9696a6c..782b674 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.12.0"); + sb.append("deepl-java/1.13.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 71cc3f4e4eaeb0c5c9886f37b458fe6773896298 Mon Sep 17 00:00:00 2001 From: Jason Gardella Date: Wed, 10 Dec 2025 15:11:38 -0500 Subject: [PATCH 103/121] feat: Add tag_handling_version parameter to translate_text --- CHANGELOG.md | 2 ++ README.md | 1 + .../com/deepl/api/TextTranslationOptions.java | 15 +++++++++ .../main/java/com/deepl/api/Translator.java | 3 ++ .../java/com/deepl/api/TranslateTextTest.java | 32 +++++++++++++++++++ 5 files changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7276a18..608e8d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added `setTagHandlingVersion()` method to `TextTranslationOptions` to specify which version of the tag handling algorithm to use. Options are `v1` and `v2`. ## [1.13.0] - 2025-12-03 ### Added diff --git a/README.md b/README.md index 1a84f34..fc10e2d 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ a `TextTranslationOptions`, with the following setters: cost of translation quality. - `setTagHandling()`: type of tags to parse before translation, options are `"html"` and `"xml"`. +- `setTagHandlingVersion()`: specifies which version of the tag handling algorithm to use. Options are `"v1"` and `"v2"`. The following options are only used if `setTagHandling()` is set to `'xml'`: diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index 8fe6603..50a7c07 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -21,6 +21,7 @@ public class TextTranslationOptions extends BaseRequestOptions { private boolean preserveFormatting = false; private String context; private String tagHandling; + private String tagHandlingVersion; private String modelType; private boolean outlineDetection = true; private Iterable ignoreTags; @@ -126,6 +127,15 @@ public TextTranslationOptions setTagHandling(String tagHandling) { return this; } + /** + * Sets the version of tag handling algorithm to use. By default, this value is null + * and the API default is used. + */ + public TextTranslationOptions setTagHandlingVersion(String tagHandlingVersion) { + this.tagHandlingVersion = tagHandlingVersion; + return this; + } + /** * Set the type of model to use for a text translation. Currently supported values: * "quality_optimized" use a translation model that maximizes translation quality, at the @@ -225,6 +235,11 @@ public String getTagHandling() { return tagHandling; } + /** Gets the current tag handling version. */ + public String getTagHandlingVersion() { + return tagHandlingVersion; + } + /** Gets the current outline detection setting. */ public boolean isOutlineDetection() { return outlineDetection; diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 782b674..127b775 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -833,6 +833,9 @@ private static ArrayList> createHttpParams( if (options.getTagHandling() != null) { params.add(new KeyValuePair<>("tag_handling", options.getTagHandling())); } + if (options.getTagHandlingVersion() != null) { + params.add(new KeyValuePair<>("tag_handling_version", options.getTagHandlingVersion())); + } if (!options.isOutlineDetection()) { params.add(new KeyValuePair<>("outline_detection", "0")); } diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index 274ba63..8f3fe06 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -299,6 +299,38 @@ void testTagHandlingHTML() throws DeepLException, InterruptedException { } } + @Test + void testTagHandlingVersionV1() throws DeepLException, InterruptedException { + Translator translator = createTranslator(); + String text = "

Hello world

"; + + TextResult result = + translator.translateText( + text, + null, + "de", + new TextTranslationOptions().setTagHandling("html").setTagHandlingVersion("v1")); + Assertions.assertNotNull(result); + Assertions.assertNotNull(result.getText()); + Assertions.assertFalse(result.getText().isEmpty()); + } + + @Test + void testTagHandlingVersionV2() throws DeepLException, InterruptedException { + Translator translator = createTranslator(); + String text = "

Hello world

"; + + TextResult result = + translator.translateText( + text, + null, + "de", + new TextTranslationOptions().setTagHandling("html").setTagHandlingVersion("v2")); + Assertions.assertNotNull(result); + Assertions.assertNotNull(result.getText()); + Assertions.assertFalse(result.getText().isEmpty()); + } + @Test void testEmptyText() { Translator translator = createTranslator(); From e91115a0cee340d8770210352bc08144dce1fbe8 Mon Sep 17 00:00:00 2001 From: Jason Gardella Date: Wed, 10 Dec 2025 15:55:41 -0500 Subject: [PATCH 104/121] Bump version to 1.14.0 --- .bumpversion.toml | 2 +- CHANGELOG.md | 5 ++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index f3dc246..d825e8f 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.13.0" +current_version = "1.14.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 608e8d3..a85e579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [1.14.0] - 2025-12-10 ### Added - Added `setTagHandlingVersion()` method to `TextTranslationOptions` to specify which version of the tag handling algorithm to use. Options are `v1` and `v2`. @@ -191,7 +193,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2022-09-08 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.13.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.14.0...HEAD +[1.14.0]: https://github.com/DeepLcom/deepl-java/compare/v1.13.0...v1.14.0 [1.13.0]: https://github.com/DeepLcom/deepl-java/compare/v1.12.0...v1.13.0 [1.12.0]: https://github.com/DeepLcom/deepl-java/compare/v1.11.0...v1.12.0 [1.11.0]: https://github.com/DeepLcom/deepl-java/compare/v1.10.3...v1.11.0 diff --git a/README.md b/README.md index fc10e2d..70cb10e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.13.0" +implementation "com.deepl.api:deepl-java:1.14.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.13.0 + 1.14.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 1986c50..b66810e 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "com.deepl.api" -version = "1.13.0" +version = "1.14.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 127b775..5c12f78 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.13.0"); + sb.append("deepl-java/1.14.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From ada4b3f6d9d765062ce3f407eed92431371c8671 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Tue, 3 Feb 2026 14:41:08 -0500 Subject: [PATCH 105/121] test: Fix tag handling and model type tests --- .../test/java/com/deepl/api/GeneralTest.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 9d22ed1..833e715 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -12,8 +12,8 @@ import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; import org.mockito.Mockito; @@ -60,13 +60,8 @@ void testExampleTranslation() throws DeepLException, InterruptedException { } @ParameterizedTest - @CsvSource({ - "quality_optimized,quality_optimized", - "prefer_quality_optimized,quality_optimized", - "latency_optimized,latency_optimized" - }) - void testModelType(String modelTypeArg, String expectedModelType) - throws DeepLException, InterruptedException { + @ValueSource(strings = {"quality_optimized", "prefer_quality_optimized", "latency_optimized"}) + void testModelType(String modelTypeArg) throws DeepLException, InterruptedException { Translator translator = createTranslator(); String sourceLang = "de"; TextResult result = @@ -75,7 +70,7 @@ void testModelType(String modelTypeArg, String expectedModelType) sourceLang, "en-US", new TextTranslationOptions().setModelType(modelTypeArg)); - Assertions.assertEquals(expectedModelType, result.getModelTypeUsed()); + Assertions.assertNotNull(result.getModelTypeUsed()); } @Test @@ -97,7 +92,9 @@ void testMixedDirectionText() throws DeepLException, InterruptedException { new TextTranslationOptions().setTagHandling("xml").setIgnoreTags(Arrays.asList("xml")); String arIgnorePart = "يجب تجاهل هذا الجزء."; String enSentenceWithArIgnorePart = - "

This is a short sentence. " + arIgnorePart + " This is another sentence."; + "

This is a short sentence.

" + + arIgnorePart + + " This is another sentence."; String enIgnorePart = "This part should be ignored."; String arSentenceWithEnIgnorePart = "

هذه جملة قصيرة. " + enIgnorePart + "هذه جملة أخرى.

"; From 290842e804c0f3d7f5d8adadc773c38520eb56b9 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Tue, 3 Feb 2026 14:41:19 -0500 Subject: [PATCH 106/121] docs: Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a85e579..b790430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Updated `testModelType()` to just check if the `model_type_used` is non-null if the `model_type` is specified in the request +- Updated `testMixedDirectionText()` to add a missing `

` tag ## [1.14.0] - 2025-12-10 ### Added From c4221dc379478740b3c4c86059650cc80ac3cdb0 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Thu, 8 Jan 2026 14:07:40 -0500 Subject: [PATCH 107/121] chore: Remove confusing check serverUrl 404 error message --- CHANGELOG.md | 1 + deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b790430..0487366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated `testModelType()` to just check if the `model_type_used` is non-null if the `model_type` is specified in the request - Updated `testMixedDirectionText()` to add a missing `

` tag +- Improved `NotFoundException` error message by removing the misleading "check server_url" suggestion. ## [1.14.0] - 2025-12-10 ### Added diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 5c12f78..cbeb37f 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -1063,7 +1063,7 @@ protected void checkResponse( if (usingGlossary) { throw new GlossaryNotFoundException("Glossary not found" + messageSuffix); } else { - throw new NotFoundException("Not found, check serverUrl" + messageSuffix); + throw new NotFoundException("Not found" + messageSuffix); } case 429: throw new TooManyRequestsException( From 46a6b63d656fcbc1819221bc1dc502ebd1545361 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Sun, 15 Mar 2026 20:31:28 -0400 Subject: [PATCH 108/121] test: Default formality is automatic and not always formal --- deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index 8f3fe06..f97153f 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -149,6 +149,7 @@ void testFormality() throws DeepLException, InterruptedException { Assertions.assertTrue(result.getText().contains("dir")); } + // Default formality is automatic, so the output may be either formal or informal result = translator.translateText( "How are you?", @@ -156,7 +157,7 @@ void testFormality() throws DeepLException, InterruptedException { "de", new TextTranslationOptions().setFormality(Formality.Default)); if (!isMockServer) { - Assertions.assertTrue(result.getText().contains("Ihnen")); + Assertions.assertTrue(result.getText().contains("Ihnen") || result.getText().contains("dir")); } result = From c937ef1400c50b45c7084a8fcd818ceb84974a78 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Sun, 15 Mar 2026 22:41:58 -0400 Subject: [PATCH 109/121] docs: Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0487366..a5c63b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- Updated formality tests to accept either formal or informal output when using default formality, + since the default formality is automatic. - Updated `testModelType()` to just check if the `model_type_used` is non-null if the `model_type` is specified in the request - Updated `testMixedDirectionText()` to add a missing `

` tag - Improved `NotFoundException` error message by removing the misleading "check server_url" suggestion. From 207736ed1a3e1d85c08798c6be7fa2e748bdac01 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Sun, 15 Mar 2026 20:03:20 -0400 Subject: [PATCH 110/121] feat: Add support for sending JSON-encoded API requests --- .../main/java/com/deepl/api/DeepLClient.java | 12 +++- .../java/com/deepl/api/HttpClientWrapper.java | 56 +++++++++++++++++-- .../java/com/deepl/api/http/HttpContent.java | 7 +++ .../deepl/api/MultilingualGlossaryTest.java | 21 +++++++ 4 files changed, 89 insertions(+), 7 deletions(-) diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index d19d208..662f481 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -577,9 +577,15 @@ public MultilingualGlossaryInfo updateMultilingualGlossaryName(String glossaryId ArrayList> bodyParams = new ArrayList<>(); bodyParams.add(new KeyValuePair<>("name", name)); String relativeUrl = String.format("/v3/glossaries/%s", glossaryId); - HttpResponse response = httpClientWrapper.sendPatchRequestWithBackoff(relativeUrl, bodyParams); - checkResponse(response, false, true); - return jsonParser.parseMultilingualGlossaryInfo(response.getBody()); + try { + HttpResponse response = + httpClientWrapper.sendPatchRequestWithBackoff(relativeUrl, bodyParams); + checkResponse(response, false, true); + return jsonParser.parseMultilingualGlossaryInfo(response.getBody()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new DeepLException("Request was interrupted", e); + } } /** diff --git a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java index 75d583d..9b3c2a5 100644 --- a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java +++ b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java @@ -9,10 +9,12 @@ import java.net.*; import java.time.*; import java.util.*; +import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpPatch; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.jetbrains.annotations.*; @@ -83,15 +85,61 @@ public HttpResponse sendPutRequestWithBackoff( return sendRequestWithBackoff(PUT, relativeUrl, content).toStringResponse(); } + public HttpResponse sendJsonRequestWithBackoff(String relativeUrl, String jsonBody) + throws InterruptedException, DeepLException { + HttpContent content = HttpContent.buildJsonContent(jsonBody); + return sendRequestWithBackoff(POST, relativeUrl, content).toStringResponse(); + } + + public HttpResponse sendJsonRequestWithBackoff(String method, String relativeUrl, String jsonBody) + throws InterruptedException, DeepLException { + HttpContent content = HttpContent.buildJsonContent(jsonBody); + return sendRequestWithBackoff(method, relativeUrl, content).toStringResponse(); + } + + public HttpResponse sendJsonPatchRequestWithBackoff(String relativeUrl, String jsonBody) + throws InterruptedException, DeepLException { + return sendPatchRequestWithBackoff(relativeUrl, HttpContent.buildJsonContent(jsonBody)); + } + public HttpResponse sendPatchRequestWithBackoff( String relativeUrl, @Nullable Iterable> params) - throws DeepLException { - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpContent content = HttpContent.buildFormURLEncodedContent(params); + throws InterruptedException, DeepLException { + HttpContent content = HttpContent.buildFormURLEncodedContent(params); + return sendPatchRequestWithBackoff(relativeUrl, content); + } + + private HttpResponse sendPatchRequestWithBackoff(String relativeUrl, HttpContent content) + throws InterruptedException, DeepLException { + BackoffTimer backoffTimer = new BackoffTimer(this.minTimeout); + while (true) { + try { + HttpResponse response = sendPatchRequest(relativeUrl, content, backoffTimer); + if (backoffTimer.getNumRetries() >= this.maxRetries) { + return response; + } else if (response.getCode() != 429 && response.getCode() < 500) { + return response; + } + } catch (ConnectionException exception) { + if (!exception.getShouldRetry() || backoffTimer.getNumRetries() >= this.maxRetries) { + throw exception; + } + } + backoffTimer.sleepUntilRetry(); + } + } + + private HttpResponse sendPatchRequest( + String relativeUrl, HttpContent content, BackoffTimer backoffTimer) throws DeepLException { + HttpClientBuilder builder = HttpClients.custom(); + if (proxy != null) { + InetSocketAddress addr = (InetSocketAddress) proxy.address(); + builder.setProxy(new HttpHost(addr.getHostName(), addr.getPort())); + } + try (CloseableHttpClient httpClient = builder.build()) { HttpPatch request = new HttpPatch(serverUrl + relativeUrl); // Set timeouts - BackoffTimer backoffTimer = new BackoffTimer(this.minTimeout); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout((int) backoffTimer.getTimeoutMillis()) diff --git a/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java index 992e5e7..bb1befe 100644 --- a/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java +++ b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java @@ -52,6 +52,13 @@ private static String urlEncode(String value) throws DeepLException { } } + public static HttpContent buildJsonContent(String jsonBody) { + if (jsonBody == null) { + throw new IllegalArgumentException("jsonBody must not be null"); + } + return new HttpContent("application/json", jsonBody.getBytes(StandardCharsets.UTF_8)); + } + public static HttpContent buildMultipartFormDataContent( Iterable> params) throws Exception { String boundary = UUID.randomUUID().toString(); diff --git a/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java index b404c62..45524e6 100644 --- a/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java +++ b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java @@ -482,6 +482,27 @@ void testGlossaryUpdateName() throws Exception { } } + @Test + void testGlossaryUpdateNameWithSpecialCharsIsProperlyEncoded() throws Exception { + DeepLClient deepLClient = createDeepLClient(); + try (MultilingualGlossaryCleanupUtility cleanup = + new MultilingualGlossaryCleanupUtility(deepLClient)) { + GlossaryEntries entries = new GlossaryEntries(); + entries.put("key1", "value1"); + List glossaryDicts = + Arrays.asList(new MultilingualGlossaryDictionaryEntries(sourceLang, targetLang, entries)); + MultilingualGlossaryInfo glossary = + deepLClient.createMultilingualGlossary(cleanup.getGlossaryName(), glossaryDicts); + + String nameWithSpecialChars = "Name with special chars: é &foo=bar \"quoted\""; + MultilingualGlossaryInfo updatedGlossary = + deepLClient.updateMultilingualGlossaryName( + glossary.getGlossaryId(), nameWithSpecialChars); + + Assertions.assertEquals(nameWithSpecialChars, updatedGlossary.getName()); + } + } + @Test void testGlossaryTranslateTextSentence() throws Exception { DeepLClient deepLClient = createDeepLClient(); From 2dad1ab5d6d9217060767490be2dfcd76ec418b3 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Mon, 16 Mar 2026 22:38:54 -0400 Subject: [PATCH 111/121] feat: Add style rules CRUD endpoints --- README.md | 120 +++++++-- .../java/com/deepl/api/CustomInstruction.java | 26 +- .../main/java/com/deepl/api/DeepLClient.java | 238 ++++++++++++++++++ .../java/com/deepl/api/parsing/Parser.java | 12 + .../java/com/deepl/api/StyleRuleTest.java | 127 ++++++++++ 5 files changed, 505 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 70cb10e..cc7ae23 100644 --- a/README.md +++ b/README.md @@ -657,42 +657,128 @@ Style rules allow you to customize your translations using a managed, shared lis of rules for style, formatting, and more. Multiple style rules can be stored with your account, each with a user-specified name and a uniquely-assigned ID. -#### Creating and managing style rules +#### Creating a style rule -Currently style rules must be created and managed in the DeepL UI via -https://www.deepl.com/en/custom-rules. Full CRUD functionality via the APIs will -come shortly. +Use `createStyleRule()` to create a new style rule. You must specify a name and +language code. You may optionally provide configured rules and custom +instructions: -#### Listing all style rules +```java +class Example { // Continuing class Example from above + public void createStyleRuleExample() throws Exception { + // Create a simple style rule + StyleRuleInfo rule = client.createStyleRule("My Style", "en", null, null); + System.out.println("Created: " + rule.getStyleId()); + + // Create with custom instructions + List instructions = new ArrayList<>(); + instructions.add(new CustomInstruction("Formal tone", "Use formal language", null)); + StyleRuleInfo ruleWithInstructions = client.createStyleRule( + "Formal Style", "en", null, instructions); + } +} +``` -`getAllStyleRules()` returns a list of `StyleRuleInfo` objects -corresponding to all of your stored style rules. The method accepts optional -parameters: `page` (page number for pagination, 0-indexed), `pageSize` (number -of items per page), and `detailed` (whether to include detailed configuration -rules in the `configuredRules` property). +#### Retrieving and listing style rules + +`getStyleRule()` retrieves a single style rule by its ID. `getAllStyleRules()` +returns a list of `StyleRuleInfo` objects corresponding to all of your stored +style rules. The method accepts optional parameters: `page` (page number for +pagination, 0-indexed), `pageSize` (number of items per page), and `detailed`. +When `true`, the response includes `configuredRules` and `customInstructions` +for each style rule. When `false` (default), these fields are omitted for faster +responses. ```java class Example { // Continuing class Example from above - public void styleRulesExample() throws Exception { + public void listStyleRulesExample() throws Exception { + // Get a single style rule by ID + StyleRuleInfo rule = client.getStyleRule("dca2e053-8ae5-45e6-a0d2-881156e7f4e4"); + System.out.println(rule.getName()); + // Get all style rules List styleRules = client.getAllStyleRules(); - for (StyleRuleInfo rule : styleRules) { - System.out.println(String.format("%s (%s)", rule.getName(), rule.getStyleId())); + for (StyleRuleInfo r : styleRules) { + System.out.println(String.format("%s (%s)", r.getName(), r.getStyleId())); } // Get style rules with detailed configuration List styleRulesDetailed = client.getAllStyleRules(null, null, true); - for (StyleRuleInfo rule : styleRulesDetailed) { - if (rule.getConfiguredRules() != null && rule.getConfiguredRules().getNumbers() != null) { + for (StyleRuleInfo r : styleRulesDetailed) { + if (r.getConfiguredRules() != null && r.getConfiguredRules().getNumbers() != null) { System.out.println(String.format("Number formatting rules: %s", - String.join(", ", rule.getConfiguredRules().getNumbers().keySet()))); + String.join(", ", r.getConfiguredRules().getNumbers().keySet()))); } } } } ``` -#### Using a stored style rule +#### Updating a style rule + +Use `updateStyleRuleName()` to rename a style rule, and +`updateStyleRuleConfiguredRules()` to replace its configured rules: + +```java +class Example { // Continuing class Example from above + public void updateStyleRuleExample() throws Exception { + String styleId = "dca2e053-8ae5-45e6-a0d2-881156e7f4e4"; + + // Update the name + StyleRuleInfo updated = client.updateStyleRuleName(styleId, "New Name"); + System.out.println("Updated name: " + updated.getName()); + + // Update configured rules + ConfiguredRules configuredRules = new ConfiguredRules(); + StyleRuleInfo updatedRules = client.updateStyleRuleConfiguredRules( + styleId, configuredRules); + } +} +``` + +#### Managing custom instructions + +Custom instructions can be created, retrieved, updated, and deleted individually +within a style rule: + +```java +class Example { // Continuing class Example from above + public void customInstructionsExample() throws Exception { + String styleId = "dca2e053-8ae5-45e6-a0d2-881156e7f4e4"; + + // Create a custom instruction + CustomInstruction instruction = client.createStyleRuleCustomInstruction( + styleId, "Formal tone", "Always use formal language", null); + String instructionId = instruction.getId(); + + // Retrieve the custom instruction + CustomInstruction retrieved = client.getStyleRuleCustomInstruction( + styleId, instructionId); + System.out.println(retrieved.getLabel()); + + // Update the custom instruction + CustomInstruction updated = client.updateStyleRuleCustomInstruction( + styleId, instructionId, "Updated label", "Updated prompt", null); + + // Delete the custom instruction + client.deleteStyleRuleCustomInstruction(styleId, instructionId); + } +} +``` + +#### Deleting a style rule + +Use `deleteStyleRule()` to permanently remove a style rule from your account: + +```java +class Example { // Continuing class Example from above + public void deleteStyleRuleExample() throws Exception { + client.deleteStyleRule("dca2e053-8ae5-45e6-a0d2-881156e7f4e4"); + } +} +``` + +#### Using a stored style rule in translations You can use a stored style rule for text translation by setting the `styleRule` argument to either the style rule ID or `StyleRuleInfo` object: diff --git a/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java b/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java index 568cbe7..552a857 100644 --- a/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java +++ b/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java @@ -8,6 +8,10 @@ /** Custom instruction for a style rule. */ public class CustomInstruction { + @SerializedName(value = "id") + @Nullable + private final String id; + @SerializedName(value = "label") private final String label; @@ -21,16 +25,36 @@ public class CustomInstruction { /** * Initializes a new {@link CustomInstruction} containing a custom instruction for a style rule. * + * @param id Optional unique identifier for the custom instruction. * @param label Label for the custom instruction. * @param prompt Prompt text for the custom instruction. * @param sourceLanguage Optional source language code for the custom instruction. */ - public CustomInstruction(String label, String prompt, @Nullable String sourceLanguage) { + public CustomInstruction( + @Nullable String id, String label, String prompt, @Nullable String sourceLanguage) { + this.id = id; this.label = label; this.prompt = prompt; this.sourceLanguage = sourceLanguage; } + /** + * Initializes a new {@link CustomInstruction} containing a custom instruction for a style rule. + * + * @param label Label for the custom instruction. + * @param prompt Prompt text for the custom instruction. + * @param sourceLanguage Optional source language code for the custom instruction. + */ + public CustomInstruction(String label, String prompt, @Nullable String sourceLanguage) { + this(null, label, prompt, sourceLanguage); + } + + /** @return Optional unique identifier for the custom instruction, or {@code null} if not set. */ + @Nullable + public String getId() { + return id; + } + /** @return Label for the custom instruction. */ public String getLabel() { return label; diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 662f481..74d14ff 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -10,7 +10,9 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.jetbrains.annotations.Nullable; public class DeepLClient extends Translator { @@ -137,6 +139,7 @@ public MultilingualGlossaryInfo createMultilingualGlossaryFromCsv( throws DeepLException, IllegalArgumentException, InterruptedException { return createGlossaryFromCsvInternal(name, sourceLanguageCode, targetLanguageCode, csvFile); } + /** * Creates a glossary in your DeepL account with the specified details and returns a {@link * MultilingualGlossaryInfo} object with details about the newly created glossary. The glossary @@ -800,6 +803,241 @@ public List getAllStyleRules() throws DeepLException, Interrupted return getAllStyleRules(null, null, null); } + /** + * Creates a new style rule with the specified details and returns a {@link StyleRuleInfo} object + * with details about the newly created style rule. + * + * @param name User-defined name for the style rule. + * @param language Language code for the style rule (e.g. "en", "de"). + * @param configuredRules Optional configured rules for the style rule. + * @param customInstructions Optional list of custom instructions for the style rule. + * @return {@link StyleRuleInfo} object with details about the newly created style rule. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public StyleRuleInfo createStyleRule( + String name, + String language, + @Nullable ConfiguredRules configuredRules, + @Nullable List customInstructions) + throws DeepLException, InterruptedException { + validateParameter("name", name); + validateParameter("language", language); + Map requestData = new HashMap<>(); + requestData.put("name", name); + requestData.put("language", language); + if (configuredRules != null) { + requestData.put("configured_rules", configuredRules); + } + if (customInstructions != null) { + requestData.put("custom_instructions", customInstructions); + } + String jsonBody = jsonParser.getGson().toJson(requestData); + HttpResponse response = + httpClientWrapper.sendJsonRequestWithBackoff("/v3/style_rules", jsonBody); + checkResponse(response, false, false); + return jsonParser.parseStyleRuleInfo(response.getBody()); + } + + /** + * Retrieves information about the style rule with the specified ID and returns a {@link + * StyleRuleInfo} object containing details. + * + * @param styleId ID of the style rule to retrieve. + * @return {@link StyleRuleInfo} object with details about the specified style rule. + * @throws NotFoundException If no style rule with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public StyleRuleInfo getStyleRule(String styleId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("styleId", styleId); + String relativeUrl = String.format("/v3/style_rules/%s", styleId); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + return jsonParser.parseStyleRuleInfo(response.getBody()); + } + + /** + * Updates the name of the style rule with the specified ID and returns the updated {@link + * StyleRuleInfo} object. + * + * @param styleId ID of the style rule to update. + * @param name New name for the style rule. + * @return {@link StyleRuleInfo} object with updated details about the style rule. + * @throws NotFoundException If no style rule with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public StyleRuleInfo updateStyleRuleName(String styleId, String name) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("styleId", styleId); + validateParameter("name", name); + String relativeUrl = String.format("/v3/style_rules/%s", styleId); + Map requestData = new HashMap<>(); + requestData.put("name", name); + String jsonBody = jsonParser.getGson().toJson(requestData); + HttpResponse response = + httpClientWrapper.sendJsonPatchRequestWithBackoff(relativeUrl, jsonBody); + checkResponse(response, false, false); + return jsonParser.parseStyleRuleInfo(response.getBody()); + } + + /** + * Deletes the style rule with the specified ID. + * + * @param styleId ID of the style rule to delete. + * @throws NotFoundException If no style rule with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public void deleteStyleRule(String styleId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("styleId", styleId); + String relativeUrl = String.format("/v3/style_rules/%s", styleId); + HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + } + + /** + * Replaces the configured rules of the style rule with the specified ID and returns the updated + * {@link StyleRuleInfo} object. + * + * @param styleId ID of the style rule to update. + * @param configuredRules The new configured rules to set for the style rule. + * @return {@link StyleRuleInfo} object with updated details about the style rule. + * @throws NotFoundException If no style rule with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public StyleRuleInfo updateStyleRuleConfiguredRules( + String styleId, ConfiguredRules configuredRules) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("styleId", styleId); + if (configuredRules == null) { + throw new IllegalArgumentException("configuredRules must not be null"); + } + String relativeUrl = String.format("/v3/style_rules/%s/configured_rules", styleId); + String jsonBody = jsonParser.getGson().toJson(configuredRules); + HttpResponse response = + httpClientWrapper.sendJsonRequestWithBackoff("PUT", relativeUrl, jsonBody); + checkResponse(response, false, false); + return jsonParser.parseStyleRuleInfo(response.getBody()); + } + + /** + * Creates a new custom instruction for the style rule with the specified ID and returns the + * created {@link CustomInstruction} object. + * + * @param styleId ID of the style rule to add the custom instruction to. + * @param label Label for the custom instruction. + * @param prompt Prompt text for the custom instruction. + * @param sourceLanguage Optional source language code for the custom instruction. + * @return {@link CustomInstruction} object with details about the newly created custom + * instruction. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public CustomInstruction createStyleRuleCustomInstruction( + String styleId, String label, String prompt, @Nullable String sourceLanguage) + throws DeepLException, InterruptedException { + validateParameter("styleId", styleId); + validateParameter("label", label); + validateParameter("prompt", prompt); + String relativeUrl = String.format("/v3/style_rules/%s/custom_instructions", styleId); + Map requestData = new HashMap<>(); + requestData.put("label", label); + requestData.put("prompt", prompt); + if (sourceLanguage != null) { + requestData.put("source_language", sourceLanguage); + } + String jsonBody = jsonParser.getGson().toJson(requestData); + HttpResponse response = httpClientWrapper.sendJsonRequestWithBackoff(relativeUrl, jsonBody); + checkResponse(response, false, false); + return jsonParser.parseCustomInstruction(response.getBody()); + } + + /** + * Retrieves information about the custom instruction with the specified ID within the style rule + * with the specified ID and returns a {@link CustomInstruction} object containing details. + * + * @param styleId ID of the style rule containing the custom instruction. + * @param instructionId ID of the custom instruction to retrieve. + * @return {@link CustomInstruction} object with details about the specified custom instruction. + * @throws NotFoundException If no style rule or custom instruction with the given IDs is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public CustomInstruction getStyleRuleCustomInstruction(String styleId, String instructionId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("styleId", styleId); + validateParameter("instructionId", instructionId); + String relativeUrl = + String.format("/v3/style_rules/%s/custom_instructions/%s", styleId, instructionId); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + return jsonParser.parseCustomInstruction(response.getBody()); + } + + /** + * Updates the custom instruction with the specified ID within the style rule with the specified + * ID and returns the updated {@link CustomInstruction} object. + * + * @param styleId ID of the style rule containing the custom instruction. + * @param instructionId ID of the custom instruction to update. + * @param label New label for the custom instruction. + * @param prompt New prompt text for the custom instruction. + * @param sourceLanguage Optional new source language code for the custom instruction. + * @return {@link CustomInstruction} object with updated details about the custom instruction. + * @throws NotFoundException If no style rule or custom instruction with the given IDs is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public CustomInstruction updateStyleRuleCustomInstruction( + String styleId, + String instructionId, + String label, + String prompt, + @Nullable String sourceLanguage) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("styleId", styleId); + validateParameter("instructionId", instructionId); + validateParameter("label", label); + validateParameter("prompt", prompt); + String relativeUrl = + String.format("/v3/style_rules/%s/custom_instructions/%s", styleId, instructionId); + Map requestData = new HashMap<>(); + requestData.put("label", label); + requestData.put("prompt", prompt); + if (sourceLanguage != null) { + requestData.put("source_language", sourceLanguage); + } + String jsonBody = jsonParser.getGson().toJson(requestData); + HttpResponse response = + httpClientWrapper.sendJsonRequestWithBackoff("PUT", relativeUrl, jsonBody); + checkResponse(response, false, false); + return jsonParser.parseCustomInstruction(response.getBody()); + } + + /** + * Deletes the custom instruction with the specified ID from the style rule with the specified ID. + * + * @param styleId ID of the style rule containing the custom instruction. + * @param instructionId ID of the custom instruction to delete. + * @throws NotFoundException If no style rule or custom instruction with the given IDs is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public void deleteStyleRuleCustomInstruction(String styleId, String instructionId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("styleId", styleId); + validateParameter("instructionId", instructionId); + String relativeUrl = + String.format("/v3/style_rules/%s/custom_instructions/%s", styleId, instructionId); + HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + } + /** Creates a glossary with given details. */ private MultilingualGlossaryInfo createGlossaryFromCsvInternal( String name, String sourceLanguageCode, String targetLanguageCode, String entries) diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index ebe99db..4adef4b 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -18,6 +18,10 @@ public class Parser { private final Gson gson; + public Gson getGson() { + return gson; + } + public Parser() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(TextResult.class, new TextResultDeserializer()); @@ -91,6 +95,14 @@ public List parseStyleRuleInfoList(String json) { return result.getStyleRules(); } + public StyleRuleInfo parseStyleRuleInfo(String json) { + return gson.fromJson(json, StyleRuleInfo.class); + } + + public CustomInstruction parseCustomInstruction(String json) { + return gson.fromJson(json, CustomInstruction.class); + } + public String parseErrorMessage(String json) { ErrorResponse response = gson.fromJson(json, ErrorResponse.class); diff --git a/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java index 35f3328..8a396b6 100644 --- a/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java +++ b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java @@ -3,7 +3,9 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.*; public class StyleRuleTest extends TestBase { @@ -40,6 +42,131 @@ void testGetAllStyleRulesWithoutDetailed() throws Exception { Assertions.assertNull(styleRules.get(0).getCustomInstructions()); } + @Test + void testStyleRuleCrud() throws Exception { + DeepLClient client = createDeepLClient(); + + // Create + StyleRuleInfo rule = client.createStyleRule("Test Rule", "en", null, null); + Assertions.assertNotNull(rule.getStyleId()); + Assertions.assertEquals("Test Rule", rule.getName()); + + String styleId = rule.getStyleId(); + + // Get + StyleRuleInfo retrieved = client.getStyleRule(styleId); + Assertions.assertEquals(styleId, retrieved.getStyleId()); + + // Update name + StyleRuleInfo updated = client.updateStyleRuleName(styleId, "Updated Name"); + Assertions.assertEquals("Updated Name", updated.getName()); + + // Update configured rules + Map datesAndTimes = new HashMap<>(); + datesAndTimes.put("calendar_era", "use_bc_and_ad"); + StyleRuleInfo configuredResult = + client.updateStyleRuleConfiguredRules( + styleId, new ConfiguredRules(datesAndTimes, null, null, null, null, null, null)); + Assertions.assertEquals(styleId, configuredResult.getStyleId()); + + // Create custom instruction + CustomInstruction instruction = + client.createStyleRuleCustomInstruction(styleId, "Test Label", "Test prompt", null); + Assertions.assertNotNull(instruction.getId()); + Assertions.assertEquals("Test Label", instruction.getLabel()); + + String instructionId = instruction.getId(); + + // Get custom instruction + CustomInstruction retrievedInstruction = + client.getStyleRuleCustomInstruction(styleId, instructionId); + Assertions.assertEquals("Test Label", retrievedInstruction.getLabel()); + + // Update custom instruction + CustomInstruction updatedInstruction = + client.updateStyleRuleCustomInstruction( + styleId, instructionId, "Updated Label", "Updated prompt", null); + Assertions.assertEquals("Updated Label", updatedInstruction.getLabel()); + + // Delete custom instruction + client.deleteStyleRuleCustomInstruction(styleId, instructionId); + + // Delete style rule + client.deleteStyleRule(styleId); + } + + @Test + void testStyleRuleValidation() throws Exception { + DeepLClient client = createDeepLClient(); + + // createStyleRule + Assertions.assertThrows( + IllegalArgumentException.class, () -> client.createStyleRule("", "en", null, null)); + Assertions.assertThrows( + IllegalArgumentException.class, () -> client.createStyleRule("Test", "", null, null)); + + // getStyleRule + Assertions.assertThrows(IllegalArgumentException.class, () -> client.getStyleRule("")); + + // updateStyleRuleName + Assertions.assertThrows( + IllegalArgumentException.class, () -> client.updateStyleRuleName("", "New Name")); + Assertions.assertThrows( + IllegalArgumentException.class, () -> client.updateStyleRuleName("some-id", "")); + + // deleteStyleRule + Assertions.assertThrows(IllegalArgumentException.class, () -> client.deleteStyleRule("")); + + // updateStyleRuleConfiguredRules + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + client.updateStyleRuleConfiguredRules( + "", new ConfiguredRules(null, null, null, null, null, null, null))); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.updateStyleRuleConfiguredRules("some-id", null)); + + // createStyleRuleCustomInstruction + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.createStyleRuleCustomInstruction("", "L", "P", null)); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.createStyleRuleCustomInstruction("some-id", "", "P", null)); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.createStyleRuleCustomInstruction("some-id", "L", "", null)); + + // getStyleRuleCustomInstruction + Assertions.assertThrows( + IllegalArgumentException.class, () -> client.getStyleRuleCustomInstruction("", "instr-id")); + Assertions.assertThrows( + IllegalArgumentException.class, () -> client.getStyleRuleCustomInstruction("some-id", "")); + + // updateStyleRuleCustomInstruction + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.updateStyleRuleCustomInstruction("", "instr-id", "L", "P", null)); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.updateStyleRuleCustomInstruction("some-id", "", "L", "P", null)); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.updateStyleRuleCustomInstruction("some-id", "instr-id", "", "P", null)); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.updateStyleRuleCustomInstruction("some-id", "instr-id", "L", "", null)); + + // deleteStyleRuleCustomInstruction + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.deleteStyleRuleCustomInstruction("", "instr-id")); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.deleteStyleRuleCustomInstruction("some-id", "")); + } + @Test void testTranslateTextWithStyleId() throws Exception { // Note: this test may use the mock server that will not translate the text From fbdee891348e3ae29dd233831e1a0d73a3ca400c Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Sun, 15 Mar 2026 21:06:39 -0400 Subject: [PATCH 112/121] docs: Update CHANGELOG --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c63b8..b4d37cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added JSON request body support to `HttpContent` and `HttpClientWrapper`, enabling + JSON-encoded API calls via `sendJsonRequestWithBackoff()` and `sendJsonPatchRequestWithBackoff()`. +- Added support for style rules CRUD endpoints in the `DeepLClient` class: + `createStyleRule()`, `getStyleRule()`, `updateStyleRuleName()`, + `updateStyleRuleConfiguredRules()`, and `deleteStyleRule()`. +- Added support for style rule custom instruction CRUD endpoints in the + `DeepLClient` class: `createStyleRuleCustomInstruction()`, + `getStyleRuleCustomInstruction()`, `updateStyleRuleCustomInstruction()`, + and `deleteStyleRuleCustomInstruction()`. + Please refer to the README for usage instructions. + ### Changed - Updated formality tests to accept either formal or informal output when using default formality, since the default formality is automatic. @@ -12,6 +24,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `testMixedDirectionText()` to add a missing `

` tag - Improved `NotFoundException` error message by removing the misleading "check server_url" suggestion. +### Fixed +- Fixed PATCH requests (`sendPatchRequestWithBackoff`) not retrying on 429/5xx errors despite + the method name promising backoff behavior. PATCH requests now use the same retry logic as all + other HTTP methods. + ## [1.14.0] - 2025-12-10 ### Added - Added `setTagHandlingVersion()` method to `TextTranslationOptions` to specify which version of the tag handling algorithm to use. Options are `v1` and `v2`. From 424aad4a91af8555aa9b7962e591f0290a09fc30 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Thu, 26 Mar 2026 15:19:49 -0400 Subject: [PATCH 113/121] docs: Bump version to 1.15.0 --- .bumpversion.toml | 2 +- CHANGELOG.md | 5 ++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index d825e8f..b2d3bcd 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.14.0" +current_version = "1.15.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index b4d37cb..566aa36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [1.15.0] - 2026-03-26 ### Added - Added JSON request body support to `HttpContent` and `HttpClientWrapper`, enabling JSON-encoded API calls via `sendJsonRequestWithBackoff()` and `sendJsonPatchRequestWithBackoff()`. @@ -216,7 +218,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2022-09-08 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.14.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.15.0...HEAD +[1.15.0]: https://github.com/DeepLcom/deepl-java/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/DeepLcom/deepl-java/compare/v1.13.0...v1.14.0 [1.13.0]: https://github.com/DeepLcom/deepl-java/compare/v1.12.0...v1.13.0 [1.12.0]: https://github.com/DeepLcom/deepl-java/compare/v1.11.0...v1.12.0 diff --git a/README.md b/README.md index cc7ae23..f9bd506 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.14.0" +implementation "com.deepl.api:deepl-java:1.15.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.14.0 + 1.15.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index b66810e..2629980 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "com.deepl.api" -version = "1.14.0" +version = "1.15.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index cbeb37f..da4ac9c 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.14.0"); + sb.append("deepl-java/1.15.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From cb043d767366ff9ef5498f0eae2b49be359e5905 Mon Sep 17 00:00:00 2001 From: MERGE_REQUEST_ROBOT_TOKEN Date: Wed, 1 Apr 2026 15:57:08 +0000 Subject: [PATCH 114/121] fix: Set explicit image for gitlab release job to avoid missing $HOME issue --- .gitlab-ci.yml | 1 + CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 81b5af6..2231304 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -234,6 +234,7 @@ publish_manual: gitlab release: stage: publish + image: registry.gitlab.com/gitlab-org/release-cli:latest extends: .create_gitlab_release rules: - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 566aa36..7e2b52d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Fixed GitLab release job using wrong image by explicitly setting `image: registry.gitlab.com/gitlab-org/release-cli:latest`. ## [1.15.0] - 2026-03-26 ### Added From 2816ca44f87e1465ac72b8294133973b672cda45 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Tue, 7 Apr 2026 12:47:52 -0400 Subject: [PATCH 115/121] feat: Add ability to text translate with a translation memory --- README.md | 65 +++++++++++++ .../main/java/com/deepl/api/DeepLClient.java | 58 ++++++++++++ .../com/deepl/api/TextTranslationOptions.java | 47 ++++++++++ .../com/deepl/api/TranslationMemoryInfo.java | 92 +++++++++++++++++++ .../main/java/com/deepl/api/Translator.java | 13 +++ .../java/com/deepl/api/parsing/Parser.java | 9 ++ .../TranslationMemoryListResponse.java | 20 ++++ .../com/deepl/api/TranslationMemoryTest.java | 61 ++++++++++++ 8 files changed, 365 insertions(+) create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryListResponse.java create mode 100644 deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java diff --git a/README.md b/README.md index f9bd506..9abdb8e 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,13 @@ a `TextTranslationOptions`, with the following setters: - `setStyleRule()`: specifies a style rule to use with translation, as a string containing the ID of the style rule, or a `StyleRuleInfo` object. - `setStyleId()` is also available, accepting a string containing the style rule ID. +- `setTranslationMemory()`: specifies a translation memory to use with translation, + as a `TranslationMemoryInfo` object. Sets the translation memory ID. + - `setTranslationMemoryId()` is also available, accepting a string containing + the translation memory ID. + - `setTranslationMemoryThreshold()` is also available, accepting an integer + from 0 to 100 to control the minimum matching percentage for translation + memory matches. We recommend a minimum threshold of 75%. - `setContext()`: specifies additional context to influence translations, that is not translated itself. Characters in the `context` parameter are not counted toward billing. See the [API documentation][api-docs-context-param] for more information and @@ -797,6 +804,64 @@ class Example { // Continuing class Example from above } ``` +### Translation Memories + +Translation memories store and reuse previously created translations, helping to +ensure consistency and reduce effort when translating similar or repeated content. + +#### Uploading and managing translation memories + +Currently translation memories must be uploaded and managed in the DeepL UI via +https://www.deepl.com/translation-memory. Full CRUD functionality via the APIs will +come shortly. + +#### Listing translation memories + +Use `listTranslationMemories()` to retrieve translation memories associated +with your account: + +```java +class Example { // Continuing class Example from above + public void listTranslationMemoriesExample() throws Exception { + List translationMemories = + client.listTranslationMemories(); + for (TranslationMemoryInfo tm : translationMemories) { + System.out.println(String.format("%s (%s)", + tm.getName(), tm.getTranslationMemoryId())); + } + } +} +``` + +#### Using a translation memory in translations + +You can use a translation memory for text translation by setting the translation +memory options in `TextTranslationOptions`: + +```java +class Example { // Continuing class Example from above + public void usingTranslationMemoryExample() throws Exception { + // Using the translation memory ID directly + TextTranslationOptions options = new TextTranslationOptions() + .setTranslationMemoryId("tm-123abc") + .setTranslationMemoryThreshold(80); + TextResult result = client.translateText( + "Hello, world!", null, "de", options); + System.out.println(result.getText()); + + // Or using a TranslationMemoryInfo object from listTranslationMemories() + List memories = client.listTranslationMemories(); + if (!memories.isEmpty()) { + TextTranslationOptions optionsFromInfo = new TextTranslationOptions() + .setTranslationMemory(memories.get(0)); + TextResult resultFromInfo = client.translateText( + "Hello, world!", null, "de", optionsFromInfo); + System.out.println(resultFromInfo.getText()); + } + } +} +``` + ### Checking account usage To check account usage, use the `getUsage()` function. diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 74d14ff..45e773b 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -803,6 +803,64 @@ public List getAllStyleRules() throws DeepLException, Interrupted return getAllStyleRules(null, null, null); } + /** + * Retrieves a list of translation memories available for the account associated with the DeepL + * API auth key. The maximum number of translation memories returned is controlled by pageSize + * (max 25). + * + * @param page Page number to retrieve (starting from 0), or null. + * @param pageSize Number of items per page, or null. + * @return List of {@link TranslationMemoryInfo} objects. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public List listTranslationMemories( + @Nullable Integer page, @Nullable Integer pageSize) + throws DeepLException, InterruptedException { + ArrayList> queryParams = new ArrayList<>(); + if (page != null) { + queryParams.add(new KeyValuePair<>("page", page.toString())); + } + if (pageSize != null) { + queryParams.add(new KeyValuePair<>("page_size", pageSize.toString())); + } + + String queryString = ""; + if (!queryParams.isEmpty()) { + StringBuilder sb = new StringBuilder("?"); + for (int i = 0; i < queryParams.size(); i++) { + if (i > 0) { + sb.append("&"); + } + KeyValuePair param = queryParams.get(i); + try { + sb.append(URLEncoder.encode(param.getKey(), StandardCharsets.UTF_8.name())) + .append("=") + .append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8.name())); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported", e); + } + } + queryString = sb.toString(); + } + + String relativeUrl = "/v3/translation_memories" + queryString; + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + return jsonParser.parseTranslationMemoryInfoList(response.getBody()); + } + + /** + * Functions the same as {@link DeepLClient#listTranslationMemories(Integer, Integer, Boolean)} + * but with default parameters (all null). + * + * @see DeepLClient#listTranslationMemories(Integer, Integer) + */ + public List listTranslationMemories() + throws DeepLException, InterruptedException { + return listTranslationMemories(null, null); + } + /** * Creates a new style rule with the specified details and returns a {@link StyleRuleInfo} object * with details about the newly created style rule. diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index 50a7c07..ef5d734 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -17,6 +17,8 @@ public class TextTranslationOptions extends BaseRequestOptions { private Formality formality; private String glossaryId; private String styleId; + private String translationMemoryId; + private Integer translationMemoryThreshold; private SentenceSplittingMode sentenceSplittingMode; private boolean preserveFormatting = false; private String context; @@ -85,6 +87,41 @@ public TextTranslationOptions setStyleRule(StyleRuleInfo styleRule) { return setStyleId(styleRule.getStyleId()); } + /** + * Sets the ID of a translation memory to use with the translation. By default, this value is + * null and no translation memory is used. + */ + public TextTranslationOptions setTranslationMemoryId(String translationMemoryId) { + this.translationMemoryId = translationMemoryId; + return this; + } + + /** + * Sets the translation memory to use with the translation. By default, this value is null + * and no translation memory is used. + */ + public TextTranslationOptions setTranslationMemory(TranslationMemoryInfo translationMemory) { + if (translationMemory == null) { + throw new IllegalArgumentException("translationMemory must not be null"); + } + return setTranslationMemoryId(translationMemory.getTranslationMemoryId()); + } + + /** + * Sets the threshold for translation memory matches. By default, this value is null + * and the API default threshold is used. Note: a translation memory ID must also be set via + * {@link #setTranslationMemoryId} or {@link #setTranslationMemory}, otherwise an error will be + * thrown at translation time. + */ + public TextTranslationOptions setTranslationMemoryThreshold(Integer translationMemoryThreshold) { + if (translationMemoryThreshold != null + && (translationMemoryThreshold < 0 || translationMemoryThreshold > 100)) { + throw new IllegalArgumentException("translationMemoryThreshold must be between 0 and 100"); + } + this.translationMemoryThreshold = translationMemoryThreshold; + return this; + } + /** * Specifies additional context to influence translations, that is not translated itself. * Characters in the `context` parameter are not counted toward billing. See the API documentation @@ -210,6 +247,16 @@ public String getStyleId() { return styleId; } + /** Gets the current translation memory ID. */ + public String getTranslationMemoryId() { + return translationMemoryId; + } + + /** Gets the current translation memory threshold. */ + public Integer getTranslationMemoryThreshold() { + return translationMemoryThreshold; + } + /** Gets the current sentence splitting mode. */ public SentenceSplittingMode getSentenceSplittingMode() { return sentenceSplittingMode; diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java new file mode 100644 index 0000000..c1e9149 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java @@ -0,0 +1,92 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; + +/** Information about a translation memory. */ +public class TranslationMemoryInfo { + @SerializedName(value = "translation_memory_id") + private final String translationMemoryId; + + @SerializedName(value = "name") + private final String name; + + @SerializedName(value = "source_language") + private final String sourceLanguage; + + @SerializedName(value = "target_languages") + private final List targetLanguages; + + @SerializedName(value = "segment_count") + private final int segmentCount; + + /** + * Initializes a new {@link TranslationMemoryInfo} containing information about a translation + * memory. + * + * @param translationMemoryId Unique ID assigned to the translation memory. + * @param name User-defined name assigned to the translation memory. + * @param sourceLanguage Source language code for the translation memory. + * @param targetLanguages List of target language codes for the translation memory. + * @param segmentCount Number of segments in the translation memory. + */ + public TranslationMemoryInfo( + String translationMemoryId, + String name, + String sourceLanguage, + List targetLanguages, + int segmentCount) { + this.translationMemoryId = translationMemoryId; + this.name = name; + this.sourceLanguage = sourceLanguage; + this.targetLanguages = targetLanguages; + this.segmentCount = segmentCount; + } + + /** @return Unique ID assigned to the translation memory. */ + public String getTranslationMemoryId() { + return translationMemoryId; + } + + /** @return User-defined name assigned to the translation memory. */ + public String getName() { + return name; + } + + /** @return Source language code for the translation memory. */ + public String getSourceLanguage() { + return sourceLanguage; + } + + /** @return List of target language codes for the translation memory. */ + public List getTargetLanguages() { + return targetLanguages; + } + + /** @return Number of segments in the translation memory. */ + public int getSegmentCount() { + return segmentCount; + } + + @Override + public String toString() { + return "TranslationMemoryInfo{" + + "translationMemoryId='" + + translationMemoryId + + '\'' + + ", name='" + + name + + '\'' + + ", sourceLanguage='" + + sourceLanguage + + '\'' + + ", targetLanguages=" + + targetLanguages + + ", segmentCount=" + + segmentCount + + '}'; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index da4ac9c..8cec476 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -852,6 +852,19 @@ private static ArrayList> createHttpParams( if (options.getStyleId() != null) { params.add(new KeyValuePair<>("style_id", options.getStyleId())); } + if (options.getTranslationMemoryId() != null) { + params.add(new KeyValuePair<>("translation_memory_id", options.getTranslationMemoryId())); + } + if (options.getTranslationMemoryThreshold() != null) { + if (options.getTranslationMemoryId() == null) { + throw new IllegalArgumentException( + "translationMemoryThreshold requires translationMemoryId"); + } + params.add( + new KeyValuePair<>( + "translation_memory_threshold", + options.getTranslationMemoryThreshold().toString())); + } if (options.getCustomInstructions() != null) { for (String instruction : options.getCustomInstructions()) { params.add(new KeyValuePair<>("custom_instructions", instruction)); diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index 4adef4b..e427a08 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -99,6 +99,15 @@ public StyleRuleInfo parseStyleRuleInfo(String json) { return gson.fromJson(json, StyleRuleInfo.class); } + public List parseTranslationMemoryInfoList(String json) { + TranslationMemoryListResponse result = gson.fromJson(json, TranslationMemoryListResponse.class); + return result.getTranslationMemories(); + } + + public TranslationMemoryInfo parseTranslationMemoryInfo(String json) { + return gson.fromJson(json, TranslationMemoryInfo.class); + } + public CustomInstruction parseCustomInstruction(String json) { return gson.fromJson(json, CustomInstruction.class); } diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryListResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryListResponse.java new file mode 100644 index 0000000..bbb9dd8 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryListResponse.java @@ -0,0 +1,20 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.TranslationMemoryInfo; +import java.util.List; + +/** + * Class representing v3 translation_memories list response by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class TranslationMemoryListResponse { + private List translation_memories; + + public List getTranslationMemories() { + return translation_memories; + } +} diff --git a/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java b/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java new file mode 100644 index 0000000..5a8750c --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java @@ -0,0 +1,61 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import java.util.List; +import org.junit.jupiter.api.*; + +public class TranslationMemoryTest extends TestBase { + private static final String DEFAULT_TM_ID = "a74d88fb-ed2a-4943-a664-a4512398b994"; + + @Test + void testListTranslationMemories() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + List translationMemories = client.listTranslationMemories(0, 10); + + Assertions.assertNotNull(translationMemories); + Assertions.assertFalse(translationMemories.isEmpty()); + Assertions.assertNotNull(translationMemories.get(0).getTranslationMemoryId()); + Assertions.assertNotNull(translationMemories.get(0).getName()); + Assertions.assertNotNull(translationMemories.get(0).getSourceLanguage()); + Assertions.assertNotNull(translationMemories.get(0).getTargetLanguages()); + } + + @Test + void testTranslateTextWithTranslationMemoryId() throws Exception { + // Note: this test may use the mock server that will not translate the text + // with a translation memory, therefore we do not check the translated result. + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + String text = "Hallo, Welt!"; + + TextResult result = + client.translateText( + text, + "de", + "en-US", + new TextTranslationOptions().setTranslationMemoryId(DEFAULT_TM_ID)); + + Assertions.assertNotNull(result); + } + + @Test + void testTranslateTextWithTranslationMemoryIdAndThreshold() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + String text = "Hallo, Welt!"; + + TextResult result = + client.translateText( + text, + "de", + "en-US", + new TextTranslationOptions() + .setTranslationMemoryId(DEFAULT_TM_ID) + .setTranslationMemoryThreshold(80)); + + Assertions.assertNotNull(result); + } +} From cfccf2e03190df4643afc3a60c925e77a751c639 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Tue, 7 Apr 2026 12:47:53 -0400 Subject: [PATCH 116/121] docs: update CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e2b52d..f8ecf69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added support for translation memories in text translation via + `setTranslationMemoryId()` and `setTranslationMemoryThreshold()` in + `TextTranslationOptions`. +- Added `listTranslationMemories()` method to the `DeepLClient` class for + listing available translation memories. + ### Fixed - Fixed GitLab release job using wrong image by explicitly setting `image: registry.gitlab.com/gitlab-org/release-cli:latest`. From 9c41d7e27eb6a76b2d877278196927b9d2431436 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Wed, 8 Apr 2026 13:47:22 -0400 Subject: [PATCH 117/121] docs: Bump version to 1.16.0 --- .bumpversion.toml | 2 +- CHANGELOG.md | 5 ++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index b2d3bcd..926dd07 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.15.0" +current_version = "1.16.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ecf69..675ad64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [1.16.0] - 2026-04-09 ### Added - Added support for translation memories in text translation via `setTranslationMemoryId()` and `setTranslationMemoryThreshold()` in @@ -227,7 +229,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2022-09-08 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.15.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.16.0...HEAD +[1.16.0]: https://github.com/DeepLcom/deepl-java/compare/v1.15.0...v1.16.0 [1.15.0]: https://github.com/DeepLcom/deepl-java/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/DeepLcom/deepl-java/compare/v1.13.0...v1.14.0 [1.13.0]: https://github.com/DeepLcom/deepl-java/compare/v1.12.0...v1.13.0 diff --git a/README.md b/README.md index 9abdb8e..96b6a96 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.15.0" +implementation "com.deepl.api:deepl-java:1.16.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.15.0 + 1.16.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 2629980..35d6234 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "com.deepl.api" -version = "1.15.0" +version = "1.16.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index 8cec476..a7c39be 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.15.0"); + sb.append("deepl-java/1.16.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 07c1d1025696b0d4fea0ab02fde902306b9ae3c2 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Fri, 7 Aug 2026 13:10:27 +0000 Subject: [PATCH 118/121] feat: support glossaryIds and document style-rule/translation-memory options --- .gitlab-ci.yml | 2 +- CHANGELOG.md | 13 ++ README.md | 14 ++ .../deepl/api/DocumentTranslationOptions.java | 124 +++++++++++++++++ .../com/deepl/api/TextTranslationOptions.java | 53 +++++++ .../main/java/com/deepl/api/Translator.java | 43 +++++- .../test/java/com/deepl/api/GlossaryTest.java | 131 ++++++++++++++++++ .../java/com/deepl/api/StyleRuleTest.java | 35 +++++ .../src/test/java/com/deepl/api/TestBase.java | 10 +- .../com/deepl/api/TranslationMemoryTest.java | 58 ++++++++ 10 files changed, 474 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2231304..7a5a621 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ include: - project: '${CI_PROJECT_NAMESPACE}/ci-libs-for-client-libraries' file: - '/${CI_PROJECT_NAME}/.gitlab-ci.yml' - - project: 'deepl/ops/ci-cd-infrastructure/gitlab-ci-lib' + - project: 'deepl-org/deepl/devex/gitlab-ci-lib' file: - '/templates/.secret-detection.yml' - '/templates/.gitlab-release.yml' diff --git a/CHANGELOG.md b/CHANGELOG.md index 675ad64..d97d660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added support for using multiple glossaries in text and document translation + via `setGlossaryIds()`/`setGlossaries()` (up to 5 glossaries) in + `TextTranslationOptions` and `DocumentTranslationOptions`. +- Added support for style rules in document translation via + `setStyleRule()`/`setStyleId()` in `DocumentTranslationOptions`. +- Added support for translation memories in document translation via + `setTranslationMemory()`/`setTranslationMemoryId()`/`setTranslationMemoryThreshold()` + in `DocumentTranslationOptions`. + +### Fixed +- Fixed incorrect example texts in the test suite for several languages (Danish, + Indonesian, Japanese, Portuguese, and Russian) to match the mock server. ## [1.16.0] - 2026-04-09 ### Added diff --git a/README.md b/README.md index 96b6a96..18dbff2 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,11 @@ a `TextTranslationOptions`, with the following setters: `listGlossaries()` or `listMultilingualGlossaries()`). - `setGlossaryId()` is also available for backward-compatibility, accepting a string containing the glossary ID. +- `setGlossaryIds()`: specifies multiple glossaries to use with translation + (up to 5), as a list or varargs of glossary ID strings. Glossaries are applied + in the order provided (first match wins). `setGlossaries()` is also available, + accepting `IGlossary` objects. This option requires a source language and + cannot be combined with `setGlossary()`/`setGlossaryId()`. - `setStyleRule()`: specifies a style rule to use with translation, as a string containing the ID of the style rule, or a `StyleRuleInfo` object. - `setStyleId()` is also available, accepting a string containing the style rule ID. @@ -323,6 +328,15 @@ arguments, `translateDocument()` accepts an optional in [Text translation options](#text-translation-options). - `setGlossaryId()`: same as in [Text translation options](#text-translation-options). +- `setGlossaryIds()`/`setGlossaries()`: same as + in [Text translation options](#text-translation-options). Specifies multiple + glossaries (up to 5) to use with document translation. +- `setStyleRule()`/`setStyleId()`: same as + in [Text translation options](#text-translation-options). Document translation + now supports style rules. +- `setTranslationMemory()`/`setTranslationMemoryId()`/`setTranslationMemoryThreshold()`: + same as in [Text translation options](#text-translation-options). Document + translation now supports translation memories. ### Glossaries diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java index 6b5c51c..b95e5d1 100644 --- a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java @@ -3,6 +3,10 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * Options to control document translation behaviour. These options may be provided to {@link * Translator#translateDocument} overloads. @@ -16,6 +20,10 @@ public class DocumentTranslationOptions extends BaseRequestOptions { private Formality formality; private String glossaryId; + private List glossaryIds; + private String styleId; + private String translationMemoryId; + private Integer translationMemoryThreshold; /** * Sets whether translations should lean toward formal or informal language. This option is only @@ -56,6 +64,102 @@ public DocumentTranslationOptions setGlossary(String glossaryId) { return this; } + /** + * Sets the list of glossary IDs to use with the translation, up to a maximum of 5. Glossaries are + * applied in the order provided (first match wins). By default, this value is null + * and no glossaries are used. This option requires a source language to be set and cannot be + * combined with {@link #setGlossaryId} or {@link #setGlossary}. + */ + public DocumentTranslationOptions setGlossaryIds(List glossaryIds) { + this.glossaryIds = glossaryIds; + return this; + } + + /** + * Sets the list of glossary IDs to use with the translation, up to a maximum of 5. Glossaries are + * applied in the order provided (first match wins). By default, this value is null + * and no glossaries are used. This option requires a source language to be set and cannot be + * combined with {@link #setGlossaryId} or {@link #setGlossary}. + */ + public DocumentTranslationOptions setGlossaryIds(String... glossaryIds) { + this.glossaryIds = Arrays.asList(glossaryIds); + return this; + } + + /** + * Sets the list of glossaries to use with the translation, up to a maximum of 5. Glossaries are + * applied in the order provided (first match wins). By default, this value is null + * and no glossaries are used. This option requires a source language to be set and cannot be + * combined with {@link #setGlossaryId} or {@link #setGlossary}. + */ + public DocumentTranslationOptions setGlossaries(IGlossary... glossaries) { + List ids = new ArrayList<>(); + for (IGlossary glossary : glossaries) { + if (glossary == null) { + throw new IllegalArgumentException("glossaries must not contain null"); + } + ids.add(glossary.getGlossaryId()); + } + this.glossaryIds = ids; + return this; + } + + /** + * Sets the ID of a style rule to use with the translation. By default, this value is + * null and no style rule is used. + */ + public DocumentTranslationOptions setStyleId(String styleId) { + this.styleId = styleId; + return this; + } + + /** + * Sets the style rule to use with the translation. By default, this value is null + * and no style rule is used. + */ + public DocumentTranslationOptions setStyleRule(StyleRuleInfo styleRule) { + if (styleRule == null) { + throw new IllegalArgumentException("styleRule must not be null"); + } + return setStyleId(styleRule.getStyleId()); + } + + /** + * Sets the ID of a translation memory to use with the translation. By default, this value is + * null and no translation memory is used. + */ + public DocumentTranslationOptions setTranslationMemoryId(String translationMemoryId) { + this.translationMemoryId = translationMemoryId; + return this; + } + + /** + * Sets the translation memory to use with the translation. By default, this value is null + * and no translation memory is used. + */ + public DocumentTranslationOptions setTranslationMemory(TranslationMemoryInfo translationMemory) { + if (translationMemory == null) { + throw new IllegalArgumentException("translationMemory must not be null"); + } + return setTranslationMemoryId(translationMemory.getTranslationMemoryId()); + } + + /** + * Sets the threshold for translation memory matches. By default, this value is null + * and the API default threshold is used. Note: a translation memory ID must also be set via + * {@link #setTranslationMemoryId} or {@link #setTranslationMemory}, otherwise an error will be + * thrown at translation time. + */ + public DocumentTranslationOptions setTranslationMemoryThreshold( + Integer translationMemoryThreshold) { + if (translationMemoryThreshold != null + && (translationMemoryThreshold < 0 || translationMemoryThreshold > 100)) { + throw new IllegalArgumentException("translationMemoryThreshold must be between 0 and 100"); + } + this.translationMemoryThreshold = translationMemoryThreshold; + return this; + } + /** Gets the current formality setting. */ public Formality getFormality() { return formality; @@ -65,4 +169,24 @@ public Formality getFormality() { public String getGlossaryId() { return glossaryId; } + + /** Gets the current list of glossary IDs. */ + public List getGlossaryIds() { + return glossaryIds; + } + + /** Gets the current style rule ID. */ + public String getStyleId() { + return styleId; + } + + /** Gets the current translation memory ID. */ + public String getTranslationMemoryId() { + return translationMemoryId; + } + + /** Gets the current translation memory threshold. */ + public Integer getTranslationMemoryThreshold() { + return translationMemoryThreshold; + } } diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java index ef5d734..e70ef07 100644 --- a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java +++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java @@ -3,6 +3,10 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * Options to control text translation behaviour. These options may be provided to {@link * Translator#translateText} overloads. @@ -16,6 +20,7 @@ public class TextTranslationOptions extends BaseRequestOptions { private Formality formality; private String glossaryId; + private List glossaryIds; private String styleId; private String translationMemoryId; private Integer translationMemoryThreshold; @@ -70,6 +75,46 @@ public TextTranslationOptions setGlossary(String glossaryId) { return this; } + /** + * Sets the list of glossary IDs to use with the translation, up to a maximum of 5. Glossaries are + * applied in the order provided (first match wins). By default, this value is null + * and no glossaries are used. This option requires a source language to be set and cannot be + * combined with {@link #setGlossaryId} or {@link #setGlossary}. + */ + public TextTranslationOptions setGlossaryIds(List glossaryIds) { + this.glossaryIds = glossaryIds; + return this; + } + + /** + * Sets the list of glossary IDs to use with the translation, up to a maximum of 5. Glossaries are + * applied in the order provided (first match wins). By default, this value is null + * and no glossaries are used. This option requires a source language to be set and cannot be + * combined with {@link #setGlossaryId} or {@link #setGlossary}. + */ + public TextTranslationOptions setGlossaryIds(String... glossaryIds) { + this.glossaryIds = Arrays.asList(glossaryIds); + return this; + } + + /** + * Sets the list of glossaries to use with the translation, up to a maximum of 5. Glossaries are + * applied in the order provided (first match wins). By default, this value is null + * and no glossaries are used. This option requires a source language to be set and cannot be + * combined with {@link #setGlossaryId} or {@link #setGlossary}. + */ + public TextTranslationOptions setGlossaries(IGlossary... glossaries) { + List ids = new ArrayList<>(); + for (IGlossary glossary : glossaries) { + if (glossary == null) { + throw new IllegalArgumentException("glossaries must not contain null"); + } + ids.add(glossary.getGlossaryId()); + } + this.glossaryIds = ids; + return this; + } + /** * Sets the ID of a style rule to use with the translation. By default, this value is * null and no style rule is used. @@ -84,6 +129,9 @@ public TextTranslationOptions setStyleId(String styleId) { * and no style rule is used. */ public TextTranslationOptions setStyleRule(StyleRuleInfo styleRule) { + if (styleRule == null) { + throw new IllegalArgumentException("styleRule must not be null"); + } return setStyleId(styleRule.getStyleId()); } @@ -242,6 +290,11 @@ public String getGlossaryId() { return glossaryId; } + /** Gets the current list of glossary IDs. */ + public List getGlossaryIds() { + return glossaryIds; + } + /** Gets the current style rule ID. */ public String getStyleId() { return styleId; diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index a7c39be..f219fed 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -795,7 +795,8 @@ private static ArrayList> createHttpParams( sourceLang, targetLang, options != null ? options.getFormality() : null, - options != null ? options.getGlossaryId() : null); + options != null ? options.getGlossaryId() : null, + options != null ? options.getGlossaryIds() : null); texts.forEach( (text) -> { if (text.isEmpty()) throw new IllegalArgumentException("text must not be empty"); @@ -892,7 +893,27 @@ protected static ArrayList> createHttpParams( sourceLang, targetLang, options != null ? options.getFormality() : null, - options != null ? options.getGlossaryId() : null); + options != null ? options.getGlossaryId() : null, + options != null ? options.getGlossaryIds() : null); + + if (options != null) { + if (options.getStyleId() != null) { + params.add(new KeyValuePair<>("style_id", options.getStyleId())); + } + if (options.getTranslationMemoryId() != null) { + params.add(new KeyValuePair<>("translation_memory_id", options.getTranslationMemoryId())); + } + if (options.getTranslationMemoryThreshold() != null) { + if (options.getTranslationMemoryId() == null) { + throw new IllegalArgumentException( + "translationMemoryThreshold requires translationMemoryId"); + } + params.add( + new KeyValuePair<>( + "translation_memory_threshold", + options.getTranslationMemoryThreshold().toString())); + } + } addExtraBodyParameters(params, options != null ? options.getExtraBodyParameters() : null); @@ -908,13 +929,15 @@ protected static ArrayList> createHttpParams( * @param targetLang Language code of the desired output language. * @param formality Formality option for translation. * @param glossaryId ID of glossary to use for translation. + * @param glossaryIds List of glossary IDs to use for translation. * @return Iterable of parameters for HTTP request. */ protected static ArrayList> createHttpParamsCommon( @Nullable String sourceLang, String targetLang, @Nullable Formality formality, - @Nullable String glossaryId) { + @Nullable String glossaryId, + @Nullable List glossaryIds) { targetLang = LanguageCode.standardize(targetLang); sourceLang = sourceLang == null ? null : LanguageCode.standardize(sourceLang); checkValidLanguages(sourceLang, targetLang); @@ -953,6 +976,20 @@ protected static ArrayList> createHttpParamsCommon( params.add(new KeyValuePair<>("glossary_id", glossaryId)); } + if (glossaryIds != null && !glossaryIds.isEmpty()) { + if (glossaryId != null) { + throw new IllegalArgumentException( + "glossaryIds cannot be used together with glossaryId; use one or the other"); + } + if (sourceLang == null) { + throw new IllegalArgumentException("sourceLang is required if using glossaries"); + } + if (glossaryIds.size() > 5) { + throw new IllegalArgumentException("glossaryIds cannot contain more than 5 glossary IDs"); + } + params.add(new KeyValuePair<>("glossary_ids", String.join(",", glossaryIds))); + } + return params; } diff --git a/deepl-java/src/test/java/com/deepl/api/GlossaryTest.java b/deepl-java/src/test/java/com/deepl/api/GlossaryTest.java index 906399b..11a1fc1 100644 --- a/deepl-java/src/test/java/com/deepl/api/GlossaryTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GlossaryTest.java @@ -355,4 +355,135 @@ void testGlossaryTranslateTextInvalid() throws Exception { Assertions.assertTrue(exception.getMessage().contains("targetLang=\"en\" is not allowed")); } } + + @Test + void testGlossaryIdsTranslateTextBasic() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanupEnDe1 = new GlossaryCleanupUtility(translator, "EnDe1"); + GlossaryCleanupUtility cleanupEnDe2 = new GlossaryCleanupUtility(translator, "EnDe2")) { + String glossaryName1 = cleanupEnDe1.getGlossaryName(); + String glossaryName2 = cleanupEnDe2.getGlossaryName(); + + GlossaryEntries entries1 = GlossaryEntries.fromTsv("Apple\tApfel"); + GlossaryEntries entries2 = GlossaryEntries.fromTsv("Banana\tBanane"); + + GlossaryInfo glossary1 = translator.createGlossary(glossaryName1, "en", "de", entries1); + GlossaryInfo glossary2 = translator.createGlossary(glossaryName2, "en", "de", entries2); + + List textsEn = + new ArrayList() { + { + add("Apple"); + add("Banana"); + } + }; + List textsDe = + new ArrayList() { + { + add("Apfel"); + add("Banane"); + } + }; + + // Using a list of glossary IDs + List result = + translator.translateText( + textsEn, + "en", + "de", + new TextTranslationOptions() + .setGlossaryIds(glossary1.getGlossaryId(), glossary2.getGlossaryId())); + Assertions.assertArrayEquals( + textsDe.toArray(), result.stream().map(TextResult::getText).toArray()); + + // Using GlossaryInfo objects + result = + translator.translateText( + textsEn, + "en", + "de", + new TextTranslationOptions().setGlossaries(glossary1, glossary2)); + Assertions.assertArrayEquals( + textsDe.toArray(), result.stream().map(TextResult::getText).toArray()); + } + } + + @Test + void testGlossaryIdsTranslateDocument() throws Exception { + Translator translator = createTranslator(); + try (GlossaryCleanupUtility cleanup1 = new GlossaryCleanupUtility(translator, "1"); + GlossaryCleanupUtility cleanup2 = new GlossaryCleanupUtility(translator, "2")) { + String glossaryName1 = cleanup1.getGlossaryName(); + String glossaryName2 = cleanup2.getGlossaryName(); + File inputFile = createInputFile("artist\nprize"); + File outputFile = createOutputFile(); + String expectedOutput = "Maler\nGewinn"; + + GlossaryEntries entries1 = GlossaryEntries.fromTsv("artist\tMaler"); + GlossaryEntries entries2 = GlossaryEntries.fromTsv("prize\tGewinn"); + + GlossaryInfo glossary1 = + translator.createGlossary(glossaryName1, sourceLang, targetLang, entries1); + GlossaryInfo glossary2 = + translator.createGlossary(glossaryName2, sourceLang, targetLang, entries2); + + translator.translateDocument( + inputFile, + outputFile, + sourceLang, + targetLang, + new DocumentTranslationOptions() + .setGlossaryIds(glossary1.getGlossaryId(), glossary2.getGlossaryId())); + Assertions.assertEquals(expectedOutput, readFromFile(outputFile)); + } + } + + @Test + void testGlossaryIdsAndGlossaryIdMutuallyExclusive() throws Exception { + Translator translator = createTranslator(); + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + translator.translateText( + "test", + "en", + "de", + new TextTranslationOptions() + .setGlossaryId(nonexistentGlossaryId) + .setGlossaryIds(nonexistentGlossaryId))); + Assertions.assertTrue( + exception.getMessage().contains("cannot be used together with glossaryId")); + } + + @Test + void testGlossaryIdsTooMany() throws Exception { + Translator translator = createTranslator(); + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + translator.translateText( + "test", + "en", + "de", + new TextTranslationOptions() + .setGlossaryIds("id1", "id2", "id3", "id4", "id5", "id6"))); + Assertions.assertTrue(exception.getMessage().contains("more than 5")); + } + + @Test + void testGlossaryIdsMissingSourceLang() throws Exception { + Translator translator = createTranslator(); + IllegalArgumentException exception = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + translator.translateText( + "test", + null, + "de", + new TextTranslationOptions().setGlossaryIds(nonexistentGlossaryId))); + Assertions.assertTrue(exception.getMessage().contains("sourceLang is required")); + } } diff --git a/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java index 8a396b6..5f8d371 100644 --- a/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java +++ b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java @@ -3,6 +3,7 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -195,4 +196,38 @@ void testTranslateTextWithStyleRuleInfo() throws Exception { Assertions.assertNotNull(result); } + + @Test + void testTranslateDocumentWithStyleId() throws Exception { + // Note: this test may use the mock server that will not translate the document + // with a style rule, therefore we do not check the translated result. + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + File inputFile = createInputFile("Hallo, Welt!"); + File outputFile = createOutputFile(); + + client.translateDocument( + inputFile, + outputFile, + "de", + "en-US", + new DocumentTranslationOptions().setStyleId(DEFAULT_STYLE_ID)); + + Assertions.assertNotNull(readFromFile(outputFile)); + } + + @Test + void testTranslateDocumentWithStyleRuleInfo() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + List styleRules = client.getAllStyleRules(); + StyleRuleInfo rule = styleRules.get(0); + File inputFile = createInputFile("Hallo, Welt!"); + File outputFile = createOutputFile(); + + client.translateDocument( + inputFile, outputFile, "de", "en-US", new DocumentTranslationOptions().setStyleRule(rule)); + + Assertions.assertNotNull(readFromFile(outputFile)); + } } diff --git a/deepl-java/src/test/java/com/deepl/api/TestBase.java b/deepl-java/src/test/java/com/deepl/api/TestBase.java index 06cc67b..1ea1032 100644 --- a/deepl-java/src/test/java/com/deepl/api/TestBase.java +++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java @@ -51,7 +51,7 @@ public class TestBase { exampleText.put("ar", "شعاع البروتون"); exampleText.put("bg", "протонен лъч"); exampleText.put("cs", "protonový paprsek"); - exampleText.put("da", "protonstråle"); + exampleText.put("da", "Protonstråle"); exampleText.put("de", "Protonenstrahl"); exampleText.put("el", "δέσμη πρωτονίων"); exampleText.put("en", "proton beam"); @@ -62,9 +62,9 @@ public class TestBase { exampleText.put("fi", "protonisäde"); exampleText.put("fr", "faisceau de protons"); exampleText.put("hu", "protonnyaláb"); - exampleText.put("id", "berkas proton"); + exampleText.put("id", "sinar proton"); exampleText.put("it", "fascio di protoni"); - exampleText.put("ja", "陽子ビーム"); + exampleText.put("ja", "陽子線"); exampleText.put("ko", "양성자 빔"); exampleText.put("lt", "protonų spindulys"); exampleText.put("lv", "protonu staru kūlis"); @@ -73,9 +73,9 @@ public class TestBase { exampleText.put("pl", "wiązka protonów"); exampleText.put("pt", "feixe de prótons"); exampleText.put("pt-BR", "feixe de prótons"); - exampleText.put("pt-PT", "feixe de prótons"); + exampleText.put("pt-PT", "feixe de protões"); exampleText.put("ro", "fascicul de protoni"); - exampleText.put("ru", "протонный луч"); + exampleText.put("ru", "протонный пучок"); exampleText.put("sk", "protónový lúč"); exampleText.put("sl", "protonski žarek"); exampleText.put("sv", "protonstråle"); diff --git a/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java b/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java index 5a8750c..bcfa22d 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java @@ -3,6 +3,7 @@ // license that can be found in the LICENSE file. package com.deepl.api; +import java.io.File; import java.util.List; import org.junit.jupiter.api.*; @@ -58,4 +59,61 @@ void testTranslateTextWithTranslationMemoryIdAndThreshold() throws Exception { Assertions.assertNotNull(result); } + + @Test + void testTranslateDocumentWithTranslationMemoryId() throws Exception { + // Note: this test may use the mock server that will not translate the document + // with a translation memory, therefore we do not check the translated result. + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + File inputFile = createInputFile("Hallo, Welt!"); + File outputFile = createOutputFile(); + + client.translateDocument( + inputFile, + outputFile, + "de", + "en-US", + new DocumentTranslationOptions().setTranslationMemoryId(DEFAULT_TM_ID)); + + Assertions.assertNotNull(readFromFile(outputFile)); + } + + @Test + void testTranslateDocumentWithTranslationMemoryIdAndThreshold() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + File inputFile = createInputFile("Hallo, Welt!"); + File outputFile = createOutputFile(); + + client.translateDocument( + inputFile, + outputFile, + "de", + "en-US", + new DocumentTranslationOptions() + .setTranslationMemoryId(DEFAULT_TM_ID) + .setTranslationMemoryThreshold(80)); + + Assertions.assertNotNull(readFromFile(outputFile)); + } + + @Test + void testTranslateDocumentWithTranslationMemoryInfo() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + List translationMemories = client.listTranslationMemories(0, 10); + TranslationMemoryInfo translationMemory = translationMemories.get(0); + File inputFile = createInputFile("Hallo, Welt!"); + File outputFile = createOutputFile(); + + client.translateDocument( + inputFile, + outputFile, + "de", + "en-US", + new DocumentTranslationOptions().setTranslationMemory(translationMemory)); + + Assertions.assertNotNull(readFromFile(outputFile)); + } } From 9f286614504cb18d986e4e4591cdef0e962b45e0 Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Tue, 11 Aug 2026 20:09:49 +0000 Subject: [PATCH 119/121] feat: add support for the translation memory management APIs --- CHANGELOG.md | 17 + README.md | 160 ++++- .../main/java/com/deepl/api/DeepLClient.java | 640 ++++++++++++++++-- .../java/com/deepl/api/HttpClientWrapper.java | 40 +- .../deepl/api/TranslationMemoryExport.java | 49 ++ .../deepl/api/TranslationMemoryImport.java | 57 ++ .../com/deepl/api/TranslationMemoryInfo.java | 49 ++ .../com/deepl/api/TranslationMemoryJob.java | 163 +++++ .../deepl/api/TranslationMemoryJobResult.java | 134 ++++ .../deepl/api/TranslationMemorySegment.java | 107 +++ .../deepl/api/TranslationMemorySegments.java | 55 ++ .../api/TranslationMemorySegmentsOptions.java | 95 +++ .../api/TranslationMemoryTargetSegment.java | 99 +++ .../java/com/deepl/api/http/HttpContent.java | 14 + .../java/com/deepl/api/parsing/Parser.java | 30 +- .../TranslationMemoryExportResponse.java | 26 + .../TranslationMemoryJobDeserializer.java | 76 +++ .../java/com/deepl/api/SessionOptions.java | 16 + .../java/com/deepl/api/TranslateTextTest.java | 15 +- .../com/deepl/api/TranslationMemoryTest.java | 263 +++++++ 20 files changed, 2045 insertions(+), 60 deletions(-) create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemoryExport.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemoryImport.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemoryJob.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemoryJobResult.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemorySegment.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemorySegments.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemorySegmentsOptions.java create mode 100644 deepl-java/src/main/java/com/deepl/api/TranslationMemoryTargetSegment.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryExportResponse.java create mode 100644 deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryJobDeserializer.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d97d660..fab50fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for translation memories in document translation via `setTranslationMemory()`/`setTranslationMemoryId()`/`setTranslationMemoryThreshold()` in `DocumentTranslationOptions`. +- Added support for the translation memory management endpoints in the + `DeepLClient` class: `getTranslationMemory()`, + `listTranslationMemorySegments()`, `deleteTranslationMemory()`, + `createTranslationMemoryImport()`, `uploadTranslationMemoryFile()`, + `createTranslationMemoryExport()`, `getTranslationMemoryJob()`, + `waitUntilTranslationMemoryJobDone()`, `downloadTranslationMemoryExport()`, + `importTranslationMemoryFromFilepath()` and + `exportTranslationMemoryToFilepath()`. +- Added `creationTime` and `updatedTime` to `TranslationMemoryInfo`. +- Added optional timeouts to `waitUntilTranslationMemoryJobDone()`, + `importTranslationMemoryFromFilepath()` and + `exportTranslationMemoryToFilepath()`. ### Fixed +- Fixed `waitUntilTranslationMemoryJobDone()` throwing when a translation memory + job reports `AwaitingInput`. An import job keeps reporting that status for a + while after its file has been uploaded, because the API detects the upload + asynchronously, so it is now polled through like any other non-terminal + status. - Fixed incorrect example texts in the test suite for several languages (Danish, Indonesian, Japanese, Portuguese, and Russian) to match the mock server. diff --git a/README.md b/README.md index 18dbff2..ad9ce45 100644 --- a/README.md +++ b/README.md @@ -823,16 +823,12 @@ class Example { // Continuing class Example from above Translation memories store and reuse previously created translations, helping to ensure consistency and reduce effort when translating similar or repeated content. -#### Uploading and managing translation memories - -Currently translation memories must be uploaded and managed in the DeepL UI via -https://www.deepl.com/translation-memory. Full CRUD functionality via the APIs will -come shortly. - #### Listing translation memories Use `listTranslationMemories()` to retrieve translation memories associated -with your account: +with your account. The number of translation memories returned is controlled by +`pageSize` (max 25). The method accepts optional parameters: `page` (page number +for pagination, 0-indexed) and `pageSize` (number of items per page). ```java class Example { // Continuing class Example from above @@ -847,6 +843,156 @@ class Example { // Continuing class Example from above } ``` +#### Retrieving a translation memory + +Use `getTranslationMemory()` to retrieve a single translation memory. It accepts +either a translation memory ID or a `TranslationMemoryInfo` object: + +```java +class Example { // Continuing class Example from above + public void getTranslationMemoryExample() throws Exception { + TranslationMemoryInfo tm = client.getTranslationMemory("YOUR_TM_ID"); + System.out.println(String.format("%s: %d segments, updated %s", + tm.getName(), tm.getSegmentCount(), tm.getUpdatedTime())); + } +} +``` + +#### Listing the segments of a translation memory + +`listTranslationMemorySegments()` returns one page of segments as a +`TranslationMemorySegments` object. Pagination is cursor-based: omit the page +cursor on the first call, then pass the previous response's +`getNextPageCursor()` until it is `null`. Optionally filter with `setFilterText()` +(at least 2 characters, matched against both source and target text) and +`setFilterCaseSensitive()`. Note that `getSegmentCount()` is the +translation-memory total and is not reduced by the filter. The API may omit the +segment timestamps, in which case `getCreationTime()`, `getUpdatedTime()` and +`getLastUsedTime()` are `null` on both segments and their targets. + +```java +class Example { // Continuing class Example from above + public void listTranslationMemorySegmentsExample() throws Exception { + String pageCursor = null; + do { + TranslationMemorySegments page = client.listTranslationMemorySegments( + "YOUR_TM_ID", + new TranslationMemorySegmentsOptions() + .setPageSize(50) + .setPageCursor(pageCursor)); + for (TranslationMemorySegment segment : page.getSegments()) { + System.out.println(segment.getSourceText()); + for (TranslationMemoryTargetSegment target : segment.getTargets()) { + System.out.println(String.format(" %s: %s", + target.getTargetLanguage(), target.getTargetText())); + } + } + pageCursor = page.getNextPageCursor(); + } while (pageCursor != null); + } +} +``` + +#### Importing a translation memory + +`importTranslationMemoryFromFilepath()` imports a TMX file as a new translation +memory: it creates the import job, uploads the file, and waits for processing to +finish. The returned `TranslationMemoryJob` carries the ID of the new translation +memory: + +```java +class Example { // Continuing class Example from above + public void importTranslationMemoryExample() throws Exception { + TranslationMemoryJob job = client.importTranslationMemoryFromFilepath( + new File("/path/to/legal.tmx"), "Legal TM", Duration.ofSeconds(300)); + System.out.println(String.format("Created translation memory %s", + job.getResult().getTranslationMemoryId())); + System.out.println(String.format("Skipped segments: %s", + job.getResult().getSkippedSegmentCount())); + } +} +``` + +The optional timeout is the maximum time to wait for the import to finish; omit +it to wait indefinitely. The job status is polled every 5 seconds, so the +timeout is not accurate to the millisecond. + +The three steps are also available separately, for example to upload the file +yourself or to poll for progress. `createTranslationMemoryImport()` returns an +upload URL that the file must be uploaded to before processing starts, then +`getTranslationMemoryJob()` reports the status: + +```java +class Example { // Continuing class Example from above + public void importTranslationMemoryStepsExample() throws Exception { + File inputFile = new File("/path/to/legal.tmx"); + byte[] fileContent = Files.readAllBytes(inputFile.toPath()); + + TranslationMemoryImport created = client.createTranslationMemoryImport( + inputFile.getName(), fileContent.length, null, "Legal TM"); + // Until the file is uploaded, the job status is AwaitingInput + client.uploadTranslationMemoryFile(created, fileContent); + + TranslationMemoryJob job = client.waitUntilTranslationMemoryJobDone( + created.getJobId(), Duration.ofSeconds(300)); + } +} +``` + +Note that an import job keeps reporting `AwaitingInput` for a while after its +file has been uploaded, because the API detects the upload asynchronously. +`waitUntilTranslationMemoryJobDone()` polls through that status like any other +non-terminal one. A job whose file is never uploaded does not finish on its own, +so pass a timeout when that is a possibility. + +#### Exporting a translation memory + +`exportTranslationMemoryToFilepath()` exports a translation memory to a TMX file: +it creates the export job, waits for it to finish, and writes the result. It +accepts either a translation memory ID or a `TranslationMemoryInfo` object: + +```java +class Example { // Continuing class Example from above + public void exportTranslationMemoryExample() throws Exception { + TranslationMemoryJob job = client.exportTranslationMemoryToFilepath( + "YOUR_TM_ID", new File("/path/to/exported.tmx"), Duration.ofSeconds(300)); + } +} +``` + +As with import, the timeout is optional; omit it to wait indefinitely. + +As with import, the individual steps are available separately. Note that the API +may reuse a previously completed export of an unchanged translation memory, +indicated by `isReusedExisting()`: + +```java +class Example { // Continuing class Example from above + public void exportTranslationMemoryStepsExample() throws Exception { + TranslationMemoryExport created = + client.createTranslationMemoryExport("YOUR_TM_ID"); + TranslationMemoryJob job = + client.waitUntilTranslationMemoryJobDone(created.getJobId()); + client.downloadTranslationMemoryExport( + job, new File("/path/to/exported.tmx")); + } +} +``` + +#### Deleting a translation memory + +Use `deleteTranslationMemory()` to permanently remove a translation memory from +your account. It accepts either a translation memory ID or a +`TranslationMemoryInfo` object: + +```java +class Example { // Continuing class Example from above + public void deleteTranslationMemoryExample() throws Exception { + client.deleteTranslationMemory("YOUR_TM_ID"); + } +} +``` + #### Using a translation memory in translations You can use a translation memory for text translation by setting the translation diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java index 45e773b..9333746 100644 --- a/deepl-java/src/main/java/com/deepl/api/DeepLClient.java +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -5,10 +5,13 @@ package com.deepl.api; import com.deepl.api.http.HttpResponse; +import com.deepl.api.http.HttpResponseStream; import com.deepl.api.utils.*; import java.io.*; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -17,6 +20,12 @@ public class DeepLClient extends Translator { + /** Default MIME type of translation memory (TMX) files. */ + public static final String TRANSLATION_MEMORY_FILE_CONTENT_TYPE = "application/xml"; + + /** Time to wait between polls of the status of a translation memory job, in milliseconds. */ + private static final long TRANSLATION_MEMORY_JOB_POLL_INTERVAL_MILLIS = 5000; + /** * Initializes a new DeepLClient object using your Authentication Key. * @@ -768,26 +777,7 @@ public List getAllStyleRules( queryParams.add(new KeyValuePair<>("detailed", detailed.toString().toLowerCase())); } - String queryString = ""; - if (!queryParams.isEmpty()) { - StringBuilder sb = new StringBuilder("?"); - for (int i = 0; i < queryParams.size(); i++) { - if (i > 0) { - sb.append("&"); - } - KeyValuePair param = queryParams.get(i); - try { - sb.append(URLEncoder.encode(param.getKey(), StandardCharsets.UTF_8.name())) - .append("=") - .append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8.name())); - } catch (java.io.UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 encoding not supported", e); - } - } - queryString = sb.toString(); - } - - String relativeUrl = "/v3/style_rules" + queryString; + String relativeUrl = "/v3/style_rules" + createQueryString(queryParams); HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); checkResponse(response, false, false); return jsonParser.parseStyleRuleInfoList(response.getBody()); @@ -825,34 +815,15 @@ public List listTranslationMemories( queryParams.add(new KeyValuePair<>("page_size", pageSize.toString())); } - String queryString = ""; - if (!queryParams.isEmpty()) { - StringBuilder sb = new StringBuilder("?"); - for (int i = 0; i < queryParams.size(); i++) { - if (i > 0) { - sb.append("&"); - } - KeyValuePair param = queryParams.get(i); - try { - sb.append(URLEncoder.encode(param.getKey(), StandardCharsets.UTF_8.name())) - .append("=") - .append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8.name())); - } catch (java.io.UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 encoding not supported", e); - } - } - queryString = sb.toString(); - } - - String relativeUrl = "/v3/translation_memories" + queryString; + String relativeUrl = "/v3/translation_memories" + createQueryString(queryParams); HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); checkResponse(response, false, false); return jsonParser.parseTranslationMemoryInfoList(response.getBody()); } /** - * Functions the same as {@link DeepLClient#listTranslationMemories(Integer, Integer, Boolean)} - * but with default parameters (all null). + * Functions the same as {@link DeepLClient#listTranslationMemories(Integer, Integer)} but with + * default parameters (all null). * * @see DeepLClient#listTranslationMemories(Integer, Integer) */ @@ -861,6 +832,561 @@ public List listTranslationMemories() return listTranslationMemories(null, null); } + /** + * Retrieves information about the translation memory with the specified ID and returns a {@link + * TranslationMemoryInfo} object containing details. + * + * @param translationMemoryId ID of the translation memory to retrieve. + * @return {@link TranslationMemoryInfo} object with details about the translation memory. + * @throws NotFoundException If no translation memory with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public TranslationMemoryInfo getTranslationMemory(String translationMemoryId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("translationMemoryId", translationMemoryId); + String relativeUrl = String.format("/v3/translation_memories/%s", translationMemoryId); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + return jsonParser.parseTranslationMemoryInfo(response.getBody()); + } + + /** + * Functions the same as {@link DeepLClient#getTranslationMemory(String)} but accepts a {@link + * TranslationMemoryInfo} object. + * + * @see DeepLClient#getTranslationMemory(String) + */ + public TranslationMemoryInfo getTranslationMemory(TranslationMemoryInfo translationMemory) + throws DeepLException, InterruptedException, NotFoundException { + return getTranslationMemory(translationMemoryId(translationMemory)); + } + + /** + * Retrieves one page of the segments of the translation memory with the specified ID. + * + *

Pagination is cursor-based: omit the page cursor on the first call, then pass the previous + * response's {@link TranslationMemorySegments#getNextPageCursor()} to fetch the next page. An + * absent next page cursor means the last page has been returned. + * + * @param translationMemoryId ID of the translation memory to retrieve the segments of. + * @param options Options influencing the returned page, or null. + * @return {@link TranslationMemorySegments} object containing the requested page. + * @throws NotFoundException If no translation memory with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public TranslationMemorySegments listTranslationMemorySegments( + String translationMemoryId, @Nullable TranslationMemorySegmentsOptions options) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("translationMemoryId", translationMemoryId); + ArrayList> queryParams = new ArrayList<>(); + if (options != null) { + if (options.getPageSize() != null) { + queryParams.add(new KeyValuePair<>("page_size", options.getPageSize().toString())); + } + if (options.getPageCursor() != null) { + queryParams.add(new KeyValuePair<>("page_cursor", options.getPageCursor())); + } + if (options.getFilterText() != null) { + queryParams.add(new KeyValuePair<>("filter_text", options.getFilterText())); + } + if (options.getFilterCaseSensitive() != null) { + queryParams.add( + new KeyValuePair<>( + "filter_case_sensitive", options.getFilterCaseSensitive().toString())); + } + } + + String relativeUrl = + String.format("/v3/translation_memories/%s/segments", translationMemoryId) + + createQueryString(queryParams); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + return jsonParser.parseTranslationMemorySegments(response.getBody()); + } + + /** + * Functions the same as {@link DeepLClient#listTranslationMemorySegments(String, + * TranslationMemorySegmentsOptions)} but with default options. + * + * @see DeepLClient#listTranslationMemorySegments(String, TranslationMemorySegmentsOptions) + */ + public TranslationMemorySegments listTranslationMemorySegments(String translationMemoryId) + throws DeepLException, InterruptedException, NotFoundException { + return listTranslationMemorySegments(translationMemoryId, null); + } + + /** + * Functions the same as {@link DeepLClient#listTranslationMemorySegments(String, + * TranslationMemorySegmentsOptions)} but accepts a {@link TranslationMemoryInfo} object. + * + * @see DeepLClient#listTranslationMemorySegments(String, TranslationMemorySegmentsOptions) + */ + public TranslationMemorySegments listTranslationMemorySegments( + TranslationMemoryInfo translationMemory, @Nullable TranslationMemorySegmentsOptions options) + throws DeepLException, InterruptedException, NotFoundException { + return listTranslationMemorySegments(translationMemoryId(translationMemory), options); + } + + /** + * Functions the same as {@link DeepLClient#listTranslationMemorySegments(String, + * TranslationMemorySegmentsOptions)} but accepts a {@link TranslationMemoryInfo} object and uses + * default options. + * + * @see DeepLClient#listTranslationMemorySegments(String, TranslationMemorySegmentsOptions) + */ + public TranslationMemorySegments listTranslationMemorySegments( + TranslationMemoryInfo translationMemory) + throws DeepLException, InterruptedException, NotFoundException { + return listTranslationMemorySegments(translationMemoryId(translationMemory), null); + } + + /** + * Deletes the translation memory with the specified ID. + * + * @param translationMemoryId ID of the translation memory to delete. + * @throws NotFoundException If no translation memory with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public void deleteTranslationMemory(String translationMemoryId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("translationMemoryId", translationMemoryId); + String relativeUrl = String.format("/v3/translation_memories/%s", translationMemoryId); + HttpResponse response = httpClientWrapper.sendDeleteRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + } + + /** + * Functions the same as {@link DeepLClient#deleteTranslationMemory(String)} but accepts a {@link + * TranslationMemoryInfo} object. + * + * @see DeepLClient#deleteTranslationMemory(String) + */ + public void deleteTranslationMemory(TranslationMemoryInfo translationMemory) + throws DeepLException, InterruptedException, NotFoundException { + deleteTranslationMemory(translationMemoryId(translationMemory)); + } + + /** + * Creates an import job for a new translation memory. + * + *

The job only declares the file; upload the TMX file itself to the returned upload URL with + * {@link DeepLClient#uploadTranslationMemoryFile(TranslationMemoryImport, byte[])}, then poll + * {@link DeepLClient#getTranslationMemoryJob(String)} for the outcome. Use {@link + * DeepLClient#importTranslationMemoryFromFilepath(File, String)} to do all three steps at once. + * + * @param fileName Name of the TMX file to import, for example "legal.tmx". + * @param contentLength Size of the TMX file in bytes. + * @param contentType Optional MIME type of the file, defaults to "application/xml". + * @param displayName Optional name for the resulting translation memory, defaults to the file + * name. + * @return {@link TranslationMemoryImport} object with the job ID and upload URL. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public TranslationMemoryImport createTranslationMemoryImport( + String fileName, + long contentLength, + @Nullable String contentType, + @Nullable String displayName) + throws DeepLException, InterruptedException { + validateParameter("fileName", fileName); + if (contentLength <= 0) { + throw new IllegalArgumentException("contentLength must be greater than 0"); + } + + Map sourceFile = new HashMap<>(); + sourceFile.put("file_name", fileName); + sourceFile.put("content_length", contentLength); + if (contentType != null) { + sourceFile.put("content_type", contentType); + } + Map requestData = new HashMap<>(); + requestData.put("source_file", sourceFile); + if (displayName != null) { + Map parameters = new HashMap<>(); + parameters.put("display_name", displayName); + requestData.put("parameters", parameters); + } + + String jsonBody = jsonParser.getGson().toJson(requestData); + HttpResponse response = + httpClientWrapper.sendJsonRequestWithBackoff("/v3/translation_memories/import", jsonBody); + checkResponse(response, false, false); + return jsonParser.parseTranslationMemoryImport(response.getBody()); + } + + /** + * Uploads a TMX file to the upload URL of an import job, which starts processing. + * + *

The upload URL is a pre-signed storage URL outside of the DeepL API, so the authentication + * key is not sent with this request. + * + * @param uploadUrl Upload URL returned by {@link + * DeepLClient#createTranslationMemoryImport(String, long, String, String)}. + * @param fileContent Content of the TMX file to upload. + * @param contentType MIME type of the file, which must match the content type declared when the + * import job was created. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while uploading the file. + */ + public void uploadTranslationMemoryFile(String uploadUrl, byte[] fileContent, String contentType) + throws DeepLException, InterruptedException { + validateParameter("uploadUrl", uploadUrl); + validateParameter("contentType", contentType); + if (fileContent == null) { + throw new IllegalArgumentException("fileContent must not be null"); + } + HttpResponse response = + httpClientWrapper.sendAssetPutRequestWithBackoff(uploadUrl, fileContent, contentType); + if (response.getCode() < 200 || response.getCode() >= 300) { + throw new DeepLException( + String.format( + "Error uploading translation memory file, HTTP status: %d", response.getCode())); + } + } + + /** + * Functions the same as {@link DeepLClient#uploadTranslationMemoryFile(String, byte[], String)} + * but accepts the {@link TranslationMemoryImport} object holding the upload URL. + * + * @see DeepLClient#uploadTranslationMemoryFile(String, byte[], String) + */ + public void uploadTranslationMemoryFile( + TranslationMemoryImport translationMemoryImport, byte[] fileContent, String contentType) + throws DeepLException, InterruptedException { + if (translationMemoryImport == null) { + throw new IllegalArgumentException("translationMemoryImport must not be null"); + } + uploadTranslationMemoryFile(translationMemoryImport.getUploadUrl(), fileContent, contentType); + } + + /** + * Functions the same as {@link DeepLClient#uploadTranslationMemoryFile(TranslationMemoryImport, + * byte[], String)} but uses the default content type "application/xml". + * + * @see DeepLClient#uploadTranslationMemoryFile(TranslationMemoryImport, byte[], String) + */ + public void uploadTranslationMemoryFile( + TranslationMemoryImport translationMemoryImport, byte[] fileContent) + throws DeepLException, InterruptedException { + uploadTranslationMemoryFile( + translationMemoryImport, fileContent, TRANSLATION_MEMORY_FILE_CONTENT_TYPE); + } + + /** + * Creates an export job for the translation memory with the specified ID. + * + *

Poll {@link DeepLClient#getTranslationMemoryJob(String)} for the download URL of the + * exported TMX file. Use {@link DeepLClient#exportTranslationMemoryToFilepath(String, File)} to + * do both steps and write the file at once. + * + * @param translationMemoryId ID of the translation memory to export. + * @return {@link TranslationMemoryExport} object with the job ID, and whether the API reused a + * previously completed export. + * @throws NotFoundException If no translation memory with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public TranslationMemoryExport createTranslationMemoryExport(String translationMemoryId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("translationMemoryId", translationMemoryId); + String relativeUrl = String.format("/v3/translation_memories/%s/export", translationMemoryId); + HttpResponse response = httpClientWrapper.sendRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + // 200 means the API reused a previously completed export, 202 that it started a new one. + return jsonParser.parseTranslationMemoryExport(response.getBody(), response.getCode() == 200); + } + + /** + * Functions the same as {@link DeepLClient#createTranslationMemoryExport(String)} but accepts a + * {@link TranslationMemoryInfo} object. + * + * @see DeepLClient#createTranslationMemoryExport(String) + */ + public TranslationMemoryExport createTranslationMemoryExport( + TranslationMemoryInfo translationMemory) + throws DeepLException, InterruptedException, NotFoundException { + return createTranslationMemoryExport(translationMemoryId(translationMemory)); + } + + /** + * Retrieves the status of the translation memory import or export job with the specified ID. + * + * @param jobId ID of the job to query. + * @return {@link TranslationMemoryJob} object with the current status of the job. + * @throws NotFoundException If no job with the given ID is found. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If any error occurs while communicating with the DeepL API. + */ + public TranslationMemoryJob getTranslationMemoryJob(String jobId) + throws DeepLException, InterruptedException, NotFoundException { + validateParameter("jobId", jobId); + String relativeUrl = String.format("/v3/translation_memories/jobs/%s", jobId); + HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl); + checkResponse(response, false, false); + return jsonParser.parseTranslationMemoryJob(response.getBody()); + } + + /** + * Polls the translation memory job with the specified ID until it finishes, sleeping between + * requests, and returns the final status. + * + *

Note that an import job keeps reporting {@link + * TranslationMemoryJobResult.Status#AwaitingInput} for a while after its file has been uploaded, + * because the API detects the upload asynchronously. That status is therefore polled through like + * any other non-terminal one. A job whose file is never uploaded does not finish on its own, so + * use {@link DeepLClient#waitUntilTranslationMemoryJobDone(String, Duration)} to pass a timeout + * when that is a possibility. + * + * @param jobId ID of the job to wait for. + * @return {@link TranslationMemoryJob} object with the status of the finished job. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the job failed or expired, or any error occurs while communicating + * with the DeepL API. + */ + public TranslationMemoryJob waitUntilTranslationMemoryJobDone(String jobId) + throws DeepLException, InterruptedException { + return waitUntilTranslationMemoryJobDone(jobId, null); + } + + /** + * Functions the same as {@link DeepLClient#waitUntilTranslationMemoryJobDone(String)} but gives + * up after the specified timeout. + * + * @param jobId ID of the job to wait for. + * @param timeout Maximum time to wait before throwing, or null to wait indefinitely. + * Note that this is not accurate to the millisecond, as the job status is only polled every 5 + * seconds. + * @return {@link TranslationMemoryJob} object with the status of the finished job. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the timeout is exceeded, the job failed or expired, or any error + * occurs while communicating with the DeepL API. + * @see DeepLClient#waitUntilTranslationMemoryJobDone(String) + */ + public TranslationMemoryJob waitUntilTranslationMemoryJobDone( + String jobId, @Nullable Duration timeout) throws DeepLException, InterruptedException { + long startTimeMillis = System.currentTimeMillis(); + TranslationMemoryJob job = getTranslationMemoryJob(jobId); + while (!job.done()) { + // The API always returns exactly one result; an empty list would never reach a terminal + // status and the no-timeout overload would poll forever. + if (job.getResults().isEmpty()) { + throw new DeepLException("Translation memory job " + jobId + " returned no results"); + } + if (timeout != null && System.currentTimeMillis() - startTimeMillis > timeout.toMillis()) { + throw new DeepLException( + String.format( + "Manual timeout of %ds exceeded for translation memory job", timeout.getSeconds())); + } + Thread.sleep(TRANSLATION_MEMORY_JOB_POLL_INTERVAL_MILLIS); + job = getTranslationMemoryJob(jobId); + } + if (!job.ok()) { + TranslationMemoryJobResult result = job.getResult(); + String message = + (result != null && result.getErrorMessage() != null) + ? result.getErrorMessage() + : "Unknown error"; + throw new DeepLException(message); + } + return job; + } + + /** + * Downloads the TMX file of a completed export job to the specified output file. + * + *

The download URL is a pre-signed storage URL outside of the DeepL API, so the authentication + * key is not sent with this request. + * + * @param job Completed export job carrying the download URL. + * @param outputFile File to download the exported translation memory to. + * @throws IOException If the output path is occupied. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the job has no download URL, or any error occurs while downloading. + */ + public void downloadTranslationMemoryExport(TranslationMemoryJob job, File outputFile) + throws DeepLException, IOException, InterruptedException { + // Checked before the try so the cleanup below only ever deletes a file this call created: + // otherwise the guard protecting an existing file would be what destroys it. + if (outputFile.exists()) { + throw new IOException("File already exists at output path"); + } + try { + try (FileOutputStream outputStream = new FileOutputStream(outputFile)) { + downloadTranslationMemoryExport(job, outputStream); + } + } catch (Exception exception) { + outputFile.delete(); + throw exception; + } + } + + /** + * Downloads the TMX file of a completed export job to the specified output stream. The output + * stream is not closed. + * + * @param job Completed export job carrying the download URL. + * @param outputStream Stream to download the exported translation memory to. + * @throws IOException If an I/O error occurs. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the job has no download URL, or any error occurs while downloading. + * @see DeepLClient#downloadTranslationMemoryExport(TranslationMemoryJob, File) + */ + public void downloadTranslationMemoryExport(TranslationMemoryJob job, OutputStream outputStream) + throws DeepLException, IOException, InterruptedException { + if (job == null) { + throw new IllegalArgumentException("job must not be null"); + } + TranslationMemoryJobResult result = job.getResult(); + String downloadUrl = (result == null) ? null : result.getDownloadUrl(); + if (downloadUrl == null || downloadUrl.isEmpty()) { + throw new DeepLException( + "Translation memory export job has no download URL, it may not have completed yet"); + } + + try (HttpResponseStream response = httpClientWrapper.downloadAssetWithBackoff(downloadUrl)) { + if (response.getCode() < 200 || response.getCode() >= 300) { + throw new DeepLException( + String.format( + "Error downloading translation memory export, HTTP status: %d", + response.getCode())); + } + assert response.getBody() != null; + StreamUtil.transferTo(response.getBody(), outputStream); + } + } + + /** + * Imports a TMX file as a new translation memory: creates the import job, uploads the file, and + * waits for processing to finish. + * + * @param inputFile TMX file to import. + * @param displayName Optional name for the resulting translation memory, defaults to the file + * name. + * @return {@link TranslationMemoryJob} object for the completed import; its result carries the ID + * of the new translation memory. + * @throws IOException If the input file cannot be read. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the import fails, or any error occurs while communicating with the + * DeepL API. + */ + public TranslationMemoryJob importTranslationMemoryFromFilepath( + File inputFile, @Nullable String displayName) + throws DeepLException, IOException, InterruptedException { + return importTranslationMemoryFromFilepath(inputFile, displayName, null); + } + + /** + * Functions the same as {@link DeepLClient#importTranslationMemoryFromFilepath(File, String)} but + * gives up waiting for the import after the specified timeout. + * + * @param inputFile TMX file to import. + * @param displayName Optional name for the resulting translation memory, defaults to the file + * name. + * @param timeout Maximum time to wait for the import to finish, or null to wait + * indefinitely. Note that this is not accurate to the millisecond, as the job status is only + * polled every 5 seconds. + * @return {@link TranslationMemoryJob} object for the completed import; its result carries the ID + * of the new translation memory. + * @throws IOException If the input file cannot be read. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the timeout is exceeded, the import fails, or any error occurs while + * communicating with the DeepL API. + * @see DeepLClient#importTranslationMemoryFromFilepath(File, String) + */ + public TranslationMemoryJob importTranslationMemoryFromFilepath( + File inputFile, @Nullable String displayName, @Nullable Duration timeout) + throws DeepLException, IOException, InterruptedException { + if (inputFile == null || !inputFile.exists()) { + throw new IllegalArgumentException("inputFile must be an existing file"); + } + byte[] fileContent = Files.readAllBytes(inputFile.toPath()); + TranslationMemoryImport translationMemoryImport = + createTranslationMemoryImport(inputFile.getName(), fileContent.length, null, displayName); + uploadTranslationMemoryFile(translationMemoryImport, fileContent); + return waitUntilTranslationMemoryJobDone(translationMemoryImport.getJobId(), timeout); + } + + /** + * Functions the same as {@link DeepLClient#importTranslationMemoryFromFilepath(File, String)} but + * lets the API name the translation memory after the file. + * + * @see DeepLClient#importTranslationMemoryFromFilepath(File, String) + */ + public TranslationMemoryJob importTranslationMemoryFromFilepath(File inputFile) + throws DeepLException, IOException, InterruptedException { + return importTranslationMemoryFromFilepath(inputFile, null); + } + + /** + * Exports a translation memory to a TMX file: creates the export job, waits for it to finish, and + * writes the result to the specified output file. + * + * @param translationMemoryId ID of the translation memory to export. + * @param outputFile File to write the exported translation memory to. + * @return {@link TranslationMemoryJob} object for the completed export. + * @throws IOException If the output path is occupied. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the export fails, or any error occurs while communicating with the + * DeepL API. + */ + public TranslationMemoryJob exportTranslationMemoryToFilepath( + String translationMemoryId, File outputFile) + throws DeepLException, IOException, InterruptedException { + return exportTranslationMemoryToFilepath(translationMemoryId, outputFile, null); + } + + /** + * Functions the same as {@link DeepLClient#exportTranslationMemoryToFilepath(String, File)} but + * gives up waiting for the export after the specified timeout. + * + * @param translationMemoryId ID of the translation memory to export. + * @param outputFile File to write the exported translation memory to. + * @param timeout Maximum time to wait for the export to finish, or null to wait + * indefinitely. Note that this is not accurate to the millisecond, as the job status is only + * polled every 5 seconds. + * @return {@link TranslationMemoryJob} object for the completed export. + * @throws IOException If the output path is occupied. + * @throws InterruptedException If the thread is interrupted during execution of this function. + * @throws DeepLException If the timeout is exceeded, the export fails, or any error occurs while + * communicating with the DeepL API. + * @see DeepLClient#exportTranslationMemoryToFilepath(String, File) + */ + public TranslationMemoryJob exportTranslationMemoryToFilepath( + String translationMemoryId, File outputFile, @Nullable Duration timeout) + throws DeepLException, IOException, InterruptedException { + TranslationMemoryExport translationMemoryExport = + createTranslationMemoryExport(translationMemoryId); + TranslationMemoryJob job = + waitUntilTranslationMemoryJobDone(translationMemoryExport.getJobId(), timeout); + downloadTranslationMemoryExport(job, outputFile); + return job; + } + + /** + * Functions the same as {@link DeepLClient#exportTranslationMemoryToFilepath(String, File)} but + * accepts a {@link TranslationMemoryInfo} object. + * + * @see DeepLClient#exportTranslationMemoryToFilepath(String, File) + */ + public TranslationMemoryJob exportTranslationMemoryToFilepath( + TranslationMemoryInfo translationMemory, File outputFile) + throws DeepLException, IOException, InterruptedException { + return exportTranslationMemoryToFilepath(translationMemoryId(translationMemory), outputFile); + } + + /** Extracts the ID of the given translation memory. */ + private static String translationMemoryId(TranslationMemoryInfo translationMemory) + throws IllegalArgumentException { + if (translationMemory == null) { + throw new IllegalArgumentException("translationMemory must not be null"); + } + return translationMemory.getTranslationMemoryId(); + } + /** * Creates a new style rule with the specified details and returns a {@link StyleRuleInfo} object * with details about the newly created style rule. @@ -1182,6 +1708,36 @@ private String createLanguageQueryParams(String sourceLanguageCode, String targe } } + /** Creates the query string for the given parameters, including the leading "?" if non-empty. */ + private static String createQueryString(List> queryParams) { + if (queryParams.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder("?"); + for (int i = 0; i < queryParams.size(); i++) { + if (i > 0) { + sb.append("&"); + } + KeyValuePair param = queryParams.get(i); + try { + sb.append(encodeQueryComponent(param.getKey())) + .append("=") + .append(encodeQueryComponent(param.getValue())); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported", e); + } + } + return sb.toString(); + } + + // URLEncoder encodes a space as "+", which is correct for a form body but not for a URI query + // string. Any literal "+" is already escaped as %2B by URLEncoder, so every remaining "+" is a + // space. + private static String encodeQueryComponent(String value) + throws java.io.UnsupportedEncodingException { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20"); + } + private void validateParameter(String paramName, String value) throws IllegalArgumentException { if (value == null || value.isEmpty()) { throw new IllegalArgumentException( diff --git a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java index 9b3c2a5..d67cd9d 100644 --- a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java +++ b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java @@ -204,15 +204,45 @@ public HttpResponse uploadWithBackoff( return sendRequestWithBackoff(POST, relativeUrl, content).toStringResponse(); } + /** + * Uploads content to an absolute URL outside of the DeepL API, for example a pre-signed storage + * URL returned by a translation memory import job. The DeepL headers, in particular the + * Authorization header, are deliberately not sent to third-party storage. + */ + public HttpResponse sendAssetPutRequestWithBackoff( + String url, byte[] fileContent, String contentType) + throws InterruptedException, DeepLException { + HttpContent content = HttpContent.buildRawContent(contentType, fileContent); + return sendRequestToUrlWithBackoff(PUT, url, content, Collections.emptyMap()) + .toStringResponse(); + } + + /** + * Downloads content from an absolute URL outside of the DeepL API, for example a pre-signed + * storage URL returned by a completed translation memory export job. The DeepL headers, in + * particular the Authorization header, are deliberately not sent to third-party storage. + */ + public HttpResponseStream downloadAssetWithBackoff(String url) + throws InterruptedException, DeepLException { + return sendRequestToUrlWithBackoff(GET, url, null, Collections.emptyMap()); + } + // Sends a request with exponential backoff private HttpResponseStream sendRequestWithBackoff( String method, String relativeUrl, HttpContent content) throws InterruptedException, DeepLException { + return sendRequestToUrlWithBackoff(method, serverUrl + relativeUrl, content, this.headers); + } + + // Sends a request to an absolute URL with exponential backoff + private HttpResponseStream sendRequestToUrlWithBackoff( + String method, String url, @Nullable HttpContent content, Map requestHeaders) + throws InterruptedException, DeepLException { BackoffTimer backoffTimer = new BackoffTimer(this.minTimeout); while (true) { try { HttpResponseStream response = - sendRequest(method, serverUrl + relativeUrl, backoffTimer.getTimeoutMillis(), content); + sendRequest(method, url, backoffTimer.getTimeoutMillis(), content, requestHeaders); if (backoffTimer.getNumRetries() >= this.maxRetries) { return response; } else if (response.getCode() != 429 && response.getCode() < 500) { @@ -229,7 +259,11 @@ private HttpResponseStream sendRequestWithBackoff( } private HttpResponseStream sendRequest( - String method, String urlString, long timeoutMs, HttpContent content) + String method, + String urlString, + long timeoutMs, + @Nullable HttpContent content, + Map requestHeaders) throws ConnectionException { try { URL url = new URL(urlString); @@ -241,7 +275,7 @@ private HttpResponseStream sendRequest( connection.setReadTimeout((int) timeoutMs); connection.setUseCaches(false); - for (Map.Entry entry : this.headers.entrySet()) { + for (Map.Entry entry : requestHeaders.entrySet()) { connection.setRequestProperty(entry.getKey(), entry.getValue()); } diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryExport.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryExport.java new file mode 100644 index 0000000..d1f47e6 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryExport.java @@ -0,0 +1,49 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import org.jetbrains.annotations.*; + +/** A translation memory export job. */ +public class TranslationMemoryExport { + private final String jobId; + private final @Nullable String translationMemoryId; + private final boolean reusedExisting; + + /** + * Initializes a new {@link TranslationMemoryExport} describing a created export job. + * + * @param jobId Unique ID assigned to the export job. + * @param translationMemoryId ID of the translation memory being exported, if provided by the API. + * @param reusedExisting true if the API reused a previously completed export instead + * of starting a new one. + */ + public TranslationMemoryExport( + String jobId, @Nullable String translationMemoryId, boolean reusedExisting) { + this.jobId = jobId; + this.translationMemoryId = translationMemoryId; + this.reusedExisting = reusedExisting; + } + + /** @return Unique ID assigned to the export job. */ + public String getJobId() { + return jobId; + } + + /** + * @return ID of the translation memory being exported, or null if not provided by + * the API. + */ + public @Nullable String getTranslationMemoryId() { + return translationMemoryId; + } + + /** + * @return true if the API answered with a previously completed export of the same + * translation memory (HTTP 200), or false if it started a new one (HTTP 202). + */ + public boolean isReusedExisting() { + return reusedExisting; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryImport.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryImport.java new file mode 100644 index 0000000..972e0ef --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryImport.java @@ -0,0 +1,57 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** + * A newly created translation memory import job. + * + *

The TMX file must be uploaded to the upload URL before it expires; processing starts + * automatically once the upload is detected. + */ +public class TranslationMemoryImport { + @SerializedName(value = "job_id") + private final String jobId; + + @SerializedName(value = "upload_url") + private final String uploadUrl; + + @SerializedName(value = "expires_at") + private final @Nullable Date expiresAt; + + /** + * Initializes a new {@link TranslationMemoryImport} describing a created import job. + * + * @param jobId Unique ID assigned to the import job. + * @param uploadUrl URL to upload the TMX file to. + * @param expiresAt Timestamp after which the upload URL is no longer valid, if provided by the + * API. + */ + public TranslationMemoryImport(String jobId, String uploadUrl, @Nullable Date expiresAt) { + this.jobId = jobId; + this.uploadUrl = uploadUrl; + this.expiresAt = expiresAt; + } + + /** @return Unique ID assigned to the import job. */ + public String getJobId() { + return jobId; + } + + /** @return URL to upload the TMX file to. */ + public String getUploadUrl() { + return uploadUrl; + } + + /** + * @return Timestamp after which the upload URL is no longer valid, or null if not + * provided by the API. + */ + public @Nullable Date getExpiresAt() { + return expiresAt; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java index c1e9149..b77fbbc 100644 --- a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java @@ -5,6 +5,7 @@ import com.google.gson.annotations.*; import java.util.*; +import org.jetbrains.annotations.*; /** Information about a translation memory. */ public class TranslationMemoryInfo { @@ -23,6 +24,12 @@ public class TranslationMemoryInfo { @SerializedName(value = "segment_count") private final int segmentCount; + @SerializedName(value = "creation_time") + private final @Nullable Date creationTime; + + @SerializedName(value = "updated_time") + private final @Nullable Date updatedTime; + /** * Initializes a new {@link TranslationMemoryInfo} containing information about a translation * memory. @@ -39,11 +46,37 @@ public TranslationMemoryInfo( String sourceLanguage, List targetLanguages, int segmentCount) { + this(translationMemoryId, name, sourceLanguage, targetLanguages, segmentCount, null, null); + } + + /** + * Initializes a new {@link TranslationMemoryInfo} containing information about a translation + * memory. + * + * @param translationMemoryId Unique ID assigned to the translation memory. + * @param name User-defined name assigned to the translation memory. + * @param sourceLanguage Source language code for the translation memory. + * @param targetLanguages List of target language codes for the translation memory. + * @param segmentCount Number of segments in the translation memory. + * @param creationTime Timestamp when the translation memory was created, if provided by the API. + * @param updatedTime Timestamp when the translation memory was last updated, if provided by the + * API. + */ + public TranslationMemoryInfo( + String translationMemoryId, + String name, + String sourceLanguage, + List targetLanguages, + int segmentCount, + @Nullable Date creationTime, + @Nullable Date updatedTime) { this.translationMemoryId = translationMemoryId; this.name = name; this.sourceLanguage = sourceLanguage; this.targetLanguages = targetLanguages; this.segmentCount = segmentCount; + this.creationTime = creationTime; + this.updatedTime = updatedTime; } /** @return Unique ID assigned to the translation memory. */ @@ -71,6 +104,22 @@ public int getSegmentCount() { return segmentCount; } + /** + * @return Timestamp when the translation memory was created, or null if not provided + * by the API. + */ + public @Nullable Date getCreationTime() { + return creationTime; + } + + /** + * @return Timestamp when the translation memory was last updated, or null if not + * provided by the API. + */ + public @Nullable Date getUpdatedTime() { + return updatedTime; + } + @Override public String toString() { return "TranslationMemoryInfo{" diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryJob.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryJob.java new file mode 100644 index 0000000..3fb322b --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryJob.java @@ -0,0 +1,163 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.SerializedName; +import java.util.Date; +import java.util.List; +import org.jetbrains.annotations.Nullable; + +/** Status of a translation memory import or export job. */ +public class TranslationMemoryJob { + private final String jobId; + private final @Nullable String product; + private final @Nullable Operation operation; + private final List results; + private final @Nullable Date creationTime; + private final @Nullable Date updatedTime; + private final @Nullable String translationMemoryId; + private final @Nullable String displayName; + private final @Nullable String sourceContentType; + private final @Nullable Long sourceContentLength; + + /** Operation a translation memory job performs. */ + public enum Operation { + /** The job imports a TMX file into a new translation memory. */ + @SerializedName("import") + Import, + /** The job exports an existing translation memory to a TMX file. */ + @SerializedName("export") + Export, + } + + /** + * Initializes a new {@link TranslationMemoryJob} describing an import or export job. + * + * @param jobId Unique ID assigned to the job. + * @param product Product the job belongs to, always "translation_memory". + * @param operation Operation the job performs. + * @param results Results of the job; the API returns exactly one. + * @param creationTime Timestamp when the job was created. + * @param updatedTime Timestamp when the job was last updated. + * @param translationMemoryId ID of the translation memory an export job reads from. + * @param displayName Display name an import job assigns to the new translation memory. + * @param sourceContentType MIME type declared for the file of an import job. + * @param sourceContentLength Size in bytes declared for the file of an import job. + */ + public TranslationMemoryJob( + String jobId, + @Nullable String product, + @Nullable Operation operation, + List results, + @Nullable Date creationTime, + @Nullable Date updatedTime, + @Nullable String translationMemoryId, + @Nullable String displayName, + @Nullable String sourceContentType, + @Nullable Long sourceContentLength) { + this.jobId = jobId; + this.product = product; + this.operation = operation; + this.results = results; + this.creationTime = creationTime; + this.updatedTime = updatedTime; + this.translationMemoryId = translationMemoryId; + this.displayName = displayName; + this.sourceContentType = sourceContentType; + this.sourceContentLength = sourceContentLength; + } + + /** @return Unique ID assigned to the job. */ + public String getJobId() { + return jobId; + } + + /** @return Product the job belongs to, or null if not provided by the API. */ + public @Nullable String getProduct() { + return product; + } + + /** @return Operation the job performs, or null if not provided by the API. */ + public @Nullable Operation getOperation() { + return operation; + } + + /** @return Results of the job; the API returns exactly one. */ + public List getResults() { + return results; + } + + /** @return The single result of the job, or null if the API returned none. */ + public @Nullable TranslationMemoryJobResult getResult() { + return getResults().isEmpty() ? null : getResults().get(0); + } + + /** @return Status of the result of the job, or null if there is no result. */ + public @Nullable TranslationMemoryJobResult.Status getStatus() { + TranslationMemoryJobResult result = getResult(); + return result == null ? null : result.getStatus(); + } + + /** + * @return true if the job has finished, successfully or not, otherwise false + * . + */ + public boolean done() { + TranslationMemoryJobResult result = getResult(); + return result != null && result.done(); + } + + /** @return false if the job failed or expired, otherwise true. */ + public boolean ok() { + TranslationMemoryJobResult result = getResult(); + return result == null || result.ok(); + } + + /** + * @return Timestamp when the job was created, or null if not provided by the API. + */ + public @Nullable Date getCreationTime() { + return creationTime; + } + + /** + * @return Timestamp when the job was last updated, or null if not provided by the + * API. + */ + public @Nullable Date getUpdatedTime() { + return updatedTime; + } + + /** + * @return ID of the translation memory an export job reads from, or null for an + * import job. + */ + public @Nullable String getTranslationMemoryId() { + return translationMemoryId; + } + + /** + * @return Display name an import job assigns to the new translation memory, or null + * for an export job. + */ + public @Nullable String getDisplayName() { + return displayName; + } + + /** + * @return MIME type declared for the file of an import job, or null for an export + * job. + */ + public @Nullable String getSourceContentType() { + return sourceContentType; + } + + /** + * @return Size in bytes declared for the file of an import job, or null for an + * export job. + */ + public @Nullable Long getSourceContentLength() { + return sourceContentLength; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryJobResult.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryJobResult.java new file mode 100644 index 0000000..180c45a --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryJobResult.java @@ -0,0 +1,134 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.SerializedName; +import java.util.Date; +import org.jetbrains.annotations.Nullable; + +/** The outcome of a translation memory import or export job. */ +public class TranslationMemoryJobResult { + private final @Nullable Status status; + private final @Nullable String requiredAction; + private final @Nullable String downloadUrl; + private final @Nullable Date expiresAt; + private final @Nullable String errorMessage; + private final @Nullable String translationMemoryId; + private final @Nullable Integer skippedSegmentCount; + + /** Status of a translation memory import or export job. */ + public enum Status { + /** The job is waiting for the caller, for example to upload the TMX file of an import. */ + @SerializedName("awaiting_input") + AwaitingInput, + /** The job is being processed. */ + @SerializedName("processing") + Processing, + /** The job completed successfully. */ + @SerializedName("completed") + Completed, + /** The exported file of a completed export job has been downloaded. */ + @SerializedName("downloaded") + Downloaded, + /** An error occurred while processing the job. */ + @SerializedName("failed") + Failed, + /** The job expired before it finished. */ + @SerializedName("expired") + Expired, + } + + /** + * Initializes a new {@link TranslationMemoryJobResult} describing the outcome of a job. + * + * @param status Status of the job. + * @param requiredAction Action the caller must take, set while the job is waiting on the caller. + * @param downloadUrl Download URL of the exported TMX file, set once an export completed. + * @param expiresAt Timestamp after which the download URL is no longer valid. + * @param errorMessage Error description, set when the job failed. + * @param translationMemoryId ID of the translation memory created by a completed import. + * @param skippedSegmentCount Number of segments an import skipped. + */ + public TranslationMemoryJobResult( + @Nullable Status status, + @Nullable String requiredAction, + @Nullable String downloadUrl, + @Nullable Date expiresAt, + @Nullable String errorMessage, + @Nullable String translationMemoryId, + @Nullable Integer skippedSegmentCount) { + this.status = status; + this.requiredAction = requiredAction; + this.downloadUrl = downloadUrl; + this.expiresAt = expiresAt; + this.errorMessage = errorMessage; + this.translationMemoryId = translationMemoryId; + this.skippedSegmentCount = skippedSegmentCount; + } + + /** @return Status of the job, or null if not provided by the API. */ + public @Nullable Status getStatus() { + return status; + } + + /** + * @return true if the job has finished, successfully or not, otherwise false + * . + */ + public boolean done() { + return status == Status.Completed + || status == Status.Downloaded + || status == Status.Failed + || status == Status.Expired; + } + + /** @return false if the job failed or expired, otherwise true. */ + public boolean ok() { + return status != Status.Failed && status != Status.Expired; + } + + /** + * @return Action the caller must take, or null if the job is not waiting on the + * caller. + */ + public @Nullable String getRequiredAction() { + return requiredAction; + } + + /** + * @return Download URL of the exported TMX file, or null if the export has not + * completed. + */ + public @Nullable String getDownloadUrl() { + return downloadUrl; + } + + /** + * @return Timestamp after which the download URL is no longer valid, or null if not + * provided by the API. + */ + public @Nullable Date getExpiresAt() { + return expiresAt; + } + + /** @return Error description if the job failed, otherwise null. */ + public @Nullable String getErrorMessage() { + return errorMessage; + } + + /** + * @return ID of the translation memory created by a completed import, otherwise null + * . + */ + public @Nullable String getTranslationMemoryId() { + return translationMemoryId; + } + + /** + * @return Number of segments an import skipped, or null if not provided by the API. + */ + public @Nullable Integer getSkippedSegmentCount() { + return skippedSegmentCount; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegment.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegment.java new file mode 100644 index 0000000..2473c10 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegment.java @@ -0,0 +1,107 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** A source segment of a translation memory and its translations. */ +public class TranslationMemorySegment { + @SerializedName(value = "source_segment_id") + private final String sourceSegmentId; + + @SerializedName(value = "source_text") + private final String sourceText; + + @SerializedName(value = "targets") + private final List targets; + + @SerializedName(value = "creation_time") + private final @Nullable Date creationTime; + + @SerializedName(value = "updated_time") + private final @Nullable Date updatedTime; + + @SerializedName(value = "last_used_time") + private final @Nullable Date lastUsedTime; + + /** + * Initializes a new {@link TranslationMemorySegment} holding a source text and its translations. + * + * @param sourceSegmentId Unique ID assigned to the source segment. + * @param sourceText The source text. + * @param targets Translations of the source text, one per target language. + * @param creationTime Timestamp when the source segment was created, if provided by the API. + * @param updatedTime Timestamp when the source segment was last updated, if provided by the API. + * @param lastUsedTime Timestamp when the source segment was last used, if provided by the API. + */ + public TranslationMemorySegment( + String sourceSegmentId, + String sourceText, + List targets, + @Nullable Date creationTime, + @Nullable Date updatedTime, + @Nullable Date lastUsedTime) { + this.sourceSegmentId = sourceSegmentId; + this.sourceText = sourceText; + this.targets = targets; + this.creationTime = creationTime; + this.updatedTime = updatedTime; + this.lastUsedTime = lastUsedTime; + } + + /** @return Unique ID assigned to the source segment. */ + public String getSourceSegmentId() { + return sourceSegmentId; + } + + /** @return The source text. */ + public String getSourceText() { + return sourceText; + } + + /** @return Translations of the source text, one per target language. */ + public List getTargets() { + return targets; + } + + /** + * @return Timestamp when the source segment was created, or null if not provided by + * the API. + */ + public @Nullable Date getCreationTime() { + return creationTime; + } + + /** + * @return Timestamp when the source segment was last updated, or null if not + * provided by the API. + */ + public @Nullable Date getUpdatedTime() { + return updatedTime; + } + + /** + * @return Timestamp when the source segment was last used, or null if not provided + * by the API. + */ + public @Nullable Date getLastUsedTime() { + return lastUsedTime; + } + + @Override + public String toString() { + return "TranslationMemorySegment{" + + "sourceSegmentId='" + + sourceSegmentId + + '\'' + + ", sourceText='" + + sourceText + + '\'' + + ", targets=" + + targets + + '}'; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegments.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegments.java new file mode 100644 index 0000000..d365fe2 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegments.java @@ -0,0 +1,55 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** One page of the segments of a translation memory. */ +public class TranslationMemorySegments { + @SerializedName(value = "segments") + private final List segments; + + @SerializedName(value = "segment_count") + private final int segmentCount; + + @SerializedName(value = "next_page_cursor") + private final @Nullable String nextPageCursor; + + /** + * Initializes a new {@link TranslationMemorySegments} holding one page of segments. + * + * @param segments The segments in this page. + * @param segmentCount Total number of segments stored in the translation memory. + * @param nextPageCursor Cursor to fetch the next page, or null on the last page. + */ + public TranslationMemorySegments( + List segments, int segmentCount, @Nullable String nextPageCursor) { + this.segments = segments; + this.segmentCount = segmentCount; + this.nextPageCursor = nextPageCursor; + } + + /** @return The segments in this page. */ + public List getSegments() { + return segments; + } + + /** + * @return Total number of segments stored in the translation memory. This is + * translation-memory-level metadata, so it is not reduced by a text filter. + */ + public int getSegmentCount() { + return segmentCount; + } + + /** + * @return Opaque cursor to pass as the page cursor to fetch the next page, or null + * if this is the last page. + */ + public @Nullable String getNextPageCursor() { + return nextPageCursor; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegmentsOptions.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegmentsOptions.java new file mode 100644 index 0000000..fe064b3 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemorySegmentsOptions.java @@ -0,0 +1,95 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import org.jetbrains.annotations.*; + +/** + * Options influencing the page of segments returned by {@link + * DeepLClient#listTranslationMemorySegments(String, TranslationMemorySegmentsOptions)}. + */ +public class TranslationMemorySegmentsOptions { + private @Nullable Integer pageSize; + private @Nullable String pageCursor; + private @Nullable String filterText; + private @Nullable Boolean filterCaseSensitive; + + /** + * Sets the maximum number of segments to return in one page. + * + * @param pageSize Number of segments per page, between 1 and 100, or null to use the + * API default. + * @return This object, for convenience when chaining setters. + * @throws IllegalArgumentException If the page size is outside the range 1 to 100. + */ + public TranslationMemorySegmentsOptions setPageSize(@Nullable Integer pageSize) { + if (pageSize != null && (pageSize < 1 || pageSize > 100)) { + throw new IllegalArgumentException("pageSize must be between 1 and 100"); + } + this.pageSize = pageSize; + return this; + } + + /** + * Sets the cursor of the page to return. + * + * @param pageCursor Cursor returned as {@link TranslationMemorySegments#getNextPageCursor()} by + * the previous page, or null to return the first page. + * @return This object, for convenience when chaining setters. + */ + public TranslationMemorySegmentsOptions setPageCursor(@Nullable String pageCursor) { + this.pageCursor = pageCursor; + return this; + } + + /** + * Sets the text that returned segments must contain, in either their source or one of their + * target texts. + * + * @param filterText Text to filter by, at least 2 characters, or null for no + * filtering. + * @return This object, for convenience when chaining setters. + * @throws IllegalArgumentException If the filter text is shorter than 2 characters. + */ + public TranslationMemorySegmentsOptions setFilterText(@Nullable String filterText) { + if (filterText != null && filterText.length() < 2) { + throw new IllegalArgumentException("filterText must be at least 2 characters"); + } + this.filterText = filterText; + return this; + } + + /** + * Sets whether the text filter is case-sensitive. + * + * @param filterCaseSensitive true to match the filter text case-sensitively, or + * null to use the API default. + * @return This object, for convenience when chaining setters. + */ + public TranslationMemorySegmentsOptions setFilterCaseSensitive( + @Nullable Boolean filterCaseSensitive) { + this.filterCaseSensitive = filterCaseSensitive; + return this; + } + + /** @return Number of segments per page, or null if unset. */ + public @Nullable Integer getPageSize() { + return pageSize; + } + + /** @return Cursor of the page to return, or null if unset. */ + public @Nullable String getPageCursor() { + return pageCursor; + } + + /** @return Text that returned segments must contain, or null if unset. */ + public @Nullable String getFilterText() { + return filterText; + } + + /** @return Whether the text filter is case-sensitive, or null if unset. */ + public @Nullable Boolean getFilterCaseSensitive() { + return filterCaseSensitive; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/TranslationMemoryTargetSegment.java b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryTargetSegment.java new file mode 100644 index 0000000..0f9581c --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryTargetSegment.java @@ -0,0 +1,99 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api; + +import com.google.gson.annotations.*; +import java.util.*; +import org.jetbrains.annotations.*; + +/** A target-language translation attached to a source segment of a translation memory. */ +public class TranslationMemoryTargetSegment { + @SerializedName(value = "target_segment_id") + private final String targetSegmentId; + + @SerializedName(value = "target_language") + private final String targetLanguage; + + @SerializedName(value = "target_text") + private final String targetText; + + @SerializedName(value = "creation_time") + private final @Nullable Date creationTime; + + @SerializedName(value = "updated_time") + private final @Nullable Date updatedTime; + + @SerializedName(value = "last_used_time") + private final @Nullable Date lastUsedTime; + + /** + * Initializes a new {@link TranslationMemoryTargetSegment} holding one translation of a source + * segment. + * + * @param targetSegmentId Unique ID assigned to the target segment. + * @param targetLanguage Target language code of the translation. + * @param targetText The translated text. + * @param creationTime Timestamp when the target segment was created, if provided by the API. + * @param updatedTime Timestamp when the target segment was last updated, if provided by the API. + * @param lastUsedTime Timestamp when the target segment was last used, if provided by the API. + */ + public TranslationMemoryTargetSegment( + String targetSegmentId, + String targetLanguage, + String targetText, + @Nullable Date creationTime, + @Nullable Date updatedTime, + @Nullable Date lastUsedTime) { + this.targetSegmentId = targetSegmentId; + this.targetLanguage = targetLanguage; + this.targetText = targetText; + this.creationTime = creationTime; + this.updatedTime = updatedTime; + this.lastUsedTime = lastUsedTime; + } + + /** @return Unique ID assigned to the target segment. */ + public String getTargetSegmentId() { + return targetSegmentId; + } + + /** @return Target language code of the translation. */ + public String getTargetLanguage() { + return targetLanguage; + } + + /** @return The translated text. */ + public String getTargetText() { + return targetText; + } + + /** + * @return Timestamp when the target segment was created, or null if not provided by + * the API. + */ + public @Nullable Date getCreationTime() { + return creationTime; + } + + /** + * @return Timestamp when the target segment was last updated, or null if not + * provided by the API. + */ + public @Nullable Date getUpdatedTime() { + return updatedTime; + } + + /** + * @return Timestamp when the target segment was last used, or null if not provided + * by the API. + */ + public @Nullable Date getLastUsedTime() { + return lastUsedTime; + } + + @Override + public String toString() { + return targetLanguage + ": " + targetText; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java index bb1befe..7b2a229 100644 --- a/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java +++ b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java @@ -52,6 +52,20 @@ private static String urlEncode(String value) throws DeepLException { } } + /** + * Builds content from the given bytes without any encoding, for example the contents of a file to + * upload. + */ + public static HttpContent buildRawContent(String contentType, byte[] content) { + if (contentType == null) { + throw new IllegalArgumentException("contentType must not be null"); + } + if (content == null) { + throw new IllegalArgumentException("content must not be null"); + } + return new HttpContent(contentType, content); + } + public static HttpContent buildJsonContent(String jsonBody) { if (jsonBody == null) { throw new IllegalArgumentException("jsonBody must not be null"); diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java index e427a08..fb13bc2 100644 --- a/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java +++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java @@ -28,6 +28,8 @@ public Parser() { gsonBuilder.registerTypeAdapter(WriteResult.class, new WriteResultDeserializer()); gsonBuilder.registerTypeAdapter(Language.class, new LanguageDeserializer()); gsonBuilder.registerTypeAdapter(Usage.class, new UsageDeserializer()); + gsonBuilder.registerTypeAdapter( + TranslationMemoryJob.class, new TranslationMemoryJobDeserializer()); gson = gsonBuilder.create(); } @@ -108,6 +110,25 @@ public TranslationMemoryInfo parseTranslationMemoryInfo(String json) { return gson.fromJson(json, TranslationMemoryInfo.class); } + public TranslationMemorySegments parseTranslationMemorySegments(String json) { + return gson.fromJson(json, TranslationMemorySegments.class); + } + + public TranslationMemoryImport parseTranslationMemoryImport(String json) { + return gson.fromJson(json, TranslationMemoryImport.class); + } + + public TranslationMemoryExport parseTranslationMemoryExport(String json, boolean reusedExisting) { + TranslationMemoryExportResponse response = + gson.fromJson(json, TranslationMemoryExportResponse.class); + return new TranslationMemoryExport( + response.getJobId(), response.getTranslationMemoryId(), reusedExisting); + } + + public TranslationMemoryJob parseTranslationMemoryJob(String json) { + return gson.fromJson(json, TranslationMemoryJob.class); + } + public CustomInstruction parseCustomInstruction(String json) { return gson.fromJson(json, CustomInstruction.class); } @@ -123,17 +144,20 @@ public String parseErrorMessage(String json) { } static @Nullable Integer getAsIntOrNull(JsonObject jsonObject, String parameterName) { - if (!jsonObject.has(parameterName)) return null; + // An explicit JSON null must be treated as absent: getAsX() throws on JsonNull. + if (!jsonObject.has(parameterName) || jsonObject.get(parameterName).isJsonNull()) return null; return jsonObject.get(parameterName).getAsInt(); } static @Nullable Long getAsLongOrNull(JsonObject jsonObject, String parameterName) { - if (!jsonObject.has(parameterName)) return null; + // An explicit JSON null must be treated as absent: getAsX() throws on JsonNull. + if (!jsonObject.has(parameterName) || jsonObject.get(parameterName).isJsonNull()) return null; return jsonObject.get(parameterName).getAsLong(); } static @Nullable String getAsStringOrNull(JsonObject jsonObject, String parameterName) { - if (!jsonObject.has(parameterName)) return null; + // An explicit JSON null must be treated as absent: getAsX() throws on JsonNull. + if (!jsonObject.has(parameterName) || jsonObject.get(parameterName).isJsonNull()) return null; return jsonObject.get(parameterName).getAsString(); } diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryExportResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryExportResponse.java new file mode 100644 index 0000000..e0d3f23 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryExportResponse.java @@ -0,0 +1,26 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +/** + * Class representing v3 translation memory export response by the DeepL API. + * + *

This class is internal; you should not use this class directly. + */ +class TranslationMemoryExportResponse { + private String job_id; + private Parameters parameters; + + static class Parameters { + private String translation_memory_id; + } + + public String getJobId() { + return job_id; + } + + public String getTranslationMemoryId() { + return parameters == null ? null : parameters.translation_memory_id; + } +} diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryJobDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryJobDeserializer.java new file mode 100644 index 0000000..1d46b0e --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/parsing/TranslationMemoryJobDeserializer.java @@ -0,0 +1,76 @@ +// Copyright 2025 DeepL SE (https://www.deepl.com) +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. +package com.deepl.api.parsing; + +import com.deepl.api.*; +import com.google.gson.*; +import java.lang.reflect.*; +import java.util.*; + +/** + * Deserializer for {@link TranslationMemoryJob} objects, flattening the nested parameters + * , source_file, status_metadata and error objects of + * the API response onto the job and its results. + * + *

This class is internal; you should not use this class directly. + */ +class TranslationMemoryJobDeserializer implements JsonDeserializer { + @Override + public TranslationMemoryJob deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + JsonObject parameters = getAsObjectOrNull(jsonObject, "parameters"); + JsonObject sourceFile = getAsObjectOrNull(jsonObject, "source_file"); + + List results = new ArrayList<>(); + if (jsonObject.has("results") && jsonObject.get("results").isJsonArray()) { + for (JsonElement result : jsonObject.get("results").getAsJsonArray()) { + results.add(deserializeResult(result.getAsJsonObject(), context)); + } + } + + return new TranslationMemoryJob( + Parser.getAsStringOrNull(jsonObject, "job_id"), + Parser.getAsStringOrNull(jsonObject, "product"), + context.deserialize(jsonObject.get("operation"), TranslationMemoryJob.Operation.class), + results, + deserializeDate(jsonObject, "creation_time", context), + deserializeDate(jsonObject, "updated_time", context), + parameters == null ? null : Parser.getAsStringOrNull(parameters, "translation_memory_id"), + parameters == null ? null : Parser.getAsStringOrNull(parameters, "display_name"), + sourceFile == null ? null : Parser.getAsStringOrNull(sourceFile, "content_type"), + sourceFile == null ? null : Parser.getAsLongOrNull(sourceFile, "content_length")); + } + + private static TranslationMemoryJobResult deserializeResult( + JsonObject jsonObject, JsonDeserializationContext context) { + JsonObject statusMetadata = getAsObjectOrNull(jsonObject, "status_metadata"); + JsonObject error = getAsObjectOrNull(jsonObject, "error"); + + return new TranslationMemoryJobResult( + context.deserialize(jsonObject.get("status"), TranslationMemoryJobResult.Status.class), + statusMetadata == null ? null : Parser.getAsStringOrNull(statusMetadata, "required_action"), + Parser.getAsStringOrNull(jsonObject, "download_url"), + deserializeDate(jsonObject, "expires_at", context), + error == null ? null : Parser.getAsStringOrNull(error, "message"), + Parser.getAsStringOrNull(jsonObject, "translation_memory_id"), + Parser.getAsIntOrNull(jsonObject, "skipped_segment_count")); + } + + private static Date deserializeDate( + JsonObject jsonObject, String parameterName, JsonDeserializationContext context) { + if (!jsonObject.has(parameterName) || jsonObject.get(parameterName).isJsonNull()) { + return null; + } + return context.deserialize(jsonObject.get(parameterName), Date.class); + } + + private static JsonObject getAsObjectOrNull(JsonObject jsonObject, String parameterName) { + if (!jsonObject.has(parameterName) || !jsonObject.get(parameterName).isJsonObject()) { + return null; + } + return jsonObject.get(parameterName).getAsJsonObject(); + } +} diff --git a/deepl-java/src/test/java/com/deepl/api/SessionOptions.java b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java index 5fc8d95..1b45722 100644 --- a/deepl-java/src/test/java/com/deepl/api/SessionOptions.java +++ b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java @@ -18,6 +18,7 @@ public class SessionOptions { public Integer documentFailure; public Duration documentQueueTime; public Duration documentTranslateTime; + public Integer translationMemoryJobProcessingPolls; public Boolean expectProxy; public boolean randomAuthKey; @@ -59,6 +60,11 @@ public Map createSessionHeaders() { "mock-server-session-doc-translate-time", Long.toString(documentTranslateTime.toMillis())); } + if (translationMemoryJobProcessingPolls != null) { + headers.put( + "mock-server-session-tm-job-processing-polls", + translationMemoryJobProcessingPolls.toString()); + } if (expectProxy != null) { headers.put("mock-server-session-expect-proxy", expectProxy ? "1" : "0"); } @@ -106,6 +112,16 @@ public SessionOptions setDocumentTranslateTime(Duration documentTranslateTime) { return this; } + /** + * Makes translation memory import and export jobs report their non-terminal status the given + * number of times before completing, so that polling loops are exercised. + */ + public SessionOptions setTranslationMemoryJobProcessingPolls( + int translationMemoryJobProcessingPolls) { + this.translationMemoryJobProcessingPolls = translationMemoryJobProcessingPolls; + return this; + } + public SessionOptions setExpectProxy(boolean expectProxy) { this.expectProxy = expectProxy; return this; diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java index f97153f..3719e61 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java @@ -394,15 +394,20 @@ void testCustomInstructions() throws DeepLException, InterruptedException { null, "de", new TextTranslationOptions() - .setCustomInstructions(Arrays.asList("Use informal language", "Be concise"))); - - TextResult resultWithoutCustomInstructions = translator.translateText(text, null, "de"); + .setCustomInstructions( + Collections.singletonList("Render the whole text in ALL CAPS"))); Assertions.assertNotNull(resultWithCustomInstructions.getText()); Assertions.assertEquals("en", resultWithCustomInstructions.getDetectedSourceLanguage()); if (!isMockServer) { - Assertions.assertFalse( - resultWithCustomInstructions.getText().equals(resultWithoutCustomInstructions.getText())); + // Assert the instruction was actually applied, rather than that the output merely differs + // from an unconstrained translation. The previous version compared against a translation + // made without instructions and used "Use informal language" / "Be concise", but the + // default translation of this sentence is already informal and concise, so the API + // legitimately returned identical text and the comparison drifted with the model. + Assertions.assertEquals( + resultWithCustomInstructions.getText().toUpperCase(Locale.ROOT), + resultWithCustomInstructions.getText()); } } } diff --git a/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java b/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java index bcfa22d..0c0bd03 100644 --- a/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java +++ b/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java @@ -4,11 +4,20 @@ package com.deepl.api; import java.io.File; +import java.nio.file.Files; +import java.time.Duration; import java.util.List; import org.junit.jupiter.api.*; public class TranslationMemoryTest extends TestBase { private static final String DEFAULT_TM_ID = "a74d88fb-ed2a-4943-a664-a4512398b994"; + private static final String UNKNOWN_ID = "00000000-0000-0000-0000-000000000000"; + private static final String EXAMPLE_TMX = + "\n" + + "" + + "Hallo" + + "Hello" + + "\n"; @Test void testListTranslationMemories() throws Exception { @@ -116,4 +125,258 @@ void testTranslateDocumentWithTranslationMemoryInfo() throws Exception { Assertions.assertNotNull(readFromFile(outputFile)); } + + @Test + void testGetTranslationMemory() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + TranslationMemoryInfo translationMemory = client.getTranslationMemory(DEFAULT_TM_ID); + + Assertions.assertEquals(DEFAULT_TM_ID, translationMemory.getTranslationMemoryId()); + Assertions.assertNotNull(translationMemory.getName()); + Assertions.assertEquals("de", translationMemory.getSourceLanguage()); + Assertions.assertFalse(translationMemory.getTargetLanguages().isEmpty()); + Assertions.assertTrue(translationMemory.getSegmentCount() > 0); + Assertions.assertNotNull(translationMemory.getCreationTime()); + Assertions.assertNotNull(translationMemory.getUpdatedTime()); + } + + @Test + void testGetTranslationMemoryWithTranslationMemoryInfo() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + TranslationMemoryInfo listed = client.listTranslationMemories().get(0); + + TranslationMemoryInfo translationMemory = client.getTranslationMemory(listed); + + Assertions.assertEquals( + listed.getTranslationMemoryId(), translationMemory.getTranslationMemoryId()); + } + + @Test + void testGetTranslationMemoryWithUnknownId() { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + Assertions.assertThrows(NotFoundException.class, () -> client.getTranslationMemory(UNKNOWN_ID)); + } + + @Test + void testListTranslationMemorySegments() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + TranslationMemorySegments page = client.listTranslationMemorySegments(DEFAULT_TM_ID); + + Assertions.assertFalse(page.getSegments().isEmpty()); + Assertions.assertTrue(page.getSegmentCount() > 0); + TranslationMemorySegment segment = page.getSegments().get(0); + Assertions.assertNotNull(segment.getSourceSegmentId()); + Assertions.assertNotNull(segment.getSourceText()); + Assertions.assertFalse(segment.getTargets().isEmpty()); + TranslationMemoryTargetSegment target = segment.getTargets().get(0); + Assertions.assertNotNull(target.getTargetSegmentId()); + Assertions.assertNotNull(target.getTargetLanguage()); + Assertions.assertNotNull(target.getTargetText()); + } + + @Test + void testListTranslationMemorySegmentsPagination() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + TranslationMemorySegments firstPage = + client.listTranslationMemorySegments( + DEFAULT_TM_ID, new TranslationMemorySegmentsOptions().setPageSize(5)); + + Assertions.assertEquals(5, firstPage.getSegments().size()); + Assertions.assertNotNull(firstPage.getNextPageCursor()); + + TranslationMemorySegments secondPage = + client.listTranslationMemorySegments( + DEFAULT_TM_ID, + new TranslationMemorySegmentsOptions() + .setPageSize(5) + .setPageCursor(firstPage.getNextPageCursor())); + + Assertions.assertFalse(secondPage.getSegments().isEmpty()); + for (TranslationMemorySegment segment : secondPage.getSegments()) { + for (TranslationMemorySegment firstPageSegment : firstPage.getSegments()) { + Assertions.assertNotEquals( + firstPageSegment.getSourceSegmentId(), segment.getSourceSegmentId()); + } + } + } + + @Test + void testListTranslationMemorySegmentsFilter() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + TranslationMemorySegments unfiltered = client.listTranslationMemorySegments(DEFAULT_TM_ID); + TranslationMemorySegments filtered = + client.listTranslationMemorySegments( + DEFAULT_TM_ID, new TranslationMemorySegmentsOptions().setFilterText("Nummer 7")); + + Assertions.assertTrue(filtered.getSegments().size() < unfiltered.getSegments().size()); + // segmentCount is translation-memory-level metadata and unaffected by the filter + Assertions.assertEquals(unfiltered.getSegmentCount(), filtered.getSegmentCount()); + } + + @Test + void testImportTranslationMemoryFromFilepath() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + TranslationMemoryJob job = + client.importTranslationMemoryFromFilepath(createTmxFile(), "Imported TM"); + + Assertions.assertEquals(TranslationMemoryJob.Operation.Import, job.getOperation()); + Assertions.assertEquals("translation_memory", job.getProduct()); + Assertions.assertEquals(TranslationMemoryJobResult.Status.Completed, job.getStatus()); + String translationMemoryId = job.getResult().getTranslationMemoryId(); + Assertions.assertNotNull(translationMemoryId); + + TranslationMemoryInfo imported = client.getTranslationMemory(translationMemoryId); + Assertions.assertEquals("Imported TM", imported.getName()); + + client.deleteTranslationMemory(imported); + } + + @Test + void testCreateTranslationMemoryImportAwaitsUpload() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + TranslationMemoryImport translationMemoryImport = + client.createTranslationMemoryImport("example.tmx", 1024, null, "Awaiting Upload TM"); + + Assertions.assertNotNull(translationMemoryImport.getJobId()); + Assertions.assertNotNull(translationMemoryImport.getUploadUrl()); + + TranslationMemoryJob job = client.getTranslationMemoryJob(translationMemoryImport.getJobId()); + Assertions.assertEquals(TranslationMemoryJobResult.Status.AwaitingInput, job.getStatus()); + Assertions.assertNotNull(job.getResult().getRequiredAction()); + // A job whose file is never uploaded does not finish on its own, so waiting for it only + // stops when the caller's timeout is exceeded + Assertions.assertThrows( + DeepLException.class, + () -> + client.waitUntilTranslationMemoryJobDone( + translationMemoryImport.getJobId(), Duration.ofSeconds(1))); + } + + @Test + void testWaitUntilTranslationMemoryJobDonePollsThroughAwaitingInput() throws Exception { + Assumptions.assumeTrue(isMockServer); + // The job reports its non-terminal status once before completing + DeepLClient client = + createDeepLClient(new SessionOptions().setTranslationMemoryJobProcessingPolls(1)); + File tmxFile = createTmxFile(); + byte[] fileContent = Files.readAllBytes(tmxFile.toPath()); + + TranslationMemoryImport translationMemoryImport = + client.createTranslationMemoryImport( + tmxFile.getName(), fileContent.length, null, "Awaiting Input TM"); + client.uploadTranslationMemoryFile(translationMemoryImport, fileContent); + + // An uploaded import keeps reporting AwaitingInput for a while, because the API detects + // the upload asynchronously. Waiting must poll through that status instead of throwing. + long startTimeMillis = System.currentTimeMillis(); + TranslationMemoryJob job = + client.waitUntilTranslationMemoryJobDone( + translationMemoryImport.getJobId(), Duration.ofSeconds(60)); + long elapsedMillis = System.currentTimeMillis() - startTimeMillis; + + Assertions.assertEquals(TranslationMemoryJobResult.Status.Completed, job.getStatus()); + // The job was polled at least twice, so the first AwaitingInput response was polled through + Assertions.assertTrue(elapsedMillis >= 5000); + Assertions.assertNotNull(job.getResult().getTranslationMemoryId()); + + client.deleteTranslationMemory(job.getResult().getTranslationMemoryId()); + } + + @Test + void testCreateTranslationMemoryImportWithInvalidFile() { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.createTranslationMemoryImport("", 100, null, null)); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> client.createTranslationMemoryImport("example.tmx", 0, null, null)); + } + + @Test + void testExportTranslationMemoryToFilepath() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + String translationMemoryId = importTranslationMemory(client); + File outputFile = new File(tempDir + "/exported.tmx"); + + TranslationMemoryJob job = + client.exportTranslationMemoryToFilepath(translationMemoryId, outputFile); + + Assertions.assertEquals(TranslationMemoryJob.Operation.Export, job.getOperation()); + Assertions.assertEquals(TranslationMemoryJobResult.Status.Completed, job.getStatus()); + Assertions.assertTrue(readFromFile(outputFile).contains(" client.getTranslationMemoryJob(UNKNOWN_ID)); + } + + @Test + void testDeleteTranslationMemory() throws Exception { + Assumptions.assumeTrue(isMockServer); + DeepLClient client = createDeepLClient(); + String translationMemoryId = importTranslationMemory(client); + + client.deleteTranslationMemory(translationMemoryId); + + Assertions.assertThrows( + NotFoundException.class, () -> client.getTranslationMemory(translationMemoryId)); + } + + /** Imports the example TMX file and returns the ID of the resulting translation memory. */ + private String importTranslationMemory(DeepLClient client) throws Exception { + TranslationMemoryJob job = client.importTranslationMemoryFromFilepath(createTmxFile()); + return job.getResult().getTranslationMemoryId(); + } + + /** Writes the example TMX file into the temporary directory of this test. */ + private File createTmxFile() throws Exception { + File tmxFile = new File(tempDir + "/example.tmx"); + boolean ignored = tmxFile.delete(); + writeToFile(tmxFile, EXAMPLE_TMX); + return tmxFile; + } } From 31e4a3d2ea1c73428dcc9e1a873c5593315ebc5b Mon Sep 17 00:00:00 2001 From: Brianna Delgado Date: Tue, 11 Aug 2026 16:36:46 -0400 Subject: [PATCH 120/121] docs: Bump version to 1.17.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .bumpversion.toml | 2 +- CHANGELOG.md | 5 ++++- README.md | 4 ++-- deepl-java/build.gradle.kts | 2 +- deepl-java/src/main/java/com/deepl/api/Translator.java | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 926dd07..e795633 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.16.0" +current_version = "1.17.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index fab50fd..2c9be6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [1.17.0] - 2026-08-11 ### Added - Added support for using multiple glossaries in text and document translation via `setGlossaryIds()`/`setGlossaries()` (up to 5 glossaries) in @@ -259,7 +261,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2022-09-08 Initial version. -[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.16.0...HEAD +[Unreleased]: https://github.com/DeepLcom/deepl-java/compare/v1.17.0...HEAD +[1.17.0]: https://github.com/DeepLcom/deepl-java/compare/v1.16.0...v1.17.0 [1.16.0]: https://github.com/DeepLcom/deepl-java/compare/v1.15.0...v1.16.0 [1.15.0]: https://github.com/DeepLcom/deepl-java/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/DeepLcom/deepl-java/compare/v1.13.0...v1.14.0 diff --git a/README.md b/README.md index ad9ce45..d52c149 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Java 1.8 or later. Add this dependency to your project's build file: ``` -implementation "com.deepl.api:deepl-java:1.16.0" +implementation "com.deepl.api:deepl-java:1.17.0" ``` ### Maven users @@ -42,7 +42,7 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.16.0 + 1.17.0 ``` diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index 35d6234..a099b12 100644 --- a/deepl-java/build.gradle.kts +++ b/deepl-java/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "com.deepl.api" -version = "1.16.0" +version = "1.17.0" val sharedManifest = the().manifest { attributes ( diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index f219fed..c44350d 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -92,7 +92,7 @@ public Translator(String authKey) throws IllegalArgumentException { */ private String constructUserAgentString(boolean sendPlatformInfo, AppInfo appInfo) { StringBuilder sb = new StringBuilder(); - sb.append("deepl-java/1.16.0"); + sb.append("deepl-java/1.17.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); From 1f05afff560664958168ed55fa0163df46c0dd80 Mon Sep 17 00:00:00 2001 From: Peter Karolyi Date: Thu, 20 Aug 2026 13:21:28 +0200 Subject: [PATCH 121/121] ci: use a GitHub App token for the issues board --- .github/workflows/add_issues_to_kanban.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/add_issues_to_kanban.yml b/.github/workflows/add_issues_to_kanban.yml index bed7595..73401d7 100644 --- a/.github/workflows/add_issues_to_kanban.yml +++ b/.github/workflows/add_issues_to_kanban.yml @@ -1,4 +1,4 @@ -name: Add bugs to bugs project +name: Add issues to the user issues board on: issues: @@ -9,8 +9,14 @@ jobs: add-to-project: name: Add issue to project runs-on: ubuntu-latest + permissions: {} steps: - - uses: actions/add-to-project@v0.5.0 + - uses: actions/create-github-app-token@v3.2.0 + id: app-token with: - project-url: https://github.com/orgs/DeepLcom/projects/1 - github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + client-id: ${{ secrets.PROJECT_BOT_CLIENT_ID }} + private-key: ${{ secrets.PROJECT_BOT_PRIVATE_KEY }} + - uses: actions/add-to-project@v2.0.0 + with: + project-url: https://github.com/orgs/DeepL/projects/1 + github-token: ${{ steps.app-token.outputs.token }}