Masterplan S.10 "Projektanfrage ohne Informationsverlust" umgesetzt.
End-to-End gegen die echte Datenbank belegt: Anfrage A-2608-0002 liegt drin.
38/38 Browsertests, 45/45 Handy-Abnahme.
Aufbau: 4 Schritte + Zusammenfassung + Dankeseite.
- Zwischenspeicherung nach jedem Tastendruck. Geschlossener Tab, leerer
Akku oder versehentliches Zurueck kosten keine Arbeit. Entwuerfe
verfallen nach 30 Tagen -- ein halbes Jahr alter Entwurf verwirrt mehr
als er hilft.
- Folgefragen erscheinen nur passend zur Auswahl und werden beim
Zurueckwechseln NICHT mitgeschickt, sonst landen alte Shop-Antworten in
einer Onepager-Anfrage.
- Die Zusammenfassung wird aus den Feldern gelesen, nicht aus einem
nebenher gepflegten Objekt -- sie kann dadurch nie etwas anderes
behaupten als das, was tatsaechlich abgeschickt wird.
- Telefon wird zur Pflicht, sobald "Per Telefon" gewaehlt ist. Ein Wunsch,
der nicht erfuellbar ist, ist schlimmer als eine Pflichtangabe.
Drei Fehler, die erst der Test gefunden hat:
1. Der "Zurueck"-Knopf stand auch auf Schritt 1 da. Ursache: das
hidden-Attribut wird nur ueber "display:none" umgesetzt und verliert
gegen jedes eigene display -- und Knoepfe sind inline-flex. Zentral
behoben ueber ".wd [hidden] { display:none !important }".
2. Nach erfolgreichem Absenden legte zeigeSeite() den geloeschten Entwurf
sofort wieder an. Beim naechsten Besuch haette eine bereits gesendete
Anfrage erneut im Formular gestanden.
3. Der Server meldet bei Spamverdacht bewusst Erfolg ohne Nummer. Ein
echter Mensch, der zufaellig sehr schnell war, haette eine Dankeseite
mit "—" gesehen und auf eine Antwort gewartet, die nie kommt. Jetzt
ein ehrlicher Hinweis mit zweitem Weg, und der Entwurf bleibt erhalten.
Pruefskripte geschaerft:
- i18n-Pruefung liest jetzt auch assets/js/wd-<seite>.js, sonst meldet sie
reihenweise "unbenutzt" fuer Schluessel, die aus dem Seitenskript kommen.
- Handy-Abnahme ueberspringt absichtlich unsichtbare Eingabefelder
(Auswahlkacheln, Honigtopf). Fehlalarme sind das Ende jeder Pruefung,
weil man sie irgendwann pauschal ignoriert.
Co-Authored-By: Claude Opus 5 <[email protected]>
118 lines
5.0 KiB
JavaScript
118 lines
5.0 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} ---`);
|
|
|
|
if (!existsSync(i18nDatei)) {
|
|
console.log(` FEHLER: ${i18nDatei} fehlt`);
|
|
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", ... }
|
|
for (const m of quelle.matchAll(/["']((?:af|pk|zg|pf|ue|fq|ab|ls|pr|s\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;
|
|
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);
|