关于前端:前端时间国际化入门

6次阅读

共计 13309 个字符,预计需要花费 34 分钟才能阅读完成。

工夫只是幻觉。—— 阿尔伯特·爱因斯坦

最近在开发一个须要欠缺国际化计划的前端我的项目,在解决工夫国际化的时候遇到了一些问题。于是花了一些工夫钻研,有了这篇文章。不过因为网上对于 JavaScript 中 Date 对象的坑的文章曾经一抓一大把了,因而这篇文章不是 JavaScript 中 Date 对象的使用指南,而是只专一于前端工夫国际化

从时区说起

要想解决工夫,UTC 是一个绕不开的名字。协调世界时(Coordinated Universal Time)是目前通用的世界工夫规范,计时基于原子钟,但并不等于 TAI(国内原子时)。TAI 不计算闰秒,但 UTC 会不定期插入闰秒,因而 UTC 与 TAI 的差别正在不断扩大。UTC 也靠近于 GMT(格林威治规范工夫),但不齐全等同。可能很多人都发现近几年 GMT 曾经越来越少呈现了,这是因为 GMT 计时基于地球自转,因为地球自转的不规则性且正在逐步变慢,目前曾经根本被 UTC 所取代了。

JavaScript 的 Date 实现不解决闰秒。实际上,因为闰秒减少的不可预测性,Unix/POSIX 工夫戳齐全不思考闰秒。在闰秒产生时,Unix 工夫戳会反复一秒。这也意味着,一个工夫戳对应两个工夫点是有可能产生的。

因为 UTC 是规范的,咱们有时会应用 UTC+/-N 的形式表白一个时区。这很容易了解,但并不精确。中国通行的 Asia/Shanghai 时区 大部分 状况下能够用 UTC+8 示意,但英国通行的 Europe/London 时区并不能用一个 UTC+N 的形式示意——因为夏令时制度,Europe/London 在夏天等于 UTC+1,在冬天等于 UTC/GMT。

一个时区与 UTC 的偏移并不一定是整小时。如 Asia/Yangon 以后为 UTC+6:30,而 Australia/Eucla 目前领有微妙的 UTC+8:45 的偏移。

夏令时的存在表明 工夫的示意不是间断的 ,时区之间的时差也并不是固定的,咱们并不能用固定时差来解决工夫,这很容易意识到。但一个不容易意识到的点是,时区还蕴含了其历史变更信息。中国目前不履行夏令时制度,那咱们就能够释怀用 UTC+8 来示意中国的时区了吗?你可能曾经留神到了上一段中形容 Asia/Shanghai 时区时我应用了 大部分 一词。Asia/Shanghai 时区在历史上履行过夏令时,因而 Asia/Shanghai 在局部时间段能够应用 UTC+9 来示意。

new Date('1988-04-18 00:00:00')
// Mon Apr 18 1988 00:00:00 GMT+0900 (中国夏令时间)

夏令时曾经够凌乱了,但它实际上比你设想得更凌乱——局部穆斯林国家一年有四次夏令时切换(进入斋月时夏令时会临时勾销),还有一些国家应用混沌的 15/30 分钟夏令时而非通常的一小时。

不要总是基于 00:00 来判断一天的开始。局部国家应用 0:00-1:00 切换夏令时,这意味着 23:59 的下一分钟有可能是 1:00。

事实上,尽管一天只有 24 个小时,但以后(2021.10)正在应用的时区有超过 300 个。每一个时区都蕴含了其特定的历史。尽管有些时区在当初看起来是统一的,但它们都蕴含了不同的历史。时区也会发明新的历史。因为政治、经济或其余起因,一些时区会调整它们与 UTC 的偏差(萨摩亚已经从 UTC-10 切换到 UTC+14,导致该国 2011.12.30 整一天都隐没了),或是启用 / 勾销夏令时,甚至有可能导致一个时区从新划分为两个。因而,为了正确处理各个时区,咱们须要一个数据库来寄存时区变更信息。还好,曾经有人帮咱们做了这些工作。目前大多数 *nix 零碎和大量开源我的项目都在应用 IANA 保护的时区数据库(IANA TZ Database),其中蕴含了自 Unix 工夫戳 0 以来各时区的变更信息。当然这一数据库也蕴含了大量 Unix 工夫戳 0 之前的时区变更信息,但并不能保障这些信息的准确性。IANA 时区数据库会定期更新,以反映新的时区变更和新发现的历史史实导致的时区历史变更。

Windows 不应用 IANA 时区数据库。微软为 Windows 本人保护了一套时区数据库,这有时会导致在一个零碎上非法的工夫在另一零碎上不非法。

既然咱们不能应用 UTC 偏移来示意一个时区,那就只能为每个时区定义一个规范名称。通常地,咱们应用 < 大洲 >/< 城市 > 来命名一个时区。这里的城市个别为该时区中人口最多的城市。于是,咱们能够将中国的通行时区示意为 Asia/Shanghai。也有一些时区有本人的别名,如太平洋规范工夫 PST 和协调世界时 UTC

时区名称应用城市而非国家,是因为国家的变动通常比城市的变动要快得多。

城市不是时区的最小单位。有很多城市同时处于多个时区,甚至澳大利亚有一个机场的跑道两端处于不同的时区。

解决时区困难重重

几个月前的一天,奶冰在他的 Telegram 频道里发了这样的一条音讯:

你想的没错,这个问题正是由时区与 UTC 偏移的不同造成的。Asia/Shanghai 时区在 1940 年前后和 1986 年前后曾履行过夏令时,而夏令时的切换会导致一小时的呈现和隐没。具体来说,启用夏令时当天会有一个小时隐没,如 2021.3.28 英国启用夏令时,1:00 间接跳到 3:00,导致 2021-03-28 01:30:00Europe/London 时区中是不非法的;勾销夏令时当天又会有一个小时反复,如 2021.10.31 英国勾销夏令时,2:00 会从新跳回 1:00 一次,导致 2021-10-31 01:30:00Europe/London 时区中对应了两个工夫点。而在奶冰的例子中,1988-04-10 00:46:50 正好处于因夏令时启用而隐没的一小时中,因而零碎会认为此工夫字符串不非法而回绝解析。

你可能会留神到在历史上 1988.4.10 这一天 Asia/Shanghai 时区实际上是去掉了 1:00-2:00 这一小时而不是 0:00-1:00。上文问题更深层次的起因是,在 IANA TZDB 2018a 及更早版本中,IANA 因不足历史材料而设置了谬误的夏令时规定,规定设定了夏令时接壤于 0:00-1:00 从而导致上文问题产生。而随后社区发现了更精确的史实,因而 IANA 更新了数据库。上文的问题在更新了零碎的时区数据库后便解决了。

再来思考另一种状况。你的利用的某位巴西用户在 2018 年保留了一个将来工夫 2022-01-15 12:00(按过后的法则那应该是个夏令时工夫),不巧那时候你的利用是以格式化的工夫字符串模式保留的工夫。之后你发现巴西曾经于 2019 年 4 月发表彻底勾销夏令时制度,那么 2022-01-15 12:00 这个工夫对应的 Unix 工夫戳产生了变动,变得不再精确,要正确处理这一字符串就须要参考这一字符串生成的工夫(或生成时计算的 UTC 偏移)来做不同的解决。因而,利用从一开始就应该防止应用字符串来传输、存储工夫,而是应用 Unix 工夫戳。如果不得不应用字符串存储工夫,请尽可能:

  • 应用 UTC 形容工夫,你永远不会晓得本地时区在将来会产生什么
  • 如果须要以当地工夫形容工夫,肯定带上以后 UTC 偏移

时区历史带来的问题往往意想不到而且远比设想得多。实际上时区历史数据十分具体而繁多且跨设施不统一,并没有简略而对立的解决办法。在须要谨严解决时区时可能须要在应用程序中内嵌一套各端对立的时区数据库,但这样的计划放在前端又会带来不少问题:

  • 体积过大。moment.js 已经设计过一种简洁的 TZDB 示意,但只管曾经尽可能压缩整个文件依然达到了 180+KB。在性能优先的 Web 利用中这是不可承受的
  • 须要继续更新。时区数据始终在变动,须要在时区数据更新时尽快更新利用内的时区数据,这带来了额定的保护老本

ES6 为咱们带来了 Intl 命名空间。在这里,JavaScript 运行时提供了不少工夫相干的国际化能力。因而,在不应用额定数据的状况下精确解决时区是可能的,但这并不完满:

  • 各端不对立。浏览器提供的时区数据受浏览器版本、零碎版本等可能变动,最新的时区更新可能无奈疾速反映到所有设施上
  • 实现简单。JavaScriptDate 对象的不良设计导致实现欠缺的时区解决并不容易,且 Intl 命名空间下的对象实例化性能开销较大,须要额定优化

Intl 命名空间下还有很多实用的国际化相干办法,值得咱们另开一篇文章来讲讲了。

在实在开发中,这须要取舍。目前支流的 JavaScript 工夫解决库都已转向浏览器内置办法,并在须要时通过 Polyfill 保障跨端一致性。在这篇文章中,咱们将尝试在不应用第三方库的状况下实现根本的工夫国际化解决。此外,还有一些诸如须要应用 Unix 工夫戳能力正确地在各端替换工夫等细节须要留神。

时区转换

JavaScript 中的 Date 并不是不蕴含时区信息——实际上,Date 对象示意的肯定是以后时区。通过尝试:

new Date('1970-01-01T00:00:00Z')
// Thu Jan 01 1970 08:00:00 GMT+0800 (中国规范工夫)

就能够晓得,JavaScript 运行时其实晓得以后时区,并会在须要的时候将其余时区的工夫转换为以后时区的工夫。那么,如何将本地工夫转换为其余时区的工夫呢?从 Date 的角度看,这并不行,因为咱们无奈设置一个 Date 对象的时区。但咱们能够“投机取巧”:将 Date 对象的工夫加上 / 减去对应的时差,只管 Date 对象依然认为本人在本地时区,但这样不就能够正确显示了嘛!但咱们会碰到上文提到的问题:时区之间的时间差并不固定,在没有额定数据的状况下很难正确计算。

还好,ES6 基于 Intl 命名空间扩大了 Date.prototype.toLocaleString() 办法,使其能够承受时区参数并按指定时区格式化工夫。如果你在搜索引擎中搜寻如何应用 JavaScript 转换时区,你大概率会在 StackOverflow 上找到相似这样的答案:

const convertTimeZone = (date, timeZone) => {return new Date(date.toLocaleString('en-US', { timeZone}))
}

const now = new Date() // Wed Oct 13 2021 01:00:00 GMT+0800 (中国规范工夫)
convertTimeZone(now, 'Europe/London') // Tue Oct 12 2021 18:00:00 GMT+0800 (中国规范工夫)

很好了解,咱们应用 en-US 的区域设置要求 JavaScript 运行时以咱们指定的时区格式化工夫,再将工夫字符串从新解析为工夫对象。这里的 timeZone 就是诸如 Asia/Shanghai 等的 IANA TZDB 时区名称。这个字符串的确须要本人提供,但这就是咱们惟一须要本人筹备的数据了!只有提供了时区名称,浏览器就会主动计算正确的工夫,无需咱们自行计算。

对于时区名称,你能够思考应用 @vvo/tzdb。这是一个声称为自动更新的 IANA TZDB 的 JSON 导出,并已被数个大型项目应用。你能够从这个包中导出所有时区名称。

这个办法看起来还不错,对吧?但实际上,它有两个问题:

  • 指定了区域设置和时区的 toLocaleString() 实际上每次调用都会在 JavaScript 运行时中创立新的 Intl.DateTimeFormat 对象(在后文详述),而后者会带来低廉的性能开销(在 Node 14 中,实例化一次会在 V8 中减少内存应用约 46.3Kb。但这是合乎预期的,详见 V8 Issue。因而,在密集调用的状况下须要思考计算并缓存时差,并在肯定工夫后或须要时进行更新
  • 应用 toLocaleString() 并应用 en-US 区域设置格式化的默认工夫格局相似于 10/13/2021, 1:00:00 AM。这能够被大部分浏览器正确解析,但这是不标准的,不同浏览器有可能产生不同后果。你也能够自行配置格局(同下文的 Intl.DateTimeFormat),但依然无奈结构出标准的字符串

因而,更佳的计划是,咱们须要建设一个可重复应用的格式化器以防止反复创立 Intl.DateTimeFormat 带来的额定开销,并须要手动结构出符合规范的工夫字符串,并将其从新解析为 Date 对象。

const timeZoneConverter = (timeZone) => {
    // 新建 DateTimeFormat 对象以供对同一指标时区重用
    // 因为时区属性必须在创立 DateTimeFormat 对象时指定,咱们只能为同一时区重用格式化器
    const formatter = new Intl.DateTimeFormat('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
        hour12: false,
        timeZone
    })
    return {
        // 提供 conver 办法以将提供的 Date 对象转换为指定时区
        convert (date) {
            // zh-CN 的区域设置会返回相似 1970/01/01 00:00:00 的字符串
            // 替换字符即可结构出相似 1970-01-01T00:00:00 的 ISO 8601 规范格局工夫字符串并被正确解析
            return new Date(formatter.format(date).replace(/\//g, '-').replace('','T').trim())
        }
    }
}

const toLondonTime = timeZoneConverter('Europe/London') // 对于同一时区,此对象可重用

const now = new Date() // Wed Oct 13 2021 01:00:00 GMT+0800 (中国规范工夫)
toLondonTime.convert(now) // Tue Oct 12 2021 18:00:00 GMT+0800 (中国规范工夫)

目前 zh-CN 的区域设置会产生相似 1970/01/01 00:00:00 的格式化字符串。这一格局目前跨端统一,但因为标准没有指定工夫格局,这个格局在将来有可能变更。更好的计划是应用 formatToParts() 办法(在后文详述)获取工夫字符串的各局部并手动拼接出规范格局的字符串,但在这个例子中间接 replace 领有更好的性能。

当初,尝试重复转换工夫至同一时区 1000 次,耗时从 toLocaleString() 1.5 秒升高到了 0.04 秒。只管代码长了点,但这次重写在最好的状况下为咱们带来了超过 20 倍的性能晋升。

须要留神的是,尽管这看起来就算最终计划了,但这个计划仍然不完满。次要有以下两个问题:

  • 在须要密集转换为不同时区时,因为无奈重用格式化器,性能仍然较差且难以进一步优化
  • 因为 Intl.DateTimeFormat 不反对格式化毫秒,在格式化字符串的过程中毫秒会失落,导致最终后果可能会与冀望后果产生最高 999ms 的误差,须要额定解决。比方须要计算时差时,咱们可能须要这么写:
const calcTimeDiff = (date, converter) => {const secDate = date - date.getMilliseconds() // 去掉毫秒,防止转换前后精度差别
    return converter.convert(new Date(secDate), tzName) - secDate
}

calcTimeDiff(new Date(), timeZoneConverter('Europe/London')) // -25200000

无论如何,在折腾一番后咱们还是把时区正确转换了。接下来筹备格式化工夫字符串了吗?不过在此之前,咱们得先来聊聊语言、文字和区域。

语言文字区域傻傻分不清

如何在计算机中示意中文?

“这不简略,”你可能会说,“用 zh 啊。”

那简体中文呢?

zh-CN。”你或者会说出这个答案。

那用于新加坡的简体中文和用于中国大陆的简体中文该如何辨别呢?

嗯……好问题。

要能正确区分不同的简体中文,咱们还得先回到定义上。实际上,“国际化”并不只是语言的翻译而已,国际化蕴含的是一整套对于各个 区域 的本地化计划。要精确示意一个国际化计划,咱们理论至多须要确定三个属性:语言(Language)、文字(Script)和区域(Locale)。

  • 语言 通常指的是声音语言。不同的语言都有一套本人的发音规定,很难互通。如中文和英语都属于语言
  • 文字 对应的是某个语言的书写形式,同样的语言可能会有多种书写计划。如中文次要有简体和繁体两种书写计划
  • 区域 指国际化面向的地区,雷同的语言和文字,在不同地区也有可能会有不同的应用习惯。如新加坡和中国大陆都应用简体中文,但两地的用词习惯等有些许差别

只有确定了这三个属性,咱们能力正确定义一个国际化计划(或者说 区域设置)。当然,还有很多其余属性能够更精确的表白某个区域设置,但通常有语言、文字和区域就曾经足够了。

于是,基于 BCP 47,咱们能够晓得:

cmn-Hans-CN = 中文普通话 - 简体 - 中国大陆
cmn-Hans-SG = 中文普通话 - 简体 - 新加坡
cmn-Hant-TW = 中文普通话 - 繁体 - 台湾
yue-Hant-HK = 中文粤语 - 繁体 - 香港

等等,这都是啥?还有 BCP 47 又是啥?BCP 是 IETF 公布的“最佳以后实际”文档,而 BCP 47 是一些国际化相干的 ISO 和备忘录的汇合,也是目前事实上由 HTML 和 ECMAScript 所应用的表白区域设置的规范。BCP 47 定义的区域设置标签实际上比较复杂,但对于大部分简略应用状况,上文示例中的格局曾经齐全够用了。简略来说,要表白一个区域设置,咱们会应用 语言 [- 文字][- 区域] 的格局,而文字和区域都是可选的。而对于每个局部的具体代码,BCP 47 也有做具体定义。其中:

  • 语言应用 ISO 639-1 定义的两位字母代码(如中文为 zh,英文为 en)或 ISO 639-2/3 定义的三位字母代码(如中文普通话为 cmn,英文为 eng),通常小写
  • 文字应用 ISO 15924 定义的四位字母代码,通常首字母大写。如简体中文是 Hans,繁体中文是 Hant
  • 区域通常应用 ISO 3166-1 定义的两位字母代码,通常大写,如中国大陆为 CN,英国为 GB

ISO 639-1/2/3 的关系理论是:ISO 639-1 是最早制订的标准,应用两位字母示意语言,但语言数量之多并不能只用两位代码示意。因而起初订正了 ISO 639-2 和 3,应用三位字母示意了更多语言。通常 639-1 代码和 ISO-2/3 代码是一对多的关系。如中文 zh 其实是中文普通话 cmn 的宏语言(macrolanguage),同样应用 zh 为宏语言的语言还有 wuu(中文吴语)、hak(中文客家话)、yue(中文粤语)等数十种。从标准上咱们当初应该应用 ISO 639-2/3 代码来代替 ISO 639-1 代码了 ,但因为历史阻力和实在需要中分类无需如此粗疏等起因, 应用 ISO 639-1 指定语言依然十分常见而且齐全能够承受。此外,特地地,咱们在 ISO 639-3 中定义未指明的语言为 und

因而,对于这一节结尾的两个问题,在 BCP 47 中正确答案其实是:

zh = 中文
cmn = 中文普通话

zh-Hans = 中文 - 简体
cmn-Hans = 中文普通话 - 简体

zh-CN 理论是指在中国大陆应用的中文,当然也蕴含在中国大陆应用的繁体中文。不过,因为大部分状况下一个区域只会通用一种文字,很多状况下咱们能够疏忽文字这一项,即应用 zh-CN(或者 cmn-CN)来示意中国大陆的简体中文普通话——毕竟在大部分业务中在中国大陆应用繁体和非普通话的状况非常少。

事实上,相似 zh-Hanszh-Hant 结尾的区域设置名称曾经被标记为 redundant 废除,因而尽可能只应用 zh-CN 或者 cmn-Hans-CN 这样的区域设置名称。所有区域设置名称的列表能够在 IANA 找到。

当初咱们能够精确定义一个区域设置了。不过咱们还有一些小小的需要。比方咱们想在 cmn-Hans-CN 的区域设置中应用农历来示意日期,但显然咱们上文定义的示意办法并不能表白这一需要。好在,Unicode 为 BCP 47 提供了 u 扩大。在区域设置名称前面加上 -u-[选项] 就能够表白更粗疏的变体了。所以咱们有:

cmn-Hans-CN-u-ca-chinese = 中文普通话 - 简体 - 中国大陆 -u- 日历 - 中国农历
jpn-Jpan-JP-u-ca-japanese = 日语 - 日文汉字 / 平假名 / 片假名 - 日本 -u- 日历 - 日本日历
cmn-Hans-CN-u-nu-hansfin = 中文普通话 - 简体 - 中国大陆 -u- 数字 - 简体大写数字

u 扩大的具体可选项能够在 Unicode 网站上找到。而多个 u 扩大还能够连贯——于是咱们甚至能够写出 cmn-Hans-CN-u-ca-chinese-nu-hansfin 这种丧心病狂的区域设置名称。当然,置信你当初曾经能够看懂这个区域设置的意思了。

不同地区可能会有不同的日历应用习惯,如中国有应用农历的需要,泰国有应用佛历的需要,咱们能够通过 u 扩大指定不同的日历。不过,大部分状况下咱们会应用规范的 ISO 8601 日历(gregory),JavaScript 的 Date 对象也只反对这种日历。

你能够应用 BCP47 language subtag lookup 工具疾速查看你编写的 BCP 47 区域标签是否标准。

终于咱们能够正确表白一个完满合乎咱们需要的区域设置了。接下来,让咱们开始格式化工夫吧。

格式化工夫

这题我会!

const formatDate(date) => {return `${date.getFullYear()}-${`${date.getMonth() + 1}`.padStart(2, '0')}-${`${date.getDate()}`.padStart(2, '0')} ${`${date.getHours()}`.padStart(2, '0')}:${`${date.getMinutes()}`.padStart(2, '0')}:${`${date.getSeconds()}`.padStart(2, '0')}`
}

formatDate(new Date()) // 2021-10-13 01:00:00

就完事了……吗?先不管这样的格式化代码难以浏览,只管上文这样的日期格局国内通用,但并非所有区域都习惯于这样的日期示意办法 。比方英语国家 / 地区在很多时候习惯在日期中退出星期,而阿拉伯语国家 / 地区在局部状况下习惯应用阿拉伯语数字(而十分用的阿拉伯 - 印度数字);再比方美式英语国家 / 地区习惯月 - 日 - 年的日期表示法,而英式英语国家 / 地区习惯日 - 月 - 年的日期表示法……不同区域在工夫示意格局习惯上的差别是微小的, 咱们很难通过一个简略的办法来正确地、国际化地格式化一个日期

好在 ES6 早就为咱们铺平了路线。还记得上文提到过的 Intl.DateTimeFormat 吗?咱们通过它来实例化一个日期格式化器并用进行日期的国际化。

间接来看例子吧:

const options = {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    weekday: 'long'
}
const now = new Date()

const enUSFormatter = new Intl.DateTimeFormat('en-US', options)

const zhCNFormatter = new Intl.DateTimeFormat('zh-CN', options)
const zhCNAltFormatter = new Intl.DateTimeFormat('zh-CN-u-ca-chinese', options)
const zhCNAlt2Formatter = new Intl.DateTimeFormat('zh-CN-u-ca-roc-nu-hansfin', options)

const jaFormatter = new Intl.DateTimeFormat('ja', options)
const jaAltFormatter = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', options)

const arEGFormatter = new Intl.DateTimeFormat('ar-EG', options)

enUSFormatter.format(now) // Wednesday, Oct 13, 2021

zhCNFormatter.format(now) // 2021 年 10 月 13 日星期三
zhCNAltFormatter.format(now) // 2021 辛丑年九月 8 星期三
zhCNAlt2Formatter.format(now) // 民国壹佰壹拾年拾月拾叁日星期三

jaFormatter.format(now) // 2021 年 10 月 13 日水曜日
jaAltFormatter.format(now) // 令和 3 年 10 月 13 日水曜日

arEGFormatter.format(now) // الأربعاء، ١٣ أكتوبر ٢٠٢١

在这里咱们应用 ISO 639-1 代码来示意语言,是因为事实上 ISO 639-1 代码更加常见与通用。在大部分反对 Intl.DateTimeFormat 的 JavaScript 运行时中咱们也能够应用 ISO 639-2/3 代码来示意语言(但理论会 fallback 至对应的 ISO 639-1 代码)。

你也能够通过在 options 中设置 calendar 属性和 numberingSystem 属性来替换区域设置名称中对 u 扩大的应用。这也是举荐形式。

这十分直观,咱们能够指定区域设置和格式化选项来初始化一个格式化器,并在之后应用格式化器对象的 format 办法来格式化一个 Date 对象。这里的格式化选项其实非常灵活,能格式化的不只是日期,工夫也能够被灵便地格式化,有十分多的组合能够抉择。咱们不会在这里具体解释每一个选项,你能够拜访 MDN 文档来理解更多。

如前文所述,Intl.DateTimeFormat 无奈格式化毫秒。

不过须要留神的是,JavaScript 运行时不肯定反对所有区域设置,也不肯定反对所有格式化选项。在遇到不反对的状况时,Intl.DateTimeFormat 默认会静默 fallback 到最匹配的反对项,因而在解决不常见的区域设置或选项时,你可能须要再额定查看。你能够通过 Intl.DateTimeFormat.supportedLocalesOf() 静态方法判断以后运行时是否反对指定的区域设置,也能够在实例化格式化器后在对象上调用 resolvedOptions() 办法来查看运行时的解析后果是否与预期统一。

new Intl.DateTimeFormat('yue-Hant-CN').resolvedOptions()
// {locale: 'zh-CN', calendar: 'gregory', …}
// fallback 至 zh-CN,与 yue-CN 的预期不统一

此外,正如你所看到的,各种语言在日期格式化中应用的文本 JavaScript 运行时都曾经帮咱们内置了。因而,咱们甚至能够利用这些国际化个性来为咱们的利用缩小一点须要翻译的字符串——打包进利用的翻译越少,利用体积也就越小了嘛——比如说获取一周七天对应的名字:

const getWeekdayNames = (locale) => {
     // 基于一个固定日期计算,这里抉择 1970.1.1
     // 不能应用 0,因为 Unix 工夫戳 0 在不同时区的日期不一样
    const base = new Date(1970, 0, 1).getTime()
    const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short'})
    return Array.from({length: 7}, (_, day) => (formatter.format(new Date(base + 3600000 * 24 * (-4 + day))) // 1970.1.1 是周四
    ))
}

getWeekdayNames('en-US') // ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
getWeekdayNames('zh-CN') // ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
getWeekdayNames('ja') // ['日', '月', '火', '水', '木', '金', '土']
getWeekdayNames('ar-EG') // ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت']

当然,如果你还是不喜爱运行时为你提供的格局,咱们还有上文提到过的 formatToParts() 办法能够用。来看一个简略的例子吧:

new Intl.DateTimeFormat('zh-CN', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    weekday: 'long',
    hour: 'numeric',
    minute: 'numeric',
    second: 'numeric',
}).formatToParts(new Date())
// [//     { type: 'year', value: '2021'},
//     {type: 'literal', value: '年'},
//     {type: 'month', value: '10'},
//     {type: 'literal', value: '月'},
//     {type: 'day', value: '13'},
//     {type: 'literal', value: '日'},
//     {type: 'weekday', value: '星期三'},
//     {type: 'literal', value: ' '},
//     {type: 'dayPeriod', value: '上午'},
//     {type: 'hour', value: '1'},
//     {type: 'literal', value: ':'},
//     {type: 'minute', value: '00'},
//     {type: 'literal', value: ':'},
//     {type: 'second', value: '00'}
// ]

随后,你就能够本人解析这个数组来结构出你想要的工夫格局了。最初,咱们还能够应用 Intl.RelativeTimeFormat 来格式化绝对日期。当然咱们不会在这里具体解说这个 API,你能够参考 MDN 文档。间接来看一个简略例子吧:

const getRelativeTime = (num, unit, locale) => {return new Intl.RelativeTimeFormat(locale, { numeric: 'auto'}).format(num, unit)
}

getRelativeTime(-3, 'day', 'en-US') // 3 days ago
getRelativeTime(-1, 'day', 'zh-CN') // 昨天
getRelativeTime(0, 'second', 'zh-CN') // 当初
getRelativeTime(3, 'hour', 'ja') // 3 時間後

Intl.RelativeTimeFormat 是一个绝对较晚进入规范的对象,因而浏览器反对水平较差,可能须要应用 Polyfill。不过目前(2021.10)支流浏览器的最新版本均已反对此 API。

将来

我心愿这篇文章时区转换的局部能够很快过期——这并非无稽之谈,目前(2021.10)TC39 的 Temporal 提案曾经进入 Stage 3 了。Temporal 提案定义了一个新的、时区敌对的 Temporal 命名空间,并冀望在不久后就能进入规范并最终利用于生产环境Temporal 定义了残缺的时区、时间段、日历规定的解决,且领有简单明了的 API。那时候,JavaScript 的时区解决就不会再如此苦楚了。因为目前 Temporal 提案还未进入规范,API 暂未稳固,咱们无奈将其用于生产环境,但咱们能够来看一个简略的例子感受一下这个 API 的弱小。

const zonedDateTime = Temporal.ZonedDateTime.from({
  timeZone: 'America/Los_Angeles',
  year: 1995,
  month: 12,
  day: 7,
  hour: 3,
  minute: 24,
  second: 30,
  millisecond: 0,
  microsecond: 3,
  nanosecond: 500,
  calendar: 'iso8601'
}) // 1995-12-07T03:24:30.0000035-08:00[America/Los_Angeles]

如果你心愿立即开始应用 Temporal,当初已有 Polyfill 可用。

不过,时区问题不会隐没,各地区的习惯也很难交融到一起。工夫的国际化解决是极其简单的,前端中的工夫国际化依然值得咱们认真关注。

正文完
 0