i18n
Internationalisation
The apps/ui uses react-i18next for translation. Keys are type-safe (typos fail the build) and hardcoded strings in JSX are a lint error, so a forgotten translation can’t ship.
Type-safe
translation keys
2
locales out of the box
Lint-enforced
no hardcoded strings
Design choices
Section titled “Design choices”Single common namespace by default
Keep it simple; split into more namespaces only when one balloons past ~200 keys.
Locales detected from the browser, fallback to first in VITE_LOCALES
Friendly default for international visitors; predictable for tests.
English + German out of the box
Two locales prove the pipeline; pick any two you actually need and replace.
Catalog files are plain JSON
Trivial to diff, translate, and review.
No Suspense for translations
Avoids a flash of fallback UI during i18n init.
Hardcoded JSX strings rejected by lint
Forgotten translations cannot ship; the linter catches the JSX literal.
How it’s wired
Section titled “How it’s wired”flowchart LR
detect["LanguageDetector<br/>browser language"]
catalogs["src/lib/i18n/locales/<br/>en/common.json · de/common.json"]
i18n["i18next + react-i18next"]
hook["useTranslation('common')<br/>const { t } = ..."]
component["component renders<br/>{t('auth.signIn')}"]
detect --> i18n
catalogs --> i18n
i18n --> hook
hook --> component
Translation flow: the LanguageDetector reads the browser’s preferred language;
JSON catalogs under src/lib/i18n/locales/ feed i18next; the
useTranslation(‘common’) hook returns t; components
call t(‘key’) with statically-checked keys.
Using it
Section titled “Using it”import { useTranslation } from "react-i18next";
const SignInButton = () => { const { t } = useTranslation(); return <button>{t("auth.signIn")}</button>;};A missing key in any locale that’s listed in VITE_LOCALES is a build-time concern, not a runtime one; the type generation step would flag a key that exists in en but not in de (or vice versa).
Adding a locale
Section titled “Adding a locale”- Drop
<lang>/common.jsonundersrc/lib/i18n/locales/<lang>/. - Add
<lang>toVITE_LOCALES(comma-separated). - Update the import in
src/lib/i18n/config.tsto register the catalog.
The language detector picks it up automatically; users on browsers in that locale start seeing it.
Adding a key
Section titled “Adding a key”- Add
"my.new.key": "English copy"toen/common.json. - Add the translation to every other locale’s
common.json(an_TODO_placeholder works as a build-tolerant intermediate). - Use it:
t("my.new.key").
For interpolation, react-i18next docs cover the syntax. For pluralization, the _one / _other suffix convention.
Patterns to avoid
Section titled “Patterns to avoid”Lint coverage
Section titled “Lint coverage”The UI app’s lint config bans hardcoded JSX strings on user-facing text. Numbers, technical identifiers, and data-* attributes are exempt. Static t("…") keys are also checked against the English catalog by @boring-stack-pkg/eslint-plugin-i18n-keys so a typo cannot ship. See Architecture rules and Lint as the contract.
Source
Section titled “Source”src/lib/i18n/; config, locales, and the catalog JSON.
Related
Section titled “Related”- Architecture rules; the component anatomy that hosts
t("…")calls. - UI template overview; where the i18n layer sits in the SPA shell.
- Lint as the contract; the
eslint-plugin-i18n-keysrule that catches typos at build. - Testing; how view-object tests stay locale-stable.