Translating the UI
The application uses Flask-Babel (gettext) for UI translation. Users choose the interface language in Settings → User preferences → UI language. The selected locale is stored in the user account (when signed in) or in the session (when not).
Overview
- Extract user-facing strings from Python and Jinja into a
.potcatalogue. - Add or update a locale (e.g.
fi) and edit the.pofile with translations. - Compile
.poto.moso the app can load them. - Configure which locales appear in the dropdown via
SUPPORTED_LOCALES(e.g.en,fi).
All translatable strings use the message ID as the default text (e.g. _("Home") shows “Home” in English and the translated string in other locales).
1. Extract messages
From the project root (use uv run python -m babel.messages.frontend so the project's Babel with Jinja2 extraction is used):
uv run python -m babel.messages.frontend extract -F babel.cfg -o messages.pot .
This scans app/**/*.py and app/templates/**/*.html and writes messages.pot. New or changed strings appear as msgid entries. The input path must be . (project root) for the paths in babel.cfg to resolve correctly.
2. Create or update a locale
New locale (e.g. Finnish):
pybabel init -i messages.pot -d translations -l fi
This creates translations/fi/LC_MESSAGES/messages.po.
Update existing locales after changing source strings:
uv run pybabel update -i messages.pot -d translations
Then edit each translations/<lang>/LC_MESSAGES/messages.po and fill in msgstr for each msgid.
3. Translate
Edit translations/fi/LC_MESSAGES/messages.po (or your locale). Example:
msgid "Home"
msgstr "Etusivu"
msgid "Settings"
msgstr "Asetukset"
msgid "project_state.planning"
msgstr "Suunnittelu"
Project lifecycle state labels use keys project_state.<code>; see Translating project lifecycle state labels. For standard terms (e.g. PM → HTKK when person-month), see Translation glossary.
4. Compile
uv run pybabel compile -d translations
This builds messages.mo from each messages.po. The app loads these at runtime.
5. Configure supported locales
Set which locales appear in User preferences → UI language:
- Environment:
SUPPORTED_LOCALES=en,fi(comma-separated codes). - Default if unset:
enonly.
Add display names for new codes in app.utils.locale.LOCALE_DISPLAY_NAMES (e.g. "fi": "Suomi") so the dropdown shows a readable name.
Using translatable strings in code
Jinja templates: use the _() function (provided by Flask-Babel):
<a href="{{ url_for('main.index') }}">{{ _('Home') }}</a>
Python: use gettext or the lazy variant:
from flask_babel import gettext
flash(gettext("Saved."), "success")
After adding or changing strings, run extract and update again, then translate and compile.
Directory layout
babel.cfg– extraction config (paths, encoding).messages.pot– generated catalogue (often not committed; regenerate with extract).translations/– one directory per locale:translations/en/LC_MESSAGES/messages.po(andmessages.moafter compile)translations/fi/LC_MESSAGES/messages.po, etc.
The app uses the translations directory by default (Flask-Babel BABEL_TRANSLATION_DIRECTORIES).
CI / automation
Optional steps in your pipeline:
uv run python -m babel.messages.frontend extract -F babel.cfg -o messages.pot .pybabel compile -d translations(fail if any .po has errors)- Optionally check that certain locales have no empty
msgstrfor critical msgids