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 01/77] 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 e37b322c1d93a0a4d3fa6451f66d6c34119d8a7f Mon Sep 17 00:00:00 2001 From: Jan Ebbing Date: Wed, 28 Feb 2024 15:15:09 +0000 Subject: [PATCH 02/77] 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 03/77] 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 04/77] 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 05/77] 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 06/77] 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 07/77] 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 08/77] 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 09/77] 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 10/77] 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 11/77] 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 12/77] 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 13/77] 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 14/77] 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 15/77] 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 16/77] 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 17/77] 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 18/77] 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 19/77] 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 20/77] 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 21/77] 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 22/77] 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 23/77] 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 24/77] 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 25/77] 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 26/77] 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 27/77] 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 28/77] 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 29/77] 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 30/77] 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 31/77] 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 32/77] 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 33/77] 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 34/77] 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 35/77] 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 36/77] 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 37/77] 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 38/77] 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 39/77] 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 40/77] 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 41/77] 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 42/77] 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 43/77] 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 44/77] 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 45/77] 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 46/77] 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 47/77] 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 48/77] 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 49/77] 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 50/77] 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 51/77] 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 52/77] 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 53/77] 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 54/77] 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 55/77] 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 56/77] 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 57/77] 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 58/77] 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 59/77] 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 60/77] 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 61/77] 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 62/77] 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 63/77] 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 64/77] 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 65/77] 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 66/77] 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 67/77] 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 68/77] 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 69/77] 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 70/77] 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 71/77] 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 72/77] 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 73/77] 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 74/77] 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 75/77] 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 76/77] 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 77/77] 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 }}