fix(i18n): drei uebersehene Umlaute und die Luecke, durch die sie schluepften
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
This commit is contained in:
@@ -1594,7 +1594,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz
|
|||||||
|
|
||||||
expect(mockSearch).not.toHaveBeenCalled();
|
expect(mockSearch).not.toHaveBeenCalled();
|
||||||
expect(result.errors).toEqual([
|
expect(result.errors).toEqual([
|
||||||
'Gruppe Sales: ungueltiger ldapObjectGuid-Wert',
|
'Gruppe Sales: ungültiger ldapObjectGuid-Wert',
|
||||||
]);
|
]);
|
||||||
expect(prisma.group.delete).not.toHaveBeenCalled();
|
expect(prisma.group.delete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1727,7 +1727,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz
|
|||||||
}
|
}
|
||||||
expect(prisma.group.delete).not.toHaveBeenCalled();
|
expect(prisma.group.delete).not.toHaveBeenCalled();
|
||||||
expect(result.errors.length).toBe(1);
|
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 () => {
|
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(prisma.group.update).not.toHaveBeenCalled();
|
||||||
expect(result.groupsDeleted).toBe(0);
|
expect(result.groupsDeleted).toBe(0);
|
||||||
expect(result.errors).toEqual([
|
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);
|
expect(groups).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1310,7 +1310,7 @@ export class LdapService {
|
|||||||
// (T-16-01).
|
// (T-16-01).
|
||||||
if (!ldapObjectGuid || !/^[0-9a-f]{32}$/.test(ldapObjectGuid)) {
|
if (!ldapObjectGuid || !/^[0-9a-f]{32}$/.test(ldapObjectGuid)) {
|
||||||
result.errors.push(
|
result.errors.push(
|
||||||
`Gruppe ${group.name}: ungueltiger ldapObjectGuid-Wert`,
|
`Gruppe ${group.name}: ungültiger ldapObjectGuid-Wert`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1427,7 +1427,7 @@ export class LdapService {
|
|||||||
}
|
}
|
||||||
if (wideHit) {
|
if (wideHit) {
|
||||||
result.errors.push(
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
"uploadCta": "Bild hochladen",
|
"uploadCta": "Bild hochladen",
|
||||||
"uploadSuccess": "Profilbild wurde erfolgreich aktualisiert.",
|
"uploadSuccess": "Profilbild wurde erfolgreich aktualisiert.",
|
||||||
"uploadError": "Hochladen fehlgeschlagen. Bitte prüfen Sie das Dateiformat und die Dateigröße.",
|
"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",
|
"passwordSectionTitle": "Passwort ändern",
|
||||||
"accentColorLabel": "Akzentfarbe",
|
"accentColorLabel": "Akzentfarbe",
|
||||||
"accentColorHelp": "Passt die Hauptfarbe der Oberfläche an.",
|
"accentColorHelp": "Passt die Hauptfarbe der Oberfläche an.",
|
||||||
@@ -173,9 +173,9 @@
|
|||||||
"catalogTitle": "Widget hinzufügen",
|
"catalogTitle": "Widget hinzufügen",
|
||||||
"deleteTooltip": "Widget entfernen",
|
"deleteTooltip": "Widget entfernen",
|
||||||
"editMode": "Dashboard bearbeiten",
|
"editMode": "Dashboard bearbeiten",
|
||||||
"saveChanges": "Aenderungen speichern",
|
"saveChanges": "Änderungen speichern",
|
||||||
"layoutLoadError": "Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.",
|
"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": {
|
"clock": {
|
||||||
"name": "Uhr",
|
"name": "Uhr",
|
||||||
"description": "Zeigt die aktuelle Uhrzeit an",
|
"description": "Zeigt die aktuelle Uhrzeit an",
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
},
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"categoryTitle": "Module: {category}",
|
"categoryTitle": "Module: {category}",
|
||||||
"openModule": "Oeffnen",
|
"openModule": "Öffnen",
|
||||||
"noModules": "Keine aktiven Module in dieser Kategorie",
|
"noModules": "Keine aktiven Module in dieser Kategorie",
|
||||||
"notFound": "Modul nicht gefunden",
|
"notFound": "Modul nicht gefunden",
|
||||||
"notFoundDescription": "Das angeforderte Modul existiert nicht oder ist nicht aktiviert.",
|
"notFoundDescription": "Das angeforderte Modul existiert nicht oder ist nicht aktiviert.",
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export const UMLAUT_REPLACEMENTS: Record<string, string> = {
|
|||||||
Ueberpruefe: 'Überprüfe',
|
Ueberpruefe: 'Überprüfe',
|
||||||
Zertifikatspruefung: 'Zertifikatsprüfung',
|
Zertifikatspruefung: 'Zertifikatsprüfung',
|
||||||
aendern: 'ändern',
|
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',
|
geaendert: 'geändert',
|
||||||
Moechten: 'Möchten',
|
Moechten: 'Möchten',
|
||||||
moeglich: 'möglich',
|
moeglich: 'möglich',
|
||||||
@@ -162,4 +168,10 @@ export const UMLAUT_ALLOWLIST: readonly string[] = [
|
|||||||
'vertrauenswürdigen',
|
'vertrauenswürdigen',
|
||||||
'ausschließen',
|
'ausschließen',
|
||||||
'entschlüsselt',
|
'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',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ import { UMLAUT_ALLOWLIST, UMLAUT_REPLACEMENTS } from './umlaut-dictionary';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const UMLAUT_TOKEN_RE = /[A-Za-zÄÖÜäöüß]+/g;
|
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. */
|
/** Recursively flattens a nested message object into dot-joined leaf key paths. */
|
||||||
function flattenKeys(obj: unknown, prefix = ''): string[] {
|
function flattenKeys(obj: unknown, prefix = ''): string[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user