diff --git a/doc/multilingual.md b/doc/multilingual.md
new file mode 100644
index 0000000..9ebd271
--- /dev/null
+++ b/doc/multilingual.md
@@ -0,0 +1,232 @@
+# 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-`