6.1 KiB
Internationalization (i18n)
Only the frontend is translated today. It is powered by i18next, react-i18next, and i18next-cli.
The backend has no translation catalog: USE_I18N is False
(src/backend/messages/settings.py), there are no locale/ directories, and
no gettext call anywhere in core/. Every string the backend emits is
English. See Backend strings below.
Development Workflow
Best practices during development
- Always write strings in English using
i18next.
👉 Translations are updated before each release.
- Frontend strings are stored in:
src/frontend/public/locales/{ns}/{locale}.json
Extraction and download of translations
The process is automated by the CI pipeline:
-
Whenever the
mainbranch is updated, the CI will:- extract translations
- upload them to Crowdin
-
Whenever a branch with the prefix
release/is created, the CI will:- download the updated translations
- create a pull request with the changes
Those processes can also be triggered manually.
Running the process locally
You can perform these steps locally using the Makefile.
⚠️ Make sure you have Crowdin environment variables configured in:
deploy/env/crowdin.local
and that you have sufficient permissions on the Crowdin project.
- Extract and upload translations to Crowdin:
make i18n-generate-and-upload
- Download and compile translations:
make i18n-download-and-compile
Updating translations locally (not recommended)
It is possible (but discouraged) to manually edit translations locally:
-
Generate translation files:
make i18n-generate -
Edit missing translations directly in the generated files.
-
Commit your changes.
The JSON catalogs are read as-is at runtime, so there is no compilation step.
⚠️ Warning: these local changes are likely to be overwritten by the next Crowdin update.
Backend strings (not translated yet)
Some user-facing text is produced by the backend rather than the frontend, and
is therefore English only. The main case today is the mailbox export
notification email (core/services/exporter/tasks.py,
_create_notification_message), which carries the download link to the
mailbox the requester picked as recipient when queueing the export.
Why not Django i18n
Reintroducing gettext is not the plan: it means a second translation format
(.po), a compile step, a second Crowdin file type, and USE_I18N = True
across the whole app, for a handful of strings.
Why the backend cannot read public/locales/common
Two blockers:
- Build context. The backend image builds from context
src/backend(compose.yaml) withCOPY . /app/(src/backend/Dockerfile).src/frontend/public/localesis outside that context, so those files are in no backend image. Sharing them means moving the backend build context to the repository root, which touches every backend/worker/flower build stanza,.dockerignore, and the CI publish workflow. - The
commonnamespace is generated.i18next-cli extract(src/frontend/i18next.config.ts) rewritescommon/en-US.jsonfrom a static scan ofsrc/**/*.tsx. A key added by hand with not()call behind it is dropped on the nextmake i18n-generate-front.
Planned approach
Give the backend its own namespace in the same format and the same pipeline:
src/backend/core/locales/{en-US,fr-FR,nl-NL}.json— flat natural-key JSON, identical in shape to the frontend catalogs (the key is the English source string,{{var}}interpolation). It lives inside the backend build context, so it ships in every image with no Dockerfile change.- A third entry in
crowdin/config.yml(source/backend/core/locales/en-US.json, dest/backend.json, translation/backend/core/locales/%locale%.json), somake i18n-uploadandmake i18n-downloadcover it with no new tooling. - A small loader, following
core/ai/thread_summarizer.py(Path(__file__).parent / …, cached):t(key, lang, **vars)doing{{var}}substitution, falling back toen-USthen to the key itself.User.languagealready storesen-us/fr-fr/nl-nl. The export notification lands in a mailbox, which has no language of its own: it would follow the language of the user who queued the export.
One caveat: the _one / _many / _other suffixes in the frontend catalogs
are CLDR plural rules (French uses one/many/other, Dutch one/other).
Reimplementing that selection in Python is the awkward part; backend strings can
avoid it by wording counts as Messages exported: {{count}} rather than
{{count}} messages exported.
Contributing as a translator or proofreader
We use Crowdin to manage translations. It allows translators and proofreaders to contribute in the languages they know best.
👉 For more information, see the Crowdin documentation.
Adding a new language
If the language you need is not yet available:
- Click Request New Language on the project page.
- We will review and may add it.
⚠️ If you request a new language, you are expected to help keep it up to date whenever strings are added or modified --- especially before each release.
If your language already exists in a different variant (e.g. Brazilian Portuguese vs. European Portuguese), consider contributing to the existing one unless you have enough resources to maintain a separate variant.