diff --git a/Web App/web/src/lib/format.js b/Web App/web/src/lib/format.js index 451b1f6..4fafc0b 100644 --- a/Web App/web/src/lib/format.js +++ b/Web App/web/src/lib/format.js @@ -87,32 +87,56 @@ export function formatDateTime(value) { // // hourCycle rather than hour12: with hour12:false the en-US formatter prints // midnight as 24:00. +// The setting decides *which* clock; this file decides how it is punctuated. +// +// That split is the whole of it. A region is worth asking whether a reader +// expects 13:45 or 01:45 pm — that is a real difference in how people tell the +// time. It is not worth asking whether the two numbers are joined by a colon or +// a dot: Danish writes 13.45, and one screen of DriverVault writing 13.45 while +// the next writes 13:45 is not local colour, it is an inconsistency. So every +// time this app prints comes out of the same two lines below, and the region is +// asked one question only, under "auto". +// +// The cost is that the am/pm marker reads in English everywhere. It is the same +// trade the setting itself makes: a 12-hour clock is not a convention most of +// these regions use, so choosing one — or living in a region that does — is +// choosing the clock that comes with it. export function formatTime(value) { if (!value) return "—"; const d = value instanceof Date ? value : new Date(value); if (isNaN(d)) return "—"; - const opts = { hour: "2-digit", minute: "2-digit" }; - const mode = prefs.timeFormat; - if (mode !== "24" && mode !== "12") { - // Auto is the region's, whole: 13.45 is how Danish writes a clock, and a - // setting that says "follow the region" has no business arguing with it. - return d.toLocaleTimeString(prefs.locale || undefined, opts); - } + const h = d.getHours(); + const pad = (n) => String(n).padStart(2, "0"); + const mm = pad(d.getMinutes()); - // Asking for a clock outright is asking for its separator too. Somebody who - // picked "24-hour" means 13:45, not the 13.45 their region would have written - // — the point of leaving auto was to stop the region deciding. Only the mark - // between the hour and the minute is pinned: the rest is still the locale's, - // including whether there is an am/pm marker, what it reads and where it sits. - opts.hourCycle = mode === "24" ? "h23" : "h12"; - const parts = new Intl.DateTimeFormat(prefs.locale || undefined, opts).formatToParts(d); - return parts - .map((p, i) => - p.type === "literal" && parts[i - 1]?.type === "hour" && parts[i + 1]?.type === "minute" - ? ":" - : p.value - ) - .join(""); + const mode = prefs.timeFormat; + const twelve = mode === "12" || (mode !== "24" && regionReadsTwelveHour()); + if (!twelve) return `${pad(h)}:${mm}`; + // 12 for both noon and midnight, and midnight is the am one. + return `${pad(h % 12 || 12)}:${mm} ${h < 12 ? "am" : "pm"}`; +} + +// Whether the chosen region tells the time on a 12-hour clock — the one question +// "auto" asks it. Cached because this is asked once per timestamp on a page that +// can hold a great many, and the answer only changes when the region does. +const twelveHourRegions = new Map(); + +function regionReadsTwelveHour() { + const locale = prefs.locale || ""; + if (!twelveHourRegions.has(locale)) { + let twelve = false; + try { + const cycle = new Intl.DateTimeFormat(locale || undefined, { hour: "numeric" }) + .resolvedOptions().hourCycle; + twelve = cycle === "h11" || cycle === "h12"; + } catch { + // An unusable locale is not a reason to print nothing; 24-hour is the + // safer default, being the one that cannot be read as the wrong half of + // the day. + } + twelveHourRegions.set(locale, twelve); + } + return twelveHourRegions.get(locale); } // Every number we render goes through here so the grouping separator follows