Docs
search Esc

Internationalisation (i18n)

Localise your game into Bemba, Nyanja, Tonga, and Lozi with the built-in UbuntuI18n layer. Translations are generated on-device by the offline AI stack, cached locally, and always fall back to English.

How it works

UbuntuPlay's i18n model is English-first with pack-first translation:

  1. Every user-facing string is registered in an English dictionary (defaultStrings) — the source of truth.
  2. When a learner switches language, UbuntuI18n loads the language's locale pack first (GET /api/locales/<lang>, static /locales/<lang>.json fallback) — compile-time translations plus teacher overrides that render instantly, offline, with zero AI. Both the classroom server and the Hub serve these routes, so games translate identically on school servers, marketplace demos, and parent-portal launches.
  3. Only strings the pack does not cover ride the runtime pipeline via UbuntuAI.batchTranslate() — the Gogo AI Bridge (LiteRT-LM + Gemma, in the teacher's browser) on classroom servers with an NLLB-200 fallback where installed, or the browser-local Gogo engine on the Hub.
  4. Real results are cached in localStorage — junk-screened first, so echoes, repetition artifacts and error strings never persist — and each string is translated once per device.
  5. If anything fails — no pack coverage, no AI bridge, unsupported device — the UI stays in English and a floating notice says so honestly. Localisation is never allowed to break gameplay.

Supported languages

CodeLanguageDisplay name to pass
enEnglish (source of truth)'English'
bemBemba'Bemba'
nyaNyanja'Nyanja'
tonTonga'Tonga'
lozLozi'Lozi'
info
Why a display name?setLanguage(lang, langName) takes both the code and a human-readable name. The name is what the AI translator is prompted with (e.g. "Translate into Bemba"), so pass the English display name shown in the table.

Quick start

Include the AI helper and the i18n layer (both are served by the classroom server and the Hub):

HTML
<script src="/js/ai-helper.js"></script>
<script src="/js/i18n.js"></script>

Mark up your UI with data-i18n keys (the element's English text doubles as the design-time placeholder):

HTML
<h1 data-i18n="mygame.title">Star Counter</h1>
<button data-i18n="mygame.start">Start</button>
<span data-i18n="common.score">Score</span>

Register your strings, then switch language and refresh:

JAVASCRIPT
// Register your game's English strings (once, at startup)
Object.assign(UbuntuI18n.defaultStrings, {
  'mygame.title': 'Star Counter',
  'mygame.start': 'Start',
  'mygame.win':   'You counted {count} stars!'
});

// Switch language (async — translates any missing strings via the AI bridge)
const ok = await UbuntuI18n.setLanguage('bem', 'Bemba');
if (ok) UbuntuI18n.refreshUI();   // re-translates every [data-i18n] element

API reference

The layer is exposed as the global window.UbuntuI18n:

MemberDescription
UbuntuI18n.t(key, params?)Returns the translated string for the current language. Falls back to the English defaultStrings entry, then to the raw key. Interpolates {name} placeholders from params.
UbuntuI18n.setLanguage(lang, langName)Async. Sets the active language for this page load (never persisted — every page deliberately boots in English), dispatches up_lang_change, loads the locale pack, then batch-translates only pack-missing strings via window.UbuntuAI.batchTranslate(). Resolves true when the language is genuinely usable (at least half of all strings render translated), false otherwise (UI should stay in English).
UbuntuI18n.getLanguage()Returns the current language code (e.g. 'bem').
UbuntuI18n.refreshUI()Sets textContent = t(key) on every element with a data-i18n attribute.
UbuntuI18n.defaultStringsThe live English dictionary. Extend it with Object.assign() to register your game's keys.
warning
refreshUI() replaces text content onlyIt writes textContent, so any child HTML inside a data-i18n element is wiped on refresh. Put the attribute on a leaf element (a <span> around the text, not the button containing icons). It also does not translate attributes like placeholder or title — set those in JS with t().

String interpolation

Use {name} placeholders in your defaults and pass values at lookup time. Placeholders are substituted after translation, so dynamic values are never sent to the translator:

JAVASCRIPT
// 'nzelu.puzzle_count': 'Puzzle {current} of {total}'
UbuntuI18n.t('nzelu.puzzle_count', { current: 3, total: 10 });
// → "Puzzle 3 of 10"  (or the Bemba equivalent with numbers injected)

Naming your keys

Keys are flat, dot-namespaced strings: <namespace>.<section>.<name>. Each official game owns a namespace (assegai.*, chenjela.*, madzi.*, mankwala.*, nzelu.*, rangi.*), and shared UI strings live under common.*:

NamespaceUse forExamples
common.*Platform-wide strings — reuse these before adding your owncommon.score, common.level, common.try_again, common.translating
<your-game-id>.*Everything specific to your gamemygame.title, mygame.hud.timer, mygame.result.win

Keep keys stable once shipped — cached translations are stored against the English text, but your markup and code reference the key.

Reacting to language changes

setLanguage() dispatches a up_lang_change CustomEvent on window. Use it to re-render strings you build dynamically in JavaScript (canvas labels, toasts, generated lists):

JAVASCRIPT
window.addEventListener('up_lang_change', (e) => {
  console.log('Language is now', e.detail.lang);
  redrawHud();          // re-run anything that calls UbuntuI18n.t(...)
});

A complete language switcher

This is the exact pattern used by the official games (from Madzi):

HTML
<select id="lang-select">
  <option value="en">English</option>
  <option value="bem">Bemba</option>
  <option value="nya">Nyanja</option>
  <option value="ton">Tonga</option>
  <option value="loz">Lozi</option>
</select>
JAVASCRIPT
document.getElementById('lang-select').addEventListener('change', async (e) => {
  const lang = e.target.value;
  const langName = e.target.options[e.target.selectedIndex].text;

  if (!window.UbuntuI18n) return;                       // degrade gracefully

  toast(UbuntuI18n.t('common.translating'));            // "Translating..."

  const ok = await UbuntuI18n.setLanguage(lang, langName);
  if (ok) {
    UbuntuI18n.refreshUI();
    toast(UbuntuI18n.t('common.translation_complete'));
  } else {
    toast(UbuntuI18n.t('common.translation_failed'));   // UI stays in English
  }
});

Where translations come from

setLanguage() delegates to UbuntuAI.batchTranslate(texts, langName) from /js/ai-helper.js, which posts to the batch endpoint:

ContextEndpoint
Classroom server (games, student/teacher pages)POST /api/ai/translate-batch
Hub portals (/hub/, /org/, /dev/)POST /hub/api/ai/translate-batch

Request body: { "texts": ["Start", "Score"], "targetLang": "Bemba" } → response: { "ok": true, "translations": ["…", "…"] }. On the classroom server the endpoint routes through the Gogo AI Bridge and falls back to the NLLB-200 translation service when available; results are additionally cached server-side in data/translation_cache.json. If the batch call fails, the helper retries strings individually before giving up.

Translation Exchange (import / export)

Strings registered per this guide are automatically part of the platform's Translation Exchange standard (uplay-translations v1) — no extra work per game. Teachers and tools can download everything that still needs translation as CSV or JSON, hand it to a human translator, and upload the finished pairs back: every row is auto-mapped to its game and exact string instance by its catalog key (<gameId>.<instance>).

ActionClassroom endpoint (teacher auth)
Download strings (CSV or JSON)GET /api/teacher/translations/:lang/export?format=csv|json&scope=untranslated|machine|fixes|all&game=<id>
Upload translated pairsPOST /api/teacher/translations/:lang/import (standard JSON, { "csv": "…" }, or a legacy fixes file; ?dryRun=1 validates without saving)

Uploaded pairs are saved as teacher overrides — they beat machine text immediately for every learner and join the Hub community corpus on the next sync. Rows whose English source has drifted, whose {placeholder} set changed, or whose key no longer exists are skipped per-row with reasons. The full file format spec is served at /locales/EXCHANGE-FORMAT.md on every classroom server.

Storage & caching

StoreKey / locationContents
Language choicenot storedLanguage is a per-page-load choice — every page boots in English and the picker starts on English. Legacy up_lang values from older versions are cleared at boot.
Locale packsGET /api/locales/<lang> · /locales/<lang>.jsonCompile-time translations + teacher overrides (+ published community fixes on the Hub), fetched fresh per page load and validated lazily against the live defaultStrings.
Translation cache (client)localStorage['up_translation_cache']{ lang: { key: translatedText } } — written as batches complete. If the quota is exceeded the write is skipped and English is used.
Translation cache (server)classroom/data/translation_cache.jsonServer-side batch results, shared by all devices in the classroom.

Best practices

  • Author in English, always. defaultStrings is the fallback of last resort — every key must have an English default.
  • Reuse common.* keys for Score/Level/Retry-style strings; they are already translated and cached on most devices.
  • Never hardcode UI strings in JS. Route every user-visible string through t() so language switches apply everywhere.
  • Keep strings short and self-contained. Small on-device models translate short UI strings far more reliably than long paragraphs. Do not split sentences across keys.
  • Use interpolation for anything dynamic — numbers, names, and emoji survive translation only when they travel as {placeholders}.
  • Test the failure path. Load your game with the AI bridge offline and confirm it is fully playable in English (setLanguage resolving false must not break anything).
  • Check window.UbuntuI18n exists before calling it — your game should also run standalone outside the platform.
lightbulb
Marking localisation supportGames that follow this guide can declare local-language support in their marketplace manifest — see Building Games for manifest fields and Best Practices for classroom UX guidance.