@@ -21,73 +21,6 @@ const COMMON_TIMEZONES = [
2121 'Australia/Sydney' ,
2222]
2323
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-
9124/** The IANA timezone the current runtime resolves to (e.g. `America/New_York`). */
9225export function getBrowserTimezone ( ) : string {
9326 return Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone
@@ -105,51 +38,45 @@ export function getSupportedTimezones(): string[] {
10538 return zones . includes ( 'UTC' ) ? zones : [ 'UTC' , ...zones ]
10639}
10740
108- /**
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-
11841/** A timezone choice for a picker: the canonical IANA value plus a display label. */
11942export interface TimezoneOption {
12043 value : string
12144 label : string
12245}
12346
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 )
47+ /** The city/locale portion of an IANA id, formatted for display (e.g. `Los Angeles`). */
48+ function timezoneCity ( timeZone : string ) : string {
49+ return ( timeZone . split ( '/' ) . pop ( ) ?? timeZone ) . replace ( / _ / g, ' ' )
50+ }
51+
52+ /** `GMT±HH:MM` for an offset expressed in minutes east of UTC (e.g. `GMT-08:00`). */
53+ function formatGmtOffset ( offsetMinutes : number ) : string {
12754 const sign = offsetMinutes >= 0 ? '+' : '-'
12855 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' ) } `
56+ const hours = String ( Math . floor ( absMinutes / 60 ) ) . padStart ( 2 , '0' )
57+ const minutes = String ( absMinutes % 60 ) . padStart ( 2 , '0' )
58+ return `GMT${ sign } ${ hours } :${ minutes } `
13459}
13560
13661/**
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.
62+ * Timezone options for a picker, following the calendar-app convention: every
63+ * zone rendered as `(GMT±HH:MM) City`, ordered west-to-east by current UTC
64+ * offset (ties alphabetical by city). The offset is computed live, so it tracks
65+ * DST automatically. Values stay canonical IANA ids — what we persist.
14266 */
14367export 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 ]
68+ const now = new Date ( )
69+ return getSupportedTimezones ( )
70+ . map ( ( value ) => ( {
71+ value,
72+ city : timezoneCity ( value ) ,
73+ offsetMinutes : Math . round ( timezoneOffsetMs ( now , value ) / 60_000 ) ,
74+ } ) )
75+ . sort ( ( a , b ) => a . offsetMinutes - b . offsetMinutes || a . city . localeCompare ( b . city ) )
76+ . map ( ( { value, city, offsetMinutes } ) => ( {
77+ value,
78+ label : `(${ formatGmtOffset ( offsetMinutes ) } ) ${ city } ` ,
79+ } ) )
15380}
15481
15582/**
0 commit comments