Internationalization (i18n) — game client
Overview
The game client supports multiple languages with URL-based locale: the first path segment is the language (/en, /ru). There is no middleware; locale is derived on the client from the path. Translations are type-safe and live in TypeScript locale files. Preferred locale is stored in localStorage and used when the user visits /.
Product context and phases: Offline Multilanguage Game POC.
Implementation summary
| What | Where |
|---|---|
| Locale from URL | First path segment; parsed in pathToLang() and in parseRoute() for view routing |
| i18n runtime | i18next + react-i18next; initialized in config.ts with namespace resources |
| Provider | I18nShell wraps the app: derives lang from path, sets document.documentElement.lang, redirects / to preferred locale, wraps children in I18nProvider which calls i18n.changeLanguage(lang) |
| Locale files | apps/game-client/src/locales/{lang}/{namespace}.ts (e.g. en/common.ts, ru/game.ts, en/metadata.ts) with as const exports |
| Metadata (SEO) | metadata namespace holds title, description, and keywords per locale. getLocaleMetadata(lang, pathSegments) returns locale-specific overrides (title, description, keywords, openGraph locale/title/description, twitter title/description); the root layout provides shared openGraph/twitter defaults and a title template (%s | Kartuli), so the document title is e.g. "Title | Kartuli". Next.js merges layout and page metadata. When path segments are passed, adds alternates.canonical, alternates.languages (hreflang), and sets openGraph.url to the page canonical. Root (/) is treated as default locale: canonical and x-default point to /en. |
| Type-safe keys | i18next.d.ts augments i18next so t('common.save') etc. are type-checked against the locale shape |
| Language switcher | LanguageSwitcher in the shell header; switches locale + URL and persists to localStorage |
| Supported locales | Single source: i18n/supported-locales.ts (supportedLngs); re-exported by config.ts. Keep in sync with route-utils.ts (SUPPORTED_LANGS). |
| Static paths | generateStaticParams in app/[[...spaRoute]]/page.tsx pre-renders /, /en, /ru (and any new locale roots you add) |
Developer experience
Working with translations
Use the correct namespace
Namespaces are per domain:common(shared: save, cancel, home, language names, SW banner, etc.),game,learn,debug,metadata(SEO title, description, keywords; used server-side bygenerateMetadata, not viat()in UI). Use the namespace that owns the string (e.g. learn screen →useTranslation('learn')).Call
t()with typed keys
Keys are inferred from the locale modules. Use dot notation for nested keys:t('common.sw.dismiss'),t('learn.back'). If you add a new key to a locale file, add it to all locale files for that namespace (en, ru, and any future locales) so types and runtime stay consistent.Shared components (e.g. in
packages/ui)
Do not put i18n inside the shared package. The consumer (game-client) callst()and passes the result as props. Example:DeploymentDebugPanelreceiveslabelsandappName; the game-client debug page buildslabelsfromt('debug.…')and passes them in.Navigation and
lang
UseuseLang()fromi18n/use-langto get the current locale and build paths:navigate(\/${lang}/learn/${lessonId}`)` so links stay locale-aware.Testing
Unit tests that render the shell mock../utils/browser(includingsetDocumentLang). E2E smoke tests for locale and language switcher live intools/e2e/tests/game-client/production/i18n.spec.tsand asserthtmllangand visible content per locale.
Adding or changing translation keys
- Add the key in every locale file for that namespace, e.g.
locales/en/common.tsandlocales/ru/common.ts. - Keep the same key structure in all locales (only values differ). The
i18next.d.tstypes are driven by the English locale shape; other locales must match that shape. - Use nested objects where it helps: e.g.
sw: { dismiss: 'Dismiss', ... }incommonso keys stay grouped.
Adding a new locale (e.g. Spanish es)
To support a new locale such as es, update the following in order. This keeps routing, i18n config, static generation, and (optionally) the service worker in sync.
1. Locale files
Create a new folder and one file per namespace, mirroring the existing locales:
apps/game-client/src/locales/es/common.tsapps/game-client/src/locales/es/debug.tsapps/game-client/src/locales/es/game.tsapps/game-client/src/locales/es/learn.tsapps/game-client/src/locales/es/metadata.ts(title, description, keywords for SEO; used bygenerateMetadata)
Copy the structure from locales/en/*.ts (or ru) and replace the values with Spanish. Export with as const so types stay consistent.
2. i18n config
File: apps/game-client/src/i18n/supported-locales.ts
- Add
'es'tosupportedLngs: change to['en', 'ru', 'es'] as const. This file is the single source;config.tsre-exports it.
File: apps/game-client/src/i18n/config.ts
- Import the new locale modules (e.g.
esCommon,esDebug,esGame,esLearn,esMetadata). - Add an
eskey to theresourcesobject with the same namespace structure asenandru(includingmetadata). - Add
'metadata'to thensarray if not already present.
3. Routing (app shell)
File: apps/game-client/src/domains/app-shell/route-utils.ts
- Add
'es'toSUPPORTED_LANGS:const SUPPORTED_LANGS = ['en', 'ru', 'es'] as const. - No other change needed;
parseRouteandisSupportedLangwill then acceptesas the first segment.
4. Static generation
File: apps/game-client/src/app/[[...spaRoute]]/page.tsx
- Add the new locale root to
STATIC_PATHS, e.g.{ spaRoute: ['es'] }, so that/esis pre-rendered at build time.
5. Locale-specific metadata (SEO)
File: apps/game-client/src/config/get-locale-metadata.ts
- Import the new locale’s metadata module (e.g.
esMetadatafrom../locales/es/metadata). - Add
es: esMetadatatometadataByLocale(must includetitle,description,keywords). - Add
es: 'es_ES'(or the appropriate Open Graph locale) toogLocaleByLang. - Add
es: 'es'tohreflangByLangso alternates.languages (hreflang) uses the correct code.
6. Language switcher (UX)
File: apps/game-client/src/i18n/LanguageSwitcher.tsx
Currently the switcher is built for two locales (
OTHER_LANG: Record<SupportedLng, SupportedLng>and a single “other” button). For a third locale you have two options:- Option A: Extend the mapping (e.g. cycle: en → ru → es → en) and keep a single “Switch to next” button.
- Option B: Replace the single button with a small dropdown or list of locale links so the user can pick any language.
Add the new language label to common in every locale: e.g.
langEs: 'Spanish'(and the localized form in each language). Use it in the switcher UI for the new option.
7. Service worker (offline)
The service worker currently precaches only /en and serves /en and /en/* from cache. To support /es (and /ru) offline:
File:
apps/game-client/src/domains/service-worker/get-aditional-precache-entries.ts
Add entries for each locale root you want offline, e.g.{ url: '/es', revision }(and/ruif not already there), using the samerevisionas for/en.File:
apps/game-client/src/service-worker/service-worker.ts
Update the fetch handler so that navigation to/esand/es/*(and other locale roots) is served from the precached shell for that locale, in the same way as/enand/en/*. Today the logic is hardcoded to/en; it would need to branch on the first path segment or a small allowlist of locale roots.
8. E2E and debug expectations (optional)
- If you have e2e tests that assert locale-specific content, add a test for
/es(and the switcher to/fromes) intools/e2e/tests/game-client/production/i18n.spec.ts. - If you use shared debug-page expectations and want the debug page under
/es/debugto be covered, extend the expectations or e2e config as needed (e.g. addesto the list of locales under test).
9. i18next type declarations (optional)
File: apps/game-client/src/i18n/i18next.d.ts
- The
resourcestype there is used for key structure, not for listing every locale. Typically it’s driven by one locale (e.g. English). You do not need to addesto this file unless you introduce a new namespace or key shape that TypeScript should know about.
References
In-repo code
- I18n config
- Route utils (SUPPORTED_LANGS)
- Locale files
- Static params and generateMetadata
- getLocaleMetadata
- supported-locales
Related docs
- Offline Multilanguage Game POC — Product spec and phases
- Game Client Hub — Overview and links