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

  1. Browser sends the request with Cookie: lang=<code> (default en).
  2. Routes::DocsRouter::detectLang(req) delegates to AppServices::I18n::lang_for(req) which validates the code and falls back to EN.
  3. DocsRouter::buildContext(page_key, lang) calls I18n::inject(ctx, lang) — every t_* key from the dict becomes a mustache token, plus {{lang}}, {{lang_label}}, {{is_<code>}} flags, and {{js_dict}}.
  4. The canonical template at public/pages/<key>.html is compiled and rendered against the context — each placeholder is substituted with the translated string.
  5. The rendered fragment is wrapped in public/_layout.html and 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

  1. Create public/pages/<section>/<slug>.html as a canonical template (use {{{t_page_<section>_<slug>_e001}}}, _e002, … for each text-carrying element).
  2. Add matching keys to public/langs/en.json (and any other language you translate).
  3. Register a PageMeta entry in Routes::DocsRouter::pages() with title, description, TOC and prev/next links:
  4. 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>