feat(person): formatLifeDateRange takes date + precision, delegates to formatDocumentDate

New formatLifeDate single-date helper carries no glyph so cards can wrap
* / † in aria-hidden spans. Missing precision falls back to YEAR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-12 18:06:16 +02:00
committed by marcel
parent 29ada9b681
commit adac1b1f99
2 changed files with 143 additions and 30 deletions

View File

@@ -1,24 +1,113 @@
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { formatLifeDateRange } from './personLifeDates'; 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', () => { describe('formatLifeDateRange', () => {
it('returns both dates when birth and death year are given', () => { describe('both dates (de default)', () => {
expect(formatLifeDateRange(1882, 1944)).toBe('* 1882 † 1944'); 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', () => { describe('single sides and empty states', () => {
expect(formatLifeDateRange(1882, undefined)).toBe('* 1882'); 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', () => { describe('locales (German-month-leak guard)', () => {
expect(formatLifeDateRange(undefined, 1944)).toBe('† 1944'); 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', () => { it('renders MONTH precision in English', () => {
expect(formatLifeDateRange(undefined, undefined)).toBe(''); expect(formatLifeDateRange('1901-03-01', 'MONTH', null, null, 'en')).toBe('* March 1901');
}); });
it('returns empty string when both are null', () => { it('renders DAY precision in Spanish', () => {
expect(formatLifeDateRange(null, null)).toBe(''); 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');
}); });
}); });

View File

@@ -1,20 +1,44 @@
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
/** /**
* Formats the life date range for a person. * Formats one life date (birth or death) at the precision the data claims,
* Examples: * delegating all rendering to {@link formatDocumentDate}. Returns '' for a
* * 1882 † 1944 (both) * missing date. Carries no * / † glyph — components that need the glyphs wrap
* * 1882 (birth only) * them in their own `aria-hidden` markup so screen readers only hear the date.
* † 1944 (death only) *
* "" (neither) * 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 { export function formatLifeDate(
if (birthYear && deathYear) { date: string | null | undefined,
return `* ${birthYear} ${deathYear}`; precision: DatePrecision | null | undefined,
locale?: string
): string {
if (!date) {
return '';
} }
if (birthYear) { return formatDocumentDate(date, precision ?? 'YEAR', null, null, locale);
return `* ${birthYear}`; }
}
if (deathYear) { /**
return `${deathYear}`; * Formats the full life date range as plain text, e.g. for dropdown subtitles.
} * Examples:
return ''; * * 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 ?? '';
} }