Skip to content
BoringStack
Star

i18n

3 min read

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 never ships.

Single common namespace by default. Keep it simple; split into more namespaces only when one grows past around 200 keys.

Locales are detected from the browser, falling back to the first in VITE_LOCALES. This is friendly for international visitors and predictable for tests. English and German are included by default. Pick any two locales you need and replace them. Catalog files are plain JSON, trivial to diff, translate, and review.

No Suspense for translations. This avoids a flash of fallback UI during i18n init. Hardcoded JSX strings are rejected by lint, so forgotten translations cannot ship; the linter catches the JSX literal directly.

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.

Fragment concatenation: t("the") + " " + t("button"). Word order is language-specific; build full sentences as keys.

Sentence-as-ID: t("Click here to continue"). Hard to refactor when copy changes; use semantic keys like t("checkout.continue").

Hardcoded aria-label or placeholder: these are content, not technical strings. The lint rule applies to all of them.

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.