Menu
Public repository / 2db2dd7dccca0704d27eae8a579adde62a95b820
AkurAI-Framework / site/content/docs/i18n.md
Latest change 9876ca1b84b75a78bec25332729ec70f36d4df3d - feat(i18n): add akurai-i18n crate and {{ t }} template helper by Ólafur Búi Ólafsson
---
title: "Internationalisation (i18n)"
description: "Locale message catalogs, the {{ t }} template helper, and Accept-Language wiring."
---
# Internationalisation (i18n)
AkurAI-Framework ships two cooperating pieces:
- **`akurai-i18n`** — loads per-locale JSON message catalogs from a directory, resolves translations with a fallback chain, and interpolates `{placeholder}` values.
- **`{{ t "key" }}`** — a template helper that reads the active locale's messages from the render context and emits the translated string.
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 (CLI / router layer) 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 a single directory (conventionally `locales/`):
```
locales/
en.json
is.json
fr.json
```
Each file is a flat JSON object mapping message keys to translated strings:
```json
{
"app.title": "My App",
"nav.home": "Home",
"greeting": "Hello, {name}! You have {count} notifications."
}
```
**Rules:**
- Keys and values must be JSON strings (no nesting, no arrays, no numbers).
- Placeholder syntax is `{identifier}` — curly braces around a plain word.
- The file stem is the locale code (`en.json` → locale `"en"`).
---
## Loading catalogs (`akurai-i18n`)
```rust
use akurai_i18n::Catalogs;
use std::path::Path;
let catalogs = Catalogs::load_dir(Path::new("locales"))?;
```
`load_dir` scans every `*.json` file in the directory; non-JSON files are silently ignored.
**Default locale selection:**
1. `"en"` if an `en.json` file is present.
2. Otherwise the lexicographically smallest locale found.
### Translation lookup
```rust
// Simple lookup — returns &str
let text = catalogs.translate("is", "nav.home"); // "Heim" or falls back
// With placeholder interpolation — returns String
let text = catalogs.translate_args(
"is",
"greeting",
&[("name", "Óli"), ("count", "3")],
);
```
**Fallback chain** (same for both methods):
1. Requested locale — key found → done.
2. Default locale — key found → done.
3. The key itself — never panics, never returns empty for a non-empty key.
---
## Template helper: `{{ t "key" }}`
Inside any AkurAI template, use the `t` helper to emit a translated string:
```html
<title>{{ t "app.title" }}</title>
<nav><a href="/">{{ t "nav.home" }}</a></nav>
```
With placeholder substitution — supply named arguments as `name=context.path`:
```html
<p>{{ t "greeting" name=user.name count=inbox.unread }}</p>
```
Each `name=path` argument resolves `path` against the current template context (dotted paths, loop variables, all the usual rules), 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.
---
## 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 |
Example context (as `akurai_json::Value`):
```json
{
"__locale": "is",
"__messages": {
"app.title": "Mitt forrit",
"nav.home": "Heim",
"greeting": "Halló, {name}!"
},
"user": { "name": "Óli" }
}
```
Both keys are optional — omitting them is safe (all `{{ t "…" }}` calls fall back to the key text).
---
## Framework wiring: `Accept-Language` negotiation
The CLI / router layer is responsible for:
1. Parsing the `Accept-Language` request header (quality-factor sorted).
2. Picking the best matching locale from the loaded `Catalogs`.
3. Calling `catalogs.translate_args(locale, key, &[])` for each key (or building a `Value::Object` from the whole catalog map) to produce `__messages`.
4. Inserting `__locale` and `__messages` into the render context before calling `engine.render(name, &ctx)`.
This wiring lives in the framework's HTTP handler layer and is **not** the template crate's concern.
### Minimal wiring sketch
```rust
// In a route handler:
let locale = negotiate_locale(&req, catalogs.default_locale());
let messages = build_messages_object(&catalogs, locale);
let ctx = Value::Object(vec![
("__locale".into(), Value::Str(locale.into())),
("__messages".into(), messages),
// … your page data …
]);
engine.render("page", &ctx)?
```
---
## Backward compatibility
Templates that do not use `{{ t "…" }}` are completely unaffected — they render byte-for-byte identically to before i18n was added. The `__locale` and `__messages` keys are ignored unless explicitly read by a `t` expression.