Internationalization (i18n)
Table of contents
- Introduction
- How the language is chosen
- Choosing your product’s languages
- Marking text for translation
- Adding a language to a feature
- Adding a language switcher
- Translating a value your product sets
- Advanced
- Reference
- See also
Introduction
SPLENT translates through Flask-Babel. Each feature ships its own catalogs, the framework picks them up as the feature loads, and your product only says which languages it speaks.
[tool.splent.config]
BABEL_DEFAULT_LOCALE = "es"
BABEL_SUPPORTED_LOCALES = "es,en"
Put that in your product’s pyproject.toml, merge the env files with product:env --merge and restart. Every feature with a Spanish catalog now speaks Spanish.
How the language is chosen
The reader’s choice in the switcher comes first, then your product’s default. The browser’s Accept-Language counts only when you turn on BABEL_NEGOTIATE_FROM_HEADER, as a product with no home country, such as a marketplace, would.
Choosing your product’s languages
Set the languages in [tool.splent.config], as in the introduction. BABEL_SUPPORTED_LOCALES is comma separated, in the switcher’s order, and the default is added first when missing. An English-only product sets nothing.
- A value that differs per environment goes in
docker/.env.dev.exampleordocker/.env.prod.example, which win over[tool.splent.config]. See Environment files. - Never edit
docker/.envby hand.product:env --mergeandproduct:derivewrite it again, and the edit never reaches production. - A regional locale can be written
pt_BRorpt-BR. The framework keeps thept_BRBabel reads, andhtml_langstill givespt-BR. A code Babel does not know is logged as an error at startup.
A class attribute in your product’s config.py works too, but a variable of the same name wins over it.
Marking text for translation
In a template, wrap the text in _().
<h1>{{ _('Notes') }}</h1>
<p>{{ _('%(count)s notes', count=notes | length) }}</p>
In Python, import gettext as _.
from flask_babel import gettext as _
flash(_("Email %(email)s is already in use", email=email), "danger")
Use named placeholders, such as %(email)s, so a translator can move them.
Adding a language to a feature
feature:translate runs the whole cycle. Extract the strings, create the catalog, translate it, compile it.
splent feature:translate notes --extract
splent feature:translate notes --init es
Fill in the msgstr lines of translations/es/LC_MESSAGES/messages.po.
msgid "Notes"
msgstr "Notas"
Then compile, and enable the language in the product.
splent feature:translate notes --compile
splent product:env --merge --dev
splent product:restart
--init on a language that exists brings in the new strings. Without a feature name, the command works on every feature of the product.
Only the compiled
.mofiles are read at runtime, so compile before you release.
The catalogs live inside the feature.
src/splent_io/splent_feature_notes/translations/
├── messages.pot # every string found in the feature
└── es/LC_MESSAGES/
├── messages.po # what a translator edits
└── messages.mo # what the app reads
The framework registers each feature’s translations/ as it loads. Never register one by hand.
Adding a language switcher
A feature draws the switcher, usually your theme. Its route stores the code in session["locale"], where the framework reads it on the next request.
from flask import current_app, redirect, request, session
from splent_framework.urls import safe_next_url
@theme_bp.route("/lang/<code>")
def set_language(code):
if code in current_app.config["BABEL_SUPPORTED_LOCALES"]:
session["locale"] = code
return redirect(safe_next_url(request.args.get("next")) or "/")
safe_next_url keeps only a path on your site, so the route cannot become an open redirect.
Every template gets supported_locales, current_locale and html_lang. Show the switcher only when there is more than one language.
{% if supported_locales | length > 1 %}
{% for lc in supported_locales %}
<a href="{{ url_for('theme.set_language', code=lc, next=request.path) }}"
class="{{ 'is-active' if lc == current_locale }}">{{ lc | upper }}</a>
{% endfor %}
{% endif %}
Translating a value your product sets
A feature’s catalog cannot hold a text your product chooses, such as a tagline or a search placeholder. Write one variant per language instead.
SEARCH_PLACEHOLDER=es:Buscar en esta wiki|en:Search this wiki
localized() picks the variant for the current language. It is a global of every template, and lives in splent_framework.i18n.
<input placeholder="{{ localized(config.SEARCH_PLACEHOLDER, _('Search')) }}">
It tries the exact locale, then the base language, then the first variant written. A value without tags, such as a name, is shown as written, and an empty one gives the default you pass.
Advanced
The framework ships its own catalog for the pages it draws, such as the 403 page and the message of a 413. It is registered before every feature, so a feature that words one of those messages differently wins in its product.
Flask-Babel merges every catalog into one per locale. When two features translate the same
msgiddifferently, the later one wins for both, so keep your strings specific.
Reference
| Setting | Default | What it does |
|---|---|---|
BABEL_DEFAULT_LOCALE |
en |
The language of every request whose session holds no supported locale, and of anything outside a request |
BABEL_SUPPORTED_LOCALES |
The default alone | Comma separated, in the switcher’s order. The default is added first when missing |
BABEL_NEGOTIATE_FROM_HEADER |
Off | 1, true, yes or on lets the browser’s Accept-Language choose before the default |
BABEL_DEFAULT_TIMEZONE |
The product’s TIMEZONE |
The zone the date filters show times in. Set it only to show another zone |
| Template variable | Value |
|---|---|
current_locale |
The language in force, such as pt_BR |
html_lang |
The same, spelled for <html lang>, such as pt-BR |
supported_locales |
The list from BABEL_SUPPORTED_LOCALES |
localized(value, default) |
One product value in the current language |
See also
feature:translate- Configuration, for how the layers of settings stack.