Translations¶
SES supports multiple UI languages. The translation catalog is database-backed and edited in the admin under System → Translations. It follows an Oracle APEX-style workflow: seed → edit → publish. Strings are stored per language, go through a review step, and only become live when they are published — nothing ships untranslated or unreviewed.
Workflow Overview¶
- Seed — new application strings are imported into the catalog for every enabled language.
- Edit — translators edit each language manually, or use Auto-translate to generate drafts.
- Approve — approved strings (not fuzzy, not needs-review) are the ones that can ship.
- Publish — publishing is live immediately at runtime; no rebuild, commit, or deploy is required.
Languages¶
Enabled languages are defined in config/settings/base.py (LANGUAGES) and in the UNFOLD language switcher in config/settings/ui.py. The built-in example languages are en, nl and de.
1. Seed (Import Strings)¶
From the catalog changelist (<institute>/admin/translations/messagetranslation/), click the Seed translations header button. It imports every msgid from locale/<code>/LC_MESSAGES/django.po into the catalog so each one can be edited, for all enabled languages.
Seed only imports — it never regenerates .po. For strings already present in the committed .po, no makemessages is needed. Only for brand-new strings added to code/templates must makemessages run first (a build/deploy step) to put them into the committed .po; seed then imports the new msgids.
Or use the equivalent management command:
python manage.py sync_translations -l <code> # e.g. sync_translations -l de
When adding a brand-new language, use add_language, which extracts the .po and seeds the catalog in one step:
python manage.py add_language <code> # e.g. add_language de
Seed and publish are read-only with respect to the .po files — they only import/snapshot the database. The committed .po is already backed up by git, so no manual backup is needed before seeding or publishing.
2. Edit & Auto-Translate¶
- To translate a language, filter Translations → Missing and edit each string, or run the Auto-translate selected action on the whole set at once.
- Auto-translation uses the backend configured on the Institute change form (Institute → Translation): enable it and pick a method — Argos Translate (built-in, offline, no key) or LibreTranslate (self-hosted, base URL + optional API key). Both are open-source.
- Auto-translated output is stored as a draft (
needs-review) and is never shipped until a reviewer approves it.
3. Approve¶
Approved strings are the only ones that can ship. Use the Approve selected action (or clear needs-review/fuzzy on the form) to mark strings as reviewed.
4. Publish (Make Live)¶
When your translations are ready, click the Publish translations header button on the catalog changelist:
- A snapshot of every approved string is stored per institute (publish history is shown under System → Translation Publishes).
- The running site picks up the snapshot immediately — the runtime translation layer serves the published strings with no rebuild, no commit, no deploy.
Edits you make after publishing stay staged: they have no effect until you publish again. Drafts and fuzzy strings are never included in a publish.
Optional: Exporting .po for Other Instances¶
Publishing makes strings live on the current instance without any build step. To also ship the same catalog to other instances (e.g. from local to deploy-hetzner), export the .po files and commit them — the target playbook pulls the repo and rebuilds; the Dockerfile compiles .po → .mo.
- Run the Export compiled .po catalog (zip) action.
- Commit and push the
.pofiles (they live inlocale/<lc>/LC_MESSAGES/django.poin thesesrepo). - On the target instance, run the ansible playbook (
deploy-local/deploy-hetzner): it pulls the repo and rebuilds the image, and the Dockerfile compiles.po→.mo.
Management Commands¶
| Command | Purpose |
|---|---|
add_language <code> |
Extract the .po and seed the catalog for a new language |
sync_translations -l <code> |
Import new .po msgids into the catalog (additive; never overwrites existing rows) |
check_translations --fail |
Coverage gate — fails while anything is missing or fuzzy |
export_translations -l <code> |
Write the .po and compile .mo at release |
Review Gate¶
The coverage gate (check_translations --fail) blocks a release while any enabled language still has missing or fuzzy strings. Publishing additionally only ships approved strings — needs-review drafts never appear until they are published.
Sidebar & menu labels¶
Admin sidebar and menu labels (e.g. "SES Exams", "Task Status") are defined in config/settings/ui.py as translatable _() strings. They only become live from the database after the full pipeline, and they are missing from the catalog when they are also missing from the committed .po.
- Extract —
makemessages. Runpython manage.py makemessages -l nl -l de(a build/deploy step) so brand-new labels are added tolocale/<code>/LC_MESSAGES/django.po. - Set the translation + clear
#, fuzzy. Whenmakemessagesmerges a new label it may match an old one and mark it#, fuzzy(e.g. a#| msgid "Backend Status"line carrying an old translation). Fuzzy rows are excluded from publishing, so edit the.poentry: set the realmsgstrand remove the#, fuzzyand#| msgid "..."lines. - Seed — import the updated
.pointo the catalog (admin upload /seed_catalog, orpython manage.py sync_translations). - Publish — only non-fuzzy, non-needs-review rows with a non-empty translation are published (
release_translations); then restart the server so workers reload the latest snapshot.
Troubleshooting: if a sidebar label stays in English, check (a) it is present in locale/<code>/LC_MESSAGES/django.po, (b) it is not #, fuzzy there, (c) it exists in the catalog and is published — then restart the server (workers cache the resolved snapshot).