先记住这个答案
Intl 系列的构造器,比如 Intl.DateTimeFormat 和 Intl.NumberFormat,都会先做语言协商:按传入的语言列表依次查找运行时有数据的语言,de-XX 找不到就去掉地区退到 de;整个列表都找不到,就用运行环境的默认语言。这个过程不报错,所以最终用的语言要用 resolvedOptions().locale 查看。supportedLocalesOf(list) 则是单独做检测,返回列表里运行时支持的那部分。应用自己的翻译文案不会跟着 Intl 回退,这一层要应用自己兜底。
supportedLocalesOf返回支持的子集,不支持的被过滤掉- 构造器会从
de-XX退到de,再退到默认语言 - 最终用的语言以
resolvedOptions().locale为准 - 格式非法的标签会直接抛
RangeError
检测支持和查看最终结果
先看运行时对几个语言标签的反应。
supportedLocalesOf 和 resolvedOptionsJavaScript// xx-YY 是不存在的语言,会被过滤掉;标签大小写会被规范化
console.log(Intl.DateTimeFormat.supportedLocalesOf(['xx-YY', 'zh-hans-cn', 'fr-CA']).join(' '))
// 构造器按顺序协商,第一个不支持就用第二个
console.log(new Intl.DateTimeFormat(['xx-YY', 'fr-CA']).resolvedOptions().locale)
// 地区不认识就去掉地区
console.log(new Intl.DateTimeFormat('de-XX').resolvedOptions().locale)
try {
new Intl.NumberFormat('en_US') // 下划线不是合法的 BCP 47 写法
} catch (err) {
console.log(err.name)
}查看输出与解释
zh-Hans-CN fr-CA
fr-CA
de
RangeErrorsupportedLocalesOf 返回的是请求列表的子集,并且把 zh-hans-cn 规范成了 zh-Hans-CN。语言标签的格式叫 BCP 47,由语言、可选的书写系统(如 Hans 简体)、可选的地区组成,中间用连字符连接。
后两行说明构造器的回退:列表里第一个不支持就用下一个,地区不认识就只保留语言。合法但不支持的标签会被静默处理,格式不合法的标签,比如用下划线的 en_US,才会抛 RangeError。
localeMatcher 选项控制匹配算法:lookup 是规范定义的逐级去子标签,结果可预测;默认的 best fit 允许引擎用自己认为更合适的匹配。需要跨浏览器结果一致时用 lookup。
应用自己的语言回退
浏览器支持法语格式化,不代表你的应用有法语翻译。页面上日期是法语,按钮是英文,这种半吊子体验很常见。所以应用要在入口按「自己有的翻译」选定一个语言,再把它同时交给 Intl 和翻译文案。
const APP_LOCALES = ['zh-CN', 'en-US', 'ja-JP'] // 我们有翻译文案的语言
function pickLocale(requested, fallback = 'en-US') {
for (const tag of requested) {
const want = new Intl.Locale(tag).maximize() // 补全书写系统和地区
const hit = APP_LOCALES.find((l) => {
const have = new Intl.Locale(l).maximize()
return have.language === want.language && have.script === want.script
})
if (hit) return hit
}
return fallback
}
console.log(pickLocale(['fr-FR', 'en-GB'])) // 没有法语,英式英文退到 en-US
console.log(pickLocale(['ja'])) // ja 补全后匹配 ja-JP
console.log(pickLocale(['zh-SG'])) // 新加坡用简体中文
console.log(pickLocale(['zh-TW'])) // 繁体不能退到简体查看输出与解释
en-US
ja-JP
zh-CN
en-USIntl.Locale 的 maximize() 会补全省略的部分:zh-TW 补全成 zh-Hant-TW,而 zh-SG 补全成 zh-Hans-SG。比较语言加书写系统,就能让新加坡用户用上简体中文,同时避免把台湾用户退到简体。直接按「语言前缀相同」匹配,zh-TW 就会错误地得到简体。
请求列表在浏览器里一般来自 navigator.languages,是用户在浏览器设置里排好序的语言。选好之后存进 cookie 或用户设置,服务端渲染和客户端都用这一个值。
容易答错的地方
- 以为不支持的语言会让构造器报错
- 合法但不支持的标签只会触发回退,最后用运行环境的默认语言,不报错。只有格式不合法的标签才抛
RangeError。所以「没报错」不代表用上了你想要的语言。 - 只检查 Intl,不检查翻译文案
- Intl 只负责日期、数字、排序这些格式。按钮文字、错误提示是应用自己的翻译资源,Intl 支持某个语言,不代表这些资源存在。语言选择要同时看两边。
面试官还会怎么问?
每次格式化前都要调 supportedLocalesOf 吗?
不需要。构造器自己会协商,supportedLocalesOf 主要用于能力检测和测试,比如启动时确认服务器的 Node 带了完整的语言数据。正常做法是入口选定语言,建好 formatter 缓存起来复用。
为什么服务端渲染的日期格式和浏览器不一样?
Node 如果是精简 ICU 构建,可能只带英文数据,其他语言都回退到英文,而浏览器支持完整的语言数据。官方发布的 Node 从 13 版开始默认带完整 ICU,但一些精简的 Docker 镜像或自己编译的版本不一定有。部署时用 supportedLocalesOf 检查一下就知道。
用户传来的语言标签怎么处理才安全?
先用 Intl.getCanonicalLocales() 规范化,它对非法标签会抛 RangeError,可以 try/catch 后丢弃。然后只在应用支持的语言列表里匹配,不要把用户输入原样拼进文件路径或者用来加载翻译文件。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。