# Internationalization (i18n) URL: /docs/usage/i18n/ Section: usage Tags: usage, i18n, translation -------------------------------------------------------------------------------- Internationalization (i18n) Kida has built-in support for translatable templates. Mark strings with {% trans %}, wire up a gettext backend, and Kida handles escaping, pluralization, and variable interpolation. Quick start {% trans %}Hello, world!{% endtrans %} Without translations installed, strings pass through unchanged. Install a gettext translations object to translate them: import gettext translations = gettext.translation("myapp", "locales", languages=["fr"]) env = Environment(loader=loader) env.install_translations(translations) Now {% trans %}Hello, world!{% endtrans %} renders "Bonjour, monde !" (or whatever the French .po file says). Syntax Simple translation {% trans %}Hello, world!{% endtrans %} Message ID: "Hello, world!" With variables Declare variable bindings in the {% trans %} tag. Reference them as {{ name }} in the body: {% trans name=user.name %}Hello, {{ name }}!{% endtrans %} Message ID: "Hello, %(name)s!" The translated string uses Python %-formatting. Variable values are HTML-escaped automatically (see Escaping below). Pluralization Use {% plural %} to provide singular and plural forms. A count variable is required -- it determines which form ngettext selects: {% trans count=items | length %} One item found. {% plural %} {{ count }} items found. {% endtrans %} Singular ID: "One item found." Plural ID: "%(count)s items found." When count is 1, the singular form renders. Otherwise, the plural form renders with count interpolated. Multiple variables Separate bindings with commas: {% trans name=user.name, count=user.posts | length %} {{ name }} has one post. {% plural %} {{ name }} has {{ count }} posts. {% endtrans %} Closing tags Both {% endtrans %} and {% end %} work: {% trans %}Hello{% endtrans %} {% trans %}Hello{% end %} Shorthand functions For inline translations, use the _ and _n globals: {{ _("Hello") }} {{ _n("%(count)s item", "%(count)s items", count) }} These are regular functions available in any expression context -- output tags, filter arguments, default values, etc.: {% def greeting(msg=_("Hello")) %} <h1>{{ msg }}</h1> {% end %} Whitespace handling Whitespace inside {% trans %} bodies is normalized. Leading and trailing whitespace is stripped, and internal whitespace is collapsed to single spaces: {% trans %} Hello, world! {% endtrans %} Message ID: "Hello, world!" (not "\n Hello,\n world!\n") This keeps message IDs clean regardless of template indentation. Escaping Translation and escaping interact carefully: Scenario Behavior No variables Translated string is escaped via the autoescape function With variables String is wrapped in Markup, variable values are auto-escaped Markup values Not double-escaped -- Markup instances pass through Variable escaping {% trans name=user_input %}Hello, {{ name }}!{% endtrans %} If user_input is "<script>alert(1)</script>", the output is: Hello, &lt;script&gt;alert(1)&lt;/script&gt;! The translated string itself is trusted (translators produce safe content). Variable values are escaped on interpolation via Markup.__mod__. Pre-escaped values If a variable is already Markup, it is not double-escaped: from kida.utils.html import Markup template.render(safe_html=Markup("<b>bold</b>")) {% trans name=safe_html %}Hello, {{ name }}!{% endtrans %} {# Hello, <b>bold</b>! #} Body restrictions Inside {% trans %} bodies, only simple {{ name }} references are allowed. Filters, attribute access, and method calls are not permitted in the body: {# Good -- bind the expression in the tag #} {% trans name=user.name | title %}Hello, {{ name }}!{% endtrans %} {# Bad -- complex expression in body #} {% trans %}Hello, {{ user.name | title }}!{% endtrans %} This constraint ensures message IDs are predictable and extractable. Bind complex expressions in the {% trans %} tag, then reference the bound name in the body. Configuration install_translations Pass a standard library gettext.GNUTranslations object (or any object with gettext() and ngettext() methods): import gettext translations = gettext.translation("myapp", "locales", languages=["fr"]) env.install_translations(translations) install_gettext_callables Pass the translation functions directly: env.install_gettext_callables( gettext=my_gettext_func, ngettext=my_ngettext_func, ) Default behavior When no translations are installed, Kida uses identity functions: _("Hello") returns "Hello" _n("item", "items", 1) returns "item" _n("item", "items", 5) returns "items" Templates render untranslated strings. No configuration needed for single-language projects. Patterns Per-request language switching In web applications, install translations per request: def get_translations(locale: str): return gettext.translation("myapp", "locales", languages=[locale]) @app.before_request def set_locale(): locale = request.accept_languages.best_match(["en", "fr", "de"]) app.jinja_env.install_translations(get_translations(locale)) Translation in components {% trans %} works inside {% def %} components: {% def nav_link(href, active=false) %} <a href="{{ href }}" {% if active %}class="active"{% end %}> {% slot %}{% end %} </a> {% end %} {% call nav_link("/settings") %} {% trans %}Settings{% endtrans %} {% end %} Pluralized counts with formatting {% trans count=notifications | length %} You have one new notification. {% plural %} You have {{ count }} new notifications. {% endtrans %} Static content with no variables For simple strings, {% trans %} requires no bindings: <footer> {% trans %}Copyright 2026. All rights reserved.{% endtrans %} </footer> -------------------------------------------------------------------------------- Metadata: - Word Count: 754 - Reading Time: 4 minutes