/* Prüft für jede Webdesign-Seite: 1. jeder data-i18n-Schlüssel existiert im Wörterbuch 2. jeder Schlüssel hat ALLE fünf Sprachen 3. keine Überschrift h1-h4 ohne data-i18n (der dokumentierte Bug-Klassiker aus DogiHompage/CLAUDE.md) 4. keine unbenutzten Schlüssel (Hinweis, kein Fehler) Aufruf: node tmp-i18n-check.mjs */ import { readFileSync, existsSync, readdirSync } from "node:fs"; import { chdir } from "node:process"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; /* Immer vom Projektordner aus arbeiten, egal von wo aufgerufen. */ chdir(join(dirname(fileURLToPath(import.meta.url)), "..")); const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"]; const KERN = "assets/js/wd-core.js"; /* Schlüssel, die wd-core.js selbst mitbringt (Navigation, Fußzeile, Fehlermeldungen) — die dürfen in jeder Seite benutzt werden. */ const kernQuelle = readFileSync(KERN, "utf8"); const kernSchluessel = new Set( [...kernQuelle.matchAll(/^\s{4}([a-z0-9_]+):\s*\{/gm)].map((m) => m[1]) ); let fehler = 0; const seiten = existsSync("webdesign") ? readdirSync("webdesign").filter((f) => f.endsWith(".html")) : []; for (const datei of seiten) { const name = datei.replace(/\.html$/, ""); const html = readFileSync(`webdesign/${datei}`, "utf8"); const i18nDatei = `assets/js/i18n-wd-${name}.js`; console.log(`\n--- ${datei} ---`); /* Bewusst einsprachige Seiten. Der öffentliche Teil läuft in fünf Sprachen, weil dort Kunden landen. In der Verwaltung landen ausschließlich Dogfather und VanVan — fünf Sprachen wären dort fünffache Pflege und fünffache Fehlerfläche ohne einen einzigen Nutzer, der sie braucht. Genau wie der bestehende interne Bereich des Universe, der ebenfalls nur Deutsch spricht. Die Tag-Balance wird trotzdem geprüft (siehe unten). */ /* NUR die Verwaltung ist einsprachig. Das Kundenportal stand hier zunaechst auch drin -- das war ein Denkfehler: die Verwaltung nutzen Dogfather und VanVan, das Portal nutzen KUNDEN. Und Kunden koennen Franzosen oder Portugiesen sein. Ein Portal, das die Sprachwahl der oeffentlichen Seite ignoriert, waere genau der Bruch, den ein Kunde sofort bemerkt. */ const NUR_DEUTSCH = ["verwaltung"]; const einsprachig = NUR_DEUTSCH.includes(name); if (!existsSync(i18nDatei) && !einsprachig) { console.log(` FEHLER: ${i18nDatei} fehlt`); fehler++; continue; } if (einsprachig) { console.log(" bewusst nur Deutsch (interner Bereich) — nur Struktur geprueft"); for (const tag of ["div", "section", "main", "article", "details", "form", "button"]) { const auf = (html.match(new RegExp(`<${tag}\\b`, "g")) || []).length; const zu = (html.match(new RegExp(``, "g")) || []).length; if (auf !== zu) { console.log(` TAG-UNGLEICHGEWICHT <${tag}>: ${auf} offen, ${zu} geschlossen`); fehler++; } } continue; } // Wörterbuch laden (setzt window.I18N_WD) global.window = {}; // eslint-disable-next-line no-eval eval(readFileSync(i18nDatei, "utf8")); const woerterbuch = global.window.I18N_WD || {}; // --- 1+2: benutzte Schlüssel prüfen --- const benutzt = new Set(); for (const m of html.matchAll(/data-i18n="([^"]+)"/g)) benutzt.add(m[1]); for (const m of html.matchAll(/data-i18n-attr="([^"]+)"/g)) { m[1].split(";").forEach((teil) => { const pos = teil.indexOf(":"); if (pos > 0) benutzt.add(teil.slice(pos + 1).trim()); }); } const titelKey = (html.match(/data-i18n-titel="([^"]+)"/) || [])[1]; if (titelKey) benutzt.add(titelKey); // Schlüssel, die nur per WD.t(...) geholt werden — im Seitenskript // unten in der HTML-Datei UND in einer zugehörigen assets/js/wd-.js. // Ohne den zweiten Teil meldet die Prüfung reihenweise "unbenutzt" für // Schlüssel, die sehr wohl benutzt werden — nur eben aus einer anderen // Datei (aufgefallen beim Anfrageformular, 22.08.2026). const quellen = [html]; const seitenSkript = `assets/js/wd-${name}.js`; if (existsSync(seitenSkript)) quellen.push(readFileSync(seitenSkript, "utf8")); for (const quelle of quellen) { for (const m of quelle.matchAll(/WD\.t\("([a-z0-9_]+)"\)/g)) benutzt.add(m[1]); /* Schlüssel aus Nachschlagetabellen, z.B. { ja: "af3_ja", ... }. Der Unterstrich im Muster ist wichtig: ohne ihn hielt die Prüfung auch gewöhnliche Wörter wie "abnahme" oder "uebergeben" (aus der Phasenliste im Portal) für Wörterbuch-Schlüssel und meldete sie als fehlend. Alle echten Schlüssel haben die Form praefix_name. */ for (const m of quelle.matchAll(/["']([a-z]{2}\d?_[a-z0-9_]+)["']\s*[,}\]]/g)) { benutzt.add(m[1]); } } const fehlend = []; const unvollstaendig = []; for (const k of benutzt) { const eintrag = woerterbuch[k]; if (!eintrag) { if (!kernSchluessel.has(k)) fehlend.push(k); continue; } for (const s of SPRACHEN) if (!eintrag[s]) unvollstaendig.push(`${k}:${s}`); } // --- 3: Überschriften ohne data-i18n --- const ohneI18n = []; for (const m of html.matchAll(/<(h[1-4])\b([^>]*)>([\s\S]*?)<\/\1>/g)) { if (m[2].includes("data-i18n")) continue; /* Auch übersetzt, wenn die Übersetzung INNEN sitzt — etwa

Hallo

, wo ein Teil fest und ein Teil dynamisch ist. Ohne diese Ausnahme meldet die Prüfung eine korrekt übersetzte Überschrift als Fehler. */ if (m[3].includes("data-i18n")) continue; const text = m[3].replace(/<[^>]*>/g, "").trim(); if (!text) continue; // rein dekorativ if (/^\$\{/.test(text)) continue; // dynamisch aus JS ohneI18n.push(`${m[1]}: ${text.slice(0, 60)}`); } // --- 4: unbenutzte Schlüssel (nur Hinweis) --- const unbenutzt = Object.keys(woerterbuch).filter((k) => !benutzt.has(k)); console.log(` Schluessel im HTML: ${benutzt.size}, im Woerterbuch: ${Object.keys(woerterbuch).length}`); if (fehlend.length) { console.log(` FEHLENDE Schluessel: ${fehlend.join(", ")}`); fehler += fehlend.length; } if (unvollstaendig.length) { console.log(` FEHLENDE Sprachen: ${unvollstaendig.join(", ")}`); fehler += unvollstaendig.length; } if (ohneI18n.length) { console.log(` Ueberschrift ohne data-i18n:\n ${ohneI18n.join("\n ")}`); fehler += ohneI18n.length; } if (unbenutzt.length) console.log(` Hinweis, unbenutzt: ${unbenutzt.join(", ")}`); if (!fehlend.length && !unvollstaendig.length && !ohneI18n.length) console.log(" alles vollstaendig"); // --- Tag-Balance grob prüfen --- for (const tag of ["div", "section", "main", "article", "details"]) { const auf = (html.match(new RegExp(`<${tag}\\b`, "g")) || []).length; const zu = (html.match(new RegExp(``, "g")) || []).length; if (auf !== zu) { console.log(` TAG-UNGLEICHGEWICHT <${tag}>: ${auf} offen, ${zu} geschlossen`); fehler++; } } } console.log(`\n=== ${fehler === 0 ? "Keine Fehler" : fehler + " Fehler"} ===`); process.exit(fehler === 0 ? 0 : 1);