Internationalization¶
Status: ๐ง Partial โ Backend translation API, database models, and both frontends are wired for i18next with all 6 languages; โ
MultilingualAIServiceis now called fromfirst_login_service.py(lazily, with sentinel guard) and fromaria_personal_intelligence_service.py. โ Player-client shipsnpm run extract-strings/npm run validate-translations; โ admin-ui shipsnpm run i18n:extract/npm run i18n:validateon tipaee475ed6(services/admin-ui/scripts/extract-strings.jsandvalidate-translations.js). โ Reserved namespaces (ai,marketing,errors,validation) are loaded on tip (aee475ed6) by both frontends โ player-clientns: ['common','game','auth','ai','marketing','errors','validation']; admin-uins: ['common','admin','auth','ai','marketing','errors','validation']. Remaining gaps: DB not seeded with all 510 keys in non-English locales on a fresh deploy; German partial. (Re-verified 2026-08-31 vs origin/feataee475ed6โ tip delta PC Soft-ORDER #1251/#1252 TypeError densify only; i18n backend/frontend wiring unchanged.)
Six-language i18next setup spanning the player client, admin UI, AI dialogue, and translation-management endpoints (English / Spanish / French / Portuguese / Chinese launch-complete; German Partial). Translations ship as bundled JSON namespaces and are also exposed via a database-backed admin API for live editing.
1. Languages¶
| Language | Code | Keys | Status |
|---|---|---|---|
| English | en |
510 | Base (100%) |
| Spanish | es |
510 | Complete (100%) |
| French | fr |
510 | Complete (100%) |
| Chinese Simplified | zh |
510 | Complete (100%) |
| Portuguese | pt |
510 | Complete (100%, Brazilian conventions) |
| German | de |
~135 | Partial โ declared in services/player-client/src/i18n.ts |
Four content namespaces (common, auth, admin, game) hold the 510-key English inventory (table in ยง2). Both frontends on tip 9dc7ed12 also load reserved namespaces ai, marketing, errors, validation (7 ns entries in each i18n.ts). Those reserved namespaces are loaded at runtime and are not included in the 510-key content total โ do not invent reserved-ns key counts, and do not read this page as โthe product has only four namespaces.โ
Translations were authored via AI linguistic generation and adapted for gaming terminology (e.g. Comandante / Commandant / ๆๆฅๅฎ / Comandante; Nave Exploradora / Vaisseau รclaireur / ไพฆๅฏ่น / Nave Explorador).
2. Delivery model¶
Translations live in the gameserver database (translation_keys table) and are served to clients over HTTP. The frontends use i18next-http-backend to fetch namespaces lazily from /api/v1/i18n/{lang}/{namespace}; results are cached in localStorage with a versioned cache-bust key.
Namespace breakdown:
| Namespace | Keys (English base) |
|---|---|
common |
121 |
auth |
69 |
admin |
154 |
game |
166 |
Reserved namespaces (ai, marketing, errors, validation) are in the i18n.ts ns arrays on both frontends on tip 9dc7ed12 but have no key counts in this table (not part of the 510-key content total).
Per-language frontend wiring lives at:
- services/admin-ui/src/i18n.ts + components/common/LanguageSwitcher.tsx
- services/player-client/src/i18n.ts + components/common/LanguageSwitcher.tsx
Admin LanguageSwitcher honesty (tip da4ec0e5): tip LanguageSwitcher.tsx no longer hardcodes completionPercentage: code === 'en' ? 100 : 0 for non-English launch locales. Launch-complete codes (en/es/fr/zh/pt via LAUNCH_COMPLETE_CODES) get static 100% (staticCompletion), then the switcher overlays GET /api/v1/i18n/admin/progress/{code} when the response yields a finite percent (fallback comment: launch-complete locales stay 100%, never a 0% bar). Do not read this page as โAdmin shows 0% for Complete localesโ โ that gap is closed on tip. โ
Progress HTTP honesty tip-PRESENT via #778: 403/401 and 429 surface progressError alerts (LanguageSwitcher.tsx:75-88,117-120). German remains Partial in the language table; DB seed gaps on a fresh deploy can still make live progress differ from the static 100% fallback. โ
changeLanguage honesty tip-PRESENT via #1186 MERGED (mergeCommit 0987726c3; Admin LEG-3236): i18n.changeLanguage catch sets changeLanguageError TypeError densify + generic recoverable copy with role="alert" (LanguageSwitcher.tsx:102-133 on tip da4ec0e5) โ not console-only. See admin-ui.md ยง i18n.
PC Soft-HOLD #1210/#1209 CLOSED tip-PRESENT (tip 63a2cabb1): player-client LanguageSwitcher API-down / static fallback uses runtime code zh (not zh-CN) and STATIC_COMPLETION_PERCENT (en/es/fr/zh/pt=100, de=26) in buildStaticLanguages() (LanguageSwitcher.tsx:27-46). scripts/validate-translations.js KNOWN_NAMESPACES lists all 7 namespaces (common,game,auth,ai,marketing,errors,validation) matching i18n.ts ns. German remains Partial; DB-seed gaps on a fresh deploy can still diverge from static percents. Ops ledger: player-client.md ยง i18n.
Both apps use i18next 23.16, react-i18next 13.5, i18next-http-backend, and the browser language detector.
Language detection¶
Detection order is ['localStorage', 'navigator', 'htmlTag'] โ the player's persisted choice wins, then the browser's navigator.language, then a <html lang="โฆ"> declaration as last resort. The chosen code is cached in localStorage under the key i18nextLng.
English-fallback payload¶
When the backend /i18n/{lang}/{namespace} fetch fails (network error, server unreachable, or backend not yet seeded), the player client falls back to a hardcoded 14-key English minimum-viable vocabulary (welcome, dashboard, login, logout, loading, error, ship, sector, credits, cargo, trade, combat, planets, galaxy) so every screen still renders something legible. This is the only player-visible string fallback โ once any namespace successfully loads from the backend, the cache replaces the hardcoded payload.
3. Backend¶
Service & models¶
services/gameserver/src/models/translation.pyโlanguages,translation_keys,user_language_preferencestables.services/gameserver/src/services/translation_service.pyโ fallback handling, caching, user-preference persistence.services/gameserver/src/services/multilingual_ai_service.pyโ multilingual context for AI responses.
API (/api/v1/i18n/)¶
Public
| Method | Path | Purpose |
|--------|------|---------|
| GET | /i18n/languages | List supported languages |
| GET | /i18n/detect | Auto-detect from request |
| GET | /i18n/{lang} | All translations for a language |
| GET | /i18n/{lang}/{namespace} | Namespace-scoped translations |
User
| Method | Path | Purpose |
|--------|------|---------|
| GET | /i18n/user/preference | Get preferred language |
| POST | /i18n/user/preference | Set preferred language |
| GET | /i18n/user/ai-context | Get AI language context |
Admin
| Method | Path | Purpose |
|--------|------|---------|
| GET | /i18n/admin/progress/{lang} | Translation completion |
| POST | /i18n/admin/translation/{lang}/{ns} | Update single translation |
| POST | /i18n/admin/bulk/{lang}/{ns} | Bulk import |
| POST | /i18n/admin/initialize | Initialize system |
11 endpoints total (4 public, 3 user, 4 admin).
Database¶
languages(id, code, name, native_name, direction, is_active, completion_percentage)
translation_keys(id, key, language_id, namespace_id, value, context, is_verified)
user_language_preferences(id, user_id, language_id, manual_override)
4. Adding new translatable content¶
1. Pick a namespace¶
common (shared UI), auth, admin, game, plus tip-loaded reserved namespaces ai, marketing, errors, validation (see Status).
2. Add the key¶
Add it via the admin API or import (English base, admin namespace):
{
"newFeature": {
"title": "New Feature",
"description": "Description of the new feature",
"actions": { "enable": "Enable", "disable": "Disable" }
}
}
3. Use it in components¶
import { useTranslation } from 'react-i18next';
const { t } = useTranslation('admin');
return <h2>{t('newFeature.title')}</h2>;
Patterns:
- Interpolation: t('welcome', { name: 'John' })
- Pluralization: t('items', { count: 5 })
- Fallback: t('key', { defaultValue: 'Default' })
4. Push to the database (optional)¶
curl -X POST /api/v1/i18n/admin/bulk/en/admin \
-H "Authorization: Bearer $TOKEN" \
-d '{"translations": {...}, "overwrite": true}'
The database path supports admin-managed dynamic translations; the file path is the day-to-day default for static text bundles.
5. Translator workflow¶
Admin UI¶
- Log into admin with translator permissions.
- Settings โ Translation Management.
- Pick language + namespace.
- Edit; check "Verified" once reviewed.
Direct API¶
curl -X POST /api/v1/i18n/admin/translation/es/admin \
-H "Authorization: Bearer $TOKEN" \
-d '{
"key": "dashboard.title",
"value": "Panel de Control Central",
"context": "Main dashboard page title"
}'
Quality bar¶
- Translate meaning, not words.
- Use established gaming terminology.
- Watch character-length constraints on UI elements.
- Preserve tone / formality per language conventions.
- Test in actual interface.
Per-language conventions¶
| Language | Notes |
|---|---|
| Spanish | Formal usted for system messages; balance formal/informal in UI; consider regional variants |
| French | Maintain language purity; correct accent marks; formal address; consider French-Canadian forks if needed |
| Chinese | Simplified character set; appropriate formality; gaming-context terminology |
| Portuguese | Brazilian conventions by default; gaming idioms |
6. Community contributions¶
- Create account, request translator permissions.
- Complete style-guide training.
- Start with simple/common translations.
- Peer review in language teams.
- Native-speaker validation before promoting to verified.
Recognition: translation credits, language-team leaderboards, contributor badges.
7. Progress tracking¶
curl -X GET /api/v1/i18n/admin/progress/es \
-H "Authorization: Bearer $TOKEN"
Returns completion %, verified vs unverified counts, per-namespace breakdown, last-updated timestamps.
8. Tooling¶
โ
Shipped (player-client) โ services/player-client/package.json on tip 9dc7ed12:
npm run extract-stringsโ scanssrc/**/*.{ts,tsx}fort('โฆ')call sites and emits a sorted JSON array of unique keys to stdout. Use as a draft checklist when adding new content.npm run validate-translationsโ checks that everyuseTranslation()call declares a namespace in the script's hardcoded known set (common/game/authfor player-client;common/admin/authfor admin-ui), and validates the structural integrity of any static seed JSON files found undergameserver/i18n/en/. Exits non-zero on hard errors (parse failures, unknown namespaces). On tip63a2cabb1, player-clientKNOWN_NAMESPACESalready includes all seven runtime namespaces (common,game,auth,ai,marketing,errors,validation) โ Soft-HOLD#1209tip-PRESENT/closed. Admin-ui validate script may still use a narrower set (common/admin/auth); do not conflate the two apps.
Scripts live at services/player-client/scripts/extract-strings.js and services/player-client/scripts/validate-translations.js.
โ
Shipped (admin-ui) โ services/admin-ui/package.json on the same tip (script names differ from player-client; do not alias):
npm run i18n:extractโnode ./scripts/extract-strings.jsnpm run i18n:validateโnode ./scripts/validate-translations.js(KNOWN_NAMESPACES=common/admin/authonly; runtimei18n.tsstill loads the four reserved namespaces โ same script-vs-runtime gap as player-client)
Scripts live at services/admin-ui/scripts/extract-strings.js and services/admin-ui/scripts/validate-translations.js.
Note: Because translations live in the DB (not static locale files), key-set completeness can only be verified against a running gameserver. The server-side /i18n/admin/bulk/{lang}/{ns} endpoint validates shape on import; the validate-translations script covers the offline/CI subset. Draft keys are added via the admin API or the Translation Management page.
Right-to-left languages are not currently in scope; adding one (e.g. Arabic) will require RTL layout work in both UIs.