全网独家-万字长文入门前端全球化
许多国内企业正积极开拓国际市场,如Sea的Shopee、阿里的Lazada、字节的TikTok、拼多多海外版Temu,以及服装快消领域的Shein等。当国内市场存量业务达到峰值预期时,海外业务正成为各公司未来收入增长的主要发力点。因此,前端国际化已成为越来越重要的职业发展方向。
国内IT企业收入天花板:10亿X2元X365天=7300亿元,也就是10亿人口,企业每天赚取用户2元,保持365天,就是单业务增长的营收天花板(大部分业务赚不到2元,用户量也没到10亿)。比如视频如果60元一个月那会员营收天花板就可以这么预估。甚至比这个还低,毕竟用户会流失,拉新也要成本,运营成本是在递增的。
国际化不仅仅是多语言文案适配这么简单,而是一全套的工程化解决方案。笔者觉得更重要的是从业人员需要具备全球视野,对多元文化有包容心和敬畏心理,同时知识面要求也较高。比如,了解SEA、US、UK等常见地区的简写,尊重伊斯兰教的斋月节等习俗。对于服务全球用户的产品来说,对应产品的要求更加复杂,多样性体现在不同的文化习俗差异上,其实即便在庞大的中国内部也存在南北差异。了解的越多越发现这个世界的“多样性”。
概念说明
苹果键盘怎么卖多国?
苹果键盘有很多型号不同型号的布局不一样 www.apple.com/shop/produc…
那如何模仿苹果造一把可以卖到世界各地的键盘?
- 电路板等硬件配件统一生产
- 制定三种布局方案(Arabic, Russian, Ukrainian归为一种, Chinese (Zhuyin)和Korean为一种, Japanese为一种), 单独开孔
- 键帽印刷不同语言的文案
- MacOS开发语言输入软件适配不同键盘的语言输入
其中2, 3, 4项都是为产品的全球化服务
全球化=国际化i18n+本地化l10n
- 产品设计和开发部署需要考虑国际化i18n:
- 多语言。
- 多布局,如阿拉伯语的RTL。
- 多货币。
- 全球多地区多机房部署(离用户越近服务体验越好,数据物理存储隔离符合各个国家数据安全要求)。
- 产品本地化L10N是国际化后的可选流程,需要引入本地化团队转化和质量验收,再投入本地化市场:
- 本地化步骤是可选的,如英美产品UI,语言基本一致可互通。本地化投入少。
- 中文地区等有简体繁体,不同地区用语习惯不一样。也要特别兼顾,如香港的粤语和广州的粤语,在一些用词上有区别,像吸管(广州)-饮筒(香港),你可以看这里。
- 阿拉伯语、希伯来语等地区RTL的阅读习惯,对产品改动较大需要特殊适配。
国际化
产品面向全球用户,需要做语言适配,针对不同国家地区的用户提供对应语言的版本。本质是文本替换,也要考虑文本阅读方向,比如阿拉伯语和希伯来语是从右到左。
可以看下Apple的做法,对不同国家地区提供不同服务
- Apple US 对应链接 www.apple.com/
- Apple CN 对应链接 www.apple.com.cn/
- Apple HK 对应链接 www.apple.com/hk/en/
常见地区语言对应关系可以看ISO 3166-1
Intl
MDN: developer.mozilla.org/en-US/docs/…
浏览器中的 Intl 是一个内置的 JavaScript 对象,用于国际化(Internationalization)支持。它提供了处理日期、时间、数字格式化以及货币和语言的能力,以便网站能够根据用户的语言和地区习惯来显示内容。
在 JavaScript 中,您可以使用 Intl 对象来执行以下操作:
- 格式化日期和时间: 使用
Intl.DateTimeFormat对象可以格式化日期和时间,并根据用户的地区偏好显示日期和时间。 - 格式化数字: 使用
Intl.NumberFormat对象可以格式化数字,并根据用户的地区偏好显示数字。 - 处理货币: 使用
Intl.NumberFormat对象结合指定的货币代码可以格式化货币,并根据用户的地区偏好显示货币。 - 语言和区域设置信息: 使用
Intl.getCanonicalLocales方法可以获取支持的语言和区域设置信息
关于标点符号,通常在国际化的环境下,标点符号使用英文是比较常见的做法,因为英文标点符号在全球范围内都有通用性,可以避免因为地域差异而引起的误解。在 Intl 对象中,一般不会直接涉及到标点符号的处理,而是主要用于处理日期、时间、数字和货币等格式化需求。
前置知识
语言标识
全地球有 N 个民族,有的民族有自己的语言,有的民族用其他国家民族传递过来的语言,融化吸收然后发展自己的文字。
按照 ISO 标准,语言可以用大类+小类表示,比如 zh 就是汉语,是一个大类。而 zh-CN 就是简体中文的缩写,新加坡华人众多久了就有 zh-SG,表示的是新加坡使用的中文。其次还有 zh-HK/zh-TW 和 zh-Hant/zh-Hans 等等。
语言声明是三段式结构 [language]-[script]-[region],如 zh-Hans-CN 表示中国地区简体中文,zh-Hant 表示所有中文繁体。
一起来看下苹果官网是如何适配多国语言的
- 澳门apple www.apple.com/mo/
- 香港apple
- 中国大陆地区apple www.apple.com.cn/
- 新加坡apple www.apple.com/sg/
- 日本apple www.apple.com/jp/
可以看到有的是根域名下通过ISO地区的path比如**/hk/**这样来区分的,有的是直接换域名,比如中国大陆地区
文字阅读顺序
按照普通的中文和英文顺序,都是LTR,上到下,都是世界范围通用的
而ar阿拉伯语, ur乌都语, he希伯来语都是特殊的从右到左, 即RTL的一般会通过标签的dir属性标识, 比如下面的解释 HTML dir Attribute
HEBREW 是指希伯来语,这是一种在以色列广泛使用的语言,也是犹太教的宗教经典文本的原始语言。它属于阿夫罗亚细亚语系,有着悠久的历史和文化价值。希伯来语有其独特的书写系统,从右向左书写。
上图的概念很少人普及,因为非国际化产品不需要多语言,做需要支持海外业务和全球应用的同学可以多了解下。传统的英文、中文简体、拉丁文等都是上图 LATIN 的阅读顺序,如果用上 top、下 bottom、左 left、右 right 代表我们的习惯,也就是 Z 这样的顺序,即行到行是从上到下的顺序,行内阅读顺序是从左到右。
即 left→right,top→bottom 的顺序,有主次分别,left→right 的优先级高于 top→bottom。
而Web标准对其定义是下面这样的
- left = inline-start
- right = inline-end
- top = block-start
- bottom = block-end
讲个笑话,古代书籍就是按照 writing-mode: vertical-rl 排版的。
布局
content-flows
比如 margin: left 或者 text-align: left 在多语言场景都是不合适的,你的左右不是其他人的左右。
而应该用 margin-inline-start 和 text-align: start 替代,即 inline 轴和 block 轴。
// 下面两两相等,请抛弃left/right/top/bottom等属性
// https://web.dev/learn/css/logical-properties/#terminology
margin-left: 1px
margin-inline-start: 1px
margin-right: 1px
margin-inline-end: 1px
margin-top: 1px
margin-block-start: 1px
margin-bottom: 1px
margin-block-end: 1px
text-align: left
text-align: start
text-align: right
text-align: end
max-width: 100px
max-inline-width: 100px
max-inline-size: 150px
max-height: 100px
max-block-width: 100px
padding-left: 1px
padding-inline-start: 1px
padding-top: 1px
padding-block-start: 1px
top: 0.2em;
inset-block-start: 0.2em;
bottom: 0.2em;
inset-block-end: 0.2em;
left: 2px;
inset-inline-start: 2px;
right: 2px;
inset-inline-end: 2px;
border-bottom: 1px solid red;
border-block-end: 1px solid red;
border-bottom-right-radius: 1em;
border-end-end-radius: 1em;
height: 160px;
block-size: 160px;
width: 160px;
inline-size: 160px;
也可以看下面的例子
如上两个例子通过 margin-inline-start 等属性,再在 HTML 元素上添加 dir: rtl 就可以实现多语言的阅读顺序兼容。
由此,常见的布局也会更新为以下形式。常见的物理盒模型用于尺寸计算,逻辑盒模型用于国际化处理。
writing mode 决定 content-flows
上面写了文档有inline and block flow,对应english的left和right,top和bottom。而 writing-mode 可以修改content-flows,比如下面的值
/* 关键值 */
writing-mode: horizontal-tb;
writing-mode: vertical-rl;
writing-mode: vertical-lr;
可以这么理解 writing-mode: horizontal-tb ,前面的horizontal/vertical是指的inline轴的方向
vi和vb
视口宽高viewport在这里也有特殊含义. 比如宽高vw和vh也被取代,用 vi(viewport inline)和vb(viewport block) 替代
1%宽度=1vw=1vi
1%高度=1vh=1vb
JS的scrollLeft
DOM的API可以通过 Element.scrollLeft 获取到元素滚动的距离,下图是一个实际例子
这里在最后做了一个遮罩(绿色边框区域),内部蓝色部分类似一个走马灯,通过overflow:hidden将蓝色高亮部分超出的区域遮住
当蓝色部分滚动到最后,绿色遮罩隐藏,达到一个遮盖,滚动到最后消失的效果,代码如下
const ref = document.querySelector('.tiktok-table__container') // 父节点,蓝色区域
const ref2 = document.querySelector('.tiktok-table__container > table') // 子节点,表格区域
const bufferWidth = 30 // 留一点buffer空间
if (ref && ref2 && ref.clientWidth + ref.scrollLeft >= ref2.clientWidth - bufferWidth) {
// 滚动到最后隐藏绿色遮罩
setTableRightMask(false)
} else {
setTableRightMask(true)
}
但是在RTL下,神奇的事情就发生了,scrollLeft居然是负数
这是因为RTL的实现是通过HTML标签增加属性 dir="rtl” 实现的,会将文档完全翻转过来,所以scrollLeft就会是负数。因为此时(0, 0)这个原点已经是表格右边了
解决方法也很简单,取绝对值就好,这样就忽略了方向的影响
locale
根据ISO标准对全球国家地区进行划分en.wikipedia.org/wiki/ISO_31…。如 "US" 表示美国,"CN" 表示中国。还有常见的如 zh-CN, en-US, en-GB 等。
- CN 是国家地区码,根据国际标准 ISO 3166-1 规定的国家和地区代码。ISO 3166-1 是用于标识国家和地区的国际标准,每个国家或地区都有一个唯一的两字母代码。"CN" 代表中华人民共和国(People's Republic of China),即中国。
- zh-CN 是语言地区码,它通常用于表示中文("zh" 代表中文)以及特定的地区或国家,这里 "CN" 代表中国。"zh" 代表中文,这是根据国际标准 ISO 639-1 规定的语言代码。ISO 639-1 是用于标识语言的国际标准,每个语言都有一个唯一的两字母代码。"zh" 代表中文,但不区分中文的不同方言,如普通话和粤语。
- zh-Hans-CN 表示中国大陆地区的简体中文。还有 zh-Hans-SG 可用于表示新加坡的官方简化中文,zh-Hans-TW 可用于表示台湾的官方简化中文。
Intl. Locale
举个🌰说下Intl API对于locale的定义
const korean = new Intl.Locale('ko', {
script: 'Kore',
region: 'KR',
hourCycle: 'h23',
calendar: 'gregory',
});
const japanese = new Intl.Locale('ja-Jpan-JP-u-ca-japanese-hc-h12');
console.log(korean.baseName, japanese.baseName);
// Expected output: "ko-Kore-KR" "ja-Jpan-JP"
可以看到Intl. Locale就是把传入的字符串拆解为 [language]-[script]-[region] 的组成。
- ja:代表语言代码,表示日语(Japanese)。
- Jpan:代表脚本代码,表示使用日语文字(Japanese script)。
- JP:代表地区代码,表示日本(Japan)。
这里的 u-ca-japanese 表示 Unicode calendar 也就是日历格式(日本日历与众不同),hc 表示 hourCycle,这里 hc-h12 表示12小时制。u-ca-japanese 和 hc-h12 的顺序无关,也就是说如下两种用法完全等价:
u-ca-japanese-hc-h12hc-h12-u-ca-japanese
const japanese = new Intl.Locale('ja-Jpan-JP-u-ca-japanese-hc-h12');
const japanese2 = new Intl.Locale('ja-Jpan-JP-u-hc-h12-ca-japanese');
-u(unicode)可以理解为额外扩展插件,插件系统支持以下扩展,如上使用calendar扩展和hourCycle扩展。
| 参数 | 缩写 |
|---|---|
| calendar | ca (extension) |
| caseFirst | kf (extension) |
| collation | co (extension) |
| hourCycle | hc (extension) |
| numberingSystem | nu (extension) |
| numeric | kn (extension) |
语言声明
<html lang="en-US">
<!-- 说明页面语言是美式英文 -->
<p>I felt some <span lang="de">schadenfreude</span>.</p> <!-- lang不是只有html标签才有, 其他标签也可以添加 -->
<a href="/path/to/german/version" hreflang="de" lang="de">Deutsche Version</a> <!-- a标签上可以用lang表示显示的文本的语言, 也可以用hreflang表示跳转页面的语言 -->
language设置
语言属性通常代表语言标识符的主要部分。它标识了用于表达语言的基本信息。例如,在 BCP 47 标准中,语言标识符通常包含了语言的主要代码部分,例如 "en" 代表英语,"es" 代表西班牙语。
const locales = ['en', 'de', 'ja'];
const displayNames = new Intl.DisplayNames('en-US', { type: 'language' });
locales.forEach(locale => console.log(displayNames.of(locale)));
// English
// German
// Japanese
如果已经有如上代码,再进一步给这些内容添加样式是非常简单的,我们可以使用CSS的选择器。如 [lang|="fr"] 或者 :lang(fr)。
[lang|="fr"] 选择属性 lang=fr 或者 lang属性以fr开头的元素,如 lang=fr-CA。
script设置
脚本属性是语言标识符的可选部分,表示使用的书写系统或文字的风格。这是一个辅助信息,用于更精确地表示特定语言的书写习惯。例如,"Hans" 代表简体中文,"Latn" 代表拉丁文。
script的of支持传入BCP47规范的二字码, 如zh
const scriptNames = new Intl.DisplayNames('en-US', { type: 'script' });
console.log(scriptNames.of('Hans')); // output:Simplified
console.log(scriptNames.of('Hant')); // output:Traditional
console.log(scriptNames.of('Latn')); // output:Latin
const scriptNames = new Intl.DisplayNames('zh-CN', { type: 'script' });
console.log(scriptNames.of('Hans')); // output:简体
console.log(scriptNames.of('Hant')); // output:繁体
console.log(scriptNames.of('Latn')); // output:拉丁文
region设置
region的of方法支持传入en.wikipedia.org/wiki/ISO_31… 里的国家二字码
const regionNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'region' });
const regionNamesInTraditionalChinese = new Intl.DisplayNames(['zh-Hant'], { type: 'region' });
console.log(regionNamesInEnglish.of('US'));
// Expected output: "United States"
console.log(regionNamesInTraditionalChinese.of('US'));
// Expected output: "美國"
文本
阅读顺序声明
html标签dir属性
可以通过html标签的dir属性设置
<html dir="ltr">
<html dir="rtl">
css属性writing-mode
writing-mode: horizontal-tb;
writing-mode: vertical-lr;
writing-mode: vertical-rl;
文本行的布局方向,以及块的排列方向。如果想作用在整个文档需要设置html标签,则全局生效。
第一个属性horizontal/vertical指的是行块的排列,第二个属性则是指文本内容的流向(content flows)
翻转
形如 ? 这种符号类,在多语言下是不一样的。
比如ar阿拉伯语和ur乌尔都斯语问号是RTL的,即 ؟ 是OK的。
而he希伯来语是LTR的,即 ? 是OK的。
是不是很神奇?一个问号也能玩出花来。
常见的需要RTL的语言有下面这些
- 阿拉伯语(AR):阿拉伯语是使用 RTL 方向书写的最著名的语言之一。它是中东和北非地区的主要语言,以及伊斯兰教的官方语言。
- 希伯来语(HE):希伯来语是犹太人的宗教和文化语言,以及以色列的官方语言。它也是一个使用 RTL 方向书写的语言。
- 波斯语(FA):波斯语,也称为法尔西语,是伊朗的官方语言,以及一些中东国家的官方或辅助语言。它使用 RTL 方向书写。
- 乌尔都语(UR):乌尔都语是巴基斯坦和印度的官方语言之一,以及一种使用 RTL 方向书写的语言。
- 帕斯图语(PS):帕斯图语是阿富汗的官方语言之一,也是使用 RTL 方向书写的语言。
const rtlLangs = [
'ar', // 阿拉伯语
'ur', // 巴基斯坦
'he', // 以色列
'he-IL', // 希伯来语(以色列)
'fa-IR', // 波斯语(伊朗)
'ps' // 帕斯图语
];
多语言文案
l10n本地化的一个比较多工作量的部分是文本的翻译, 一种文本到N种文本的翻译需要引入本地化团队. 技术实现上选择也很多
程序打包嵌入文案
通过 key: text 映射,比如 t('key') 最后程序跑出来就是text文案,这种方式不会依赖其他东西,跟普通网页一样内容都是CDN文件。缺点是文案做为静态资源需要用户额外获取,如果处理不好替换错误就展示 key 内容
以下例子以Vue为例,配置如 en.json, fr.json 等等的静态配置文案,打包嵌入CDN的JS文件里。
接口获取
程序运行时通过接口拿文案。可以通过html标签添加query参数 lang=xxx 标记页面语言,或者cookie标记语言选择。
实时翻译替换
加载翻译的脚本,在切换语言的时候替换掉加载的文本。好处是加载的脚本是当前语言所需要的,不会有其他语言的冗余。缺点是依赖一个翻译服务,如果翻译服务宕机了网页就不能正常访问了。
User -> gateway -> SSR -> i18n cache -> read-time translation services(实时翻译服务)
占位符与单复数处理-ICU语法
ICU语法即通用的有if-else逻辑的DSL,如下DSL可以根据传入的值换取不同的表示,常用于国际化业务
I have {num, plural, one{{number} # apple} other{# apples}},but it's too small
Intl. Segmenter 分段器
如果你用过vim一定知道w(word)可以移动到下个单词。英文里把文本分为单词、句子和段落。同理,中文也是。
wword 一个单词ssentence 一个句子pparagraph 一个段落
const segmenter = new Intl.Segmenter('en-US', { granularity: 'word' });
const text = 'This is a sample text for demonstration purposes.';
// 使用 Segmenter 对文本进行分割
const segments = [...segmenter.segment(text)];
console.log(segments);
// 0: {segment: 'This', index: 0, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 1: {segment: ' ', index: 4, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 2: {segment: 'is', index: 5, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 3: {segment: ' ', index: 7, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 4: {segment: 'a', index: 8, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 5: {segment: ' ', index: 9, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 6: {segment: 'sample', index: 10, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 7: {segment: ' ', index: 16, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 8: {segment: 'text', index: 17, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 9: {segment: ' ', index: 21, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 10: {segment: 'for', index: 22, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 11: {segment: ' ', index: 25, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 12: {segment: 'demonstration', index: 26, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 13: {segment: ' ', index: 39, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 14: {segment: 'purposes', index: 40, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 15: {segment: '.', index: 48, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
Intl. Segmenter分段器可以把句子、段落、文章等按照配置切割为不同的segment数组,结构类似正则,有segment属性。
再举个例子,中文语境下真的其实是一个词。
// 创建分段器,指定语言环境和分段类型为'word'
const segmenter = new Intl.Segmenter(['en', 'zh'], { granularity: 'word' });
// 要分割的字符串
const text = 'Hello世界Hello world';
// 使用分段器分割字符串
const segments = segmenter.segment(text);
// 遍历并打印每个分段的结果
for (const segment of segments) {
console.log(`Segment: ${segment.segment}, Index: ${segment.index}, IsWordLike: ${segment.isWordLike}`);
}
// Segment: Hello, Index: 0, IsWordLike: true
// Segment: 世界, Index: 5, IsWordLike: true
// Segment: Hello, Index: 7, IsWordLike: true
// Segment: , Index: 12, IsWordLike: false
// Segment: world, Index: 13, IsWordLike: true
const str = "我真的很强, 强哥的强";
const segmenterJa = new Intl.Segmenter("zh-CN", { granularity: "word" });
const segments = segmenterJa.segment(str);
console.log(Array.from(segments));
// 0: {segment: '我', index: 0, input: '我真的很强, 强哥的强', isWordLike: true}
// 1: {segment: '真的', index: 1, input: '我真的很强, 强哥的强', isWordLike: true}
// 2: {segment: '很', index: 3, input: '我真的很强, 强哥的强', isWordLike: true}
// 3: {segment: '强', index: 4, input: '我真的很强, 强哥的强', isWordLike: true}
// 4: {segment: ',', index: 5, input: '我真的很强, 强哥的强', isWordLike: false}
// 5: {segment: ' ', index: 6, input: '我真的很强, 强哥的强', isWordLike: false}
// 6: {segment: '强', index: 7, input: '我真的很强, 强哥的强', isWordLike: true}
// 7: {segment: '哥', index: 8, input: '我真的很强, 强哥的强', isWordLike: true}
// 8: {segment: '的', index: 9, input: '我真的很强, 强哥的强', isWordLike: true}
// 9: {segment: '强', index: 10, input: '我真的很强, 强哥的强', isWordLike: true}
时间&时区
国际化会有时区划分问题, 时区产生于太阳下地球自转导致的昼夜交替. 而全球不同国家地区当地时间与UTC时间是不一致的. 全球大部分人都可以说自己早上起床, 晚上睡觉. 上下文是通的. 但是这个早上的时间根据UTC来定义是不一样的
GMT和UTC
- GTM = Greenwich Mean Time,GTM是英国格林威治时间,但是与太阳时偏差较大,已成为历史不再作为标准
- UTC = 协调世界时(UTC: Coordinated Universal Time)- 由原子钟提供
通过NTP协议,让计算机在全球网络里保持时间一致
Offset与Timezone
- Offset即偏移量,比如中国在东八区,Offset是
+08:00:00 - 而东八区不止包括中国时间,而是一组东西经符合一个区域的集合,比如
东八区 = {CST(中国标准时),SGT(新加坡时间),AWST(澳洲西部标准时)... }
如何获取当前用户的时区信息
// 所在地区的时区标识符, 如 America/New_York
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log("用户时区偏移:" + timeZone); // 用户时区偏移:Asia/Shanghai
// 获取本地时间与UTC时间偏移值,最小单位是分钟. 如"-480", 表示-8小时. 其中正负表示UTC前后, 如美国东部时间是UTC-5, 中国北京时间是UTC+8
const date = new Date();
const timeZoneOffset = date.getTimezoneOffset();
console.log("时区偏移:" + timeZoneOffset); // 时区偏移:-480
Intl是新的浏览器API,与Math类似是全局静态对象,专门用于处理国际化和本地化业务。其下的DateTimeFormat可以处理时间相关国际化问题。
DST
DST(Daylight saving time),日光节约时,夏令时/冬令时等等名称。它会在每年春天的某一天将时钟向后拨一小时,又在秋天的某一天将时钟向前拨动一个小时。非国际化业务很少遇到这个情况,主要因为中国不实行夏令时/冬令时。
- 为什么要实行夏令时?一战时德国率先实行,将每年夏天增加1h,冬天减少1h
- 会产生什么现象?因为是行政约定,每年都可以自由选择某天某时进入夏令时,各国自由发布。IANA会存储同步各国DST,计算机每小时同步时间后会在某一秒发生突变,比如1:59到2点的时候突变会1:00
- 计算机如何表示时间?计算机都有一个unixTime,它表示当前时间距离世界标准时的1970年1月1日0点0分0秒的毫秒数,是一个绝对值,也就是UTC时间
- 但是不同地区设备会根据本地有一个格式化,将UTC时间转化为本地时间,比如中国在东八区
2021-03-14 01:59:59 GMT-08:00(太平洋标准时间,PST)
2021-03-14T01:59:59.000-08:00(ISO格式表示)
2021-03-14T09:59:59.000Z(转换为UTC时间并以ISO格式表示)
// 下一秒时间突变
2021-03-14 03:00:00 GMT-07:00(太平洋夏令时间,PDT)
2021-03-14T03:00:00.000-07:00(ISO格式表示)
2021-03-14T10:00:00.000Z(转换为UTC时间并以ISO格式表示)
// 原始时间字符串
const timeString = "2021-03-14T09:59:59.000Z";
// 将时间字符串转换为 Date 对象
const date = new Date(timeString);
const pstOutput = date.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour12: false });
console.log(pstOutput); // 3/14/2021, 01:59:59
// 获取时间戳
const timestamp = date.getTime();
// 增加1秒
const newTimestamp = timestamp + 1000;
// 创建新的 Date 对象并格式化为 PDT 时间
const newDate = new Date(newTimestamp);
const pdtOutput = newDate.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour12: false });
console.log(pdtOutput); // 3/14/2021, 03:00:00
时间处理
Dayjs插件
dayjs: day.js.org/docs/zh-CN/…
国际化支持 github.com/iamkun/dayj…
原理:通过拉取多语言文案输出不同的formated日期时间字符串
可以看这个demo: Days of the week
因我本地系统设置了每周第一天为星期日
Intl API
const date = new Date();
const formattedDate = new Intl.DateTimeFormat('en-US').format(date);
console.log(formattedDate); // 10/29/2023
const formattedDate = new Intl.DateTimeFormat('zh-CN').format(date);
console.log(formattedDate); // 2023/10/29
本地时间输出
// 创建 DateTimeFormat 对象,并指定语言和地区
const dateFormatterCN = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'long', // 使用完整的月份名称
day: 'numeric',
});
console.log(dateFormatterCN.format(new Date('2024-04-28'))); // 2024年4月28日
const dateFormatterUS = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long', // 使用完整的月份名称
day: 'numeric',
});
console.log(dateFormatterUS.format(new Date('2024-04-28'))); // April 28, 2024
Intl. RelativeTimeFormat 相对时间
Intl.RelativeTimeFormat 是 JavaScript 中的国际化 API,用于格式化相对时间,例如“1 小时前”或“2 天后”。这个 API 可以根据不同的语言和地区设置,以自然语言的方式呈现相对时间,使应用程序能够更好地适应多语言环境。
const rtf1 = new Intl.RelativeTimeFormat('zh', { style: 'short' });
console.log(rtf1.format(3, 'quarter'));
// Expected output: "3个季度后"
console.log(rtf1.format(-1, 'day'));
// Expected output: "1天前"
const rtf2 = new Intl.RelativeTimeFormat('jp', { numeric: 'auto' });
console.log(rtf2.format(2, 'day'));
// Expected output: "后天"
我们知道中文语境是以万以上可以缩写为1万,或者是 1 0000。也就是4位数字。比如 1 2345 6789或者1’2345’6789(’是万位分隔符)可以一眼看出来是一亿两千三百四十五万六千七百八十九。而如果是123, 456, 789可能很多人会愣很久重新数才知道是多少。但是现在很多银行APP都在推跟欧美一样的属于后者的千位分隔符。可以看这篇讨论觉得写的在理。
设计产品时,你是如何掉入从众的陷阱中的? – 人人都是产品经理
类似以上例子可以再看下面的举例,可以发现在德语和法语下,千分位分隔符分别是. 和
const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('zh-CN').format(number);
console.log(formattedNumber); // 1,234,567.89
const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('en-US').format(number);
console.log(formattedNumber); // 1,234,567.89
const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('de-DE').format(number);
console.log(formattedNumber); // 1.234.567,89
const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('fr-FR').format(number);
console.log(formattedNumber); // 1 234 567,89
单复数
英文复数是要加s的,比如apples
const numbers = [1, 2, 5, 10, 100];
for (const number of numbers) {
const pluralRules = new Intl.PluralRules('en-US'); // 使用英语环境
const pluralForm = pluralRules.select(number);
console.log(`In English, ${number} item${pluralForm !== 'one' ? 's' : ''}.`);
}
// In English, 1 item.
// In English, 2 items.
// In English, 5 items.
// In English, 10 items.
// In English, 100 items.
在比如顺序,第一第二第三,英文分别为first, second, third, fourth, fifth. 聪明的你一定发现规律了。除了123后面就是数字+th。简写是1st, 2nd。根据下表可以发现规律:
- 1 → st,后面除了11外,21-91都是21st, 91st这种
- 2 → nd,后面除了12外,22-92都是22nd, 92nd这种
- 3 → rd,后面除了13外,23-93都是23rd, 93rd这种
- 其他都是补th
| 数字 | 英文 | 第N |
|---|---|---|
| 1 | One | 1st |
| 2 | Two | 2nd |
| 3 | Three | 3rd |
| 4 | Four | 4th |
| 10 | Ten | 10th |
| 11 | Eleven | 11th |
| 12 | Twelve | 12th |
| 13 | Thirteen | 13th |
| 20 | Twenty | 20th |
| 21 | Twenty-one | 21st |
| 30 | Thirty | 22nd |
| 31 | Thirty-one | 21st |
| 100 | One hundred | 100th |
同样可以用代码实现如上规律
const enOrdinalRules = new Intl.PluralRules("en-US", { type: "ordinal" });
const suffixes = new Map([
["one", "st"],
["two", "nd"],
["few", "rd"],
["other", "th"],
]);
const formatOrdinals = (n) => {
const rule = enOrdinalRules.select(n);
const suffix = suffixes.get(rule);
return `${n}${suffix}`;
};
formatOrdinals(0); // '0th'
formatOrdinals(1); // '1st'
formatOrdinals(2); // '2nd'
formatOrdinals(3); // '3rd'
formatOrdinals(4); // '4th'
formatOrdinals(11); // '11th'
formatOrdinals(21); // '21st'
formatOrdinals(42); // '42nd'
formatOrdinals(103); // '103rd'
数字格式化
整数分隔和小数分隔
常见的整数分隔符号有千分位分隔,比如 1000,000,也有万位分隔比如 1000 0000。不同语言不一样。
常见的小数分隔符号是.,比如 1000.00。不同语言也不一样。
const number = 1234567.89;
// 格式化为默认数字格式
const formattedNumber = new Intl.NumberFormat().format(number);
console.log(formattedNumber); // 输出: 1,234,567.89
// 格式化为指定语言环境的数字格式
const formattedNumberDE = new Intl.NumberFormat('de-DE').format(number);
console.log(formattedNumberDE); // 输出: 1.234.567,89
// 格式化为指定语言环境的数字格式
const formattedNumberFR = new Intl.NumberFormat('fr-FR').format(number);
console.log(formattedNumberFR); // 输出: 1 234 567,89
const formattedNumberCN = new Intl.NumberFormat('zh-CN').format(number);
console.log(formattedNumberCN) // 输出: 1,234,567.89
也可以通过参数配置控制小数部分最多或者最少有多少位
const number = 1234567.89123;
const formattedNumber = new Intl.NumberFormat('en-US', {
style: 'decimal', // 可选 'decimal' 表示常规数字格式
maximumFractionDigits: 3, // 小数部分最多显示三位
}).format(number);
console.log(formattedNumber); // 输出: 1,234,567.891
百分比
正常百分比是0-100数字+%,但是法语环境百分比符号习惯是 '% '而不是'%',多了一个空格。
const percentage = 0.75;
// 使用默认语言环境
const formattedPercentageDefault = new Intl.NumberFormat('fr-FR', {
style: 'percent'
}).format(percentage);
console.log(formattedPercentageDefault); // 输出: '75 %'
// 使用指定语言环境
const formattedPercentageFR = new Intl.NumberFormat('fr-FR', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(percentage);
console.log(formattedPercentageFR); // 输出: '75,00 %'
// 使用默认语言环境
const formattedPercentageUS = new Intl.NumberFormat('en-US', {
style: 'percent'
}).format(percentage);
console.log(formattedPercentageUS); // 输出: '75%'
// 使用指定语言环境
const formattedPercentageCN = new Intl.NumberFormat('zh-CN', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(percentage);
console.log(formattedPercentageCN); // 输出: '75.00%'
缩写
console.log(new Intl.NumberFormat('en-US', { notation: "compact" , compactDisplay: "short", maximumFractionDigits: 2 }).format(987654321)) // 987.65M
console.log(new Intl.NumberFormat('zh-CN', { notation: "compact" , compactDisplay: "short", maximumFractionDigits: 2 }).format(987654321)) // 9.88亿
货币
货币符号
比如人民币是 ¥,美元是 $,欧元 €,英镑 £
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).formatToParts().filter(i => i.type === 'currency')[0].value // '$'
new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).formatToParts().filter(i => i.type === 'currency')[0].value // '¥'
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).formatToParts().filter(i => i.type === 'currency')[0].value // '€'
货币格式化
用常见的几个经济体和身边用的多的case举例说明,注意看输出内容
// 美元 $是美元符号
const numberUSD = 123456789.12;
const formattedNumberUSD = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(numberUSD);
console.log(formattedNumberUSD); // $123,456,789.12
// 人民币 ¥是人民币符号
const numberCNY = 123456789.12;
const formattedNumberCNY = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(numberCNY);
console.log(formattedNumberCNY); // ¥123,456,789.12
// 欧元 €是欧元符号
const numberEUR = 123456789.12;
const formattedNumberEUR = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(numberEUR);
console.log(formattedNumberEUR); // 123.456.789,12 €
// 日元
const numberJPY = 123456789.12;
const formattedNumberJPY = new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(numberJPY);
console.log(formattedNumberJPY); // ¥123,456,789
// 英镑 £是英镑符号
const numberGBP = 123456789.12;
const formattedNumberGBP = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(numberGBP);
console.log(formattedNumberGBP); // £123,456,789.12
// 港币
const numberHKD = 123456789.12;
const formattedNumberHKD = new Intl.NumberFormat('zh-HK', { style: 'currency', currency: 'HKD' }).format(numberHKD);
console.log(formattedNumberHKD); // HK$123,456,789.12
// 韩元
const numberKRW = 123456789.12;
const formattedNumberKRW = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(numberKRW);
console.log(formattedNumberKRW); // ₩123,456,789.12
货币的兼容性兜底可以用 Number.prototype.toLocaleString 实现,也可以用formatjs提供的polyfill。
// 美元 $是美元符号
const numberUSD = 123456789.12;
const formattedNumberUSD = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(numberUSD);
const formatttdNumberUSDByLocaleString = Number(numberUSD).toLocaleString('en-US', { style: 'currency', currency: 'USD' });
console.log(formattedNumberUSD); // $123,456,789.12
console.log(numberUSD.toLocaleString()) // 123,456,789.12
console.log(formatttdNumberUSDByLocaleString) // $123,456,789.12
货币单位显示
比如美国是美元,中国有人民币。可以直接格式化出来。
currencyNames = new Intl.DisplayNames(["zh-Hans"], { type: "currency" });
console.log(currencyNames.of("USD")); // "美元"
console.log(currencyNames.of("EUR")); // "欧元"
console.log(currencyNames.of("TWD")); // "新台币"
console.log(currencyNames.of("CNY")); // "人民币"
currencyNames = new Intl.DisplayNames(["zh-Hant"], { type: "currency" });
console.log(currencyNames.of("USD")); // "美元"
console.log(currencyNames.of("EUR")); // "歐元"
console.log(currencyNames.of("TWD")); // "新台幣"
console.log(currencyNames.of("CNY")); // "人民幣"
排序&列表
Intl.Collator
常见的电话本,地址簿排序。不同语言因为字母转换后排序不一致。
Intl.Collator 是 JavaScript 的国际化 API 之一,用于字符串比较和排序,以便在多语言环境中执行正确的排序操作。它允许你创建一个 Collator 对象,用于根据特定语言和区域设置执行字符串比较和排序,考虑到不同语言的差异。
console.log(['Z', 'a', 'z', 'ä'].sort(new Intl.Collator('de').compare));
// Expected output: Array ["a", "ä", "z", "Z"]
console.log(['Z', 'a', 'z', 'ä'].sort(new Intl.Collator('sv').compare));
// Expected output: Array ["a", "z", "Z", "ä"]
console.log(['Z', 'a', 'z', 'ä'].sort(new Intl.Collator('de', { caseFirst: 'upper' }).compare));
// Expected output: Array ["a", "ä", "Z", "z"]
//创建一个Intl.Collator对象
const collator = new Intl.Collator('en-US', { sensitivity: 'base', usage: 'sort' });
// 可以看出以下输出是按照拼音排序, guang jin mei ming tian yang
console.log(['今','天','阳','光', '明', '媚'].sort(new Intl.Collator('zh').compare)); // ['光', '今', '媚', '明', '天', '阳']
可以发现 options可以传递参数usage和sensitivity, 有如下取值
- 排序方式 (usage):
usage选项指定排序的目的'sort':用于排序。'search':用于搜索操作,通常不区分大小写。
- 敏感性 (sensitivity):
sensitivity选项指定字符串比较的敏感性级别'base':基本敏感性,不区分重音符号。'accent':考虑重音符号,但不区分大小写。'case':区分大小写,同时考虑重音符号。'case':区分大小写,但不考虑重音符号。
- 大小写 (caseFirst)
"upper":表示大写字母(uppercase)在排序中优先考虑。这意味着排序会先考虑所有大写字母,然后再考虑小写字母。在caseFirst: "upper"情况下,大写字母会排在小写字母之前。"lower":表示小写字母(lowercase)在排序中优先考虑。这意味着排序会先考虑所有小写字母,然后再考虑大写字母。在caseFirst: "lower"情况下,小写字母会排在大写字母之前。"false":表示不指定大写字母和小写字母的排序顺序,它们会一起排序,不区分大小写。
- ignorePunctuation: boolean, 表示是否忽略标点符号
有如下的应用方式
字符串比较
const collator = new Intl.Collator('en-US', { sensitivity: 'base', usage: 'sort' }); //创建一个Intl.Collator对象
const result = collator.compare('apple', 'Banana');
console.log(result); // 根据配置输出 -1(apple 在 Banana 前面)
数组排序
我们知道英文字母默认按照ASCII排序,而如果需要AaBb这样排序只能自己写排序回调
// 创建一个自定义Collator对象
const customCollator = new Intl.Collator('en-US', {
sensitivity: 'base',
usage: 'sort',
ignorePunctuation: true,
caseFirst: 'false',
});
// 自定义比较函数, 忽略空格并不区分大小写
function customCompare(a, b) {
// 移除字符串中的空格并转为小写后再比较
const stringA = a.replace(/\\s/g, '').toLowerCase();
const stringB = b.replace(/\\s/g, '').toLowerCase();
if (stringA < stringB) {
return -1;
}
if (stringA > stringB) {
return 1;
}
return 0;
}
const data = ['Apple', 'banana', 'cherry', 'apple pie', 'Banana Split', 'cherry tart'];
const data2 = data.slice()
// 老方式: 使用sort回调排序
console.log(data.sort(customCompare)); // 输出排序结果:['Apple', 'apple pie', 'banana', 'Banana Split', 'cherry', 'cherry tart']
// 新方式: 使用自定义Collator对象进行排序
console.log(data2.sort(customCollator.compare)); // 输出排序结果:['Apple', 'apple pie', 'banana', 'Banana Split', 'cherry', 'cherry tart']
可以发现两种方式结果一样,但是明显Intl. Collator更加优雅,是配置化的。
Intl.ListFormat
Intl.ListFormat 是 JavaScript 的国际化 API 之一,它用于格式化列表,以便在多语言环境中创建自然语言的列表表示。Intl.ListFormat 允许你指定列表项的连接方式(如逗号、"和" 等),以及列表项的样式和语言设置。
const listFormatter = new Intl.ListFormat('en-US', { style: 'long', type: 'disjunction' });
const items = ['apples', 'bananas', 'cherries'];
const formattedList = listFormatter.format(items);
console.log(formattedList); // 根据配置输出例如:"apples, bananas, or cherries"
const listFormatter = new Intl.ListFormat('en-US', { style: 'short', type: 'conjunction' });
const items = ['apples', 'bananas', 'cherries'];
const formattedList = listFormatter.format(items);
console.log(formattedList); // 根据配置输出例如:"apples, bananas, & cherries"
const listFormatter = new Intl.ListFormat('en-US', { style: 'narrow', type: 'conjunction' });
const items = ['apples', 'bananas', 'cherries'];
const formattedList = listFormatter.format(items);
console.log(formattedList); // 根据配置输出例如:"apples, bananas, cherries"
可以发现 options可以传递参数style和type, 有如下取值
- 样式 (style):
style选项指定列表的样式,有三个可能的值'long':使用完整的语言表达,例如 "A, B, and C"。'short':使用缩略形式,例如 "A, B, & C"。'narrow':使用极简的形式,例如 "A B C"。
- 连接方式 (type):
type选项指定连接列表项的方式,有两个可能的值'conjunction':使用 "和"(默认值),例如 "A、B和C"。'disjunction':使用 "或",例如 "A、B或C"。
日历
日历是一种常见的东西,在中国我们经常接触到公历和农历。公历全称格里高利历,英文gregory。
现在国家节日很多都是跟随农历的,比如春节、中秋节等。以前家家人手一本农历,上面会写着今天适宜做什么,比如出行、动土等。东南亚和韩国也有对应的日历本,样子还不太一样。
而与此相同,每个地方都有自己的历法:
- 伊斯兰历(Hijri Calendar):也称为伊斯兰农历,是伊斯兰教的官方日历,基于月亮的循环。伊斯兰历的年份比公历年份短,每年有12个月,因此季节日期会变化。
- 希伯来历(Hebrew Calendar):希伯来历是犹太教的官方日历,基于太阳和月亮的周期。它有13个月,其中一些月份可以有不同的天数,以保持与农历季节的一致性。
- 农历(Lunar Calendar):农历基于月亮的循环,不同地区和文化有不同的农历系统,如中国农历、韩国农历、越南农历等。
- 波斯历(Persian Calendar):波斯历,也称波斯太阳历,是伊朗和一些邻近国家使用的太阳历,与公历有一些差异。
- 印度历法(Indian Calendar):印度有多种历法,包括维基历(Vikram Samvat)、国民历法(Saka Samvat)、泰米尔历法(Tamil Calendar)等。
- 巴哈伊历(Bahá'í Calendar):巴哈伊信仰使用的独特日历,包括19个月,每个月19天。
- 民族历法:一些文化和民族拥有自己的独特历法,用于纪念特定历史事件和节日。
const date = new Date(); // 当前日期, Mon Oct 30 2023 20:00:50 GMT+0800 (中国标准时间)
const formattedDate = new Intl.DateTimeFormat('ar-SA-u-ca-islamic', { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
console.log(formattedDate); // ١٦ ربيع الآخر ١٤٤٥ هـ
const date = new Date(1994, 1, 26);
const formattedDate = new Intl.DateTimeFormat('zh-CN-u-ca-chinese', { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
console.log(formattedDate); // 1994甲戌年正月17
// 不同语言下不同日历的名称
const calendarNames = new Intl.DisplayNames('en-US', { type: 'calendar' });
console.log(calendarNames.of('gregory')); // 输出:Gregorian
console.log(calendarNames.of('islamic')); // 输出:Islamic
const calendarNames = new Intl.DisplayNames('zh-CN', { type: 'calendar' });
console.log(calendarNames.of('gregory')); // 输出:公历
console.log(calendarNames.of('islamic')); // 输出:伊斯兰历
除了上述gregory格里高利历, 取值还有下面这些
- "buddhist":佛教历(Buddhist Calendar)
- "chinese":中国农历(Chinese Lunar Calendar)
- "coptic":科普特历(Coptic Calendar)
- "ethiopic":埃塞俄比亚历(Ethiopic Calendar)
- "gregory":格里高利历(Gregorian Calendar,即公历)
- "hebrew":希伯来历(Hebrew Calendar)
- "indian":印度历法(Indian Calendar),包括维基历(Vikram Samvat)等
- "islamic":伊斯兰历(Islamic Calendar)
- "japanese":和历(Japanese Calendar)
- "persian":波斯历(Persian Calendar)
- "islamic-civil":伊斯兰历的公民版本(Islamic Civil Calendar),通常用于文书、合同等民事事务
星期
不得不说日本的和历, 真的是很神奇. 不是中文的周一到周日, 也不是Sunday-Saturday. 首先日本还有皇帝, 有皇帝就有年号. 常见下面的一些年份
- 明治时代 (Meiji Era):
- 年号:明治(Meiji)
- 年份范围:1868年 - 1912年
- 注释:明治时代标志着日本的近代化和工业化的开始。
- 大正时代 (Taisho Era):
- 年号:大正(Taisho)
- 年份范围:1912年 - 1926年
- 注释:大正时代是日本的一个相对短暂时期,也标志着日本的一些政治和社会变革。
- 昭和时代 (Showa Era):
- 年号:昭和(Showa)
- 年份范围:1926年 - 1989年
- 注释:昭和时代见证了日本的战争和战后重建,以及日本成为现代工业强国。
- 平成时代 (Heisei Era):
- 年号:平成(Heisei)
- 年份范围:1989年 - 2019年
- 注释:平成时代包括了日本的经济繁荣和一些社会变革。
- 令和时代 (Reiwa Era): 令和系奥特曼
- 年号:令和(Reiwa)
- 年份范围:2019年 - 至今
- 注释:令和时代是日本当前的年号,始于2019年5月1日,标志着新的时代的开始。
可以看到日本的日历起始时周日(日),但是周一到周六分别对应月火水木金土。与众不同。
// 获取今天过去7天的日期
// 日期对象
const today = new Date();
// 创建一个选项对象,指定输出的语言和风格
const optionsCN = { weekday: 'long' };
const optionsJP = { weekday: 'long' };
// 获取过去一周的日期
console.log('\n过去一周的日期:');
const CNArr = []
const JPArr = []
for (let i = 0; i < 7; i++) {
const pastDate = new Date(today);
pastDate.setDate(today.getDate() - i);
CNArr.unshift(new Intl.DateTimeFormat('zh-CN', optionsCN).format(pastDate))
JPArr.unshift(new Intl.DateTimeFormat('ja-JP', optionsJP).format(pastDate))
}
console.log('CNArr', CNArr.join(' ')) // CNArr 星期二 星期三 星期四 星期五 星期六 星期日 星期一
console.log('JPArr', JPArr.join(' ')) // JPArr 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日 月曜日
日历单位
const dateTimeFields = new Intl.DisplayNames('en-US', { type: 'dateTimeField' });
console.log(dateTimeFields.of('era')); // 输出:Era, 纪元的意思
console.log(dateTimeFields.of('year')); // 输出:Year
console.log(dateTimeFields.of('month')); // 输出:Month
console.log(dateTimeFields.of('day')); // 输出:Day
console.log(dateTimeFields.of('weekday')); // 输出:Day of the week
console.log(dateTimeFields.of('hour')); // 输出:Hour
console.log(dateTimeFields.of('minute')); // 输出:Minute
console.log(dateTimeFields.of('second')); // 输出:Second
console.log(dateTimeFields.of('quarter')); // 输出:Quarter
const dateTimeFields = new Intl.DisplayNames('ja-JP', { type: 'dateTimeField' });
console.log(dateTimeFields.of('era')); // 输出:時代
console.log(dateTimeFields.of('year')); // 输出:年
console.log(dateTimeFields.of('month')); // 输出:月
console.log(dateTimeFields.of('day')); // 输出:日
console.log(dateTimeFields.of('weekday')); // 输出:曜日
console.log(dateTimeFields.of('hour')); // 输出:時
console.log(dateTimeFields.of('minute')); // 输出:分
console.log(dateTimeFields.of('second')); // 输出:秒
console.log(dateTimeFields.of('quarter')); // 输出:四半期
每周第一天
en-US一般每周第一天是周日,而zh-CN一般每周第一天是周一。可以通过如下信息判断:
- Intl. Locale函数返回属性里firstDay是一个数字,其中 0或7 表示星期日,1 表示星期一,依此类推。
- 不同的地区和文化可能会将每周的第一天设置为不同的日期,因此这个属性可以帮助你确定每周的起始日期,例如,星期天或星期一。
(new Intl.Locale('zh-CN')).weekInfo // {"firstDay":1,"weekend":[6,7],"minimalDays":1}
(new Intl.Locale('en-US')).weekInfo // {"firstDay":7,"weekend":[6,7],"minimalDays":1}
const locale = 'zh-CN'
console.log(new Intl.DateTimeFormat(locale, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(locale).weekInfo.firstDay))) // '星期一'
const locale = 'en-US'
console.log(new Intl.DateTimeFormat(locale, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(locale).weekInfo.firstDay))) // 'Sunday'
能生效的原因是 new Date(0,0,7) 等价于 new Date(1900,0,7) 对应1900年1月7日(UTC+0),此时对应US的Sunday。而 new Date(0,0,1) 对应1900年1月1日(UTC+0),对应CN的星期一。
如何写一个国际化日历组件
- zh-CN下是星期一开头
- en-US是Sunday开头
- 日语环境符合 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日 月曜日等描述
实现细节
- 如何获取一周的第一天
const localeUS = 'en-US'
new Intl.DateTimeFormat(localeUS, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(localeUS).weekInfo.firstDay)) // 'Sunday'
const localeCN = 'zh-CN'
new Intl.DateTimeFormat(localeCN, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(localeCN).weekInfo.firstDay)) // '星期一'
- 输出一周7天
const locale = "en-US";
const firstDay = new Intl.Locale(locale).weekInfo.firstDay;
const formatInstace = new Intl.DateTimeFormat(locale, { weekday: "long" });
for (let i = 0; i < 7; i++) {
console.log(formatInstace.format(new Date(0, 0, firstDay + i)));
}
// Sunday
// Monday
// Tuesday
// Wednesday
// Thursday
// Friday
// Saturday
const localeCN = "zh-CN";
const firstDayCN = new Intl.Locale(localeCN).weekInfo.firstDay;
const formatInstaceCN = new Intl.DateTimeFormat(localeCN, { weekday: "long" });
for (let i = 0; i < 7; i++) {
console.log(formatInstaceCN.format(new Date(0, 0, firstDayCN + i)));
}
// 星期一
// 星期二
// 星期三
// 星期四
// 星期五
// 星期六
// 星期日
Intl支持
支持查询
可以通过API, Intl.supportedValuesOf 获取到所有支持的
const calendars = Intl.supportedValuesOf("calendar");
console.log(calendars); // 输出所有支持的日历系统
// (18) ['buddhist', 'chinese', 'coptic', 'dangi', 'ethioaa', 'ethiopic', 'gregory', 'hebrew', 'indian', 'islamic', 'islamic-civil', 'islamic-rgsa', 'islamic-tbla', 'islamic-umalqura', 'iso8601', 'japanese', 'persian', 'roc']
const currencies = Intl.supportedValuesOf("currency");
console.log(currencies); // 输出所有支持的货币代码
// (159) ['AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', 'BSD', 'BTN', 'BWP', 'BYN', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY', 'COP', 'CRC', 'CUC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD', 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', 'IDR', 'ILS', 'INR', 'IQD', 'IRR', 'ISK', 'JMD', 'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD', 'MMK', 'MNT', 'MOP', 'MRU', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', …]
const timeZones = Intl.supportedValuesOf("timeZone");
console.log(timeZones); // 输出所有支持的时区
// (428) ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmera', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre', 'Africa/Brazzaville', 'Africa/Bujumbura', 'Africa/Cairo', 'Africa/Casablanca', 'Africa/Ceuta', 'Africa/Conakry', 'Africa/Dakar', 'Africa/Dar_es_Salaam', 'Africa/Djibouti', 'Africa/Douala', 'Africa/El_Aaiun', 'Africa/Freetown', 'Africa/Gaborone', 'Africa/Harare', 'Africa/Johannesburg', 'Africa/Juba', 'Africa/Kampala', 'Africa/Khartoum', 'Africa/Kigali', 'Africa/Kinshasa', 'Africa/Lagos', 'Africa/Libreville', 'Africa/Lome', 'Africa/Luanda', 'Africa/Lubumbashi', 'Africa/Lusaka', 'Africa/Malabo', 'Africa/Maputo', 'Africa/Maseru', 'Africa/Mbabane', 'Africa/Mogadishu', 'Africa/Monrovia', 'Africa/Nairobi', 'Africa/Ndjamena', 'Africa/Niamey', 'Africa/Nouakchott', 'Africa/Ouagadougou', 'Africa/Porto-Novo', 'Africa/Sao_Tome', 'Africa/Tripoli', 'Africa/Tunis', 'Africa/Windhoek', 'America/Adak', 'America/Anchorage', 'America/Anguilla', 'America/Antigua', 'America/Araguaina', 'America/Argentina/La_Rioja', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Aruba', 'America/Asuncion', 'America/Bahia', 'America/Bahia_Banderas', 'America/Barbados', 'America/Belem', 'America/Belize', 'America/Blanc-Sablon', 'America/Boa_Vista', 'America/Bogota', 'America/Boise', 'America/Buenos_Aires', 'America/Cambridge_Bay', 'America/Campo_Grande', 'America/Cancun', 'America/Caracas', 'America/Catamarca', 'America/Cayenne', 'America/Cayman', 'America/Chicago', 'America/Chihuahua', 'America/Ciudad_Juarez', 'America/Coral_Harbour', 'America/Cordoba', 'America/Costa_Rica', 'America/Creston', 'America/Cuiaba', 'America/Curacao', 'America/Danmarkshavn', 'America/Dawson', 'America/Dawson_Creek', 'America/Denver', 'America/Detroit', 'America/Dominica', 'America/Edmonton', 'America/Eirunepe', …]
低版本兼容
Intl是浏览器对i18n提供的底层API,用于处理国际化相关内容。
附带BrowserStack云真机测试工具(Can I Use推荐):live.browserstack.com/dashboard
如果没处理好兼容性问题直接使用API,会报JS Error。内容为 Intl.DisplayNames is not a constructor。
对应的,一些操作系统低版本的用户(长期不升级系统)会遇到JS Error导致白屏。
FormatJS
可以用FormatJS提供的polyfill做低版本兼容:formatjs.io/docs/gettin…
-
支持关系有先后依赖,需要按照依赖顺序依次倒入对应的包。
-
移动端场景如小程序,如果全量导入移动端场景会让包体积爆炸(普通压缩包Gzip后2.6M,全量导入后会到9.1M)。最后polyfill按照顺序依次导入,且移动端场景只兜底英文部分,限制包体积在3M内。
async function loadPolyfill() {
// 如果当前环境不支持 Intl 或者 Intl.DisplayNames
if (!window.Intl || !window.Intl.DisplayNames) {
window.Intl = window.Intl || {}
// 加载 polyfill
await import('@formatjs/intl-getcanonicallocales/polyfill-force')
await import('@formatjs/intl-locale/polyfill-force')
await import('@formatjs/intl-displaynames/polyfill-force')
await import('@formatjs/intl-displaynames/locale-data/en')
return false
} else {
// 当前环境支持 Intl.DisplayNames API,不需要 Polyfill
return true
}
}
Why not Babel?
众所周知,Babel有一个babel-preset-env,用于在编译代码时智能(基于core-js-compat)引入helper和polyfill。
智能的含义是可以设置最低兼容的浏览器和代码,动态引用所需的helper和polyfill。
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage', // 根据每个文件里面,用到了哪些es的新特性和targets导入polyfill,更加精简
corejs: 3, // 指定 core-js 版本
targets: "> 0.25%, not dead" // 指定目标浏览器, 选取全球使用率超过 0.25% 的浏览器版本
},
],
],
};
Babel底层使用core-js进行polyfill,但是core-js不包含Intl API部分的polyfill,所以Babel并不能为Intl API做polyfill。
Nodejs使用
- 安装
npm i @formatjs/intl
- 使用
import {createIntl, createIntlCache} from '@formatjs/intl'
// This is optional but highly recommended
// since it prevents memory leak
const cache = createIntlCache()
const intlFr = createIntl(
{
locale: 'fr-FR',
messages: {},
},
cache
)
const intlEn = createIntl(
{
locale: 'en-US',
message: {},
cache
}
)
// Call imperatively
console.log(intlFr.formatNumber(2000000000000)) // 2 000 000 000 000
console.log(intlEn.formatNumber(2000000000000)) // 2,000,000,000,000