283 lines
11 KiB
Markdown
283 lines
11 KiB
Markdown
# Internationalization (i18n)
|
||
|
||
This project uses [i18next](https://www.i18next.com/) with [react-i18next](https://react.i18next.com/). 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 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 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:
|
||
|
||
```javascript
|
||
const { t } = useTranslation();
|
||
// same as useTranslation('translation')
|
||
```
|
||
|
||
Legal page example:
|
||
|
||
```javascript
|
||
const { t } = useTranslation('legal-impressum');
|
||
return t('title');
|
||
```
|
||
|
||
Multiple namespaces in one component:
|
||
|
||
```javascript
|
||
const { t: tDelivery } = useTranslation('legal-agb-delivery');
|
||
const { t: tPayment } = useTranslation('legal-agb-payment');
|
||
```
|
||
|
||
## Usage in components
|
||
|
||
### Function components
|
||
|
||
```javascript
|
||
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`:
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```javascript
|
||
import { withTranslation } from 'react-i18next';
|
||
import { withLanguage } from '../i18n/withTranslation.js';
|
||
|
||
export default withTranslation()(withLanguage(MyComponent));
|
||
```
|
||
|
||
Destructuring in `render`:
|
||
|
||
```javascript
|
||
const { t, title } = this.props;
|
||
t('product.new');
|
||
```
|
||
|
||
`this.props.t('…')` resolves against the default `translation` namespace.
|
||
|
||
### Language context (`changeLanguage`)
|
||
|
||
```javascript
|
||
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.js` → `cart.*`). 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:
|
||
|
||
```javascript
|
||
t('product.weight', { weight: '1,2' });
|
||
```
|
||
|
||
```javascript
|
||
// 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 project’s 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`:
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```bash
|
||
npm run i18n:check-keys
|
||
# or
|
||
node scripts/check-i18n-keys.mjs
|
||
```
|
||
|
||
Machine-readable output:
|
||
|
||
```bash
|
||
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).
|
||
|
||
### Embedded legal copy vs separate namespaces
|
||
|
||
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.
|