The labels this application writes itself already read "6:30 pm", while a translated notice kept whatever the French said, so the screen could show "Closes at 6:30 pm" above "Delayed opening at 14:00". Two conventions side by side on one panel. The conversion is done in code, not by the prompt. The model is still told to leave times alone — reading a clock and rewriting it is deterministic work, and asking a language model to do it introduces a failure mode for no benefit. Only HH:mm is touched, never a time that already carries am or pm, so the pass is idempotent and years, dates written 20/07 and percentages are left alone. Hand-written translations are not rewritten: that wording was somebody's explicit choice. Verified against the live service: "Ouvert 10:00 – 13:00 puis 14:00 – 18:30" now comes back as "Open 10:00 am – 1:00 pm then 2:00 pm – 6:30 pm". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
176 lines
5.4 KiB
TypeScript
176 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
formatDateEn,
|
||
formatDateFr,
|
||
formatHoursText,
|
||
formatNextChangeEn,
|
||
formatNextChangeFr,
|
||
formatTimeEn,
|
||
shortDayLabelEn,
|
||
shortDayLabelFr,
|
||
toEnglishClock,
|
||
} from './format';
|
||
|
||
describe('formatDateFr / formatDateEn', () => {
|
||
it('writes the day and month in full', () => {
|
||
expect(formatDateFr('2026-09-20')).toBe('Dimanche 20 septembre');
|
||
expect(formatDateEn('2026-09-20')).toBe('Sunday 20 September');
|
||
});
|
||
|
||
it('does not pad the day number', () => {
|
||
expect(formatDateFr('2026-09-01')).toBe('Mardi 1 septembre');
|
||
expect(formatDateEn('2026-09-01')).toBe('Tuesday 1 September');
|
||
});
|
||
|
||
it('handles every month', () => {
|
||
expect(formatDateFr('2026-01-15')).toBe('Jeudi 15 janvier');
|
||
expect(formatDateFr('2026-08-15')).toBe('Samedi 15 août');
|
||
expect(formatDateFr('2026-12-25')).toBe('Vendredi 25 décembre');
|
||
expect(formatDateEn('2026-12-25')).toBe('Friday 25 December');
|
||
});
|
||
});
|
||
|
||
describe('short day labels', () => {
|
||
it('uses three letters, Monday first on the screen', () => {
|
||
expect([1, 2, 3, 4, 5, 6, 0].map(shortDayLabelFr)).toEqual([
|
||
'Lun',
|
||
'Mar',
|
||
'Mer',
|
||
'Jeu',
|
||
'Ven',
|
||
'Sam',
|
||
'Dim',
|
||
]);
|
||
expect([1, 2, 3, 4, 5, 6, 0].map(shortDayLabelEn)).toEqual([
|
||
'Mon',
|
||
'Tue',
|
||
'Wed',
|
||
'Thu',
|
||
'Fri',
|
||
'Sat',
|
||
'Sun',
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe('formatHoursText', () => {
|
||
it('renders a single slot', () => {
|
||
expect(formatHoursText([{ open: '10:00', close: '18:30' }])).toBe('10:00 – 18:30');
|
||
});
|
||
|
||
it('joins several slots with a middle dot', () => {
|
||
expect(
|
||
formatHoursText([
|
||
{ open: '10:00', close: '13:00' },
|
||
{ open: '14:00', close: '18:30' },
|
||
]),
|
||
).toBe('10:00 – 13:00 · 14:00 – 18:30');
|
||
});
|
||
|
||
it('returns an empty string when the shop is closed', () => {
|
||
expect(formatHoursText([])).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('formatTimeEn', () => {
|
||
it('uses the twelve-hour clock', () => {
|
||
expect(formatTimeEn('18:30')).toBe('6:30 pm');
|
||
expect(formatTimeEn('10:00')).toBe('10:00 am');
|
||
});
|
||
|
||
it('names noon and midnight correctly', () => {
|
||
expect(formatTimeEn('12:00')).toBe('12:00 pm');
|
||
expect(formatTimeEn('00:30')).toBe('12:30 am');
|
||
});
|
||
});
|
||
|
||
describe('formatNextChange', () => {
|
||
it('announces today’s closing time', () => {
|
||
expect(formatNextChangeFr({ kind: 'CLOSES', date: '2026-09-20', time: '18:30' }, '2026-09-20')).toBe(
|
||
'Ferme à 18:30',
|
||
);
|
||
expect(formatNextChangeEn({ kind: 'CLOSES', date: '2026-09-20', time: '18:30' }, '2026-09-20')).toBe(
|
||
'Closes at 6:30 pm',
|
||
);
|
||
});
|
||
|
||
it('announces a reopening later today', () => {
|
||
expect(formatNextChangeFr({ kind: 'OPENS', date: '2026-09-20', time: '14:00' }, '2026-09-20')).toBe(
|
||
'Ouvre à 14:00',
|
||
);
|
||
expect(formatNextChangeEn({ kind: 'OPENS', date: '2026-09-20', time: '14:00' }, '2026-09-20')).toBe(
|
||
'Opens at 2:00 pm',
|
||
);
|
||
});
|
||
|
||
it('announces tomorrow by name', () => {
|
||
expect(formatNextChangeFr({ kind: 'OPENS', date: '2026-09-21', time: '10:00' }, '2026-09-20')).toBe(
|
||
'Ouvre demain à 10:00',
|
||
);
|
||
expect(formatNextChangeEn({ kind: 'OPENS', date: '2026-09-21', time: '10:00' }, '2026-09-20')).toBe(
|
||
'Opens tomorrow at 10:00 am',
|
||
);
|
||
});
|
||
|
||
it('names the weekday when the reopening is further out', () => {
|
||
expect(formatNextChangeFr({ kind: 'OPENS', date: '2026-09-22', time: '10:00' }, '2026-09-20')).toBe(
|
||
'Ouvre mardi à 10:00',
|
||
);
|
||
expect(formatNextChangeEn({ kind: 'OPENS', date: '2026-09-22', time: '10:00' }, '2026-09-20')).toBe(
|
||
'Opens Tuesday at 10:00 am',
|
||
);
|
||
});
|
||
|
||
it('says nothing when there is nothing to announce', () => {
|
||
expect(formatNextChangeFr(null, '2026-09-20')).toBe('');
|
||
expect(formatNextChangeEn(null, '2026-09-20')).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('toEnglishClock', () => {
|
||
it('converts a single time', () => {
|
||
expect(toEnglishClock('Delayed opening at 14:00 today')).toBe(
|
||
'Delayed opening at 2:00 pm today',
|
||
);
|
||
});
|
||
|
||
it('converts every time in a range', () => {
|
||
expect(toEnglishClock('Open 10:00 – 13:00 · 14:00 – 18:30')).toBe(
|
||
'Open 10:00 am – 1:00 pm · 2:00 pm – 6:30 pm',
|
||
);
|
||
});
|
||
|
||
it('handles times written without a space around the dash', () => {
|
||
expect(toEnglishClock('10:00-18:30')).toBe('10:00 am-6:30 pm');
|
||
});
|
||
|
||
it('names noon and midnight', () => {
|
||
expect(toEnglishClock('Closing at 12:00')).toBe('Closing at 12:00 pm');
|
||
expect(toEnglishClock('Until 00:30')).toBe('Until 12:30 am');
|
||
});
|
||
|
||
it('leaves a time that already has am or pm alone', () => {
|
||
// Otherwise a second pass would mangle what the first one produced.
|
||
expect(toEnglishClock('Closes at 6:30 pm')).toBe('Closes at 6:30 pm');
|
||
expect(toEnglishClock('Closes at 6:30 PM')).toBe('Closes at 6:30 PM');
|
||
});
|
||
|
||
it('is stable when applied twice', () => {
|
||
const once = toEnglishClock('Open 10:00 – 18:30');
|
||
expect(toEnglishClock(once)).toBe(once);
|
||
});
|
||
|
||
it('leaves years, dates and prices alone', () => {
|
||
expect(toEnglishClock('Annual leave from July 20 to August 10')).toBe(
|
||
'Annual leave from July 20 to August 10',
|
||
);
|
||
expect(toEnglishClock('Collection 2026')).toBe('Collection 2026');
|
||
expect(toEnglishClock('Sale: 30% off')).toBe('Sale: 30% off');
|
||
});
|
||
|
||
it('leaves text with no time untouched', () => {
|
||
expect(toEnglishClock('New collection in store')).toBe('New collection in store');
|
||
});
|
||
});
|