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]>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/* =====================================================================
|
||||
Prueft die Rechtstexte in fuenf Sprachen.
|
||||
|
||||
Der wichtigste Test hier ist NICHT "ist alles uebersetzt", sondern:
|
||||
Steht unter jedem Schluessel auch der Text, der im HTML an dieser
|
||||
Stelle steht?
|
||||
|
||||
Beim Bauen waren die Schluessel ab einer Stelle um eins verschoben,
|
||||
weil zwei Bloecke uebersprungen worden waren. Im Musterformular haette
|
||||
dadurch ueber jedem Feld die falsche Beschriftung gestanden -- "Datum"
|
||||
wo "Name" hingehoert. In fuenf Sprachen waere das niemandem
|
||||
aufgefallen, denn jede Sprache war ja vollstaendig gefuellt.
|
||||
|
||||
Gegen diese Art Fehler ist eine Vollstaendigkeitspruefung wehrlos.
|
||||
Deshalb wird hier der deutsche Text im Woerterbuch mit dem im HTML
|
||||
verglichen -- Zeichen fuer Zeichen.
|
||||
===================================================================== */
|
||||
import { chromium } from "playwright";
|
||||
import http from "http";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const TYP = {
|
||||
".html": "text/html", ".css": "text/css", ".js": "text/javascript",
|
||||
".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp",
|
||||
".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg",
|
||||
};
|
||||
|
||||
const srv = http.createServer((q, r) => {
|
||||
const p = path.join(process.cwd(), decodeURIComponent(q.url.split("?")[0]));
|
||||
if (fs.existsSync(p) && !p.endsWith("/")) {
|
||||
r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" });
|
||||
return r.end(fs.readFileSync(p));
|
||||
}
|
||||
r.writeHead(404);
|
||||
r.end("");
|
||||
});
|
||||
|
||||
let ok = 0, fehl = 0;
|
||||
const pruefe = (name, gut, info) => {
|
||||
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
|
||||
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
|
||||
};
|
||||
|
||||
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
|
||||
|
||||
srv.listen(4406, async () => {
|
||||
const browser = await chromium.launch();
|
||||
const seite = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
const skriptfehler = [];
|
||||
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140)));
|
||||
await seite.goto("http://127.0.0.1:4406/webdesign/rechtliches.html");
|
||||
await seite.waitForTimeout(1600);
|
||||
|
||||
console.log("1. LIEGT UNTER JEDEM SCHLUESSEL DER RICHTIGE TEXT?");
|
||||
const versatz = await seite.evaluate(() => {
|
||||
const W = window.I18N_WD || {};
|
||||
/* HTML-Entitaeten aufloesen, bevor verglichen wird.
|
||||
|
||||
Im Woerterbuch steht "&", im Browser liest textContent daraus
|
||||
ein "&". Ohne dieses Aufloesen meldet der Test eine Abweichung,
|
||||
wo in Wahrheit derselbe Text steht -- genau bei der Ueberschrift
|
||||
"Impressum, AGB & Datenschutz". */
|
||||
const entschluesseln = (t) => {
|
||||
const hilf = document.createElement("textarea");
|
||||
hilf.innerHTML = t;
|
||||
return hilf.value;
|
||||
};
|
||||
const sauber = (t) => entschluesseln(t).replace(/\s+/g, " ").trim();
|
||||
const raus = [];
|
||||
for (const el of document.querySelectorAll("[data-i18n^='rl_']")) {
|
||||
const k = el.getAttribute("data-i18n");
|
||||
if (!W[k]) { raus.push({ k, grund: "kein Eintrag", html: sauber(el.textContent).slice(0, 50) }); continue; }
|
||||
const imBuch = sauber(W[k].de.replace(/<[^>]*>/g, " "));
|
||||
const imHtml = sauber(el.textContent);
|
||||
if (imBuch.slice(0, 45) !== imHtml.slice(0, 45)) {
|
||||
raus.push({ k, grund: "Text weicht ab", html: imHtml.slice(0, 45), buch: imBuch.slice(0, 45) });
|
||||
}
|
||||
}
|
||||
return raus;
|
||||
});
|
||||
pruefe("jeder Schluessel traegt seinen eigenen Text",
|
||||
versatz.length === 0,
|
||||
versatz.length ? versatz.slice(0, 3).map((v) => v.k + " (" + v.grund + ")").join(", ")
|
||||
: "alle 95 stimmen");
|
||||
|
||||
console.log("\n2. GREIFT DIE UEBERSETZUNG IN JEDER SPRACHE?");
|
||||
const proben = { rl_001: "Titel", rl_003: "Sprachhinweis", rl_047: "Widerrufsrecht" };
|
||||
const gesehen = {};
|
||||
for (const spr of SPRACHEN) {
|
||||
const r = await seite.evaluate((p) => {
|
||||
window.WD.setSprache(p);
|
||||
const hole = (k) => document.querySelector(`[data-i18n="${k}"]`)?.textContent.trim() || "";
|
||||
return {
|
||||
lang: document.documentElement.lang,
|
||||
rl_001: hole("rl_001"),
|
||||
rl_003: hole("rl_003"),
|
||||
rl_047: hole("rl_047"),
|
||||
leer: [...document.querySelectorAll("[data-i18n^='rl_']")]
|
||||
.filter((el) => !el.textContent.trim()).length,
|
||||
};
|
||||
}, spr);
|
||||
gesehen[spr] = r;
|
||||
pruefe(` ${spr}: lang-Attribut folgt`, r.lang === spr, r.lang);
|
||||
pruefe(` ${spr}: kein Textblock ist leer`, r.leer === 0, r.leer + " leer");
|
||||
}
|
||||
|
||||
/* Gegenprobe: Der Test darf nicht gruen sein, weil die Umschaltung
|
||||
gar nichts tut. Die drei Fremdsprachen MUESSEN sich vom Deutschen
|
||||
unterscheiden. */
|
||||
for (const spr of ["en", "fr", "pt"]) {
|
||||
pruefe(` ${spr} unterscheidet sich wirklich vom Deutschen`,
|
||||
gesehen[spr].rl_003 !== gesehen.de.rl_003,
|
||||
gesehen[spr].rl_003.slice(0, 38));
|
||||
}
|
||||
|
||||
/* de-CH ist bei Rechtstexten ABSICHTLICH kein Dialekt, sondern
|
||||
Hochdeutsch in Schweizer Rechtschreibung -- also identisch bis auf
|
||||
das Eszett. Eine Widerrufsbelehrung auf Mundart waere unserioes. */
|
||||
console.log("\n3. SCHWEIZ: HOCHDEUTSCH OHNE ESZETT, KEIN DIALEKT");
|
||||
const ch = await seite.evaluate(() => {
|
||||
window.WD.setSprache("de-CH");
|
||||
const W = window.I18N_WD;
|
||||
let mitEszett = 0, abweichend = 0;
|
||||
for (const v of Object.values(W)) {
|
||||
if (/ß/.test(v["de-CH"])) mitEszett++;
|
||||
if (v["de-CH"] !== v.de.replace(/ß/g, "ss")) abweichend++;
|
||||
}
|
||||
return { mitEszett, abweichend };
|
||||
});
|
||||
pruefe("kein Eszett in der Schweizer Fassung", ch.mitEszett === 0, ch.mitEszett + " Stellen");
|
||||
pruefe("sonst wortgleich mit dem Deutschen", ch.abweichend === 0, ch.abweichend + " Abweichungen");
|
||||
|
||||
console.log("\n4. DIE MASSGEBLICHKEITSKLAUSEL STEHT IN JEDER SPRACHE");
|
||||
/* Ohne diesen Hinweis waere die Uebersetzung ein Risiko: Ein Kunde
|
||||
koennte sich auf die franzoesische Fassung berufen. */
|
||||
const klausel = await seite.evaluate(() => {
|
||||
const W = window.I18N_WD;
|
||||
const muster = {
|
||||
de: /deutsch/i, "de-CH": /deutsch/i, en: /german/i,
|
||||
fr: /allemand/i, pt: /alem/i,
|
||||
};
|
||||
const raus = {};
|
||||
for (const [spr, m] of Object.entries(muster)) {
|
||||
raus[spr] = m.test(W.rl_003[spr]) && m.test(W.rl_004[spr]);
|
||||
}
|
||||
return raus;
|
||||
});
|
||||
for (const [spr, gut] of Object.entries(klausel)) {
|
||||
pruefe(` ${spr}: nennt die deutsche Fassung als verbindlich`, gut);
|
||||
}
|
||||
|
||||
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "");
|
||||
|
||||
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
|
||||
await browser.close();
|
||||
srv.close();
|
||||
process.exit(fehl ? 1 : 0);
|
||||
});
|
||||
Reference in New Issue
Block a user