diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..e795633 --- /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.17.0" +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 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 }} diff --git a/.github/workflows/run_ci.yml b/.github/workflows/run_ci.yml new file mode 100644 index 0000000..b25a5f9 --- /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 -DrunV1ApiTests=true +# - 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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db1c327..7a5a621 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,9 +3,11 @@ 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' + - template: Security/SAST.gitlab-ci.yml # Global -------------------------- @@ -15,6 +17,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 +80,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: @@ -108,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" @@ -128,7 +147,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 @@ -152,14 +171,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 @@ -183,14 +204,37 @@ test_examples_manual: # stage: publish ------------------------- -publish: +.publish_base: stage: publish extends: .publish dependencies: - build_scheduled - build_manual + script: + - 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 + +publish: + extends: .publish_base rules: - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' - script: - - ./gradlew publish +publish_manual: + extends: .publish_base + when: manual + +## gitlab release + +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 574a734..2c9be6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,102 +4,282 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## [1.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 + `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`. +- 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()`. -## Unreleased ### Fixed -* Change document upload to use the path `/v2/document` instead of `/v2/document/` (no trailing `/`). - Both paths will continue to work in the v2 version of the API, but `/v2/document` is the intended one. +- 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. +## [1.16.0] - 2026-04-09 +### 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. -## [1.4.0] - 2023-11-03 +### 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 -* Add optional `context` parameter for text translation, that specifies - additional context to influence translations, that is not translated itself. +- 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. +- 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. + ### Fixed -* Remove unused `commons-math` dependency +- 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`. +## [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 + 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.3.0] - 2023-06-09 +## [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.). + +## [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). + +## [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 ### 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. +- 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 + 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 -## [1.2.0] - 2023-03-22 +## [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 + 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 + 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 + 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. + +## [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 -* 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. +- 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. + +## [1.4.0] - 2023-11-03 +### Added +- Add optional `context` parameter for text translation, that specifies + additional context to influence translations, that is not translated itself. + +### 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. + +## [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. ## [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.4.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 +[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 +[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 +[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 +[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 [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..d52c149 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 @@ -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.17.0" ``` ### Maven users @@ -42,13 +42,13 @@ Add this dependency to your project's POM: com.deepl.api deepl-java - 1.4.0 + 1.17.0 ``` ## 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 @@ -96,35 +96,39 @@ 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, +- `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 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?" 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?", + 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( @@ -155,17 +159,47 @@ 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. +- `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. +- `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. 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. +- `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. + - `'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"`. +- `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'`: @@ -183,6 +217,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 @@ -201,7 +289,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 @@ -240,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 @@ -247,20 +344,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](upgrading_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 @@ -270,16 +384,24 @@ class Example { // Continuing class Example from above put("artist", "Maler"); put("prize", "Gewinn"); }}; - GlossaryInfo myGlossary = - translator.createGlossary("My glossary", "en", "de", entries); - - System.out.printf("Created '%s' (%s) %s->%s containing %d entries\n", + 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 } } ``` @@ -289,18 +411,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 = - translator.createGlossaryFromCsv("My glossary", - "en", - "de", - csvFile); + MultilingualGlossaryInfo myGlossary = + client.createMultilingualGlossaryFromCsv("My glossary", + "en", + "de", + csvFile); } } ``` @@ -308,30 +430,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 = translator.getGlossary(glossaryId); + MultilingualGlossaryInfo myGlossary = + client.getMultilingualGlossary(glossaryId); + + client.deleteMultilingualGlossaryDictionary(glossaryId, + myGlossary.getDictionaries()[0]); // Find and delete glossaries named 'Old glossary' - List glossaries = translator.listGlossaries(); - for (GlossaryInfo glossary : glossaries) { + List glossaries = + client.listMultilingualGlossaries(); + for (MultilingualGlossaryInfo glossary : glossaries) { if (glossary.getName() == "Old glossary") { - translator.deleteGlossary(glossary); + client.deleteMultilingualGlossary(glossary); } } } @@ -340,19 +475,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 = translator.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: @@ -362,25 +499,150 @@ 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); 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." } } @@ -391,14 +653,14 @@ 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); 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", @@ -410,6 +672,356 @@ 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 a style rule + +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: + +```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); + } +} +``` + +#### 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 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 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 r : styleRulesDetailed) { + if (r.getConfiguredRules() != null && r.getConfiguredRules().getNumbers() != null) { + System.out.println(String.format("Number formatting rules: %s", + String.join(", ", r.getConfiguredRules().getNumbers().keySet()))); + } + } + } +} +``` + +#### 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: + +```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()); + } +} +``` + +### Translation Memories + +Translation memories store and reuse previously created translations, helping to +ensure consistency and reduce effort when translating similar or repeated content. + +#### Listing translation memories + +Use `listTranslationMemories()` to retrieve translation memories associated +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 + public void listTranslationMemoriesExample() throws Exception { + List translationMemories = + client.listTranslationMemories(); + for (TranslationMemoryInfo tm : translationMemories) { + System.out.println(String.format("%s (%s)", + tm.getName(), tm.getTranslationMemoryId())); + } + } +} +``` + +#### 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 +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. @@ -429,7 +1041,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."); } @@ -461,8 +1073,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", @@ -498,7 +1110,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(), @@ -525,35 +1137,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); } } ``` @@ -570,31 +1182,35 @@ 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 -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/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 diff --git a/deepl-java/build.gradle.kts b/deepl-java/build.gradle.kts index a42c861..a099b12 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` @@ -6,7 +10,7 @@ plugins { } group = "com.deepl.api" -version = "1.4.0" +version = "1.17.0" val sharedManifest = the().manifest { attributes ( @@ -27,6 +31,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.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") @@ -66,8 +71,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 @@ -123,3 +128,42 @@ 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" + + println("Triggering deployment for namespace: $namespace") + println("Endpoint: $url") + + 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() ?: "No error response" + } + + println("Response code: $responseCode") + if (response.isNotBlank()) { + println("Response body: $response") + } + + if (responseCode != 200) { + throw GradleException("Failed to trigger deployment: HTTP $responseCode - $response") + } + + println("Deployment triggered successfully") + } +} + 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/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..552a857 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/CustomInstruction.java @@ -0,0 +1,73 @@ +// 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 = "id") + @Nullable + private final String id; + + @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 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( + @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; + } + + /** @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/DeepLApiVersion.java b/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java new file mode 100644 index 0000000..19a5768 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/DeepLApiVersion.java @@ -0,0 +1,23 @@ +// 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 { + 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 new file mode 100644 index 0000000..9333746 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClient.java @@ -0,0 +1,1806 @@ +// 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.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; +import java.util.Map; +import org.jetbrains.annotations.Nullable; + +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. + * + *

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. + * @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. + * + *

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. + */ + @SuppressWarnings("deprecation") + public DeepLClient(String authKey, DeepLClientOptions 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( + String.format("/%s/write/rephrase", apiVersion), params); + checkResponse(response, false, false); + 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); + 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); + } + } + + /** + * 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. + */ + 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); + } + + /** + * 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()); + } + + /** + * 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 relativeUrl = "/v3/style_rules" + createQueryString(queryParams); + 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); + } + + /** + * 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 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)} but with + * default parameters (all null). + * + * @see DeepLClient#listTranslationMemories(Integer, Integer) + */ + public List listTranslationMemories() + throws DeepLException, InterruptedException { + 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. + * + * @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) + 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); + } + } + + /** 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( + 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); + 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; + } + + 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/DeepLClientOptions.java b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java new file mode 100644 index 0000000..1f2a6b3 --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/DeepLClientOptions.java @@ -0,0 +1,28 @@ +// 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.Nullable; + +/** {@inheritDoc} */ +@SuppressWarnings("deprecation") +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/DocumentTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java index c143a8a..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. @@ -13,9 +17,13 @@ * .setFormality(Formality.Less).setGlossaryId("f63c02c5-f056-.."); * */ -public class DocumentTranslationOptions { +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 @@ -43,7 +51,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()); } @@ -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/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..d67cd9d 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,14 @@ 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.*; /** @@ -21,6 +29,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 +54,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 +78,102 @@ 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 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 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 + 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 { @@ -92,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) { @@ -117,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); @@ -129,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/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/LanguageCode.java b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java index bbba689..b4c9448 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"; @@ -110,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/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/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/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/TextResult.java b/deepl-java/src/main/java/com/deepl/api/TextResult.java index d7c8b6b..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,15 +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) { + 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. */ @@ -23,4 +33,14 @@ public String getText() { public String getDetectedSourceLanguage() { return detectedSourceLanguage; } + + /** Number of characters billed for this text. */ + 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 b2a8e2f..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. @@ -13,17 +17,24 @@ * .setFormality(Formality.Less).setGlossaryId("f63c02c5-f056-.."); * */ -public class TextTranslationOptions { +public class TextTranslationOptions extends BaseRequestOptions { private Formality formality; private String glossaryId; + private List glossaryIds; + private String styleId; + private String translationMemoryId; + private Integer translationMemoryThreshold; private SentenceSplittingMode sentenceSplittingMode; private boolean preserveFormatting = false; private String context; private String tagHandling; + private String tagHandlingVersion; + private String modelType; private boolean outlineDetection = true; 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 @@ -51,7 +62,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()); } @@ -65,9 +76,104 @@ 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. + * 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. + */ + 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) { + 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 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 + * for more information and example usage. */ public TextTranslationOptions setContext(String context) { this.context = context; @@ -106,6 +212,29 @@ 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 + * 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. @@ -142,6 +271,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; @@ -152,6 +290,26 @@ 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; + } + /** Gets the current sentence splitting mode. */ public SentenceSplittingMode getSentenceSplittingMode() { return sentenceSplittingMode; @@ -167,11 +325,21 @@ 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; } + /** Gets the current tag handling version. */ + public String getTagHandlingVersion() { + return tagHandlingVersion; + } + /** Gets the current outline detection setting. */ public boolean isOutlineDetection() { return outlineDetection; @@ -191,4 +359,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/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 new file mode 100644 index 0000000..b77fbbc --- /dev/null +++ b/deepl-java/src/main/java/com/deepl/api/TranslationMemoryInfo.java @@ -0,0 +1,141 @@ +// 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 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; + + @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. + * + * @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, 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. */ + 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; + } + + /** + * @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{" + + "translationMemoryId='" + + translationMemoryId + + '\'' + + ", name='" + + name + + '\'' + + ", sourceLanguage='" + + sourceLanguage + + '\'' + + ", targetLanguages=" + + targetLanguages + + ", segmentCount=" + + segmentCount + + '}'; + } +} 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/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java index f0bc640..c44350d 100644 --- a/deepl-java/src/main/java/com/deepl/api/Translator.java +++ b/deepl-java/src/main/java/com/deepl/api/Translator.java @@ -24,8 +24,9 @@ public class Translator { /** 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; + protected final DeepLApiVersion apiVersion; /** * Initializes a new Translator object using your Authentication Key. @@ -37,21 +38,28 @@ 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"); + if (authKey == null || authKey.isEmpty()) { + throw new IllegalArgumentException("authKey cannot be null or empty"); } + + String sanitizedAuthKey = authKey.trim(); + this.apiVersion = options.apiVersion; 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())); @@ -70,7 +78,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()); } @@ -82,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.4.0"); + sb.append("deepl-java/1.17.0"); if (sendPlatformInfo) { sb.append(" ("); Properties props = System.getProperties(); @@ -191,7 +201,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()); } @@ -247,7 +259,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()); } @@ -291,7 +304,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()); } @@ -308,7 +323,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()); } @@ -447,7 +463,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()); } @@ -491,7 +507,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()); } @@ -521,7 +538,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()); @@ -595,7 +612,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; @@ -678,7 +696,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()); @@ -694,7 +712,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()); } @@ -725,7 +744,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()); @@ -750,7 +769,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); } @@ -776,12 +795,15 @@ 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"); 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 @@ -806,9 +828,15 @@ 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())); } + if (options.getTagHandlingVersion() != null) { + params.add(new KeyValuePair<>("tag_handling_version", options.getTagHandlingVersion())); + } if (!options.isOutlineDetection()) { params.add(new KeyValuePair<>("outline_detection", "0")); } @@ -822,6 +850,28 @@ 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())); + } + 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)); + } + } + addExtraBodyParameters(params, options.getExtraBodyParameters()); } return params; } @@ -836,13 +886,38 @@ 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, - 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, + 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); + + return params; } /** @@ -854,13 +929,15 @@ private 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. */ - private static ArrayList> createHttpParamsCommon( + 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); @@ -899,6 +976,20 @@ private 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; } @@ -907,6 +998,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. * @@ -915,7 +1023,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"); @@ -945,7 +1053,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()); } @@ -977,7 +1087,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) { @@ -1003,7 +1113,7 @@ private 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( 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..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,6 +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() { + apiVersion = DeepLApiVersion.VERSION_2; + } /** * Set the maximum number of failed attempts that {@link Translator} will retry, per request. By 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/http/HttpContent.java b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java index 992e5e7..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,27 @@ 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"); + } + 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/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 ffd7814..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 @@ -18,11 +18,18 @@ public class Parser { private final Gson gson; + public Gson getGson() { + return gson; + } + 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()); + gsonBuilder.registerTypeAdapter( + TranslationMemoryJob.class, new TranslationMemoryJobDeserializer()); gson = gsonBuilder.create(); } @@ -31,6 +38,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); } @@ -56,11 +68,71 @@ 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 List parseStyleRuleInfoList(String json) { + StyleRuleListResponse result = gson.fromJson(json, StyleRuleListResponse.class); + return result.getStyleRules(); + } + + 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 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); + } + public String parseErrorMessage(String json) { ErrorResponse response = gson.fromJson(json, ErrorResponse.class); @@ -72,12 +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) { + // 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/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/main/java/com/deepl/api/parsing/TextResultDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java index 15d6180..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,8 +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("detected_source_language").getAsString(), + jsonObject.get("billed_characters").getAsInt(), + modelType != null ? (modelType.getAsString()) : null); } } 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/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/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); } 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/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java index 1a0306a..833e715 100644 --- a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java +++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java @@ -3,17 +3,17 @@ // license that can be found in the LICENSE file. package com.deepl.api; -import static org.mockito.Mockito.*; - import java.io.*; import java.net.*; import java.time.*; import java.util.*; import java.util.stream.Stream; import org.junit.jupiter.api.*; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; import org.mockito.Mockito; @@ -29,6 +29,16 @@ void testEmptyAuthKey() { }); } + @Test + void testNullAuthKey() { + IllegalArgumentException thrown = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> { + Translator translator = new Translator(null); + }); + } + @Test void testInvalidAuthKey() { String authKey = "invalid"; @@ -45,9 +55,24 @@ 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()); } } + @ParameterizedTest + @ValueSource(strings = {"quality_optimized", "prefer_quality_optimized", "latency_optimized"}) + void testModelType(String modelTypeArg) throws DeepLException, InterruptedException { + Translator translator = createTranslator(); + String sourceLang = "de"; + TextResult result = + translator.translateText( + exampleText.get(sourceLang), + sourceLang, + "en-US", + new TextTranslationOptions().setModelType(modelTypeArg)); + Assertions.assertNotNull(result.getModelTypeUsed()); + } + @Test void testInvalidServerUrl() { Assertions.assertThrows( @@ -59,6 +84,28 @@ 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(); @@ -66,6 +113,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(); @@ -290,6 +352,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 @@ -337,4 +424,8 @@ private static Stream provideUserAgentTestData() { customUserAgent, detailedPlatformInfoWithAppInfo)); } + + boolean runV1ApiTests() { + return Boolean.getBoolean("runV1ApiTests"); + } } 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/MultilingualGlossaryCleanupUtility.java b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java new file mode 100644 index 0000000..a2825f4 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryCleanupUtility.java @@ -0,0 +1,63 @@ +// 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 { + deepLClient.deleteMultilingualGlossary(glossary.getGlossaryId()); + } catch (Exception exception) { + // Ignore + System.out.println( + "Failed to delete glossary: " + + glossaryName + + "\nException: " + + exception.getMessage()); + } + } + } + } + + 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..45524e6 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/MultilingualGlossaryTest.java @@ -0,0 +1,718 @@ +// 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 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(); + 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/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/SessionOptions.java b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java index 83d0116..1b45722 100644 --- a/deepl-java/src/test/java/com/deepl/api/SessionOptions.java +++ b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java @@ -12,12 +12,13 @@ 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; + 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"); } @@ -76,17 +82,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; } @@ -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/StyleRuleTest.java b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java new file mode 100644 index 0000000..5f8d371 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/StyleRuleTest.java @@ -0,0 +1,233 @@ +// 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.HashMap; +import java.util.List; +import java.util.Map; +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 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 + // 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); + } + + @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 b3b6398..1ea1032 100644 --- a/deepl-java/src/test/java/com/deepl/api/TestBase.java +++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java @@ -48,9 +48,10 @@ public class TestBase { } exampleText = new HashMap<>(); + 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"); @@ -61,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"); @@ -72,13 +73,14 @@ 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"); exampleText.put("tr", "proton ışını"); + exampleText.put("uk", "Протонний промінь"); exampleText.put("zh", "质子束"); String tmpdir = System.getProperty("java.io.tmpdir"); @@ -89,6 +91,7 @@ protected TestBase() { tempDir = createTempDir(); } + // TODO: Delete `createTranslator` methods, replace with `createDeepLClient` protected Translator createTranslator() { SessionOptions sessionOptions = new SessionOptions(); return createTranslator(sessionOptions); @@ -123,6 +126,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(); 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 666a4cf..3719e61 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 @@ -145,9 +146,10 @@ 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")); } + // Default formality is automatic, so the output may be either formal or informal result = translator.translateText( "How are you?", @@ -155,14 +157,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.getText().contains("dir")); } 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 = @@ -172,7 +174,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 = @@ -182,7 +184,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")); } } @@ -298,6 +300,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(); @@ -329,4 +363,51 @@ 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()); + } + + @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( + Collections.singletonList("Render the whole text in ALL CAPS"))); + + Assertions.assertNotNull(resultWithCustomInstructions.getText()); + Assertions.assertEquals("en", resultWithCustomInstructions.getDetectedSourceLanguage()); + if (!isMockServer) { + // 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 new file mode 100644 index 0000000..0c0bd03 --- /dev/null +++ b/deepl-java/src/test/java/com/deepl/api/TranslationMemoryTest.java @@ -0,0 +1,382 @@ +// 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.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 { + 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); + } + + @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)); + } + + @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; + } +} 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"); + } + } + ```