# LibreTranslate setup for diplomatic mail This document describes how to run the LibreTranslate backend that the diplomatic-mail subsystem uses for body translation. The instructions target three audiences: developers spinning up LibreTranslate alongside `tools/local-dev`, operators preparing a real deployment, and reviewers verifying the end-to-end translation flow by hand. ## When you need LibreTranslate The diplomatic-mail worker runs unconditionally — `make up` and `make test` both work without any translator. With `BACKEND_DIPLOMAIL_TRANSLATOR_URL` unset, the noop translator short-circuits the pipeline: messages are delivered in the original language, and the inbox handler returns the original body to every reader. You only need LibreTranslate when you want to exercise the cross- language path: sender writes in language X, recipient's `accounts.preferred_language` is Y, the worker is expected to fetch a Y rendering. The pipeline is otherwise identical and unaware of which engine is producing translations. ## Running a local instance LibreTranslate ships a public Docker image at `libretranslate/libretranslate`. The image is ~3 GB on first pull because it bundles every supported language model; subsequent runs reuse the layer cache. The simplest setup is a one-shot container: ```bash docker run --rm -d --name libretranslate \ -p 5000:5000 \ -e LT_LOAD_ONLY=en,ru \ libretranslate/libretranslate:latest ``` The `LT_LOAD_ONLY` whitelist trims the loaded model set so the container fits in ~600 MB of RAM. Drop the variable to load every language pair LibreTranslate ships. LibreTranslate boots in ~30 seconds (cold) or ~5 seconds (warm model cache). Wait until `curl -s http://localhost:5000/languages` returns a JSON array before pointing backend at it. ## Wiring backend at it Add three env vars to the backend process: ``` BACKEND_DIPLOMAIL_TRANSLATOR_URL=http://localhost:5000 BACKEND_DIPLOMAIL_TRANSLATOR_TIMEOUT=10s BACKEND_DIPLOMAIL_TRANSLATOR_MAX_ATTEMPTS=5 ``` When backend lives inside the `tools/local-dev` Docker network and LibreTranslate runs on the host, replace `localhost` with the host's docker-bridge address (`http://host.docker.internal:5000` on Docker Desktop; `http://172.17.0.1:5000` on a Linux bridge by default). For a stack-internal deployment, drop LibreTranslate into the same Docker compose file alongside backend and reach it by its service name: ```yaml services: libretranslate: image: libretranslate/libretranslate:latest environment: LT_LOAD_ONLY: "en,ru" healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:5000/languages"] interval: 5s timeout: 2s retries: 12 backend: environment: BACKEND_DIPLOMAIL_TRANSLATOR_URL: "http://libretranslate:5000" depends_on: libretranslate: condition: service_healthy ``` ## Manual smoke test Once both services are up: 1. Register two accounts via the public auth flow. Set the second account's `preferred_language` to a value that differs from the sender's writing language (e.g. sender writes in English, second account is `ru`). 2. Create a private game with the first account, invite the second, land both as active members. 3. Send a personal message: `POST /api/v1/user/games/{id}/mail/messages` with the body in English. 4. Watch backend logs for the diplomail worker. After ~2 seconds you should see `translator attempt succeeded` (or equivalent INFO line) and the recipient flipped to `available_at`. 5. As the second account, fetch `GET /api/v1/user/games/{id}/mail/messages/{message_id}`. The response should carry both `body` (English original) and `translated_body` (Russian) along with the `translation_lang` and `translator` fields. ## Operational notes - **Resource budget.** With `LT_LOAD_ONLY=en,ru` the container peaks around 800 MB resident; with all languages, ~3 GB. Plan accordingly. - **CPU.** LibreTranslate is CPU-bound. One translation of a 200- word body takes ~200 ms on a modern x86 core; the diplomail worker is single-threaded by design, so steady-state throughput is `1 / avg_latency` per backend instance. - **Outage behaviour.** A LibreTranslate outage stalls delivery of pending pairs by at most ~31 seconds per pair (the worker's exponential backoff schedule), then falls back to the original body. Inbox listings never depend on the translator's availability. - **API key.** Backend does not send an API key. Self-hosted deployments without `LT_API_KEYS` configured accept anonymous POSTs by default, which matches our deployment posture (LibreTranslate sits on the internal docker network, not reachable from outside). - **Models.** Adding a new target language is an operator-side task: install the corresponding Argos model into the LibreTranslate container (`argospm install …`) and either restart the container or send a SIGHUP. The diplomail pipeline notices the new language pair automatically — there is no allow-list inside backend. ## Troubleshooting - **`translator: do request: dial tcp ...: connect: connection refused`.** LibreTranslate is not listening on the configured address. Verify with `curl http://${URL}/languages`. On Docker setups, double- check the bridge address discussion above. - **`translator: libretranslate http 400`** in worker logs but the language pair clearly exists. Make sure the request used the two-letter codes (`en`, not `en-US`). Backend normalises before sending; if you see a region subtag in the log, file an issue against `internal/diplomail` — the normalisation should be unconditional. - **`translator: libretranslate http 503`.** Container is still loading models. Wait for `/languages` to respond `200`. The worker retries with backoff, so steady-state recovers automatically. - **Worker logs only "noop translator returned, delivering fallback".** `BACKEND_DIPLOMAIL_TRANSLATOR_URL` is empty in the backend process. Confirm with `docker compose exec backend env | grep DIPLOMAIL`. ## Future work - Adding an OpenTelemetry counter and histogram for translator outcomes is tracked in the diplomail package README; the metrics will surface in Grafana once LibreTranslate is deployed. - Email-alerting on prolonged outage (e.g. ≥ N consecutive failures in M minutes) is planned through a new `diplomail.translator.unhealthy` notification kind. Not wired yet — current monitoring lives in zap logs.