// Time-zone option helpers for the Profile screen's `` can render it without losing the saved * zone. The original groups are not mutated. */ export function withPreservedValue( groups: readonly TimeZoneGroup[], value: string, ): readonly TimeZoneGroup[] { const trimmed = value.trim(); if (trimmed === "") return groups; for (const group of groups) { if (group.values.includes(trimmed)) return groups; } const extra: TimeZoneGroup = { label: OTHER_GROUP, values: [trimmed] }; // Merge with an existing "Other" group if one is already present, // otherwise append a fresh one. const next: TimeZoneGroup[] = []; let mergedIntoOther = false; for (const group of groups) { if (group.label === OTHER_GROUP) { mergedIntoOther = true; next.push({ label: OTHER_GROUP, values: [...group.values, trimmed].sort((a, b) => a.localeCompare(b)), }); } else { next.push(group); } } if (!mergedIntoOther) next.push(extra); return next; } /** * browserTimeZone returns the time zone the runtime believes the * user is in. An empty string is returned when `Intl.DateTimeFormat` * is missing or rejects the resolution. */ export function browserTimeZone(): string { try { return Intl.DateTimeFormat().resolvedOptions().timeZone ?? ""; } catch { return ""; } } interface IntlWithSupportedValues { supportedValuesOf?: (key: "timeZone") => string[]; } function listSupportedZones(): string[] { const intl = Intl as unknown as IntlWithSupportedValues; if (typeof intl.supportedValuesOf !== "function") return []; try { const zones = intl.supportedValuesOf("timeZone"); return Array.isArray(zones) ? zones.slice() : []; } catch { return []; } } function groupZones(zones: readonly string[]): readonly TimeZoneGroup[] { const buckets = new Map(); const others: string[] = []; for (const zone of zones) { const slash = zone.indexOf("/"); if (slash === -1) { others.push(zone); continue; } const prefix = zone.slice(0, slash); const bucket = buckets.get(prefix); if (bucket === undefined) { buckets.set(prefix, [zone]); } else { bucket.push(zone); } } const groups: TimeZoneGroup[] = []; const sortedPrefixes = Array.from(buckets.keys()).sort((a, b) => a.localeCompare(b), ); for (const prefix of sortedPrefixes) { const values = (buckets.get(prefix) ?? []).slice().sort((a, b) => a.localeCompare(b), ); groups.push({ label: prefix, values }); } if (others.length > 0) { groups.push({ label: OTHER_GROUP, values: others.slice().sort((a, b) => a.localeCompare(b)), }); } return groups; }