Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039) | 1 | /** |
| 2 | * Time zones on a profile: the IANA names a person picks from in their | |
| 3 | * settings, and the local time the card over their name shows. Pure, so it | |
| 4 | * is tested on its own. | |
| 5 | */ | |
| 6 | ||
| 7 | /** Whether the runtime knows `zone` as a time zone. */ | |
| 8 | export function knownTimeZone(zone: string | null | undefined): zone is string { | |
| 9 | if (!zone) return false; | |
| 10 | try { | |
| 11 | new Intl.DateTimeFormat("en-US", { timeZone: zone }); | |
| 12 | return true; | |
| 13 | } catch { | |
| 14 | return false; | |
| 15 | } | |
| 16 | } | |
| 17 | ||
| 18 | /** | |
| 19 | * Every IANA zone the runtime knows, sorted, with `current` kept in the | |
| 20 | * list even when the runtime does not know it, so a saved choice is never | |
| 21 | * dropped. UTC is always there. | |
| 22 | */ | |
| 23 | export function timeZoneNames(current?: string | null): string[] { | |
| 24 | let names: string[] = []; | |
| 25 | try { | |
| 26 | names = Intl.supportedValuesOf("timeZone"); | |
| 27 | } catch { | |
| 28 | names = []; | |
| 29 | } | |
| 30 | const all = new Set(names); | |
| 31 | all.add("UTC"); | |
| 32 | if (current) all.add(current); | |
| 33 | return [...all].sort((a, b) => a.localeCompare(b)); | |
| 34 | } | |
| 35 | ||
| 36 | /** A zone's name as a person reads it: `America/Port_of_Spain` as `America/Port of Spain`. */ | |
| 37 | export function timeZoneLabel(zone: string): string { | |
| 38 | return zone.replaceAll("_", " "); | |
| 39 | } | |
| 40 | ||
| 41 | /** The zone's offset from UTC at `now`, such as `UTC−06:00`; null when unknown. */ | |
| 42 | export function utcOffset(zone: string, now: number): string | null { | |
| 43 | try { | |
| 44 | const part = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "longOffset" }) | |
| 45 | .formatToParts(now) | |
| 46 | .find((one) => one.type === "timeZoneName")?.value; | |
| 47 | if (!part) return null; | |
| 48 | // "GMT-06:00", or plain "GMT" at UTC itself. | |
| 49 | const offset = part.replace(/^GMT/, "") || "+00:00"; | |
| 50 | return `UTC${offset.replace("-", "−")}`; | |
| 51 | } catch { | |
| 52 | return null; | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * The time of day at `now` in `zone`, such as `3:42 PM`, in `locale` (the | |
| 58 | * viewer's own when left out); null when there is no zone or the runtime | |
| 59 | * does not know it. | |
| 60 | */ | |
| 61 | export function localTime(zone: string | null | undefined, now: number, locale?: string): string | null { | |
| 62 | if (!knownTimeZone(zone)) return null; | |
| 63 | return new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone: zone }).format(now); | |
| 64 | } | |
| 65 | ||
| 66 | /** The browser's own zone, or null where it cannot say (on the server, say). */ | |
| 67 | export function browserTimeZone(): string | null { | |
| 68 | try { | |
| 69 | const zone = Intl.DateTimeFormat().resolvedOptions().timeZone; | |
| 70 | return knownTimeZone(zone) ? zone : null; | |
| 71 | } catch { | |
| 72 | return null; | |
| 73 | } | |
| 74 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.