11/**
2- * The most-used zones, ranked by popularity. Doubles as the popularity prefix
3- * for {@link getTimezonesByPopularity} and as a curated fallback for runtimes
4- * without `Intl.supportedValuesOf` (e.g. Safari < 15.4), so the timezone picker
5- * is never an empty dead-end.
2+ * A curated fallback for runtimes without `Intl.supportedValuesOf` (e.g. Safari
3+ * < 15.4), so the timezone picker is never an empty dead-end.
64 */
75const COMMON_TIMEZONES = [
86 'UTC' ,
@@ -23,6 +21,73 @@ const COMMON_TIMEZONES = [
2321 'Australia/Sydney' ,
2422]
2523
24+ /**
25+ * Curated, human-friendly timezone labels in popularity order. Each label reads
26+ * as "{Region} Time - {City} ({ABBR})" so the picker shows recognizable names
27+ * instead of raw IANA paths. Values stay canonical IANA ids (what we persist);
28+ * every zone the runtime knows but isn't curated still appears below these with
29+ * an auto-generated "{City} (GMT±X)" label, so coverage is never lost.
30+ */
31+ const CURATED_TIMEZONES : ReadonlyArray < { value : string ; label : string } > = [
32+ { value : 'UTC' , label : 'Coordinated Universal Time (UTC)' } ,
33+ { value : 'America/New_York' , label : 'US Eastern Time - New York (ET)' } ,
34+ { value : 'America/Chicago' , label : 'US Central Time - Chicago (CT)' } ,
35+ { value : 'America/Denver' , label : 'US Mountain Time - Denver (MT)' } ,
36+ { value : 'America/Phoenix' , label : 'US Mountain Time - Phoenix (MST, no DST)' } ,
37+ { value : 'America/Los_Angeles' , label : 'US Pacific Time - Los Angeles (PT)' } ,
38+ { value : 'America/Anchorage' , label : 'US Alaska Time - Anchorage (AKT)' } ,
39+ { value : 'Pacific/Honolulu' , label : 'US Hawaii Time - Honolulu (HST)' } ,
40+ { value : 'America/Toronto' , label : 'Canada Eastern Time - Toronto (ET)' } ,
41+ { value : 'America/Winnipeg' , label : 'Canada Central Time - Winnipeg (CT)' } ,
42+ { value : 'America/Edmonton' , label : 'Canada Mountain Time - Edmonton (MT)' } ,
43+ { value : 'America/Vancouver' , label : 'Canada Pacific Time - Vancouver (PT)' } ,
44+ { value : 'America/Halifax' , label : 'Canada Atlantic Time - Halifax (AT)' } ,
45+ { value : 'America/St_Johns' , label : "Canada Newfoundland Time - St. John's (NT)" } ,
46+ { value : 'America/Mexico_City' , label : 'Mexico Central Time - Mexico City (CST)' } ,
47+ { value : 'America/Bogota' , label : 'Colombia Time - Bogotá (COT)' } ,
48+ { value : 'America/Lima' , label : 'Peru Time - Lima (PET)' } ,
49+ { value : 'America/Sao_Paulo' , label : 'Brazil Time - São Paulo (BRT)' } ,
50+ { value : 'America/Argentina/Buenos_Aires' , label : 'Argentina Time - Buenos Aires (ART)' } ,
51+ { value : 'America/Santiago' , label : 'Chile Time - Santiago (CLT)' } ,
52+ { value : 'Europe/London' , label : 'UK Time - London (GMT/BST)' } ,
53+ { value : 'Europe/Dublin' , label : 'Ireland Time - Dublin (GMT/IST)' } ,
54+ { value : 'Europe/Lisbon' , label : 'Portugal Time - Lisbon (WET)' } ,
55+ { value : 'Europe/Paris' , label : 'Central European Time - Paris (CET)' } ,
56+ { value : 'Europe/Berlin' , label : 'Central European Time - Berlin (CET)' } ,
57+ { value : 'Europe/Madrid' , label : 'Central European Time - Madrid (CET)' } ,
58+ { value : 'Europe/Rome' , label : 'Central European Time - Rome (CET)' } ,
59+ { value : 'Europe/Amsterdam' , label : 'Central European Time - Amsterdam (CET)' } ,
60+ { value : 'Europe/Zurich' , label : 'Central European Time - Zurich (CET)' } ,
61+ { value : 'Europe/Stockholm' , label : 'Central European Time - Stockholm (CET)' } ,
62+ { value : 'Europe/Athens' , label : 'Eastern European Time - Athens (EET)' } ,
63+ { value : 'Europe/Helsinki' , label : 'Eastern European Time - Helsinki (EET)' } ,
64+ { value : 'Europe/Istanbul' , label : 'Turkey Time - Istanbul (TRT)' } ,
65+ { value : 'Europe/Moscow' , label : 'Moscow Time - Moscow (MSK)' } ,
66+ { value : 'Africa/Lagos' , label : 'West Africa Time - Lagos (WAT)' } ,
67+ { value : 'Africa/Cairo' , label : 'Egypt Time - Cairo (EET)' } ,
68+ { value : 'Africa/Nairobi' , label : 'East Africa Time - Nairobi (EAT)' } ,
69+ { value : 'Africa/Johannesburg' , label : 'South Africa Time - Johannesburg (SAST)' } ,
70+ { value : 'Asia/Jerusalem' , label : 'Israel Time - Jerusalem (IST)' } ,
71+ { value : 'Asia/Riyadh' , label : 'Arabia Time - Riyadh (AST)' } ,
72+ { value : 'Asia/Dubai' , label : 'Gulf Time - Dubai (GST)' } ,
73+ { value : 'Asia/Karachi' , label : 'Pakistan Time - Karachi (PKT)' } ,
74+ { value : 'Asia/Kolkata' , label : 'India Time - Kolkata (IST)' } ,
75+ { value : 'Asia/Dhaka' , label : 'Bangladesh Time - Dhaka (BST)' } ,
76+ { value : 'Asia/Bangkok' , label : 'Indochina Time - Bangkok (ICT)' } ,
77+ { value : 'Asia/Jakarta' , label : 'Western Indonesia Time - Jakarta (WIB)' } ,
78+ { value : 'Asia/Singapore' , label : 'Singapore Time - Singapore (SGT)' } ,
79+ { value : 'Asia/Hong_Kong' , label : 'Hong Kong Time - Hong Kong (HKT)' } ,
80+ { value : 'Asia/Shanghai' , label : 'China Time - Shanghai (CST)' } ,
81+ { value : 'Asia/Taipei' , label : 'Taipei Time - Taipei (CST)' } ,
82+ { value : 'Asia/Seoul' , label : 'Korea Time - Seoul (KST)' } ,
83+ { value : 'Asia/Tokyo' , label : 'Japan Time - Tokyo (JST)' } ,
84+ { value : 'Australia/Perth' , label : 'Australia Western Time - Perth (AWST)' } ,
85+ { value : 'Australia/Adelaide' , label : 'Australia Central Time - Adelaide (ACT)' } ,
86+ { value : 'Australia/Brisbane' , label : 'Australia Eastern Time - Brisbane (AEST, no DST)' } ,
87+ { value : 'Australia/Sydney' , label : 'Australia Eastern Time - Sydney (AET)' } ,
88+ { value : 'Pacific/Auckland' , label : 'New Zealand Time - Auckland (NZT)' } ,
89+ ]
90+
2691/** The IANA timezone the current runtime resolves to (e.g. `America/New_York`). */
2792export function getBrowserTimezone ( ) : string {
2893 return Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone
@@ -41,18 +106,50 @@ export function getSupportedTimezones(): string[] {
41106}
42107
43108/**
44- * Supported timezones ordered by popularity: the most-used zones
45- * ({@link COMMON_TIMEZONES}) first, in ranked order, followed by every
46- * remaining zone alphabetically. For the picker, where the zones people
47- * actually pick should surface above the long alphabetical tail.
109+ * Legacy IANA aliases (from older ICU data) mapped to the canonical id used in
110+ * {@link CURATED_TIMEZONES}, so a runtime that reports the alias doesn't surface
111+ * it as a duplicate of an already-curated zone.
112+ */
113+ const TIMEZONE_ALIASES : Record < string , string > = {
114+ 'Asia/Calcutta' : 'Asia/Kolkata' ,
115+ 'America/Buenos_Aires' : 'America/Argentina/Buenos_Aires' ,
116+ }
117+
118+ /** A timezone choice for a picker: the canonical IANA value plus a display label. */
119+ export interface TimezoneOption {
120+ value : string
121+ label : string
122+ }
123+
124+ /** `GMT±H` / `GMT±H:MM` for `timeZone` at the current instant (e.g. `GMT-7`). */
125+ function formatGmtOffset ( timeZone : string ) : string {
126+ const offsetMinutes = Math . round ( timezoneOffsetMs ( new Date ( ) , timeZone ) / 60_000 )
127+ const sign = offsetMinutes >= 0 ? '+' : '-'
128+ const absMinutes = Math . abs ( offsetMinutes )
129+ const hours = Math . floor ( absMinutes / 60 )
130+ const minutes = absMinutes % 60
131+ return minutes === 0
132+ ? `GMT${ sign } ${ hours } `
133+ : `GMT${ sign } ${ hours } :${ String ( minutes ) . padStart ( 2 , '0' ) } `
134+ }
135+
136+ /**
137+ * Timezone options for the picker: the curated, human-friendly zones
138+ * ({@link CURATED_TIMEZONES}) first in popularity order, then every remaining
139+ * zone the runtime knows — alphabetically, with an auto-generated
140+ * "{City} (GMT±X)" label — so the common picks read naturally while full
141+ * coverage stays searchable.
48142 */
49- export function getTimezonesByPopularity ( ) : string [ ] {
50- const supported = getSupportedTimezones ( )
51- const supportedSet = new Set ( supported )
52- const popular = COMMON_TIMEZONES . filter ( ( tz ) => supportedSet . has ( tz ) )
53- const popularSet = new Set ( popular )
54- const rest = supported . filter ( ( tz ) => ! popularSet . has ( tz ) ) . sort ( ( a , b ) => a . localeCompare ( b ) )
55- return [ ...popular , ...rest ]
143+ export function getTimezoneOptions ( ) : TimezoneOption [ ] {
144+ const curatedValues = new Set ( CURATED_TIMEZONES . map ( ( option ) => option . value ) )
145+ const rest = getSupportedTimezones ( )
146+ . filter ( ( tz ) => {
147+ const canonical = TIMEZONE_ALIASES [ tz ] ?? tz
148+ return ! curatedValues . has ( canonical )
149+ } )
150+ . sort ( ( a , b ) => a . localeCompare ( b ) )
151+ . map ( ( tz ) => ( { value : tz , label : `${ tz . replace ( / _ / g, ' ' ) } (${ formatGmtOffset ( tz ) } )` } ) )
152+ return [ ...CURATED_TIMEZONES , ...rest ]
56153}
57154
58155/**
0 commit comments