("mavenJava") {
+ from(components["java"])
+ pom {
+ name.set("deepl-java")
+ description.set("DeepL API Java Client Library")
+ url.set("https://www.github.com/DeepLcom/deepl-java")
+ properties.set(mapOf(
+ "java.version" to "1.8",
+ "project.build.sourceEncoding" to "UTF-8",
+ "project.reporting.outputEncoding" to "UTF-8"
+ ))
+ licenses {
+ license {
+ name.set("MIT License")
+ url.set("https://www.opensource.org/licenses/mit-license.php")
+ }
+ }
+ developers {
+ developer {
+ id.set("deepl")
+ name.set("DeepL SE")
+ email.set("open-source@deepl.com")
+ }
+ }
+ organization {
+ name.set("DeepL SE")
+ url.set("https://www.deepl.com")
+ }
+ scm {
+ connection.set("scm:git:git://github.com/DeepLcom/deepl-java.git")
+ developerConnection.set("scm:git:ssh://github.com/DeepLcom/deepl-java.git")
+ url.set("https://www.github.com/DeepLcom/deepl-java")
+ }
+ }
+ }
+ }
+}
+
+signing {
+ val signingKey: String? by project
+ val signingPassword: String? by project
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ sign(publishing.publications["mavenJava"])
+}
+
diff --git a/deepl-java/src/main/java/com/deepl/api/AuthorizationException.java b/deepl-java/src/main/java/com/deepl/api/AuthorizationException.java
new file mode 100644
index 0000000..86e095a
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/AuthorizationException.java
@@ -0,0 +1,11 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Exception thrown when the specified authentication key was invalid. */
+public class AuthorizationException extends DeepLException {
+ public AuthorizationException(String message) {
+ super(message);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/ConnectionException.java b/deepl-java/src/main/java/com/deepl/api/ConnectionException.java
new file mode 100644
index 0000000..1e41a19
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/ConnectionException.java
@@ -0,0 +1,22 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Exception thrown when a connection error occurs while accessing the DeepL API. */
+public class ConnectionException extends DeepLException {
+ private final boolean shouldRetry;
+
+ public ConnectionException(String message, boolean shouldRetry, Throwable cause) {
+ super(message, cause);
+ this.shouldRetry = shouldRetry;
+ }
+
+ /**
+ * Returns true
if this exception occurred due to transient condition and the request
+ * should be retried, otherwise false
.
+ */
+ public boolean getShouldRetry() {
+ return shouldRetry;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/DeepLException.java b/deepl-java/src/main/java/com/deepl/api/DeepLException.java
new file mode 100644
index 0000000..2f93893
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/DeepLException.java
@@ -0,0 +1,15 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Base class for all exceptions thrown by this library. */
+public class DeepLException extends Exception {
+ public DeepLException(String message) {
+ super(message);
+ }
+
+ public DeepLException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentHandle.java b/deepl-java/src/main/java/com/deepl/api/DocumentHandle.java
new file mode 100644
index 0000000..1c7279d
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/DocumentHandle.java
@@ -0,0 +1,34 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Handle to an in-progress document translation.
+ *
+ * @see Translator#translateDocumentStatus(DocumentHandle)
+ */
+public class DocumentHandle {
+ @SerializedName(value = "document_id")
+ private final String documentId;
+
+ @SerializedName(value = "document_key")
+ private final String documentKey;
+
+ public DocumentHandle(String documentId, String documentKey) {
+ this.documentId = documentId;
+ this.documentKey = documentKey;
+ }
+
+ /** Get the ID of associated document request. */
+ public String getDocumentId() {
+ return documentId;
+ }
+
+ /** Get the key of associated document request. */
+ public String getDocumentKey() {
+ return documentKey;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentNotReadyException.java b/deepl-java/src/main/java/com/deepl/api/DocumentNotReadyException.java
new file mode 100644
index 0000000..d961098
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/DocumentNotReadyException.java
@@ -0,0 +1,11 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Exception thrown when attempting to download a translated document before it is ready. */
+public class DocumentNotReadyException extends DeepLException {
+ public DocumentNotReadyException(String message) {
+ super(message);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentStatus.java b/deepl-java/src/main/java/com/deepl/api/DocumentStatus.java
new file mode 100644
index 0000000..4d529c0
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/DocumentStatus.java
@@ -0,0 +1,104 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.Nullable;
+
+/** Status of an in-progress document translation. */
+public class DocumentStatus {
+ @SerializedName(value = "document_id")
+ private final String documentId;
+
+ @SerializedName(value = "status")
+ private final StatusCode status;
+
+ @SerializedName(value = "billed_characters")
+ private final @Nullable Long billedCharacters;
+
+ @SerializedName(value = "seconds_remaining")
+ private final @Nullable Long secondsRemaining;
+
+ @SerializedName(value = "error_message")
+ private final @Nullable String errorMessage;
+
+ /** Status code indicating status of the document translation. */
+ public enum StatusCode {
+ /** Document translation has not yet started, but will begin soon. */
+ @SerializedName("queued")
+ Queued,
+ /** Document translation is in progress. */
+ @SerializedName("translating")
+ Translating,
+ /**
+ * Document translation completed successfully, and the translated document may be downloaded.
+ */
+ @SerializedName("done")
+ Done,
+ /** An error occurred during document translation. */
+ @SerializedName("error")
+ Error,
+ }
+
+ public DocumentStatus(
+ String documentId,
+ StatusCode status,
+ @Nullable Long billedCharacters,
+ @Nullable Long secondsRemaining,
+ @Nullable String errorMessage) {
+ this.documentId = documentId;
+ this.status = status;
+ this.billedCharacters = billedCharacters;
+ this.secondsRemaining = secondsRemaining;
+ this.errorMessage = errorMessage;
+ }
+
+ /** @return Document ID of the associated document. */
+ public String getDocumentId() {
+ return documentId;
+ }
+
+ /** @return Status of the document translation. */
+ public StatusCode getStatus() {
+ return status;
+ }
+
+ /**
+ * @return true
if no error has occurred during document translation, otherwise
+ * false
.
+ */
+ public boolean ok() {
+ return status != null && status != StatusCode.Error;
+ }
+
+ /**
+ * @return true
if document translation has completed successfully, otherwise
+ * false
.
+ */
+ public boolean done() {
+ return status != null && status == StatusCode.Done;
+ }
+
+ /**
+ * @return Number of seconds remaining until translation is complete if available, otherwise
+ * null
. Only available while document is in translating state.
+ */
+ public @Nullable Long getSecondsRemaining() {
+ return secondsRemaining;
+ }
+
+ /**
+ * @return Number of characters billed for the translation of this document if available,
+ * otherwise null
. Only available after document translation is finished and the
+ * status is {@link StatusCode#Done}, otherwise null
.
+ */
+ public @Nullable Long getBilledCharacters() {
+ return billedCharacters;
+ }
+
+ /** @return Short description of the error if available, otherwise null
. */
+ public @Nullable String getErrorMessage() {
+ return errorMessage;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationException.java b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationException.java
new file mode 100644
index 0000000..6fb39be
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationException.java
@@ -0,0 +1,30 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Exception thrown when an error occurs during {@link Translator#translateDocument}. If the error
+ * occurs after the document was successfully uploaded, the {@link DocumentHandle} for the
+ * associated document is included, to allow later retrieval of the document.
+ */
+public class DocumentTranslationException extends DeepLException {
+
+ private final @Nullable DocumentHandle handle;
+
+ public DocumentTranslationException(
+ String message, Throwable throwable, @Nullable DocumentHandle handle) {
+ super(message, throwable);
+ this.handle = handle;
+ }
+
+ /**
+ * Get the handle to the in-progress document translation, or null
if an error
+ * occurred before uploading the document.
+ */
+ public @Nullable DocumentHandle getHandle() {
+ return handle;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java
new file mode 100644
index 0000000..ac5d6a6
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/DocumentTranslationOptions.java
@@ -0,0 +1,51 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/**
+ * Options to control document translation behaviour. These options may be provided to {@link
+ * Translator#translateDocument} overloads.
+ *
+ * All properties have corresponding setters in fluent-style, so the following is possible:
+ *
+ * DocumentTranslationOptions options = new DocumentTranslationOptions()
+ * .setFormality(Formality.Less).setGlossaryId("f63c02c5-f056-..");
+ *
+ */
+public class DocumentTranslationOptions {
+ private Formality formality;
+ private String glossaryId;
+
+ /**
+ * Sets whether translations should lean toward formal or informal language. This option is only
+ * applicable for target languages that support the formality option. By default, this value is
+ * null
and translations use the default formality.
+ *
+ * @see Language#getSupportsFormality()
+ * @see Formality
+ */
+ public DocumentTranslationOptions setFormality(Formality formality) {
+ this.formality = formality;
+ return this;
+ }
+
+ /**
+ * Sets the ID of a glossary to use with the translation. By default, this value is
+ * null
and no glossary is used.
+ */
+ public DocumentTranslationOptions setGlossaryId(String glossaryId) {
+ this.glossaryId = glossaryId;
+ return this;
+ }
+
+ /** Gets the current formality setting. */
+ public Formality getFormality() {
+ return formality;
+ }
+
+ /** Gets the current glossary ID. */
+ public String getGlossaryId() {
+ return glossaryId;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/Formality.java b/deepl-java/src/main/java/com/deepl/api/Formality.java
new file mode 100644
index 0000000..c3ed4e9
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/Formality.java
@@ -0,0 +1,16 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Desired level of formality for translation. */
+public enum Formality {
+ /** Standard level of formality. */
+ Default,
+
+ /** Less formality, i.e. more informal. */
+ Less,
+
+ /** Increased formality. */
+ More,
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java
new file mode 100644
index 0000000..2c33c20
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/HttpClientWrapper.java
@@ -0,0 +1,142 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import com.deepl.api.http.*;
+import com.deepl.api.utils.*;
+import java.io.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import org.jetbrains.annotations.*;
+
+/** Helper class providing functions to make HTTP requests and retry with exponential-backoff. */
+class HttpClientWrapper {
+ private static final String CONTENT_TYPE = "Content-Type";
+ private static final String POST = "POST";
+ private final String serverUrl;
+ private final Map headers;
+ private final Duration minTimeout;
+ private final @Nullable Proxy proxy;
+ private final int maxRetries;
+
+ public HttpClientWrapper(
+ String serverUrl,
+ Map headers,
+ Duration minTimeout,
+ @Nullable Proxy proxy,
+ int maxRetries) {
+ this.serverUrl = serverUrl;
+ this.headers = headers;
+ this.minTimeout = minTimeout;
+ this.proxy = proxy;
+ this.maxRetries = maxRetries;
+ }
+
+ public HttpResponse sendRequestWithBackoff(String relativeUrl)
+ throws InterruptedException, DeepLException {
+ return sendRequestWithBackoff(POST, relativeUrl, null).toStringResponse();
+ }
+
+ public HttpResponse sendRequestWithBackoff(
+ String relativeUrl, @Nullable Iterable> params)
+ throws InterruptedException, DeepLException {
+ HttpContent content = HttpContent.buildFormURLEncodedContent(params);
+ return sendRequestWithBackoff(POST, relativeUrl, content).toStringResponse();
+ }
+
+ public HttpResponseStream downloadWithBackoff(
+ String relativeUrl, @Nullable Iterable> params)
+ throws InterruptedException, DeepLException {
+ HttpContent content = HttpContent.buildFormURLEncodedContent(params);
+ return sendRequestWithBackoff(POST, relativeUrl, content);
+ }
+
+ public HttpResponse uploadWithBackoff(
+ String relativeUrl,
+ @Nullable Iterable> params,
+ String fileName,
+ InputStream inputStream)
+ throws InterruptedException, DeepLException {
+ ArrayList> fields = new ArrayList<>();
+ fields.add(new KeyValuePair<>("file", new NamedStream(fileName, inputStream)));
+ if (params != null) {
+ params.forEach(
+ (KeyValuePair entry) -> {
+ fields.add(new KeyValuePair<>(entry.getKey(), entry.getValue()));
+ });
+ }
+ HttpContent content;
+ try {
+ content = HttpContent.buildMultipartFormDataContent(fields);
+ } catch (Exception e) {
+ throw new DeepLException("Failed building request", e);
+ }
+ return sendRequestWithBackoff(POST, relativeUrl, content).toStringResponse();
+ }
+
+ // Sends a request with exponential backoff
+ private HttpResponseStream sendRequestWithBackoff(
+ String method, String relativeUrl, HttpContent content)
+ throws InterruptedException, DeepLException {
+ BackoffTimer backoffTimer = new BackoffTimer(this.minTimeout);
+ while (true) {
+ try {
+ HttpResponseStream response =
+ sendRequest(method, serverUrl + relativeUrl, backoffTimer.getTimeoutMillis(), content);
+ if (backoffTimer.getNumRetries() >= this.maxRetries) {
+ return response;
+ } else if (response.getCode() != 429
+ && (response.getCode() < 500 || response.getCode() == 503)) {
+ return response;
+ }
+ response.close();
+ } catch (ConnectionException exception) {
+ if (!exception.getShouldRetry() || backoffTimer.getNumRetries() >= this.maxRetries) {
+ throw exception;
+ }
+ }
+ backoffTimer.sleepUntilRetry();
+ }
+ }
+
+ private HttpResponseStream sendRequest(
+ String method, String urlString, long timeoutMs, HttpContent content)
+ throws ConnectionException {
+ try {
+ URL url = new URL(urlString);
+ HttpURLConnection connection =
+ (HttpURLConnection) (proxy != null ? url.openConnection(proxy) : url.openConnection());
+
+ connection.setRequestMethod(method);
+ connection.setConnectTimeout((int) timeoutMs);
+ connection.setReadTimeout((int) timeoutMs);
+ connection.setUseCaches(false);
+
+ for (Map.Entry entry : this.headers.entrySet()) {
+ connection.setRequestProperty(entry.getKey(), entry.getValue());
+ }
+
+ if (content != null) {
+ connection.setDoOutput(true);
+ connection.setRequestProperty(CONTENT_TYPE, content.getContentType());
+
+ try (OutputStream output = connection.getOutputStream()) {
+ output.write(content.getContent());
+ }
+ }
+
+ int responseCode = connection.getResponseCode();
+ InputStream responseStream =
+ (responseCode >= 200 && responseCode < 400)
+ ? connection.getInputStream()
+ : connection.getErrorStream();
+ return new HttpResponseStream(responseCode, responseStream);
+ } catch (SocketTimeoutException e) {
+ throw new ConnectionException(e.getMessage(), true, e);
+ } catch (RuntimeException | IOException e) {
+ throw new ConnectionException(e.getMessage(), false, e);
+ }
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/Language.java b/deepl-java/src/main/java/com/deepl/api/Language.java
new file mode 100644
index 0000000..748e458
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/Language.java
@@ -0,0 +1,57 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A language supported by DeepL translation. The {@link Translator} class provides functions to
+ * retrieve the available source and target languages.
+ *
+ * @see Translator#getSourceLanguages()
+ * @see Translator#getTargetLanguages()
+ */
+public class Language {
+ private final String name;
+ private final String code;
+ private final @Nullable Boolean supportsFormality;
+
+ /**
+ * Initializes a new Language object.
+ *
+ * @param name The name of the language in English.
+ * @param code The language code.
+ * @param supportsFormality true
for a target language that supports the {@link
+ * TextTranslationOptions#setFormality} option for translations, false
for other
+ * target languages, or null
for source languages.
+ */
+ public Language(String name, String code, @Nullable Boolean supportsFormality) {
+ this.name = name;
+ this.code = LanguageCode.standardize(code);
+ this.supportsFormality = supportsFormality;
+ }
+
+ /** @return The name of the language in English, for example "Italian" or "Romanian". */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return The language code, for example "it", "ro" or "en-US". Language codes follow ISO 639-1
+ * with an optional regional code from ISO 3166-1.
+ */
+ public String getCode() {
+ return code;
+ }
+
+ /**
+ * @return true
if this language is a target language that supports the {@link
+ * TextTranslationOptions#setFormality} option for translations, false
if this
+ * language is a target language that does not support formality, or null
if this
+ * language is a source language.
+ */
+ public @Nullable Boolean getSupportsFormality() {
+ return supportsFormality;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/LanguageCode.java b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java
new file mode 100644
index 0000000..686f0c4
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/LanguageCode.java
@@ -0,0 +1,134 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/**
+ * Language codes for the languages currently supported by DeepL translation. New languages may be
+ * added in the future; to retrieve the currently supported languages use {@link
+ * Translator#getSourceLanguages()} and {@link Translator#getTargetLanguages()}.
+ */
+public class LanguageCode {
+ /** Bulgarian language code, may be used as source or target language. */
+ public static final String Bulgarian = "bg";
+
+ /** Czech language code, may be used as source or target language. */
+ public static final String Czech = "cs";
+
+ /** Danish language code, may be used as source or target language. */
+ public static final String Danish = "da";
+
+ /** German language code, may be used as source or target language. */
+ public static final String German = "de";
+
+ /** Greek language code, may be used as source or target language. */
+ public static final String Greek = "el";
+
+ /**
+ * English language code, may only be used as a source language. In input texts, this language
+ * code supports all English variants.
+ */
+ public static final String English = "en";
+
+ /** British English language code, may only be used as a target language. */
+ public static final String EnglishBritish = "en-GB";
+
+ /** American English language code, may only be used as a target language. */
+ public static final String EnglishAmerican = "en-US";
+
+ /** Spanish language code, may be used as source or target language. */
+ public static final String Spanish = "es";
+
+ /** Estonian language code, may be used as source or target language. */
+ public static final String Estonian = "et";
+
+ /** Finnish language code, may be used as source or target language. */
+ public static final String Finnish = "fi";
+
+ /** French language code, may be used as source or target language. */
+ public static final String French = "fr";
+
+ /** Hungarian language code, may be used as source or target language. */
+ public static final String Hungarian = "hu";
+
+ /** Indonesian language code, may be used as source or target language. */
+ public static final String Indonesian = "id";
+
+ /** Italian language code, may be used as source or target language. */
+ public static final String Italian = "it";
+
+ /** Japanese language code, may be used as source or target language. */
+ public static final String Japanese = "ja";
+
+ /** Lithuanian language code, may be used as source or target language. */
+ public static final String Lithuanian = "lt";
+
+ /** Latvian language code, may be used as source or target language. */
+ public static final String Latvian = "lv";
+
+ /** Dutch language code, may be used as source or target language. */
+ public static final String Dutch = "nl";
+
+ /** Polish language code, may be used as source or target language. */
+ public static final String Polish = "pl";
+
+ /**
+ * Portuguese language code, may only be used as a source language. In input texts, this language
+ * code supports all Portuguese variants.
+ */
+ public static final String Portuguese = "pt";
+
+ /** Brazilian Portuguese language code, may only be used as a target language. */
+ public static final String PortugueseBrazilian = "pt-BR";
+
+ /** European Portuguese language code, may only be used as a target language. */
+ public static final String PortugueseEuropean = "pt-PT";
+
+ /** Romanian language code, may be used as source or target language. */
+ public static final String Romanian = "ro";
+
+ /** Russian language code, may be used as source or target language. */
+ public static final String Russian = "ru";
+
+ /** Slovak language code, may be used as source or target language. */
+ public static final String Slovak = "sk";
+
+ /** Slovenian language code, may be used as source or target language. */
+ public static final String Slovenian = "sl";
+
+ /** Swedish language code, may be used as source or target language. */
+ public static final String Swedish = "sv";
+
+ /** Turkish language code, may be used as source or target language. */
+ public static final String Turkish = "tr";
+
+ /** Chinese language code, may be used as source or target language. */
+ public static final String Chinese = "zh";
+
+ /**
+ * Removes the regional variant (if any) from the given language code.
+ *
+ * @param langCode Language code possibly containing a regional variant.
+ * @return The language code without a regional variant.
+ */
+ public static String removeRegionalVariant(String langCode) {
+ String[] parts = langCode.split("-", 2);
+ return parts[0].toLowerCase();
+ }
+
+ /**
+ * Changes the upper- and lower-casing of the given language code to match ISO 639-1 with an
+ * optional regional code from ISO 3166-1.
+ *
+ * @param langCode String containing language code to standardize.
+ * @return String containing the standardized language code.
+ */
+ public static String standardize(String langCode) {
+ String[] parts = langCode.split("-", 2);
+ if (parts.length == 1) {
+ return parts[0].toLowerCase();
+ } else {
+ return parts[0].toLowerCase() + "-" + parts[1].toUpperCase();
+ }
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/LanguageType.java b/deepl-java/src/main/java/com/deepl/api/LanguageType.java
new file mode 100644
index 0000000..cfe6523
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/LanguageType.java
@@ -0,0 +1,10 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Enum specifying a source or target language type. */
+public enum LanguageType {
+ Source,
+ Target,
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/NotFoundException.java b/deepl-java/src/main/java/com/deepl/api/NotFoundException.java
new file mode 100644
index 0000000..b7fd2b1
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/NotFoundException.java
@@ -0,0 +1,11 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Exception thrown when the specified resource could not be found. */
+public class NotFoundException extends DeepLException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/QuotaExceededException.java b/deepl-java/src/main/java/com/deepl/api/QuotaExceededException.java
new file mode 100644
index 0000000..55fecee
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/QuotaExceededException.java
@@ -0,0 +1,11 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Exception thrown when the DeepL translation quota has been reached. */
+public class QuotaExceededException extends DeepLException {
+ public QuotaExceededException(String message) {
+ super(message);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/SentenceSplittingMode.java b/deepl-java/src/main/java/com/deepl/api/SentenceSplittingMode.java
new file mode 100644
index 0000000..84241d3
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/SentenceSplittingMode.java
@@ -0,0 +1,24 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Enum controlling how input translation text should be split into sentences. */
+public enum SentenceSplittingMode {
+ /**
+ * Input translation text will be split into sentences using both newlines and punctuation, this
+ * is the default behaviour.
+ */
+ All,
+
+ /**
+ * Input text will not be split into sentences. This is advisable for applications where each
+ * input translation text is only one sentence.
+ */
+ Off,
+
+ /**
+ * Input translation text will be split into sentences using only punctuation but not newlines.
+ */
+ NoNewlines,
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/TextResult.java b/deepl-java/src/main/java/com/deepl/api/TextResult.java
new file mode 100644
index 0000000..d7c8b6b
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/TextResult.java
@@ -0,0 +1,26 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** The result of a text translation. */
+public class TextResult {
+ private final String text;
+ private final String detectedSourceLanguage;
+
+ /** Constructs a new instance. */
+ public TextResult(String text, String detectedSourceLanguage) {
+ this.text = text;
+ this.detectedSourceLanguage = LanguageCode.standardize(detectedSourceLanguage);
+ }
+
+ /** The translated text. */
+ public String getText() {
+ return text;
+ }
+
+ /** The language code of the source text detected by DeepL. */
+ public String getDetectedSourceLanguage() {
+ return detectedSourceLanguage;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java
new file mode 100644
index 0000000..0f721df
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/TextTranslationOptions.java
@@ -0,0 +1,161 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/**
+ * Options to control text translation behaviour. These options may be provided to {@link
+ * Translator#translateText} overloads.
+ *
+ * All properties have corresponding setters in fluent-style, so the following is possible:
+ *
+ * TextTranslationOptions options = new TextTranslationOptions()
+ * .setFormality(Formality.Less).setGlossaryId("f63c02c5-f056-..");
+ *
+ */
+public class TextTranslationOptions {
+ private Formality formality;
+ private String glossaryId;
+ private SentenceSplittingMode sentenceSplittingMode;
+ private boolean preserveFormatting = false;
+ private String tagHandling;
+ private boolean outlineDetection = true;
+ private Iterable ignoreTags;
+ private Iterable nonSplittingTags;
+ private Iterable splittingTags;
+
+ /**
+ * Sets whether translations should lean toward formal or informal language. This option is only
+ * applicable for target languages that support the formality option. By default, this value is
+ * null
andnull
translations use the default formality.
+ *
+ * @see Language#getSupportsFormality()
+ * @see Formality
+ */
+ public TextTranslationOptions setFormality(Formality formality) {
+ this.formality = formality;
+ return this;
+ }
+
+ /**
+ * Sets the ID of a glossary to use with the translation. By default, this value is
+ * null
and no glossary is used.
+ */
+ public TextTranslationOptions setGlossaryId(String glossaryId) {
+ this.glossaryId = glossaryId;
+ return this;
+ }
+
+ /**
+ * Specifies how input translation text should be split into sentences. By default, this value is
+ * null
and the default sentence splitting mode is used.
+ *
+ * @see SentenceSplittingMode
+ */
+ public TextTranslationOptions setSentenceSplittingMode(
+ SentenceSplittingMode sentenceSplittingMode) {
+ this.sentenceSplittingMode = sentenceSplittingMode;
+ return this;
+ }
+
+ /**
+ * Sets whether formatting should be preserved in translations. Set to true
to
+ * prevent the translation engine from correcting some formatting aspects, and instead leave the
+ * formatting unchanged, default is false
.
+ */
+ public TextTranslationOptions setPreserveFormatting(boolean preserveFormatting) {
+ this.preserveFormatting = preserveFormatting;
+ return this;
+ }
+
+ /**
+ * Set the type of tags to parse before translation, only "xml"
and "html"
+ *
are currently available. By default, this value is null
and no
+ * tag-handling is used.
+ */
+ public TextTranslationOptions setTagHandling(String tagHandling) {
+ this.tagHandling = tagHandling;
+ return this;
+ }
+
+ /**
+ * Sets whether outline detection is used; set to false
to disable automatic tag
+ * detection, default is true
.
+ */
+ public TextTranslationOptions setOutlineDetection(boolean outlineDetection) {
+ this.outlineDetection = outlineDetection;
+ return this;
+ }
+
+ /**
+ * Sets the list of XML tags containing content that should not be translated. By default, this
+ * value is null
and no tags are specified.
+ */
+ public TextTranslationOptions setIgnoreTags(Iterable ignoreTags) {
+ this.ignoreTags = ignoreTags;
+ return this;
+ }
+
+ /**
+ * Sets the list of XML tags that should not be used to split text into sentences. By default,
+ * this value is null
and no tags are specified.
+ */
+ public TextTranslationOptions setNonSplittingTags(Iterable nonSplittingTags) {
+ this.nonSplittingTags = nonSplittingTags;
+ return this;
+ }
+
+ /**
+ * Set the list of XML tags that should be used to split text into sentences. By default, this
+ * value is null
and no tags are specified.
+ */
+ public TextTranslationOptions setSplittingTags(Iterable splittingTags) {
+ this.splittingTags = splittingTags;
+ return this;
+ }
+
+ /** Gets the current formality setting. */
+ public Formality getFormality() {
+ return formality;
+ }
+
+ /** Gets the current glossary ID. */
+ public String getGlossaryId() {
+ return glossaryId;
+ }
+
+ /** Gets the current sentence splitting mode. */
+ public SentenceSplittingMode getSentenceSplittingMode() {
+ return sentenceSplittingMode;
+ }
+
+ /** Gets the current preserve formatting setting. */
+ public boolean isPreserveFormatting() {
+ return preserveFormatting;
+ }
+
+ /** Gets the current tag handling setting. */
+ public String getTagHandling() {
+ return tagHandling;
+ }
+
+ /** Gets the current outline detection setting. */
+ public boolean isOutlineDetection() {
+ return outlineDetection;
+ }
+
+ /** Gets the current ignore tags list. */
+ public Iterable getIgnoreTags() {
+ return ignoreTags;
+ }
+
+ /** Gets the current non-splitting tags list. */
+ public Iterable getNonSplittingTags() {
+ return nonSplittingTags;
+ }
+
+ /** Gets the current splitting tags list. */
+ public Iterable getSplittingTags() {
+ return splittingTags;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/TooManyRequestsException.java b/deepl-java/src/main/java/com/deepl/api/TooManyRequestsException.java
new file mode 100644
index 0000000..7624192
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/TooManyRequestsException.java
@@ -0,0 +1,11 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+/** Exception thrown when too many requests are made to the DeepL API too quickly. */
+public class TooManyRequestsException extends DeepLException {
+ public TooManyRequestsException(String message) {
+ super(message);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/Translator.java b/deepl-java/src/main/java/com/deepl/api/Translator.java
new file mode 100644
index 0000000..cd44766
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/Translator.java
@@ -0,0 +1,802 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import com.deepl.api.http.HttpResponse;
+import com.deepl.api.http.HttpResponseStream;
+import com.deepl.api.parsing.Parser;
+import com.deepl.api.utils.*;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.util.*;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Client for the DeepL API. To use the DeepL API, initialize an instance of this class using your
+ * DeepL Authentication Key as found in your DeepL
+ * account.
+ */
+public class Translator {
+ /** Base URL for DeepL API Free accounts. */
+ private static final String DEEPL_SERVER_URL_FREE = "https://api-free.deepl.com";
+ /** Base URL for DeepL API Pro accounts */
+ private static final String DEEPL_SERVER_URL_PRO = "https://api.deepl.com";
+
+ private final Parser jsonParser = new Parser();
+ private final HttpClientWrapper httpClientWrapper;
+
+ /**
+ * Initializes a new Translator object using your Authentication Key.
+ *
+ * Note: This function does not establish a connection to the DeepL API. To check connectivity,
+ * use {@link Translator#getUsage()}.
+ *
+ * @param authKey DeepL Authentication Key as found in your DeepL account.
+ * @param options Additional options controlling Translator behaviour.
+ * @throws IllegalArgumentException If authKey is invalid.
+ */
+ public Translator(String authKey, TranslatorOptions options) throws IllegalArgumentException {
+ if (authKey == null || authKey.length() == 0) {
+ throw new IllegalArgumentException("authKey must be a non-empty string");
+ }
+ String serverUrl =
+ (options.getServerUrl() != null)
+ ? options.getServerUrl()
+ : (isFreeAccountAuthKey(authKey) ? 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("User-Agent", "deepl-java/0.1.0");
+
+ this.httpClientWrapper =
+ new HttpClientWrapper(
+ serverUrl, headers, options.getTimeout(), options.getProxy(), options.getMaxRetries());
+ }
+
+ /**
+ * Initializes a new Translator object using your Authentication Key.
+ *
+ * Note: This function does not establish a connection to the DeepL API. To check connectivity,
+ * use {@link Translator#getUsage()}.
+ *
+ * @param authKey DeepL Authentication Key as found in your DeepL account.
+ * @throws IllegalArgumentException If authKey is invalid.
+ */
+ public Translator(String authKey) throws IllegalArgumentException {
+ this(authKey, new TranslatorOptions());
+ }
+
+ /**
+ * Determines if the given DeepL Authentication Key belongs to an API Free account.
+ *
+ * @param authKey DeepL Authentication Key as found in your DeepL account.
+ * @return true
if the Authentication Key belongs to an API Free account, otherwise
+ * false
.
+ */
+ public static boolean isFreeAccountAuthKey(String authKey) {
+ return authKey != null && authKey.endsWith(":fx");
+ }
+
+ /**
+ * Translate specified text from source language into target language.
+ *
+ * @param text Text to translate; must not be empty.
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return Text translated into specified target language, and detected source language.
+ * @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 TextResult translateText(
+ String text,
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable TextTranslationOptions options)
+ throws InterruptedException, DeepLException {
+ ArrayList texts = new ArrayList<>();
+ texts.add(text);
+ return translateText(texts, sourceLang, targetLang, options).get(0);
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateText(String, String, String,
+ * TextTranslationOptions)} but with default options.
+ *
+ * @see Translator#translateText(String, String, String, TextTranslationOptions)
+ */
+ public TextResult translateText(String text, @Nullable String sourceLang, String targetLang)
+ throws DeepLException, InterruptedException {
+ return translateText(text, sourceLang, targetLang, null);
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateText(String, String, String,
+ * TextTranslationOptions)} but accepts {@link Language} objects for source and target languages,
+ * and uses default options.
+ *
+ * @see Translator#translateText(String, String, String, TextTranslationOptions)
+ */
+ public TextResult translateText(String text, @Nullable Language sourceLang, Language targetLang)
+ throws DeepLException, InterruptedException {
+ return translateText(
+ text, (sourceLang != null) ? sourceLang.getCode() : null, targetLang.getCode(), null);
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateText(String, String, String,
+ * TextTranslationOptions)} but accepts {@link Language} objects for source and target languages.
+ *
+ * @see Translator#translateText(String, String, String, TextTranslationOptions)
+ */
+ public TextResult translateText(
+ String text,
+ @Nullable Language sourceLang,
+ Language targetLang,
+ @Nullable TextTranslationOptions options)
+ throws DeepLException, InterruptedException {
+ return translateText(
+ text, (sourceLang != null) ? sourceLang.getCode() : null, targetLang.getCode(), options);
+ }
+
+ /**
+ * Translate specified texts from source language into target language.
+ *
+ * @param texts List of texts to translate; each text must not be empty.
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return List of texts translated into specified target language, and detected source language.
+ * @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 translateText(
+ List texts,
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable TextTranslationOptions options)
+ throws DeepLException, InterruptedException {
+ Iterable> params =
+ createHttpParams(texts, sourceLang, targetLang, options);
+ HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/translate", params);
+ checkResponse(response, false);
+ return jsonParser.parseTextResult(response.getBody());
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateText(List, String, String,
+ * TextTranslationOptions)} but accepts {@link Language} objects for source and target languages,
+ * and uses default options.
+ *
+ * @see Translator#translateText(List, String, String, TextTranslationOptions)
+ */
+ public List translateText(
+ List texts, @Nullable Language sourceLang, Language targetLang)
+ throws DeepLException, InterruptedException {
+ return translateText(
+ texts, (sourceLang != null) ? sourceLang.getCode() : null, targetLang.getCode(), null);
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateText(List, String, String,
+ * TextTranslationOptions)} but accepts {@link Language} objects for source and target languages.
+ *
+ * @see Translator#translateText(List, String, String, TextTranslationOptions)
+ */
+ public List translateText(
+ List texts,
+ @Nullable Language sourceLang,
+ Language targetLang,
+ @Nullable TextTranslationOptions options)
+ throws DeepLException, InterruptedException {
+ return translateText(
+ texts, (sourceLang != null) ? sourceLang.getCode() : null, targetLang.getCode(), options);
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateText(List, String, String,
+ * TextTranslationOptions)} but uses default options.
+ *
+ * @see Translator#translateText(List, String, String, TextTranslationOptions)
+ */
+ public List translateText(
+ List texts, @Nullable String sourceLang, String targetLang)
+ throws DeepLException, InterruptedException {
+ return translateText(texts, sourceLang, targetLang, null);
+ }
+
+ /**
+ * Retrieves the usage in the current billing period for this DeepL account. This function can
+ * also be used to check connectivity with the DeepL API and that the account has access.
+ *
+ * @return {@link Usage} object containing account usage information.
+ * @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 Usage getUsage() throws DeepLException, InterruptedException {
+ HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/usage");
+ checkResponse(response, false);
+ return jsonParser.parseUsage(response.getBody());
+ }
+
+ /**
+ * Retrieves the list of supported translation source languages.
+ *
+ * @return List of {@link Language} objects representing the available translation source
+ * languages.
+ * @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 getSourceLanguages() throws DeepLException, InterruptedException {
+ return getLanguages(LanguageType.Source);
+ }
+
+ /**
+ * Retrieves the list of supported translation target languages.
+ *
+ * @return List of {@link Language} objects representing the available translation target
+ * languages.
+ * @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 getTargetLanguages() throws DeepLException, InterruptedException {
+ return getLanguages(LanguageType.Target);
+ }
+
+ /**
+ * Retrieves the list of supported translation source or target languages.
+ *
+ * @param languageType The type of languages to retrieve, source or target.
+ * @return List of {@link Language} objects representing the available translation source or
+ * target languages.
+ * @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 getLanguages(LanguageType languageType)
+ throws DeepLException, InterruptedException {
+ ArrayList> params = new ArrayList<>();
+ if (languageType == LanguageType.Target) {
+ params.add(new KeyValuePair<>("type", "target"));
+ }
+ HttpResponse response = httpClientWrapper.sendRequestWithBackoff("/v2/languages", params);
+ checkResponse(response, false);
+ return jsonParser.parseLanguages(response.getBody());
+ }
+
+ /**
+ * Translate specified document content from source language to target language and store the
+ * translated document content to specified stream.
+ *
+ * @param inputFile File to upload to be translated.
+ * @param outputFile File to download translated document to.
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return Status when document translation completed, this allows the number of billed characters
+ * to be queried.
+ * @throws IOException If the output path is occupied or the input file does not exist.
+ * @throws DocumentTranslationException If any error occurs while communicating with the DeepL
+ * API, or if the thread is interrupted during execution of this function. The exception
+ * includes the document handle that may be used to retrieve the document.
+ */
+ public DocumentStatus translateDocument(
+ File inputFile,
+ File outputFile,
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable DocumentTranslationOptions options)
+ throws DocumentTranslationException, IOException {
+ try {
+ if (outputFile.exists()) {
+ throw new IOException("File already exists at output path");
+ }
+ try (InputStream inputStream = new FileInputStream(inputFile);
+ OutputStream outputStream = new FileOutputStream(outputFile)) {
+ return translateDocument(
+ inputStream, inputFile.getName(), outputStream, sourceLang, targetLang, options);
+ }
+ } catch (Exception exception) {
+ outputFile.delete();
+ throw exception;
+ }
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateDocument(File, File, String, String,
+ * DocumentTranslationOptions)} but uses default options.
+ *
+ * @see Translator#translateDocument(File, File, String, String, DocumentTranslationOptions)
+ */
+ public DocumentStatus translateDocument(
+ File inputFile, File outputFile, @Nullable String sourceLang, String targetLang)
+ throws DocumentTranslationException, IOException {
+ return translateDocument(inputFile, outputFile, sourceLang, targetLang, null);
+ }
+
+ /**
+ * Translate specified document content from source language to target language and store the
+ * translated document content to specified stream. On return, input stream will be at end of
+ * stream and neither stream will be closed.
+ *
+ * @param inputStream Stream containing file to upload to be translated.
+ * @param fileName Name of the input file. The file extension is used to determine file type.
+ * @param outputStream Stream to download translated document to.
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return Status when document translation completed, this allows the number of billed characters
+ * to be queried.
+ * @throws DocumentTranslationException If any error occurs while communicating with the DeepL
+ * API, or if the thread is interrupted during execution of this function. The exception
+ * includes the document handle that may be used to retrieve the document.
+ */
+ public DocumentStatus translateDocument(
+ InputStream inputStream,
+ String fileName,
+ OutputStream outputStream,
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable DocumentTranslationOptions options)
+ throws DocumentTranslationException {
+ DocumentHandle handle = null;
+ try {
+ handle = translateDocumentUpload(inputStream, fileName, sourceLang, targetLang, options);
+ DocumentStatus status = translateDocumentWaitUntilDone(handle);
+ translateDocumentDownload(handle, outputStream);
+ return status;
+ } catch (Exception exception) {
+ throw new DocumentTranslationException(
+ "Error occurred during document translation: " + exception.getMessage(),
+ exception,
+ handle);
+ }
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateDocument(InputStream, String, OutputStream,
+ * String, String, DocumentTranslationOptions)} but uses default options.
+ *
+ * @see Translator#translateDocument(InputStream, String, OutputStream, String, String,
+ * DocumentTranslationOptions)
+ */
+ public DocumentStatus translateDocument(
+ InputStream inputFile,
+ String fileName,
+ OutputStream outputFile,
+ @Nullable String sourceLang,
+ String targetLang)
+ throws DocumentTranslationException {
+ return translateDocument(inputFile, fileName, outputFile, sourceLang, targetLang, null);
+ }
+
+ /**
+ * Upload document at specified input path for translation from source language to target
+ * language. See the DeepL API
+ * documentation for the currently supported document types.
+ *
+ * @param inputFile File containing document to be translated.
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return Handle associated with the in-progress document translation.
+ * @throws IOException If the input file does not exist.
+ * @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 DocumentHandle translateDocumentUpload(
+ File inputFile,
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable DocumentTranslationOptions options)
+ throws DeepLException, IOException, InterruptedException {
+ Iterable> params =
+ createHttpParams(sourceLang, targetLang, options);
+ try (FileInputStream inputStream = new FileInputStream(inputFile)) {
+ HttpResponse response =
+ httpClientWrapper.uploadWithBackoff(
+ "/v2/document/", params, inputFile.getName(), inputStream);
+ checkResponse(response, false);
+ return jsonParser.parseDocumentHandle(response.getBody());
+ }
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateDocumentUpload(File, String, String,
+ * DocumentTranslationOptions)} but uses default options.
+ *
+ * @see Translator#translateDocumentUpload(File, String, String, DocumentTranslationOptions)
+ */
+ public DocumentHandle translateDocumentUpload(
+ File inputFile, @Nullable String sourceLang, String targetLang)
+ throws DeepLException, IOException, InterruptedException {
+ return translateDocumentUpload(inputFile, sourceLang, targetLang, null);
+ }
+
+ /**
+ * Upload document at specified input path for translation from source language to target
+ * language. See the DeepL API
+ * documentation for the currently supported document types.
+ *
+ * @param inputStream Stream containing document to be translated. On return, input stream will be
+ * at end of stream and will not be closed.
+ * @param fileName Name of the input file. The file extension is used to determine file type.
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return Handle associated with the in-progress document translation.
+ * @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 DocumentHandle translateDocumentUpload(
+ InputStream inputStream,
+ String fileName,
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable DocumentTranslationOptions options)
+ throws DeepLException, InterruptedException {
+ Iterable> params =
+ createHttpParams(sourceLang, targetLang, options);
+ HttpResponse response =
+ httpClientWrapper.uploadWithBackoff("/v2/document/", params, fileName, inputStream);
+ checkResponse(response, false);
+ return jsonParser.parseDocumentHandle(response.getBody());
+ }
+
+ /**
+ * Functions the same as {@link Translator#translateDocumentUpload(InputStream, String, String,
+ * String, DocumentTranslationOptions)} but uses default options.
+ *
+ * @see Translator#translateDocumentUpload(InputStream, String, String, String,
+ * DocumentTranslationOptions)
+ */
+ public DocumentHandle translateDocumentUpload(
+ InputStream inputStream, String fileName, @Nullable String sourceLang, String targetLang)
+ throws DeepLException, InterruptedException {
+ return translateDocumentUpload(inputStream, fileName, sourceLang, targetLang, null);
+ }
+
+ /**
+ * Retrieve the status of in-progress document translation associated with specified handle.
+ *
+ * @param handle Handle associated with document translation to check.
+ * @return Status of the document translation.
+ * @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 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());
+ HttpResponse response = httpClientWrapper.sendRequestWithBackoff(relativeUrl, params);
+ checkResponse(response, false);
+ return jsonParser.parseDocumentStatus(response.getBody());
+ }
+
+ /**
+ * Checks document translation status and waits until document translation is complete or fails
+ * due to an error.
+ *
+ * @param handle Handle associated with document translation to wait for.
+ * @return Status when document translation completed, this allows the number of billed characters
+ * to be queried.
+ * @throws InterruptedException If the thread is interrupted while waiting for the document
+ * translation to complete.
+ * @throws DeepLException If any error occurs while communicating with the DeepL API.
+ */
+ public DocumentStatus translateDocumentWaitUntilDone(DocumentHandle handle)
+ throws InterruptedException, DeepLException {
+ DocumentStatus status = translateDocumentStatus(handle);
+ while (status.ok() && !status.done()) {
+ Thread.sleep(calculateDocumentWaitTimeMillis(status.getSecondsRemaining()));
+ status = translateDocumentStatus(handle);
+ }
+
+ if (!status.ok()) {
+ String message =
+ (status.getErrorMessage() != null) ? status.getErrorMessage() : "Unknown error";
+ throw new DeepLException(message);
+ }
+ return status;
+ }
+
+ /**
+ * Downloads the resulting translated document associated with specified handle to the specified
+ * output file. The document translation must be complete i.e. {@link DocumentStatus#done()} for
+ * the document status must be true
.
+ *
+ * @param handle Handle associated with document translation to download.
+ * @param outputFile File to download translated document to.
+ * @throws IOException If the output path is occupied.
+ * @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 translateDocumentDownload(DocumentHandle handle, File outputFile)
+ throws DeepLException, IOException, InterruptedException {
+ try {
+ if (outputFile.exists()) {
+ throw new IOException("File already exists at output path");
+ }
+ try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
+ translateDocumentDownload(handle, outputStream);
+ }
+ } catch (Exception exception) {
+ outputFile.delete();
+ throw exception;
+ }
+ }
+
+ /**
+ * Downloads the resulting translated document associated with specified handle to the specified
+ * output stream. The document translation must be complete i.e. {@link DocumentStatus#done()} for
+ * the document status must be true
. The output stream is not closed.
+ *
+ * @param handle Handle associated with document translation to download.
+ * @param outputStream Stream to download translated document to.
+ * @throws IOException If an I/O error occurs.
+ * @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 translateDocumentDownload(DocumentHandle handle, OutputStream outputStream)
+ 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());
+ try (HttpResponseStream response = httpClientWrapper.downloadWithBackoff(relativeUrl, params)) {
+ checkResponse(response);
+ assert response.getBody() != null;
+ StreamUtil.transferTo(response.getBody(), outputStream);
+ }
+ }
+
+ /**
+ * Checks the specified texts, languages and options are valid, and returns an iterable of
+ * containing the parameters to include in HTTP request.
+ *
+ * @param texts Iterable of texts to translate.
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return Iterable of parameters for HTTP request.
+ */
+ private static ArrayList> createHttpParams(
+ List texts,
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable TextTranslationOptions options) {
+ ArrayList> params =
+ createHttpParamsCommon(
+ sourceLang,
+ targetLang,
+ options != null ? options.getFormality() : null,
+ options != null ? options.getGlossaryId() : null);
+ texts.forEach(
+ (text) -> {
+ if (text.isEmpty()) throw new IllegalArgumentException("text must not be empty");
+ params.add(new KeyValuePair<>("text", text));
+ });
+
+ if (options != null) {
+ // Note: formality and glossaryId are added above
+ if (options.getSentenceSplittingMode() != null
+ && options.getSentenceSplittingMode() != SentenceSplittingMode.All) {
+ switch (options.getSentenceSplittingMode()) {
+ case Off:
+ params.add(new KeyValuePair<>("split_sentences", "0"));
+ break;
+ case NoNewlines:
+ params.add(new KeyValuePair<>("split_sentences", "nonewlines"));
+ break;
+ default:
+ break;
+ }
+ }
+ if (options.isPreserveFormatting()) {
+ params.add(new KeyValuePair<>("preserve_formatting", "1"));
+ }
+ if (options.getTagHandling() != null) {
+ params.add(new KeyValuePair<>("tag_handling", options.getTagHandling()));
+ }
+ if (!options.isOutlineDetection()) {
+ params.add(new KeyValuePair<>("outline_detection", "0"));
+ }
+ if (options.getSplittingTags() != null) {
+ params.add(new KeyValuePair<>("splitting_tags", joinTags(options.getSplittingTags())));
+ }
+ if (options.getNonSplittingTags() != null) {
+ params.add(
+ new KeyValuePair<>("non_splitting_tags", joinTags(options.getNonSplittingTags())));
+ }
+ if (options.getIgnoreTags() != null) {
+ params.add(new KeyValuePair<>("ignore_tags", joinTags(options.getIgnoreTags())));
+ }
+ }
+ return params;
+ }
+
+ /**
+ * Checks the specified languages and document translation options are valid, and returns an
+ * iterable of containing the parameters to include in HTTP request.
+ *
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param options Options influencing translation.
+ * @return Iterable of parameters for HTTP request.
+ */
+ private static ArrayList> createHttpParams(
+ String sourceLang, String targetLang, DocumentTranslationOptions options) {
+ return createHttpParamsCommon(
+ sourceLang,
+ targetLang,
+ options != null ? options.getFormality() : null,
+ options != null ? options.getGlossaryId() : null);
+ }
+
+ /**
+ * Checks the specified parameters common to both text and document translation are valid, and
+ * returns an iterable of containing the parameters to include in HTTP request.
+ *
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @param targetLang Language code of the desired output language.
+ * @param formality Formality option for translation.
+ * @param glossaryId ID of glossary to use for translation.
+ * @return Iterable of parameters for HTTP request.
+ */
+ private static ArrayList> createHttpParamsCommon(
+ @Nullable String sourceLang,
+ String targetLang,
+ @Nullable Formality formality,
+ @Nullable String glossaryId) {
+ targetLang = LanguageCode.standardize(targetLang);
+ sourceLang = sourceLang == null ? null : LanguageCode.standardize(sourceLang);
+ checkValidLanguages(sourceLang, targetLang);
+
+ ArrayList> params = new ArrayList<>();
+ if (sourceLang != null) {
+ params.add(new KeyValuePair<>("source_lang", sourceLang));
+ }
+ params.add(new KeyValuePair<>("target_lang", targetLang));
+
+ if (formality != null && formality != Formality.Default) {
+ switch (formality) {
+ case More:
+ params.add(new KeyValuePair<>("formality", "more"));
+ break;
+ case Less:
+ params.add(new KeyValuePair<>("formality", "less"));
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (glossaryId != null) {
+ params.add(new KeyValuePair<>("glossary_id", glossaryId));
+ }
+
+ return params;
+ }
+
+ /** Combine XML tags with comma-delimiter to be included in HTTP request parameters. */
+ private static String joinTags(Iterable tags) {
+ return String.join(",", tags);
+ }
+
+ /**
+ * Checks the specified source and target language are valid.
+ *
+ * @param sourceLang Language code of the input language, or null
to use
+ * auto-detection.
+ * @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)
+ throws IllegalArgumentException {
+ if (sourceLang != null && sourceLang.isEmpty()) {
+ throw new IllegalArgumentException("sourceLang must be null or non-empty");
+ }
+ if (targetLang.isEmpty()) {
+ throw new IllegalArgumentException("targetLang must not be empty");
+ }
+ switch (targetLang) {
+ case "en":
+ throw new IllegalArgumentException(
+ "targetLang=\"en\" is not allowed, please use \"en-GB\" or \"en-US\" instead");
+ case "pt":
+ throw new IllegalArgumentException(
+ "targetLang=\"pt\" is not allowed, please use \"pt-PT\" or \"pt-BR\" instead");
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Functions the same as {@link Translator#checkResponse(HttpResponse, boolean)} but accepts
+ * response stream for document downloads. If the HTTP status code represents failure, the
+ * response stream is converted to a String response to throw the appropriate exception.
+ *
+ * @see Translator#checkResponse(HttpResponse, boolean)
+ */
+ private void checkResponse(HttpResponseStream response) throws DeepLException {
+ if (response.getCode() >= HttpURLConnection.HTTP_OK
+ && response.getCode() < HttpURLConnection.HTTP_BAD_REQUEST) {
+ return;
+ }
+ if (response.getBody() == null) {
+ throw new DeepLException("response stream is empty");
+ }
+ checkResponse(response.toStringResponse(), true);
+ }
+
+ /**
+ * Checks the response HTTP status is OK, otherwise throws corresponding exception.
+ *
+ * @param response Response received from DeepL API.
+ * @throws DeepLException Throws {@link DeepLException} or a derived exception depending on the
+ * type of error.
+ */
+ private void checkResponse(HttpResponse response, boolean inDocumentDownload)
+ throws DeepLException {
+ if (response.getCode() >= 200 && response.getCode() < 300) {
+ return;
+ }
+
+ String messageSuffix = jsonParser.parseErrorMessage(response.getBody());
+ if (!messageSuffix.isEmpty()) {
+ messageSuffix = ", " + messageSuffix;
+ }
+ switch (response.getCode()) {
+ case HttpURLConnection.HTTP_BAD_REQUEST:
+ throw new DeepLException("Bad request" + messageSuffix);
+ case HttpURLConnection.HTTP_FORBIDDEN:
+ throw new AuthorizationException("Authorization failure, check auth_key" + messageSuffix);
+ case HttpURLConnection.HTTP_NOT_FOUND:
+ throw new NotFoundException("Not found, check serverUrl" + messageSuffix);
+ case 429:
+ throw new TooManyRequestsException(
+ "Too many requests, DeepL servers are currently experiencing high load"
+ + messageSuffix);
+ case 456:
+ throw new QuotaExceededException(
+ "Quota for this billing period has been exceeded" + messageSuffix);
+ case HttpURLConnection.HTTP_UNAVAILABLE:
+ {
+ if (inDocumentDownload) {
+ throw new DocumentNotReadyException("Document not ready" + messageSuffix);
+ } else {
+ throw new DeepLException("Service unavailable" + messageSuffix);
+ }
+ }
+ default:
+ throw new DeepLException("Unknown error" + messageSuffix);
+ }
+ }
+
+ private int calculateDocumentWaitTimeMillis(Long secondsRemaining) {
+ if (secondsRemaining != null) {
+ double secs = ((double) secondsRemaining) / 2.0 + 1.0;
+ secs = max(1.0, min(secs, 60.0));
+ return (int) (secs * 1000);
+ }
+ return 1000;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java
new file mode 100644
index 0000000..e080ac9
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/TranslatorOptions.java
@@ -0,0 +1,96 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import java.net.Proxy;
+import java.time.Duration;
+import java.util.Map;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Options to control translator behaviour. These options may be provided to the {@link Translator}
+ * constructor.
+ *
+ * All properties have corresponding setters in fluent-style, so the following is possible:
+ *
+ * TranslatorOptions options = new TranslatorOptions()
+ * .setTimeout(Duration.ofSeconds(1)).setMaxRetries(2);
+ *
+ */
+public class TranslatorOptions {
+ private int maxRetries = 5;
+ private Duration timeout = Duration.ofSeconds(10);
+ @Nullable private Proxy proxy = null;
+ @Nullable private Map headers = null;
+ @Nullable private String serverUrl = null;
+
+ /**
+ * Set the maximum number of failed attempts that {@link Translator} will retry, per request. By
+ * default, 5 retries are made. Note: only errors due to transient conditions are retried.
+ */
+ public TranslatorOptions setMaxRetries(int maxRetries) {
+ this.maxRetries = maxRetries;
+ return this;
+ }
+
+ /** Set the connection timeout used for each HTTP request retry, the default is 10 seconds. */
+ public TranslatorOptions setTimeout(Duration timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * Set the proxy to use for HTTP requests. By default, this value is null
and no
+ * proxy will be used.
+ */
+ public TranslatorOptions setProxy(Proxy proxy) {
+ this.proxy = proxy;
+ return this;
+ }
+
+ /**
+ * Set HTTP headers attached to every HTTP request. By default, this value is null
+ * and no extra headers are used. Note that in the {@link Translator} constructor the headers for
+ * Authorization and User-Agent are added, unless they are overridden in this option.
+ */
+ public TranslatorOptions setHeaders(Map headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ /**
+ * Set the base URL for DeepL API that may be overridden for testing purposes. By default, this
+ * value is null
and the correct DeepL API base URL is selected based on the API
+ * account type (free or paid).
+ */
+ public TranslatorOptions setServerUrl(String serverUrl) {
+ this.serverUrl = serverUrl;
+ return this;
+ }
+
+ /** Gets the current maximum number of retries. */
+ public int getMaxRetries() {
+ return maxRetries;
+ }
+
+ /** Gets the current maximum request timeout. */
+ public Duration getTimeout() {
+ return timeout;
+ }
+
+ /** Gets the current proxy. */
+ public @Nullable Proxy getProxy() {
+ return proxy;
+ }
+
+ /** Gets the current HTTP headers. */
+ public @Nullable Map getHeaders() {
+ return headers;
+ }
+
+ /** Gets the current custom server URL. */
+ public @Nullable String getServerUrl() {
+ return serverUrl;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/Usage.java b/deepl-java/src/main/java/com/deepl/api/Usage.java
new file mode 100644
index 0000000..eddeafd
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/Usage.java
@@ -0,0 +1,109 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import java.util.function.BiConsumer;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Information about DeepL account usage for the current billing period, for example the number of
+ * characters translated.
+ *
+ * Depending on the account type, some usage types will be omitted. See the API documentation for more information.
+ */
+public class Usage {
+ private final @Nullable Detail character;
+ private final @Nullable Detail document;
+ private final @Nullable Detail teamDocument;
+
+ /** The character usage if included for the account type, or null
. */
+ public @Nullable Detail getCharacter() {
+ return character;
+ }
+
+ /** The document usage if included for the account type, or null
. */
+ public @Nullable Detail getDocument() {
+ return document;
+ }
+
+ /** The team document usage if included for the account type, or null
. */
+ public @Nullable Detail getTeamDocument() {
+ return teamDocument;
+ }
+
+ /** Stores the amount used and maximum amount for one usage type. */
+ public static class Detail {
+ private final int count;
+ private final int limit;
+
+ public Detail(int count, int limit) {
+ this.count = count;
+ this.limit = limit;
+ }
+
+ /** @return The currently used number of items for this usage type. */
+ public int getCount() {
+ return count;
+ }
+
+ /** @return The maximum permitted number of items for this usage type. */
+ public int getLimit() {
+ return limit;
+ }
+
+ /**
+ * @return true
if the amount used meets or exceeds the limit, otherwise
+ * false
.
+ */
+ public boolean limitReached() {
+ return getCount() >= getLimit();
+ }
+
+ @Override
+ public String toString() {
+ return getCount() + " of " + getLimit();
+ }
+ }
+
+ public Usage(
+ @Nullable Detail character, @Nullable Detail document, @Nullable Detail teamDocument) {
+ this.character = character;
+ this.document = document;
+ this.teamDocument = teamDocument;
+ }
+
+ /**
+ * @return true
if any of the usage types included for the account type have been
+ * reached, otherwise false
.
+ */
+ public boolean anyLimitReached() {
+ return (getCharacter() != null && getCharacter().limitReached())
+ || (getDocument() != null && getDocument().limitReached())
+ || (getTeamDocument() != null && getTeamDocument().limitReached());
+ }
+
+ /**
+ * Returns a string representing the usage. This function is for diagnostic purposes only; the
+ * content of the returned string is exempt from backwards compatibility.
+ *
+ * @return A string containing the usage for this billing period.
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Usage this billing period:");
+
+ BiConsumer addLabelledDetail =
+ (label, detail) -> {
+ if (detail != null) {
+ sb.append("\n").append(label).append(": ").append(detail);
+ }
+ };
+
+ addLabelledDetail.accept("Characters", getCharacter());
+ addLabelledDetail.accept("Documents", getDocument());
+ addLabelledDetail.accept("Team documents", getTeamDocument());
+ return sb.toString();
+ }
+}
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
new file mode 100644
index 0000000..d38ac38
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/http/HttpContent.java
@@ -0,0 +1,114 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.http;
+
+import com.deepl.api.*;
+import com.deepl.api.utils.*;
+import java.io.*;
+import java.net.*;
+import java.nio.charset.*;
+import java.util.*;
+import org.jetbrains.annotations.*;
+
+public class HttpContent {
+ private static final String LINE_BREAK = "\r\n";
+ private final String contentType;
+ private final byte[] content;
+
+ private HttpContent(String contentType, byte[] content) {
+ this.contentType = contentType;
+ this.content = content;
+ }
+
+ public byte[] getContent() {
+ return content;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public static HttpContent buildFormURLEncodedContent(
+ @Nullable Iterable> params) throws DeepLException {
+ StringBuilder sb = new StringBuilder();
+ if (params != null) {
+ for (KeyValuePair pair : params) {
+ if (sb.length() != 0) sb.append("&");
+ sb.append(urlEncode(pair.getKey()));
+ sb.append("=");
+ sb.append(urlEncode(pair.getValue()));
+ }
+ }
+ return new HttpContent(
+ "application/x-www-form-urlencoded", sb.toString().getBytes(StandardCharsets.UTF_8));
+ }
+
+ private static String urlEncode(String value) throws DeepLException {
+ try {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException exception) {
+ throw new DeepLException("Error while URL-encoding request", exception);
+ }
+ }
+
+ public static HttpContent buildMultipartFormDataContent(
+ Iterable> params) throws Exception {
+ String boundary = UUID.randomUUID().toString();
+ return buildMultipartFormDataContent(params, boundary);
+ }
+
+ private static HttpContent buildMultipartFormDataContent(
+ Iterable> params, String boundary) throws Exception {
+ try (ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ PrintWriter writer =
+ new PrintWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) {
+
+ if (params != null) {
+ for (KeyValuePair entry : params) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ if (entry.getValue() instanceof NamedStream) {
+ NamedStream namedStream = (NamedStream) entry.getValue();
+ String probableContentType =
+ URLConnection.guessContentTypeFromName(namedStream.getFileName());
+ writer.append("--").append(boundary).append(LINE_BREAK);
+ writer
+ .append("Content-Disposition: form-data; name=\"")
+ .append(key)
+ .append("\"; filename=\"")
+ .append(namedStream.getFileName())
+ .append("\"")
+ .append(LINE_BREAK);
+ writer.append("Content-Type: ").append(probableContentType).append(LINE_BREAK);
+ writer.append("Content-Transfer-Encoding: binary").append(LINE_BREAK);
+ writer.append(LINE_BREAK);
+ writer.flush();
+
+ StreamUtil.transferTo(namedStream.getInputStream(), stream);
+
+ writer.append(LINE_BREAK);
+ writer.flush();
+ } else if (value instanceof String) {
+ writer.append("--").append(boundary).append(LINE_BREAK);
+ writer
+ .append("Content-Disposition: form-data; name=\"")
+ .append(key)
+ .append("\"")
+ .append(LINE_BREAK);
+ writer.append(LINE_BREAK);
+ writer.append((String) value).append(LINE_BREAK);
+ writer.flush();
+ } else {
+ throw new Exception("Unknown argument type: " + value.getClass().getName());
+ }
+ }
+ }
+
+ writer.append("--").append(boundary).append("--").append(LINE_BREAK);
+ writer.flush();
+ writer.close();
+ return new HttpContent("multipart/form-data; boundary=" + boundary, stream.toByteArray());
+ }
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/http/HttpResponse.java b/deepl-java/src/main/java/com/deepl/api/http/HttpResponse.java
new file mode 100644
index 0000000..eadcf00
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/http/HttpResponse.java
@@ -0,0 +1,24 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.http;
+
+public class HttpResponse {
+
+ private final int code;
+
+ private final String body;
+
+ public HttpResponse(int code, String body) {
+ this.code = code;
+ this.body = body;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getBody() {
+ return body;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/http/HttpResponseStream.java b/deepl-java/src/main/java/com/deepl/api/http/HttpResponseStream.java
new file mode 100644
index 0000000..5d51d19
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/http/HttpResponseStream.java
@@ -0,0 +1,50 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.http;
+
+import com.deepl.api.*;
+import com.deepl.api.utils.*;
+import java.io.*;
+import org.jetbrains.annotations.*;
+
+public class HttpResponseStream implements AutoCloseable {
+
+ private final int code;
+
+ @Nullable private final InputStream body;
+
+ public HttpResponseStream(int code, @Nullable InputStream body) {
+ this.code = code;
+ this.body = body;
+ }
+
+ public void close() {
+ try {
+ if (this.body != null) {
+ this.body.close();
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ public HttpResponse toStringResponse() throws DeepLException {
+ try {
+ String content = this.body == null ? "" : StreamUtil.readStream(this.body);
+ return new HttpResponse(getCode(), content);
+ } catch (IOException exception) {
+ throw new DeepLException("Error reading stream", exception);
+ } finally {
+ close();
+ }
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public @Nullable InputStream getBody() {
+ return body;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java
new file mode 100644
index 0000000..31468e4
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/parsing/ErrorResponse.java
@@ -0,0 +1,21 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.parsing;
+
+import org.jetbrains.annotations.Nullable;
+
+class ErrorResponse {
+ @Nullable String message;
+ @Nullable String detail;
+
+ public String getErrorMessage() {
+ StringBuilder sb = new StringBuilder();
+ if (message != null) sb.append("message: ").append(message);
+ if (detail != null) {
+ if (sb.length() != 0) sb.append(", ");
+ sb.append("detail: ").append(detail);
+ }
+ return sb.toString();
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/LanguageDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/LanguageDeserializer.java
new file mode 100644
index 0000000..da16b1c
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/parsing/LanguageDeserializer.java
@@ -0,0 +1,20 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.parsing;
+
+import com.deepl.api.Language;
+import com.google.gson.*;
+import java.lang.reflect.Type;
+
+class LanguageDeserializer implements JsonDeserializer {
+ public Language deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ JsonObject jsonObject = json.getAsJsonObject();
+ Boolean supportsFormality = Parser.getAsBooleanOrNull(jsonObject, "supports_formality");
+ return new Language(
+ jsonObject.get("name").getAsString(),
+ jsonObject.get("language").getAsString(),
+ supportsFormality);
+ }
+}
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
new file mode 100644
index 0000000..b761bb3
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/parsing/Parser.java
@@ -0,0 +1,70 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.parsing;
+
+import com.deepl.api.*;
+import com.google.gson.*;
+import com.google.gson.reflect.*;
+import java.lang.reflect.*;
+import java.util.*;
+import org.jetbrains.annotations.*;
+
+public class Parser {
+ private final Gson gson;
+
+ public Parser() {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeAdapter(TextResult.class, new TextResultDeserializer());
+ gsonBuilder.registerTypeAdapter(Language.class, new LanguageDeserializer());
+ gsonBuilder.registerTypeAdapter(Usage.class, new UsageDeserializer());
+ gson = gsonBuilder.create();
+ }
+
+ public List parseTextResult(String json) {
+ TextResponse result = gson.fromJson(json, TextResponse.class);
+ return result.translations;
+ }
+
+ public Usage parseUsage(String json) {
+ return gson.fromJson(json, Usage.class);
+ }
+
+ public List parseLanguages(String json) {
+ Type languageListType = new TypeToken>() {}.getType();
+ return gson.fromJson(json, languageListType);
+ }
+
+ public DocumentStatus parseDocumentStatus(String json) {
+ return gson.fromJson(json, DocumentStatus.class);
+ }
+
+ public DocumentHandle parseDocumentHandle(String json) {
+ return gson.fromJson(json, DocumentHandle.class);
+ }
+
+ public String parseErrorMessage(String json) {
+ ErrorResponse response = gson.fromJson(json, ErrorResponse.class);
+
+ if (response != null) {
+ return response.getErrorMessage();
+ } else {
+ return "";
+ }
+ }
+
+ static @Nullable Integer getAsIntOrNull(JsonObject jsonObject, String parameterName) {
+ if (!jsonObject.has(parameterName)) return null;
+ return jsonObject.get(parameterName).getAsInt();
+ }
+
+ static @Nullable String getAsStringOrNull(JsonObject jsonObject, String parameterName) {
+ if (!jsonObject.has(parameterName)) return null;
+ return jsonObject.get(parameterName).getAsString();
+ }
+
+ static @Nullable Boolean getAsBooleanOrNull(JsonObject jsonObject, String parameterName) {
+ if (!jsonObject.has(parameterName)) return null;
+ return jsonObject.get(parameterName).getAsBoolean();
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TextResponse.java b/deepl-java/src/main/java/com/deepl/api/parsing/TextResponse.java
new file mode 100644
index 0000000..c6515d1
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/parsing/TextResponse.java
@@ -0,0 +1,11 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.parsing;
+
+import com.deepl.api.TextResult;
+import java.util.List;
+
+class TextResponse {
+ public List translations;
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java
new file mode 100644
index 0000000..43a849f
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/parsing/TextResultDeserializer.java
@@ -0,0 +1,18 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.parsing;
+
+import com.deepl.api.TextResult;
+import com.google.gson.*;
+import java.lang.reflect.Type;
+
+class TextResultDeserializer implements JsonDeserializer {
+ public TextResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ JsonObject jsonObject = json.getAsJsonObject();
+ return new TextResult(
+ jsonObject.get("text").getAsString(),
+ jsonObject.get("detected_source_language").getAsString());
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java b/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java
new file mode 100644
index 0000000..ce2d417
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/parsing/UsageDeserializer.java
@@ -0,0 +1,28 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.parsing;
+
+import com.deepl.api.*;
+import com.google.gson.*;
+import java.lang.reflect.*;
+import org.jetbrains.annotations.*;
+
+class UsageDeserializer implements JsonDeserializer {
+ public Usage deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ JsonObject jsonObject = json.getAsJsonObject();
+
+ return new Usage(
+ createDetail(jsonObject, "character_"),
+ createDetail(jsonObject, "document_"),
+ createDetail(jsonObject, "team_document_"));
+ }
+
+ public static @Nullable Usage.Detail createDetail(JsonObject jsonObject, String prefix) {
+ Integer count = Parser.getAsIntOrNull(jsonObject, prefix + "count");
+ Integer limit = Parser.getAsIntOrNull(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/utils/BackoffTimer.java b/deepl-java/src/main/java/com/deepl/api/utils/BackoffTimer.java
new file mode 100644
index 0000000..f082e51
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/utils/BackoffTimer.java
@@ -0,0 +1,65 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.utils;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class BackoffTimer {
+
+ private int numRetries;
+ private Duration backoff;
+ private final Duration minTimeout;
+ private Instant deadline;
+
+ private static final Duration backoffInitial = Duration.ofSeconds(1);
+ private static final Duration backoffMax = Duration.ofSeconds(120);
+ private static final float jitter = 0.23F;
+ private static final float multiplier = 1.6F;
+
+ public BackoffTimer(Duration minTimeout) {
+ numRetries = 0;
+ backoff = backoffInitial;
+ this.minTimeout = minTimeout;
+ deadline = Instant.now().plus(backoff);
+ }
+
+ public Duration getTimeout() {
+ Duration timeToDeadline = getTimeUntilDeadline();
+ if (timeToDeadline.compareTo(minTimeout) < 0) return minTimeout;
+ return timeToDeadline;
+ }
+
+ public long getTimeoutMillis() {
+ return getTimeout().toMillis();
+ }
+
+ public int getNumRetries() {
+ return numRetries;
+ }
+
+ public void sleepUntilRetry() throws InterruptedException {
+ try {
+ Thread.sleep(getTimeUntilDeadline().toMillis());
+ } catch (InterruptedException exception) {
+ Thread.currentThread().interrupt();
+ throw exception;
+ }
+
+ backoff = Duration.ofNanos((long) (backoff.toNanos() * multiplier));
+ if (backoff.compareTo(backoffMax) > 0) backoff = backoffMax;
+
+ float randomJitter = (ThreadLocalRandom.current().nextFloat() * 2.0F - 1.0F) * jitter + 1.0F;
+ Duration jitteredBackoff = Duration.ofNanos((long) (backoff.toNanos() * randomJitter));
+ deadline = Instant.now().plus(jitteredBackoff);
+ ++numRetries;
+ }
+
+ private Duration getTimeUntilDeadline() {
+ Instant currentTime = Instant.now();
+ if (currentTime.isAfter(deadline)) return Duration.ZERO;
+ return Duration.between(currentTime, deadline);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/utils/KeyValuePair.java b/deepl-java/src/main/java/com/deepl/api/utils/KeyValuePair.java
new file mode 100644
index 0000000..6dcf923
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/utils/KeyValuePair.java
@@ -0,0 +1,13 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.utils;
+
+import java.util.AbstractMap;
+
+public class KeyValuePair extends AbstractMap.SimpleEntry {
+
+ public KeyValuePair(K key, V value) {
+ super(key, value);
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/utils/NamedStream.java b/deepl-java/src/main/java/com/deepl/api/utils/NamedStream.java
new file mode 100644
index 0000000..42ed981
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/utils/NamedStream.java
@@ -0,0 +1,24 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.utils;
+
+import java.io.*;
+
+public class NamedStream {
+ private final String fileName;
+ private final InputStream inputStream;
+
+ public NamedStream(String fileName, InputStream inputStream) {
+ this.fileName = fileName;
+ this.inputStream = inputStream;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+}
diff --git a/deepl-java/src/main/java/com/deepl/api/utils/StreamUtil.java b/deepl-java/src/main/java/com/deepl/api/utils/StreamUtil.java
new file mode 100644
index 0000000..662f606
--- /dev/null
+++ b/deepl-java/src/main/java/com/deepl/api/utils/StreamUtil.java
@@ -0,0 +1,47 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api.utils;
+
+import java.io.*;
+import java.nio.charset.*;
+
+public class StreamUtil {
+ public static final int DEFAULT_BUFFER_SIZE = 1024;
+
+ public static String readStream(InputStream inputStream) throws IOException {
+ Charset charset = StandardCharsets.UTF_8;
+ final char[] buffer = new char[DEFAULT_BUFFER_SIZE];
+ final StringBuilder sb = new StringBuilder();
+ final Reader in = new BufferedReader(new InputStreamReader(inputStream, charset));
+ int charsRead;
+ while ((charsRead = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) > 0) {
+ sb.append(buffer, 0, charsRead);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Reads all bytes from input stream and writes the bytes to the given output stream in the order
+ * that they are read. On return, input stream will be at end of stream. This method does not
+ * close either stream.
+ *
+ * Implementation based on {@link InputStream#transferTo(OutputStream)} added in Java 9.
+ *
+ * @param inputStream The input stream, non-null.
+ * @param outputStream The output stream, non-null.
+ * @return Number of bytes transferred.
+ * @throws IOException if an I/O error occurs when reading or writing.
+ */
+ public static long transferTo(InputStream inputStream, OutputStream outputStream)
+ throws IOException {
+ long transferred = 0;
+ final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+ int read;
+ while ((read = inputStream.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
+ outputStream.write(buffer, 0, read);
+ transferred += read;
+ }
+ return transferred;
+ }
+}
diff --git a/deepl-java/src/test/java/com/deepl/api/GeneralTest.java b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java
new file mode 100644
index 0000000..9912753
--- /dev/null
+++ b/deepl-java/src/test/java/com/deepl/api/GeneralTest.java
@@ -0,0 +1,229 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import java.io.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import org.junit.jupiter.api.*;
+
+class GeneralTest extends TestBase {
+
+ @Test
+ void testEmptyAuthKey() {
+ IllegalArgumentException thrown =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ Translator translator = new Translator("");
+ });
+ }
+
+ @Test
+ void testInvalidAuthKey() {
+ String authKey = "invalid";
+ Translator translator = new Translator(authKey);
+ Assertions.assertThrows(AuthorizationException.class, translator::getUsage);
+ }
+
+ @Test
+ void testExampleTranslation() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+
+ for (Map.Entry entry : exampleText.entrySet()) {
+ String inputText = entry.getValue();
+ String sourceLang = LanguageCode.removeRegionalVariant(entry.getKey());
+ TextResult result = translator.translateText(inputText, sourceLang, "en-US");
+ Assertions.assertTrue(result.getText().toLowerCase().contains("proton"));
+ }
+ }
+
+ @Test
+ void testInvalidServerUrl() {
+ Assertions.assertThrows(
+ DeepLException.class,
+ () -> {
+ Translator translator =
+ new Translator(authKey, new TranslatorOptions().setServerUrl("http:/api.deepl.com"));
+ translator.getUsage();
+ });
+ }
+
+ @Test
+ void testUsage() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ Usage usage = translator.getUsage();
+ Assertions.assertTrue(usage.toString().contains("Usage this billing period"));
+ }
+
+ @Test
+ void testGetSourceAndTargetLanguages() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ List sourceLanguages = translator.getSourceLanguages();
+ List targetLanguages = translator.getTargetLanguages();
+
+ for (Language language : sourceLanguages) {
+ if (Objects.equals(language.getCode(), "en")) {
+ Assertions.assertEquals("English", language.getName());
+ }
+ Assertions.assertNull(language.getSupportsFormality());
+ }
+ Assertions.assertTrue(sourceLanguages.size() > 20);
+
+ for (Language language : targetLanguages) {
+ Assertions.assertNotNull(language.getSupportsFormality());
+ if (Objects.equals(language.getCode(), "de")) {
+ Assertions.assertTrue(language.getSupportsFormality());
+ Assertions.assertEquals("German", language.getName());
+ }
+ }
+ Assertions.assertTrue(targetLanguages.size() > 20);
+ }
+
+ @Test
+ void testAuthKeyIsFreeAccount() {
+ Assertions.assertTrue(
+ Translator.isFreeAccountAuthKey("b493b8ef-0176-215d-82fe-e28f182c9544:fx"));
+ Assertions.assertFalse(Translator.isFreeAccountAuthKey("b493b8ef-0176-215d-82fe-e28f182c9544"));
+ }
+
+ @Test
+ void testProxyUsage() throws DeepLException, InterruptedException, MalformedURLException {
+ Assumptions.assumeTrue(isMockProxyServer);
+ SessionOptions sessionOptions = new SessionOptions();
+ sessionOptions.expectProxy = true;
+ Map headers = sessionOptions.createSessionHeaders();
+
+ URL proxyUrl = new URL(TestBase.proxyUrl);
+ TranslatorOptions options =
+ new TranslatorOptions()
+ .setProxy(
+ new Proxy(
+ Proxy.Type.HTTP, new InetSocketAddress(proxyUrl.getHost(), proxyUrl.getPort())))
+ .setHeaders(headers)
+ .setServerUrl(serverUrl);
+ Translator translator = new Translator(authKey, options);
+ translator.getUsage();
+ }
+
+ @Test
+ void testUsageNoResponse() {
+ Assumptions.assumeTrue(isMockServer);
+ // Lower the retry count and timeout for this test
+ Translator translator =
+ createTranslator(
+ new SessionOptions().setNoResponse(2),
+ new TranslatorOptions().setMaxRetries(0).setTimeout(Duration.ofMillis(1)));
+
+ Assertions.assertThrows(ConnectionException.class, translator::getUsage);
+ }
+
+ @Test
+ void testTranslateTooManyRequests() {
+ Assumptions.assumeTrue(isMockServer);
+ // Lower the retry count and timeout for this test
+ Translator translator =
+ createTranslator(
+ new SessionOptions().setRespondWith429(2), new TranslatorOptions().setMaxRetries(0));
+
+ Assertions.assertThrows(
+ TooManyRequestsException.class,
+ () -> translator.translateText(exampleText.get("en"), null, "DE"));
+ }
+
+ @Test
+ void testUsageOverrun() throws DeepLException, InterruptedException, IOException {
+ Assumptions.assumeTrue(isMockServer);
+ int characterLimit = 20;
+ int documentLimit = 1;
+ // Lower the retry count and timeout for this test
+ Translator translator =
+ createTranslator(
+ new SessionOptions()
+ .setInitCharacterLimit(characterLimit)
+ .setInitDocumentLimit(documentLimit)
+ .withRandomAuthKey(),
+ new TranslatorOptions().setMaxRetries(0).setTimeout(Duration.ofMillis(1)));
+
+ Usage usage = translator.getUsage();
+ Assertions.assertNotNull(usage.getCharacter());
+ Assertions.assertNotNull(usage.getDocument());
+ Assertions.assertNull(usage.getTeamDocument());
+ Assertions.assertEquals(0, usage.getCharacter().getCount());
+ Assertions.assertEquals(0, usage.getDocument().getCount());
+ Assertions.assertEquals(characterLimit, usage.getCharacter().getLimit());
+ Assertions.assertEquals(documentLimit, usage.getDocument().getLimit());
+ Assertions.assertTrue(usage.toString().contains("Characters: 0 of 20"));
+ Assertions.assertTrue(usage.toString().contains("Documents: 0 of 1"));
+
+ File inputFile = createInputFile();
+ writeToFile(inputFile, repeatString("a", characterLimit));
+ File outputFile = createOutputFile();
+
+ translator.translateDocument(inputFile, outputFile, null, "de");
+
+ usage = translator.getUsage();
+ Assertions.assertTrue(usage.anyLimitReached());
+ Assertions.assertNotNull(usage.getCharacter());
+ Assertions.assertNotNull(usage.getDocument());
+ Assertions.assertTrue(usage.getDocument().limitReached());
+ Assertions.assertTrue(usage.getCharacter().limitReached());
+
+ Assertions.assertThrows(
+ IOException.class,
+ () -> {
+ translator.translateDocument(inputFile, outputFile, null, "de");
+ });
+ outputFile.delete();
+
+ DocumentTranslationException thrownDeepLException =
+ Assertions.assertThrows(
+ DocumentTranslationException.class,
+ () -> {
+ translator.translateDocument(inputFile, outputFile, null, "de");
+ });
+ Assertions.assertNull(thrownDeepLException.getHandle());
+ Assertions.assertEquals(
+ QuotaExceededException.class, thrownDeepLException.getCause().getClass());
+
+ Assertions.assertThrows(
+ QuotaExceededException.class,
+ () -> {
+ translator.translateText(exampleText.get("en"), null, "de");
+ });
+ }
+
+ @Test
+ void testUsageTeamDocumentLimit() throws Exception {
+ Assumptions.assumeTrue(isMockServer);
+ int teamDocumentLimit = 1;
+ Translator translator =
+ createTranslator(
+ new SessionOptions()
+ .setInitCharacterLimit(0)
+ .setInitDocumentLimit(0)
+ .setInitTeamDocumentLimit(teamDocumentLimit)
+ .withRandomAuthKey());
+
+ Usage usage = translator.getUsage();
+ Assertions.assertNull(usage.getCharacter());
+ Assertions.assertNull(usage.getDocument());
+ Assertions.assertNotNull(usage.getTeamDocument());
+ Assertions.assertEquals(0, usage.getTeamDocument().getCount());
+ Assertions.assertEquals(teamDocumentLimit, usage.getTeamDocument().getLimit());
+ Assertions.assertTrue(usage.toString().contains("Team documents: 0 of 1"));
+
+ File inputFile = createInputFile();
+ writeToFile(inputFile, "a");
+ File outputFile = createOutputFile();
+
+ translator.translateDocument(inputFile, outputFile, null, "de");
+
+ usage = translator.getUsage();
+ Assertions.assertTrue(usage.anyLimitReached());
+ Assertions.assertNotNull(usage.getTeamDocument());
+ Assertions.assertTrue(usage.getTeamDocument().limitReached());
+ }
+}
diff --git a/deepl-java/src/test/java/com/deepl/api/SessionOptions.java b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java
new file mode 100644
index 0000000..83d0116
--- /dev/null
+++ b/deepl-java/src/test/java/com/deepl/api/SessionOptions.java
@@ -0,0 +1,118 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class SessionOptions {
+ // Mock server session options
+ public Integer noResponse;
+ public Integer respondWith429;
+ public Integer initCharacterLimit;
+ public Integer initDocumentLimit;
+ public Integer initTeamDocumentLimit;
+ public Integer documentFailure;
+ public Duration documentQueueTime;
+ public Duration documentTranslateTime;
+ public Boolean expectProxy;
+
+ public boolean randomAuthKey;
+
+ SessionOptions() {
+ randomAuthKey = false;
+ }
+
+ public Map createSessionHeaders() {
+ Map headers = new HashMap<>();
+
+ String uuid = UUID.randomUUID().toString();
+ headers.put("mock-server-session", "deepl-java-test/" + uuid);
+
+ if (noResponse != null) {
+ headers.put("mock-server-session-no-response-count", noResponse.toString());
+ }
+ if (respondWith429 != null) {
+ headers.put("mock-server-session-429-count", respondWith429.toString());
+ }
+ if (initCharacterLimit != null) {
+ headers.put("mock-server-session-init-character-limit", initCharacterLimit.toString());
+ }
+ if (initDocumentLimit != null) {
+ headers.put("mock-server-session-init-document-limit", initDocumentLimit.toString());
+ }
+ if (initTeamDocumentLimit != null) {
+ headers.put("mock-server-session-init-team-document-limit", initTeamDocumentLimit.toString());
+ }
+ if (documentFailure != null) {
+ headers.put("mock-server-session-doc-failure", documentFailure.toString());
+ }
+ if (documentQueueTime != null) {
+ headers.put(
+ "mock-server-session-doc-queue-time", Long.toString(documentQueueTime.toMillis()));
+ }
+ if (documentTranslateTime != null) {
+ headers.put(
+ "mock-server-session-doc-translate-time",
+ Long.toString(documentTranslateTime.toMillis()));
+ }
+ if (expectProxy != null) {
+ headers.put("mock-server-session-expect-proxy", expectProxy ? "1" : "0");
+ }
+
+ return headers;
+ }
+
+ public SessionOptions setNoResponse(int noResponse) {
+ this.noResponse = noResponse;
+ return this;
+ }
+
+ public SessionOptions setRespondWith429(int respondWith429) {
+ this.respondWith429 = respondWith429;
+ return this;
+ }
+
+ public SessionOptions setInitCharacterLimit(int initCharacterLimit) {
+ this.initCharacterLimit = initCharacterLimit;
+ return this;
+ }
+
+ public SessionOptions setInitDocumentLimit(int initDocumentLimit) {
+ this.initDocumentLimit = initDocumentLimit;
+ return this;
+ }
+
+ public SessionOptions setInitTeamDocumentLimit(int initTeamDocumentLimit) {
+ this.initTeamDocumentLimit = initTeamDocumentLimit;
+ return this;
+ }
+
+ public SessionOptions setDocumentFailure(int documentFailure) {
+ this.documentFailure = documentFailure;
+ return this;
+ }
+
+ public SessionOptions setDocumentQueueTime(Duration documentQueueTime) {
+ this.documentQueueTime = documentQueueTime;
+ return this;
+ }
+
+ public SessionOptions setDocumentTranslateTime(Duration documentTranslateTime) {
+ this.documentTranslateTime = documentTranslateTime;
+ return this;
+ }
+
+ public SessionOptions setExpectProxy(boolean expectProxy) {
+ this.expectProxy = expectProxy;
+ return this;
+ }
+
+ public SessionOptions withRandomAuthKey() {
+ this.randomAuthKey = true;
+ return this;
+ }
+}
diff --git a/deepl-java/src/test/java/com/deepl/api/TestBase.java b/deepl-java/src/test/java/com/deepl/api/TestBase.java
new file mode 100644
index 0000000..b0c6664
--- /dev/null
+++ b/deepl-java/src/test/java/com/deepl/api/TestBase.java
@@ -0,0 +1,177 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import com.deepl.api.utils.*;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class TestBase {
+ protected static final boolean isMockServer;
+ protected static final boolean isMockProxyServer;
+ protected static final String authKey;
+ protected static final String serverUrl;
+ protected static final String proxyUrl;
+
+ protected static final Map exampleText;
+
+ private static final String tempDirBase;
+
+ final String exampleInput = exampleText.get("en");
+ final String exampleLargeInput = repeatString(exampleText.get("en") + "\n", 1000);
+ final String exampleOutput = exampleText.get("de");
+ final String exampleLargeOutput = repeatString(exampleText.get("de") + "\n", 1000);
+ final String tempDir;
+
+ static {
+ isMockServer = System.getenv("DEEPL_MOCK_SERVER_PORT") != null;
+ serverUrl = System.getenv("DEEPL_SERVER_URL");
+ proxyUrl = System.getenv("DEEPL_PROXY_URL");
+ isMockProxyServer = proxyUrl != null;
+ if (isMockServer) {
+ authKey = "mock_server";
+ if (serverUrl == null) {
+ System.err.println(
+ "DEEPL_SERVER_URL environment variable must be set when using mock server.");
+ System.exit(1);
+ }
+ } else {
+ authKey = System.getenv("DEEPL_AUTH_KEY");
+ if (authKey == null) {
+ System.err.println(
+ "DEEPL_AUTH_KEY environment variable must be set unless using mock server.");
+ System.exit(1);
+ }
+ }
+
+ exampleText = new HashMap<>();
+ exampleText.put("bg", "протонен лъч");
+ exampleText.put("cs", "protonový paprsek");
+ exampleText.put("da", "protonstråle");
+ exampleText.put("de", "Protonenstrahl");
+ exampleText.put("el", "δέσμη πρωτονίων");
+ exampleText.put("en", "proton beam");
+ exampleText.put("en-US", "proton beam");
+ exampleText.put("en-GB", "proton beam");
+ exampleText.put("es", "haz de protones");
+ exampleText.put("et", "prootonikiirgus");
+ exampleText.put("fi", "protonisäde");
+ exampleText.put("fr", "faisceau de protons");
+ exampleText.put("hu", "protonnyaláb");
+ exampleText.put("id", "berkas proton");
+ exampleText.put("it", "fascio di protoni");
+ exampleText.put("ja", "陽子ビーム");
+ exampleText.put("lt", "protonų spindulys");
+ exampleText.put("lv", "protonu staru kūlis");
+ exampleText.put("nl", "protonenbundel");
+ 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("ro", "fascicul de protoni");
+ 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("zh", "质子束");
+
+ String tmpdir = System.getProperty("java.io.tmpdir");
+ tempDirBase = tmpdir.endsWith("/") ? tmpdir : tmpdir + "/";
+ }
+
+ protected TestBase() {
+ tempDir = createTempDir();
+ }
+
+ protected Translator createTranslator() {
+ SessionOptions sessionOptions = new SessionOptions();
+ return createTranslator(sessionOptions);
+ }
+
+ protected Translator createTranslator(SessionOptions sessionOptions) {
+ TranslatorOptions translatorOptions = new TranslatorOptions();
+ return createTranslator(sessionOptions, translatorOptions);
+ }
+
+ protected Translator createTranslator(
+ 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 Translator(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();
+ return newTempDir;
+ }
+
+ protected void writeToFile(File file, String content) throws IOException {
+ Boolean justCreated = file.createNewFile();
+ FileWriter writer = new FileWriter(file);
+ writer.write(content);
+ writer.flush();
+ writer.close();
+ }
+
+ protected String readFromFile(File file) throws IOException {
+ if (!file.exists()) return "";
+ return StreamUtil.readStream(new FileInputStream(file));
+ }
+
+ /**
+ * Returns a string containing the input string repeated given number of times. Note:
+ * String.repeat() was added in Java 11.
+ *
+ * @param input Input string to be repeated.
+ * @param number Number of times to repeat string.
+ * @return Input string repeated given number of times.
+ */
+ protected static String repeatString(String input, int number) {
+ StringBuilder sb = new StringBuilder(input.length() * number);
+ for (int i = 0; i < number; i++) {
+ sb.append(input);
+ }
+ return sb.toString();
+ }
+
+ protected File createInputFile() throws IOException {
+ return createInputFile(exampleInput);
+ }
+
+ protected File createInputFile(String content) throws IOException {
+ File inputFile = new File(tempDir + "/example_document.txt");
+ inputFile.delete();
+ inputFile.createNewFile();
+ writeToFile(inputFile, content);
+ return inputFile;
+ }
+
+ protected File createOutputFile() {
+ File outputFile = new File(tempDir + "/output/example_document.txt");
+ new File(outputFile.getParent()).mkdir();
+ outputFile.delete();
+ return outputFile;
+ }
+}
diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java
new file mode 100644
index 0000000..f39b8f8
--- /dev/null
+++ b/deepl-java/src/test/java/com/deepl/api/TranslateDocumentTest.java
@@ -0,0 +1,227 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Date;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+
+public class TranslateDocumentTest extends TestBase {
+ @Test
+ void testTranslateDocument() throws Exception {
+ Translator translator = createTranslator();
+
+ File inputFile = createInputFile();
+ File outputFile = createOutputFile();
+
+ translator.translateDocument(inputFile, outputFile, "en", "de");
+ Assertions.assertEquals(exampleOutput, readFromFile(outputFile));
+
+ // Test with output path occupied
+ Assertions.assertThrows(
+ IOException.class,
+ () -> {
+ translator.translateDocument(inputFile, outputFile, "en", "de");
+ });
+ }
+
+ @Test
+ void testTranslateDocumentFailsWithOutputOccupied() throws Exception {
+ Translator translator = createTranslator();
+
+ File inputFile = createInputFile();
+ File outputFile = createOutputFile();
+ outputFile.createNewFile();
+
+ // Test with output path occupied
+ Assertions.assertThrows(
+ IOException.class,
+ () -> {
+ translator.translateDocument(inputFile, outputFile, "en", "de");
+ });
+ }
+
+ @Test
+ void testTranslateDocumentWithRetry() throws Exception {
+ Assumptions.assumeTrue(isMockServer);
+ Translator translator =
+ createTranslator(
+ new SessionOptions().setNoResponse(1),
+ new TranslatorOptions().setTimeout(Duration.ofSeconds(1)));
+
+ File outputFile = createOutputFile();
+ translator.translateDocument(createInputFile(), outputFile, "en", "de");
+ Assertions.assertEquals(exampleOutput, readFromFile(outputFile));
+ }
+
+ @Test
+ void testTranslateDocumentWithWaiting() throws Exception {
+ Assumptions.assumeTrue(isMockServer);
+ Translator translator =
+ createTranslator(
+ new SessionOptions()
+ .setDocumentTranslateTime(Duration.ofSeconds(2))
+ .setDocumentQueueTime(Duration.ofSeconds(2)));
+ File outputFile = createOutputFile();
+ translator.translateDocument(createInputFile(), outputFile, "en", "de");
+ Assertions.assertEquals(exampleOutput, readFromFile(outputFile));
+ }
+
+ @Test
+ void testTranslateLargeDocument() throws Exception {
+ Assumptions.assumeTrue(isMockServer);
+ Translator translator = createTranslator();
+ File inputFile = createInputFile(exampleLargeInput);
+ File outputFile = createOutputFile();
+ translator.translateDocument(inputFile, outputFile, "en", "de");
+ Assertions.assertEquals(exampleLargeOutput, readFromFile(outputFile));
+ }
+
+ @Test
+ void testTranslateDocumentFormality() throws Exception {
+ Translator translator = createTranslator();
+ File inputFile = createInputFile("How are you?");
+ File outputFile = createOutputFile();
+ translator.translateDocument(
+ inputFile,
+ outputFile,
+ "en",
+ "de",
+ new DocumentTranslationOptions().setFormality(Formality.More));
+ if (!isMockServer) {
+ Assertions.assertEquals("Wie geht es Ihnen?", readFromFile(outputFile));
+ }
+
+ outputFile.delete();
+
+ translator.translateDocument(
+ inputFile,
+ outputFile,
+ "en",
+ "de",
+ new DocumentTranslationOptions().setFormality(Formality.Less));
+ if (!isMockServer) {
+ Assertions.assertEquals("Wie geht es dir?", readFromFile(outputFile));
+ }
+ }
+
+ @Test
+ void testTranslateDocumentFailureDuringTranslation() throws Exception {
+ Translator translator = createTranslator();
+
+ // Translating text from DE to DE will trigger error
+ File inputFile = createInputFile(exampleText.get("de"));
+ File outputFile = createOutputFile();
+
+ DocumentTranslationException exception =
+ Assertions.assertThrows(
+ DocumentTranslationException.class,
+ () -> {
+ translator.translateDocument(inputFile, outputFile, null, "de");
+ });
+ Assertions.assertTrue(exception.getMessage().contains("Source and target language"));
+ }
+
+ @Test
+ void testInvalidDocument() throws Exception {
+ Translator translator = createTranslator();
+ File inputFile = new File(tempDir + "/document.xyz");
+ writeToFile(inputFile, exampleText.get("en"));
+ File outputFile = new File(tempDir + "/output_document.xyz");
+ outputFile.delete();
+
+ DocumentTranslationException exception =
+ Assertions.assertThrows(
+ DocumentTranslationException.class,
+ () -> {
+ translator.translateDocument(inputFile, outputFile, "en", "de");
+ });
+ Assertions.assertNull(exception.getHandle());
+ }
+
+ @Test
+ void testTranslateDocumentLowLevel() throws Exception {
+ Assumptions.assumeTrue(isMockServer);
+ // Set a small document queue time to attempt downloading a queued document
+ Translator translator =
+ createTranslator(new SessionOptions().setDocumentQueueTime(Duration.ofMillis(100)));
+
+ File inputFile = createInputFile();
+ File outputFile = createOutputFile();
+ final DocumentHandle handle = translator.translateDocumentUpload(inputFile, "en", "de");
+
+ DocumentStatus status = translator.translateDocumentStatus(handle);
+ Assertions.assertEquals(handle.getDocumentId(), status.getDocumentId());
+ Assertions.assertTrue(status.ok());
+ Assertions.assertFalse(status.done());
+
+ // Downloading before document is ready will fail
+ Assertions.assertThrows(
+ DocumentNotReadyException.class,
+ () -> {
+ translator.translateDocumentDownload(handle, outputFile);
+ });
+ // Output file should not exist in case of failure
+ Assertions.assertFalse(outputFile.exists());
+
+ // Test recreating a document handle from id & key
+ String documentId = handle.getDocumentId();
+ String documentKey = handle.getDocumentKey();
+ DocumentHandle recreatedHandle = new DocumentHandle(documentId, documentKey);
+ status = translator.translateDocumentStatus(recreatedHandle);
+ Assertions.assertTrue(status.ok());
+
+ while (status.ok() && !status.done()) {
+ Thread.sleep(200);
+ status = translator.translateDocumentStatus(recreatedHandle);
+ }
+
+ Assertions.assertTrue(status.ok() && status.done());
+ translator.translateDocumentDownload(recreatedHandle, outputFile);
+ Assertions.assertEquals(exampleOutput, readFromFile(outputFile));
+ }
+
+ @Test
+ void testTranslateDocumentRequestFields() throws Exception {
+ Assumptions.assumeTrue(isMockServer);
+ Translator translator =
+ createTranslator(
+ new SessionOptions()
+ .setDocumentTranslateTime(Duration.ofSeconds(2))
+ .setDocumentQueueTime(Duration.ofSeconds(2)));
+ File inputFile = createInputFile();
+ File outputFile = createOutputFile();
+
+ long timeBefore = new Date().getTime();
+ DocumentHandle handle = translator.translateDocumentUpload(inputFile, "en", "de");
+ DocumentStatus status = translator.translateDocumentStatus(handle);
+ Assertions.assertTrue(status.ok());
+ Assertions.assertTrue(
+ status.getSecondsRemaining() == null || status.getSecondsRemaining() >= 0);
+ status = translator.translateDocumentWaitUntilDone(handle);
+ translator.translateDocumentDownload(handle, outputFile);
+ long timeAfter = new Date().getTime();
+
+ Assertions.assertEquals(exampleInput.length(), status.getBilledCharacters());
+ Assertions.assertTrue(timeAfter - timeBefore > 4000);
+ Assertions.assertEquals(exampleOutput, readFromFile(outputFile));
+ }
+
+ @Test
+ void testRecreateDocumentHandleInvalid() {
+ Translator translator = createTranslator();
+ String documentId = repeatString("12AB", 8);
+ String documentKey = repeatString("CD34", 16);
+ DocumentHandle handle = new DocumentHandle(documentId, documentKey);
+ Assertions.assertThrows(
+ NotFoundException.class,
+ () -> {
+ translator.translateDocumentStatus(handle);
+ });
+ }
+}
diff --git a/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java
new file mode 100644
index 0000000..57da312
--- /dev/null
+++ b/deepl-java/src/test/java/com/deepl/api/TranslateTextTest.java
@@ -0,0 +1,296 @@
+// Copyright 2022 DeepL SE (https://www.deepl.com)
+// Use of this source code is governed by an MIT
+// license that can be found in the LICENSE file.
+package com.deepl.api;
+
+import java.util.*;
+import java.util.function.Consumer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+
+public class TranslateTextTest extends TestBase {
+
+ @Test
+ void testSingleText() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ TextResult result = translator.translateText(exampleText.get("en"), null, LanguageCode.German);
+ Assertions.assertEquals(exampleText.get("de"), result.getText());
+ Assertions.assertEquals("en", result.getDetectedSourceLanguage());
+ }
+
+ @Test
+ void testTextArray() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ List texts = new ArrayList<>();
+ texts.add(exampleText.get("fr"));
+ texts.add(exampleText.get("en"));
+ List result = translator.translateText(texts, null, LanguageCode.German);
+ Assertions.assertEquals(exampleText.get("de"), result.get(0).getText());
+ Assertions.assertEquals(exampleText.get("de"), result.get(1).getText());
+ }
+
+ @Test
+ void testSourceLang() throws DeepLException, InterruptedException {
+ Consumer checkResult =
+ (result) -> {
+ Assertions.assertEquals(exampleText.get("de"), result.getText());
+ Assertions.assertEquals("en", result.getDetectedSourceLanguage());
+ };
+
+ Translator translator = createTranslator();
+ checkResult.accept(translator.translateText(exampleText.get("en"), null, "DE"));
+ checkResult.accept(translator.translateText(exampleText.get("en"), "En", "DE"));
+ checkResult.accept(translator.translateText(exampleText.get("en"), "en", "DE"));
+
+ List sourceLanguages = translator.getSourceLanguages();
+ Language sourceLanguageEn =
+ sourceLanguages.stream()
+ .filter((language -> Objects.equals(language.getCode(), "en")))
+ .findFirst()
+ .orElse(null);
+ Language sourceLanguageDe =
+ sourceLanguages.stream()
+ .filter((language -> Objects.equals(language.getCode(), "de")))
+ .findFirst()
+ .orElse(null);
+ Assertions.assertNotNull(sourceLanguageEn);
+ Assertions.assertNotNull(sourceLanguageDe);
+ checkResult.accept(
+ translator.translateText(exampleText.get("en"), sourceLanguageEn, sourceLanguageDe));
+ }
+
+ @Test
+ void testTargetLang() throws DeepLException, InterruptedException {
+ Consumer checkResult =
+ (result) -> {
+ Assertions.assertEquals(exampleText.get("de"), result.getText());
+ Assertions.assertEquals("en", result.getDetectedSourceLanguage());
+ };
+
+ Translator translator = createTranslator();
+ checkResult.accept(translator.translateText(exampleText.get("en"), null, "De"));
+ checkResult.accept(translator.translateText(exampleText.get("en"), null, "de"));
+ checkResult.accept(translator.translateText(exampleText.get("en"), null, "DE"));
+
+ List targetLanguages = translator.getTargetLanguages();
+ Language targetLanguageDe =
+ targetLanguages.stream()
+ .filter((language -> Objects.equals(language.getCode(), "de")))
+ .findFirst()
+ .orElse(null);
+ Assertions.assertNotNull(targetLanguageDe);
+ checkResult.accept(translator.translateText(exampleText.get("en"), null, targetLanguageDe));
+
+ // Check that en and pt as target languages throw an exception
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ translator.translateText(exampleText.get("de"), null, "en");
+ });
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ translator.translateText(exampleText.get("de"), null, "pt");
+ });
+ }
+
+ @Test
+ void testInvalidLanguage() {
+ Translator translator = createTranslator();
+ DeepLException thrown;
+ thrown =
+ Assertions.assertThrows(
+ DeepLException.class,
+ () -> {
+ translator.translateText(exampleText.get("en"), null, "XX");
+ });
+ Assertions.assertTrue(thrown.getMessage().contains("target_lang"));
+
+ thrown =
+ Assertions.assertThrows(
+ DeepLException.class,
+ () -> {
+ translator.translateText(exampleText.get("en"), "XX", "de");
+ });
+ Assertions.assertTrue(thrown.getMessage().contains("source_lang"));
+ }
+
+ @Test
+ void testTranslateWithRetries() throws DeepLException, InterruptedException {
+ Assumptions.assumeTrue(isMockServer);
+ Translator translator = createTranslator(new SessionOptions().setRespondWith429(2));
+
+ long timeBefore = new Date().getTime();
+ List texts = new ArrayList<>();
+ texts.add(exampleText.get("en"));
+ texts.add(exampleText.get("ja"));
+ List result = translator.translateText(texts, null, "de");
+ long timeAfter = new Date().getTime();
+
+ Assertions.assertEquals(2, result.size());
+ Assertions.assertEquals(exampleText.get("de"), result.get(0).getText());
+ Assertions.assertEquals("en", result.get(0).getDetectedSourceLanguage());
+ Assertions.assertEquals(exampleText.get("de"), result.get(1).getText());
+ Assertions.assertEquals("ja", result.get(1).getDetectedSourceLanguage());
+ Assertions.assertTrue(timeAfter - timeBefore > 1000);
+ }
+
+ @Test
+ void testFormality() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ TextResult result;
+
+ result =
+ translator.translateText(
+ "How are you?", null, "de", new TextTranslationOptions().setFormality(Formality.Less));
+ if (!isMockServer) {
+ Assertions.assertEquals("Wie geht es dir?", result.getText());
+ }
+
+ result =
+ translator.translateText(
+ "How are you?",
+ null,
+ "de",
+ new TextTranslationOptions().setFormality(Formality.Default));
+ if (!isMockServer) {
+ Assertions.assertEquals("Wie geht es Ihnen?", result.getText());
+ }
+
+ result =
+ translator.translateText(
+ "How are you?", null, "de", new TextTranslationOptions().setFormality(Formality.More));
+ if (!isMockServer) {
+ Assertions.assertEquals("Wie geht es Ihnen?", result.getText());
+ }
+ }
+
+ @Test
+ void testSplitSentences() throws DeepLException, InterruptedException {
+ Assumptions.assumeTrue(isMockServer);
+
+ Translator translator = createTranslator();
+ String text =
+ "If the implementation is hard to explain, it's a bad idea.\nIf the implementation is easy to explain, it may be a good idea.";
+
+ translator.translateText(
+ text,
+ null,
+ "de",
+ new TextTranslationOptions().setSentenceSplittingMode(SentenceSplittingMode.Off));
+ translator.translateText(
+ text,
+ null,
+ "de",
+ new TextTranslationOptions().setSentenceSplittingMode(SentenceSplittingMode.All));
+ translator.translateText(
+ text,
+ null,
+ "de",
+ new TextTranslationOptions().setSentenceSplittingMode(SentenceSplittingMode.NoNewlines));
+ }
+
+ @Test
+ void testPreserveFormatting() throws DeepLException, InterruptedException {
+ Assumptions.assumeTrue(isMockServer);
+
+ Translator translator = createTranslator();
+ translator.translateText(
+ exampleText.get("en"),
+ null,
+ "de",
+ new TextTranslationOptions().setPreserveFormatting(true));
+ translator.translateText(
+ exampleText.get("en"),
+ null,
+ "de",
+ new TextTranslationOptions().setPreserveFormatting(false));
+ }
+
+ @Test
+ void testTagHandlingXML() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ String text =
+ "A document's title"
+ + ""
+ + "This is a sentence split"
+ + "across two <span> tags that should be treated as one."
+ + ""
+ + ""
+ + "Here is a sentence. Followed by a second one."
+ + "This sentence will not be translated."
+ + ""
+ + "";
+ TextResult result =
+ translator.translateText(
+ text,
+ null,
+ "de",
+ new TextTranslationOptions()
+ .setTagHandling("xml")
+ .setOutlineDetection(false)
+ .setNonSplittingTags(Arrays.asList("span"))
+ .setSplittingTags(Arrays.asList("title", "par"))
+ .setIgnoreTags(Arrays.asList("raw")));
+ if (!isMockServer) {
+ Assertions.assertTrue(
+ result.getText().contains("This sentence will not be translated."));
+ Assertions.assertTrue(result.getText().matches(".*.*Der Titel.*.*"));
+ }
+ }
+
+ @Test
+ void testTagHandlingHTML() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ String text =
+ ""
+ + ""
+ + ""
+ + "My First Heading
"
+ + "My first paragraph.
"
+ + ""
+ + "";
+
+ TextResult result =
+ translator.translateText(
+ text, null, "de", new TextTranslationOptions().setTagHandling("html"));
+ if (!isMockServer) {
+ Assertions.assertTrue(result.getText().contains("Meine erste Überschrift
"));
+ Assertions.assertTrue(
+ result.getText().contains("My first paragraph.
"));
+ }
+ }
+
+ @Test
+ void testEmptyText() {
+ Translator translator = createTranslator();
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ translator.translateText("", null, "de");
+ });
+ }
+
+ @Test
+ void testMixedCaseLanguages() throws DeepLException, InterruptedException {
+ Translator translator = createTranslator();
+ TextResult result;
+
+ result = translator.translateText(exampleText.get("de"), null, "en-us");
+ Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase());
+ Assertions.assertEquals("de", result.getDetectedSourceLanguage());
+
+ result = translator.translateText(exampleText.get("de"), null, "EN-us");
+ Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase());
+ Assertions.assertEquals("de", result.getDetectedSourceLanguage());
+
+ result = translator.translateText(exampleText.get("de"), "de", "EN-US");
+ Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase());
+ Assertions.assertEquals("de", result.getDetectedSourceLanguage());
+
+ result = translator.translateText(exampleText.get("de"), "dE", "EN-US");
+ Assertions.assertEquals(exampleText.get("en-US"), result.getText().toLowerCase());
+ Assertions.assertEquals("de", result.getDetectedSourceLanguage());
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..656f2e6
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+org.gradle.jvmargs=-Dfile.encoding=UTF-8
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..41d9927
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..aa991fc
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..1b6c787
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..37af439
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,3 @@
+
+rootProject.name = "deepl-java"
+include("deepl-java")