Internationalization (i18n)

Table of contents

  1. Introduction
  2. How the language is chosen
  3. Choosing your product’s languages
  4. Marking text for translation
  5. Adding a language to a feature
  6. Adding a language switcher
  7. Translating a value your product sets
  8. Advanced
  9. Reference
  10. 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

How a request picks its language and its translations WHICH LANGUAGE, FIRST MATCH 1 session["locale"] set by the switcher, if supported 2 Accept-Language only with header negotiation on 3 BABEL_DEFAULT_LOCALE also outside any request WHICH TRANSLATION, LATER CATALOG WINS Framework catalog registered first, at startup Each feature's catalog in load order, as it loads _("Events") one merged catalog per locale A feature that words a message differently overrides an earlier catalog.
The first source that names a supported locale decides the language. The catalogs are merged in registration order, so a later one wins a shared message.

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.example or docker/.env.prod.example, which win over [tool.splent.config]. See Environment files.
  • Never edit docker/.env by hand. product:env --merge and product:derive write it again, and the edit never reaches production.
  • A regional locale can be written pt_BR or pt-BR. The framework keeps the pt_BR Babel reads, and html_lang still gives pt-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 .mo files 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 msgid differently, 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


Back to top

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