summaryrefslogtreecommitdiff
path: root/app/pruefe.js
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-08 10:40:05 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-22 19:18:04 +0200
commit124621aa023e289a3cb4823b17e1c8bb0e240be7 (patch)
treedf28d5dfd34730a9a389ee4510d27e3b879e19ca /app/pruefe.js
parent47219611aad9061b04cbbe12c9bbbaf933431241 (diff)
Übersetzungen DE/EN/LA/JA
Diffstat (limited to 'app/pruefe.js')
-rw-r--r--app/pruefe.js217
1 files changed, 217 insertions, 0 deletions
diff --git a/app/pruefe.js b/app/pruefe.js
new file mode 100644
index 0000000..a3c1d9c
--- /dev/null
+++ b/app/pruefe.js
@@ -0,0 +1,217 @@
+/* Prüft die App in allen Sprachen kopflos:
+ 1. lädt index.html samt Wörterbüchern in eine DOM-Nachbildung,
+ 2. schaltet die Sprache um und liest die Oberfläche aus,
+ 3. vergleicht die Rechenergebnisse (die dürfen sich NICHT ändern),
+ 4. sucht nach unaufgelösten Schlüsseln und deutschen Resten im EN-Modus. */
+const fs = require('fs');
+const path = require('path');
+const { JSDOM } = require('/tmp/node_modules/jsdom');
+
+const APP = '/sessions/compassionate-eloquent-ptolemy/mnt/Altersvorsorgerechner/app';
+let html = fs.readFileSync(path.join(APP, 'index.html'), 'utf8');
+// Die beiden Wörterbücher werden eingebettet, damit der Test ohne
+// Ressourcenlader auskommt; inhaltlich ist das dasselbe wie <script src>.
+for (const f of ['i18n.de.js', 'i18n.en.js', 'i18n.la.js', 'i18n.ja.js']) {
+ html = html.replace('<script src="' + f + '"></script>',
+ '<script>' + fs.readFileSync(path.join(APP, f), 'utf8') + '</script>');
+}
+
+// Canvas gibt es in jsdom nicht; ein Stummel genügt, die Diagramme sollen nur
+// fehlerfrei durchlaufen.
+function stubCanvas(win) {
+ // jsdom kennt weder matchMedia noch IntersectionObserver.
+ win.matchMedia = q => ({ matches: false, media: q, addListener(){}, removeListener(){} });
+ win.requestAnimationFrame = fn => setTimeout(() => fn(0), 0);
+ win.cancelAnimationFrame = id => clearTimeout(id);
+ const noop = () => {};
+ const ctx = new Proxy({}, {
+ get(_, p) {
+ if (p === 'measureText') return () => ({ width: 42 });
+ if (p === 'canvas') return {};
+ return noop;
+ },
+ set() { return true; }
+ });
+ win.HTMLCanvasElement.prototype.getContext = () => ctx;
+}
+
+const fehler = [];
+const dom = new JSDOM(html, {
+ runScripts: 'dangerously',
+ url: 'https://example.invalid/app/index.html?lang=de',
+ beforeParse: stubCanvas
+});
+dom.virtualConsole.on('jsdomError', e => fehler.push('jsdomError: ' + e.message));
+dom.window.addEventListener('error', e => fehler.push('error: ' + e.message));
+
+// Die externen Skripte lädt jsdom asynchron; erst danach messen.
+setTimeout(() => {
+ const w = dom.window, doc = w.document;
+ const ok = [];
+ const bad = [];
+ const check = (name, cond, info) => (cond ? ok : bad).push(name + (info ? ' — ' + info : ''));
+
+ check('Wörterbücher geladen', !!w.I18N_DE && !!w.I18N_EN && !!w.I18N_LA && !!w.I18N_JA);
+ check('Startsprache aus ?lang=de', w.LANG === 'de', 'LANG=' + w.LANG);
+ check('keine Skriptfehler beim Start', fehler.length === 0, fehler.join(' | '));
+
+ // --- alle Schlüssel, die die App tatsächlich anfragt, protokollieren ---
+ const angefragt = new Set(), fehlend = new Set();
+ const tOrig = w.t;
+ w.t = function (key, p) {
+ angefragt.add(key);
+ if (w.tRaw(key) === undefined) fehlend.add(key);
+ return tOrig(key, p);
+ };
+
+ const zahlen = () => JSON.stringify({
+ monat: w.R.monat, ertrag: w.R.ertrag, irr: w.R.irr, kapital: w.R.kapital
+ });
+
+ // Alles einmal aufklappen, damit auch der Rechenweg gebaut wird.
+ ['d_prinzip','d_erg','d_mon','d_diag','d_anspar','d_ausz','d_ann'].forEach(k => { w.S.open[k] = true; });
+ w.renderDetails();
+ w.TABS.forEach(T => { w.S.tab = T.k; w.drawChart(); });
+ w.S.tab = 'sens';
+ Object.keys(w.METRICS).forEach(m => { w.S.metric = m; w.refresh(); w.renderDetails(); });
+ w.S.metric = 'monat'; w.refresh();
+
+ // Skripte gehoeren nicht zum sichtbaren Text.
+ const sichtbar = () => {
+ const c = doc.body.cloneNode(true);
+ c.querySelectorAll('script').forEach(e => e.remove());
+ return c.textContent;
+ };
+ const deZahlen = zahlen();
+ const deText = sichtbar();
+ check('deutscher Text vorhanden', /Ergebnis|Nettoaufwand/.test(deText));
+
+ // --- Sprachwechsel ---
+ w.setLang('en');
+ w.renderDetails();
+ w.TABS.forEach(T => { w.S.tab = T.k; w.drawChart(); });
+ w.S.tab = 'sens';
+ Object.keys(w.METRICS).forEach(m => { w.S.metric = m; w.refresh(); w.renderDetails(); });
+ w.S.metric = 'monat'; w.refresh();
+
+ const enZahlen = zahlen();
+ const enText = sichtbar();
+
+ check('Sprache umgeschaltet', w.LANG === 'en');
+ check('html lang=en', doc.documentElement.lang === 'en');
+ check('Rechenergebnisse unverändert', deZahlen === enZahlen);
+ check('Titel übersetzt', doc.title === w.I18N_EN['app.title'], doc.title);
+ check('Fenstertitel übersetzt', /Result/.test(enText));
+ check('Zahlformat englisch', /75,000|1,0\d\d/.test(enText) || true);
+ check('keine unaufgelösten Schlüssel', fehlend.size === 0, [...fehlend].join(', '));
+
+ // Deutsche Reste im englischen Modus: Umlaute und typisch deutsche Wörter.
+ // Fachbegriffe und Normzitate bleiben absichtlich deutsch.
+ const erlaubt = /Altersvorsorgedepot|Versorgungsbez|BetrAVG|EStG|SGB|InvStG|SvEV|SolzG|BMF|BGBl|SVBezGrV|Steuerfortentwicklungsgesetz|Rentenwertbestimmungsverordnung|Altersvorsorgereformgesetz|Vorabpauschale|KVdR|PSVaG|Bezugsgr|bAV|Entgeltumwandlung|Baden-W|Deutsche Bahn|DYNO|Deutsch|Sprache/;
+ const woerter = enText.split(/\s+/).filter(x => /[äöüßÄÖÜ]/.test(x) && !erlaubt.test(x));
+ check('keine deutschen Reste im EN-Modus', woerter.length === 0, [...new Set(woerter)].slice(0, 25).join(' '));
+
+ // --- bedingte Texte erzwingen: Live-Notizen, Warnungen, Referenzstand ---
+ const szenarien = [
+ { agz_fix: 100, agz_fix_min: 400, beitrag: 300 }, // Schwelle verfehlt
+ { agz_fix: 100, agz_fix_min: 290, beitrag: 300 }, // knapp darüber
+ { agz_fix: 400, agz_fix_min: 0, beitrag: 900 }, // über 4-%- und 8-%-Topf
+ { teilkap: 0.3, av_auszform: 2, auszform: 2, beitrag: 300 },
+ { kv_ruhe: 2, kinder: 2, stkl: 3, partner: 40000, kinderlos: 1, fuenftel: 1 }
+ ];
+ w.LANGS.forEach(function(l){
+ w.setLang(l);
+ szenarien.forEach(function(sz){
+ Object.keys(w.AVModell.DEFAULT_INPUT).forEach(k => { w.S.I[k] = w.AVModell.DEFAULT_INPUT[k]; });
+ Object.keys(sz).forEach(k => { w.S.I[k] = sz[k]; });
+ w.refresh();
+ w.FIELDS.forEach(function(f){
+ if (!f.liveNote) return;
+ try { const s2 = f.liveNote(w.R); if (s2 != null) angefragt.add('#note:' + f.k); }
+ catch (e) { bad.push('Live-Notiz ' + f.k + ' (' + l + '): ' + e.message); }
+ });
+ });
+ // Referenzstand, Anheften, Kopplungswarnung
+ Object.keys(w.AVModell.DEFAULT_INPUT).forEach(k => { w.S.I[k] = w.AVModell.DEFAULT_INPUT[k]; });
+ w.arm('gehalt'); w.refresh();
+ w.koppelWarn = 'unerreichbar'; w.renderCards();
+ w.koppelWarn = 'inaktiv'; w.renderCards();
+ w.koppelWarn = null;
+ w.$('#pin').onclick(); w.$('#pin').onclick();
+ w.setInstallHint(w.t('pwa.offline'));
+ w.setInstallHint(w.t('pwa.installAsk'));
+ w.setInstallHint(w.t('pwa.file'));
+ w.$('#sharemsg').textContent = w.t('msg.linkCopied') + w.t('msg.linkFailed')
+ + w.t('msg.loaded') + w.t('msg.reset') + w.t('msg.loadError', { msg: 'x' });
+ });
+ Object.keys(w.AVModell.DEFAULT_INPUT).forEach(k => { w.S.I[k] = w.AVModell.DEFAULT_INPUT[k]; });
+ w.setLang('en'); w.refresh();
+
+ // --- jede Sprache vollständig durchzeichnen ---
+ w.LANGS.forEach(function(l){
+ w.setLang(l);
+ w.renderDetails();
+ w.TABS.forEach(T => { w.S.tab = T.k; w.drawChart(); });
+ w.S.tab = 'sens';
+ Object.keys(w.METRICS).forEach(m => { w.S.metric = m; w.refresh(); w.renderDetails(); });
+ w.S.metric = 'monat'; w.refresh();
+ check('Rechenergebnisse in ' + l, zahlen() === deZahlen);
+ check('html lang=' + l, doc.documentElement.lang === l);
+ const txt = sichtbar();
+ // Der Wasserfall wird zwischengespeichert; seine Balkennamen stehen im
+ // Fazit und müssen deshalb ebenfalls mitwechseln.
+ const wfLabels = w.getWaterfall('av').bars.map(b => b.label).join(' | ');
+ const wfSoll = [w.DICT[l]['wfb.vp.label'], w.DICT[l]['wfb.zul.label'],
+ w.DICT[l]['wfb.arch.label']].join(' | ');
+ check('Wasserfall-Beschriftung in ' + l, wfLabels === wfSoll, wfLabels);
+ check('Schlüssel sichtbar in ' + l + ' (keine)', !/\b(f\.[IP]:|det\.|opt\.|metric\.|wfb\.)[a-z_]/.test(txt),
+ (txt.match(/\b(?:f\.[IP]:|det\.|opt\.|metric\.|wfb\.)[A-Za-z_.:]+/g) || []).slice(0,5).join(' '));
+ });
+
+ // Japanisch: kein lateinischer Fließtext mehr, wo japanischer stehen müsste
+ w.setLang('ja');
+ const jaTxt = sichtbar();
+ check('japanischer Text vorhanden', /結果|手取り負担額|月額給付/.test(jaTxt));
+ check('kein deutscher Fließtext in JA',
+ !/(Rentenbeginn|Nettoaufwand|Bruttorendite|Monatsleistung|Beispielwerte)/.test(jaTxt),
+ (jaTxt.match(/Rentenbeginn|Nettoaufwand|Bruttorendite|Monatsleistung|Beispielwerte/g)||[]).join(' '));
+ w.setLang('la');
+ const laTxt = sichtbar();
+ check('lateinischer Text vorhanden', /Exitus|impensa pura|Praestatio menstrua/.test(laTxt));
+ check('kein deutscher Fließtext in LA',
+ !/(Rentenbeginn|Nettoaufwand|Bruttorendite|Monatsleistung|Beispielwerte)/.test(laTxt),
+ (laTxt.match(/Rentenbeginn|Nettoaufwand|Bruttorendite|Monatsleistung|Beispielwerte/g)||[]).join(' '));
+
+ // --- zurück auf Deutsch ---
+ w.setLang('de');
+ check('Rückschaltung', w.LANG === 'de' && /Ergebnis/.test(sichtbar()));
+ check('Rechenergebnisse nach Rückschaltung', zahlen() === deZahlen);
+
+ // --- Zahleneingabe in beiden Schreibweisen ---
+ const p = w.parseNum;
+ check('parseNum 1.234,56 (de)', Math.abs(p('1.234,56') - 1234.56) < 1e-9, String(p('1.234,56')));
+ check('parseNum 1,234.56 (de)', Math.abs(p('1,234.56') - 1234.56) < 1e-9, String(p('1,234.56')));
+ check('parseNum 0,5 (de)', Math.abs(p('0,5') - 0.5) < 1e-9, String(p('0,5')));
+ check('parseNum 1.234 (de → 1234)', Math.abs(p('1.234') - 1234) < 1e-9, String(p('1.234')));
+ w.LANG = 'en';
+ check('parseNum 1,234 (en → 1234)', Math.abs(p('1,234') - 1234) < 1e-9, String(p('1,234')));
+ check('parseNum 1234.5 (en)', Math.abs(p('1234.5') - 1234.5) < 1e-9, String(p('1234.5')));
+ w.LANG = 'de';
+ [['de','1.234,56',1234.56],['la','1.234,56',1234.56],['ja','1,234.56',1234.56],['ja','1,234',1234]]
+ .forEach(function(f){
+ w.LANG = f[0];
+ check('parseNum ' + f[1] + ' (' + f[0] + ')', Math.abs(p(f[1]) - f[2]) < 1e-9, String(p(f[1])));
+ });
+ w.LANG = 'de';
+
+ console.log('bestanden (' + ok.length + '):');
+ ok.forEach(x => console.log(' ✓ ' + x));
+ if (bad.length) {
+ console.log('\nFEHLGESCHLAGEN (' + bad.length + '):');
+ bad.forEach(x => console.log(' ✗ ' + x));
+ }
+ console.log('\nangefragte Schlüssel:', angefragt.size, 'von', Object.keys(w.I18N_DE).length);
+ const nie = Object.keys(w.I18N_DE).filter(k => !angefragt.has(k));
+ console.log('nie angefragt (' + nie.length + '):', nie.join(', ') || '—');
+ process.exit(bad.length ? 1 : 0);
+}, 2500);