feat(frontend): add precision-aware document date formatter

Adds formatDocumentDate — a pure, branch-per-precision label function that
renders a document date at exactly the precision the data claims (DAY → full
date, MONTH → "Juni 1916", SEASON → localized season word, YEAR → "1916",
APPROX → "ca. 1916", RANGE with collapse/expand/open-ended, UNKNOWN → "Datum
unbekannt"). Delegates to the existing date.ts helpers (shared T12:00:00
convention) and routes every localized word through Paraglide.

A shared docs/date-label-fixtures.json table is asserted by this spec and will
be asserted by the Java title formatter, as the drift guard requested in
review (Markus/Sara). Adds de/en/es precision/season/edit-form i18n keys.

Assumption: SEASON structured label is localized per locale (Decision 4),
with the verbatim raw cell preserved as a separate secondary line by callers.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-27 11:43:32 +02:00
parent e4a154406e
commit f2a74a6064
6 changed files with 419 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { formatDocumentDate } from './documentDate';
import { m } from '$lib/paraglide/messages.js';
// ─── Shared drift-guard fixture ─────────────────────────────────────────────
// The same table is asserted by the Java DocumentTitleFormatter test so the two
// label implementations cannot drift. Expected values are the German canonical
// form (see docs/date-label-fixtures.json).
type FixtureCase = {
name: string;
precision: string;
anchor: string | null;
end: string | null;
raw: string | null;
expected: string;
};
const fixtures = JSON.parse(
readFileSync(resolve(process.cwd(), '../docs/date-label-fixtures.json'), 'utf-8')
) as { cases: FixtureCase[] };
describe('formatDocumentDate shared fixture table (de)', () => {
for (const c of fixtures.cases) {
it(c.name, () => {
expect(
formatDocumentDate(
c.anchor,
c.precision as Parameters<typeof formatDocumentDate>[1],
c.end,
c.raw,
'de'
)
).toBe(c.expected);
});
}
});
// ─── Anti-fabrication: suppressed components never leak ──────────────────────
describe('formatDocumentDate suppressed precision components', () => {
it('YEAR of a June date renders the year only, never the month', () => {
const label = formatDocumentDate('1916-06-15', 'YEAR');
expect(label).toBe('1916');
expect(label).not.toContain('Juni');
expect(label).not.toContain('15');
});
it('MONTH never renders the day-of-month', () => {
const label = formatDocumentDate('1916-06-01', 'MONTH', null, 'Juni 1916');
expect(label).toBe('Juni 1916');
expect(label).not.toMatch(/\b1\.\s/);
});
});
// ─── i18n: localized structured label ───────────────────────────────────────
describe('formatDocumentDate localization', () => {
it('localizes the UNKNOWN label per locale', () => {
expect(formatDocumentDate(null, 'UNKNOWN', null, '?', 'en')).toBe(
m.date_precision_unknown(undefined, { locale: 'en' })
);
});
it('localizes the APPROX prefix per locale', () => {
expect(formatDocumentDate('1920-01-01', 'APPROX', null, null, 'en')).toBe(
`${m.date_precision_approx_prefix(undefined, { locale: 'en' })} 1920`
);
});
it('localizes the SEASON word per locale when raw is absent', () => {
expect(formatDocumentDate('1916-07-01', 'SEASON', null, null, 'en')).toBe(
`${m.date_season_summer(undefined, { locale: 'en' })} 1916`
);
});
it('localizes the SEASON word even when the raw cell is verbatim German (Decision 4)', () => {
expect(formatDocumentDate('1916-06-01', 'SEASON', null, 'Sommer 1916', 'en')).toBe(
`${m.date_season_summer(undefined, { locale: 'en' })} 1916`
);
});
});
// ─── Security: untrusted raw must never influence the structured label ───────
describe('formatDocumentDate security', () => {
it('ignores a malicious raw value for the structured label (raw is rendered separately, escaped)', () => {
const label = formatDocumentDate(null, 'UNKNOWN', null, '<img src=x onerror=alert(1)>');
expect(label).toBe('Datum unbekannt');
expect(label).not.toContain('<img');
});
});
// ─── Defensive null handling ─────────────────────────────────────────────────
describe('formatDocumentDate defensive null handling', () => {
it('renders the unknown label when the anchor is null but precision is not UNKNOWN', () => {
expect(formatDocumentDate(null, 'DAY')).toBe('Datum unbekannt');
});
it('falls back to start-day only for a RANGE whose end is null', () => {
expect(formatDocumentDate('1917-01-10', 'RANGE', null)).toBe('ab 10. Jan. 1917');
});
});