diff --git a/lib/lolyo.ts b/lib/lolyo.ts index 3befe3d..a71f0a3 100644 --- a/lib/lolyo.ts +++ b/lib/lolyo.ts @@ -59,6 +59,30 @@ export function anrede(gender: string): string { return ""; } +/** + * Typografische Zeichen auf ihre ASCII-Entsprechung. + * + * Im Zielsystem kam der Gedankenstrich „–" als `–` an — das sind seine + * UTF-8-Bytes, gelesen als Windows-1252. Gemessen hat die Vorlage des Anbieters + * dieselbe Kodierung wie unsere Datei (UTF-8 ohne BOM, nachgeprüft am + * „bestätigt" in ihrer Kopfzeile), die Ursache liegt also nicht in dem, was wir + * schreiben. Solange sie nicht geklärt ist, geht der Weg über das Zeichen + * selbst: ein Bindestrich sagt dasselbe und überlebt jede Fehlinterpretation, + * weil er in beiden Kodierungen dasselbe Byte ist. + * + * Umlaute bleiben. „Bäckerei" zu „Baeckerei" zu machen hiesse, ein Problem zu + * verstecken, das es entweder gar nicht gibt — oder das dann auch Namen + * betrifft, und dort wäre es keine Kosmetik mehr. + */ +export function ohneTypografie(text: string): string { + return text + .replace(/[‐-―−]/g, "-") + .replace(/[‘’‚‛′]/g, "'") + .replace(/[“”„‟″]/g, '"') + .replace(/…/g, "...") + .replace(/[   ]/g, " "); +} + /** Die Person, so wie der Export sie braucht. */ export type LolyoQuelle = { id: string; @@ -86,17 +110,26 @@ export type LolyoZeile = Record; * employees.email: die Datei legt Konten bei einem fremden Anbieter an. */ export function baueLolyoZeile(p: LolyoQuelle, passwort: string): LolyoZeile { + // ohneTypografie steht an jeder Zelle, die aus den Daten kommt, und nicht nur + // an der Position: aufgefallen ist es dort, weil die Tätigkeiten einen + // Gedankenstrich führen — dieselben Zeichen stehen aber auch in Namen und + // Titeln, und die fielen erst bei der Person auf, die einen trägt. + const aus = (wert: string) => ohneTypografie(wert); + return { "Benutzer/Code": p.id, - Email: p.company_email ?? "", + Email: aus(p.company_email ?? ""), + // **Ohne** Umschrift: das Passwort muss Zeichen für Zeichen das sein, was + // die Person eingibt. Ein ersetztes Zeichen fiele niemandem auf — es gäbe + // nur ein Konto, in das niemand hineinkommt. Passwort: passwort, - "Titel prefix": p.title_prefix.join(" "), - Vorname: p.first_name, - Nachname: p.last_name, - "Title Suffix": p.title_suffix.join(" "), - Position: p.job_title, + "Titel prefix": aus(p.title_prefix.join(" ")), + Vorname: aus(p.first_name), + Nachname: aus(p.last_name), + "Title Suffix": aus(p.title_suffix.join(" ")), + Position: aus(p.job_title), Gruppen: LOLYO_GRUPPE, - Telefon: p.phone ?? "", + Telefon: aus(p.phone ?? ""), Anrede: anrede(p.gender), Sprache: LOLYO_SPRACHE, "E-Mail bestätigt": LOLYO_EMAIL_BESTAETIGT, diff --git a/tests/unit/lolyo.test.ts b/tests/unit/lolyo.test.ts index e536a40..8088fa0 100644 --- a/tests/unit/lolyo.test.ts +++ b/tests/unit/lolyo.test.ts @@ -9,6 +9,7 @@ import { LOLYO_SPALTEN, LOLYO_SPRACHE, lolyoSpalten, + ohneTypografie, type LolyoQuelle, } from "@/lib/lolyo"; @@ -107,6 +108,42 @@ describe("Die Zuordnung", () => { }); }); +describe("Typografische Zeichen", () => { + it("weichen dem Bindestrich", () => { + // Der Gedankenstrich kam im Zielsystem als „–" an. Ein Bindestrich sagt + // dasselbe und ist in beiden Kodierungen dasselbe Byte. + expect(ohneTypografie("Angestellte/r – F&E")).toBe("Angestellte/r - F&E"); + expect(ohneTypografie("Lehrling — Wolkersdorf")).toBe("Lehrling - Wolkersdorf"); + }); + + it("gelten für Anführungszeichen, Auslassungspunkte und geschützte Leerzeichen", () => { + expect(ohneTypografie("„Schnitte“")).toBe('"Schnitte"'); + expect(ohneTypografie("O’Brien")).toBe("O'Brien"); + expect(ohneTypografie("und so weiter …")).toBe("und so weiter ..."); + expect(ohneTypografie("W 17")).toBe("W 17"); + }); + + it("lassen Umlaute in Ruhe", () => { + // „Bäckerei" zu „Baeckerei" zu machen hiesse, ein Problem zu verstecken, + // das dann auch Namen beträfe — und dort wäre es keine Kosmetik mehr. + expect(ohneTypografie("Arbeiter/in – Bäckerei")).toBe("Arbeiter/in - Bäckerei"); + expect(ohneTypografie("Müller-Weiß")).toBe("Müller-Weiß"); + }); + + it("gelten für jede Zelle aus den Daten, nicht nur für die Position", () => { + const z = zeile({ job_title: "Angestellte/r – F&E", last_name: "O’Brien", title_suffix: ["M–A"] }); + expect(z.Position).toBe("Angestellte/r - F&E"); + expect(z.Nachname).toBe("O'Brien"); + expect(z["Title Suffix"]).toBe("M-A"); + }); + + it("gelten nicht für das Passwort", () => { + // Ein ersetztes Zeichen fiele niemandem auf — es gäbe nur ein Konto, in + // das niemand hineinkommt. + expect(baueLolyoZeile(person(), "Pass–wort–2026").Passwort).toBe("Pass–wort–2026"); + }); +}); + describe("Die Datei", () => { it("ist semikolongetrennt und ohne BOM", () => { // Das BOM hinge unsichtbar am Namen der ersten Spalte, und