Internationalisation (i18n)
The docs site (and any consumer page) is served through a dictionary-backed i18n pipeline. Every page is one canonical mustache template with {{t_...}} placeholders; the actual text lives in per-language JSON dictionaries loaded at boot.
Overview
There is exactly one HTML file per documentation page. Language selection happens at render time by injecting a translated context into mustache. Per-language HTML directories were eliminated once every prose element was moved into the JSON dictionaries.
Missing keys fall back to the English value; a completely missing key falls back to the literal key name (so gaps are visible in the UI).
File layout
Two folders under public/ together drive the i18n system:
public/
├── langs/
│ ├── en.json ← default + ultimate fallback
│ ├── bg.json
│ ├── ru.json
│ ├── es.json
│ ├── tr.json
│ └── pt.json
└── pages/ ← one canonical template per page
├── home.html, license.html, privacy.html, reference.html
├── garvan/ (12 pages)
├── getting_started/ (6 pages)
└── guides/ (20 pages)
There are no public/pages/<lang>/ subdirectories. The 42 canonical templates are language-agnostic.
Component map
| Component | File | Responsibility |
|---|---|---|
AppServices::I18n |
app/services/I18n.{h,cpp} |
Lazy-loads dicts, resolves keys with EN fallback, populates the mustache context. |
Routes::DocsRouter |
routes/DocsRouter.cpp |
Page metadata index (PageMeta), route wiring, delegates translation to AppServices::I18n. |
| Crow mustache | framework | Renders {{t_*}} tokens against the injected context. |
tools/extract_docs_i18n.py |
tools/ |
One-shot migration helper: extracts chrome/nav strings from the DocsRouter initializer into JSON. |
tools/extract_pages_i18n.py |
tools/ |
One-shot migration helper: extracts body prose from per-lang HTML into JSON and deploys canonical templates. |
Request lifecycle
- Browser sends the request with
Cookie: lang=<code>(defaulten). Routes::DocsRouter::detectLang(req)delegates toAppServices::I18n::lang_for(req)which validates the code and falls back to EN.DocsRouter::buildContext(page_key, lang)callsI18n::inject(ctx, lang)— everyt_*key from the dict becomes a mustache token, plus{{lang}},{{lang_label}},{{is_<code>}}flags, and{{js_dict}}.- The canonical template at
public/pages/<key>.htmlis compiled and rendered against the context — each placeholder is substituted with the translated string. - The rendered fragment is wrapped in
public/_layout.htmland returned to the client.
Fallback chain
AppServices::I18n::t(lang, key) resolves in this order:
current lang → EN → literal key name
Language switching
The site sets a 1-year lang cookie via a dedicated endpoint:
GET /lang/<code>
→ Set-Cookie: lang=<code>; Path=/; Max-Age=31536000; SameSite=Lax
→ 302 redirect to Referer
AppServices::I18n::langCookieHeader(lang) builds the Set-Cookie header value. Unknown codes are coerced to EN.
Adding a translation key
Add the key to every public/langs/<lang>.json (missing langs fall back to EN):
// public/langs/en.json
{
"t_my_new_key": "Hello world",
...
}
// public/langs/bg.json
{
"t_my_new_key": "Здравей, свят",
...
}
Reference the key from any mustache template — use double braces for HTML-escaped output, triple braces for raw HTML (needed when the value itself contains tags like <code>):
<h1></h1> <!-- HTML-escaped -->
<div></div> <!-- raw HTML -->
No rebuild is required: dicts are read on first request per process boot.
Adding a new documentation page
- Create
public/pages/<section>/<slug>.htmlas a canonical template (use{{{t_page_<section>_<slug>_e001}}},_e002, … for each text-carrying element). - Add matching keys to
public/langs/en.json(and any other language you translate). - Register a
PageMetaentry inRoutes::DocsRouter::pages()with title, description, TOC and prev/next links: - Optionally add a sidebar link in
public/sidebar.html.
// routes/DocsRouter.cpp — pages() entry
{"garvan/mypage", {
"garvan/mypage", "garvan",
"My page", "Моята страница",
"Short description.", "Кратко описание.",
{
{"section-a", "Section A", "Раздел A", false},
},
"garvan/prev", "garvan/next"
}},
JS strings (opt-in)
Pages that need translated strings in client-side JavaScript can embed {{{js_dict}}} — AppServices::I18n::js_dict(lang) emits a <script>window.T={...}</script> blob with every key resolved for the active language, plus T.lang:
<!-- somewhere in your template -->
<script>
console.log(window.T.t_nav_home); // "Home" / "Начало" / ...
console.log(window.T.lang); // "en" / "bg" / ...
</script>