Pricing comparison
Real pricing based on monthly character volume. All prices in USD.
| Volume | Google Translate | DeepL | Langbly | You Save |
|---|---|---|---|---|
| 500K chars/mo | $0 | Check live plan | $0 | Both included/evaluation |
| 1M chars/mo | $10 | Check live plan | $2.50 | 75% vs Google |
| 5M chars/mo | $90 | Check live plan | $22.50 | 75% vs Google |
| 25M chars/mo | $490 | Check live plan | $122.50 | 75% vs Google |
| 100M chars/mo | $1,990 | Above Growth limit | $497.50 | 75% vs Google |
Feature comparison
| Feature | Google Translate | DeepL | Langbly |
|---|---|---|---|
| Official Java SDK | google-cloud-translate | deepl-java | No SDK needed |
| Maven dependency | com.google.cloud | com.deepl.api | None |
| Auth method | Service account JSON | API key header | API key header |
| Works with java.net.http.HttpClient | Needs OAuth2 token | ||
| Works with Spring RestClient / WebClient | Needs OAuth2 token | ||
| Setup time to first call | 10–15 min | 2 min | 2 min |
| Request format | Google v2 JSON | DeepL JSON | Google v2 JSON |
| Google v2 compatible | |||
| Async via CompletableFuture | |||
| Source language auto-detect | |||
| HTML-safe mode | |||
| Language coverage | 100+ | 30+ | 100+ |
| Published usage rate | $20/1M after 500K | Check live Growth plan | $5/1M after 500K |
| Included/evaluation usage | 500K/mo | Developer: 1M total | 500K/mo |
| Translation quality | NMT | NMT + AI | Next-gen AI |
| Context-aware | Limited |
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.