5 | Amendments to the ECMAScript® 2024 Internationalization API Specification
6 |
7 |
8 |
9 | This section lists amendments which must be made to ECMA-402, the ECMAScript® 2024 Internationalization API Specification.
10 |
11 |
12 | This text is based on https://github.com/tc39/proposal-temporal/blob/main/spec/intl.html, which specifies the changes to ECMA-402 made by the Temporal proposal.
13 | Text to be added is marked like this, and text to be deleted is marked like this.
14 |
15 |
16 |
17 |
18 | Use of the IANA Time Zone Database
19 |
20 |
21 |
22 | This proposal adds a note to the Temporal proposal's Use of the IANA Time Zone Database section, which replaces the current Time Zone Names section in ECMA-402.
23 |
24 |
25 | One paragraph of existing text above and below the inserted note is included for context.
26 |
27 |
28 |
29 | [...]
30 |
31 |
32 | The IANA Time Zone Database is typically updated between five and ten times per year.
33 | These updates may add new Zone or Link names, may change Zones to Links, and may change the UTC offsets and transitions associated with any Zone.
34 | ECMAScript implementations are recommended to include updates to the IANA Time Zone Database as soon as possible.
35 | Such prompt action ensures that ECMAScript programs can accurately perform time-zone-sensitive calculations and can use newly-added available named time zone identifiers supplied by external input or the host environment.
36 |
37 |
38 |
39 |
40 |
41 | Although the IANA Time Zone Database maintainers strive for stability, in rare cases (averaging less than once per year) a Zone may be replaced by a new Zone.
42 | For example, in 2022 "*Europe/Kiev*" was deprecated to a Link resolving to a new "*Europe/Kyiv*" Zone.
43 |
44 |
45 | To reduce disruption from renaming changes, ECMAScript implementations are encouraged to initially add the new Zone as a non-primary time zone identifier that resolves to the current primary identifier.
46 | Then, after a waiting period, implementations are recommended to promote the new Zone to a primary time zone identifier while simultaneously demoting the deprecated name to non-primary.
47 | The recommended waiting period is two years after the IANA Time Zone Database release containing the changes.
48 | This delay allows other systems, that ECMAScript programs may interact with, to be updated to recognize the new Zone.
49 |
50 |
51 | A waiting period should only apply when a new Zone is added to replace an existing Zone.
52 | If an existing Zone and Link are swapped, then no waiting period is necessary.
53 |
54 |
55 |
56 |
57 |
58 | If implementations revise time zone information during the lifetime of an agent, then which identifiers are supported, the primary time zone identifier associated with any identifier, and the UTC offsets and transitions associated with any Zone, must be consistent with results previously observed by that agent.
59 | Due to the complexity of supporting this requirement, it is recommended that implementations maintain a fully consistent copy of the IANA Time Zone Database for the lifetime of each agent.
60 |
61 |
62 | [...]
63 |
64 |
65 |
66 | Temporal.ZonedDateTime.prototype.toLocaleString ( [ _locales_ [ , _options_ ] ] )
67 | This definition supersedes the definition provided in .
68 |
69 | This method performs the following steps when called:
70 |
71 |
72 | 1. Let _zonedDateTime_ be the *this* value.
73 | 1. Perform ? RequireInternalSlot(_zonedDateTime_, [[InitializedTemporalZonedDateTime]]).
74 | 1. Let _dateTimeFormat_ be ! OrdinaryCreateFromConstructor(%DateTimeFormat%, %DateTimeFormat.prototype%, « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[Weekday]], [[Era]], [[Year]], [[Month]], [[Day]], [[DayPeriod]], [[Hour]], [[Minute]], [[Second]], [[FractionalSecondDigits]], [[TimeZoneName]], [[HourCycle]], [[Pattern]], [[BoundFormat]] »).
75 | 1. Let _timeZone_ be ? ToTemporalTimeZoneIdentifier(_zonedDateTime_.[[TimeZone]]).
76 | 1. If IsOffsetTimeZoneIdentifier(_timeZone_) is *true*, throw a *RangeError* exception.
77 | 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_timeZone_).
78 | 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception.
79 | 1. Set _timeZone_ to _timeZoneIdentifierRecord_.[[PrimaryIdentifierIdentifier]].
80 | 1. Perform ? InitializeDateTimeFormat(_dateTimeFormat_, _locales_, _options_, _timeZone_).
81 | 1. Let _calendar_ be ? ToTemporalCalendarIdentifier(_zonedDateTime_.[[Calendar]]).
82 | 1. If _calendar_ is not *"iso8601"* and not equal to _dateTimeFormat_.[[Calendar]], then
83 | 1. Throw a *RangeError* exception.
84 | 1. Let _instant_ be ! CreateTemporalInstant(_zonedDateTime_.[[Nanoseconds]]).
85 | 1. Return ? FormatDateTime(_dateTimeFormat_, _instant_).
86 |
87 |
88 |
89 |
90 | InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ [ , _toLocaleStringTimeZone_ ] )
91 |
92 |
93 |
94 | This text is based on the revisions to InitializeDateTimeFormat made by the Temporal proposal, not the current text in ECMA-402.
95 |
96 |
97 | Only one line is changed and is marked like this.
98 | The rest of the text has been included for context, but it can be ignored.
99 |
100 |
101 |
102 |
103 | The abstract operation InitializeDateTimeFormat accepts the arguments _dateTimeFormat_ (which must be an object), _locales_, and _options_.
104 | It initializes _dateTimeFormat_ as a DateTimeFormat object.
105 | If an additional _toLocaleStringTimeZone_ argument is provided (which, if present, must be a canonical time zone name string), the time zone will be overridden and some adjustments will be made to the defaults in order to implement the behaviour of `Temporal.ZonedDateTime.prototype.toLocaleString`.
106 | This abstract operation functions as follows:
107 |
108 |
109 |
110 | The following algorithm refers to the `type` nonterminal from UTS 35's Unicode Locale Identifier grammar.
111 |
112 |
113 |
114 | 1. Let _requestedLocales_ be ? CanonicalizeLocaleList(_locales_).
115 | 1. Let _opt_ be a new Record.
116 | 1. Let _matcher_ be ? GetOption(_options_, *"localeMatcher"*, *"string"*, « *"lookup"*, *"best fit"* », *"best fit"*).
117 | 1. Set _opt_.[[localeMatcher]] to _matcher_.
118 | 1. Let _calendar_ be ? GetOption(_options_, *"calendar"*, *"string"*, *undefined*, *undefined*).
119 | 1. If _calendar_ is not *undefined*, then
120 | 1. If _calendar_ does not match the Unicode Locale Identifier `type` nonterminal, throw a *RangeError* exception.
121 | 1. Set _opt_.[[ca]] to _calendar_.
122 | 1. Let _numberingSystem_ be ? GetOption(_options_, *"numberingSystem"*, *"string"*, *undefined*, *undefined*).
123 | 1. If _numberingSystem_ is not *undefined*, then
124 | 1. If _numberingSystem_ does not match the Unicode Locale Identifier `type` nonterminal, throw a *RangeError* exception.
125 | 1. Set _opt_.[[nu]] to _numberingSystem_.
126 | 1. Let _hour12_ be ? GetOption(_options_, *"hour12"*, *"boolean"*, *undefined*, *undefined*).
127 | 1. Let _hourCycle_ be ? GetOption(_options_, *"hourCycle"*, *"string"*, « *"h11"*, *"h12"*, *"h23"*, *"h24"* », *undefined*).
128 | 1. If _hour12_ is not *undefined*, then
129 | 1. Set _hourCycle_ to *null*.
130 | 1. Set _opt_.[[hc]] to _hourCycle_.
131 | 1. Let _localeData_ be %DateTimeFormat%.[[LocaleData]].
132 | 1. Let _r_ be ResolveLocale(%DateTimeFormat%.[[AvailableLocales]], _requestedLocales_, _opt_, %DateTimeFormat%.[[RelevantExtensionKeys]], _localeData_).
133 | 1. Set _dateTimeFormat_.[[Locale]] to _r_.[[locale]].
134 | 1. Let _resolvedCalendar_ be _r_.[[ca]].
135 | 1. Set _dateTimeFormat_.[[Calendar]] to _resolvedCalendar_.
136 | 1. Set _dateTimeFormat_.[[NumberingSystem]] to _r_.[[nu]].
137 | 1. Let _dataLocale_ be _r_.[[dataLocale]].
138 | 1. Let _dataLocaleData_ be _localeData_.[[<_dataLocale_>]].
139 | 1. Let _hcDefault_ be _dataLocaleData_.[[hourCycle]].
140 | 1. If _hour12_ is *true*, then
141 | 1. If _hcDefault_ is *"h11"* or *"h23"*, let _hc_ be *"h11"*. Otherwise, let _hc_ be *"h12"*.
142 | 1. Else if _hour12_ is *false*, then
143 | 1. If _hcDefault_ is *"h11"* or *"h23"*, let _hc_ be *"h23"*. Otherwise, let _hc_ be *"h24"*.
144 | 1. Else,
145 | 1. Assert: _hour12_ is *undefined*.
146 | 1. Let _hc_ be _r_.[[hc]].
147 | 1. If _hc_ is *null*, set _hc_ to _hcDefault_.
148 | 1. Let _timeZone_ be ? Get(_options_, *"timeZone"*).
149 | 1. If _timeZone_ is *undefined*, then
150 | 1. If _toLocaleStringTimeZone_ is present, then
151 | 1. Set _timeZone_ to _toLocaleStringTimeZone_.
152 | 1. Else,
153 | 1. Set _timeZone_ to SystemTimeZoneIdentifier().
154 | 1. Else,
155 | 1. If _toLocaleStringTimeZone_ is present, throw a *TypeError* exception.
156 | 1. Set _timeZone_ to ? ToString(_timeZone_).
157 | 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_timeZone_).
158 | 1. If _timeZoneIdentifierRecord_ is ~empty~, then
159 | 1. Throw a *RangeError* exception.
160 | 1. Set _timeZone_ to _timeZoneIdentifierRecord_.[[PrimaryIdentifierIdentifier]].
161 | 1. Set _dateTimeFormat_.[[TimeZone]] to _timeZone_.
162 | 1. Let _formatOptions_ be a new Record.
163 | 1. Set _formatOptions_.[[hourCycle]] to _hc_.
164 | 1. Let _hasExplicitFormatComponents_ be *false*.
165 | 1. For each row of , except the header row, in table order, do
166 | 1. Let _prop_ be the name given in the Property column of the row.
167 | 1. If _prop_ is *"fractionalSecondDigits"*, then
168 | 1. Let _value_ be ? GetNumberOption(_options_, *"fractionalSecondDigits"*, 1, 3, *undefined*).
169 | 1. Else,
170 | 1. Let _values_ be a List whose elements are the strings given in the Values column of the row.
171 | 1. Let _value_ be ? GetOption(_options_, _prop_, *"string"*, _values_, *undefined*).
172 | 1. Set _formatOptions_.[[<_prop_>]] to _value_.
173 | 1. If _value_ is not *undefined*, then
174 | 1. Set _hasExplicitFormatComponents_ to *true*.
175 | 1. Let _matcher_ be ? GetOption(_options_, *"formatMatcher"*, *"string"*, « *"basic"*, *"best fit"* », *"best fit"*).
176 | 1. Let _dateStyle_ be ? GetOption(_options_, *"dateStyle"*, *"string"*, « *"full"*, *"long"*, *"medium"*, *"short"* », *undefined*).
177 | 1. Set _dateTimeFormat_.[[DateStyle]] to _dateStyle_.
178 | 1. Let _timeStyle_ be ? GetOption(_options_, *"timeStyle"*, *"string"*, « *"full"*, *"long"*, *"medium"*, *"short"* », *undefined*).
179 | 1. Set _dateTimeFormat_.[[TimeStyle]] to _timeStyle_.
180 | 1. Let _expandedOptions_ be a copy of _formatOptions_.
181 | 1. Let _needDefaults_ be *true*.
182 | 1. For each element _field_ of « *"weekday"*, *"year"*, *"month"*, *"day"*, *"hour"*, *"minute"*, *"second"* » in List order, do
183 | 1. If _expandedOptions_.[[<_field_>]] is not *undefined*, then
184 | 1. Set _needDefaults_ to *false*.
185 | 1. If _needDefaults_ is *true*, then
186 | 1. For each element _field_ of « *"year"*, *"month"*, *"day"*, *"hour"*, *"minute"*, *"second"* » in List order, do
187 | 1. Set _expandedOptions_.[[<_field_>]] to *"numeric"*.
188 | 1. Let _bestFormat_ be GetDateTimeFormatPattern(_dateStyle_, _timeStyle_, _matcher_, _expandedOptions_, _dataLocaleData_, _hc_, _resolvedCalendar_, _hasExplicitFormatComponents_).
189 | 1. Set _dateTimeFormat_.[[Pattern]] to _bestFormat_.[[pattern]].
190 | 1. Set _dateTimeFormat_.[[RangePatterns]] to _bestFormat_.[[rangePatterns]].
191 | 1. For each row in , except the header row, in table order, do
192 | 1. Let _limitedOptions_ be a new Record.
193 | 1. Let _needDefaults_ be *true*.
194 | 1. Let _fields_ be the list of fields in the Supported fields column of the row.
195 | 1. For each field _field_ of _formatOptions_, do
196 | 1. If _field_ is in _fields_, then
197 | 1. Set _needDefaults_ to *false*.
198 | 1. Set _limitedOptions_.[[<_field_>]] to _formatOptions_.[[<_field_>]].
199 | 1. If _needDefaults_ is *true*, then
200 | 1. Let _defaultFields_ be the list of fields in the Default fields column of the row.
201 | 1. If the Pattern column of the row is [[TemporalInstantPattern]], and _toLocaleStringTimeZone_ is present, append [[timeZoneName]] to _defaultFields_.
202 | 1. For each element _field_ of _defaultFields_, do
203 | 1. If _field_ is [[timeZoneName]], then
204 | 1. Let _defaultValue_ be *"short"*.
205 | 1. Else,
206 | 1. Let _defaultValue_ be *"numeric"*.
207 | 1. Set _limitedOptions_.[[<_field_>]] to _defaultValue_.
208 | 1. Let _bestFormat_ be GetDateTimeFormatPattern(_dateStyle_, _timeStyle_, _matcher_, _limitedOptions_, _dataLocaleData_, _hc_, _resolvedCalendar_, _hasExplicitFormatComponents_).
209 | 1. If _bestFormat_ does not have any fields that are in _fields_, then
210 | 1. Set _bestFormat_ to *null*.
211 | 1. Set _dateTimeFormat_'s internal slot whose name is the Pattern column of the row to _bestFormat_.
212 | 1. Return _dateTimeFormat_.
213 |
214 |
215 |
216 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Handling Time Zone Canonicalization Changes
2 |
3 | Time zones in ECMAScript rely on IANA Time Zone Database ([TZDB](https://www.iana.org/time-zones)) identifiers like `America/Los_Angeles` or `Asia/Tokyo`.
4 | This proposal improves developer experience when the canonical identifier of a time zone is changed in TZDB, for example from `Europe/Kiev` to `Europe/Kyiv`.
5 |
6 | ## Status
7 |
8 | This proposal is currently [Stage 3](https://github.com/tc39/proposals/tree/main#stage-3) following the [TC39 process](https://tc39.es/process-document/).
9 |
10 | This proposal reached Stage 3 at the July 2023 TC39 meeting.
11 | Stage 3 reviewers were: Philip Chimento ([@ptomato](https://github.com/ptomato)), Daniel Minor ([@dminor](https://github.com/dminor)), and Jordan Harband ([@ljharb](https://github.com/ljharb)).
12 |
13 | It was agreed at that meeting that this proposal should be merged into the [Temporal proposal](https://github.com/tc39/proposal-temporal), which is also at Stage 3.
14 | This merger will happen via a normative PR into the Temporal repo, which this proposal's champions will file soon.
15 | After that PR lands, unresolved issues in this repo will be migrated to the [ECMA-402 repo](https://github.com/tc39/ecma402) and may be addressed via later normative PRs to ECMA-402.
16 |
17 | Here are [Slides](https://docs.google.com/presentation/d/1MVBKAB8U16ynSHmO6Mkt26hT5U-28OjyG9-L-GFdikE) for the July 2023 TC39 plenary meeting where this proposal advanced to Stage 3.
18 |
19 | Here are [Slides](https://docs.google.com/presentation/d/13vW8JxkbzyzGubT5ZkqUIxtpOQGNSUlguVwgcrbitog/) for the May 2023 TC39 plenary meeting where this proposal advanced to Stage 2.
20 |
21 | Here are [Slides](https://docs.google.com/presentation/d/13vW8JxkbzyzGubT5ZkqUIxtpOQGNSUlguVwgcrbitog/) from the March 2023 TC39 plenary meeting where this proposal advanced to Stage 1.
22 |
23 | This proposal's specification is "stacked" on top of the [Temporal proposal](https://github.com/tc39/proposal-temporal), because this proposal builds on Temporal features, especially the [`Temporal.TimeZone`](https://tc39.es/proposal-temporal/docs/timezone.html) built-in object.
24 |
25 | ## Specification
26 |
27 | The specification text for this proposal can be found at https://tc39.es/proposal-canonical-tz.
28 | This proposal's spec changes are narrow: about 15 lines of spec text changes (mostly one-line changes in existing abstract operations), plus one prose paragraph.
29 |
30 | ## Champions
31 |
32 | - Justin Grant ([@justingrant](https://github.com/justingrant))
33 | - Richard Gibson ([@gibson042](https://github.com/gibson042/))
34 |
35 | ## Tests
36 |
37 | Test262 tests covering this proposal's entire surface area are available at https://github.com/tc39/test262/pull/3837.
38 |
39 | ## Polyfill
40 |
41 | A non-production, for-testing-only polyfill is available.
42 | This proposal's polyfill is stacked on the Temporal proposal's non-production, test-only polyfill. See this proposal's [`polyfill README`](./polyfill/README.md) for more information.
43 |
44 | ## Contents
45 |
46 | - [Motivation](#Motivatione)
47 | - [Definitions](#Definitions)
48 | - [Proposed Solution](#Proposed-Solution---Summary)
49 | - [References](#References)
50 |
51 | ## Motivation
52 |
53 | ### Variation between implementations + spec doesn't match web reality
54 |
55 | ```javascript
56 | Temporal.TimeZone.from('Asia/Kolkata');
57 | // => Asia/Kolkata (Firefox)
58 | // => Asia/Calcutta (Chrome, Safari, Node -- does not conform to ECMA-402)
59 | ```
60 |
61 | ### Vague spec text: _"in the IANA Time Zone Database"_
62 |
63 | ```javascript
64 | // TZDB has build options and the spec is silent on which to pick.
65 | // Default build options, while conforming, are bad for users.
66 | Temporal.TimeZone.from('Atlantic/Reykjavik');
67 | // => Africa/Abidjan
68 | Temporal.TimeZone.from('Europe/Stockholm');
69 | // => Europe/Berlin
70 | Temporal.TimeZone.from('Europe/Zagreb');
71 | // => Europe/Belgrade
72 | ```
73 |
74 | ### User Complaints
75 |
76 | - [CLDR-9892: 'Asia/Calcutta', 'Asia/Saigon' and 'Asia/Katmandu' are canonical even though they became obsolete in 1993](https://unicode-org.atlassian.net/browse/CLDR-9892)
77 | - [Chromium 580195: Asia/Calcutta Timezone Identifier should be replaced by Asia/Kolkata](https://bugs.chromium.org/p/chromium/issues/detail?id=580195)
78 | - [moment.tz.guess() in chrome is different from moment.tz.guess() in IE · Issue #453 · moment/moment-timezone ](https://github.com/moment/moment-timezone/issues/453)
79 | - [CLDR-5612: Timezones very outdated](https://unicode-org.atlassian.net/browse/CLDR-5612)
80 | - [CLDR-9718: Rename from Asia/Calcutta to Asia/Kolkata in Zone - Tzid mapping and windows mapping](https://unicode-org.atlassian.net/browse/CLDR-9718)
81 | - [Incorrect canonical time zone name for Asia/Kolkata · Issue #1076 · tc39/proposal-temporal](https://github.com/tc39/proposal-temporal/issues/1076)
82 | - [[tz] Kyiv not Kiev](https://mm.icann.org/pipermail/tz/2021-January/029695.html) (from [IANA TZDB mailing list](https://mm.icann.org/pipermail/tz/)).
83 | - [WebKit 218542: Incorrect timezone returned for Buenos Aires](https://bugs.webkit.org/show_bug.cgi?id=218542)
84 | - [Firefox 1796393: Javascript returns problematic timezone, breaking sites](https://bugzilla.mozilla.org/show_bug.cgi?id=1796393)
85 | - [Firefox 1825512: Europe/Kyiv is not a valid IANA timezone identifier](https://bugzilla.mozilla.org/show_bug.cgi?id=1825512)
86 | - It's easy to find dozens more.
87 |
88 | ### Can't depend on static data behaving the same over time
89 |
90 | ```shell
91 | > npm test
92 | # result = someFunctionToTest('Asia/Calcutta');
93 | # assertEqual(result.timeZone.id, 'Asia/Calcutta');
94 | ✅
95 | > brew upgrade node
96 | > npm test
97 | ❌
98 | Expected: 'Asia/Calcutta'
99 | Actual: 'Asia/Kolkata'
100 | ```
101 |
102 | ### Comparing persisted identifiers with `===` is unreliable
103 |
104 | ```javascript
105 | userProfile.timeZoneId = Temporal.Now.timeZoneId();
106 | userProfile.save();
107 | // 1 year later (after canonicalization changed)
108 | userProfile.load();
109 | if (userProfile.timeZoneId !== Temporal.Now.timeZoneId()) {
110 | alert('You moved!');
111 | }
112 | ```
113 |
114 | ### Temporal makes these problems more disruptive
115 |
116 | ```javascript
117 | // Today, canonicalization is invisible for most common use cases.
118 | // Intl.DateTimeFormat `format` localizes time zone names.
119 | // Only `resolvedOptions()` exposes the underlying IANA time zone.
120 | timestamp = '2023-03-10T12:00:00Z';
121 | timeZone = 'Asia/Kolkata';
122 | dtf = new Intl.DateTimeFormat('en', { timeZone, timeZoneName: 'long' });
123 | dtf.format(new Date(timestamp));
124 | // => '3/10/2023, India Standard Time'
125 | dtf.resolvedOptions().timeZone;
126 | // => 'Asia/Kolkata' (Firefox)
127 | // => 'Asia/Calcutta' (Chrome, Safari, Node)
128 |
129 | // In Temporal, canonical identifiers are *very* noticeable.
130 | zdt = Temporal.Instant.from(timestamp).toZonedDateTimeISO({ timeZone });
131 | zdt.timeZoneId;
132 | // => 'Asia/Kolkata' (Firefox)
133 | // => 'Asia/Calcutta' (Chrome, Safari, Node)
134 | zdt.getTimeZone().id;
135 | // => 'Asia/Kolkata' (Firefox)
136 | // => 'Asia/Calcutta' (Chrome, Safari, Node)
137 | zdt.getTimeZone().toString();
138 | // => 'Asia/Kolkata' (Firefox)
139 | // => 'Asia/Calcutta' (Chrome, Safari, Node)
140 | zdt.toString();
141 | // => '2023-03-10T17:30:00+05:30[Asia/Kolkata]' (Firefox)
142 | // => '2023-03-10T17:30:00+05:30[Asia/Calcutta]' (Chrome, Safari, Node)
143 | ```
144 |
145 | ## Definitions
146 |
147 | Additional details related to the terms below can be found at [Time Zone Identifiers](https://tc39.es/ecma262/#sec-time-zone-identifiers) in ECMA-262 and [Use of the IANA Time Zone Database](https://tc39.es/proposal-temporal/#sec-use-of-iana-time-zone-database) in the ECMA-402-amending part of the Temporal specification.
148 |
149 | - **Available Named Time Zone Identifier** - Time zone name in TZDB (and accepted in ECMAScript).
150 | Available named time zone identifiers are ASCII-only and are case-insensitive.
151 | There are currently 594 available named time zone identifiers (avg length 14.3 chars).
152 | Currently fewer than 5 identifiers are added per year.
153 | - **Primary Time Zone Identifier** - The preferred identifier for an available named time zone.
154 | - **Non-primary Time Zone Identifier** - Other identifiers for an available named time zone.
155 | An available named time zone has one primary identifier and 0 or more non-primary ones.
156 | - **Zone** - TZDB term for a collection of rules named by an identifier (see [How to Read the tz Database](https://data.iana.org/time-zones/tz-how-to.html) and [Theory and pragmatics](https://data.iana.org/time-zones/theory.html)).
157 | There are currently 461 Zones (avg length 14.9 chars).
158 | Except rare exceptions like `Etc/GMT`, all Zones in TZDB are primary time zone identifiers in ECMAScript.
159 | - **Link** - TZDB term for an Identifier that lacks its own Zone record but instead targets another Identifier and uses its rules.
160 | When a Zone is renamed, a Link from the old name to the new one is added to [`backward`](https://github.com/eggert/tz/blob/main/backward).
161 | Renaming to update the spelling of a city happens every few years.
162 | There are currently 133 Links (avg length 12.2 chars).
163 | Links in TZDB are generally non-primary time zone identifiers in ECMAScript, but variation in TZDB build options means that some Links in some builds of TZDB are primary time zone identifiers in ECMAScript.
164 | - **Case-normalization** - Match user-provided IDs to case in IANA TZDB. Example: `america/los_angeles` ⇨ `America/Los_Angeles`
165 | - **Canonicalization** - Return the IANA Zone, resolving Links if needed. Example: Europe/Kiev ⇨ Europe/Kyiv
166 | - **CLDR Canonicalization** - Zone & Link [data](https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-bcp47/bcp47/timezone.json) used by V8 & WebKit (via ICU libraries)
167 | - **IANA Canonicalization** - Zone & Link [data](https://github.com/tc39/proposal-temporal/issues/2509#issuecomment-1461418026) used by Firefox (in custom TZDB build)
168 |
169 | ## Proposed Solution - Summary
170 |
171 | Steps below are designed to be "severable"; if blocked due to implementation complexity and/or lack of consensus, we can still move forward on the others.
172 |
173 | ### Reduce variation between implementations, and between implementations and spec
174 |
175 | 1. **[DONE - Simplify abstract operations](#1-done---editorial-cleanup-of-identifier-related-terms-and-abstract-operations)** dealing with time zone identifiers.
176 | 2. **[DONE - Clarify spec](#2-done---clarify-spec-to-prevent-more-divergence)** to prevent more divergence
177 | 3. **[Help V8 and WebKit update 13 out-of-date canonicalizations](#3-fix-out-of-date-canonicalizations-in-v8webkit)** like `Asia/Calcutta`, `Europe/Kiev`, and `Asia/Saigon` before wide Temporal adoption makes this painful. (no spec text required)
178 | 4. [**Prescriptive spec text to reduce divergence between implementations.**](#4-prescriptive-spec-text-to-reduce-divergence-between-implementations)
179 | This step requires finding common ground between implementers as well as TG2 (the ECMA-402 team) about how canonicalization should work.
180 |
181 | ### Reduce impact of canonicalization changes
182 |
183 | 5. [**Avoid observable following of Links.**](#5-defer-link-traversing-canonicalization)
184 | If canonicalization changes don't affect existing code, then it's much less likely for future canonicalization changes to break the Web.
185 | Because canonicalization is implementation-defined, this change may (or may not; needs research) be safe to ship after Temporal Stage 4, but best to not wait too long.
186 |
187 | ```javascript
188 | Temporal.TimeZone.from('Asia/Calcutta');
189 | // => Asia/Kolkata (current Temporal behavior on Firefox)
190 | // => Asia/Calcutta (proposed: don't follow Links when returning IDs to callers)
191 | ```
192 |
193 | 6. [**Add `Temporal.TimeZone.prototype.equals`.**](#6-add-temporaltimezoneprototypeequals)
194 | Because (5) would stop canonicalizing IDs upon `TimeZone` object creation, it'd be helpful to have an ergonomic way to know if two `TimeZone` objects represent the same Zone.
195 |
196 | ```javascript
197 | // More ergonomic canonical-equality testing
198 | Temporal.TimeZone.from('Asia/Calcutta').equals('Asia/Kolkata');
199 | // => true
200 | ```
201 |
202 | ## Proposed Solution - Details
203 |
204 | ### 1. DONE - Editorial cleanup of identifier-related terms and Abstract Operations
205 |
206 | _Status: Editorial PR merged as [tc39/ecma262#3035](https://github.com/tc39/ecma262/pull/3035)_
207 |
208 | A recent PR landed in ECMA-262 refactored the spec text for time zone identifiers.
209 | One change was a new [Time Zone Identifiers](https://tc39.es/ecma262/#sec-time-zone-identifiers) prose section that defines identifier-related terms and concepts.
210 | The other change was refactoring time-zone-identifier-related abstract operations, so that other time-zine-identifier-related functionality in ECMA-262, ECMA-402, and Temporal (including the normative changes in this proposal) can be built of these without resorting to new non-implementation-defined AOs:
211 |
212 | **`AvailableNamedTimeZoneIdentifiers()`** - Returns an implementation-defined List of Records, each composed of:
213 |
214 | - `[[Identifier]]`: identifier in the IANA TZDB
215 | - `[[PrimaryIdentifier]]`: identifier of transitive IANA Link target (following as many Links as necessary to reach a Zone), with the same special-casing of `"UTC"` as in ECMA-402's [CanonicalizeTimeZoneName](https://tc39.es/ecma402/#sec-canonicalizetimezonename).
216 |
217 | **`GetAvailableNamedTimeZoneIdentifier(_identifier_)`** - Filters the result of `AvailableNamedTimeZoneIdentifiers` to return the record where `[[Identifier]]` is an ASCII-case-insensitive match for `_identifier_`, or `~empty~` if there's no match.
218 | This AO:
219 |
220 | - Replaces `CanonicalizeTimeZoneName` in [Temporal](https://tc39.es/proposal-temporal/#sec-canonicalizetimezonename) and [ECMA-402](https://tc39.es/ecma402/#sec-canonicalizetimezonename) by using the `[[PrimaryIdentifier]]` of the result.
221 | - Replaces [`IsAvailableTimeZoneName`](https://tc39.es/proposal-temporal/#sec-isavailabletimezonename) in Temporal and [`IsValidTimeZoneName`](https://tc39.es/ecma402/#sec-isvalidtimezonename) in ECMA-402, by comparing the result to `~empty~`.
222 | - Fetches a case-normalized time zone identifier by using the `[[Identifier]]` of the result.
223 | This capability will be used as part of this proposal.
224 |
225 | The specification text of this proposal is stacked on top of these recently-merged editorial changes.
226 |
227 | ### 2. DONE - Clarify spec to prevent more divergence
228 |
229 | _Status: Editorial PR merged as [tc39/proposal-temporal#2573](https://github.com/tc39/proposal-temporal/pull/2573)._
230 |
231 | The ECMA-402 section of the Temporal spec now includes a new [Use of the Time Zone Database](https://tc39.es/proposal-temporal/#sec-use-of-iana-time-zone-database) section that recommends best practices for building and handling updates to the IANA Time Zone Database.
232 |
233 | These recommendations are broad enough to encompass existing implementations in Firefox and V8/WebKit but not any broader than that.
234 | For example, all ECMAScript implementations seem to avoid use of the the default TZDB build options that perform over-eager canonicalization like `Atlantic/Reykjavik=>Africa/Abidjan`.
235 | We will recommend that all implementations follow this best practice.
236 |
237 | The goal of these changes was to provide a web-reality baseline that further cross-implementation alignment can build on.
238 |
239 | ### 3. Fix out-of-date canonicalizations in V8/WebKit
240 |
241 | _Status: At a CLDR meeting on 2023-03-29, CLDR [agreed](https://unicode-org.atlassian.net/browse/CLDR-14453?focusedCommentId=169191) to develop a design proposal to provide IANA current canonicalization info._
242 | _Please follow https://unicode-org.atlassian.net/browse/CLDR-14453 for updates on progress._
243 |
244 | The list below shows 13 Links that have been superseded in IANA and Firefox, but still canonicalize to the "old" identifier in CLDR (and hence ICU and therefore V8 and WebKit).
245 | The data below comes from the 2022g version of TZDB, via a simple [CodeSandbox app](https://4rylir.csb.app/) that tests browsers' canonicalization behavior.
246 |
247 | There is some urgency to fix these outdated Links because as noted [above](#Temporal-makes-these-problems-more-disruptive), changing canonicalization after Temporal is widely adopted will cause more churn and customer complaints.
248 |
249 | To fix these outdated Links, we'd partner with representatives from V8 and WebKit (and maybe ICU and CLDR too) to see if there's a quick, low-cost way for V8/WebKit to update canonicalization of these 13 Zones.
250 |
251 | Note that renaming of TZDB identifiers is very infrequent.
252 | From a review of the TZDB [NEWS](https://data.iana.org/time-zones/tzdb/NEWS) file, there was only one rename per year from 2020-2022.
253 | Before that, the last rename was Rangoon => Yangon in 2016.
254 | Ideally ICU would provide a timely solution to these outdated identifiers.
255 | But in the meantime, if implementations had to hand-code overrides in a special-case list, then it would not need to be updated often.
256 |
257 | ```javascript
258 | // [0] => canonical in V8/WebKit (from CLDR)
259 | // [1] => canonical in Firefox (from IANA)
260 | // [2] => non-canonical Link (if present)
261 | const outofDateLinks = [
262 | ['Asia/Calcutta', 'Asia/Kolkata'],
263 | ['Europe/Kiev', 'Europe/Kyiv'],
264 | ['Asia/Saigon', 'Asia/Ho_Chi_Minh'],
265 | ['Asia/Rangoon', 'Asia/Yangon'],
266 | ['Asia/Ulaanbaatar', 'Asia/Ulan_Bator'],
267 | ['Asia/Katmandu', 'Asia/Kathmandu'],
268 | ['Africa/Asmera', 'Africa/Asmara'],
269 | ['America/Coral_Harbour', 'America/Atikokan'],
270 | ['Atlantic/Faeroe', 'Atlantic/Faroe'],
271 | ['America/Godthab', 'America/Nuuk'],
272 | ['Pacific/Truk', 'Pacific/Chuuk', 'Pacific/Yap'],
273 | ['Pacific/Enderbury', 'Pacific/Kanton'],
274 | ['Pacific/Ponape', 'Pacific/Pohnpei']
275 | ];
276 | ```
277 |
278 | ### 4. Prescriptive spec text to reduce divergence between implementations
279 |
280 | _Status: At this point it seems unlikely that we'll get this cross-implementer agreement soon. So it's likely that we'll remove this step from the scope of this proposal, and follow up separately in parallel._
281 |
282 | This step involves agreement between implementers and TG2 about how canonicalization should work.
283 | It may require agreeing on (or recommending) which external source of canonicalization (IANA or CLDR) ECMAScript should rely on, and (if IANA) which TZDB build options should be used.
284 | Making progress here requires input from specifiers and implementers who understand the tradeoffs involved.
285 | Note that one acceptable outcome may be to “agree to disagree” as long as we can agree on most parts.
286 | We don’t need perfect alignment to reduce ecosystem variance.
287 |
288 | There's useful info in [@anba](https://github.com/anba)'s comments [here](https://github.com/tc39/proposal-temporal/issues/2509#issuecomment-1461418026) that could be used as a starting point.
289 |
290 | It will likely be much easier to achieve consensus on this spec text after progress is made on (3) above, because those 13 outdated Links are the largest current difference in canonicalization behavior between Firefox and V8/WebKit.
291 |
292 | ### 5. Defer Link-traversing canonicalization
293 |
294 | _Status: [Spec text](https://tc39.es/proposal-canonical-tz), [polyfill](./polyfill/README.md), and [tests](https://github.com/tc39/test262/pull/3837) are complete._
295 |
296 | This normative change would defer Link traversal to enable a Link identifier to be stored in internal slots of `ZonedDateTime`, `TimeZone`, and `Intl.DateTimeFormat`, so that it can be returned back to the user.
297 |
298 | The justification for this change is that canonicalization itself is problematic because it always makes at least some people unhappy: developers of existing code are annoyed when their code behaves differently, while other developers are annoyed if outdated identifiers are used.
299 | To sidestep both problems, this proposed change would make canonicalization mostly invisible to Temporal users, except one place: time zone identifiers returned from `Temporal.Now`.
300 |
301 | A tradeoff of this change is that comparing the string representation of `TimeZone` objects (or their `id` properties, or the time zone slot of `Temporal.ZonedDateTime`) would no longer be a reliable way to test for equality.
302 | Therefore, (6) below proposes a new `TimeZone.prototype.equals` API.
303 |
304 | This change requires the following normative edits:
305 |
306 | - a) Change `GetAvailableNamedTimeZoneIdentifier(id).[[PrimaryIdentifier]]` to `GetAvailableNamedTimeZoneIdentifier(id).[[Identifier]]` in places where user input identifiers are parsed and/or stored.
307 | - b) Call `GetAvailableNamedTimeZoneIdentifier(id).[[PrimaryIdentifier]]` before using identifiers for purposes that require canonicalization, such as the `TimeZoneEquals` abstract operation.
308 |
309 | These changes are described in the [spec text](https://tc39.es/proposal-canonical-tz), [polyfill](./polyfill/README.md), and [tests](https://github.com/tc39/test262/pull/3837) of this proposal.
310 |
311 | A few performance-related notes:
312 |
313 | - Storing user-input identifier strings is not necessary because identifiers are case-normalized by `GetCanonicalTimeZoneIdentifier` before storing.
314 | There are fewer than 600 identifiers, so built-in time zone identifiers could be stored as a 2-byte (or even 10-bit) indexes into a ~9KB array of ASCII strings.
315 | - This proposal WOULD NOT require storing both original and canonical ID indexes in each `TimeZone`, `ZonedDateTime`, and `Intl.DateTimeFormat` instance.
316 | Implementations could choose to do this for ease of implementation, but they can also save 1-2 bytes per instance by canonicalizing just-in-time via a 2.3KB map of each identifier's index to its corresponding Zone's identifier's index.
317 |
318 | ### 6. Add `Temporal.TimeZone.prototype.equals`
319 |
320 | _Status: [Spec text](https://tc39.es/proposal-canonical-tz), [polyfill](./polyfill/README.md), and [tests](https://github.com/tc39/test262/pull/3837) are complete._
321 |
322 | The final step would expose Temporal's [`TimeZoneEquals`](https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals) to ECMAScript code to enable developers to compare two time zones to see if they resolve to the same Zone.
323 |
324 | ```javascript
325 | // More ergonomic canonical-equality testing
326 | Temporal.TimeZone.from('Asia/Calcutta').equals('Asia/Kolkata');
327 | // => true
328 | ```
329 |
330 | This `equals` pattern matches how other Temporal types like `ZonedDateTime` and `PlainDate` offer equality comparisons.
331 |
332 | Behavior of `ZonedDateTime.prototype.equals` API would not change, because it (just like all other APIs other than those that return identifiers) would canonicalize before using time zone identifiers.
333 |
334 | A reason to include this new API in this proposal instead of waiting until later is that it'd prevent the pattern `tz1.id === tz2.id` from becoming endemic in ECMAScript code, because that pattern will be broken by this proposal.
335 |
336 | Without this API, testing for canonical equality is still possible, it's just less ergonomic:
337 |
338 | ```javascript
339 | const EPOCH = Temporal.Instant.fromNanoseconds(0n);
340 | function canonicalEquals(zone1, zone2) {
341 | const zdt1 = EPOCH.toZonedDateTimeISO(zone1);
342 | const zdt2 = EPOCH.toZonedDateTimeISO(zone2);
343 | return zdt1.equals(zdt2);
344 | }
345 | ```
346 |
347 | Longer-term extensions (out of scope to this proposal) could be added to force canonicalization at creation time:
348 |
349 | ```javascript
350 | Temporal.TimeZone.from('Asia/Calcutta', { canonicalize: 'full' });
351 | // => Asia/Kolkata
352 | // Opt-in canonicalization
353 | Temporal.TimeZone.canonicalize('Asia/Calcutta');
354 | // => Asia/Kolkata
355 | ```
356 |
357 | That said, exposing canonical identifiers has been a source of grief in every software platform.
358 | So adding APIs that make canonicalization more visible might invite more user complaints.
359 | This is another good reason to defer these kinds of APIs until a later proposal. :smile:
360 |
361 | ## TZDB size calculations
362 |
363 | Source: https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/cldr-bcp47/bcp47/timezone.json
364 |
365 | ```javascript
366 | // all identifiers
367 | ianaIdLists = Object.values(t.keyword.u.tz)
368 | .map((tz) => tz._alias)
369 | .filter((s) => s)
370 | .map((s) => s.split(/,* /g));
371 | ids = ianaIdLists.flat();
372 | zones = ianaIdLists.map((list) => list[0]);
373 | links = ianaIdLists.flatMap((list) => list.slice(1));
374 |
375 | [ids, zones, links].map((arr) => arr.length);
376 | // => [594, 461, 133]
377 |
378 | avgLength = (arr) => arr.join('').length / arr.length;
379 | [ids, zones, links].map((arr) => avgLength(arr).toFixed(1));
380 | // => [14.3, 14.9, 12.2]
381 | ```
382 |
383 | CLDR Links and Zones above used in V8 and WebKit aren't exactly the same as IANA data used in Firefox.
384 | But CLDR is close enough that the numbers above should be within 20% of Firefox stats.
385 |
386 | ## References
387 |
388 | ### ICU4X
389 |
390 | Rust localization API (including Temporal-friendly [timezone API](https://github.com/unicode-org/icu4x/tree/main/components/timezone)) that's being implemented now.
391 |
392 | See https://github.com/unicode-org/icu4x/issues/2909 for canonicalization API discussion.
393 |
394 | ### IANA Time Zone Database ([TZDB](https://www.iana.org/time-zones))
395 |
396 | Standard repository of time zone data, including Link and Zone identifiers and data required to calculate the UTC offset of moments in time for any time zone.
397 |
398 | Maintained via PRs to the [eggert/tz](https://github.com/eggert/tz) repo, with discussion on the [TZDB mailing list](https://mm.icann.org/pipermail/tz/).
399 |
400 | The data in the TZDB repo is not intended to be used raw.
401 | Instead, the repo's MAKEFILE offers various build options which will generate data files for use by applications.
402 | Changes in build options can yield very different output, including large differences in canonicalization behavior.
403 |
404 | One of the goals of this proposal is to define which of these build options should be used by ECMAScript implementations.
405 |
406 | ### [global-tz](https://github.com/JodaOrg/global-tz) repo
407 |
408 | Provides pre-built TZDB data files using build options that are more aligned with the needs of Java (and also ECMAScript) than the default TZDB build options.
409 |
410 | The files in global-tz are claimed (by the [TZDB News](https://github.com/eggert/tz/blob/27148539e699d9abe50df84371a077fdf2bc13de/NEWS#L427-L430) file) to be the same as the results of building TZDB with `make PACKRATDATA=backzone PACKRATLIST=zone.tab`.
411 | This build configuration backs out undesirable (from ECMAScript's point of view) merging of unrelated time zones like `Atlantic/Reykjavik` and `Africa/Abidjan` that may diverge in the future.
412 |
413 | Also, this build configuration is similar to the Zones and Links used by Firefox.
414 |
415 | This repo is maintained by the champion of [JSR-310](https://jcp.org/en/jsr/detail?id=310), the current Java date/time API and maintainer of the [Joda](https://github.com/JodaOrg/joda-time) date/time API library which is used by older Java implementations and which JSR-310 was based on.
416 |
417 | Note that the string serialization format of `Temporal.ZonedDateTime`, including use of IANA time zone identifiers, was designed to be interoperable with [`java.time.ZonedDateTime`](https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html).
418 | In addition, ECMAScript's time zone use cases are similar to Java's.
419 | So this may be a useful standard TZDB build configuration to consider recommending for in ECMAScript.
420 |
--------------------------------------------------------------------------------