Masterplan S.13. Anfragen ansehen, filtern, durchsuchen, Status aendern, interne Notizen, archivieren. 9/9 Sicherheitstests. Bewusste Entscheidungen: * NUR AUF DEUTSCH. Der oeffentliche Teil laeuft in fuenf Sprachen, weil dort Kunden landen. Hier landen ausschliesslich Dogfather und VanVan -- fuenf Sprachen waeren fuenffache Pflege und fuenffache Fehlerflaeche ohne einen einzigen Nutzer, der sie braucht. Genau wie der bestehende interne Bereich des Universe. * Anmeldung ueber die BESTEHENDE Team-Anmeldung (/auth/login) statt einer zweiten eigenen. Zwei Anmeldungen fuer dieselben zwei Personen waeren doppelte Pflege und ein zweiter Ort, an dem ein Zugang vergessen werden kann. Der Code der Zugangswand gilt hier ausdruecklich NICHT -- die Verwaltung steckt hinter zwei getrennten Tueren. * Sitzungstoken im sessionStorage, nicht localStorage: es verschwindet beim Schliessen, dieselbe Regel wie an der Zugangswand. Der Test prueft das ausdruecklich. * Bei abgelaufener Sitzung geht es zurueck zur Anmeldung statt zu einer leeren Liste. Eine leere Liste sieht aus wie "keine Anfragen" und ist damit eine stille Falschaussage. * Gold nur beim Status "neu" -- also genau dort, wo wirklich etwas zu tun ist. Wuerde alles leuchten, leuchtet nichts. * Archivieren statt Loeschen, und die Oberflaeche sagt das auch dazu. Pruefskript: kennt jetzt bewusst einsprachige Seiten (verwaltung, portal) und prueft dort nur die Struktur, statt ein fehlendes Woerterbuch zu melden. Co-Authored-By: Claude Opus 5 <[email protected]>
137 lines
6.0 KiB
JavaScript
137 lines
6.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} ---`);
|
|
|
|
/* 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). */
|
|
const NUR_DEUTSCH = ["verwaltung", "portal"];
|
|
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(`</${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", ... }
|
|
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);
|