Neue Marker Unterarm

This commit is contained in:
chk
2026-06-16 13:55:42 +02:00
parent ef4c7e6144
commit f983d69a0c
2 changed files with 240 additions and 4 deletions

232
doc/multilingual.md Normal file
View File

@@ -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: `<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. 6080 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).

View File

@@ -332,13 +332,13 @@
} }
], ],
"markers": [ "markers": [
{"id": 244, "name": "aruco_244", "position": [125, 0, 0], "normal": [1, 0, 0], "size": 25, "spin": 0}, {"id": 116, "name": "aruco_116", "position": [125, 0, 0], "normal": [1, 0, 0], "size": 25, "spin": 0},
{"id": 245, "name": "aruco_245", "position": [90, 0, -35], "normal": [0, 0, -1], "size": 25, "spin": 0}, {"id": 245, "name": "aruco_245", "position": [90, 0, -35], "normal": [0, 0, -1], "size": 25, "spin": 0},
{"id": 226, "name": "aruco_246", "position": [90, 0, 35], "normal": [0, 0, 1], "size": 25}, {"id": 129, "name": "aruco_129", "position": [90, 0, 35], "normal": [0, 0, 1], "size": 25},
{"id": 247, "name": "aruco_247", "position": [52.5, 0, 35], "normal": [0, 0, 1], "size": 25}, {"id": 132, "name": "aruco_132", "position": [52.5, 0, 35], "normal": [0, 0, 1], "size": 25},
{"id": 248, "name": "aruco_248", "position": [52.5, 0, -35], "normal": [0, 0, -1], "size": 25}, {"id": 248, "name": "aruco_248", "position": [52.5, 0, -35], "normal": [0, 0, -1], "size": 25},
{"id": 232, "name": "aruco_232", "position": [90, 24.75, -24.75], "normal": [0, 1, -1], "size": 25}, {"id": 232, "name": "aruco_232", "position": [90, 24.75, -24.75], "normal": [0, 1, -1], "size": 25},
{"id": 231, "name": "aruco_231", "position": [90, 24.75, 24.75], "normal": [0, 1, 1], "size": 25} {"id": 121, "name": "aruco_121", "position": [90, 24.75, 24.75], "normal": [0, 1, 1], "size": 25}
] ]
}, },
"Arm2": { "Arm2": {
@@ -363,6 +363,10 @@
} }
], ],
"markers": [ "markers": [
{"id": 148, "name": "aruco_148", "position": [-35, -219, 0], "normal": [-1,0,0], "size": 25},
{"id": 144, "name": "aruco_144", "position": [-35, -112, 0], "normal": [-1,0,0], "size": 25},
{"id": 146, "name": "aruco_146", "position": [-24.75, -112, 24.75], "normal": [-1,0,1], "size": 25}
{"id": 143, "name": "aruco_143", "position": [-24.75, -182, 24.75], "normal": [-1,0,1], "size": 25}
] ]
}, },
"Hand": { "Hand": {