Narrative language files¶
Valhalla supports localized instructions in multiple languages for both textual and verbal phrases. Translations are managed as gettext .po files in the locales directory — one per language, e.g. de-DE.po. We rely on external contributors to provide translations of these phrases.
The gettext files are the only committed translation artifacts: valhalla.pot is the hand-maintained English source (msgids plus #. e.g. ... example-phrase comments; its header carries the en-US metadata), and each language has a .po with the translations. The per-language JSONs odin expects are generated from them by locales/po_tools.py po2json and committed alongside, so that building libvalhalla needs no tooling beyond CMake, which embeds them into the library. Regenerate and commit them whenever you change a .pot or .po file; CI fails if they disagree. Generating them needs nothing but python3. Only the two maintainer commands that write .po files, init and lint --fix, additionally need polib (pip install polib).
Contributing to existing translations¶
Edit your language's .po file with Poedit (recommended), any other gettext editor, or a plain text editor. Then regenerate the JSON odin reads and open a PR with both files:
python3 locales/po_tools.py po2json
What to know while translating:
- Each entry shows the English source (
msgid), your translation (msgstr), the JSON path as context (msgctxt, e.g.instructions.bear.phrases.1) and English example phrases as comments. - Phrase tags like
<STREET_NAMES>are replaced with real values at runtime. Reorder them as your grammar requires, but keep them spelled exactly as in the English source — a misspelled tag ends up verbatim in user-facing instructions. CI checks this (po_tools.py lint). - Entries flagged fuzzy (Poedit: "Needs work") are ignored at runtime — the English source is used instead. They mark translations that need review, typically because the English phrase changed since they were translated. Filter for them to see what your language needs.
- Untranslated entries likewise fall back to English.
Contributing a new language¶
- Determine the language tag per IETF BCP 47, typically
<ISO 639 two-letter language code>-<ISO 3166 two-letter country code>, e.g.cs-CZ. - Create
locales/<tag>.pofrom the template:python3 locales/po_tools.py init cs-CZ. It fills the header for you:X-Valhalla-Posix-Locale(defaultcs_CZ.UTF-8, override with--posix-locale) andX-Valhalla-Aliases(default the bare language codecs, unique across languages; override with--aliases, comma-separated or empty). - Translate, then regenerate the JSONs:
python3 locales/po_tools.py po2json. - Add a phrase for the new language to the
lang_phrasevector intest/gurka/test_route_with_narrative_languages.cc(easiest: add a bogus phrase, run the test, copy the expected one from the failure output). - Submit a pull request. Thank you!
Maintainer workflow¶
Changing or adding English phrases¶
CLI¶
- Edit
locales/valhalla.potdirectly. Path segments that are numbers become JSON arrays in the generated files, except underphrases, which odin reads by numeric string key. Non-phrasessub-keys (e.g.relative_directions,empty_street_name_labelsetc) carry areplacementmarker, e.g.instructions.bear.replacement.relative_directions.0, so alphabetical sort keeps them right after theirinstructions.bear.phrases.*block.po_tools.pytakes care of the substitution and linting automatically. - Propagate to all languages (requires gettext):
python3 locales/po_tools.py updatemsgmergekeeps every existing translation. Entries whose English changed keep the old translation but are flagged fuzzy (with the previous English kept as a#|comment), so each language's translators see exactly what needs review; new phrases appear untranslated. Both fall back to English until translated.msgmergereorders each.poto the.pot's entry order, so the files stay sorted as long as the.potis (runpo_tools.py lint --fixif you added entries out of order). - Regenerate the JSONs (
python3 locales/po_tools.py po2json) and commit them together with the changedvalhalla.potand*.pofiles.
poedit¶
The same can be achieved with poedit in its GUI.
Tooling reference¶
All state lives in the .pot/.po files — no external service involved.
| Command | Purpose |
|---|---|
po_tools.py init <lang> |
Start a new language: create <lang>.po from the template with the header filled in; needs polib |
po_tools.py update |
msgmerge the valhalla.pot template into every .po |
po_tools.py po2json [--out DIR] |
Generate the JSONs odin reads from the gettext files (fuzzy/empty → English); run it after any .pot/.po change and commit the result, python3 is all it needs |
po_tools.py lint [--fix] [--strict] |
Check placeholder tokens (errors on tokens Odin would never substitute), that non-phrases sub-keys carry the replacement sort marker, and that .pot/.po are sorted; --fix inserts missing markers and sorts in place instead of erroring (and needs polib for that); --strict also fails on warnings. |
po_tools.py stats [langs] |
Per-language coverage as JSON (object per language: translated/fuzzy/untranslated/total/percent); "translated" = non-fuzzy msgstr that differs from English (carry-overs and fuzzy don't count). Understates English variants (en-GB/en-AU) |
po_tools.py print-posix-locales |
Print every language's POSIX locale, e.g. to feed localedef by hand |
msgattrib --untranslated --fuzzy <lang>.po |
List what needs work in a language |
msgfmt --check --statistics <lang>.po |
Validate syntax, show translation coverage |
CI enforces: valid .pot/.po syntax, placeholder correctness, and that the committed JSONs match what the gettext files generate.