Front-End-Checklist 前端国际化格式化指南:用 Intl API 替代手写拼接,优雅处理货币、数字与日期

发布时间:2026/9/19 5:51:22
Front-End-Checklist 前端国际化格式化指南:用 Intl API 替代手写拼接,优雅处理货币、数字与日期 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规则skills/currency-formatting/references/rule.md展开。这是一份写给人和 AI Agent 的工程规范在 Web 应用中货币、数字与日期应一律使用浏览器与 Node.js 内置的Intl.NumberFormat、Intl.DateTimeFormat等 API 完成本地化格式化杜绝手写分隔符、硬编码符号和裸调toLocaleString()。读完本文你将掌握货币/数字/日期的正确格式化姿势、实例缓存与 locale 回退链的落地方法并能对照仓库源码识别出项目里现存的格式化反模式。为什么必须使用 Intl API本地化的真实差异不同语言环境对数字、货币、日期的表达差异远超想象。同一个值1234.56美国英语en-US写作$1,234.56美元符号在前、千位用逗号、小数点用句点德国德语de-DE写作1.234,50 €符号在后、千位用句点、小数点用逗号日本日语ja-JP写作¥1,235日元无辅币单位直接舍入到整数位瑞士法语fr-CH写作CHF 1234.50千位分隔符是撇号。这正是该规则所强调的硬编码的格式化必然在国际用户面前出错。靠手写正则插入千位分隔符、写死$/€/£符号每新增一个 locale 就要维护一套逻辑成本高且极易遗漏。而Intl命名空间是每个现代浏览器和 Node.js 内置的、带完整 CLDR 数据的本地化格式化体系不需要引入任何第三方库。在 Front-End-Checklist 中这条规则被收录为 SKILL.md 元数据所声明的i18n类别规则priority: mediumdifficulty: beginner预计耗时 20 分钟并被设计为可直接指导代码审查流程的规范审查工具函数与组件属性时凡涉及数字、价格、百分比或日期渲染都应检查是否使用了 Intl 原生 API。货币格式化Intl.NumberFormat 的核心用法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 1234.50值得注意的两个细节货币代码必须是 ISO 4217 标准代码USD、EUR、JPY而不是符号。引擎内部依据代码查询该货币的默认小数位数日元、韩元没有辅币单位maximumFractionDigits会自动归零因此1234.5日元显示为¥1,235四舍五入locale 使用 BCP 47 语言标签如en-US、de-DE、fr-CH它同时决定数字格式约定分组符、小数点与货币符号的摆放位置。在 Front-End-Checklist 的 i18n 包packages/i18n/src/utils.ts中日期格式化就是同样的模式formatDate仅一行即完成new Intl.DateTimeFormat(locale).format(date)locale 由调用方显式传入绝不依赖运行时的默认值。复用格式化器实例避免每次渲染重复构造Intl.NumberFormat构造需要解析 locale 与 CLDR 数据每次渲染都 new 一个实例是明显的浪费。正确做法是按「locale currency」组合缓存实例const formatterCache new Mapstring, 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; }缓存键采用${locale}-${currency}既保证每种组合只构造一次又避免跨 locale 串用实例。这在列表视图中尤为重要——几十上百个价格条目共享同一缓存格式化开销从 O(n) 构造降为 O(n) 纯format()调用。该规则在 rule.md 的验证清单中专门列出「确认格式化器实例按 locale 缓存避免大量格式化值的列表视图出现性能回退」。通用数字格式化百分比、紧凑记法与单位货币只是Intl.NumberFormat的一种风格。任何数字渲染——百分比、大数缩写、计量单位——都应走同一套 API// 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三个关键选项style: percent自动完成小数到百分比的换算与符号放置且 locale 相关——德语中百分号与数字之间带空格74 %notation: compact大数缩写完全本地化。1_500_000在英文环境是1.5M在日文环境则是150万style: unitunitunitDisplay计量单位格式化unitDisplay: long输出全称。对比仓库现状能直接发现反模式案例apps/web/lib/github.ts中的count 1000 ?${(count / 1000).toFixed(1)}k 和apps/web/components/mentions/embeds/mention-embeds.tsx里的(num / 1000000).toFixed(1) M都是手写紧凑记法——它们无法处理非英语 locale、无法正确舍入千位分组正是规则验证清单第 1 条要搜索并替换的目标详见下文「验证清单」。日期与时间格式化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要点短日期在不同 locale 下格式完全不同美式3/11/2025、德式11.3.2025、日式2025/3/11dateStyletimeStyle组合是「长日期 短时间」的声明式写法比手拼11 March 2025 at 14:30模板字符串可靠得多Intl.RelativeTimeFormat负责「3 days ago」「tomorrow」这类相对时间numeric: auto允许引擎选择「tomorrow」而非「in 1 day」这类自然表达。Front-End-Checklist 的 i18n 包把这三类能力封装为可直接复用的工具函数packages/i18n/src/utils.tsformatDate(date, locale)基于Intl.DateTimeFormat的本地化日期formatRelativeTime(date, locale)基于Intl.RelativeTimeFormat按秒差从大到小匹配year → month → week → day → hour → minute → second七个单位Math.round后交给rtf.format()例如 2 天前返回「2 days ago」getPlural(count, locale)基于Intl.PluralRules获取Intl.LDMLPluralRulezero/one/two/few/many/other用于文案的复数选择。这些函数都要求调用方显式传SupportedLocale见 packages/i18n/src/types.ts 中 14 种语言联合类型从类型层面杜绝了「漏传 locale 导致 SSR/客户端不一致」的问题。Locale 感知排序与回退链格式化只是本地化的一半。排序与搜索同样要尊重 locale 的排序规则并在目标 locale 不可用时优雅回退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)三个关键点回退链Intl.NumberFormat.supportedLocalesOf(requestedLocales)会从[fr-CA, fr, en]里挑出运行时实际支持的第一个 locale?? en兜底避免RangeErrorIntl.Collator排序sensitivity: base忽略大小写与重音差异eclair与Éclair归为一类numeric: true让eclair 2排在eclair 10之前自然数字排序而非字典序.sort()不能直接用于本地化字符串——默认排序基于 UTF-16 码元对带重音字符和数字混合文本都会给出反直觉结果。仓库的 i18n 初始化packages/i18n/src/index.ts展示了同一思路在生产中的应用initI18n()从用户偏好存储读取已保存的语言loadPreferences()作为初始lng若为空则使用DEFAULT_CONFIG.defaultLocalefallbackLng同样指向默认语言——这正是规则所说的「回退 locale 链」在真实配置里的形态。SSR/水合不一致必须显式传 locale规则特别强调一个隐蔽的坑不带 locale 参数调用Intl.NumberFormat()会使用运行时的默认 locale——服务端渲染SSR时是服务器 locale客户端则是浏览器 locale两端输出不一致在 Next.js 等框架中直接引发 hydration mismatch。// ❌ 危险写法依赖运行时默认 locale new Intl.NumberFormat().format(1234.5);解决方式是始终显式传入 locale来源可以是用户偏好设置如 i18n 包中loadPreferences()读取的locale字段见 packages/i18n/src/index.tsURL 路径段如/de-DE/product/123中的语言标识服务端根据 Accept-Language 头解析出的 locale。仓库中apps/web/components/guides/guide-card.tsx与apps/web/components/guides/guide-link-builders.ts的日期格式化实现均显式传入enlocale即为该规范的实例。反模式清单三类必须消灭的写法rule.md 明确列出了三类反面写法// ❌ Hardcoded symbol and separator — breaks for non-US locales const price $${(amount).toFixed(2).replace(/\B(?(\d{3})(?!\d))/g, ,)}; // ❌ 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);逐一拆解手写符号与正则分隔符toFixed(2) 千位分隔正则只在英语地区正确且不处理货币小数位规则如 JPY 应为 0 位无参toLocaleString()它与无参Intl.NumberFormat()同病——服务端与客户端默认 locale 可能不同产生水合不一致正确用法是amount.toLocaleString(userLocale)或直接构造Intl.NumberFormat(userLocale)正确姿势Intl.NumberFormat(userLocale, { style: currency, currency: userCurrency })locale 与货币都显式给出。仓库代码中的现存案例可以作为反面教材对照apps/web/lib/github.ts的(count / 1000).toFixed(1) k、apps/web/components/mentions/embeds/mention-embeds-compact.tsx与mention-embeds.tsx中的(num / 1000000).toFixed(1) M以及apps/web/app/(site)/u/[username]/public-profile-client.tsx与apps/web/app/(site)/(account)/profile/profile-github-metadata-section.tsx中对followers.toLocaleString()的无参调用——这些位置若面向多语言用户都应迁移为带显式 locale 的Intl.NumberFormatcompact 风格可直接用notation: compact。标准参照与验证清单标准参照规则原文要求以 MDN 的Intl.NumberFormat与Intl.DateTimeFormat文档作为渲染行为的最终标准来校验实现而不是只对照源字符串或配置文件在判定规则满足之前实现必须通过上述文档逐项核对。五步验证清单rule.md 原文搜索代码库中的toFixed、正则逗号插入以及硬编码的$、€、£符号替换为Intl.NumberFormat在 Storybook 中用localede-DE与localeja-JP渲染价格组件确认格式化结果随 locale 正确变化检查所有Intl.NumberFormat()与Intl.DateTimeFormat()调用是否都显式传入了 locale防止 SSR/客户端水合不一致确认格式化器实例按 locale 缓存避免大量格式化值的列表视图出现性能回退搜索对本地化字符串的.sort()调用确认 locale 感知排序使用带明确回退 locale 链的Intl.Collator。这条验证路径同时可以作为Agent 代码审查清单审查工具函数、组件 props 或模板字符串时凡是格式化数字、价格、百分比或日期的位置都要确认 Intl API 的正确使用——这正是 SKILL.md 中Check、Fix、Code Review三个动作的完整闭环。小结货币、数字与日期的本地化格式化是国际化应用最容易出错也最不该手写的部分。Intl.NumberFormat、Intl.DateTimeFormat、Intl.RelativeTimeFormat、Intl.Collator与Intl.PluralRules构成了浏览器与 Node.js 内置的完整本地化工具箱显式传入 locale、按组合缓存实例、用supportedLocalesOf构建回退链、用Intl.Collator做感知排序配合规则文档中的反模式清单与五步验证清单即可在代码审查层面系统性地消灭手写格式化。在 Front-End-Checklist 仓库中packages/i18n/src/utils.ts 已经给出日期、相对时间与复数规则的封装范例apps/web中现存的手写缩写与无参toLocaleString()调用则是下一步改造的落点——改造完成后整个仓库的数字与日期渲染将在所有支持的语言环境中保持一致且正确的表现。【免费下载链接】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),仅供参考