Extensions

Babel

Sending mail to people usually means speaking their language. So the natural next battery is internationalization. Our Babel extension wraps Babel and gives you familiar gettext in your templates, locale aware formatting filters and a database-backed translation store you can update at runtime.

Enabling Babel

Babel leans on the database for its runtime translations, so it requires the SQLAlchemy extension to be enabled. Flip both switches and you are set:

app_configs/app.ini ini
[general]
SECRET_KEY = supersecret

[data]
DATABASE_URI = sqlite:///app.db

[extensions]
EXT_SQLALCHEMY = 1
EXT_BABEL = 1

When Babel expands your fluid it installs the i18n extension into your jinja_env, registers a set of formatting filters and wires up a small WebSocket so the same translations can be reused on the client. It also brings its own model, I18nMessage, which is where runtime translations are persisted.

 

Because I18nMessage is a regular db model, it becomes part of your schema the moment Babel is on. So this is the perfect time to run the migration flow from the previous chapter again (wf migrate revision app -a and wf migrate upgrade app) to create the translation table.

Translating in templates

The gettext callables are installed new-style, so the usual underscore helper is available everywhere in your templates. A source string is its own key — if no translation is found, the source is returned untouched:

fluid/templates/index.html html
{% extends "fluid_base.html" %}

{% block title %}{{ _('Home') }}{% endblock %}

{% block content %}
    <h1>{{ _('Hello %(name)s!', name=name) }}</h1>
    <p>{{ ngettext('%(num)d model', '%(num)d models', count) }}</p>
{% endblock %}

Alongside gettext you get ngettext (plurals), pgettext (contextual) and npgettext. For strings that are built outside of a request — module level constants, for example — reach for the lazy variants like babel.lazy_gettext, which only resolve once they are actually rendered.

Providing translations

The fastest way to ship translations is the runtime store. You hand Babel a mapping of locale → key → plural form → message and it writes them into the I18nMessage table and the in-memory cache during startup. Note that updates are only accepted before the server is up:

fluid/i18n.py python
from webfluid.core.ext import babel


translations = {
    "en": {
        "Home": { ("one",): "Home" },
        "Hello %(name)s!": { ("one",): "Hello %(name)s!" }
    },
    "de": {
        "Home": { ("one",): "Startseite" },
        "Hello %(name)s!": { ("one",): "Hallo %(name)s!" }
    }
}

# The default domain is called "messages". Push the catalog into
# the runtime store. This must run before fluid.mix().
babel.update_translations("messages", translations)

The tuple key is the plural form (("one",), ("other",), ...) and an optional context as its second element. Import this module from your config or factory so it runs at the right time. If you prefer classic .po catalogs instead, extract them once with the CLI — more on that below.

 

The runtime translation structure is admittedly a bit cursed in this alpha, and the db-backed cache is not optimized for large catalogs yet. For small to medium projects it is perfectly usable, but keep an eye on the overview page for the running list of rough edges here.

Selecting the locale

Out of the box Babel resolves the active locale from the lang cookie, then the Accept-Language header (best match against your supported locales) and finally the default. The timezone works the same way through the tz cookie or the X-Timezone header. You declare your supported set in the config:

fluid/config.py python
from webfluid.core.config import register_config


@register_config(10)
class Config:
    SESSION_COOKIE_SECURE = True

    BABEL_DEFAULT_LOCALE = "en"
    BABEL_DEFAULT_TIMEZONE = "UTC"
    BABEL_SUPPORTED_LOCALES = ["en", "de"]

Need your own logic? Register a selector. Whatever it returns wins over the defaults, and you can temporarily override both with the Babel.force / Babel.aforce context managers:

example python
from webfluid.core.ext import babel


@babel.locale_selector
def select_locale():
    from webfluid.core.context import FluidContext
    user = FluidContext.current().request.session.get("user")
    return user["locale"] if user else babel.default_locale


# Render something in a fixed locale, no matter the request:
async with babel.aforce(locale="de"):
    subject = babel.gettext("Welcome aboard!")

Formatting filters

Numbers, dates and currencies should follow the user's locale too. Babel registers a family of filters for exactly that, all locale (and where it matters, timezone) aware:

fluid/templates/index.html html
<p>{{ model.created_at|datetimeformat }}</p>
<p>{{ model.created_at|dateformat('full') }}</p>
<p>{{ 1234.5|decimalformat }}</p>
<p>{{ 19.99|currencyformat('EUR') }}</p>
<p>{{ 0.75|percentformat }}</p>

The full set is datetimeformat, dateformat, timeformat, timedeltaformat, numberformat, decimalformat, currencyformat, percentformat and scientificformat.

On the frontend

Because the extension also exposes a translation WebSocket, the same catalog is reachable from the browser. WebFluid injects a small i18n.js client that mirrors the server API (_, _n, _p, _np) and caches the active locale in localStorage. We'll come back to client side wiring in the Frontend chapters — for now it is enough to know it is there.

Fallback catalogs

If you want gettext-style .po files as a static fallback below the runtime store, the Babel CLI extracts and compiles them for you, scanning both your project and the framework templates:

terminal bash
wf babel extract

This produces a messages.pot, initializes or updates the catalogs under translations/ and compiles them. The same step runs automatically if you ever scaffold a project with wf create project --babel-fallback.

Continue reading

From here you can continue straight with Events.