For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/year/pending/proposal-canonical-tz.md.
  • 简体中文
  • Time Zone Canonicalization ?

    中文标题:时区规范化

    提案概览
    提案速览

    ECMAScript 时区使用 IANA 时区数据库标识符,当某个时区的规范标识符发生变化时,不同实现可能不一致,现有代码也可能被破坏。该提案通过澄清规范、对齐实现,并让 Temporal 对标识符变化不那么敏感来处理这些规范化变更。它推迟在存储标识符时遍历 Link,并新增 Temporal.TimeZone.prototype.equals,用于比较两个时区是否解析为同一个 Zone。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    处理时区规范化变更

    ECMAScript 中的时区依赖 IANA 时区数据库(TZDB)标识符,例如 America/Los_Angeles 或 Asia/Tokyo。 本提案改善了当时区在 TZDB 中的规范标识符发生变化时的开发者体验,例如从 Europe/Kiev 变为 Europe/Kyiv。

    状态

    本提案目前遵循 TC39 流程,处于 Stage 3。

    本提案在 2023 年 7 月的 TC39 会议上达到 Stage 3。 Stage 3 审阅者是:Philip Chimento(@ptomato)、Daniel Minor(@dminor)和 Jordan Harband(@ljharb)。

    该会议同意本提案应合并到同样处于 Stage 3 的 Temporal 提案中。 此次合并将通过向 Temporal 仓库提交规范性 PR 来完成,本提案的 champions 将很快提交该 PR。 该 PR 落地后,本仓库中未解决的问题将迁移到 ECMA-402 仓库,并可能通过后续对 ECMA-402 的规范性 PR 来处理。

    以下是本提案推进到 Stage 3 的 2023 年 7 月 TC39 全体会议的幻灯片。

    以下是本提案推进到 Stage 2 的 2023 年 5 月 TC39 全体会议的幻灯片。

    以下是本提案推进到 Stage 1 的 2023 年 3 月 TC39 全体会议的幻灯片。

    本提案的规范“堆叠”在 Temporal 提案之上,因为本提案建立在 Temporal 特性之上,尤其是内置对象 Temporal.TimeZone。

    规范

    本提案的规范文本可在 https://tc39.es/proposal-canonical-tz 找到。 本提案的规范变更很窄:大约 15 行规范文本变更(大多是现有抽象操作中的单行变更),外加一段说明文字。

    提案负责人

    测试

    覆盖本提案全部表面范围的 Test262 测试可在 https://github.com/tc39/test262/pull/3837 获取。

    Polyfill

    有一个非生产环境、仅供测试的 polyfill 可用。 本提案的 polyfill 堆叠在 Temporal 提案的非生产、仅供测试的 polyfill 之上。更多信息请参见本提案的 polyfill README。

    目录

    动机

    实现之间的差异 + 规范与 Web 现实不符

    Temporal.TimeZone.from('Asia/Kolkata');
    // => Asia/Kolkata (Firefox)
    // => Asia/Calcutta (Chrome, Safari, Node -- does not conform to ECMA-402)

    模糊的规范文本:"在 IANA 时区数据库中"

    // TZDB has build options and the spec is silent on which to pick.
    // Default build options, while conforming, are bad for users.
    Temporal.TimeZone.from('Atlantic/Reykjavik');
    // => Africa/Abidjan
    Temporal.TimeZone.from('Europe/Stockholm');
    // => Europe/Berlin
    Temporal.TimeZone.from('Europe/Zagreb');
    // => Europe/Belgrade

    用户投诉

    不能依赖静态数据随时间保持相同行为

    > npm test
    # result = someFunctionToTest('Asia/Calcutta');
    # assertEqual(result.timeZone.id, 'Asia/Calcutta');
    ✅
    > brew upgrade node
    > npm test
    ❌
    Expected: 'Asia/Calcutta'
    Actual: 'Asia/Kolkata'

    使用 === 比较持久化标识符不可靠

    userProfile.timeZoneId = Temporal.Now.timeZoneId();
    userProfile.save();
    // 1 year later (after canonicalization changed)
    userProfile.load();
    if (userProfile.timeZoneId !== Temporal.Now.timeZoneId()) {
      alert('You moved!');
    }

    Temporal 使这些问题更具破坏性

    // Today, canonicalization is invisible for most common use cases.
    // Intl.DateTimeFormat `format` localizes time zone names.
    // Only `resolvedOptions()` exposes the underlying IANA time zone.
    timestamp = '2023-03-10T12:00:00Z';
    timeZone = 'Asia/Kolkata';
    dtf = new Intl.DateTimeFormat('en', { timeZone, timeZoneName: 'long' });
    dtf.format(new Date(timestamp));
    // => '3/10/2023, India Standard Time'
    dtf.resolvedOptions().timeZone;
    // => 'Asia/Kolkata' (Firefox)
    // => 'Asia/Calcutta' (Chrome, Safari, Node)
    
    // In Temporal, canonical identifiers are *very* noticeable.
    zdt = Temporal.Instant.from(timestamp).toZonedDateTimeISO({ timeZone });
    zdt.timeZoneId;
    // => 'Asia/Kolkata' (Firefox)
    // => 'Asia/Calcutta' (Chrome, Safari, Node)
    zdt.getTimeZone().id;
    // => 'Asia/Kolkata' (Firefox)
    // => 'Asia/Calcutta' (Chrome, Safari, Node)
    zdt.getTimeZone().toString();
    // => 'Asia/Kolkata' (Firefox)
    // => 'Asia/Calcutta' (Chrome, Safari, Node)
    zdt.toString();
    // => '2023-03-10T17:30:00+05:30[Asia/Kolkata]' (Firefox)
    // => '2023-03-10T17:30:00+05:30[Asia/Calcutta]' (Chrome, Safari, Node)

    定义

    与以下术语相关的更多细节可在 ECMA-262 的 时区标识符以及 Temporal 规范中修订 ECMA-402 的部分里的 IANA 时区数据库的使用中找到。

    • 可用命名时区标识符 - TZDB 中的时区名称(并在 ECMAScript 中被接受)。 可用命名时区标识符仅由 ASCII 组成,并且不区分大小写。 目前有 594 个可用命名时区标识符(平均长度 14.3 个字符)。 目前每年新增的标识符不到 5 个。
    • 主要时区标识符 - 某个可用命名时区的首选标识符。
    • 非主要时区标识符 - 某个可用命名时区的其他标识符。 一个可用命名时区有一个主要标识符以及 0 个或多个非主要标识符。
    • Zone - TZDB 术语,指由标识符命名的一组规则(参见 如何阅读 tz 数据库和 理论与实用)。 目前有 461 个 Zone(平均长度 14.9 个字符)。 除 Etc/GMT 等罕见例外,TZDB 中的所有 Zone 在 ECMAScript 中都是主要时区标识符。
    • Link - TZDB 术语,指没有自己的 Zone 记录,而是指向另一个标识符并使用其规则的标识符。 当 Zone 被重命名时,会向 backward 添加一个从旧名称到新名称的 Link。 为更新城市拼写而进行的重命名每隔几年发生一次。 目前有 133 个 Link(平均长度 12.2 个字符)。 TZDB 中的 Link 在 ECMAScript 中通常是非主要时区标识符,但 TZDB 构建选项的差异意味着某些 TZDB 构建中的某些 Link 在 ECMAScript 中是主要时区标识符。
    • 大小写规范化 - 将用户提供的 ID 匹配到 IANA TZDB 中的大小写。例如:america/los_angeles ⇨ America/Los_Angeles
    • 规范化 - 返回 IANA Zone,必要时解析 Link。例如:Europe/Kiev ⇨ Europe/Kyiv
    • CLDR 规范化 - V8 和 WebKit(通过 ICU 库)使用的 Zone & Link 数据
    • IANA 规范化 - Firefox(在自定义 TZDB 构建中)使用的 Zone & Link 数据

    提议的解决方案 - 摘要

    以下步骤被设计为“可分离的”;如果因实现复杂性和/或缺乏共识而受阻,我们仍可继续推进其他步骤。

    减少实现之间以及实现与规范之间的差异

    1. 已完成 - 简化抽象操作,处理时区标识符。
    2. 已完成 - 澄清规范,以防止更多分歧
    3. 帮助 V8 和 WebKit 更新 13 个过时的规范化,例如 Asia/Calcutta、Europe/Kiev 和 Asia/Saigon,以免在 Temporal 被广泛采用后这变得痛苦。(无需规范文本)
    4. 用于减少实现之间差异的规定性规范文本。 此步骤需要在实现者以及 TG2(ECMA-402 团队)之间就规范化应如何工作找到共同点。

    减少规范化变更的影响

    1. 避免可观察地跟随 Link。 如果规范化变更不影响现有代码,那么未来的规范化变更破坏 Web 的可能性就会小得多。 由于规范化是由实现定义的,此变更可能在 Temporal Stage 4 之后可以安全发布(也可能不可以;需要研究),但最好不要等太久。
    Temporal.TimeZone.from('Asia/Calcutta');
    // => Asia/Kolkata (current Temporal behavior on Firefox)
    // => Asia/Calcutta (proposed: don't follow Links when returning IDs to callers)
    1. 添加 Temporal.TimeZone.prototype.equals。 因为(5)会停止在创建 TimeZone 对象时规范化 ID,所以有一种符合人体工程学的方式来知道两个 TimeZone 对象是否表示同一个 Zone 会很有帮助。
    // More ergonomic canonical-equality testing
    Temporal.TimeZone.from('Asia/Calcutta').equals('Asia/Kolkata');
    // => true

    提议的解决方案 - 细节

    1. 已完成 - 标识符相关术语和抽象操作的编辑性清理

    状态:编辑性 PR 已合并为 tc39/ecma262#3035

    最近落地到 ECMA-262 的一个 PR 重构了时区标识符的规范文本。 其中一项变更是新增了 时区标识符 说明性章节,定义与标识符相关的术语和概念。 另一项变更是重构与时区标识符相关的抽象操作,以便 ECMA-262、ECMA-402 和 Temporal 中其他与时区标识符相关的功能(包括本提案中的规范性变更)可以基于这些操作构建,而不必求助于新的、非实现定义的 AO:

    AvailableNamedTimeZoneIdentifiers() - 返回一个由实现定义的 Records 列表,每个 Record 由以下部分组成:

    • [[Identifier]]:IANA TZDB 中的标识符
    • [[PrimaryIdentifier]]:传递性 IANA Link 目标的标识符(跟随尽可能多的 Link 以到达 Zone),并与 ECMA-402 的 CanonicalizeTimeZoneName 中对 "UTC" 的特殊处理相同。

    GetAvailableNamedTimeZoneIdentifier(_identifier_) - 过滤 AvailableNamedTimeZoneIdentifiers 的结果,返回其中 [[Identifier]] 与 _identifier_ 进行 ASCII 大小写不敏感匹配的 record,如果没有匹配则返回 ~empty~。 此 AO:

    • 通过使用结果的 [[PrimaryIdentifier]],替换 Temporal 和 ECMA-402 中的 CanonicalizeTimeZoneName。
    • 通过将结果与 ~empty~ 比较,替换 Temporal 中的 IsAvailableTimeZoneName 和 ECMA-402 中的 IsValidTimeZoneName。
    • 通过使用结果的 [[Identifier]],获取大小写规范化后的时区标识符。 此能力将作为本提案的一部分使用。

    本提案的规范文本堆叠在这些最近合并的编辑性变更之上。

    2. 已完成 - 澄清规范以防止更多分歧

    状态:编辑性 PR 已合并为 tc39/proposal-temporal#2573。

    Temporal 规范的 ECMA-402 部分现在包含一个新的 时区数据库的使用章节,推荐构建和处理 IANA 时区数据库更新的最佳实践。

    这些建议的范围足够宽泛,可以涵盖 Firefox 和 V8/WebKit 中的现有实现,但不会比这更宽泛。 例如,所有 ECMAScript 实现似乎都避免使用执行过度急切规范化的默认 TZDB 构建选项,例如 Atlantic/Reykjavik=>Africa/Abidjan。 我们将建议所有实现遵循此最佳实践。

    这些变更的目标是提供一个 Web 现实基线,供进一步的跨实现对齐在此基础上构建。

    3. 修复 V8/WebKit 中过时的规范化

    状态:在 2023-03-29 的 CLDR 会议上,CLDR 同意制定一项设计提案,以提供 IANA 当前规范化信息。 请关注 https://unicode-org.atlassian.net/browse/CLDR-14453 以了解进展更新。

    下面的列表显示了 13 个 Link,它们在 IANA 和 Firefox 中已被取代,但在 CLDR 中仍然规范化为“旧”标识符(因此也影响 ICU,从而影响 V8 和 WebKit)。 下面的数据来自 TZDB 的 2022g 版本,通过一个测试浏览器规范化行为的简单 CodeSandbox 应用获得。

    修复这些过时的 Link 有一定紧迫性,因为如上文所述,在 Temporal 被广泛采用后更改规范化将导致更多变动和客户投诉。

    为了修复这些过时的 Link,我们将与 V8 和 WebKit 的代表(也许还有 ICU 和 CLDR 的代表)合作,看看是否有快速、低成本的方法让 V8/WebKit 更新这 13 个 Zone 的规范化。

    请注意,TZDB 标识符的重命名非常少见。 从对 TZDB NEWS 文件的审查来看,2020-2022 年每年只有一次重命名。 在那之前,最后一次重命名是 2016 年的 Rangoon => Yangon。 理想情况下,ICU 会为这些过时标识符提供及时的解决方案。 但与此同时,如果实现必须在特殊案例列表中硬编码覆盖,那么它不需要经常更新。

    // [0] => canonical in V8/WebKit (from CLDR)
    // [1] => canonical in Firefox (from IANA)
    // [2] => non-canonical Link (if present)
    const outofDateLinks = [
      ['Asia/Calcutta', 'Asia/Kolkata'],
      ['Europe/Kiev', 'Europe/Kyiv'],
      ['Asia/Saigon', 'Asia/Ho_Chi_Minh'],
      ['Asia/Rangoon', 'Asia/Yangon'],
      ['Asia/Ulaanbaatar', 'Asia/Ulan_Bator'],
      ['Asia/Katmandu', 'Asia/Kathmandu'],
      ['Africa/Asmera', 'Africa/Asmara'],
      ['America/Coral_Harbour', 'America/Atikokan'],
      ['Atlantic/Faeroe', 'Atlantic/Faroe'],
      ['America/Godthab', 'America/Nuuk'],
      ['Pacific/Truk', 'Pacific/Chuuk', 'Pacific/Yap'],
      ['Pacific/Enderbury', 'Pacific/Kanton'],
      ['Pacific/Ponape', 'Pacific/Pohnpei']
    ];

    4. 用于减少实现之间差异的规定性规范文本

    状态:目前看来我们不太可能很快获得这种跨实现者协议。因此,我们可能会从本提案的范围中移除这一步,并另行并行跟进。

    此步骤涉及实现者和 TG2 之间就规范化应如何工作达成一致。 它可能需要就 ECMAScript 应依赖哪个外部规范化来源(IANA 或 CLDR)达成一致(或提出建议),以及(如果选择 IANA)应使用哪些 TZDB 构建选项。 在这里取得进展需要理解相关权衡的规范制定者和实现者的输入。 请注意,一个可接受的结果可能是“同意各自保留不同意见”,只要我们能就大部分内容达成一致。 我们不需要完美对齐来减少生态系统差异。

    @anba 在这里的评论中有有用的信息,可作为起点。

    在上述 (3) 取得进展后,就这段规范文本达成共识可能会容易得多,因为那 13 个过时的 Link 是当前 Firefox 与 V8/WebKit 之间规范化行为的最大差异。

    状态:规范文本、polyfill 和 测试均已完成。

    这一规范性变更将推迟 Link 遍历,使 Link 标识符能够存储在 ZonedDateTime、TimeZone 和 Intl.DateTimeFormat 的内部槽中,从而可以将其返回给用户。

    此变更的理由是规范化本身就有问题,因为它总是至少让一些人感到不满:现有代码的开发者在其代码行为不同时会感到恼火,而其他开发者在使用了过时标识符时也会感到恼火。 为了规避这两个问题,这一提议的变更将使规范化对 Temporal 用户几乎不可见,只有一个地方例外:从 Temporal.Now 返回的时区标识符。

    此变更的一个权衡是,比较 TimeZone 对象的字符串表示(或其 id 属性,或 Temporal.ZonedDateTime 的时区槽)将不再是测试相等性的可靠方式。 因此,下面的 (6) 提议新增一个 TimeZone.prototype.equals API。

    此变更需要以下规范性编辑:

    • a) 在解析和/或存储用户输入标识符的地方,将 GetAvailableNamedTimeZoneIdentifier(id).[[PrimaryIdentifier]] 改为 GetAvailableNamedTimeZoneIdentifier(id).[[Identifier]]。
    • b) 在将标识符用于需要规范化的目的(例如 TimeZoneEquals 抽象操作)之前,调用 GetAvailableNamedTimeZoneIdentifier(id).[[PrimaryIdentifier]]。

    这些变更描述在本提案的规范文本、polyfill 和 测试中。

    一些与性能相关的说明:

    • 存储用户输入的标识符字符串并不必要,因为标识符在存储前会由 GetCanonicalTimeZoneIdentifier 进行大小写规范化。 标识符不到 600 个,因此内置时区标识符可以存储为指向约 9KB ASCII 字符串数组的 2 字节(甚至 10 位)索引。
    • 本提案不会要求在每 个 TimeZone、ZonedDateTime 和 Intl.DateTimeFormat 实例中同时存储原始和规范 ID 索引。 实现可以选择这样做以简化实现,但它们也可以通过一个 2.3KB 的映射(将每个标识符的索引映射到其对应 Zone 的标识符索引)进行即时规范化,从而每个实例节省 1-2 字节。

    6. 添加 Temporal.TimeZone.prototype.equals

    状态:规范文本、polyfill 和 测试均已完成。

    最后一步将把 Temporal 的 TimeZoneEquals 暴露给 ECMAScript 代码,使开发者能够比较两个时区,看看它们是否解析为同一个 Zone。

    // More ergonomic canonical-equality testing
    Temporal.TimeZone.from('Asia/Calcutta').equals('Asia/Kolkata');
    // => true

    这个 equals 模式与 ZonedDateTime 和 PlainDate 等其他 Temporal 类型提供相等比较的方式一致。

    ZonedDateTime.prototype.equals API 的行为不会改变,因为它(就像除返回标识符的 API 之外的所有其他 API 一样)会在使用时区标识符之前进行规范化。

    将此新 API 包含在本提案中而不是等到以后的一个原因是,它可以防止 tz1.id === tz2.id 模式在 ECMAScript 代码中变得流行,因为该模式将被本提案破坏。

    没有此 API 时,仍然可以测试规范相等性,只是不那么符合人体工程学:

    const EPOCH = Temporal.Instant.fromNanoseconds(0n);
    function canonicalEquals(zone1, zone2) {
      const zdt1 = EPOCH.toZonedDateTimeISO(zone1);
      const zdt2 = EPOCH.toZonedDateTimeISO(zone2);
      return zdt1.equals(zdt2);
    }

    长期扩展(不在本提案范围内)可以被添加,以在创建时强制规范化:

    Temporal.TimeZone.from('Asia/Calcutta', { canonicalize: 'full' });
    // => Asia/Kolkata
    // Opt-in canonicalization
    Temporal.TimeZone.canonicalize('Asia/Calcutta');
    // => Asia/Kolkata

    话虽如此,暴露规范标识符在每个软件平台都一直是令人烦恼的根源。 因此,添加使规范化更可见的 API 可能会招致更多用户投诉。 这是将这些类型的 API 推迟到以后提案的另一个好理由。 :smile:

    TZDB 大小计算

    来源:https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/cldr-bcp47/bcp47/timezone.json

    // all identifiers
    ianaIdLists = Object.values(t.keyword.u.tz)
      .map((tz) => tz._alias)
      .filter((s) => s)
      .map((s) => s.split(/,* /g));
    ids = ianaIdLists.flat();
    zones = ianaIdLists.map((list) => list[0]);
    links = ianaIdLists.flatMap((list) => list.slice(1));
    
    [ids, zones, links].map((arr) => arr.length);
    // => [594, 461, 133]
    
    avgLength = (arr) => arr.join('').length / arr.length;
    [ids, zones, links].map((arr) => avgLength(arr).toFixed(1));
    // => [14.3, 14.9, 12.2]

    上面用于 V8 和 WebKit 的 CLDR Link 和 Zone 与 Firefox 使用的 IANA 数据并不完全相同。 但 CLDR 足够接近,因此上面的数字应在 Firefox 统计数据的 20% 以内。

    参考资料

    ICU4X

    Rust 本地化 API(包括对 Temporal 友好的 时区 API),目前正在实现中。

    关于规范化 API 的讨论见 https://github.com/unicode-org/icu4x/issues/2909。

    IANA 时区数据库(TZDB)

    时区数据的标准仓库,包括 Link 和 Zone 标识符,以及计算任意时区在某一时刻的 UTC 偏移所需的数据。

    通过向 eggert/tz 仓库提交 PR 进行维护,并在 TZDB 邮件列表上讨论。

    TZDB 仓库中的数据并非设计为直接原始使用。 相反,该仓库的 MAKEFILE 提供各种构建选项,这些选项会生成供应用程序使用的数据文件。 构建选项的变化可能产生非常不同的输出,包括规范化行为的巨大差异。

    本提案的目标之一是定义 ECMAScript 实现应使用这些构建选项中的哪些。

    global-tz 仓库

    提供预构建的 TZDB 数据文件,使用的构建选项比默认 TZDB 构建选项更符合 Java(以及 ECMAScript)的需求。

    global-tz 中的文件被声称(由 TZDB News 文件)与使用 make PACKRATDATA=backzone PACKRATLIST=zone.tab 构建 TZDB 的结果相同。 此构建配置消除了(从 ECMAScript 的角度看)不希望的合并,例如 Atlantic/Reykjavik 和 Africa/Abidjan 等不相关时区的合并,这些时区未来可能会分化。

    此外,此构建配置与 Firefox 使用的 Zone 和 Link 类似。

    此仓库由 JSR-310 的 champion 维护,JSR-310 是当前的 Java 日期/时间 API,他还维护 Joda 日期/时间 API 库,供较旧的 Java 实现使用,JSR-310 也基于该库。

    注意,Temporal.ZonedDateTime 的字符串序列化格式(包括 IANA 时区标识符的使用)被设计为可与 java.time.ZonedDateTime 互操作。 此外,ECMAScript 的时区用例与 Java 类似。 因此,这可能是一个有用的标准 TZDB 构建配置,可考虑在 ECMAScript 中推荐。