I18n (Internationalisation)

AppKernel’s translation support is built on Babel. Strings that need to be internationalised are marked with a _() function, which Babel extracts into .pot / .po files for translators.

How it works

AppKernelEngine sets up a LocaleMiddleware at startup. On every request it reads the Accept-Language header, selects the best match from the languages configured in cfg.yml, and swaps in the corresponding Babel Translations catalog for the duration of that request. Any translatable string evaluated during request handling — validation error messages, field labels, service exceptions — is looked up in that catalog automatically.

There is nothing extra to wire up per-route. The middleware handles it.

Marking strings for translation

AppKernel exposes its internal translation function as _translate. Import it and alias it to _ in modules that raise user-facing messages:

from appkernel.model import _translate as _

def validate(self):
    if len(self.customer_id) < 16:
        raise ValidationException(_('Card number must be at least 16 characters.'))

For strings that must be evaluated lazily — class-level attributes, module-level constants, or anything resolved before a request starts — use lazy_gettext instead:

from appkernel.model import lazy_gettext

class Payment(Model):
    HINT = lazy_gettext('Enter your 16-digit card number.')

lazy_gettext wraps the string in a babel.support.LazyProxy so it is only translated when it is actually rendered, at which point the correct locale is already active.

You can add translator notes as inline comments directly above the marked string — Babel will include them in the .pot file alongside the string:

# NOTE: Shown when card validation fails.
raise ValidationException(_('Card number must be at least 16 characters.'))

Configuration

Languages

List the languages your service supports in cfg.yml:

appkernel:
  i18n:
    languages: ['en-US', 'de-DE']

AppKernel automatically expands en-US to also accept bare en, so you don’t need to list both. The first entry is not treated as a special fallback — matching falls through to the next language in the list if no catalog is found.

Translations directory

At startup, AppKernel searches for a translations/ directory in three locations (in order) relative to cfg_dir:

<cfg_dir>/translations/
<cfg_dir>/tests/translations/
<parent of cfg_dir>/translations/

The first existing directory wins. If none is found, translation is silently disabled and all _() calls return the original string unchanged.

babel.cfg

Create a babel.cfg file at the root of your project to tell Babel which files to scan:

[model_messages: **.py]
extract_messages = _

[python: **.py]
encoding = utf-8

The model_messages section uses AppKernel’s custom extractor (registered via the babel.extractors entry point in pyproject.toml). It scans Model subclasses and emits a translatable label for every declared field (e.g. User.name, User.email). These labels are used by the /meta endpoint for frontend UI rendering. The python section covers ordinary _() calls everywhere else.

Note

The custom extractor targets the legacy Parameter() declaration style. Fields declared with the modern Annotated[] syntax have their labels auto-generated by lazy_gettext inside AppKernelMeta and will appear in the catalog only if you run pybabel extract after the app has been imported (so the metaclass has processed the annotations).

Generating translation files

With babel.cfg in place, run the following commands from your project root:

pybabel extract -F babel.cfg -k _ -o messages.pot .
pybabel init -i messages.pot -d ./translations -l en
pybabel init -i messages.pot -d ./translations -l de
pybabel compile -d ./translations

extract writes every marked string into messages.pot. init creates per-language .po files under translations/. Edit those files to provide the actual translations, then compile produces the binary .mo files that Babel loads at runtime.

Updating translation files

When you add new translatable strings, re-extract and merge without losing existing translations:

pybabel extract -F babel.cfg -k _ -o messages.pot .
pybabel update -i messages.pot -d ./translations
pybabel compile -d ./translations

update merges new strings into the existing .po files and marks removed strings as obsolete, so nothing is lost.