# Mehrsprachigkeit (DE/EN) – Vorschlag
## Ausgangslage
Die App ist heute komplett deutschsprachig, und zwar an drei verschiedenen Stellen, die unterschiedlich behandelt werden müssen:
1. **Statisches HTML-Markup** – Labels, Überschriften, `placeholder`/`title`-Attribute direkt in
`public/*.html` (`index.html`, `homing.html`, `calibration*.html`, …).
Beispiel: `
Aktionen `, ``.
2. **Dynamisch erzeugte Strings in Client-JS** – Template-Literals in `public/client.js`,
`public/homing.js`, `public/calibration.js` etc., z. B.
`txt.textContent = text || \`Schritt ${step} / ${total}\`;` oder
`result.innerHTML = '⚠ Bitte Set auswählen, das verschoben werden soll. '`.
3. **Server-seitige Strings**, die als JSON an den Client durchgereicht und dort angezeigt werden,
z. B. `res.status(400).json({ error: '"y" und "z" müssen Zahlen sein.' })` in
[server/server.js](../server/server.js).
Es gibt kein Build-Tool (kein Webpack/Vite/React) – alles ist Vanilla HTML/JS, das Express
als statische Dateien ausliefert ([server/server.js](../server/server.js)). Jede Lösung sollte
also **ohne Build-Schritt** funktionieren und sich inkrementell einführen lassen (nicht „big bang“,
sondern Seite für Seite).
## Zielbild
- Sprache wird primär aus `navigator.language` (Browser-Einstellung) bestimmt, mit manuellem
Override (Dropdown/Flag-Icon in der Topbar) und Persistenz in `localStorage`.
- Fallback-Sprache ist Deutsch (aktueller Stand), zweite Sprache Englisch.
- Übersetzungen liegen in einfachen JSON-Dateien, kein zusätzliches npm-Paket nötig
(es reicht ein paar Dutzend Zeilen eigener Lade-/Ersetzungslogik).
- Server-Fehlermeldungen werden nicht als fertiger Text geschickt, sondern als
**Fehlercode**, den der Client anhand der aktuellen Sprache übersetzt.
## 1. Struktur der Sprachdateien
```
public/
i18n/
de.json
en.json
i18n.js <- kleine Lade-/Helper-Bibliothek, von jeder Seite eingebunden
```
`de.json` (Auszug, Keys gruppiert nach Seite/Bereich, „flach mit Punktnotation“ ist für diese
Größenordnung einfacher zu pflegen als tief verschachteltes JSON):
```json
{
"common.back": "← Zurück",
"common.error": "Fehler",
"index.actions.title": "Aktionen",
"index.actions.runHoming": "📷 Foto & Homing berechnen",
"index.actions.sendToRobot": "✅ An Roboter senden",
"index.actions.sendDisabledTitle": "Erst Homing ausführen",
"index.status.idle": "○ Warte",
"index.status.running": "● Läuft …",
"calibration.move.selectSetWarning": "⚠ Bitte Set auswählen, das verschoben werden soll.",
"calibration.move.sameSetWarning": "⚠ \"Bleibt\" und \"verschoben\" dürfen nicht dasselbe Set sein.",
"server.error.yzMustBeNumbers": "\"y\" und \"z\" müssen Zahlen sein.",
"server.error.invalidAxisPayload": "Ungültige Nutzlast: axis.dir und axis.referencePoint erwartet"
}
```
`en.json` hat exakt dieselben Keys mit englischen Werten. Das ist die einzige Stelle, an der eine
fehlende Übersetzung sofort auffällt: Key in `de.json` ohne Pendant in `en.json` → Fallback auf
Deutsch (siehe unten), kein Crash.
**Warum nicht ein JSON pro HTML-Seite?** Weil viele Strings (Status-Badges, Fehlermeldungen,
Buttons wie „Zurück“) seitenübergreifend wiederverwendet werden. Eine flache `common.*`-Gruppe plus
eine Gruppe pro Seite (`index.*`, `homing.*`, `calibration.*`) hält das überschaubar, ohne dass man
Strings doppelt pflegt.
## 2. Statisches HTML markieren
Jedes übersetzbare Element bekommt ein `data-i18n`-Attribut mit dem Key. Für Attribute (z. B.
`title`, `placeholder`) gibt es ein zusätzliches `data-i18n-attr`:
```html
Aktionen
✅ An Roboter senden
```
Wichtig: Der **deutsche Text bleibt im HTML stehen** (als Fallback/Default und damit die Seite ohne
JS oder bei Ladefehler nicht leer ist). `i18n.js` ersetzt den Inhalt nur, wenn die Zielsprache nicht
Deutsch ist bzw. wenn ein Override gesetzt wurde. Das macht die Migration risikoarm: Man kann Seite
für Seite Attribute ergänzen, ohne dass etwas kaputtgeht, falls eine Seite noch nicht migriert ist.
## 3. `i18n.js` – minimale Laufzeit-Bibliothek
Kernfunktionen, ca. 60–80 Zeilen, kein Tooling nötig:
```js
// public/i18n.js
const SUPPORTED = ['de', 'en'];
const FALLBACK = 'de';
function detectLang() {
const stored = localStorage.getItem('lang');
if (stored && SUPPORTED.includes(stored)) return stored;
const nav = (navigator.language || FALLBACK).slice(0, 2).toLowerCase();
return SUPPORTED.includes(nav) ? nav : FALLBACK;
}
let dict = {};
let fallbackDict = {};
export async function initI18n() {
const lang = detectLang();
[dict, fallbackDict] = await Promise.all([
fetch(`/i18n/${lang}.json`).then(r => r.json()),
lang === FALLBACK ? Promise.resolve({}) : fetch(`/i18n/${FALLBACK}.json`).then(r => r.json()),
]);
document.documentElement.lang = lang;
applyToDom();
return lang;
}
export function t(key, vars) {
let str = dict[key] ?? fallbackDict[key] ?? key; // Key selbst als letzter Fallback -> sichtbar im UI statt "undefined"
if (vars) for (const [k, v] of Object.entries(vars)) str = str.replaceAll(`{${k}}`, v);
return str;
}
function applyToDom(root = document) {
root.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); });
root.querySelectorAll('[data-i18n-attr-title]').forEach(el => { el.title = t(el.dataset.i18nAttrTitle); });
root.querySelectorAll('[data-i18n-attr-placeholder]').forEach(el => { el.placeholder = t(el.dataset.i18nAttrPlaceholder); });
}
export async function setLang(lang) {
localStorage.setItem('lang', lang);
location.reload(); // einfach & robust, kein dynamisches Re-Rendering nötig bei dieser App-Größe
}
```
Einbindung pro Seite (vor dem bestehenden Seiten-Script):
```html
```
Da `client.js`/`homing.js`/`calibration.js` aktuell keine ES-Module sind, reicht es, `initI18n()`
in einem kleinen Inline-`