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. At build time, CMake reconstructs the per-language JSONs odin expects from them (locales/po_tools.py po2json, which parses the gettext files with polib — pip install polib) and embeds those into libvalhalla — no JSON exists in the repo at all.
Contributing to existing translations¶
Edit your language's .po file with Poedit (recommended), any other gettext editor, or a plain text editor, then open a PR with just that file.
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.
- 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). - Commit the changed
valhalla.potand*.pofiles together.
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 |
po_tools.py update |
msgmerge the valhalla.pot template into every .po |
po_tools.py po2json [--out DIR] |
Generate the JSONs from the gettext files (fuzzy/empty → English); run by CMake at build time |
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; --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; used by the localedef test target |
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 JSONs generate cleanly.