Core UI Translations
How to add or update a language for oc8's own UI — the shell, buttons,
toasts, everything that isn't a capa's own content. If you're translating
a capa's fields and labels instead, see that capa's own i18n/ folder
and Developer → Packaging;
this page is specifically about <repo root>/i18n/.
How it works
One gettext .po catalog per language, at <repo root>/i18n/<locale>.po
(e.g. i18n/de.po, i18n/fr.po). msgid is the literal English string as
it appears in the source — content-as-key, no invented translation keys,
the same convention a capa's own i18n/*.po catalogs use
(Core Architecture). There is no i18n/en.po: English
is the source text, so it needs no lookup.
At runtime, backend/src/oc8/i18n/catalog.py reads every .po file under
i18n/ and GET /api/v1/i18n/core serves them as one JSON payload the
frontend fetches once at startup. The i18n/ folder is mounted read-only
into the backend container (docker-compose.yml) — dropping in a new
or edited .po file only needs a backend restart, never a frontend
rebuild. A locale missing a given msgid (or missing the whole file)
falls back to the English source string, exactly like a capa's catalogs
do.
German is a partial exception, for now: every t("English", "German")
call site in the frontend still carries its German text as a literal
second argument, so German keeps working even before i18n/de.po is read
at all. A catalog entry for German, when present, overrides that literal —
so i18n/de.po is genuinely editable-without-a-code-change too, it just
isn't the only thing keeping German alive. Every other language has no
such literal and is entirely sourced from its .po file.
Adding a new language
-
From
frontend/, run:npm run i18n:export -- <locale><locale>is a short code likefr,es, orpt-BR— it becomes the filename. This scans everyt("English", …)call in the frontend and writesi18n/<locale>.powith one emptymsgstrper string. -
Open
i18n/<locale>.po. Fill in the header:"X-Native-Name: Français\n""X-Flag: 🇫🇷\n"X-Native-Nameis what the language switcher shows;X-Flagis the emoji next to it. -
Translate. Leave a
msgstrempty for anything you don't want to translate yet — it just falls back to the Englishmsgid, it never errors. -
Restart the backend (
docker compose up -d backendafter a freshdocker compose build backendif the image itself needs rebuilding, or just restarting it against an already-mounted checkout in dev) and confirm the new language appears in the language switcher and reads correctly. -
Open a pull request against
i18n/<locale>.po(see Opening a pull request). No other file needs to change — that's the whole point of the catalog being data, not code.
Updating an existing language
Run the same command again:
npm run i18n:export -- de
It merges rather than overwrites: translations already in the file are
kept, new English strings the UI has picked up since you last ran it
appear with an empty msgstr, and strings no longer used anywhere are
dropped. Fill in the new blanks and open a PR.
A rough edge worth knowing about
Content-as-key means the exact same English string always resolves to
the exact same translation, wherever it appears — gettext and Odoo work
the same way. Two UI call sites that happen to share an English string
("Runs", say) but want different translations in your language can't
both be satisfied by one msgid. When the export script finds the core
team's own German catalog disagreeing with itself this way for a given
string, it leaves that msgstr empty rather than guessing, and that
call site quietly keeps using its own inline literal instead. If you hit
the same ambiguity in your language, either translation choice is fine —
there's no clean fix short of giving oc8 a msgctxt scheme, which hasn't
been needed yet.