Umlaute statt Umschreibungen

Oberflaeche, Fehlermeldungen und Kommentare schrieben durchgehend ae, oe, ue
und ss. Dafuer gab es keinen Grund - die Anwendung ist deutschsprachig und
laeuft durchgehend in UTF-8.

Betroffen waren 55 Dateien, darunter alle sichtbaren Texte: Guthaben-Zaehler,
Import, Nutzerverwaltung, Passwortwechsel und die Zwei-Faktor-Einrichtung.

Der Name einer Migration bleibt im README-Beispiel bewusst bei ASCII, weil
daraus ein Verzeichnisname wird.
This commit is contained in:
2026-09-18 16:57:39 +02:00
parent b9364151b0
commit 724f68644b
49 changed files with 256 additions and 254 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ export interface TestDatabase {
}
/**
* Startet eine Postgres-Instanz im Speicher und stellt sie ueber einen lokalen
* Startet eine Postgres-Instanz im Speicher und stellt sie über einen lokalen
* Port bereit, damit Prisma sich wie an eine echte Datenbank verbindet.
* So laufen die Tests ohne Docker oder installiertes Postgres.
*/
@@ -26,7 +26,7 @@ export async function startTestDatabase(port: number): Promise<TestDatabase> {
return {
// connection_limit=1, weil die Testinstanz immer nur eine Verbindung
// gleichzeitig bedient. Echtes Postgres braucht die Einschraenkung nicht.
// gleichzeitig bedient. Echtes Postgres braucht die Einschränkung nicht.
url: `postgresql://postgres:postgres@127.0.0.1:${port}/postgres?connection_limit=1`,
stop: async () => {
await server.stop();
+12 -12
View File
@@ -81,7 +81,7 @@ describe('Anmeldung', () => {
assert.equal(response.statusCode, 401);
});
it('verraet nicht, ob es den Benutzernamen gibt', async () => {
it('verrät nicht, ob es den Benutzernamen gibt', async () => {
const unknownUser = await app.inject({
method: 'POST',
url: '/api/auth/login',
@@ -97,7 +97,7 @@ describe('Anmeldung', () => {
assert.equal(unknownUser.json().message, wrongPassword.json().message);
});
it('laesst ein gesperrtes Konto nicht herein', async () => {
it('lässt ein gesperrtes Konto nicht herein', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
@@ -129,7 +129,7 @@ describe('CSRF-Schutz', () => {
assert.equal(response.json().error, 'FORBIDDEN');
});
it('blockt einen gefaelschten Token', async () => {
it('blockt einen gefälschten Token', async () => {
const credentials = await login('team');
const response = await app.inject({
@@ -142,7 +142,7 @@ describe('CSRF-Schutz', () => {
assert.equal(response.statusCode, 403);
});
it('laesst die Anfrage mit passendem Token durch', async () => {
it('lässt die Anfrage mit passendem Token durch', async () => {
const credentials = await login('team');
const response = await app.inject({
@@ -155,7 +155,7 @@ describe('CSRF-Schutz', () => {
assert.equal(response.statusCode, 201);
});
it('laesst lesende Anfragen ohne Token zu', async () => {
it('lässt lesende Anfragen ohne Token zu', async () => {
const credentials = await login('gast');
const response = await app.inject({
@@ -227,7 +227,7 @@ describe('Erstpasswort', () => {
method: 'POST',
url: '/api/players',
headers: authHeaders(credentials),
payload: { displayName: 'Zu frueh' },
payload: { displayName: 'Zu früh' },
});
assert.equal(response.statusCode, 403);
@@ -251,8 +251,8 @@ describe('Erstpasswort', () => {
});
});
describe('Guthaben ueber die API', () => {
it('ein CC-Paket schlaegt in beiden Toepfen auf', async () => {
describe('Guthaben über die API', () => {
it('ein CC-Paket schlägt in beiden Töpfen auf', async () => {
const credentials = await login('team');
const created = await app.inject({
@@ -287,11 +287,11 @@ describe('Guthaben ueber die API', () => {
});
});
describe('Auslieferung der Oberflaeche', () => {
describe('Auslieferung der Oberfläche', () => {
// Diese Header sind der Grund, warum ein Update beim Nutzer ankommt. Wird die
// index.html cachebar ausgeliefert, sieht er nach einem Deploy weiter die
// alte Oberflaeche - ohne dass irgendwo ein Fehler auftaucht.
it('laesst die index.html nicht zwischenspeichern', async () => {
// alte Oberfläche - ohne dass irgendwo ein Fehler auftaucht.
it('lässt die index.html nicht zwischenspeichern', async () => {
const response = await app.inject({ method: 'GET', url: '/' });
assert.equal(response.statusCode, 200);
@@ -309,7 +309,7 @@ describe('Auslieferung der Oberflaeche', () => {
assert.match(response.headers['cache-control'] as string, /no-cache/);
});
it('laesst gehashte Assets lange liegen', async () => {
it('lässt gehashte Assets lange liegen', async () => {
const page = await app.inject({ method: 'GET', url: '/' });
const asset = /\/assets\/[^"]+\.js/.exec(page.body)?.[0];
assert.ok(asset, 'die Seite verweist auf ein Asset');
+4 -4
View File
@@ -14,7 +14,7 @@ let ctx: { actorId: number; actorName: string };
const CSV = [
'Bestellnummer;Steam-ID;Spielername;Produkt;Kaufdatum',
'A-1001;76561198000000001;Rabe;CC-Paket Gross;2025-01-15',
'A-1001;76561198000000001;Rabe;CC-Paket Groß;2025-01-15',
'A-1002;76561198000000002;Fuchs;Redesign Gutschein;2025-01-16',
'A-1003;76561198000000001;Rabe;Spielerfarbe Blau;2025-01-17',
'A-1004;76561198000000003;Dachs;CC-Paket Klein;2025-01-18',
@@ -92,7 +92,7 @@ describe('Vorschau und Buchung', () => {
const summary = await imports.applyMapping(importId, MAPPING, ctx);
assert.equal(summary.CREATE_PLAYER_AND_BOOK, 3, 'drei neue Spieler mit gueltigem Produkt');
assert.equal(summary.CREATE_PLAYER_AND_BOOK, 3, 'drei neue Spieler mit gültigem Produkt');
assert.equal(summary.SKIP_IGNORED, 1, 'die Spielerfarbe wird ignoriert');
assert.equal(summary.SKIP_DUPLICATE, 0);
});
@@ -165,7 +165,7 @@ describe('Vorschau und Buchung', () => {
assert.equal(summary.SKIP_DUPLICATE, 1);
});
it('ueberspringt unbekannte Spieler, wenn sie nicht angelegt werden sollen', async () => {
it('überspringt unbekannte Spieler, wenn sie nicht angelegt werden sollen', async () => {
const csv = ['Bestellnummer;Steam-ID;Produkt', 'D-1;76561198000009999;CC-Paket'].join('\n');
const parsed = imports.parseUpload('fremd.csv', Buffer.from(csv, 'utf8'));
const importId = await imports.createImport('fremd.csv', parsed, ctx);
@@ -184,7 +184,7 @@ describe('Vorschau und Buchung', () => {
assert.equal(summary.SKIP_NO_PLAYER, 1);
});
it('ein abgeschlossener Import laesst sich nicht erneut buchen', async () => {
it('ein abgeschlossener Import lässt sich nicht erneut buchen', async () => {
const csv = ['Bestellnummer;Steam-ID;Produkt', 'E-1;76561198000000001;CC-Paket'].join('\n');
const parsed = imports.parseUpload('nochmal.csv', Buffer.from(csv, 'utf8'));
const importId = await imports.createImport('nochmal.csv', parsed, ctx);
+6 -6
View File
@@ -18,7 +18,7 @@ let audit: Audit;
let db: Db;
let ctx: { actorId: number; actorName: string };
// Ein Export, wie ihn ein Shop liefern koennte: mehr Spalten als die App braucht.
// Ein Export, wie ihn ein Shop liefern könnte: mehr Spalten als die App braucht.
const CSV = [
'Bestellnummer;Steam-ID;Produkt;Email;Rechnungsadresse;Zahlungsart',
'P-1;76561198000000501;CC-Paket;spieler@example.com;Musterweg 1, 12345 Musterstadt;PayPal',
@@ -45,7 +45,7 @@ before(async () => {
audit = await import('../src/services/audit.js');
const user = await db.prisma.user.create({
data: { username: 'datenschuetzer', passwordHash: 'x', role: 'ADMIN' },
data: { username: 'datenschützer', passwordHash: 'x', role: 'ADMIN' },
});
ctx = { actorId: user.id, actorName: user.username };
@@ -80,7 +80,7 @@ describe('Datenminimierung beim Import', () => {
assert.equal(rawAfter['Steam-ID'], '76561198000000501', 'die zugeordnete Spalte bleibt');
});
it('raeumt auch einen verworfenen Import auf', async () => {
it('räumt auch einen verworfenen Import auf', async () => {
const parsed = imports.parseUpload('shop2.csv', Buffer.from(CSV, 'utf8'));
const importId = await imports.createImport('shop2.csv', parsed, ctx);
await imports.applyMapping(importId, MAPPING, ctx);
@@ -116,7 +116,7 @@ describe('Fehlgeschlagene Anmeldung', () => {
});
});
describe('Loeschung eines Spielers', () => {
describe('Löschung eines Spielers', () => {
it('entfernt auch Spuren in Import-Zeilen und Audit-Details', async () => {
// Eigene Bestellnummer, sonst greift zu Recht die Dedup-Logik.
const csv = [
@@ -152,7 +152,7 @@ describe('Loeschung eines Spielers', () => {
const auditEntries = await db.prisma.auditLog.findMany({
where: { targetType: 'player', targetId: String(player.id) },
});
assert.ok(auditEntries.length > 0, 'die Vorgaenge selbst bleiben nachvollziehbar');
assert.ok(auditEntries.length > 0, 'die Vorgänge selbst bleiben nachvollziehbar');
for (const entry of auditEntries) {
const details = JSON.stringify(entry.details ?? {});
assert.ok(
@@ -164,7 +164,7 @@ describe('Loeschung eines Spielers', () => {
});
describe('Aufbewahrung des Audit-Logs', () => {
it('loescht Eintraege jenseits der Frist, neuere bleiben', async () => {
it('löscht Einträge jenseits der Frist, neuere bleiben', async () => {
const alt = new Date(Date.now() - 400 * 24 * 3600 * 1000);
const veraltet = await db.prisma.auditLog.create({
+17 -17
View File
@@ -40,7 +40,7 @@ after(async () => {
await database.stop();
});
describe('Guthaben-Toepfe', () => {
describe('Guthaben-Töpfe', () => {
it('ein CC-Paket erzeugt CC UND bezahltes Redesign', async () => {
const playerId = await freshPlayer('Paket-Spieler');
const { balances } = await credits.bookCcPackage(playerId, ctx);
@@ -58,7 +58,7 @@ describe('Guthaben-Toepfe', () => {
assert.equal(balances.REDESIGN_PAID, 1);
});
it('Kulanz landet im eigenen Topf und zaehlt zur Redesign-Summe', async () => {
it('Kulanz landet im eigenen Topf und zählt zur Redesign-Summe', async () => {
const playerId = await freshPlayer('Kulanz-Spieler');
const { balances } = await credits.bookKulanz(playerId, ctx);
@@ -67,7 +67,7 @@ describe('Guthaben-Toepfe', () => {
assert.equal(balances.redesignTotal, 1);
});
it('ein ueberzogenes Konto wird zugelassen, nicht blockiert', async () => {
it('ein überzogenes Konto wird zugelassen, nicht blockiert', async () => {
const playerId = await freshPlayer('Minus-Spieler');
const result = await characters.createCharacter(playerId, { name: 'Vorab-CC' }, ctx);
@@ -107,7 +107,7 @@ describe('CC-Lebenszyklus', () => {
assert.equal(
result.character.lineageId,
first.character.lineageId,
'die Kette behaelt ihre lineageId',
'die Kette behält ihre lineageId',
);
assert.equal(result.balances.REDESIGN_PAID, 0);
});
@@ -130,7 +130,7 @@ describe('CC-Lebenszyklus', () => {
});
describe('Reaktivierung', () => {
it('ein wegen Inaktivitaet entfernter CC kommt gratis zurueck', async () => {
it('ein wegen Inaktivität entfernter CC kommt gratis zurück', async () => {
const playerId = await freshPlayer('Inaktiver');
await credits.bookCcPackage(playerId, ctx);
const created = await characters.createCharacter(playerId, { name: 'Geputzt' }, ctx);
@@ -144,7 +144,7 @@ describe('Reaktivierung', () => {
assert.deepEqual(result.balances, before, 'kein Guthaben wurde bewegt');
});
it('ein reworkter CC kommt NICHT gratis zurueck', async () => {
it('ein reworkter CC kommt NICHT gratis zurück', async () => {
const playerId = await freshPlayer('Rework-Spieler');
await credits.bookCcPackage(playerId, ctx);
const first = await characters.createCharacter(playerId, { name: 'Alt' }, ctx);
@@ -160,7 +160,7 @@ describe('Reaktivierung', () => {
);
});
it('ein reworkter CC kommt gegen CC-Guthaben zurueck', async () => {
it('ein reworkter CC kommt gegen CC-Guthaben zurück', async () => {
const playerId = await freshPlayer('Wiederbeleber');
await credits.bookCcPackage(playerId, ctx);
await credits.bookCcPackage(playerId, ctx);
@@ -178,7 +178,7 @@ describe('Reaktivierung', () => {
assert.equal(result.balances.CC, before.CC - 1);
});
it('auch ein aus anderem Grund entfernter CC kommt gratis zurueck', async () => {
it('auch ein aus anderem Grund entfernter CC kommt gratis zurück', async () => {
const playerId = await freshPlayer('Einheit-weg');
await credits.bookCcPackage(playerId, ctx);
const created = await characters.createCharacter(playerId, { name: 'MTF-Operativer' }, ctx);
@@ -187,19 +187,19 @@ describe('Reaktivierung', () => {
created.character.id,
'OTHER',
ctx,
'Einheit bis Maerz entfernt',
'Einheit bis März entfernt',
);
const deactivated = await db.prisma.character.findUniqueOrThrow({
where: { id: created.character.id },
});
assert.equal(deactivated.inactiveNote, 'Einheit bis Maerz entfernt', 'der Grund bleibt lesbar');
assert.equal(deactivated.inactiveNote, 'Einheit bis März entfernt', 'der Grund bleibt lesbar');
const before = await credits.getBalances(playerId);
const result = await characters.reactivateFree(created.character.id, ctx);
assert.equal(result.character.status, 'ACTIVE');
assert.equal(result.character.inactiveNote, null, 'die Notiz wird beim Zurueckholen geloescht');
assert.equal(result.character.inactiveNote, null, 'die Notiz wird beim Zurückholen gelöscht');
assert.deepEqual(result.balances, before, 'kein Guthaben wurde bewegt');
});
@@ -222,7 +222,7 @@ describe('Reaktivierung', () => {
});
describe('Korrekturen', () => {
it('ein Paket-Storno nimmt beide Zeilen gemeinsam zurueck', async () => {
it('ein Paket-Storno nimmt beide Zeilen gemeinsam zurück', async () => {
const playerId = await freshPlayer('Storno-Spieler');
const booked = await credits.bookCcPackage(playerId, ctx);
@@ -236,7 +236,7 @@ describe('Korrekturen', () => {
assert.equal(result.balances.REDESIGN_PAID, 0);
});
it('dieselbe Buchung laesst sich kein zweites Mal stornieren', async () => {
it('dieselbe Buchung lässt sich kein zweites Mal stornieren', async () => {
const playerId = await freshPlayer('Doppel-Storno');
const booked = await credits.bookKulanz(playerId, ctx);
const entry = await db.prisma.creditLedger.findFirstOrThrow({
@@ -266,8 +266,8 @@ describe('Korrekturen', () => {
);
});
it('eine Ruecknahme bucht das Guthaben zurueck und reaktiviert den Vorgaenger', async () => {
const playerId = await freshPlayer('Ruecknehmer');
it('eine Rücknahme bucht das Guthaben zurück und reaktiviert den Vorgänger', async () => {
const playerId = await freshPlayer('Rücknehmer');
await credits.bookCcPackage(playerId, ctx);
const first = await characters.createCharacter(playerId, { name: 'Original' }, ctx);
const redesigned = await characters.redesignCharacter(
@@ -292,7 +292,7 @@ describe('Korrekturen', () => {
assert.equal(restored.inactiveReason, null);
});
it('eine Ruecknahme mitten in der Kette wird abgelehnt', async () => {
it('eine Rücknahme mitten in der Kette wird abgelehnt', async () => {
const playerId = await freshPlayer('Ketten-Spieler');
await credits.bookCcPackage(playerId, ctx);
await credits.bookRedesignPackage(playerId, ctx);
@@ -309,7 +309,7 @@ describe('Korrekturen', () => {
);
await assert.rejects(
() => characters.revertCharacter(second.character.id, 'Zu spaet', ctx),
() => characters.revertCharacter(second.character.id, 'Zu spät', ctx),
(error: Error & { code?: string }) => error.code === 'HAS_SUCCESSORS',
);
});
+8 -8
View File
@@ -39,7 +39,7 @@ describe('Profil auslesen', () => {
assert.equal(cc.purchasedAt?.getDate(), 6);
});
it('erkennt stornierte Kaeufe', () => {
it('erkennt stornierte Käufe', () => {
const revoked = profile.packages.find((p) => p.packageBoughtId === 9007);
assert.equal(revoked?.revoked, true);
});
@@ -54,7 +54,7 @@ describe('Profil auslesen', () => {
'donations_data[9004] = {kaputt"id"',
);
const result = parseProfile(broken, '76561198000000999');
assert.equal(result.packages.length, 7, 'nur die kaputte Zeile faellt weg');
assert.equal(result.packages.length, 7, 'nur die kaputte Zeile fällt weg');
});
});
@@ -73,21 +73,21 @@ describe('Pakete zuordnen', () => {
});
});
it('zaehlt "Custom Character Redesign" als Redesign, nicht als CC', () => {
it('zählt "Custom Character Redesign" als Redesign, nicht als CC', () => {
assert.deepEqual(classifyPackage(pkg({ packageName: 'Custom Character Redesign' })), {
kind: 'package',
target: 'REDESIGN_PACKAGE',
});
});
it('liest den Verbrauchszaehler samt Anzahl', () => {
it('liest den Verbrauchszähler samt Anzahl', () => {
const result = classifyPackage(
pkg({ packageName: 'CC-Paket oder Redesign genutzt | 3 Mal' }),
);
assert.deepEqual(result, { kind: 'usage_counter', times: 3 });
});
it('ignoriert Merch und Raenge trotz SCP-RP-Tag', () => {
it('ignoriert Merch und Ränge trotz SCP-RP-Tag', () => {
assert.equal(classifyPackage(pkg({ packageName: 'FG-Tasse | Merch' })).kind, 'ignore');
assert.equal(classifyPackage(pkg({ packageName: '25 Ingame-Level' })).kind, 'ignore');
assert.equal(classifyPackage(pkg({ packageName: 'VIP (12 Monate)' })).kind, 'ignore');
@@ -105,7 +105,7 @@ describe('Pakete zuordnen', () => {
assert.equal(result.kind, 'ignore');
});
it('schiebt unklare CC-Titel in die Handpruefung statt zu raten', () => {
it('schiebt unklare CC-Titel in die Handprüfung statt zu raten', () => {
assert.equal(classifyPackage(pkg({ packageName: 'CC Sonderaktion Winter' })).kind, 'unclear');
});
});
@@ -116,13 +116,13 @@ describe('SteamID-Schreibweisen', () => {
const modern = '[U:1:39735271]';
const sixtyFour = '76561198000000999';
it('fuehrt alle drei Formate auf dieselbe ID zusammen', () => {
it('führt alle drei Formate auf dieselbe ID zusammen', () => {
assert.equal(normalizeSteamId(legacy), sixtyFour);
assert.equal(normalizeSteamId(modern), sixtyFour);
assert.equal(normalizeSteamId(sixtyFour), sixtyFour);
});
it('rechnet zurueck in die Legacy-Form', () => {
it('rechnet zurück in die Legacy-Form', () => {
assert.equal(toLegacySteamId(sixtyFour), legacy);
});