Front-End-Checklist 货币格式化规则实战:用 Intl API 取代手工拼接,实现跨区域数字、货币与日期本地化
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
本文基于开源仓库 Front-End-Checklist 的 currency-formatting 规则(i18n分类、medium 优先级、beginner 难度、约 20 分钟)及其完整实现文档 references/rule.md,系统讲解如何用浏览器与 Node.js 内置的Intl.NumberFormat、Intl.DateTimeFormat、Intl.Collator等 API 替代手工字符串拼接,完成货币、数字、日期、排序的本地化。读完本文,你将掌握一套可直接落地的格式化工具函数、性能优化方案(实例缓存)、SSR 水合一致性避坑要点,以及可执行的代码审查与验证清单。
一、为什么必须用 Intl API:本地化格式差异远超想象
数字与货币的书写规则在不同 locale 之间差异巨大。同一个数值,在美国写作"$1,234.56",在德国写作"1.234,56 $"(小数点与千分位互换、货币符号后置),在日本写作"¥1,235"(无小数位、符号前置)。如果像下面这样把$、,、.硬编码进模板字符串:
const price = `$${(amount).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;那么面向国际用户的页面会直接显示错误格式,而且每新增一个 locale 都需要手工维护一套正则与分隔符逻辑,成本随语言数量线性增长。这正是规则文档中点名的最典型反模式。
Intl命名空间是内置在每一个现代浏览器和 Node.js 中的一套 locale 感知格式化构造函数,涵盖数字、货币、日期、百分比、复数、排序等能力,无需引入任何第三方库,即可自动处理符号位置、小数位数、千分位分组等全部细节。
二、Quick Reference:五条核心结论
SKILL.md 中给出了五条可直接当作审查标尺的快速结论:
- 绝不硬编码货币符号或千分位分隔符(如
$、逗号); - 货币金额必须使用
Intl.NumberFormat的currency样式; - 日期时间显示使用
Intl.DateTimeFormat做 locale 感知格式化; - 显式传入用户 locale,而不是依赖浏览器默认值;
- 排序与搜索使用
Intl.Collator,并配置 locale 回退链。
这五条覆盖了"格式化 + 排序 + 容错"三块完整能力,下文逐一展开。
三、货币格式化核心实操:Intl.NumberFormat 的 currency 样式
Intl.NumberFormat构造函数接受style: 'currency'选项和 ISO 4217 货币代码,格式化器会针对每个 locale 自动处理符号摆放、小数位数与千分位分组。规则文档给出了一个可直接使用的工具函数:
// formatCurrency.ts /** * Format a numeric amount as a locale-aware currency string. * @param amount - The numeric value (e.g. 1234.5) * @param currency - ISO 4217 currency code (e.g. 'USD', 'EUR', 'JPY') * @param locale - BCP 47 language tag (e.g. 'en-US', 'de-DE', 'ja-JP') */ export function formatCurrency( amount: number, currency: string, locale: string ): string { return new Intl.NumberFormat(locale, { style: 'currency', currency, // Optional: control how many fraction digits to display // JPY has no minor units, so maximumFractionDigits defaults to 0 }).format(amount); } // Output comparison for the same value across locales const amount = 1234.5; formatCurrency(amount, 'USD', 'en-US'); // "$1,234.50" formatCurrency(amount, 'EUR', 'de-DE'); // "1.234,50 €" formatCurrency(amount, 'JPY', 'ja-JP'); // "¥1,235" formatCurrency(amount, 'GBP', 'en-GB'); // "£1,234.50" formatCurrency(amount, 'CHF', 'fr-CH'); // "CHF 1'234.50"注意代码注释中的细节:JPY(日元)没有辅币单位,因此maximumFractionDigits默认为 0,自动省去小数位——这正是"交给 Intl 处理"的价值:每种货币的小数规则由运行时内置数据决定,不需要开发者自己维护一张货币→小数位映射表。
需要精确控制显示位数时,可通过minimumFractionDigits/maximumFractionDigits选项覆盖默认行为,例如强制两位小数new Intl.NumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 })。
四、性能优化:按 locale + currency 缓存格式化器实例
Intl.NumberFormat构造过程需要加载并解析该 locale 的完整格式数据,每次渲染都 new 一个实例是浪费的。规则文档给出的方案是按locale-currency组合缓存实例:
const formatterCache = new Map<string, Intl.NumberFormat>(); export function getCurrencyFormatter( currency: string, locale: string ): Intl.NumberFormat { const key = `${locale}-${currency}`; if (!formatterCache.has(key)) { formatterCache.set( key, new Intl.NumberFormat(locale, { style: 'currency', currency }) ); } return formatterCache.get(key)!; } // Usage in a React component function PriceDisplay({ amount, currency }: { amount: number; currency: string }) { const locale = useLocale(); const formatted = getCurrencyFormatter(currency, locale).format(amount); return <span>{formatted}</span>; }在长列表渲染大量价格(商品列表、订单明细)时,缓存带来的收益非常明显:格式化本身是幂等的纯函数,实例可安全复用,而format()调用可以高频执行。规则文档的验证清单第 4 条也明确要求"确认格式化器按 locale 缓存,避免列表视图中大量格式化值导致性能回退"。
五、通用数字格式化:百分比、紧凑记法与单位
Intl.NumberFormat不只是货币,任何数值展示都适用——百分比、大数紧凑记法、带单位的度量值:
// Percentage new Intl.NumberFormat('en-US', { style: 'percent' }).format(0.742); // "74%" new Intl.NumberFormat('de-DE', { style: 'percent' }).format(0.742); // "74 %" // Compact notation for large numbers new Intl.NumberFormat('en-US', { notation: 'compact' }).format(1_500_000); // "1.5M" new Intl.NumberFormat('ja-JP', { notation: 'compact' }).format(1_500_000); // "150万" // Unit formatting (metres, kilograms, etc.) new Intl.NumberFormat('en-US', { style: 'unit', unit: 'kilometer', unitDisplay: 'long', }).format(42); // "42 kilometers"三个例子分别展示了:同一样式在不同 locale 的间隔差异(74%vs74 %)、紧凑记法的本地化缩写(英文1.5M对应日文150万),以及style: 'unit'的度量单位格式化(unitDisplay: 'long'输出完整单词)。
六、日期与时间格式化:Intl.DateTimeFormat 与相对时间
Intl.DateTimeFormat负责 locale 特定的日期时间模式。规则文档覆盖了三种典型场景——短日期、长日期+时间、相对时间:
const date = new Date('2025-03-11T14:30:00Z'); // Short date new Intl.DateTimeFormat('en-US').format(date); // "3/11/2025" new Intl.DateTimeFormat('de-DE').format(date); // "11.3.2025" new Intl.DateTimeFormat('ja-JP').format(date); // "2025/3/11" // Long date with time new Intl.DateTimeFormat('en-GB', { dateStyle: 'long', timeStyle: 'short', }).format(date); // "11 March 2025 at 14:30" // Relative time ("3 days ago") const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); rtf.format(-3, 'day'); // "3 days ago" rtf.format(1, 'day'); // "tomorrow"Intl.RelativeTimeFormat的numeric: 'auto'选项允许使用"明天/昨天"这类口语化表达而非机械的 "1 day ago",让相对时间在新闻时间戳、评论列表等场景更自然。
七、排序与搜索:Intl.Collator 和 locale 回退链
格式化只是本地化的一半。排序与搜索同样需要尊重 locale 的排序规则,而默认的String.prototype.sort()按 UTF-16 码点比较,会把带重音符号的词排错位置。规则文档给出的示例:
const requestedLocales = ['fr-CA', 'fr', 'en'] const resolvedLocale = Intl.NumberFormat.supportedLocalesOf(requestedLocales)[0] ?? 'en' const collator = new Intl.Collator(resolvedLocale, { sensitivity: 'base', numeric: true, }) const products = ['eclair', 'Éclair', 'eclair 2', 'eclair 10'] products.sort(collator.compare)这里有两个关键点:
supportedLocalesOf回退链:['fr-CA', 'fr', 'en']表示优先使用fr-CA,运行时不支持时依次回退到fr、en,取第一个可用项;?? 'en'兜底保证一定有一个可用的 locale,避免排序行为在不受支持的运行时上失效;Collator选项:sensitivity: 'base'忽略大小写与重音差异(把eclair与Éclair视为同一等级),numeric: true让'eclair 2'排在'eclair 10'之前,而不是按字符逐位比较导致'10' < '2'。
八、SSR 水合不一致:必须显式传递 locale
规则文档用一个醒目的警告强调:不带 locale 参数调用Intl.NumberFormat()(或toLocaleString())会使用运行时的默认 locale——SSR 期间是服务器 locale,客户端是浏览器 locale,二者不一致就会在 Next.js 等框架中造成水合(hydration)不匹配,页面刷新前后显示不同。
// ❌ Using toLocaleString() without an explicit locale const price = amount.toLocaleString(); // different on server vs client // ✅ Explicit locale from user preferences or URL segment const price = new Intl.NumberFormat(userLocale, { style: 'currency', currency: userCurrency, }).format(amount);正确做法是让用户 locale 成为可确定的单一来源:来自用户偏好设置、路由 URL 段或服务端请求头,并在服务端与客户端渲染路径上保持一致。SKILL.md 的 Check 提示也要求"标记任何缺少回退 locale 的格式化器",回退链不仅是排序的容错,也是 SSR/CSR 一致性的一部分。
九、反模式清单:代码审查时重点排查的写法
综合规则文档的 Anti-Patterns 部分,审查时应对以下写法亮红灯:
| 反模式 | 问题 | 替代方案 |
|---|---|---|
硬编码$+toFixed(2)+ 正则插千分位 | 非美式 locale 直接显示错误 | Intl.NumberFormat(locale, { style: 'currency', currency }) |
toLocaleString()不带 locale 参数 | 服务端/客户端结果不一致 | 显式传入userLocale |
String.prototype.sort()排序本地化字符串 | 重音、数字比较错乱 | Intl.Collator的compare方法 |
每次渲染new Intl.NumberFormat() | 列表渲染性能回退 | 按locale-currency缓存实例 |
十、仓库源码佐证:Front-End-Checklist 自身的 Intl 实践
该仓库的@repo/i18n包正是这套规则的落地实现,可作为"正确写法"的参考模板:
- packages/i18n/src/utils.ts 中的
formatDate直接用new Intl.DateTimeFormat(locale).format(date);formatRelativeTime用Intl.RelativeTimeFormat按年/月/周/日/时/分/秒递推选择最合适的单位;getPlural用Intl.PluralRules计算复数形式——四个函数全部显式接收locale参数,没有任何一处依赖运行时默认 locale; - packages/i18n/src/types.ts 定义了
SupportedLocale联合类型(en、fr、es、de、ja、zh、ar等 14 种语言),isRTL明确列出ar、he、fa、ur四个从右往左书写的 locale——RTL 语言是本地化测试中极易遗漏的场景; - packages/i18n/src/index.ts 用 i18next 初始化时读取用户存储的
locale偏好,并设置fallbackLng: DEFAULT_CONFIG.defaultLocale,实现"用户偏好 → 全局默认"的回退语义,与规则要求的 fallback locale 链同构; - packages/i18n/src/tests/i18n.test.ts 中的测试验证了
formatDate对不同 locale 输出不同结果、formatRelativeTime对过去时间与当前时间均返回字符串、isRTL('ar') === true、getPlural(2, 'en') === 'other',可作为格式化函数单元测试的参考范式。
从源码结构看,仓库将"locale 的确定"(initI18n/changeLanguage+ 持久化存储)与"locale 的使用"(utils.ts中的纯函数)分离,这正是规则反复强调"显式传 locale"的工程化体现:locale 一旦来自单一可信来源,格式化层的每个调用点都保持纯粹与可预测。
十一、验收与验证清单(Verification)
规则文档给出了五步可执行的验证方案,可直接用于评审或 CI 检查:
- 全文搜索
toFixed、正则插千分位、硬编码的$/€/£符号,确认全部替换为Intl.NumberFormat; - 多 locale 渲染:用 Storybook 以
locale='de-DE'和locale='ja-JP'渲染价格组件,确认格式随 locale 正确变化; - 显式 locale 检查:确认没有任何
Intl.NumberFormat()/Intl.DateTimeFormat()调用缺失 locale 参数,防止 SSR/客户端水合不匹配; - 实例缓存检查:确认格式化器按 locale 缓存,避免长列表渲染的性能回退;
- 排序检查:搜索对本地化字符串的
.sort()调用,确认使用带回退 locale 链的Intl.Collator。
十二、相关规则联动
currency-formatting 属于 i18n 范畴,官方规则页在 packages/content/rules/en/i18n/currency-formatting.mdx 中还关联了四条同域规则,评审时可一并覆盖:
- text-expansion:格式化后的数字在不同 locale 长度差异很大(如
"1.234,50 €"明显长于"$1,234.50"),需与文本膨胀一起处理 UI 布局溢出; - pluralization:复数规则直接影响 i18n 质量,常与数字格式化同场评审;
- input-types:两者在真实审查中相互交叉,常影响同一处实现决策(如金额输入框的类型与校验);
- locale-images:同样影响 i18n 质量,通常在本地化审查中一起检查。
结语
从formatCurrency工具函数到Intl.Collator排序回退链,从实例缓存到 SSR 水合一致性,currency-formatting 规则覆盖了"数字本地化"从展示到排序、从性能到容错的完整链路。核心心法只有一句:把格式化的决定权交给内置 Intl 运行时,开发者只负责确定"用户是谁"(locale)与"值是什么"(货币/单位/日期),其余交给引擎。这也是 SKILL.md 希望人与 AI Agent 在评审中共同贯彻的标准。
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考