--- type: concept tags: [parking, frontend, i18n, localization] sources: [] updated: 2026-06-18 status: open --- # Internationalization (i18n) The operator UI ships in **two languages: Albanian (default) and English**. Language is a **per-user preference stored server-side** and loaded on login — not a browser/localStorage setting, not a site-wide one. So an operator's choice follows their account and is restored on every login from any booth. (Decided + built 2026-06-18.) ## Decisions - **Albanian is the default and fallback.** English is the second language. A missing English key falls back to Albanian. - **Per-user, server-side preference.** `users.language` (`'sq' | 'en'`, default `'sq'`; migration 0003). Returned from `/api/auth/login` and `/api/auth/me`, and changed via **`PUT /api/auth/language`** (self-service, any signed-in role). It is **deliberately NOT in the JWT** (identity/role only) — so changing language is a DB write + immediate `/me`, with no token refresh / re-login. See [[local-jwt-auth]]. - **Library: react-i18next** (i18next). Chosen over a hand-rolled `t()` for pluralization, interpolation, and headroom beyond two languages. The active language is applied after `/me` resolves (App effect on `user.language`); the header **SQ/EN toggle** switches instantly *and* persists. - **Printed tickets/receipts stay Albanian.** Customer-facing paper is **independent** of the operator's UI language — an operator reading the UI in English still prints Albanian tickets. The print strings live in the device driver's `STR` table ([[ticket-encoding]], [[site-metadata]]); can become a `site_config.print_language` setting later if a site ever needs English receipts. ## As-built (2026-06-18) - **Backend:** `users.language` + the three auth touch-points above (`apps/server/src/routes/auth.ts`). - **Frontend:** `apps/web/src/lib/i18n/` — `sq.ts` (default/fallback), `en.ts`, and `index.ts` (init + `setLanguage()`). **Type-safe key parity:** `Catalog` is the *shape* of `sq` with string-typed values, so TypeScript forces `en.ts` to supply every key (and the build fails on a missing/typo'd key). Keys are dot-namespaced by area (`common`, `nav`, `status`, `auth`, `booth`, `pay`, `shift`, `site`, `permits`, `tariff`). - **Translated screens:** the booth ([[booth-console]] — screen, pay/exit modal, active sessions, snapshots, status), Login, ShiftControl, SiteSettings, PermitManager, TariffComposer. ## Open / deferred - **SetupWizard is NOT translated** (deliberate). Its content is mostly **server-provided** — driver labels and config-field labels/help come from the backend device-catalog API ([[device-registry]], [[first-run-setup]]). Translating only its static chrome would leave a half-English screen; it's deferred until **backend catalog i18n** is scoped, then chrome + catalog localize together. - **Server API error strings** are still English (surfaced raw in the UI). v1 relies on the client mapping known errors; a fuller approach would translate by error *code*, not message. - **Behaviour note (not a bug):** a *hard navigation* (new URL) re-bootstraps the language from the user's stored preference via `/me` — so an un-persisted toggle resets. Correct: the stored pref wins. The toggle persists via the PUT, so it survives once saved.