Internationalization (i18n)

SPLENT uses Flask-Babel for internationalization. Each feature ships its own .po translation files, and the framework auto-registers them at startup.

Table of contents

Overview

Translations in SPLENT are per-feature. Each feature maintains its own translations/ directory with .pot (template), .po (human-edited), and .mo (compiled) files. During application startup, the LocaleManager initializes Flask-Babel and the FeatureLoader registers every feature’s translation directory automatically.

This means features are fully self-contained: a feature like splent_feature_auth can ship Spanish translations without any product-level configuration beyond enabling the locale.


Marking strings for translation

In Python code

Import gettext from flask_babel and wrap translatable strings:

from flask_babel import gettext as _

# Simple string
flash(_("Invalid credentials"), "danger")

# String with variable interpolation
flash(_("Email %(email)s is already in use", email=email), "danger")

The _() function is an alias for gettext(). Use named parameters (%(name)s) for interpolation so translators can reorder them.

In Jinja2 templates

Use the _() function directly in template expressions:

<h1>{{ _("Welcome") }}</h1>
<p>{{ _("Hello, %(name)s!", name=user.name) }}</p>
<button type="submit">{{ _("Log in") }}</button>

Translation directory structure

Each feature stores translations inside its source package:

splent_feature_auth/
└── src/splent_io/splent_feature_auth/
    ├── routes.py
    ├── templates/
    └── translations/
        ├── messages.pot              # Extracted template (source of truth)
        └── es/
            └── LC_MESSAGES/
                ├── messages.po       # Human-edited Spanish translations
                └── messages.mo       # Compiled binary (generated, git-ignored)
  • messages.pot – The extraction template. Generated by pybabel extract, contains all translatable strings found in the feature’s Python and Jinja2 files.
  • messages.po – One per locale. Created by pybabel init, then edited by a translator.
  • messages.mo – Compiled binary loaded at runtime. Generated by pybabel compile.

How the framework registers translations

During application startup, LocaleManager is initialized as part of the manager pipeline in create_app(). After that, FeatureManager loads each feature, and the FeatureLoader calls _register_translations() for every feature it loads:

# In feature_loader.py
def _register_translations(self, module, import_name: str) -> None:
    pkg_dir = os.path.dirname(module.__file__)
    translations_dir = os.path.join(pkg_dir, "translations")
    if os.path.isdir(translations_dir):
        LocaleManager.register_translation_dir(self._app, translations_dir)

LocaleManager.register_translation_dir() appends the directory to Babel’s translation_directories list, so strings from all features are resolved at runtime without any manual registration.


Locale selection

The LocaleManager uses the following priority to determine the active locale for each request:

Priority Source How it is set
1 session["locale"] Set programmatically (e.g. a language switcher)
2 Accept-Language header Sent by the browser, matched against BABEL_SUPPORTED_LOCALES
3 BABEL_DEFAULT_LOCALE Fallback from product configuration

The selector function:

def get_locale():
    locale = session.get("locale")
    if locale:
        return locale

    supported = current_app.config.get("BABEL_SUPPORTED_LOCALES", ["en"])
    return request.accept_languages.best_match(supported)

Setting the user’s locale

To let users choose their language, set the locale key in the Flask session. A typical language switcher route:

from flask import session, redirect, request

@app.route("/set-language/<locale>")
def set_language(locale):
    supported = current_app.config.get("BABEL_SUPPORTED_LOCALES", ["en"])
    if locale in supported:
        session["locale"] = locale
    return redirect(request.referrer or "/")

Once session["locale"] is set, Flask-Babel will use it for all subsequent requests in that session.


Product configuration

Products configure i18n in their config.py:

class Config:
    BABEL_DEFAULT_LOCALE = "en"
    BABEL_SUPPORTED_LOCALES = ["en", "es"]
Setting Default Description
BABEL_DEFAULT_LOCALE "en" Fallback locale when no session or header match is found
BABEL_SUPPORTED_LOCALES ["en"] List of locales the product accepts. Used for Accept-Language negotiation.

Both settings have defaults in LocaleManager, so a product that only uses English needs no configuration at all.


Example: auth feature with Spanish translations

The splent_feature_auth feature marks error messages for translation in routes.py:

from flask_babel import gettext as _

@auth_bp.route("/signup/", methods=["GET", "POST"])
def show_signup_form():
    form = SignupForm()
    if form.validate_on_submit():
        email = form.email.data
        if not authentication_service.is_email_available(email):
            flash(_("Email %(email)s is already in use", email=email), "danger")
            return render_template("auth/signup_form.html", form=form)
        ...

The Spanish translation file (translations/es/LC_MESSAGES/messages.po) provides the translations:

msgid "Email %(email)s is already in use"
msgstr "El email %(email)s ya esta en uso"

msgid "Error creating user"
msgstr "Error al crear el usuario"

msgid "Invalid credentials"
msgstr "Credenciales incorrectas"

When a user’s session locale is es, Flask-Babel returns the Spanish string automatically.


Workflow: adding translations to a feature

The full workflow uses the feature:translate CLI command:

1. Mark strings in code

from flask_babel import gettext as _

flash(_("Password updated successfully"), "success")

2. Extract translatable strings

splent feature:translate auth --extract

This scans the feature’s Python and Jinja2 files and writes translations/messages.pot.

3. Initialize a locale

splent feature:translate auth --init es

Creates translations/es/LC_MESSAGES/messages.po from the .pot template. If the locale already exists, it updates (merges) instead.

4. Edit the .po file

Open translations/es/LC_MESSAGES/messages.po and fill in the msgstr fields.

5. Compile

splent feature:translate auth --compile

Generates messages.mo binary files from all .po files. The compiled .mo files are what Flask-Babel reads at runtime.

6. Enable the locale in the product

Add the locale to BABEL_SUPPORTED_LOCALES in the product’s config.py:

BABEL_SUPPORTED_LOCALES = ["en", "es"]

Notes

  • Translations are resolved per-feature. If two features define the same msgid, each feature’s routes and templates will use their own translation.
  • The .mo files should be compiled before deployment. The feature:translate --compile command handles this.
  • babel.cfg is auto-created by feature:translate --extract if it does not exist. The default configuration extracts from **.py and **/templates/**.html.

Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es