Files
dogfather-universe/server/pruefe-webdesign-i18n.mjs
T
DogFatherGitandClaude Opus 5 2e2172202e Rechtstexte in fuenf Sprachen
Impressum, AGB, Widerrufsbelehrung und Datenschutzerklaerung gibt es
jetzt auf Deutsch, Schweizer Hochdeutsch, Englisch, Franzoesisch und
Portugiesisch. 95 Textbloecke, rund 13.000 Zeichen je Sprache.

DIE MASSGEBLICHKEITSKLAUSEL STEHT IN JEDER SPRACHE

Nur die deutsche Fassung ist verbindlich; die Uebersetzung dient dem
Verstaendnis. Ohne diesen Hinweis waere die Uebersetzung ein Risiko --
ein Kunde koennte sich auf die franzoesische Fassung berufen. Der
Hinweis stand schon auf der Seite, aber selbst nur auf Deutsch: Genau
die Leute, fuer die er gedacht ist, konnten ihn nicht lesen.

WIDERRUFSBELEHRUNG UND MUSTERFORMULAR SIND AMTLICH

Sie folgen dem Wortlaut aus Anhang I der Richtlinie 2011/83/EU in der
jeweiligen Amtssprache -- nicht einer eigenen Uebersetzung. Der
Gesetzgeber hat diese Saetze selbst formuliert; wer sie neu uebersetzt,
verliert den Schutz der Musterbelehrung. Angepasst ist nur die Person:
Die Muster sprechen von "wir/uns", hier schreibt ein Einzelunternehmer
"ich/mir" -- diese Anpassung sieht das Muster ausdruecklich vor.

SCHWEIZERDEUTSCH GIBT ES HIER BEWUSST NICHT

Auf allen anderen Seiten spricht "de-CH" echten Dialekt. In
Rechtstexten waere das unserioes -- es gibt keine Widerrufsbelehrung auf
Mundart. Die Schweiz schreibt Hochdeutsch, nur ohne Eszett. Genau das
erzeugt eine Zeile Code aus dem deutschen Text, statt 95 von Hand
gepflegter Doppeleintraege, die beim naechsten Aendern auseinanderlaufen
wuerden.

DER FEHLER, DER FAST LIVE GEGANGEN WAERE

Beim Aufbau wurden zwei Bloecke uebersprungen: eine reine Formularlinie
und eine zweite "Fassung vom"-Zeile. Ab dort war jede Schluesselnummer
um eins verschoben.

Im Musterformular haette dadurch ueber jedem Feld die falsche
Beschriftung gestanden -- "Datum" wo "Name" hingehoert. Und in fuenf
Sprachen waere es niemandem aufgefallen, denn jede Sprache war ja
vollstaendig gefuellt: Eine Vollstaendigkeitspruefung ist gegen diese
Art Fehler wehrlos.

Die Schluessel sind deshalb jetzt ueber den TEXT zugeordnet, nicht ueber
eine laufende Nummer. Ein eigener Durchlauf vergleicht jeden deutschen
Text im Woerterbuch mit dem an derselben Stelle im HTML.

WAS DER NEUE DURCHLAUF SONST PRUEFT

- Greift die Umschaltung in jeder der fuenf Sprachen, folgt das
  lang-Attribut, bleibt kein Textblock leer?
- Gegenprobe, dass die Umschaltung ueberhaupt etwas tut: Englisch,
  Franzoesisch und Portugiesisch MUESSEN sich vom Deutschen
  unterscheiden. Ohne diese Probe waere ein Woerterbuch, das ueberall
  denselben deutschen Text zurueckgibt, gruen durchgelaufen.
- Kein Eszett in der Schweizer Fassung, sonst wortgleich.
- Die Massgeblichkeitsklausel nennt in jeder Sprache die deutsche
  Fassung.

Geprueft: 1303 Pruefungen gruen (Browser 737, Server 566), i18n ohne
Fehler, WCAG ohne Fundstellen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 22:28:52 +02:00

192 lines
9.2 KiB
JavaScript

/* 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. */
/* Zwei weitere Seiten kamen hinzu, beide aus eigenem Grund:
"diagnose" ist ein Werkzeug fuer den Betrieb, bewusst ohne externe
Dateien gebaut. Es landet dort niemand ausser Dogfather selbst.
"rechtliches" ist der heiklere Fall und ausdruecklich eine
ENTSCHEIDUNG, keine Luecke: Rechtstexte durch eine nicht gepruefte
Uebersetzung zu schicken ist gefaehrlicher, als sie einsprachig zu
lassen -- ein Uebersetzungsfehler in einer Widerrufsbelehrung oder
in den Geschaeftsbedingungen wirkt gegen den Verfasser. Wenn die
Seite mehrsprachig werden soll, gehoert die Uebersetzung von
jemandem geprueft, der dafuer geradesteht, nicht aus einem
Woerterbuch nebenbei erzeugt.
Frueher meldete die Pruefung fuer beide schlicht "Datei fehlt".
Das las sich wie ein Versehen und stand deshalb monatelang als
Fehler in der Liste, ohne dass jemand etwas tat. */
/* "rechtliches" ist seit dem 25.08.2026 UEBERSETZT und steht deshalb
nicht mehr hier. Die Rechtstexte gibt es jetzt in allen fuenf
Sprachen, mit der Massgeblichkeitsklausel in jeder davon: Nur die
deutsche Fassung ist verbindlich, die Uebersetzung dient dem
Verstaendnis. Widerrufsbelehrung und Musterformular folgen dem
amtlichen Wortlaut aus Anhang I der Richtlinie 2011/83/EU. */
const NUR_DEUTSCH = ["verwaltung", "diagnose"];
const einsprachig = NUR_DEUTSCH.includes(name);
if (!existsSync(i18nDatei) && !einsprachig) {
console.log(` FEHLER: ${i18nDatei} fehlt`);
fehler++;
continue;
}
if (einsprachig) {
console.log(" bewusst nur Deutsch — nur Struktur geprueft");
/* NUR das echte Markup zaehlen, nicht die Skriptbloecke.
Die Verwaltung baut ihre Listen im Browser zusammen. Im
JavaScript stehen deshalb HTML-Schnipsel als Zeichenketten --
und dort steht ein oeffnendes <div> regelmaessig in einer
anderen Zeichenkette als sein </div>, weil die Teile erst beim
Zusammensetzen ein Ganzes ergeben.
Ohne diesen Ausschluss meldete die Pruefung dauerhaft je ein
div und ein button zu viel. Gegengeprueft im Browser: Der
geparste Baum ist einwandfrei, es gibt keinen Skriptfehler und
die Verschachtelungstiefe ist unauffaellig. Es war also nie ein
Strukturfehler, sondern eine Fehlmessung -- die aber monatelang
als "2 Fehler" in der Liste stand und damit jede echte Meldung
entwertet hat, die dazugekommen waere. */
const nurMarkup = html.replace(/<script\b[\s\S]*?<\/script>/gi, "");
for (const tag of ["div", "section", "main", "article", "details", "form", "button"]) {
const auf = (nurMarkup.match(new RegExp(`<${tag}\\b`, "g")) || []).length;
const zu = (nurMarkup.match(new RegExp(`</${tag}>`, "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-<name>.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
<h1><span data-i18n="po_hallo">Hallo</span> <span id="name"></span></h1>,
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(`</${tag}>`, "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);