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.