500K characters included every month. Add payment details to start.
Java Developer Guide

Best Translation API
for Java & Spring Boot

Compare Google Translate, DeepL, and Langbly from a Java perspective. Auth setup, HttpClient and RestClient examples, async translation, and what each one costs.

Payment details required. No charge within 500K characters/month.

Pricing comparison

Real pricing based on monthly character volume. All prices in USD.

VolumeGoogle TranslateDeepLLangblyYou Save
500K chars/mo$0Check live plan$0
Both included/evaluation
1M chars/mo$10Check live plan$2.50
75% vs Google
5M chars/mo$90Check live plan$22.50
75% vs Google
25M chars/mo$490Check live plan$122.50
75% vs Google
100M chars/mo$1,990Above Growth limit$497.50
75% vs Google

Feature comparison

FeatureGoogle TranslateDeepLLangbly
Official Java SDKgoogle-cloud-translatedeepl-javaNo SDK needed
Maven dependencycom.google.cloudcom.deepl.apiNone
Auth methodService account JSONAPI key headerAPI key header
Works with java.net.http.HttpClientNeeds OAuth2 token
Works with Spring RestClient / WebClientNeeds OAuth2 token
Setup time to first call10–15 min2 min2 min
Request formatGoogle v2 JSONDeepL JSONGoogle v2 JSON
Google v2 compatible
Async via CompletableFuture
Source language auto-detect
HTML-safe mode
Language coverage100+30+100+
Published usage rate$20/1M after 500KCheck live Growth plan$5/1M after 500K
Included/evaluation usage500K/moDeveloper: 1M total500K/mo
Translation qualityNMTNMT + AINext-gen AI
Context-awareLimited

Calling a translation API from plain Java

Java 11 shipped java.net.http.HttpClient, and for a JSON translation API that is genuinely all you need. No client library, no shaded transitive dependency tree, no version conflict with the rest of your build.

That only works if the API accepts a static API key. Google Cloud Translation does not: it expects an OAuth2 access token minted from a service account, so you either pull in google-cloud-translate and its auth stack or you implement JWT signing yourself. DeepL and Langbly both take a plain Authorization header, which is why the examples below fit on one screen.

Here is a complete Langbly client using HttpClient, a record, and Jackson:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Map;

public final class LangblyClient {

    private static final URI ENDPOINT =
            URI.create("https://api.langbly.com/language/translate/v2");

    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
    private final ObjectMapper mapper = new ObjectMapper();
    private final String apiKey;

    public LangblyClient(String apiKey) {
        this.apiKey = apiKey;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Translation(String translatedText, String detectedSourceLanguage) { }

    @JsonIgnoreProperties(ignoreUnknown = true)
    record Payload(List<Translation> translations) { }

    @JsonIgnoreProperties(ignoreUnknown = true)
    record TranslateResponse(Payload data) { }

    public Translation translate(String text, String target)
            throws IOException, InterruptedException {

        String json = mapper.writeValueAsString(
                Map.of("q", text, "target", target, "format", "text"));

        HttpRequest request = HttpRequest.newBuilder(ENDPOINT)
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .timeout(Duration.ofSeconds(30))
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

        HttpResponse<String> response =
                http.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new IOException("Langbly returned HTTP " + response.statusCode()
                    + ": " + response.body());
        }

        return mapper.readValue(response.body(), TranslateResponse.class)
                .data().translations().get(0);
    }
}

Calling it:

LangblyClient client = new LangblyClient(System.getenv("LANGBLY_API_KEY"));
LangblyClient.Translation result = client.translate("Hello world", "nl");

System.out.println(result.translatedText());         // Hallo wereld
System.out.println(result.detectedSourceLanguage()); // en

The wire format is the Google Translate v2 format, which matters if you are migrating. Request body takes q and target, plus optional source and format ("text" or "html"). The response looks like this:

{"data":{"translations":[{"translatedText":"Hallo wereld","detectedSourceLanguage":"en"}]}}

Because Langbly is a drop-in replacement for the Google Translate v2 API, an existing Java integration usually needs one change: swap the base URL and send a bearer token instead of an OAuth2 credential. Your DTOs stay exactly as they are.

Note that there is no official Langbly Java SDK. Official SDKs cover Python, JavaScript/TypeScript, and PHP. On the JVM the raw HTTP surface is small enough that a thin wrapper like the one above tends to be less trouble than a dependency anyway.

Spring Boot: a translation service bean you can inject

In a Spring Boot app you want the key in configuration, the HTTP plumbing in one bean, and a method signature the rest of the codebase can call without knowing anything about translation.

Keep the key out of source control by reading it from the environment in application.yml:

langbly:
  api-key: ${LANGBLY_API_KEY}

Then build the service on RestClient, the synchronous client introduced in Spring Framework 6.1 and Spring Boot 3.2. It replaces RestTemplate for new code and gives you a fluent API without dragging in the reactive stack:

package com.example.i18n;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

import java.util.List;
import java.util.Map;

@Service
public class TranslationService {

    private final RestClient client;

    public TranslationService(RestClient.Builder builder,
                              @Value("${langbly.api-key}") String apiKey) {
        this.client = builder
                .baseUrl("https://api.langbly.com")
                .defaultHeader("Authorization", "Bearer " + apiKey)
                .build();
    }

    public record Translation(String translatedText, String detectedSourceLanguage) { }

    record Payload(List<Translation> translations) { }

    record TranslateResponse(Payload data) { }

    public String translate(String text, String targetLanguage) {
        TranslateResponse response;
        try {
            response = client.post()
                    .uri("/language/translate/v2")
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(Map.of("q", text, "target", targetLanguage, "format", "text"))
                    .retrieve()
                    .body(TranslateResponse.class);
        } catch (RestClientException e) {
            throw new TranslationFailedException(
                    "Translation request to Langbly failed", e);
        }

        if (response == null
                || response.data() == null
                || response.data().translations().isEmpty()) {
            throw new TranslationFailedException("Langbly returned an empty translation");
        }

        return response.data().translations().get(0).translatedText();
    }
}

// TranslationFailedException.java
public class TranslationFailedException extends RuntimeException {

    public TranslationFailedException(String message) {
        super(message);
    }

    public TranslationFailedException(String message, Throwable cause) {
        super(message, cause);
    }
}

Two details worth keeping. First, take RestClient.Builder from the constructor rather than calling RestClient.create(): Spring Boot pre-configures the builder with your observability, logging, and timeout settings, and you inherit all of it for free. Second, retrieve() throws on 4xx and 5xx by default, so catching RestClientException is what turns an HTTP failure into a domain exception instead of a null downstream.

On a WebFlux stack the shape is identical with WebClient: swap RestClient.Builder for WebClient.Builder, use bodyValue(...), and return Mono<String> from bodyToMono(TranslateResponse.class).map(...).

Doing the same against Google Cloud Translation means a service account JSON file on disk or in a secret mount, GoogleCredentials wiring, and token refresh. It works, but it is a lot more surface area for something that is one POST request.

Translating MessageSource bundles in bulk, and what it costs

The most common real Java use case is not translating one string at a time. It is taking messages.properties and producing messages_nl.properties, messages_de.properties, and so on for every locale your MessageSource serves.

A few hundred keys done sequentially is slow, so fire the requests concurrently. Add an async method to the client from the first section:

import com.fasterxml.jackson.core.JsonProcessingException;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

public CompletableFuture<String> translateAsync(String text, String target) {
    String json;
    try {
        json = mapper.writeValueAsString(
                Map.of("q", text, "target", target, "format", "text"));
    } catch (JsonProcessingException e) {
        return CompletableFuture.failedFuture(e);
    }

    HttpRequest request = HttpRequest.newBuilder(ENDPOINT)
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();

    return http.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(response -> {
                if (response.statusCode() != 200) {
                    throw new CompletionException(new IOException(
                            "Langbly returned HTTP " + response.statusCode()));
                }
                try {
                    return mapper.readValue(response.body(), TranslateResponse.class)
                            .data().translations().get(0).translatedText();
                } catch (JsonProcessingException e) {
                    throw new CompletionException(e);
                }
            });
}

Now the bundle translator is a short loop plus one join:

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;

public class BundleTranslator {

    private final LangblyClient client;

    public BundleTranslator(LangblyClient client) {
        this.client = client;
    }

    public void translate(Path source, Path target, String language) throws IOException {
        Properties in = new Properties();
        try (Reader reader = Files.newBufferedReader(source, StandardCharsets.UTF_8)) {
            in.load(reader);
        }

        Map<String, CompletableFuture<String>> pending = new LinkedHashMap<>();
        for (String key : in.stringPropertyNames()) {
            pending.put(key, client.translateAsync(in.getProperty(key), language));
        }

        CompletableFuture
                .allOf(pending.values().toArray(CompletableFuture[]::new))
                .join();

        Properties out = new Properties();
        pending.forEach((key, future) -> out.setProperty(key, future.join()));

        try (Writer writer = Files.newBufferedWriter(target, StandardCharsets.UTF_8)) {
            out.store(writer, "Machine translated to " + language);
        }
    }
}

Two gotchas. MessageFormat placeholders such as {0} and single-quote escaping survive most of the time, but review the generated file before shipping a locale, because a moved placeholder is a runtime formatting bug rather than a typo. And Properties does not preserve key order when it writes, so if you want clean diffs, write the file yourself from a sorted key set instead of calling store.

Frequently asked questions

What is the best translation API for Java?

It depends on what you optimise for. Langbly is the easiest to wire up from Java because it takes a bearer token and speaks the Google Translate v2 format, so java.net.http.HttpClient or Spring RestClient is all you need, and it is the cheapest at $5 per million characters. DeepL is a good choice if your languages are mostly European and it ships an official deepl-java library. Google Translate has the widest reach but needs service account credentials and OAuth2 token handling.

Is there an official Langbly Java SDK?

No. Official SDKs cover Python, JavaScript/TypeScript, and PHP. On the JVM you call the REST endpoint directly with java.net.http.HttpClient, Spring RestClient, WebClient, OkHttp, or anything else that speaks HTTP. The API takes a JSON body with q and target and returns JSON, so a small wrapper class plus Jackson is usually 40 lines.

How do I translate text in a Spring Boot application?

Create a @Service bean that holds a RestClient built from the injected RestClient.Builder, set the base URL and an Authorization header with your key from @Value, then POST to /language/translate/v2 with q and target. Deserialize the response into a small record and return translatedText. Inject that service anywhere you need a translation.

Is the Google Translate API free for Java developers?

Google Cloud Translation includes the first 500,000 characters each month, then charges $20 per million for standard NMT. Langbly also includes the first 500,000 input characters each month and charges $5 per million additional input characters. DeepL Developer includes one million characters in total; check the live Growth plan for recurring use.

Can I switch from Google Translate to Langbly without rewriting my Java code?

In most cases yes. Langbly is a drop-in replacement for the Google Translate v2 API: same request fields, same response shape with data.translations[].translatedText and detectedSourceLanguage. Point your client at https://api.langbly.com/language/translate/v2 and send an Authorization: Bearer header instead of an OAuth2 token. Your existing DTOs and mapping code keep working, and you drop the service account plumbing.

Ready to try Langbly?

500K characters included every month. Migrate in minutes.

Same API as Google Translate v2. Just change the URL.