From 836c9594d4f89f5e6c6f46fca77f6c39f67db089 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 11 Jun 2026 08:30:23 +0200 Subject: [PATCH] refactor(person): one author-name fallback, localized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit joinNameOrUnknown()/unknownPersonName() in personFormat.ts replace the three hand-rolled '[Unbekannt]' literals (geschichte/utils, GeschichtenCard, personOption) — the fallback is now the i18n key person_unknown (de/en/es), and formatAuthorDisplayName localizes the server-side literal on the pass-through path. Review round 3: Felix, Markus, Leonie (7). Co-Authored-By: Claude Fable 5 --- frontend/messages/de.json | 2 ++ frontend/messages/en.json | 2 ++ frontend/messages/es.json | 2 ++ frontend/src/lib/geschichte/GeschichtenCard.svelte | 6 ++---- frontend/src/lib/geschichte/utils.ts | 10 ++++++---- frontend/src/lib/person/personFormat.ts | 14 ++++++++++++++ frontend/src/lib/person/personOption.ts | 3 ++- 7 files changed, 30 insertions(+), 9 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 997a8dda..204026cc 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -1214,8 +1214,10 @@ "error_journey_note_too_long": "Die Notiz ist zu lang (maximal 2000 Zeichen).", "error_geschichte_title_too_long": "Der Titel ist zu lang (maximal 255 Zeichen).", "error_geschichte_intro_too_long": "Die Einleitung ist zu lang (maximal 4000 Zeichen).", + "person_unknown": "[Unbekannt]", "error_geschichte_title_too_long": "Der Titel ist zu lang (maximal 255 Zeichen).", "error_geschichte_intro_too_long": "Die Einleitung ist zu lang (maximal 4000 Zeichen).", + "person_unknown": "[Unbekannt]", "error_journey_document_already_added": "Dieser Brief ist bereits in der Lesereise enthalten.", "error_geschichte_type_immutable": "Der Typ einer Geschichte kann nach der Erstellung nicht mehr geändert werden." } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 07a5a503..4bce75a0 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1214,8 +1214,10 @@ "error_journey_note_too_long": "The note is too long (maximum 2000 characters).", "error_geschichte_title_too_long": "The title is too long (maximum 255 characters).", "error_geschichte_intro_too_long": "The introduction is too long (maximum 4000 characters).", + "person_unknown": "[Unknown]", "error_geschichte_title_too_long": "The title is too long (maximum 255 characters).", "error_geschichte_intro_too_long": "The introduction is too long (maximum 4000 characters).", + "person_unknown": "[Unknown]", "error_journey_document_already_added": "This letter is already included in the reading journey.", "error_geschichte_type_immutable": "The type of a story cannot be changed after creation." } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index de4beda4..8a8b5706 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -1214,8 +1214,10 @@ "error_journey_note_too_long": "La nota es demasiado larga (máximo 2000 caracteres).", "error_geschichte_title_too_long": "El título es demasiado largo (máximo 255 caracteres).", "error_geschichte_intro_too_long": "La introducción es demasiado larga (máximo 4000 caracteres).", + "person_unknown": "[Desconocido]", "error_geschichte_title_too_long": "El título es demasiado largo (máximo 255 caracteres).", "error_geschichte_intro_too_long": "La introducción es demasiado larga (máximo 4000 caracteres).", + "person_unknown": "[Desconocido]", "error_journey_document_already_added": "Esta carta ya está incluida en el viaje de lectura.", "error_geschichte_type_immutable": "El tipo de una historia no se puede cambiar después de su creación." } diff --git a/frontend/src/lib/geschichte/GeschichtenCard.svelte b/frontend/src/lib/geschichte/GeschichtenCard.svelte index 353909fe..3b6349f3 100644 --- a/frontend/src/lib/geschichte/GeschichtenCard.svelte +++ b/frontend/src/lib/geschichte/GeschichtenCard.svelte @@ -3,6 +3,7 @@ import { m } from '$lib/paraglide/messages.js'; import type { components } from '$lib/generated/api'; import { plainExcerpt } from '$lib/shared/utils/extractText'; import { formatDate } from '$lib/shared/utils/date'; +import { formatAuthorName } from './utils'; type GeschichteSummary = components['schemas']['GeschichteSummary']; @@ -24,10 +25,7 @@ function formatPublishedDate(g: GeschichteSummary): string | null { } function authorName(g: GeschichteSummary): string { - const a = g.author; - if (!a) return ''; - const full = [a.firstName, a.lastName].filter(Boolean).join(' ').trim(); - return full || '[Unbekannt]'; + return formatAuthorName(g.author); } diff --git a/frontend/src/lib/geschichte/utils.ts b/frontend/src/lib/geschichte/utils.ts index 25b4fa1f..47722729 100644 --- a/frontend/src/lib/geschichte/utils.ts +++ b/frontend/src/lib/geschichte/utils.ts @@ -1,5 +1,6 @@ import { formatDate } from '$lib/shared/utils/date'; import { m } from '$lib/paraglide/messages.js'; +import { joinNameOrUnknown, unknownPersonName } from '$lib/person/personFormat'; type AuthorSummary = { firstName?: string; lastName?: string }; type DocumentMeta = { documentDate?: string; senderName?: string; receiverName?: string }; @@ -7,13 +8,14 @@ type AuthorView = { displayName: string }; export function formatAuthorName(author: AuthorSummary | null | undefined): string { if (!author) return ''; - const full = [author.firstName, author.lastName].filter(Boolean).join(' ').trim(); - // Mirrors the server-side fallback in GeschichteService.toView — email is no longer exposed. - return full || '[Unbekannt]'; + // Email is no longer exposed — names or the localized fallback only. + return joinNameOrUnknown(author.firstName, author.lastName); } export function formatAuthorDisplayName(author: AuthorView | null | undefined): string { - return author?.displayName ?? ''; + if (!author) return ''; + // The server-side fallback is the literal '[Unbekannt]' — localize it here. + return author.displayName === '[Unbekannt]' ? unknownPersonName() : author.displayName; } export function formatPublishedAt( diff --git a/frontend/src/lib/person/personFormat.ts b/frontend/src/lib/person/personFormat.ts index 1ce32e71..37cc7f88 100644 --- a/frontend/src/lib/person/personFormat.ts +++ b/frontend/src/lib/person/personFormat.ts @@ -1,4 +1,5 @@ import { formatDate } from '$lib/shared/utils/date'; +import { m } from '$lib/paraglide/messages.js'; type Person = { firstName?: string | null; lastName: string; displayName: string }; type DocForMeta = { @@ -17,6 +18,19 @@ function djb2(str: string): number { return Math.abs(hash); } +/** Localized fallback when a person has no name parts. */ +export function unknownPersonName(): string { + return m.person_unknown(); +} + +/** + * Single source for the join-names-or-fallback rule. Mirrors the server-side + * fallback in GeschichteService.toView (which emits the literal '[Unbekannt]'). + */ +export function joinNameOrUnknown(firstName?: string | null, lastName?: string | null): string { + return [firstName, lastName].filter(Boolean).join(' ').trim() || unknownPersonName(); +} + export function getInitials(name: string): string { const words = name.trim().split(/\s+/).filter(Boolean); if (words.length === 0) return ''; diff --git a/frontend/src/lib/person/personOption.ts b/frontend/src/lib/person/personOption.ts index cc594d38..2cf3f427 100644 --- a/frontend/src/lib/person/personOption.ts +++ b/frontend/src/lib/person/personOption.ts @@ -1,4 +1,5 @@ import type { components } from '$lib/generated/api'; +import { joinNameOrUnknown } from './personFormat'; type Person = components['schemas']['Person']; @@ -21,6 +22,6 @@ export function toPersonOption(p: { }): PersonOption { return { id: p.id, - displayName: [p.firstName, p.lastName].filter(Boolean).join(' ') || '[Unbekannt]' + displayName: joinNameOrUnknown(p.firstName, p.lastName) }; }