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.pofile 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 countvariable
is required -- it determines which formngettextselects:
{% trans count=items | length %}
One item found.
{% plural %}
{{ count }} items found.
{% endtrans %}
Singular ID: "One item found."
Plural ID:"%(count)s items found."
Whencountis 1, the singular form renders. Otherwise, the plural form
renders withcountinterpolated.
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 _nglobals:
{{ _("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 inMarkup, variable values are auto-escaped |
Markupvalues |
Not double-escaped --Markupinstances pass through |
Variable escaping
{% trans name=user_input %}Hello, {{ name }}!{% endtrans %}
If user_input is "<script>alert(1)</script>", the output is:
Hello, <script>alert(1)</script>!
The translated string itself is trusted (translators produce safe content).
Variable values are escaped on interpolation viaMarkup.__mod__.
Pre-escaped values
If a variable is alreadyMarkup, 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 librarygettext.GNUTranslationsobject (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>