title: "Internationalisation (i18n)" description: "Locale message catalogs, the {{ t }} template helper, and Accept-Language wiring."


Internationalisation (i18n)

AkurAI-Framework ships two cooperating pieces:

The two pieces are decoupled: the template engine knows nothing about files or locales — it just does a cheap map lookup on reserved context keys. The framework bridges them by negotiating Accept-Language and injecting the right catalog into the context before rendering.


Catalog format

One JSON file per locale, placed in backend/locales/ next to backend/page.json:

backend/
  locales/
    en.json
    is.json

Each file is a flat JSON object mapping message keys to translated strings:

{
  "footer.tagline": "A single-binary, pure-Rust web framework.",
  "nav.docs": "Docs",
  "hero.cta": "Get started"
}

Rules:


Wiring: how it works

When akurai serve / akurai dev starts up:

  1. If backend/locales/ exists, all *.json files are loaded via akurai_i18n::Catalogs::load_dir.
  2. On every page request, the Accept-Language header is parsed and the best available locale is selected (negotiate_locale).
  3. The active locale's messages are built into a Value::Object and injected into the render context as __locale (the locale code) and __messages (the full key → string map).
  4. Template {{ t "key" }} expressions resolve against __messages — a cheap map lookup.

No backend/locales/ directory → catalogs are None, the __locale / __messages keys are absent, and all {{ t "…" }} expressions fall back to the key text. Existing templates are unaffected.


Accept-Language negotiation

The negotiation function (negotiate_locale) implements RFC 7231 quality factors:

Accept-Language: is-IS, en;q=0.8
→ chooses "is" (primary subtag match, quality 1.0)

Accept-Language: en;q=0.5, is;q=0.9
→ chooses "is" (higher quality weight)

Accept-Language: fr
→ chooses "en" (default, fr not available)

(empty)
→ chooses "en" (default)

Template helper: {{ t "key" }}

Inside any AkurAI template, use the t helper to emit a translated string:

<p>{{ t "footer.tagline" }}</p>
<a href="/docs">{{ t "nav.docs" }}</a>
<a href="/start">{{ t "hero.cta" }}</a>

With placeholder substitution — supply named arguments as name=context.path:

<p>{{ t "greeting" name=user.name count=inbox.unread }}</p>

Each name=path argument resolves path against the current template context, then replaces {name} in the translated string.

Behavior summary:

| Situation | Output | |-----------|--------| | Key found in __messages | Translated string (HTML-escaped) | | Key missing from __messages | Key text itself (HTML-escaped) | | __messages not in context | Key text itself | | Placeholder arg resolves to nothing | {placeholder} left verbatim | | {{{ t "key" }}} triple-brace form | Translated string, not HTML-escaped |

The helper never panics. Missing translations degrade gracefully to the key — visible, distinguishable, fixable.


Demo element

The site footer (site/frontend/footer.html) uses:

<p class="muted">{{ t "footer.tagline" }}</p>

With backend/locales/ present, switching Accept-Language visibly changes the footer tagline:

# English (default)
curl -H 'Accept-Language: en' http://localhost:8090/
# → footer shows: A single-binary, pure-Rust web framework.

# Icelandic
curl -H 'Accept-Language: is' http://localhost:8090/
# → footer shows: Eitt-tvíandarforrit, hrein Rust vefumgjörð.

Without backend/locales/, {{ t "footer.tagline" }} renders as the literal key footer.tagline — backward-compatible and visibly flagged.


Adding a locale

  1. Create backend/locales/<code>.json with your translations:
{
  "footer.tagline": "Ein einzelnes Binärprogramm, reines Rust-Framework.",
  "nav.docs": "Dokumentation",
  "hero.cta": "Loslegen"
}
  1. Restart akurai serve — catalogs are loaded at startup.
  2. Browsers or API clients sending Accept-Language: de (or de-DE) will now receive German translations.

Keys absent from a locale catalog fall back to the default locale (en), then to the key itself — no missing-translation crashes.


Context contract (__locale + __messages)

The template engine reads two reserved keys from the root context:

| Key | Type | Content | |-----|------|---------| | __locale | Str | Active locale code, e.g. "is" | | __messages | Object | Flat map of key → translated string for the active locale |

Both keys are optional — omitting them is safe. The CLI injects them automatically when backend/locales/ is present.


SSG note

Static-generation (prerender / ISR) renders use the default locale — there is no live Accept-Language header available at warm time or during background revalidation. Dynamic page routes (no prerender/revalidate) negotiate per-request as normal.