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 bypybabel extract, contains all translatable strings found in the feature’s Python and Jinja2 files.messages.po– One per locale. Created bypybabel init, then edited by a translator.messages.mo– Compiled binary loaded at runtime. Generated bypybabel 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
.mofiles should be compiled before deployment. Thefeature:translate --compilecommand handles this. babel.cfgis auto-created byfeature:translate --extractif it does not exist. The default configuration extracts from**.pyand**/templates/**.html.