# 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: `<h2>Aktionen</h2>`, `<button title="Erst Homing ausführen">`.
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 = '<span>⚠ Bitte Set auswählen, das verschoben werden soll.</span>'`.
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
<h2 data-i18n="index.actions.title">Aktionen</h2>

<button id="btn-homing-send" disabled
  data-i18n="index.actions.sendToRobot"
  data-i18n-attr-title="index.actions.sendDisabledTitle"
  title="Erst Homing ausführen">
  ✅ An Roboter senden
</button>
```

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
<script type="module" src="/i18n.js"></script>
<script type="module">
  import { initI18n } from '/i18n.js';
  await initI18n();
</script>
<script src="/client.js"></script>
```

Da `client.js`/`homing.js`/`calibration.js` aktuell keine ES-Module sind, reicht es, `initI18n()`
in einem kleinen Inline-`<script type="module">` vor dem normalen `<script>`-Tag auszuführen –
kein Umbau der bestehenden Dateien zu Modulen nötig (DOM ist zu diesem Zeitpunkt geparst, weil
Module standardmäßig wie `defer` laufen).

## 4. Dynamische Strings in Client-JS

Für Strings, die per JS erzeugt werden (Status-Badges, Fehlermeldungen, generierte Tabellen),
wird `t()` global verfügbar gemacht (z. B. `window.t = t;` am Ende von `i18n.js`) und an den
Stellen, an denen heute literal Deutsch steht, durch einen Key-Aufruf ersetzt:

```js
// vorher (public/homing.js)
if (txt) txt.textContent = text || `Schritt ${step} / ${total}`;

// nachher
if (txt) txt.textContent = text || t('homing.progress.step', { step, total });
// "homing.progress.step": "Schritt {step} / {total}"  /  "Step {step} of {total}"
```

```js
// vorher (public/calibration.js)
result.innerHTML = '<span style="color:#f87171">⚠ Bitte Set auswählen, das verschoben werden soll.</span>';

// nachher
result.innerHTML = `<span style="color:#f87171">⚠ ${t('calibration.move.selectSetWarning')}</span>`;
```

Das ist die aufwändigste Stelle (laut grep aktuell **deutlich über 1000 Treffer** für deutsche
Wortfragmente über alle `public/*.js`-Dateien), aber rein mechanisch und gut inkrementell machbar:
eine Datei nach der anderen, jeweils committed und getestet.

## 5. Server-seitige Strings (Fehlermeldungen)

`server/server.js` schickt aktuell fertige deutsche Sätze im JSON zurück, z. B.

```js
return res.status(400).json({ error: '"y" und "z" müssen Zahlen sein.' });
```

Diese Strings landen direkt im UI (`infoEl.textContent = data.error` o. ä.) – der Server kann aber
die Browsersprache des Clients nicht zuverlässig kennen (und sollte sie auch nicht raten müssen).
Saubere Lösung: Server schickt einen **Code**, Client übersetzt:

```js
// server/server.js
return res.status(400).json({ errorCode: 'yzMustBeNumbers' });
```

```js
// Client, z. B. client.js / homing.js
const msg = data.errorCode ? t(`server.error.${data.errorCode}`) : (data.error ?? t('common.unknownError'));
```

Migration ist optional und kann zuletzt erfolgen – bis dahin einfach `error` (deutsch) weiter
durchreichen und im Client mit `data.error ?? t(...)` abfangen, damit nichts bricht, während man
Stück für Stück auf `errorCode` umstellt.

## 6. Sprachumschalter in der UI

Kleines Dropdown/Flag-Toggle in der Topbar (`calib-topbar` existiert schon in mehreren Seiten),
das `setLang('en')` / `setLang('de')` aufruft. Reicht als erster Schritt; ein automatisches
Re-Rendering ohne Reload ist bei dieser Seitenanzahl unnötiger Aufwand.

## 7. Reihenfolge der Umsetzung (Vorschlag)

1. `public/i18n/de.json` aus den **aktuellen** deutschen Strings extrahieren (1:1, keine
   Textänderung) + `i18n.js` Lib bauen. Keine sichtbare Änderung im Verhalten.
2. `public/i18n/en.json` befüllen (Übersetzung).
3. Seite für Seite umstellen, anzufangen bei `index.html` + `client.js` (am meisten genutzt),
   danach `homing.html`/`homing.js`, danach `calibration*.html`/`calibration.js`.
4. Sprachumschalter in der Topbar ergänzen.
5. Server-Fehlermeldungen optional zuletzt auf `errorCode` umstellen.

## Warum keine Library wie i18next?

Für die Größe dieser App (kein Build-Schritt, ~5 HTML-Seiten, kein Framework) wäre i18next/
FormatJS deutlich mehr Gewicht (zusätzliche Dependency, Build-Integration, ICU-Syntax) als Nutzen.
Die ~80 Zeilen oben decken Pluralisierung zwar nicht ab, aber dafür gibt es in der App aktuell
keinen Bedarf (keine zählerabhängigen Sätze wie „1 Marker“ vs. „3 Marker“ – falls das später
gebraucht wird, reicht eine simple `count === 1 ? key + '.one' : key + '.many'`-Konvention).
