diff --git a/gottext/CHANGELOG.md b/gottext/CHANGELOG.md index 66b7478..8f51e57 100644 --- a/gottext/CHANGELOG.md +++ b/gottext/CHANGELOG.md @@ -10,6 +10,7 @@ from CI. ### Fixed - `mismatched` reads the tags a message carries, so a translation that adds, drops, renames or repeats one is named. +- `formatTime` writes a bare wall clock time, such as `09:30`, as the time it names for every reader, instead of Invalid Date. ## [0.8.0] - 2026-11-02 diff --git a/gottext/src/format.ts b/gottext/src/format.ts index 7333bbf..c80fa9d 100644 --- a/gottext/src/format.ts +++ b/gottext/src/format.ts @@ -8,6 +8,9 @@ let named: string | undefined /** CALENDAR_DAY is the shape of a bare calendar day, such as 2026-09-01. */ const CALENDAR_DAY = /^\d{4}-\d{2}-\d{2}$/ +/** WALL_CLOCK is the shape of a bare wall clock time, such as 09:30. */ +const WALL_CLOCK = /^\d{2}:\d{2}$/ + /** DAY is how a date is written unless the caller asks otherwise. */ const DAY: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit', year: 'numeric' } @@ -68,11 +71,17 @@ export function formatDate(at: Date | string, options: Intl.DateTimeFormatOption /** * Returns a moment as a time of day in the format locale. - * @param at - The moment, or the text a server stored. + * @param at - The moment, the text a server stored, or a bare wall clock time. * @returns The time, such as 09:05, empty when there is no moment. */ export function formatTime(at: Date | string): string { - return at === '' ? '' : new Date(at).toLocaleTimeString(formatLocale(), CLOCK) + if (at === '') { + return '' + } + if (typeof at === 'string' && WALL_CLOCK.test(at)) { + return new Date(`1970-01-01T${at}Z`).toLocaleTimeString(formatLocale(), { ...CLOCK, timeZone: 'UTC' }) + } + return new Date(at).toLocaleTimeString(formatLocale(), CLOCK) } /** diff --git a/gottext/test/format.test.ts b/gottext/test/format.test.ts index e93b143..9299486 100644 --- a/gottext/test/format.test.ts +++ b/gottext/test/format.test.ts @@ -62,6 +62,33 @@ test('writes a time as two digit hours and minutes on the es-ES clock', () => { expect(formatTime('2026-09-30T09:05:00Z')).toBe('09:05') }) +test('writes a bare wall clock time as the time it names in every time zone', () => { + rememberFormatLocale('es-ES') + vi.stubEnv('TZ', 'America/Los_Angeles') + + expect(formatTime('09:30')).toBe('09:30') + expect(formatTime('21:05')).toBe('21:05') + expect(formatTime('00:00')).toBe('00:00') + expect(formatTime('23:59')).toBe('23:59') +}) + +test('writes a bare wall clock time on the en-US clock as the time it names', () => { + rememberFormatLocale('en-US') + vi.stubEnv('TZ', 'Asia/Tokyo') + + expect(formatTime('09:30')).toBe('09:30 AM') + expect(formatTime('21:05')).toBe('09:05 PM') + expect(formatTime('00:00')).toBe('12:00 AM') +}) + +test('writes a timestamp a server stored on the reader clock, with or without a zone', () => { + rememberFormatLocale('es-ES') + vi.stubEnv('TZ', 'America/Los_Angeles') + + expect(formatTime('2026-09-30T09:05:00Z')).toBe('02:05') + expect(formatTime('2026-09-30T09:05')).toBe('09:05') +}) + test('writes nothing for an absent moment', () => { rememberFormatLocale('es-ES')