From 85d2d772b6cb14b9d90dd28c04132496e92320cd Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 7 Sep 2026 13:16:57 +0200 Subject: [PATCH] fix(i18n): drei uebersehene Umlaute und die Luecke, durch die sie schluepften MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Gegenprobe im Browser hat drei Stellen gefunden, die der erste Durchgang nicht erwischt hat — darunter zwei gut sichtbare Schaltflaechen: Aenderungen speichern -> Änderungen speichern Oeffnen -> Öffnen Eine Aenderung ... -> Eine Änderung ... Die Ursache ist dieselbe fuer alle drei und steckte im Waechter selbst: sein Verdachtsmuster /(ae|oe|ue|ss)/ war case-sensitiv. "Aenderungen" beginnt mit "Ae", nicht mit "ae", und ist deshalb durchgerutscht — der Waechter konnte gar nicht anschlagen. Muster jetzt case-insensitiv; damit erfasst es auch die grossgeschriebenen Formen. Durch die schaerfere Pruefung melden sich neu die Abkuerzungen RSS, RSSGenerator und SSL. Sie tragen ein doppeltes S ohne Umlaut-Bezug und stehen jetzt auf der Positivliste. Ausserdem zwei Meldungen des LDAP-Abgleichs korrigiert, die dem Administrator in der Oberflaeche angezeigt werden (result.errors landet in der Fehlerliste der LDAP-Seite): "ungueltiger ldapObjectGuid-Wert" und "Base-DN-Konfiguration pruefen. Nicht geloescht." Drei Tests pinnen diese Texte bewusst und wurden mitgezogen. Bewusst NICHT angefasst: die Warnung in crypto.service.ts. Sie geht ueber logger.warn ins Protokoll und nicht an einen Nutzer. Unabhaengig gegengeprueft: von allen Tokens in de.json, die ae/oe/ue tragen, ist keines mehr eine Ersatzschreibung. 642 API-Tests und 225 Web-Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq --- apps/api/src/ldap/ldap.service.spec.ts | 6 +++--- apps/api/src/ldap/ldap.service.ts | 4 ++-- apps/web/src/messages/de.json | 8 ++++---- apps/web/src/messages/umlaut-dictionary.ts | 12 ++++++++++++ apps/web/src/messages/umlaut-guard.spec.ts | 6 +++++- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/api/src/ldap/ldap.service.spec.ts b/apps/api/src/ldap/ldap.service.spec.ts index 84457bd..52c7b21 100644 --- a/apps/api/src/ldap/ldap.service.spec.ts +++ b/apps/api/src/ldap/ldap.service.spec.ts @@ -1594,7 +1594,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz expect(mockSearch).not.toHaveBeenCalled(); expect(result.errors).toEqual([ - 'Gruppe Sales: ungueltiger ldapObjectGuid-Wert', + 'Gruppe Sales: ungültiger ldapObjectGuid-Wert', ]); expect(prisma.group.delete).not.toHaveBeenCalled(); }); @@ -1727,7 +1727,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz } expect(prisma.group.delete).not.toHaveBeenCalled(); expect(result.errors.length).toBe(1); - expect(result.errors[0]).toContain('Nicht geloescht'); + expect(result.errors[0]).toContain('Nicht gelöscht'); }); it('is idempotent: a second run over an unchanged AD state issues no group.update or group.delete call', async () => { @@ -1832,7 +1832,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz expect(prisma.group.update).not.toHaveBeenCalled(); expect(result.groupsDeleted).toBe(0); expect(result.errors).toEqual([ - "Gruppe Sales: nicht mehr unter den konfigurierten Base-DNs gefunden, existiert aber weiterhin unter 'cn=Sales,ou=Archive,dc=example,dc=com' — vermutlich im Verzeichnis verschoben, Base-DN-Konfiguration pruefen. Nicht geloescht.", + "Gruppe Sales: nicht mehr unter den konfigurierten Base-DNs gefunden, existiert aber weiterhin unter 'cn=Sales,ou=Archive,dc=example,dc=com' — vermutlich im Verzeichnis verschoben, Base-DN-Konfiguration prüfen. Nicht gelöscht.", ]); expect(groups).toHaveLength(1); }); diff --git a/apps/api/src/ldap/ldap.service.ts b/apps/api/src/ldap/ldap.service.ts index 190e0e1..46d0ba8 100644 --- a/apps/api/src/ldap/ldap.service.ts +++ b/apps/api/src/ldap/ldap.service.ts @@ -1310,7 +1310,7 @@ export class LdapService { // (T-16-01). if (!ldapObjectGuid || !/^[0-9a-f]{32}$/.test(ldapObjectGuid)) { result.errors.push( - `Gruppe ${group.name}: ungueltiger ldapObjectGuid-Wert`, + `Gruppe ${group.name}: ungültiger ldapObjectGuid-Wert`, ); continue; } @@ -1427,7 +1427,7 @@ export class LdapService { } if (wideHit) { result.errors.push( - `Gruppe ${group.name}: nicht mehr unter den konfigurierten Base-DNs gefunden, existiert aber weiterhin unter '${wideHit.dn}' — vermutlich im Verzeichnis verschoben, Base-DN-Konfiguration pruefen. Nicht geloescht.`, + `Gruppe ${group.name}: nicht mehr unter den konfigurierten Base-DNs gefunden, existiert aber weiterhin unter '${wideHit.dn}' — vermutlich im Verzeichnis verschoben, Base-DN-Konfiguration prüfen. Nicht gelöscht.`, ); continue; } diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 0205fa8..d908614 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -128,7 +128,7 @@ "uploadCta": "Bild hochladen", "uploadSuccess": "Profilbild wurde erfolgreich aktualisiert.", "uploadError": "Hochladen fehlgeschlagen. Bitte prüfen Sie das Dateiformat und die Dateigröße.", - "ldapManagedNotice": "Ihr Passwort wird über das Verzeichnis (LDAP) verwaltet. Eine Aenderung ist hier nicht möglich.", + "ldapManagedNotice": "Ihr Passwort wird über das Verzeichnis (LDAP) verwaltet. Eine Änderung ist hier nicht möglich.", "passwordSectionTitle": "Passwort ändern", "accentColorLabel": "Akzentfarbe", "accentColorHelp": "Passt die Hauptfarbe der Oberfläche an.", @@ -173,9 +173,9 @@ "catalogTitle": "Widget hinzufügen", "deleteTooltip": "Widget entfernen", "editMode": "Dashboard bearbeiten", - "saveChanges": "Aenderungen speichern", + "saveChanges": "Änderungen speichern", "layoutLoadError": "Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.", - "widgetSaveError": "Aenderungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.", + "widgetSaveError": "Änderungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.", "clock": { "name": "Uhr", "description": "Zeigt die aktuelle Uhrzeit an", @@ -479,7 +479,7 @@ }, "modules": { "categoryTitle": "Module: {category}", - "openModule": "Oeffnen", + "openModule": "Öffnen", "noModules": "Keine aktiven Module in dieser Kategorie", "notFound": "Modul nicht gefunden", "notFoundDescription": "Das angeforderte Modul existiert nicht oder ist nicht aktiviert.", diff --git a/apps/web/src/messages/umlaut-dictionary.ts b/apps/web/src/messages/umlaut-dictionary.ts index b6ccd32..4804e60 100644 --- a/apps/web/src/messages/umlaut-dictionary.ts +++ b/apps/web/src/messages/umlaut-dictionary.ts @@ -42,6 +42,12 @@ export const UMLAUT_REPLACEMENTS: Record = { Ueberpruefe: 'Überprüfe', Zertifikatspruefung: 'Zertifikatsprüfung', aendern: 'ändern', + // Nachtrag 2026-09-07: die grossgeschriebenen Formen fehlten. Der Waechter + // hat sie nicht gemeldet, weil sein Verdachtsmuster case-sensitiv war und + // "Ae"/"Oe" deshalb durchrutschten — mitbehoben in umlaut-guard.spec.ts. + Aenderung: 'Änderung', + Aenderungen: 'Änderungen', + Oeffnen: 'Öffnen', geaendert: 'geändert', Moechten: 'Möchten', moeglich: 'möglich', @@ -162,4 +168,10 @@ export const UMLAUT_ALLOWLIST: readonly string[] = [ 'vertrauenswürdigen', 'ausschließen', 'entschlüsselt', + // Nachtrag 2026-09-07: Abkuerzungen mit doppeltem S. Sie tauchen erst auf, + // seit das Verdachtsmuster case-insensitiv prueft — vorher fiel jedes + // grossgeschriebene "SS" durch dasselbe Loch wie "Aenderungen". + 'RSS', + 'RSSGenerator', + 'SSL', ]; diff --git a/apps/web/src/messages/umlaut-guard.spec.ts b/apps/web/src/messages/umlaut-guard.spec.ts index e12fb4b..d1f4960 100644 --- a/apps/web/src/messages/umlaut-guard.spec.ts +++ b/apps/web/src/messages/umlaut-guard.spec.ts @@ -22,7 +22,11 @@ import { UMLAUT_ALLOWLIST, UMLAUT_REPLACEMENTS } from './umlaut-dictionary'; */ const UMLAUT_TOKEN_RE = /[A-Za-zÄÖÜäöüß]+/g; -const SUSPECT_RE = /(ae|oe|ue|ss)/; +// Case-insensitive, sonst rutscht jede grossgeschriebene Ersatzschreibung durch: +// "Aenderungen" beginnt mit "Ae", nicht mit "ae". Genau daran sind beim ersten +// Durchgang "Aenderung", "Aenderungen" und "Oeffnen" vorbeigekommen — darunter +// zwei gut sichtbare Schaltflaechen. +const SUSPECT_RE = /(ae|oe|ue|ss)/i; /** Recursively flattens a nested message object into dot-joined leaf key paths. */ function flattenKeys(obj: unknown, prefix = ''): string[] {