Dynamic Translations

Translations live in the translations database table - the source of truth (see Translations). Translating by hand through the /system/translations admin UI is the most reliable path: you see exactly what each language looks like. For projects with many languages or frequent content updates, the AI translation pass fills the gaps automatically, leaving you to review the result rather than type every string.

The automated path is bun reeman sync-translations --translate - it scans the database for keys present in one locale but missing (or still untranslated) in another, translates them via an LLM, and writes the results back to the database. It is non-destructive: already-translated values are left untouched.

Synchronising Translation Keys

When a key exists for one locale but not another - you added an English string, or just added a new locale - sync-translations closes the gap. It scans the translations table across every namespace, finds keys missing from a locale, AI-translates them, and writes them straight to the DB:

bun reeman sync-translations --translate

Because the database is authoritative, there are no JSON files to edit and no placeholder markers to find and replace - the missing rows are simply created with translated values. Run it whenever you've added strings or a locale and want the other locales filled in.

To find or scaffold keys that templates reference but the DB doesn't have yet, use the Sync missing translations reeman tool, and to remove DB keys no longer referenced by any template, use Prune unused translations - both emit reviewable .sql files. See Translations → Maintenance.

AI Translation Pass

reeman Add locale flow entering locale code de-de and confirming AI translation via OpenRouter for all keys

sync-translations --translate is the standalone translation pass. The same --translate flag is also accepted by the resource generators, so a freshly generated module can have its labels translated in the same step:

bun reeman crud users --translate

The pass works across all locales the project supports (from config/supported_locales.ts). It reads the English rows from the database, sends them to the LLM, and writes the returned translations back into the translations table for each locale. Only keys that are missing or still hold the English placeholder are sent - already-translated rows are left untouched.

Setup

Set exactly one provider key in your .env. The shipped .env.example configures OpenRouter:

OPENROUTER_KEY=...
OPENROUTER_MODEL=your-openrouter-model

You can switch to another backend entirely - see Choosing a Provider below.

Choosing a Provider

generator/ai-provider.ts supports seven providers. Configure exactly one of them; setting zero or more than one provider key is an error. The list below is the validation order, not a fallback chain:

  1. Ollama - if OLLAMA_URL is set, a local model is used (set OLLAMA_MODEL too).
  2. Gemini - if GEMINI_API_KEY is set. Set GEMINI_MODEL when this provider is selected.
  3. OpenAI - if OPENAI_API_KEY is set. Set OPENAI_MODEL when this provider is selected.
  4. Claude - if CLAUDE_API_KEY is set. Set CLAUDE_MODEL when this provider is selected.
  5. xAI Grok - if XAI_API_KEY is set. Set XAI_MODEL when this provider is selected.
  6. Hugging Face Inference - if HF_TOKEN is set. Override with HF_URL / HF_MODEL; the language pair (e.g. en-sl) is derived automatically.
  7. OpenRouter - if OPENROUTER_KEY is set. The shipped .env.example sets OPENROUTER_MODEL; set it to pick a different model.
# Configure one provider only:
# OLLAMA_URL=http://localhost:11434
# OLLAMA_MODEL=gemma4:e4b

# Gemini
# GEMINI_API_KEY=...
# GEMINI_MODEL=your-gemini-model

# OpenAI
# OPENAI_API_KEY=...
# OPENAI_MODEL=gpt-4o-mini

# Claude
# CLAUDE_API_KEY=...
# CLAUDE_MODEL=your-claude-model

# xAI Grok
# XAI_API_KEY=...
# XAI_MODEL=your-grok-model

# Hugging Face
# HF_TOKEN=...
# HF_MODEL=Helsinki-NLP/opus-mt

# OpenRouter
# OPENROUTER_KEY=...
# OPENROUTER_MODEL=deepseek/deepseek-chat

All providers feed the same translation pipeline, so switching backends is purely an environment change - replace the current provider settings rather than adding another provider key.

The adapters are not interchangeable endpoints: Ollama, OpenAI, xAI, and OpenRouter use chat-completions-style payloads, but each adapter targets its own service URL and credentials. Claude uses Anthropic's native Messages API, Gemini uses Google's native API, and Hugging Face uses its inference API. Selecting an OpenAI-compatible model therefore still requires the matching provider adapter and key.

What Gets Translated

The AI translates every key in the namespaces it processes, including:

  • UI labels, headings, and button text
  • Validation error messages
  • Field labels and placeholders
  • Action names and selectors

route_name values are included in the AI prompt and translated like other keys. Because they become URL segments, review the generated slugs before deploying; the route map slugifies them and does not inherit a missing route_name from another language.

Typical Workflow

The flow for adding a batch of new strings across all locales:

# 1. Add the new keys to the DB (e.g. via the Sync missing translations reeman,
#    which scans templates for refs and emits INSERT SQL), then apply the SQL.

# 2. Auto-translate everything still untranslated across all locales.
bun reeman sync-translations --translate

sync-translations --translate sends only the keys that are missing or still hold their English placeholder - anything already translated is untouched. After the LLM returns, review the result in the /system/translations admin UI.

Manual Review After Translation

AI-translated strings are usually correct for the common cases - button labels, field names, short messages. For domain-specific terms, brand names, or context-dependent phrasing, a human review pass is essential.

The /system/translations admin UI is the place to do it: browse a namespace, compare the languages side by side, and correct anything the model got wrong. Because edits write directly to the translations table, corrections take effect on the next request (the admin module reloads translations after each save) - no redeploy.

For terms the AI consistently gets wrong, the simplest fix is to translate those keys by hand once and leave them - sync-translations won't overwrite a key that already has a non-placeholder value.

When Not to Automate

AI translation is appropriate for:

  • Bulk-translating a newly added locale
  • Filling in 50+ strings after adding a feature
  • Keeping translations in sync during early development

AI translation is not appropriate for:

  • Marketing copy, landing pages, or any text where tone and voice matter
  • Legal disclaimers, terms of service, or anything with legal implications
  • route_name values, unless you have reviewed the generated URL slugs
  • Any string that, if translated wrong, would confuse or mislead users

For those cases, translate by hand through the admin UI - the AI pass skips keys that already have a translated value.