.. _Translations: 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. .. _Babel: https://babel.pocoo.org/ 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``:: /translations/ /tests/translations/ /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.