Skip to content

i18n#

Translations for Hayavega -- plain JSON catalogs, not gettext.

Why this works differently from Django#

Django does i18n with GNU gettext -- .po/.mo files, and makemessages/compilemessages tooling that needs the external msgfmt compiler (a system tool, not a Python package). Hayavega's hayavega.i18n uses plain JSON catalogs instead: a project hand-edits locale/en.json/locale/es.json directly -- no extraction step, no compiler, no new hayavega command needed at all. This is a narrower tool than gettext (see "What this doesn't do" below), traded deliberately for staying dependency-free.

A translation catalog#

locale/en.json
{
  "welcome": "Welcome!",
  "greeting": "Hello, {name}!",
  "item_count": {"one": "{count} item", "other": "{count} items"}
}
locale/es.json
{
  "welcome": "¡Bienvenido!",
  "greeting": "¡Hola, {name}!",
  "item_count": {"one": "{count} artículo", "other": "{count} artículos"}
}

A value is either a plain string (str.format(**kwargs)-interpolated) or a two-key {"one": "...", "other": "..."} dict, selected by a count keyword argument -- binary singular/plural only, not full CLDR plural-category rules (a language needing 3+ plural forms -- Russian, Arabic, Polish -- isn't fully supported by this).

Configuring from settings#

config/settings.py
class Settings:
    class i18n:
        languages = ["en", "es"]
        default_language = "en"
        locale_dir = BASE_DIR / "locale"
from hayavega.conf import load_settings
from hayavega.i18n import catalog_from_settings

settings = load_settings()
catalog = catalog_from_settings(settings.i18n)

locale_dir defaults to "locale" (relative to the process's working directory, used as-is -- pass an absolute path, e.g. BASE_DIR / "locale", for anything beyond local experimentation), the same way mail.FileEmailBackend's directory is. default_language must be one of languages, or Catalog raises ValueError at construction time -- a misconfiguration caught immediately, not on first use.

Translating in a view#

from hayavega.i18n import translator_for


def home(request):
    t = translator_for(request, catalog)
    return Response(render_template("home.html", {"t": t}))
message = t("greeting", name="Alice")  # "Hello, Alice!"
count_message = t("item_count", count=3)  # "3 items"

translator_for resolves the request's language from its Accept-Language header (RFC 4647-style q= negotiation: highest quality first, an exact language match preferred, falling back to a primary-subtag match -- en-GB matches a supported en -- then to default_language if nothing matches) and returns a LanguageCatalog bound to it.

An explicit language override#

resolve_language/translator_for take an override -- a ?lang=-style query parameter, a cookie, a session value, whatever fits your app. hayavega.i18n doesn't persist the choice itself; reuse hayavega.security's cookie helpers or your own session mechanism for that, the same way Messages reuses them instead of inventing a second persistence layer:

t = translator_for(request, catalog, override=request.query_params.get("lang"))

Translating in a template#

templates.py's expression grammar supports only dotted-path lookups (no function calls, no filters -- see Templates), so LanguageCatalog exposes translations through plain attribute access instead of a {% trans %}-style tag:

<h1>{{ t.welcome }}</h1>

{{ t.welcome }} calls no interpolation -- it's the same dotted-path lookup any other context value gets. For an interpolated message, call t(...) from the view and pass the already-rendered string into the template context instead:

return Response(render_template("home.html", {"greeting": t("greeting", name=user.name)}))

A missing key returns the key itself, unchanged, from both t.key and t("key") -- never raises, the same "degrade gracefully, never crash a render" choice render_template makes for an undefined variable.

What this doesn't do#

  • No gettext/.po/.mo compatibility, no makemessages/ compilemessages-equivalent command -- catalogs are hand-edited JSON files, not extracted or compiled.
  • Built-in error messages aren't translated automatically -- forms.py's field errors, auth.py's validate_password, and similar built-in strings stay in English. Translating your own application's messages (as shown above) works; threading a translator through the framework's own internals does not, and isn't planned.
  • Binary plural forms only (one/other), not full CLDR plural categories.