A small desktop tool that fixes the "raw localization keys instead of translated text" problem you see when you run a Paradox grand-strategy game in a non-English language with mods that ship English-only localization.
It scans every mod you've subscribed to on the Steam Workshop, finds the strings the mod author left untranslated for your language, and writes a sidecar .yml file alongside the mod with either:
- the English text copied into the target-language slot — instant fix for the "shows the key name" issue, no translation needed, or
- a real machine translation — done locally via Ollama, or remotely via any OpenAI-compatible API (OpenAI, OpenRouter, Groq, Together, a self-hosted vLLM/llama.cpp server, …).
Translations are cached per game so you can stop and resume safely, and re-running picks up only newly-added strings.
- Subscribe to your mods on the Steam Workshop and run the game once so it downloads them.
- Download the latest binary for your OS from the Releases page.
- Run it. It auto-detects which Paradox games you have installed.
- Pick a game and a target language. Click Start Processing.
- Launch the game in your target language — the missing translations are now filled in.
That's it. No translation backend is required if you only want to get rid of the "raw key names" display issue — leave Enable Translation unchecked and the tool simply copies English text into the target-language slot.
If you do want real translations, see Translation backends below.
Europa Universalis 5 · Crusader Kings 3 · Victoria 3 · Hearts of Iron 4 · Stellaris · Imperator: Rome
Russian, German, French, Spanish, Simplified Chinese, Japanese, Korean, Polish, Portuguese (Brazilian), Italian, Dutch, Turkish, Arabic.
Translation is optional. When enabled, you pick one of two backends.
Install Ollama and make sure it's running. The first translation run downloads qwen2.5:1.5b (~1 GB). It's small and fast but the quality is mediocre on rarer languages — fine for casual play, not for publishable translations.
You can override the server URL and model in the GUI, or via the OLLAMA_HOST environment variable. Pointing the URL at a remote machine running Ollama lets you offload work to a beefier PC on your LAN.
Pick "Cloud LLM (OpenAI-compatible)" and fill in:
- API URL —
https://api.openai.com/v1for OpenAI, or any compatible endpoint (OpenRouter, Groq, Together, DeepInfra, Mistral, a self-hosted vLLM, etc.). - Model — e.g.
gpt-4o-mini, or whatever your provider exposes. - API key — stored in plain text in the local app preferences file. Don't share that file.
Bring-your-own-key avoids bundling any third-party credentials in the binary.
A third backend, "lloc (Ollama Cloud)", appears in the list when all of these are found at startup:
- Python 3.11+ on
PATH; - a checkout of lloc in a
llocfolder next to this tool (or one or two levels up), or whereverLLOC_DIRpoints; - a local Ollama daemon signed in to Ollama Cloud (
ollama signin).
Strings are translated by one cloud model (lloc's second translator and judge are switched off: on mod texts they changed about 6% of the strings, mostly in style, for three times the requests). Game script syntax is validated instead of masked: a string that loses a placeholder, a §Y…§! colour code or comes back in the wrong alphabet is sent back with the error, and if it still fails it stays in English and is retried on the next run. Terminology comes from the game's own official localization, which is read once from the game's install folder. Mod entries left in English by the author are always re-translated. Anything lloc leaves out is filled with the English text, as with the other backends.
Its state lives in <cache dir>/lloc/<game>/ and is incremental: an interrupted run continues where it stopped, and "Clear translation cache" removes it. If lloc stops on a fatal error (a dropped connection, a crash), the run is restarted automatically up to three times; a mod lloc cannot read is skipped without losing its earlier translations.
Each mod is described to the translator by the game, game-specific rules (how names are handled, whose voice event options use), the mod's name without version labels and its subject tags. Mods that depend on another installed mod are grouped with it, so a sub-mod reuses the names and terms of the mod it extends. Mods with long texts get smaller requests, so that a reply does not time out.
- Walks each mod folder under
…/Steam/steamapps/workshop/content/<game-id>/. - Parses every
*_l_english.yml. Identifies keys that the mod author hasn't already translated to your target language. - For each untranslated key, either copies the English text or sends it to the chosen LLM with a short system prompt that explains the Paradox placeholder rules.
- Writes the result into the mod's
localization/<lang>/folder as two files:0_<modid>_consolidated_l_<lang>.yml(sorts first in ASCII)zz_<modid>_consolidated_l_<lang>.yml(sorts last) This is a deliberate hack: different Paradox games use different load-order rules for localization files (FIOS = first-in-order-sorted, others use reverse-alpha), and writing both prefixes guarantees our file wins regardless. The two files are hardlinked on platforms that allow it, so storage cost is negligible.
- Caches translations per
(game, source-lang, target-lang)triple so re-runs and other mods that share strings get cache hits.
Rate limits, server errors and timeouts are retried with growing waits. If a mod still cannot be finished, its untranslated strings are written in English and translated on the next run: an entry still identical to the English text is never taken for a finished translation.
Closing the window stops the work cleanly: requests in flight are cancelled, lloc is interrupted, and what is done is written before the window closes. Files are written to a temporary name and renamed, so an interruption never leaves a half-written file.
Paradox localization values can contain non-translatable syntax: $VARIABLE$, [GetModifier(...)], @icon!, #bold text#!, \n. Before sending text to the LLM the tool replaces these with neutral {0} {1} {2} tokens, validates that every token survives in the response, and restores them afterwards. If a token went missing in translation, the original English string is kept for that key — wrong, but safe.
Translation cache is stored in:
- Linux:
~/.cache/paradox-localize/translation_cache_<gameid>_en_<lang>.json - macOS:
~/Library/Caches/paradox-localize/translation_cache_*.json - Windows:
%LOCALAPPDATA%\paradox-localize\translation_cache_*.json
The cache is JSON-Lines (one entry per line) so it can be appended to incrementally without rewriting the whole file. Safe to delete; the tool will rebuild it.
The tool only writes to …/Steam/steamapps/workshop/content/<game-id>/<modid>/localization/<lang>/0_<modid>_consolidated_l_<lang>.yml (and the zz_… twin). It never touches the mod author's own files, and "Delete old consolidated files before processing" cleans up only files matching that pattern.
Re-subscribing to a mod or having Steam re-validate workshop content will overwrite our files; just re-run the tool.
- Go 1.24+
- CGO enabled (the GUI uses Fyne, which needs a C compiler and OpenGL).
- For cross-compilation:
mingw-w64for Windows, osxcross for macOS.
All builds drop into bin/. Run make help for the full list. The common ones:
make build-linux # bin/paradox-localize-linux-amd64
make build-windows # cross-compile → bin/paradox-localize-windows-amd64.exe
make build-mac-intel # cross-compile → bin/paradox-localize-macos-amd64
make build-mac-arm # cross-compile → bin/paradox-localize-macos-arm64
make build-all # all four platforms
make release # clean + build-all (use this for tagged releases)
make test # unit tests
make check # gofmt-check + vet + test
make run # build + launch GUI
make clean # rm -rf bin/If your osxcross or LLVM live somewhere other than the defaults, override on the command line:
make build-mac-arm OSXCROSS_BIN=/opt/osxcross/target/bin LLVM_BIN=/usr/lib/llvm-18/binosxcross requires clang in PATH. If you only have clang-20, the Makefile's default LLVM_BIN=/usr/lib/llvm-20/bin already covers that.
Bug reports and PRs are welcome. The tricky bits live in internal/translation/placeholder.go (regex-based syntax protection) and internal/localization/writer.go (the 0_/zz_ load-order trick). If you change either, the existing tests should still pass; please add new ones for any new edge case you handle.
MIT — see LICENSE.