So Google Translate Knows Your Business Literally
Every time you paste text into Google Translate, that text goes to Google. For a quick recipe or a tourist menu, sure, whatever. But if you’re translating customer support tickets, internal docs, medical notes, or literally anything that touches a user’s data, you’ve just handed it to a multi-billion-dollar ad company under a ToS that says, in so many words, “thanks for the training data.”
The Google Translate API is also pay-per-character past the free tier. DeepL is SaaS-only. Microsoft Translator wants an Azure account. All of them want your data living on their infrastructure.
LibreTranslate says no to all of that. It’s an open-source, self-hosted translation API running on Argos Translate models. Neural machine translation that runs entirely on your hardware, air-gapped if you want. Fifty-plus language pairs as of 2026, REST API, Docker-native, and it doesn’t phone home.
Let’s set it up.
What LibreTranslate Actually Is
LibreTranslate is a wrapper and API server around Argos Translate, which is itself a Python NMT (Neural Machine Translation) library. The models are standard .argosmodel packages, pre-trained on open datasets, downloadable, and self-contained. No runtime cloud calls, no API keys going out.
You get:
- A REST API at
/translate,/languages,/detect - A basic web UI included (you can disable it)
- Per-language-pair models that load independently, so you’re not pulling a 20 GB monolith
- Optional API key auth for public-facing instances
- GPU acceleration if you have CUDA
The catch: model quality is genuinely good for major language pairs (English ↔ French, Spanish, German, Chinese, Japanese) and noticeably weaker for low-resource languages. It’s not DeepL. It’s also not sending your company’s customer emails to Mountain View.
Docker Compose Setup
The key to keeping LibreTranslate usable on normal hardware is LT_LOAD_ONLY. Don’t load every model. Each language pair eats roughly 1 to 2 GB of RAM. Load only what you need.
services: libretranslate: image: libretranslate/libretranslate:v1.9.5 container_name: libretranslate restart: unless-stopped ports: - "5000:5000" environment: LT_LOAD_ONLY: "en,es,fr,de,zh,ja,pt,ru" LT_THREADS: "4" LT_CHAR_LIMIT: "5000" LT_REQ_LIMIT: "100" LT_BATCH_LIMIT: "25" LT_API_KEYS: "false" LT_DISABLE_WEB_UI: "false" LT_UPDATE_MODELS: "false" volumes: - lt_models:/home/libretranslate/.local/share/argos-translate - lt_db:/app/db healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:5000/languages || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 120s
volumes: lt_models: lt_db:A few things worth explaining:
LT_LOAD_ONLY: This is the most important env var. The format is a comma-separated list of language codes. LibreTranslate will only download and load models that involve those languages. English-to-French, French-to-English, etc. If you omit this, it tries to load every model it knows about and your box will have a bad time.
Set LT_UPDATE_MODELS to "false" after first run once your models are cached in the volume. On first boot, the container downloads the models (this takes a few minutes and several gigabytes depending on your language set). After that, you want it reading from volume, not re-downloading.
start_period: 120s: First boot takes a while. Don’t let your orchestrator restart-loop it while models are downloading.
LT_THREADS sets the CPU threads for translation. On a Pi 5 (4 cores), 4 is reasonable. On a real server, bump it.
First run, just do:
docker compose up -ddocker compose logs -f libretranslateWatch the logs. You’ll see it downloading .argosmodel files. Once you see Running on http://0.0.0.0:5000, you’re ready.
The API Three Endpoints You Actually Need
GET /languages
curl http://localhost:5000/languagesReturns a JSON array of all loaded language pairs:
[ {"code": "en", "name": "English"}, {"code": "fr", "name": "French"}, {"code": "de", "name": "German"}, {"code": "es", "name": "Spanish"}, {"code": "zh", "name": "Chinese"}, {"code": "ja", "name": "Japanese"}, {"code": "pt", "name": "Portuguese"}, {"code": "ru", "name": "Russian"}]POST /translate
curl -s -X POST http://localhost:5000/translate \ -H "Content-Type: application/json" \ -d '{ "q": "The quick brown fox jumps over the lazy dog.", "source": "en", "target": "fr" }'Response:
{ "translatedText": "Le rapide renard brun saute par-dessus le chien paresseux."}For comparison, Google Translate returns: “Le renard brun rapide saute par-dessus le chien paresseux.”
Both are correct French. Word order differs slightly because French adjective placement varies by style. For everyday use, you won’t notice the difference. For literary translation, hire a human.
POST /detect
curl -s -X POST http://localhost:5000/detect \ -H "Content-Type: application/json" \ -d '{"q": "Hola mundo"}'[ {"confidence": 0.99, "language": "es"}]Useful when your input language is unknown: pipe /detect first, then /translate.
Translating Large Blobs
LibreTranslate has a character limit (LT_CHAR_LIMIT, default 5000). For anything larger (a support ticket, a document, a log file), you need to chunk it.
The right way to chunk is at sentence boundaries, not arbitrary character counts. Cutting mid-sentence produces garbage translation at the seam.
Here’s a Python example using NLTK for sentence tokenization:
import requestsimport nltk
nltk.download("punkt_tab", quiet=True)from nltk.tokenize import sent_tokenize
LIBRETRANSLATE_URL = "http://localhost:5000/translate"CHUNK_SIZE = 4000 # characters, stay under LT_CHAR_LIMIT
def chunk_sentences(text: str, max_chars: int) -> list[str]: sentences = sent_tokenize(text) chunks = [] current = "" for sentence in sentences: if len(current) + len(sentence) + 1 > max_chars: if current: chunks.append(current.strip()) current = sentence else: current += " " + sentence if current: chunks.append(current.strip()) return chunks
def translate(text: str, source: str = "en", target: str = "fr") -> str: chunks = chunk_sentences(text, CHUNK_SIZE) translated_parts = [] for chunk in chunks: resp = requests.post( LIBRETRANSLATE_URL, json={"q": chunk, "source": source, "target": target}, timeout=30, ) resp.raise_for_status() translated_parts.append(resp.json()["translatedText"]) return " ".join(translated_parts)
if __name__ == "__main__": sample = """Your support ticket content goes here. It can be multiple paragraphs. LibreTranslate will handle it in chunks, respecting sentence boundaries so the output doesn't read like it was cut with a chainsaw.""" print(translate(sample, source="en", target="de"))Performance Reality Check
Honestly? CPU-only translation is not instant. Here’s what to expect:
| Hardware | Language Pair | ~Time per sentence |
|---|---|---|
| Raspberry Pi 5 (4-core) | en → fr | ~2s |
| Mid-range server (8-core) | en → fr | ~0.3s |
| NVIDIA GPU (CUDA) | en → fr | <0.1s |
For async workloads (translate a doc in the background, webhook when done), Pi 5 is fine. For real-time chat translation where a user is staring at a spinner, you want actual CPU headroom or a GPU.
GPU Acceleration
If you have an NVIDIA GPU with CUDA support, add the following to your Compose file:
services: libretranslate: # ... existing config ... environment: LT_LOAD_ONLY: "en,es,fr,de,zh,ja,pt,ru" LT_THREADS: "8" # CUDA is handled by the runtime below deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu]Make sure nvidia-container-toolkit is installed on the host. The image ships with CUDA support; it just needs the runtime exposed.
RAM Budget
Plan for roughly 1 to 2 GB per loaded language pair. “Language pair” here means directional: en→fr and fr→en are two pairs.
With LT_LOAD_ONLY: "en,es,fr,de,zh,ja,pt,ru":
- That’s 8 languages
- Worst case: 7 languages × 2 directions × ~1.2 GB = ~17 GB
- Real-world (shared base models): closer to 8 to 10 GB for this set
If you’re on a 8 GB homelab box, trim your language list hard. English + Spanish + French is a very usable starting set that’ll stay under 4 GB.
Use Cases That Actually Make Sense
Nextcloud Talk Bot
If you’re running Nextcloud Talk as your team’s internal chat, you can wire LibreTranslate as the translation backend. Nextcloud 28+ supports pluggable translation providers. Point it at your LibreTranslate instance in the admin settings under “Artificial Intelligence” → “Translation provider.”
No setup beyond the URL. Your chat translations never leave your server.
Home Assistant TTS Pipeline
HA’s tts.speak action works in the language your TTS engine supports. If your engine doesn’t support a language, translate first:
- alias: "Translate and speak alert" trigger: ... action: - action: rest_command.libretranslate data: q: "Motion detected in the garage" source: "en" target: "es" response_variable: translated - action: tts.speak target: entity_id: media_player.kitchen_speaker data: message: "{{ translated.translatedText }}"Define the rest_command.libretranslate in your configuration.yaml:
rest_command: libretranslate: url: "http://libretranslate:5000/translate" method: POST headers: Content-Type: "application/json" payload: '{"q": "{{ q }}", "source": "{{ source }}", "target": "{{ target }}"}' content_type: "application/json"Web Frontend Integration
The web UI is built in and reasonably clean for internal tools. If you want to embed translation in your own frontend:
async function translateText(text, source, target) { const response = await fetch("http://localhost:5000/translate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ q: text, source, target }), }); const data = await response.json(); return data.translatedText;}Lock It Down: API Keys and Rate Limiting
Don’t expose an unauthenticated LibreTranslate instance to the internet. Anyone who finds it gets free translation on your hardware. Set LT_API_KEYS: "true" and manage keys through the admin UI, or via the SQLite DB it creates.
With API keys enabled, requests need an api_key field:
curl -X POST http://localhost:5000/translate \ -H "Content-Type: application/json" \ -d '{ "q": "Hello", "source": "en", "target": "fr", "api_key": "your-key-here" }'For external exposure, put Caddy in front:
translate.yourdomain.com { rate_limit { zone translate { key {remote_host} events 10 window 1m } } reverse_proxy libretranslate:5000}Heads up: rate_limit isn’t built into stock Caddy; you need a custom build with the caddy-ratelimit module (xcaddy build --with github.com/mholt/caddy-ratelimit). If you don’t want to build a custom binary, do the rate limiting in LibreTranslate itself via LT_REQ_LIMIT.
LibreTranslate vs. Bergamot vs. DeepL
Bergamot Translator is the engine behind Firefox Translations: in-browser, offline, privacy-preserving. It’s great if your use case is “a person using Firefox translating web pages.” It doesn’t give you an API, doesn’t run server-side, and has more limited language support. It and LibreTranslate are solving different problems.
DeepL has no self-hosted option. DeepL Pro is SaaS, the API is SaaS, and the quality is genuinely better than either of the above, but every query goes to their servers. If you need the quality and can accept the cloud dependency, use DeepL. If you need local, it’s not an option.
LibreTranslate lives in the middle: server-side API you control, good quality for major language pairs, open source, air-gappable. The tradeoff is CPU cost and slightly lower quality on edge cases.
Should You Bother?
Yes, if:
- You’re translating anything with user data, customer content, or internal business context
- You want a translation API you can embed in self-hosted apps (Nextcloud, Home Assistant, custom tooling)
- You’re running an enterprise-adjacent homelab and Google Translate’s ToS makes your legal team nervous
- You want no per-request billing and full control over model updates
Probably not, if:
- You need the absolute best translation quality and the cloud is fine with you: DeepL wins on quality
- Your use case is purely in Firefox: use the built-in Firefox Translations (Bergamot) instead
- You’re translating low-resource languages (Swahili, Tagalog, etc.): the Argos models for those are noticeably weak
The RAM cost is real. The setup is real. But “I don’t want Google reading my support tickets” is also a real requirement, and LibreTranslate is currently the cleanest open-source answer to it.
Your 2 AM on-call self will appreciate not having to explain to legal why every customer complaint is in Google’s training dataset.