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#
{
"welcome": "Welcome!",
"greeting": "Hello, {name}!",
"item_count": {"one": "{count} item", "other": "{count} items"}
}
{
"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#
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:
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:
{{ 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:
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/.mocompatibility, nomakemessages/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'svalidate_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.