From 15bcfe75eba18b91c08aec510dc0f1fd6632a15d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 21 Jul 2026 11:33:09 +0200 Subject: [PATCH] frontend: Mehrsprachige UI (DE/EN/ES/FR) mit Umschalter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprachumschalter oben rechts neben dem Titel. Alle UI-Texte, Tooltips, die Legende und die dynamischen Diagramm-Texte (Status-Tooltips, Geister-Knoten, Warnungen, Leer-Hinweis) laufen über ein I18N-Objekt mit t()/applyLang(); Auswahl wird in localStorage gemerkt, html-lang gesetzt. Deutsch ist Quell- und Default-Sprache. Der Beispiel-Notationstext bleibt unverändert (Nutzerdaten). CLAUDE.md-Konvention entsprechend angepasst. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 4 +- frontend/index.html | 256 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 213 insertions(+), 47 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d7d6173..2aa03d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,9 @@ Integrations-Backend. - Ziele: docs/ROADMAP.md · Offene Arbeit: docs/TASKS.md (Checkboxen pflegen). ## Querschnitts-Konventionen -- UI-Texte und Doku auf Deutsch. +- Doku auf Deutsch. Die Editor-UI ist mehrsprachig (DE/EN/ES/FR, Umschalter + oben rechts); **Deutsch ist die Quellsprache** — neue UI-Texte zuerst auf + Deutsch im `I18N`-Objekt (frontend/index.html) anlegen, dann übersetzen. - Keine neuen Laufzeit-Abhängigkeiten ohne Rückfrage (gilt in beiden Teilen). - Der Notationstext ist das führende Datenformat; kein Teil erfindet ein eigenes Speicherformat für die Struktur. diff --git a/frontend/index.html b/frontend/index.html index 041743c..0178e42 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -32,8 +32,21 @@ padding:24px 20px 12px; } header{max-width:1200px;width:100%;margin:0 auto 16px;flex:0 0 auto} + .header-row{display:flex;align-items:center;justify-content:space-between;gap:16px} h1{font-size:1.25rem;font-weight:600;letter-spacing:-.01em} header p{margin-top:4px;color:var(--muted);font-size:.9rem} + .langsel{ + display:inline-flex;flex:0 0 auto; + border:1px solid rgba(36,52,71,.2);border-radius:999px;overflow:hidden; + } + .langsel button{ + border:0;background:var(--card);cursor:pointer;color:var(--muted); + font-family:'IBM Plex Mono',monospace;font-size:.72rem;letter-spacing:.04em; + padding:5px 10px; + } + .langsel button + button{border-left:1px solid rgba(36,52,71,.14)} + .langsel button:hover:not(.active){color:var(--or)} + .langsel button.active{background:var(--or);color:#fff} .site-footer{ max-width:1200px;margin:28px auto 0; color:var(--muted);font-size:.78rem;text-align:center; @@ -420,36 +433,44 @@
-

- - Werkbaum -

-

Werkbaum – Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree Diagramm-Editor.

+
+

+ + Werkbaum +

+
+ + + + +
+
+

Werkbaum – Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree Diagramm-Editor.

- Struktur (Text) - -
- - - + Struktur (Text) + +
+ + +
- Agenda -
+ Agenda +
Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.
-  Teilpaket, alle erforderlich (all of)
|  Alternative, eine wählen (any of)
@@ -478,36 +499,36 @@
- Diagramm + Diagramm
-
-
- - - +
+ + +
    -
    - - - +
    + + +
    @@ -599,15 +620,13 @@ function visibleChildren(n){ return n.children.filter(k => !k.status || k.status.key !== 'verworfen'); } -const GHOST = '
    Untergliederung fehlt
    '; - function nodeHtml(n, extra){ const need = needsBreakdown(n); const cls = ['node', extra || '', n.status ? 'st-' + n.status.key : ''] .filter(Boolean).join(' '); - const title = n.status ? ` title="${n.status.name}"` : ''; + const title = n.status ? ` title="${esc(t('st_' + n.status.key)).replace(/"/g,'"')}"` : ''; const tagsHtml = n.tags && n.tags.length - ? `${n.tags.map(t => `${esc(t)}`).join('')}` + ? `${n.tags.map(tag => `${esc(tag)}`).join('')}` : ''; const inner = esc(n.label) + (n.url ? '' : '') + @@ -616,7 +635,8 @@ function nodeHtml(n, extra){ const html = n.url ? `${inner}` : `
    ${inner}
    `; - return html + (need ? GHOST : ''); + const ghost = `
    ${esc(t('ghost'))}
    `; + return html + (need ? ghost : ''); } function renderChildren(node, warnings){ @@ -624,7 +644,7 @@ function renderChildren(node, warnings){ if(!kids.length) return ''; const types = new Set(kids.map(k => k.type)); if(types.size > 1){ - warnings.push(`Zeile ${kids[0].line}: Unter „${esc(node.label)}“ sind - und | gemischt — dargestellt nach dem ersten Kind.`); + warnings.push(t('mixedWarn', {line: kids[0].line, label: esc(node.label)})); } const gate = gateOf(kids); const items = kids.map(k => { @@ -646,7 +666,7 @@ function render(){ } if(!roots.length){ - out.innerHTML = '
    Noch keine Struktur — einfach lostippen.
    '; + out.innerHTML = `
    ${esc(t('empty'))}
    `; warnBox.innerHTML = ''; return; } @@ -808,14 +828,158 @@ copyBtn.addEventListener('click', async () => { src.setSelectionRange(0, 0); src.blur(); } - copyBtn.textContent = 'kopiert ✓'; + copyBtn.textContent = t('copyDone'); copyBtn.classList.add('done'); - setTimeout(() => { copyBtn.textContent = 'kopieren'; copyBtn.classList.remove('done'); }, 1500); + setTimeout(() => { copyBtn.textContent = t('copy'); copyBtn.classList.remove('done'); }, 1500); +}); + +/* ---------- Internationalisierung (DE/EN/ES/FR) ---------- */ +const I18N = { + de: { + subtitle:"Werkbaum – Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree Diagramm-Editor.", + editorTitle:"Struktur (Text)", diagramTitle:"Diagramm", + copy:"kopieren", copyDone:"kopiert ✓", copyTitle:"Text in die Zwischenablage kopieren", + minimize:"minimieren", normal:"normal", maximize:"maximieren", + agenda:"Agenda", discarded:"verworfene", + modeHorizontal:"Horizontal – Organigramm, Diagramm über dem Editor", + modeKompakt:"Kompakt – alles nach unten, platzsparend", + modeVertikal:"Vertikal – Baum nach rechts, Diagramm neben dem Editor", + zoomOut:"verkleinern", zoomReset:"zurücksetzen", zoomIn:"vergrößern", + zoomAria:"Zoom (Strg/Cmd + Mausrad)", + empty:"Noch keine Struktur — einfach lostippen.", ghost:"Untergliederung fehlt", + mixedWarn:"Zeile {line}: Unter „{label}“ sind - und | gemischt — dargestellt nach dem ersten Kind.", + st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich", + st_fertig:"fertig", st_prod:"in Produktion", st_verworfen:"verworfen", + hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.", + hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen", + hint_root:"Zeile ohne Zeichen = Wurzelknoten. Geschwister sollten dasselbe Zeichen tragen.", + hint_status:"Status als Kästchen nach dem Zeichen, z. B.", + hint_size:"Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:", + hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.", + hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.", + hint_people:"Personen mit @name — erscheinen unten rechts am Knoten." + }, + en: { + subtitle:"Werkbaum – A text-based WBS / work breakdown structure / feature-tree diagram editor.", + editorTitle:"Structure (text)", diagramTitle:"Diagram", + copy:"copy", copyDone:"copied ✓", copyTitle:"Copy text to clipboard", + minimize:"minimize", normal:"normal", maximize:"maximize", + agenda:"Legend", discarded:"discarded", + modeHorizontal:"Horizontal – org chart, diagram above the editor", + modeKompakt:"Compact – everything downward, space-saving", + modeVertikal:"Vertical – tree to the right, diagram beside the editor", + zoomOut:"zoom out", zoomReset:"reset", zoomIn:"zoom in", + zoomAria:"Zoom (Ctrl/Cmd + mouse wheel)", + empty:"No structure yet — just start typing.", ghost:"Breakdown missing", + mixedWarn:"Line {line}: under “{label}”, - and | are mixed — rendered by the first child.", + st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton", + st_fertig:"done", st_prod:"in production", st_verworfen:"discarded", + hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.", + hint_all:"sub-task, all required", hint_any:"alternative, choose one", + hint_root:"Line without a marker = root node. Siblings should share the same marker.", + hint_status:"Status as a checkbox after the marker, e.g.", + hint_size:"Effort as a T-shirt size in parentheses; add a link simply as a URL:", + hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.", + hint_comment:"Comments with %% — whole line or at the end of a line.", + hint_people:"People with @name — shown at the bottom-right of the node." + }, + es: { + subtitle:"Werkbaum – Un editor de diagramas basado en texto para EDT / estructura de desglose del trabajo / árbol de características.", + editorTitle:"Estructura (texto)", diagramTitle:"Diagrama", + copy:"copiar", copyDone:"copiado ✓", copyTitle:"Copiar el texto al portapapeles", + minimize:"minimizar", normal:"normal", maximize:"maximizar", + agenda:"Leyenda", discarded:"descartados", + modeHorizontal:"Horizontal – organigrama, diagrama sobre el editor", + modeKompakt:"Compacto – todo hacia abajo, ahorra espacio", + modeVertikal:"Vertical – árbol hacia la derecha, diagrama junto al editor", + zoomOut:"alejar", zoomReset:"restablecer", zoomIn:"acercar", + zoomAria:"Zoom (Ctrl/Cmd + rueda del ratón)", + empty:"Aún no hay estructura — simplemente empieza a escribir.", ghost:"Falta el desglose", + mixedWarn:"Línea {line}: bajo «{label}» se mezclan - y | — se representa según el primer hijo.", + st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional", + st_fertig:"terminado", st_prod:"en producción", st_verworfen:"descartado", + hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.", + hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una", + hint_root:"Línea sin marcador = nodo raíz. Los hermanos deberían llevar el mismo marcador.", + hint_status:"Estado como casilla tras el marcador, p. ej.", + hint_size:"Esfuerzo como talla de camiseta entre paréntesis; añade un enlace simplemente como URL:", + hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.", + hint_comment:"Comentarios con %% — línea completa o al final de la línea.", + hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo." + }, + fr: { + subtitle:"Werkbaum – Un éditeur de diagrammes basé sur le texte pour WBS / organigramme des tâches / arbre de fonctionnalités.", + editorTitle:"Structure (texte)", diagramTitle:"Diagramme", + copy:"copier", copyDone:"copié ✓", copyTitle:"Copier le texte dans le presse-papiers", + minimize:"réduire", normal:"normal", maximize:"agrandir", + agenda:"Légende", discarded:"abandonnés", + modeHorizontal:"Horizontal – organigramme, diagramme au-dessus de l'éditeur", + modeKompakt:"Compact – tout vers le bas, gain de place", + modeVertikal:"Vertical – arbre vers la droite, diagramme à côté de l'éditeur", + zoomOut:"dézoomer", zoomReset:"réinitialiser", zoomIn:"zoomer", + zoomAria:"Zoom (Ctrl/Cmd + molette)", + empty:"Pas encore de structure — commencez à taper.", ghost:"Décomposition manquante", + mixedWarn:"Ligne {line} : sous « {label} », - et | sont mélangés — rendu selon le premier enfant.", + st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel", + st_fertig:"terminé", st_prod:"en production", st_verworfen:"abandonné", + hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.", + hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une", + hint_root:"Ligne sans marqueur = nœud racine. Les frères devraient porter le même marqueur.", + hint_status:"Statut sous forme de case après le marqueur, p. ex.", + hint_size:"Effort en taille de T-shirt entre parenthèses ; ajoutez un lien simplement comme URL :", + hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.", + hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.", + hint_people:"Personnes avec @nom — affichées en bas à droite du nœud." + } +}; +let lang = 'de'; +function t(key, vars){ + let s = (I18N[lang] && I18N[lang][key]) ?? I18N.de[key] ?? key; + if(vars) for(const k in vars) s = s.split('{'+k+'}').join(vars[k]); + return s; +} +function buildHint(){ + const chip = (key, code) => `${code} ${esc(t('st_'+key))}`; + return `${esc(t('hint_indent'))}
    + -  ${esc(t('hint_all'))} (all of)
    + |  ${esc(t('hint_any'))} (any of)
    + ${esc(t('hint_root'))}
    + ${esc(t('hint_status'))} - [~] Frontend: +
    + ${chip('idee','[?]')} + ${chip('geplant','[ ]')} + ${chip('arbeit','[~]')} + ${chip('durchstich','[/]')} + ${chip('fertig','[x]')} + ${chip('prod','[^]')} + ${chip('verworfen','[-]')} +
    + ${esc(t('hint_size'))}
    + - [ ] Backend (L) https://…
    + ${esc(t('hint_break'))}
    + ${esc(t('hint_comment'))} + ${esc(t('hint_people'))}`; +} +function applyLang(l){ + lang = l; + document.documentElement.lang = l; + document.querySelectorAll('[data-i18n]').forEach(el => el.textContent = t(el.dataset.i18n)); + document.querySelectorAll('[data-i18n-title]').forEach(el => el.title = t(el.dataset.i18nTitle)); + document.querySelectorAll('[data-i18n-aria]').forEach(el => el.setAttribute('aria-label', t(el.dataset.i18nAria))); + document.getElementById('hint').innerHTML = buildHint(); + document.querySelectorAll('.langsel button').forEach(b => b.classList.toggle('active', b.dataset.lang===l)); + render(); + try{ localStorage.setItem('werkbaum-lang', l); }catch(_){} +} +document.querySelectorAll('.langsel button').forEach(b => { + b.addEventListener('click', () => applyLang(b.dataset.lang)); }); applySplit(); /* Startzustand: normal, markiert die normal-Buttons aktiv */ src.value = INITIAL; -render(); +let startLang = 'de'; +try{ startLang = localStorage.getItem('werkbaum-lang') || 'de'; }catch(_){} +applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */