11 KiB
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
loadLanguageinsrc/i18n/index.js). - Persistence: Language choice is stored in
sessionStorage/localStorage(i18nextLng) via a custom detector insrc/i18n/index.js. - HTML
lang: Updated when the language changes (seeLanguageProviderinsrc/i18n/withTranslation.js). - Fallback:
fallbackLngisde.
Product-facing behavior
- Language switcher —
src/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 maintranslationnamespace (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 singletranslation.json). - Legal bundles — e.g.
legal-agb-delivery.js,legal-datenschutz-basic.js,legal-impressum.js, registered as their own i18next namespaces insrc/i18n/index.js.
Namespaces
translation(default) — Everything merged inlocales/<lang>/index.js. Keys are dot paths such ascart.addToCartorproduct.title.legal-*— Long-form legal copy. Loaded the same way, but you select the namespace inuseTranslation('legal-impressum')(or equivalent). Keys are relative to that namespace, e.g.sections.operator.title, notlegal-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 insideindex.js(e.g.cart.js→cart.*). Keep new strings in the appropriate module and export them fromindex.jsif 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
- Edit the right module under
src/i18n/locales/de/(source of truth for structure). - Mirror changes in other languages under
src/i18n/locales/<lang>/as needed, or use the project’s translation tooling (npm run translate, etc. — seetranslate-i18n.jsandpackage.jsonscripts). - For a new top-level section, add the module import and export in
locales/<lang>/index.jsfor 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
- Add a folder
src/i18n/locales/<code>/with the same file set as German (at minimumindex.jsand every moduleindex.jsimports, plus alllegal-*.jsfiles registered insrc/i18n/index.js). - Ensure
loadLanguageinsrc/i18n/index.jscan import that folder (pattern already usesimport(\./locales/${language}/...`)`). - Include the code in
allLanguages(and any UI lists) inwithTranslation.js/LanguageSwitcher.jsas needed. - Clear the language cache logic if you add special cases (
languageCacheinindex.js).
Configuration notes
- Detection: Custom detector in
src/i18n/index.jsprefers session/localStorage, then defaults tode(browser language is not used for the initial default). - Debug: i18next
debugfollowsNODE_ENV(on in development). - Missing keys:
saveMissingcan run in development (seesrc/i18n/index.js).
SEO
- Document
langupdates with language changes. - You can extend the app with hreflang, localized routes, or metadata per language as needed.
Best practices
- Keep structure parallel across
locales/<lang>/files. - Prefer German (
de) as the structural source; keepfallbackLngaligned with your primary market. - Use interpolation for variable fragments:
t('key', { name: value }). - 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');thentDelivery('deliveryTerms.1') - Destructuring
const { t } = this.props/propsfor HOC-wrapped components - Fully static template literals without interpolation:
t(`some.key`)
It flattens locale objects the same way i18next does (leaf keys only).
Embedded legal copy vs separate namespaces
Legal strings exist in two shapes:
- Separate namespace (what pages use):
legal-agb-delivery+ keydeliveryTerms.1. - Embedded in
translation:legalAgbDelivery.deliveryTerms.1insidelocales/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.jssrc/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) inscripts/check-i18n-keys.mjsso 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
- Call
twith a string literal (or a template literal without${…}) as the first argument. - Use
useTranslation('namespace')with a string literal namespace so aliases liketFoomap correctly. - For class components, keep
const { t } = this.props(orprops) so the script tiesttotranslation.
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
deentry).
The report is best-effort; use it to find obvious dead strings and drift, not as a formal guarantee.