import { __ } from '@wordpress/i18n'; let context = { locale: 'en-US', timezone: 'UTC', timeFormat: 'H:i' }; export function configureDateTime(config: { locale?: string; timezone?: string; timeFormat?: string; }) { context = { locale: (config.locale || document.documentElement.lang || 'en-US').replace('_', '-'), timezone: config.timezone || 'UTC', timeFormat: config.timeFormat || 'H:i' }; } export function formattingLocale(): string { return context.locale; } const formatters = new Map(); function formatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat { const key = JSON.stringify([locale, options]); let value = formatters.get(key); if (!value) { value = new Intl.DateTimeFormat(locale.replace('_', '-'), options); if (formatters.size >= 32) formatters.clear(); formatters.set(key, value); } return value; } function timeOptions(): Intl.DateTimeFormatOptions { const pattern = context.timeFormat.replace(/\\./g, ''); return { hour: /[hH]/.test(pattern) ? '2-digit' : 'numeric', minute: '2-digit', ...(pattern.includes('s') ? { second: '2-digit' } : {}), ...(/[aA]/.test(pattern) ? { hour12: true } : { hourCycle: 'h23' }) }; } function instant(value: string): Date { return new Date(/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/.test(value) ? value.replace(' ', 'T') + 'Z' : value); } export function formatDate(value: string | null | undefined, style: 'short' | 'long' | 'full' = 'short', locale = context.locale): string { if (!value) return '—'; const date = new Date(value.slice(0, 10) + 'T12:00:00Z'); return Number.isNaN(date.getTime()) ? '—' : formatter(locale, { ...(style === 'short' ? { year: 'numeric' as const, month: 'numeric' as const, day: 'numeric' as const } : { dateStyle: style }), timeZone: 'UTC' }).format(date); } export function formatTime(value: string, timezone = context.timezone): string { const date = instant(value); return Number.isNaN(date.getTime()) ? '—' : formatter(context.locale, { hour: '2-digit', minute: '2-digit', ...timeOptions(), timeZone: timezone }).format(date); } export function formatDateTime(value: string | null | undefined, timezone = context.timezone): string { if (!value) return '—'; const date = instant(value); return Number.isNaN(date.getTime()) ? '—' : formatter(context.locale, { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', ...timeOptions(), timeZone: timezone }).format(date); } export function formatRange(start: string, end: string, timezone = context.timezone, withDate = true): string { const startDate = toLocalInput(start, timezone).slice(0, 10), endDate = toLocalInput(end, timezone).slice(0, 10); return `${withDate ? formatDate(startDate) + ', ' : ''}${formatTime(start, timezone)}–${startDate !== endDate ? formatDate(endDate) + ', ' : ''}${formatTime(end, timezone)}`; } export function choiceTime(value: string, choices: string[], timezone = context.timezone): string { const label = formatTime(value, timezone), same = choices.filter(v => formatTime(v, timezone) === label); return same.length > 1 ? `${label} (${same.indexOf(value) === 0 ? __('First occurrence', 'abece-booking') : __('Second occurrence', 'abece-booking')})` : label; } export function toLocalInput(value: string | null | undefined, timezone = context.timezone): string { if (!value) return ''; const date = instant(value); if (Number.isNaN(date.getTime())) return ''; const parts = formatter('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' }).formatToParts(date); const part = (type: string) => parts.find(p => p.type === type)?.value; return `${part('year')}-${part('month')}-${part('day')}T${part('hour')}:${part('minute')}:${part('second')}`; } /** Local wall-time inputs become explicit instants; gaps and new ambiguous times are rejected. */ export function localInputToISO(value: string, original?: string | null, timezone = context.timezone): string { if (!value) return ''; const canonical = value.length === 16 ? value + ':00' : value; const wall = Date.parse(canonical + 'Z'); if (!Number.isFinite(wall)) throw new Error(__('Select a valid local date and time.', 'abece-booking')); if (original && toLocalInput(original, timezone) === canonical) return instant(original).toISOString().replace('.000Z', 'Z'); const matches: string[] = []; for (let minutes = -14 * 60; minutes <= 14 * 60; minutes += 15) { const candidate = new Date(wall + minutes * 60000).toISOString(); if (toLocalInput(candidate, timezone) === canonical) matches.push(candidate); } if (matches.length !== 1) throw new Error(__('This local time is missing or ambiguous because of a clock change. Select another time.', 'abece-booking')); return matches[0].replace('.000Z', 'Z'); }