Skip to content
BoringStack
GitHub

i18n

3 min read

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

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.

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.

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).

  1. Drop <lang>/common.json under src/lib/i18n/locales/<lang>/.
  2. Add <lang> to VITE_LOCALES (comma-separated).
  3. Update the import in src/lib/i18n/config.ts to register the catalog.

The language detector picks it up automatically; users on browsers in that locale start seeing it.

  1. Add "my.new.key": "English copy" to en/common.json.
  2. Add the translation to every other locale’s common.json (an _TODO_ placeholder works as a build-tolerant intermediate).
  3. Use it: t("my.new.key").

For interpolation, react-i18next docs cover the syntax. For pluralization, the _one / _other suffix convention.

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.

src/lib/i18n/; config, locales, and the catalog JSON.