Vollstaendiger Durchlauf ueber alle Pruefungen. Dabei kamen fuenf Dinge ans Licht, die teils seit Wochen offen standen. 1. WCAG-KONTRAST -- der einzige Punkt, der echte Besucher betraf Die lila Schilder erreichten nur 4,08:1, verlangt sind 4,5:1 fuer normalen Text. 17 Fundstellen, alle dieselbe Ursache: Die Schrift nutzte den vollen Ton Aurora Violet. Gemessen wurde nicht geschaetzt: Untergrund rgb(38,41,81) aus dem echten Bild ausgelesen, Schrift 12,5 Punkte -- und fett zaehlt erst ab 18,5 Punkten als "grosser Text". Neu ist --wd-lila-hell (#C197FF, 6,02:1). Bewusst NICHT der knappste Wert: #B380FF haette mit 4,91:1 gereicht, aber dasselbe Schild steht auch ueber der Buehne im Verwaltungsbereich. Ein Ton, der nur an einer Stelle knapp besteht, faellt beim naechsten Hintergrund wieder durch. Dasselbe Muster wie --wd-blau-hell, das genau deshalb existiert. 2. GEHEIMNIS-TEST -- der Test war veraltet, nicht der Code Er verlangte ".env hat Vorrang", die Umsetzung macht das Gegenteil. Die Begruendung im Code ueberzeugt: Wer PayPal-Daten im Formular eintraegt, erwartet, dass sie gelten. Andernfalls koennte ein alter Wert in der .env sie stumm ueberstimmen -- und man sucht stundenlang. Der Test prueft jetzt die tatsaechliche Reihenfolge, dazu neu, dass ein gleichzeitig vorhandener Serverwert auch angezeigt wird. Ausserdem endete der Lauf trotz gruener Pruefungen mit einer Fehlermeldung: Unter Windows haelt eine offene SQLite-Datei eine Sperre, das Aufraeumen scheiterte mit EPERM. Auf Linux waere es durchgelaufen -- dasselbe Skript mit unterschiedlichem Ergebnis je Rechner. Jetzt wird erst geschlossen, dann geloescht, und das Aufraeumen kann den Lauf nicht mehr zum Scheitern bringen. 3. PORTAL-TEST -- ebenfalls veraltet Er suchte "1500,00" und schlug fehl, seit der Formatierer den Tausenderpunkt setzt. "1.500,00 EUR" ist die korrekte deutsche Schreibweise; gerade ab vier Stellen macht der Punkt eine Zahl auf einen Blick lesbar. 4. ZWEI SEITEN OHNE WOERTERBUCH -- eine Entscheidung, keine Luecke "diagnose" ist ein Betriebswerkzeug. "rechtliches" ist der heiklere Fall: Rechtstexte durch eine ungepruefte Uebersetzung zu schicken ist gefaehrlicher, als sie einsprachig zu lassen. Ein Fehler in einer Widerrufsbelehrung wirkt gegen den Verfasser. Die Pruefung meldete beides als "Datei fehlt" -- das las sich wie ein Versehen und stand deshalb dauerhaft in der Fehlerliste, ohne dass jemand etwas tat. Jetzt sind beide benannt und begruendet. 5. TAG-UNGLEICHGEWICHT -- eine Fehlmessung Die Pruefung zaehlte auch HTML-Schnipsel, die als Zeichenketten im JavaScript stehen. Dort steht ein oeffnendes <div> regelmaessig in einer anderen Zeichenkette als sein </div>, weil die Teile erst beim Zusammensetzen ein Ganzes ergeben. Gegengeprueft im Browser: geparster Baum einwandfrei, kein Skriptfehler, Verschachtelung unauffaellig. Es war nie ein Strukturfehler. Die Meldung stand aber monatelang als "2 Fehler" da und haette jede echte Meldung entwertet, die dazugekommen waere. STAND Browser 717 Pruefungen 0 offen Server 566 Pruefungen 0 offen i18n keine Fehler WCAG 0 Fundstellen (vorher 17) Co-Authored-By: Claude Opus 5 <[email protected]>
186 lines
8.8 KiB
JavaScript
186 lines
8.8 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. */
|
|
const NUR_DEUTSCH = ["verwaltung", "diagnose", "rechtliches"];
|
|
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);
|