# Altersvorsorge-Vergleich — Web-App Progressive Web-App zum Excel-Modell im übergeordneten Ordner. Derselbe Rechenkern, dieselben Ergebnisse, aber bedienbar von Menschen, die keine Tabellenkalkulation aufmachen wollen. | Datei | Rolle | |---|---| | `index.html` | **Die ganze App.** Rechenkern, Oberfläche, Diagramme — eine Datei, keine externen Abhängigkeiten außer den Wörterbüchern. Läuft per Doppelklick. | | `i18n.de.js` | Wörterbuch Deutsch. Hauptsprache und Rückfallebene. **Vollständig.** | | `i18n.en.js` `i18n.nl.js` `i18n.fr.js` `i18n.la.js` `i18n.ja.js` | Englisch, Niederländisch, Französisch, Latein, Japanisch. **Vollständig** (459 Schlüssel). | | `i18n.nds.js` `i18n.bar.js` `i18n.eu.js` `i18n.eo.js` `i18n.tr.js` `i18n.uk.js` | Niederdeutsch, Bairisch, Baskisch, Esperanto, Türkisch, Ukrainisch. **Teilweise** (315 Schlüssel, siehe 3b). | | `i18n.got.js` `i18n.goh.js` `i18n.gmh.js` `i18n.grc.js` `i18n.sa.js` | Gotisch, Althochdeutsch, Mittelhochdeutsch, Altgriechisch, Sanskrit. **Teilweise**. | | `manifest.json` | Macht die App installierbar (Name, Farben, Symbole). | | `service-worker.js` | Offline-Cache, `stale-while-revalidate`. | | `icon-*.png` | App-Symbole, 192 / 512 / 512-maskable. | --- ## 1. Weitergeben — drei Wege **a) Nur die Dateien.** `index.html` samt allen neunzehn `i18n.*.js` verschicken — sie gehören zusammen und müssen im selben Ordner liegen. Doppelklick genügt, alles rechnet, Eingaben werden im Browser gespeichert. Was fehlt, ist nur die PWA-Hülle — kein Symbol auf dem Startbildschirm, kein Offline-Cache. Beides braucht es hier auch nicht, die Datei liegt ja schon auf dem Gerät. Die App sagt das dem Nutzer auch selbst. **b) Der ganze Ordner auf einem Webspace.** Irgendein HTTPS-Hosting genügt (GitHub Pages, Netlify, ein Unterverzeichnis auf der eigenen Domain). Dann ist sie installierbar und offlinefähig. Es gibt kein Backend, keine Datenbank, keine Build-Kette. ```bash # lokal ausprobieren, mit Service Worker: cd app && python3 -m http.server 8000 # -> http://localhost:8000 ``` Service Worker laufen auch auf `http://localhost` — als „secure context“ ist localhost ausdrücklich ausgenommen. **c) Als Link.** Der Knopf *„Link mit meinen Zahlen kopieren“* kodiert den kompletten Eingabestand base64url in den Fragment-Bezeichner. Fragmente werden vom Browser **nicht** an den Server geschickt (RFC 3986 § 3.5) — der Link enthält also persönliche Zahlen, überträgt sie aber an niemanden außer den Empfänger. ## 2. Wo die Daten liegen Ausschließlich in `localStorage` unter dem Schlüssel `altersvorsorge.v1`, also im Browser des Nutzers. Kein Server, keine Analytik, keine Schriftarten von fremden Domains, keine externen Skripte. Die einzigen Netzwerkzugriffe der App sind die, die der Service Worker macht, um sich selbst zu aktualisieren. ## 3. Aufbau der Datei `index.html` ist in nummerierte Teile gegliedert: | Teil | Inhalt | |---|---| | 0 | **Sprachen** — `t()`, Sprachwahl, Rückfall auf Deutsch | | 1 | **Rechenkern** — Portierung von `build_bav.py`. Reine Funktionen, kein DOM | | 2 | Felddefinitionen: 80 Eingaben mit Bereich, Einheit, Wörterbuchschlüssel | | 3 | Zustand, `localStorage`, Kodierung für den Teilen-Link | | 4 | Formatierung und Zahleneingabe, sprachabhängig | | 5 | Kennzahlen | | 6 | Aufbau der Oberfläche | | 7 | Ergebniskarten mit Δ-Anzeige | | 7b | Klartext-Fazit, Beispielwert-Hinweis, mitlaufende Ergebnisleiste | | 8 | Diagramme, reines Canvas | | 9 | Rechenweg | | 10 | Render-Schleife | | 11 | Installation und Offline-Betrieb | | 12 | Fensterrahmen: Titelleisten und Minimieren | | 13 | Kachelung: ein, zwei oder drei Fensterspalten | | 14 | Sprachumschaltung in der Oberfläche | Der Rechenkern in Teil 1 ist **die einzige Stelle, an der gerechnet wird.** Er ist zeichengenau aus `build_bav.py` übertragen; Änderungen an der Rechtslage gehören in beide Dateien. ### Brutto und Netto sind gekoppelt Es gibt keinen Umschalter „ich gebe vor: Brutto / Netto“ mehr. Beide Felder zeigen dieselbe Größe, nur in verschiedener Währung: ``` N(B) = B − SV-Ersparnis_AN(B) − Steuerersparnis(B) ``` `N` ist auf `[0, Bruttogehalt]` stetig und wegen `dN/dB = 1 − Grenz-SV-Satz − Grenzsteuersatz > 0` streng monoton wachsend, also eine Bijektion auf `[0, N(Bruttogehalt)]`. Ein Umschalter wäre eine Auswahl zwischen einer Funktion und ihrer Umkehrfunktion — also keine. `beitrag` bleibt die einzige Quelle der Wahrheit; `netto_ziel` ist ein **abgeleitetes Feld** mit `get` (Vorwärtsrechnung, Jahr 0 der Ansparphase) und `set` (Bisektion, `bruttoVonNetto`). Beide werten dasselbe erste Jahr aus — Lohn- und Tarifindex sind dort 1 —, weshalb die Richtungen exakt invers sind. Gemessen über 12 Parameterkombinationen × 5 Stützstellen: | Richtung | größter relativer Fehler | |---|---| | B → N → B | 2,3 · 10⁻⁸ | | N → B → N | 2,1 · 10⁻⁸ | Das ist genau die Auflösung der Bisektion: 34 Halbierungen auf `[0, 250.000 €]` sind 1,5 · 10⁻⁵ €/Jahr, also gut ein Millionstel Cent im Monat. Die Monotonie ist zusätzlich auf `1 … 1.500 €/Monat` in Ein-Euro-Schritten geprüft; die kleinste beobachtete Steigung ist `dN/dB = 0,545`, d. h. Steuer und Sozialabgaben tragen zusammen nie mehr als 45,5 % des Bruttobeitrags. Fällt der Nettoaufwand für das erste Jahr auf null — weil man dann bereits ausgeschieden ist —, existiert die Umkehrung nicht. Die App sagt das statt still nichts zu tun. ## 3b. Neunzehn Sprachen Deutsch ist die Hauptsprache; alle übrigen sind Übersetzungen derselben Rechtslage, kein zweiter Rechtsstand. **Die Normzitate bleiben in allen Sprachen deutsch** (`§ 1a BetrAVG`, `§ 226 Abs. 2 S. 2 SGB V`, …) — sie sind die Primärquellen und haben keine amtliche Übersetzung; eine eigene wäre hier keine Hilfe, sondern eine Fehlerquelle. Ebenso bleiben zwei Produktnamen stehen, weil sie Eigennamen sind: `Altersvorsorgedepot` und `Versorgungsbezüge`; im Japanischen mit Glosse (`Versorgungsbezüge(企業年金等の給付)`). | | Sprachen | Schlüssel | |---|---|---| | **vollständig** | `de` `de-x-amt` `de-x-sales` `en` `nl` `fr` `la` `ja` `nds` `bar` `eu` `eo` `tr` `uk` `got` `goh` `gmh` `grc` `sa` | 459 | | **teilweise** | — | — | Alle neunzehn Sprachen sind inzwischen vollständige Wörterbücher (459 von 459 Schlüsseln); die Tabellenzeile „teilweise“ bleibt stehen, weil `TEILWEISE` in `index.html` als Mechanismus weiterlebt — sie ist nur derzeit leer. Ursprünglich war die Auswahl strukturell, nicht nach Zeichenzahl: zuerst übersetzt wurde, was man anfasst oder als Überschrift liest — Feldbeschriftungen, Einheiten, Auswahlwerte, Fenstertitel, Kennzahlen, Tabellenköpfe, Diagrammlegenden, Meldungen; die langen juristischen Erläuterungen (die `?`-Texte an den Feldern, das Fazit, die Notizen unter den Feldern, der ganze Rechenweg) kamen erst in einer zweiten Fassung dazu. Für die drei historischen Sprachstufen und für Sanskrit und Altgriechisch ist der Vertrauensgrad in diesen Fließtext-Passagen entsprechend niedriger als in den Feldbeschriftungen selbst — siehe die Kopfkommentare der jeweiligen `i18n.*.js`-Datei für die genauen Prozentsätze und die dort neu geprägten Fachbegriffe. **Die drei alten germanischen Fassungen** (`got` Gotisch, `goh` Althochdeutsch, `gmh` Mittelhochdeutsch) sind kein Scherz, sondern eine Probe darauf, wie viel Fachsprache eine Sprachstufe trägt. Nur `goh` und `gmh` sind Vorstufen des Deutschen; **Gotisch ist ostgermanisch und damit eine Schwester, keine Ahnin** — es steht hier, weil es das älteste umfangreich überlieferte Germanisch überhaupt ist. Das Ergebnis ist ein klares Gefälle, und es folgt genau der Verstädterung: - **Gotisch** hat mit Wulfilas Bibel ein Korpus von rund 3.000 Lexemen, praktisch keines davon wirtschaftlich. Belegt sind immerhin `faihu` (Vermögen), `gild` (Abgabe), `wokrs` (Zins, Lk 19,23 *miþ wokra*), `mizdo` (Lohn), `asneis` (Lohnarbeiter). Alles Übrige — `haubidafaihu` für Kapital, `gagaleikeins` für Vergleich — ist geprägt. Vertrauensgrad ≈ 30 %. - **Althochdeutsch** gewinnt durch Tatian und Notker: `gelt`, `zins`, `lon`, `wuohhar` sind belegt, und **`widarmezzon` steht im Tatian genau für lateinisch *comparare*** — der Fenstertitel ist also kein Kunstwort. Vertrauensgrad ≈ 50 %. - **Mittelhochdeutsch** ist die tragfähigste Stufe, weil es bereits eine städtische Geld- und Rechtssprache gibt. Drei Treffer sind keine Prägung, sondern der jeweilige Terminus: **`houbetguot`** ist das Kapital, **`lîpgedinge`** die lebenslange Leibrente, **`tiurunge`** der Preisanstieg — also die Inflation. Dazu `gülte`, `stiure`, `wuocher`, `gewin`, `koste`. Vertrauensgrad ≈ 65 %. **Gotisch steht in lateinischer Umschrift** (Streitberg), nicht im Wulfila-Alphabet (U+10330–1034F). Der Grund ist der Grundsatz aus § 2: die App lädt keine Schriften von fremden Domains. Für gotische Schrift gibt es auf keinem verbreiteten System eine vorinstallierte Datei — 𐌰𐌻𐌻𐌰𐌹𐌼 wären leere Kästchen. Die Umschrift ist ohnehin die Form, in der das Gotische gelesen wird. Wer die Schrift will, braucht eine eingebettete Schriftart und damit eine andere Grundsatzentscheidung. ### Registervarianten des Deutschen: gebaut **„Beamtendeutsch“ und „Vertrieb“** sind keine Sprachen, sondern Stilebenen derselben Sprache — sie stehen daher nach BCP 47 § 2.2.7 unter `de` mit privatem Untertag: `de-x-amt` und `de-x-sales`, in `i18n.de-x-amt.js` und `i18n.de-x-sales.js`. Anders als bei den übrigen Sprachen genügt hier keine Teilfassung: der Witz liegt gerade im Fließtext, den eine Teilübersetzung weglassen würde. Derselbe § 1a BetrAVG heißt im Amtsdeutsch „Ein Rechtsanspruch besteht ausschließlich bis zur Höhe von 4 vom Hundert der Beitragsbemessungsgrenze der Rentenversicherung“ und im Vertriebsdeutsch „Bis zu 4 % on top — geschenkt vom Gesetzgeber!“ — beide beschreiben exakt dieselbe Rechtslage, und der Vergleich ist der Punkt. Die Zahlen ändern sich auch dort nicht; `pruefe.js` prüft das wie bei jeder anderen Sprache ohne Sonderfall. Technisch brauchte das drei Änderungen: `pickLang()` matcht seither zusätzlich zur Grundsprache beliebig viele `-x-[a-z0-9]{1,8}`-Untertags; `LANGS` und `DICT` bekamen Einträge mit Bindestrich; `LOCALES`/`GRUPPEN` brauchten dagegen **keine** neuen Einträge, weil ihre bestehenden `||`-Rückfälle (`'de-DE'` bzw. `'.'`) für beide Varianten schon das Richtige liefern. Der Sprachknopf zeigt nicht den vollen Code (`DE-X-AMT` wäre zu lang für die zweizeilige Schaltfläche), sondern nur den Teil nach dem letzten Bindestrich (`AMT`, `SALES`); `lang="…"` und das Autonym im `title` tragen weiterhin den vollen Code bzw. den vollen Namen. ### Vorgemerkt, noch nicht gebaut **Rechtsläufige Sprachen (`he`, `fa`, `ar`).** Die Übersetzung ist der billige Teil. Teuer ist dreierlei: `applyStatic()` muss neben `lang` auch `dir` setzen; im Stilblatt müssen `margin-left`/`-right` durch die logischen Eigenschaften ersetzt werden, sonst kippt die Kachelung; und Teil 8 zeichnet auf Canvas, wo es **keinen Bidi-Algorithmus gibt** — Achsen, Legenden und der Wasserfall müssten von Hand gespiegelt werden. Dazu `fa-u-nu-latn` und `ar-u-nu-latn` in `LOCALES`, sonst liefert ICU ostarabische Ziffern (۱۲۳۴) neben einem `€`. Die Einzelheiten stehen als Kommentarblock in `index.html` direkt bei `TEILWEISE`. **Codes.** ISO 639-1, wo es einen gibt, sonst 639-3. Dreibuchstabig sind `grc` (**nicht** `el` — das wäre Neugriechisch), `nds` und `bar`; keine der drei hat einen zweibuchstabigen Code. Deshalb matcht `pickLang()` `[a-z]{2,3}` als Grundcode, optional gefolgt von den erwähnten `-x-…`-Untertags. **Wahl der Sprache**, in dieser Reihenfolge: 1. `?lang=…` in der Adresse — teilbar, überschreibt alles, 2. die gemerkte Wahl in `localStorage` unter `altersvorsorge.lang`, 3. `navigator.language`, nach BCP-47-Teilkette (`xx` oder `xx-…`). **Latein, Altgriechisch, Sanskrit und Esperanto werden nie automatisch gewählt** — ein Browser, der `la` meldet, meint es fast sicher nicht als Anzeigesprache. Die Knöpfe tragen das Kürzel, der `title` das **Autonym** (`Українська`, `Plattdüütsch`, `संस्कृतम्`) — nicht den übersetzten Namen. Wer Ukrainisch sucht, erkennt `Українська` auch dann, wenn die Oberfläche gerade auf Baskisch steht; nebenbei spart das 14 × 14 Wörterbucheinträge. **Zahlenformat.** `LOCALES` bildet jeden Code auf ein ICU-Tag ab, `GRUPPEN` auf das Tausenderzeichen, das `parseNum()` beim mehrdeutigen `1,234` heranzieht. Sprachen ohne eigene ICU-Daten (`nds`, `bar`, `eo`, `la`, `grc`, `sa`) bekommen `de-DE`: Devanāgarī-Ziffern neben einem `€` wären eine Schikane, keine Genauigkeit, und griechische Buchstabenzahlen erst recht. `fr` gruppiert mit U+202F, `uk` mit U+00A0 — beides so, wie ICU es tatsächlich setzt, nachgeprüft gegen `formatToParts`. **Zu Latein und Japanisch.** Das Latein ist pragmatisches Neulatein: klassische Syntax, aber gebildete Neuprägungen für Moderne (`pensio operativa`, `impensa pura`, `valor praesens`, `usura interna`). Römische Ziffern wären ein Witz auf Kosten der Lesbarkeit — und die Trennzeichenkonvention ist ohnehin jünger als jede lateinische Quelle. Das Japanische folgt dem, was für Software dieser Art dort üblich ist: Erklärtexte in です・ます, Feldbeschriftungen als Nominalphrasen im 体言止め. Für die Schrift ist der `--ui`-Stapel um MS PGothic, Hiragino, Yu Gothic und Meiryo erweitert — eine Rasterschrift mit harten Kanten passt zum Thema. Der japanische Satzbau erzwingt, wofür die Funktionsform der Einträge da ist. Das Fazit lautet im Deutschen „X liegt vorn mit W — D mehr als Y“, im Japanischen dagegen `Xが首位です(W)。YをD上回ります。` — Verb am Ende, Vergleichsgröße mit を statt „als“. Als Verkettung fester Bausteine ließe sich das nicht bauen. Der Umschalter oben im Kopffenster schreibt beides fort, Speicher und Adresse. Der Teilen-Link nimmt die Suchzeichenfolge mit, der Zustand steckt weiter im Fragment — beides stört sich nicht. **Aufbau der Wörterbücher.** Ein Eintrag ist entweder eine Zeichenkette oder eine Funktion `(p, f) → string`. Die Funktionsform ist keine Bequemlichkeit, sondern notwendig: ein Satz ist nicht die Verkettung seiner Teile. Stellung, Numerus und Rektion hängen an der Sprache, und `'mehr als ' + nom` funktioniert nur, solange beide Sprachen denselben Satzbau haben. Deshalb bekommen die Funktionen **Rohwerte** — Zahlen, keine fertigen Textstücke — und formatieren selbst über `f` (`f.eur`, `f.pct`, `f.nf2`, …). Sichtbar wird der Unterschied etwa im Fazit: das Deutsche braucht dort einen Genitiv (`der bAV`), das Englische eine Präposition. ``` t('n.beitrag.ueber8', { v8: v8 }) de: 'Über 8 % der Beitragsbemessungsgrenze RV (' + f.eur(p.v8) + ' im Jahr). …' en: 'Above 8 % of the pension-insurance contribution ceiling (' + f.eur(p.v8) + ' per year). …' ``` **Schlüsselschema.** Eingabefelder liegen unter `f.:.label|hint|unit|o`, die Optionen also unter ihrem *Wert*, nicht unter ihrem Index — dann bleibt die Zuordnung richtig, wenn eine Option dazukommt. `FIELDS`, `OPTS` und `METRICS` tragen ihre Texte nicht mehr als Feld, sondern als `Object.defineProperty`-Zugriffsfunktion: `f.label` steht unverändert an jeder Aufrufstelle und liefert nach einem Sprachwechsel von selbst den neuen Wert. **Fehlt ein Eintrag**, greift Deutsch; fehlt auch der, erscheint der Schlüssel selbst im Klartext. Ein stiller Leerstring wäre die schlechtere Wahl — er verschwindet unbemerkt. **Zahlen.** Umgeschaltet wird nur die Zifferngruppierung — `de` und `la` → `de-DE`, `en` → `en-GB`, `ja` → `ja-JP`; die Währung bleibt überall der nachgestellte Euro: `75.000,00 €` bzw. `75,000.00 €`. Die Eingabe nimmt **beide** Schreibweisen an, damit ein Sprachwechsel keine getippte Zahl entwertet. Die Regel in `parseNum`: stehen beide Zeichen im Text, ist das rechte das Dezimalzeichen; steht nur eines und folgen ihm genau drei Ziffern, ist es die Tausendergruppe der aktuellen Sprache (`GRUPPEN`: `.` für de und la, `,` für en und ja). Damit bleibt allein `1,234` mehrdeutig — und genau dort entscheidet die Sprache. **Was ein Sprachwechsel nicht anfasst:** die Eingaben, den Referenzstand und die Ergebnisse. `setLang` baut nur die Teile neu, deren Beschriftungen beim Aufbau festgeschrieben werden (Kennzahlenleiste, Registerkarten, Felder, Fenstertitel); alles Übrige zeichnet `refresh()` ohnehin bei jedem Durchgang. Der Referenzstand merkt sich deshalb den *Feldschlüssel* und nicht den fertigen Satz — sonst stünde nach dem Wechsel „vor der Änderung von …“ noch in der alten Sprache da. **Prüfung.** `pruefe.js` (Entwicklungswerkzeug, wird vom Service Worker nicht ausgeliefert) lädt die Seite kopflos in jsdom, schaltet alle neunzehn Sprachen durch, klappt jeden Abschnitt auf, erzwingt über fünf Szenarien die bedingten Texte und prüft: kein unaufgelöster Schlüssel, keine deutschen Reste im englischen Modus außer den erlaubten Fachbegriffen, kein sichtbar gebliebener Schlüssel, und — die eigentliche Invariante — **identische Rechenergebnisse in allen neunzehn Sprachen**. Die Liste der Wörterbücher liest das Skript aus `index.html` statt sie zu pflegen: eine neue Sprache soll den Test nicht stillschweigend an einem leeren Objekt vorbeilaufen lassen. Das Regex für die `