背景
Intl
对象是 ECMAScript 国际化 API 的一个命名空间,它提供了精确的字符串对比、数字格式化,和日期时间格式化,详情见Intl规范
3 年前,ECMA 标准通过了 Intl 提案,如今各大浏览器都已经支持了提案中的绝大多数 API,但是我发现在工作中仍然有很多人并不了解 Intl 的使用,在自己编写函数实现早已被原生实现的功能(写的还有bug),这篇文章从实用性的角度简单介绍 Intl 中几个非常实用的 API。
序列展示格式化
在工作中我们经常遇到需要展示多条消息给用户的场景,比如在表单的提交校验中,如果某些行校验不通过,前端需要弹窗提示 第1、2 和 3 行金额校验失败,请重新填写,这个1、2 和 3是动态的,使用时会是这个样子
// 定义模板
const messages = {
zh: {
error: '第{0}行金额校验失败,请重新填写'
},
en: {
error: 'Amount verification failed at line {0}, please re-fill'
},
}
// 定义一个转换函数,将数组用,或者、拼接
const formatList = (list) => { ... }
// 使用
$t('error', formatList(errorList))
复制代码
可以看到我们需要实现一个formatList函数,这个函数需要考虑到多语言、数量是否大于3、末尾连接符转换等问题,并不是一个很简单的函数,而事实上 Intl 早就已经内置实现了
// 我们全局定义一个格式化函数
const formatList = new Intl.ListFormat(locale, { style: 'long', type: 'conjunction' });
// 使用
$t('error', formatList(errorList))
// 效果
// 第1、2和3行金额校验失败,请重新填写
// Amount verification failed at line 1, 2, and 3, please re-fill
复制代码
金额格式化
金额格式化是一种特殊的数字格式化,比如展示金额的时候,需要做千分位符和小数位的格式化,例如将1000展示成1,000.00,多数国际化工具都是内置有这个功能的,例如vue-i18n
但是现实的场景可能会更复杂一些,比如说我们需要实现一个金额输入框,只允许输入符合标准的数字,输入时是正常的数字,失焦之后展示为格式化后的金额格式,同时要根据当前的语言环境和选用的币种进行格式化(币种涉及数字的精度,例如日元没有小数),输入时也不允许输入超出币种精度的小数位数,效果如下
这需求描述看起来就不简单,会有很多边界情况需要考虑,但是利用 Intl 就可以很方便地实现这个功能,主要原理就是用Intl.NumberFormat方法,具体参数可以参考MDN
这里贴一下实现
// 小数位分隔符;小知识:葡语系的小数位和千分位是倒过来的,这里先不考虑
export const decimalSplit = '.';
// 千分位分隔符
const currencySplit = ',';
const getDecimal = (s: string) => s.split(decimalSplit)[1] || '';
export const getDecimalLength = (s: string) =>
(getDecimal(s) || { length: 0 }).length;
const joinNumber = (integer: string, decimal: string) =>
decimal ? `${integer}${decimalSplit}${decimal}` : integer;
export const decode = (s: string) =>
s
.replace(new RegExp(currencySplit, 'g'), '')
.replace(new RegExp(`\${decimalSplit}0*$`, 'g'), '')
.replace(new RegExp(`(\${decimalSplit}[^0]*)0*$`, 'g'), '$1');
export const formatGen = R.curry((currency = 'CNY', value: string) =>
new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency,
})
.format(Number(decode(String(value || '0'))))
.replace(/[^0-9,.-]/g, ''),
);
// 使用
const format = useMemo(() => formatGen(currency), [currency]);
<Input
onBlur={(evt: React.FocusEvent<HTMLInputElement>) => {
const validValue = format(evt.target.value.trim());
onChange?.(validValue);
}}
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
const v = evt.target.value;
const [head, tail] = v.split(decimalSplit);
const validHead = head.slice(0, maxIntegerLength);
const hasDecimal = v.includes(decimalSplit);
const decimal = tail || '';
const integer = hasDecimal ? validHead.concat(decimalSplit) : validHead;
const validValue = integer
.concat(decimal.slice(0, getDecimalLength(format('0'))))
.replace(new RegExp(`[^-0-9${decimalSplit}]`, 'g'), '');
innerValue.current = validValue;
onChange?.(validValue);
}}
/>
复制代码
其他
Intl 还提供了很多其他工具,这里展示两个我觉得很实用的方法
Intl.RelativeTimeFormat
Intl.DateTimeFormat