diff --git a/frontend/src/lib/person/personLifeDates.spec.ts b/frontend/src/lib/person/personLifeDates.spec.ts index cbe23ab8..934395e0 100644 --- a/frontend/src/lib/person/personLifeDates.spec.ts +++ b/frontend/src/lib/person/personLifeDates.spec.ts @@ -1,24 +1,113 @@ -import { describe, it, expect } from 'vitest'; -import { formatLifeDateRange } from './personLifeDates'; +import { describe, expect, it } from 'vitest'; +import { formatLifeDate, formatLifeDateRange } from './personLifeDates'; +// Delegates all precision rendering to formatDocumentDate — these tests pin the +// composition (glyphs, dash, empty sides) and one rendering per precision so a +// regression in the delegation is caught here, not on a person card. describe('formatLifeDateRange', () => { - it('returns both dates when birth and death year are given', () => { - expect(formatLifeDateRange(1882, 1944)).toBe('* 1882 – † 1944'); + describe('both dates (de default)', () => { + it('renders DAY precision as full dates', () => { + expect(formatLifeDateRange('1901-03-14', 'DAY', '1944-11-02', 'DAY')).toBe( + '* 14. März 1901 – † 2. November 1944' + ); + }); + + it('renders MONTH precision as month + year', () => { + expect(formatLifeDateRange('1901-03-01', 'MONTH', '1944-11-01', 'MONTH')).toBe( + '* März 1901 – † November 1944' + ); + }); + + it('renders YEAR precision as bare years', () => { + expect(formatLifeDateRange('1901-01-01', 'YEAR', '1944-01-01', 'YEAR')).toBe( + '* 1901 – † 1944' + ); + }); + + it('renders mixed precisions per side', () => { + expect(formatLifeDateRange('1901-03-14', 'DAY', '1944-01-01', 'YEAR')).toBe( + '* 14. März 1901 – † 1944' + ); + }); + + it('renders APPROX precision with the ca. prefix (legacy imports)', () => { + expect(formatLifeDateRange('1901-01-01', 'APPROX', '1944-01-01', 'APPROX')).toBe( + '* ca. 1901 – † ca. 1944' + ); + }); }); - it('returns only birth year when only birthYear is given', () => { - expect(formatLifeDateRange(1882, undefined)).toBe('* 1882'); + describe('single sides and empty states', () => { + it('renders birth only without dash or dagger', () => { + expect(formatLifeDateRange('1901-03-14', 'DAY', null, null)).toBe('* 14. März 1901'); + }); + + it('renders death only without dash or asterisk', () => { + expect(formatLifeDateRange(null, null, '1944-11-02', 'DAY')).toBe('† 2. November 1944'); + }); + + it('renders YEAR birth only', () => { + expect(formatLifeDateRange('1882-01-01', 'YEAR', null, null)).toBe('* 1882'); + }); + + it('renders APPROX death only', () => { + expect(formatLifeDateRange(null, null, '1944-01-01', 'APPROX')).toBe('† ca. 1944'); + }); + + it('returns empty string when both dates are null', () => { + expect(formatLifeDateRange(null, null, null, null)).toBe(''); + }); + + it('returns empty string when both dates are null even with UNKNOWN precisions', () => { + expect(formatLifeDateRange(null, 'UNKNOWN', null, 'UNKNOWN')).toBe(''); + }); + + it('falls back to YEAR rendering when a precision is missing', () => { + expect(formatLifeDateRange('1901-01-01', null, null, null)).toBe('* 1901'); + }); }); - it('returns only death year when only deathYear is given', () => { - expect(formatLifeDateRange(undefined, 1944)).toBe('† 1944'); - }); + describe('locales (German-month-leak guard)', () => { + it('renders DAY precision in English', () => { + expect(formatLifeDateRange('1901-03-14', 'DAY', null, null, 'en')).toBe('* March 14, 1901'); + }); - it('returns empty string when neither year is given', () => { - expect(formatLifeDateRange(undefined, undefined)).toBe(''); - }); + it('renders MONTH precision in English', () => { + expect(formatLifeDateRange('1901-03-01', 'MONTH', null, null, 'en')).toBe('* March 1901'); + }); - it('returns empty string when both are null', () => { - expect(formatLifeDateRange(null, null)).toBe(''); + it('renders DAY precision in Spanish', () => { + expect(formatLifeDateRange('1901-03-14', 'DAY', null, null, 'es')).toBe( + '* 14 de marzo de 1901' + ); + }); + + it('renders MONTH precision in Spanish', () => { + expect(formatLifeDateRange('1901-03-01', 'MONTH', null, null, 'es')).toBe('* marzo de 1901'); + }); + }); +}); + +// Single-date helper for components that must keep the * / † glyphs in their own +// aria-hidden markup (PersonCard, PersonHoverCard) instead of in the string. +describe('formatLifeDate', () => { + it('renders a DAY-precision date without any glyph', () => { + expect(formatLifeDate('1901-03-14', 'DAY')).toBe('14. März 1901'); + }); + + it('renders an APPROX-precision date (legacy imports)', () => { + expect(formatLifeDate('1901-01-01', 'APPROX')).toBe('ca. 1901'); + }); + + it('falls back to YEAR rendering when precision is missing', () => { + expect(formatLifeDate('1901-01-01', null)).toBe('1901'); + }); + + it('returns empty string for a null date', () => { + expect(formatLifeDate(null, 'DAY')).toBe(''); + }); + + it('renders in the requested locale', () => { + expect(formatLifeDate('1901-03-14', 'DAY', 'en')).toBe('March 14, 1901'); }); }); diff --git a/frontend/src/lib/person/personLifeDates.ts b/frontend/src/lib/person/personLifeDates.ts index 4adbb2fa..c6d72adc 100644 --- a/frontend/src/lib/person/personLifeDates.ts +++ b/frontend/src/lib/person/personLifeDates.ts @@ -1,20 +1,44 @@ +import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate'; + /** - * Formats the life date range for a person. - * Examples: - * * 1882 – † 1944 (both) - * * 1882 (birth only) - * † 1944 (death only) - * "" (neither) + * Formats one life date (birth or death) at the precision the data claims, + * delegating all rendering to {@link formatDocumentDate}. Returns '' for a + * missing date. Carries no * / † glyph — components that need the glyphs wrap + * them in their own `aria-hidden` markup so screen readers only hear the date. + * + * A missing precision falls back to YEAR: pre-V76 rows only knew a year, and + * a bare year is the only safe rendering for a date without precision metadata. */ -export function formatLifeDateRange(birthYear?: number | null, deathYear?: number | null): string { - if (birthYear && deathYear) { - return `* ${birthYear} – † ${deathYear}`; +export function formatLifeDate( + date: string | null | undefined, + precision: DatePrecision | null | undefined, + locale?: string +): string { + if (!date) { + return ''; } - if (birthYear) { - return `* ${birthYear}`; - } - if (deathYear) { - return `† ${deathYear}`; - } - return ''; + return formatDocumentDate(date, precision ?? 'YEAR', null, null, locale); +} + +/** + * Formats the full life date range as plain text, e.g. for dropdown subtitles. + * Examples: + * * 14. März 1901 – † 2. November 1944 (both, DAY precision) + * * 1882 (birth only, YEAR precision) + * † ca. 1944 (death only, APPROX precision) + * "" (neither) + */ +export function formatLifeDateRange( + birthDate: string | null | undefined, + birthDatePrecision: DatePrecision | null | undefined, + deathDate: string | null | undefined, + deathDatePrecision: DatePrecision | null | undefined, + locale?: string +): string { + const birth = birthDate ? `* ${formatLifeDate(birthDate, birthDatePrecision, locale)}` : null; + const death = deathDate ? `† ${formatLifeDate(deathDate, deathDatePrecision, locale)}` : null; + if (birth && death) { + return `${birth} – ${death}`; + } + return birth ?? death ?? ''; }