i18n contribution
Goal
Add or fix a translation in the GENIE.AI web UI, mobile app, or Keycloak login pages without breaking the locale whitelist or the no-duplicate-keys invariant.
Three translation surfaces
| Surface | Where | File pattern |
|---|---|---|
| Web (Vue 3 SPA) | components/gov-chat-frontend/src/i18n/locales/<lang>.js | one file per locale |
| Mobile (Flutter) | mobile/genie_ai_mobile/lib/src/localization/app_en.arb (source) + Flutter-generated app_localizations_<lang>.dart | ARB (English only) + generated Dart |
| Keycloak email theme | configs/keycloak/themes/genie/email/messages/messages_<lang>.properties | Java properties |
nameEN field of every translatable
resource (service categories, services) is what the RAG pipeline retrieves
against. Do not translate nameEN.Locale availability (whitelist)
| Surface | Knob | Default |
|---|---|---|
| Web | VUE_APP_AVAILABLE_LOCALES (comma-separated codes, e.g. en,es) | All locales shipped |
| Mobile | KeycloakConfig.supportedLocaleCodes per flavor | All locales shipped |
| Keycloak | KEYCLOAK_SUPPORTED_LOCALES (JSON array, e.g. ["en","es"]) | Curated default |
Setting a whitelist hides locales from the picker but does not delete the source files.
See Restrict active locales for the canonical deployer reference.
Translation-key parity rules
When you add a new key, every active locale must include the same key to avoid partial-translation fallbacks at runtime:
- Add the key to
locales/en.js(English) with the canonical wording. - Add the same key to every other active locale file. Use the source language’s structure as the source of truth.
- Do not add keys only to a subset of locales — missing keys fall back to English and silently break locale coverage.
No-duplicate-keys invariant
Translation files are merged at runtime via
require.context('./locales', ...) followed by localeContext.keys().forEach(...)
in components/gov-chat-frontend/src/i18n/index.js. Duplicate keys within a
locale file cause silent overrides and corrupt the UI. Review each locale
file manually for duplicates before committing — git grep -n "<key>" across
locales/<lang>.js to spot accidental repeats.
Service / category translations
Service categories live in the serviceCategories ArangoDB collection.
Category translations go in the serviceCategoryTranslations collection
with key pattern <categoryKey>_<LANGCODE> where LANGCODE is the source
locale uppercased (e.g. catKey_ES, catKey_FR). Service translations
go in the separate serviceTranslations collection with key pattern
<serviceKey>_<LANGCODE> (e.g. serviceKey_ES). Each translation document
has serviceCategoryId or serviceId, languageCode (UPPERCASE), and
translation (the localized text). The RAG pipeline only ever reads
nameEN.
Recent key changes
- 2026-09-22 —
weatherAuthRequiredadded (UX copy for the weather prompt that surfaces when the connected weather provider is missing an API key).weatherErrorremoved (dead key, no callers). Contributors verifying key parity should confirm the current key list against the locale files, not the docs — the docs only describe recent additions and removals, not the full enumerated set.
Workflow
git checkout -b i18n/<handle>/<locale>-<key-or-area>
# edit components/gov-chat-frontend/src/i18n/locales/<locale>.js
# manually verify no duplicate keys (git grep "<key>" locales/<locale>.js)
npm run lint # Vue / JS lint
git add components/gov-chat-frontend/src/i18n/locales/<locale>.js
git commit -m "i18n(<locale>): add <key> — <one-line why>"
git push -u origin i18n/<handle>/<locale>-<key-or-area>
glab mr create --target-branch main
Best practices for translators
- Match the tone of the source locale — formal for government contexts, clear and direct for user-facing copy.
- Don’t translate proper nouns (Keycloak, OPEA, GENIE.AI, ArangoDB).
- Don’t translate placeholder values (
{count},{name}). - Keep the line length under 100 chars where possible (Vue template wrapping).
- Test in the UI — switch the locale picker and walk through the affected screen.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| UI shows a fallback English string | Missing key in another locale | Add the key to every active locale file |
| Key silently overridden | Duplicate key in the same locale file | Search the file for the key, rename one |
| Mobile picker doesn’t show your locale | Whitelist excludes it | Add the code to KeycloakConfig.supportedLocaleCodes |