Files
reactShop/docs/i18n.md

11 KiB
Raw Blame History

Internationalization (i18n)

This project uses i18next with react-i18next. The setup works with both function components and class components (HOCs).

Overview

  • Default language: German (de), bundled at startup.
  • Other languages: Loaded on demand when the user switches (see loadLanguage in src/i18n/index.js).
  • Persistence: Language choice is stored in sessionStorage / localStorage (i18nextLng) via a custom detector in src/i18n/index.js.
  • HTML lang: Updated when the language changes (see LanguageProvider in src/i18n/withTranslation.js).
  • Fallback: fallbackLng is de.

Product-facing behavior

  • Language switchersrc/components/LanguageSwitcher.js: shows current language with flag, dropdown to change language, persists selection.
  • Translated areas — Navigation, auth, cart, checkout, product UI, footer, profile, and more; legal pages use dedicated legal-* namespaces (long-form text).

Layout

Path Role
src/i18n/index.js Initializes i18n, registers namespaces, lazy-loads non-German locales
src/i18n/withTranslation.js LanguageProvider, withTranslation, withI18n, withLanguage
src/i18n/locales/<lang>/ Per-language modules (de, en, …)

Each language folder contains:

  • index.js — Merges feature modules into the main translation namespace (shop UI: navigation, cart, product, …). Some legal content is also re-exported here under camelCase keys (e.g. legalAgbDelivery) for convenience; runtime legal pages use separate namespaces (see below).
  • One file per topic — e.g. cart.js, navigation.js, product.js (not a single translation.json).
  • Legal bundles — e.g. legal-agb-delivery.js, legal-datenschutz-basic.js, legal-impressum.js, registered as their own i18next namespaces in src/i18n/index.js.

Namespaces

  • translation (default) — Everything merged in locales/<lang>/index.js. Keys are dot paths such as cart.addToCart or product.title.
  • legal-* — Long-form legal copy. Loaded the same way, but you select the namespace in useTranslation('legal-impressum') (or equivalent). Keys are relative to that namespace, e.g. sections.operator.title, not legal-impressum.sections.operator.title.

Default namespace is translation; omit the argument for shop UI:

const { t } = useTranslation();
// same as useTranslation('translation')

Legal page example:

const { t } = useTranslation('legal-impressum');
return t('title');

Multiple namespaces in one component:

const { t: tDelivery } = useTranslation('legal-agb-delivery');
const { t: tPayment } = useTranslation('legal-agb-payment');

Usage in components

Function components

import { useTranslation } from 'react-i18next';

const MyComponent = () => {
  const { t } = useTranslation();

  return <Typography>{t('navigation.home')}</Typography>;
};

Class components — withI18n (translation + language context)

withI18n combines translation and language context. Use it when you need t and optionally languageContext:

import { withI18n } from '../i18n/withTranslation.js';

class MyComponent extends Component {
  render() {
    const { t } = this.props;

    return <Typography>{t('navigation.home')}</Typography>;
  }
}

export default withI18n()(MyComponent);

Class components — withTranslation + withLanguage

Some components only need t and languageContext from separate HOCs:

import { withTranslation } from 'react-i18next';
import { withLanguage } from '../i18n/withTranslation.js';

export default withTranslation()(withLanguage(MyComponent));

Destructuring in render:

const { t, title } = this.props;
t('product.new');

this.props.t('…') resolves against the default translation namespace.

Language context (changeLanguage)

import { withLanguage } from '../i18n/withTranslation.js';

class MyComponent extends Component {
  render() {
    const { languageContext } = this.props;

    return (
      <Button onClick={() => languageContext.changeLanguage('en')}>
        English
      </Button>
    );
  }
}

export default withLanguage(MyComponent);

Available language codes are defined on LanguageProvider (allLanguages in withTranslation.js); the switcher only offers languages that are loaded or loading.

Key naming

  • Use dot notation for nested objects in the JS exports: product.inclVat, delivery.times.standard2to3Days.
  • File names in locales/de/ map to top-level segments inside index.js (e.g. cart.jscart.*). Keep new strings in the appropriate module and export them from index.js if you add a new file.

Interpolation

i18next interpolation uses {{name}} in strings and an options object:

t('product.weight', { weight: '1,2' });
// locale string example: "Gewicht: {{weight}} kg"

Language switching

LanguageProvider coordinates i18n.changeLanguage and lazy loading. Non-default languages are loaded via loadLanguage in src/i18n/index.js, which dynamic-imports the same file layout under locales/<lang>/.

Adding or changing copy

  1. Edit the right module under src/i18n/locales/de/ (source of truth for structure).
  2. Mirror changes in other languages under src/i18n/locales/<lang>/ as needed, or use the projects translation tooling (npm run translate, etc. — see translate-i18n.js and package.json scripts).
  3. For a new top-level section, add the module import and export in locales/<lang>/index.js for every language you ship.

Example (new keys in an existing file)

src/i18n/locales/de/cart.js:

export default {
  // ...
  newFeature: {
    title: 'Neuer Titel',
    description: 'Neue Beschreibung',
  },
};

Use in code: t('cart.newFeature.title') (assuming the cart object is merged under cart in index.js).

Adding a new language

  1. Add a folder src/i18n/locales/<code>/ with the same file set as German (at minimum index.js and every module index.js imports, plus all legal-*.js files registered in src/i18n/index.js).
  2. Ensure loadLanguage in src/i18n/index.js can import that folder (pattern already uses import(\./locales/${language}/...`)`).
  3. Include the code in allLanguages (and any UI lists) in withTranslation.js / LanguageSwitcher.js as needed.
  4. Clear the language cache logic if you add special cases (languageCache in index.js).

Configuration notes

  • Detection: Custom detector in src/i18n/index.js prefers session/localStorage, then defaults to de (browser language is not used for the initial default).
  • Debug: i18next debug follows NODE_ENV (on in development).
  • Missing keys: saveMissing can run in development (see src/i18n/index.js).

SEO

  • Document lang updates with language changes.
  • You can extend the app with hreflang, localized routes, or metadata per language as needed.

Best practices

  1. Keep structure parallel across locales/<lang>/ files.
  2. Prefer German (de) as the structural source; keep fallbackLng aligned with your primary market.
  3. Use interpolation for variable fragments: t('key', { name: value }).
  4. Test critical flows after adding strings or languages.

Translation coverage (high level)

Coverage varies by screen; shop flows, auth, cart, products, and legal pages have substantial strings. Remaining gaps are normal for a growing app—search for hard-coded German/English in components when polishing.

Performance

  • German is in the initial bundle.
  • Other languages load on first switch (dynamic import), then stay cached in the i18n layer for the session.

Browser support

Modern browsers with ES modules, localStorage / sessionStorage, and your supported React version.


Key usage check (scripts/check-i18n-keys.mjs)

The check answers: which keys defined for German (src/i18n/locales/de) appear to be unused in src/?

Run:

npm run i18n:check-keys
# or
node scripts/check-i18n-keys.mjs

Machine-readable output:

node scripts/check-i18n-keys.mjs --json

What the script detects

The script only understands static key strings:

  • t('cart.addToCart'), t("…")
  • this.props.t('…'), props.t('…')
  • Aliases from useTranslation('namespace'), e.g. const { t: tDelivery } = useTranslation('legal-agb-delivery'); then tDelivery('deliveryTerms.1')
  • Destructuring const { t } = this.props / props for HOC-wrapped components
  • Fully static template literals without interpolation: t(`some.key`)

It flattens locale objects the same way i18next does (leaf keys only).

Legal strings exist in two shapes:

  1. Separate namespace (what pages use): legal-agb-delivery + key deliveryTerms.1.
  2. Embedded in translation: legalAgbDelivery.deliveryTerms.1 inside locales/de/index.js.

The script treats a key as used in the embedded translation tree when the matching separate-namespace key is used, so you do not see false “unused” duplicates for the same text.

Dynamic keys (t(\…${x}…`)`)

If the key is built at runtime, the static scan will not see it. The script includes hard-coded expansions for known patterns on:

  • src/pages/AGB.js
  • src/pages/Datenschutz.js (basic Datenschutz sections)
  • src/pages/Impressum.js

If you add new dynamic patterns elsewhere, either:

  • Prefer static keys where reasonable (t('feature.section.title') per section), or
  • Extend addKnownDynamicLegalKeys (or equivalent) in scripts/check-i18n-keys.mjs so the checker knows which key paths can occur.

Other files may still be listed under “template literals in t(...)” when the script cannot prove all keys.

Making new keys “count” for the checker

  1. Call t with a string literal (or a template literal without ${…}) as the first argument.
  2. Use useTranslation('namespace') with a string literal namespace so aliases like tFoo map correctly.
  3. For class components, keep const { t } = this.props (or props) so the script ties t to translation.

Avoid only referencing a key from non-src code (build scripts, server-only files): the checker only scans src/ and skips src/i18n/locales/**.

Interpreting results

  • Unused — No static (and no expanded dynamic) reference found; may still be dead copy, or only used via variables/dynamic templates.
  • Used but not in locale files — A reference in code does not match any defined German key (typo or missing de entry).

The report is best-effort; use it to find obvious dead strings and drift, not as a formal guarantee.