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 .pot catalogue.
  • Add or update a locale (e.g. fi) and edit the .po file with translations.
  • Compile .po to .mo so 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: en only.

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 (and messages.mo after 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:

  1. uv run python -m babel.messages.frontend extract -F babel.cfg -o messages.pot .
  2. pybabel compile -d translations (fail if any .po has errors)
  3. Optionally check that certain locales have no empty msgstr for critical msgids