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
parent d0895412d8
commit c41c69d0d1
2 changed files with 143 additions and 30 deletions

View File

@@ -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');
});
});

View File

@@ -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 ?? '';
}