时间只是幻觉。—— 阿尔伯特·爱因斯坦
最近在开发一个需要完善国际化方案的前端项目,在处理时间国际化的时候遇到了一些问题。于是花了一些时间研究,有了这篇文章。不过由于网上关于 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 来表示。
newDate('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 维护的时区数据库[1](IANA TZ Database),其中包含了自 Unix 时间戳 0 以来各时区的变更信息。当然这一数据库也包含了大量 Unix 时间戳 0 之前的时区变更信息,但并不能保证这些信息的准确性。IANA 时区数据库会定期更新,以反映新的时区变更和新发现的历史史实导致的时区历史变更。
Windows 不使用 IANA 时区数据库。微软为 Windows 自己维护了一套时区数据库[2],这有时会导致在一个系统上合法的时间在另一系统上不合法。
既然我们不能使用 UTC 偏移来表示一个时区,那就只能为每个时区定义一个标准名称。通常地,我们使用 <大洲>/<城市>
来命名一个时区。这里的城市一般为该时区中人口最多的城市。于是,我们可以将中国的通行时区表示为 Asia/Shanghai
。也有一些时区有自己的别名,如太平洋标准时间 PST
和协调世界时 UTC
。
时区名称使用城市而非国家,是由于国家的变动通常比城市的变动要快得多。
城市不是时区的最小单位。有很多城市同时处于多个时区,甚至澳大利亚有一个机场[3]的跑道两端处于不同的时区。
几个月前的一天,奶冰在他的 Telegram 频道里发了这样的一条消息:
你想的没错,这个问题正是由时区与 UTC 偏移的不同造成的。Asia/Shanghai
时区在 1940 年前后和 1986 年前后曾实行过夏令时,**而夏令时的切换会导致一小时的出现和消失**。具体来说,启用夏令时当天会有一个小时消失,如 2021.3.28 英国启用夏令时,1:00 直接跳到 3:00,导致 2021-03-28 01:30:00
在 Europe/London
时区中是不合法的;取消夏令时当天又会有一个小时重复,如 2021.10.31 英国取消夏令时,2:00 会重新跳回 1:00 一次,导致 2021-10-31 01:30:00
在 Europe/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 从而导致上文问题发生。而随后社区发现了更准确的史实[4],因此 IANA 更新了数据库。上文的问题在更新了系统的时区数据库后便解决了。
IANA TZDB 2018a 及之前版本的错误数据
再来考虑另一种情况。你的应用的某位巴西用户在 2018 年保存了一个未来时间 2022-01-15 12:00
(按当时的规律那应该是个夏令时时间),不巧那时候你的应用是以格式化的时间字符串形式保存的时间。之后你发现巴西已经于 2019 年 4 月宣布彻底取消夏令时制度,那么 2022-01-15 12:00
这个时间对应的 Unix 时间戳发生了变化,变得不再准确,要正确处理这一字符串就需要参考这一字符串生成的时间(或生成时计算的 UTC 偏移)来做不同的处理。**因此,应用从一开始就应该避免使用字符串来传输、存储时间,而是使用 Unix 时间戳**。如果不得不使用字符串存储时间,请尽可能:
时区历史带来的问题往往意想不到而且远比想象得多。实际上时区历史数据非常详细而繁多且跨设备不一致,并没有简单而统一的处理方法。在需要严谨处理时区时可能需要在应用程序中内嵌一套各端统一的时区数据库,但这样的方案放在前端又会带来不少问题:
ES6 为我们带来了 Intl
命名空间[6]。在这里,JavaScript 运行时提供了不少时间相关的国际化能力。因此,在不使用额外数据的情况下准确处理时区是可能的,但这并不完美:
JavaScript
中 Date
对象的不良设计导致实现完善的时区处理并不容易,且 Intl
命名空间下的对象实例化性能开销较大,需要额外优化
Intl
命名空间下还有很多实用的国际化相关方法,值得我们另开一篇文章来讲讲了。
在真实开发中,这需要取舍。目前主流的 JavaScript 时间处理库都已转向浏览器内置方法,并在需要时通过 Polyfill 保证跨端一致性。在这篇文章中,我们将尝试在不使用第三方库的情况下实现基本的时间国际化处理。此外,还有一些诸如需要使用 Unix 时间戳才能正确地在各端交换时间等细节需要注意。
JavaScript 中的 Date
并不是不包含时区信息——实际上,Date
对象表示的一定是当前时区。通过尝试:
newDate('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) => {
returnnewDate(date.toLocaleString('en-US', { timeZone }))
}
const now = newDate() // 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[7]。这是一个声称为自动更新的 IANA TZDB 的 JSON 导出,并已被数个大型项目使用。你可以从这个包中导出所有时区名称。
这个方法看起来还不错,对吧?但实际上,它有两个问题:
toLocaleString()
实际上每次调用都会在 JavaScript 运行时中创建新的 Intl.DateTimeFormat
对象(在后文详述),而后者会带来昂贵的性能开销(在 Node 14 中,实例化一次会在 V8 中增加内存使用约 46.3Kb。但这是符合预期的,详见 V8 Issue[8])。因此,在密集调用的情况下需要考虑计算并缓存时差,并在一定时间后或需要时进行更新toLocaleString()
并使用 en-US
区域设置格式化的默认时间格式类似于 10/13/2021, 1:00:00 AM
。这可以被大部分浏览器正确解析,**但这是不规范的,不同浏览器有可能产生不同结果**。你也可以自行配置格式(同下文的 Intl.DateTimeFormat
),但仍然无法构造出规范的字符串因此,更佳的方案是,我们需要建立一个可反复使用的格式化器以避免重复创建 Intl.DateTimeFormat
带来的额外开销,并需要手动构造出符合规范的时间字符串,并将其重新解析为 Date
对象。
const timeZoneConverter = (timeZone) => {
// 新建 DateTimeFormat 对象以供对同一目标时区重用
// 由于时区属性必须在创建 DateTimeFormat 对象时指定,我们只能为同一时区重用格式化器
const formatter = newIntl.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 标准格式时间字符串并被正确解析
returnnewDate(formatter.format(date).replace(/\//g, '-').replace(' ', 'T').trim())
}
}
}
const toLondonTime = timeZoneConverter('Europe/London') // 对于同一时区,此对象可重用
const now = newDate() // 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(newDate(secDate), tzName) - secDate
}
calcTimeDiff(newDate(), timeZoneConverter('Europe/London')) // -25200000
无论如何,在折腾一番后我们还是把时区正确转换了。接下来准备格式化时间字符串了吗?不过在此之前,我们得先来聊聊语言、文字和区域。
如何在计算机中表示中文?
“这不简单,”你可能会说,“用 zh
啊。”
那简体中文呢?
“zh-CN
。”你或许会说出这个答案。
那用于新加坡的简体中文和用于中国大陆的简体中文该如何区分呢?
嗯……好问题。
要能正确区分不同的简体中文,我们还得先回到定义上。实际上,“国际化”并不只是语言的翻译而已,国际化包含的是一整套对于各个区域的本地化方案。要准确表示一个国际化方案,我们实际至少需要确定三个属性:语言(Language)、文字(Script)和区域(Locale)。
只有确定了这三个属性,我们才能正确定义一个国际化方案(或者说区域设置)。当然,还有很多其他属性可以更准确的表达某个区域设置,但通常有语言、文字和区域就已经足够了。
于是,基于 BCP 47[9],我们可以知道:
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 也有做具体定义。其中:
zh
,英文为 en
)或 ISO 639-2/3[10] 定义的三位字母代码(如中文普通话为 cmn
,英文为 eng
),通常小写Hans
,繁体中文是 Hant
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-Hans
和zh-Hant
开头的区域设置名称已经被标记为redundant
废弃,因此尽可能只使用zh-CN
或者cmn-Hans-CN
这样的区域设置名称。所有区域设置名称的列表可以在 IANA[13] 找到。
现在我们可以准确定义一个区域设置了。不过我们还有一些小小的需求。比如我们想在 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 网站[14]上找到。而多个 u 扩展还可以连接——于是我们甚至可以写出 cmn-Hans-CN-u-ca-chinese-nu-hansfin
这种丧心病狂的区域设置名称。当然,相信你现在已经可以看懂这个区域设置的意思了。
不同地区可能会有不同的日历使用习惯,如中国有使用农历的需求,泰国有使用佛历的需求,我们可以通过 u 扩展指定不同的日历。不过,大部分情况下我们会使用标准的 ISO 8601 日历(gregory),JavaScript 的
Date
对象也只支持这种日历。
你可以使用 BCP47 language subtag lookup[15] 工具快速检查你编写的 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(newDate()) // 2021-10-13 01:00:00
就完事了……吗?先不论这样的格式化代码难以阅读,尽管上文这样的日期格式国际通用,但并非所有区域都习惯于这样的日期表示方法。比如英语国家/地区在很多时候习惯在日期中加入星期,而阿拉伯语国家/地区在部分情况下习惯使用阿拉伯语数字(而非常用的阿拉伯-印度数字);再比如美式英语国家/地区习惯月-日-年的日期表示法,而英式英语国家/地区习惯日-月-年的日期表示法……不同区域在时间表示格式习惯上的差异是巨大的,我们很难通过一个简单的方法来正确地、国际化地格式化一个日期。
好在 ES6 早就为我们铺平了道路。还记得上文提到过的 Intl.DateTimeFormat
吗?我们通过它来实例化一个日期格式化器并用进行日期的国际化。
直接来看例子吧:
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'long'
}
const now = newDate()
const enUSFormatter = newIntl.DateTimeFormat('en-US', options)
const zhCNFormatter = newIntl.DateTimeFormat('zh-CN', options)
const zhCNAltFormatter = newIntl.DateTimeFormat('zh-CN-u-ca-chinese', options)
const zhCNAlt2Formatter = newIntl.DateTimeFormat('zh-CN-u-ca-roc-nu-hansfin', options)
const jaFormatter = newIntl.DateTimeFormat('ja', options)
const jaAltFormatter = newIntl.DateTimeFormat('ja-JP-u-ca-japanese', options)
const arEGFormatter = newIntl.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 文档[16]来了解更多。
如前文所述,
Intl.DateTimeFormat
无法格式化毫秒。
不过需要注意的是,JavaScript 运行时不一定支持所有区域设置,也不一定支持所有格式化选项。在遇到不支持的情况时,Intl.DateTimeFormat
默认会静默 fallback 到最匹配的支持项,因此在处理不常见的区域设置或选项时,你可能需要再额外检查。你可以通过 Intl.DateTimeFormat.supportedLocalesOf()
静态方法判断当前运行时是否支持指定的区域设置,也可以在实例化格式化器后在对象上调用 resolvedOptions()
方法来检查运行时的解析结果是否与预期一致。
newIntl.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 = newDate(1970, 0, 1).getTime()
const formatter = newIntl.DateTimeFormat(locale, { weekday: 'short' })
returnArray.from({ length: 7 }, (_, day) => (
formatter.format(newDate(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()
方法可以用。来看一个简单的例子吧:
newIntl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
}).formatToParts(newDate())
// [
// { 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 文档[17]。直接来看一个简单例子吧:
const getRelativeTime = (num, unit, locale) => {
returnnewIntl.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[18]。不过目前(2021.10)主流浏览器的最新版本均已支持此 API。
我希望这篇文章时区转换的部分可以很快过时——这并非无稽之谈,目前(2021.10)TC39 的 Temporal
提案[19]已经进入 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 可用。
不过,时区问题不会消失,各地区的习惯也很难融合到一起。时间的国际化处理是极其复杂[20]的,前端中的时间国际化仍然值得我们认真关注。
[1]IANA 维护的时区数据库: https://www.iana.org/time-zones
[2]微软为 Windows 维护的时区数据库: https://docs.microsoft.com/zh-cn/troubleshoot/windows-client/system-management-components/daylight-saving-time-help-support
[3]澳大利亚一个两端处于不同时区的机场: https://en.wikipedia.org/wiki/Gold_Coast_Airport
[4]发现了更准确的史实: http://mm.icann.org/pipermail/tz/2018-August/026760.html
[5]一种简洁的 TZDB 表示: https://github.com/moment/moment-timezone/blob/develop/data/packed/2021a.json
[6]Intl 命名空间: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl
[7]@vvo/tzdb: https://github.com/vvo/tzdb/
[8]V8 Issue: https://github.com/vvo/tzdb/
[9]BCP 47: https://tools.ietf.org/rfc/bcp/bcp47.txt
[10]ISO 639-2/3: https://iso639-3.sil.org/code_tables/639/data
[11]ISO 15924: https://unicode.org/iso15924/iso15924-codes.html
[12]ISO 3166-1: https://www.iso.org/iso-3166-country-codes.html
[13]IANA: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
[14]Unicode 网站: https://www.unicode.org/reports/tr35/tr35-21.html#Unicode_locale_identifier
[15]BCP47 language subtag lookup: https://r12a.github.io/app-subtags/
[16]MDN 文档: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
[17]MDN 文档: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
[18]Polyfill: https://formatjs.io/docs/polyfills/intl-relativetimeformat
[19]Temporal 提案: https://github.com/tc39/proposal-temporal
[20]极其复杂: https://yourcalendricalfallacyis.com/
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/fepgCR4hikmMwIG3Fbb9gQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。