locale 敏感行为与工程影响
High Contrast
Dark Mode
Light Mode
Sepia
Forest
4 min read789 words

locale 敏感行为与工程影响

核心问题:locale 到底会改变程序的哪些行为?为什么同一段代码在不同服务器上输出不同结果?


真实场景

你的电商平台按价格排序,在北京测试没问题,但部署到德国服务器后排序结果混乱。原因:服务器 locale 是 de_DE,字符串比较规则不同。locale 不只是"显示语言",它影响排序、数字格式、日期解析等一系列行为。


locale 敏感行为全景

mindmap root((locale 影响)) 数字格式 千分位分隔符 小数点符号 数字系统 日期时间 日期格式顺序 月份名称 星期起始日 12/24小时制 货币 货币符号位置 货币符号本身 排序 字母排序规则 大小写处理 重音字符 复数规则 单复数形式数量 文本方向 LTR vs RTL 度量单位 公制 vs 英制

数字格式

不同 locale 的数字书写方式差异巨大:

locale 一百万零五分之三 千分位 小数点
en-US 1,000,000.30 , .
de-DE 1.000.000,30 . ,
fr-FR 1 000 000,30 (空格) ,
hi-IN 10,00,000.30 印度分组 .
ar-SA ١٬٠٠٠٬٠٠٠٫٣٠ ٬ ٫
zh-CN 1,000,000.30 , .
const num = 1000000.3;
// 不同 locale 的格式化结果
new Intl.NumberFormat('en-US').format(num);  // "1,000,000.3"
new Intl.NumberFormat('de-DE').format(num);  // "1.000.000,3"
new Intl.NumberFormat('fr-FR').format(num);  // "1 000 000,3"
new Intl.NumberFormat('ar-SA').format(num);  // "١٬٠٠٠٬٠٠٠٫٣"
// ⚠️ 危险:直接 parseFloat() 解析德语格式数字
parseFloat('1.000.000,30');  // 1  (错误!解析成 1)
// 正确做法:不要用 parseFloat 解析用户输入,用专门的库
// 或在服务端始终以标准格式(en-US 或 API 格式)传输数字

工程建议:前后端数字传输

API 传输:始终使用 JSON 数字类型(无格式)
API → 前端:number 类型,不带任何格式字符
前端显示:用 Intl.NumberFormat 根据用户 locale 格式化
❌ 错误:API 返回 "1,000,000.30"(字符串,有格式)
✅ 正确:API 返回 1000000.3(JSON 数字)

日期与时间格式

日期顺序

locale 格式 例子
en-US MM/DD/YYYY 03/22/2026
en-GB DD/MM/YYYY 22/03/2026
zh-CN YYYY/MM/DD 2026/03/22
de-DE DD.MM.YYYY 22.03.2026
ja-JP YYYY年MM月DD日 2026年03月22日
ko-KR YYYY. MM. DD. 2026. 03. 22.

歧义陷阱03/04/2026 在美国是 3 月 4 日,在英国是 4 月 3 日!

// 始终用 ISO 8601 格式在系统间传输日期
// YYYY-MM-DD 或 YYYY-MM-DDTHH:mm:ssZ(UTC)
const isoDate = '2026-03-22T00:00:00Z';
// 显示时用 Intl.DateTimeFormat 按用户 locale 格式化
new Intl.DateTimeFormat('en-US').format(new Date(isoDate));  // "3/22/2026"
new Intl.DateTimeFormat('en-GB').format(new Date(isoDate));  // "22/03/2026"
new Intl.DateTimeFormat('zh-CN').format(new Date(isoDate));  // "2026/3/22"
new Intl.DateTimeFormat('ja-JP').format(new Date(isoDate));  // "2026/3/22"(CLDR 决定)
// 自定义格式
new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date(isoDate));  // "2026年3月22日"

星期起始日

locale 星期起始日
en-US 星期日
zh-CNen-GBde-DE 星期一
大部分伊斯兰国家 星期六

这影响日历控件的渲染。多数 UI 框架提供 firstDayOfWeek 配置项。

12 小时 vs 24 小时制

locale 时制
en-US 12 小时(AM/PM)
zh-CNde-DEfr-FR 24 小时
en-GB 通常 24 小时(正式),12 小时(口语)
// 24小时制
new Intl.DateTimeFormat('zh-CN', { hour: 'numeric', minute: 'numeric', hour12: false })
.format(new Date('2026-03-22T15:30:00'));  // "15:30"
// 12小时制
new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
.format(new Date('2026-03-22T15:30:00'));  // "3:30 PM"

货币格式

locale 货币 格式示例
en-US USD $1,234.56
zh-CN CNY ¥1,234.56
de-DE EUR 1.234,56 €
fr-FR EUR 1 234,56 €
ja-JP JPY ¥1,235(日元无小数)
ar-SA SAR ١٬٢٣٤٫٥٦ ر.س.

注意货币符号的位置(前缀/后缀)和小数位数也随 locale 不同。

// 货币格式化
const formatter = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
});
formatter.format(1234.56);  // "1.234,56 €"
// 注意:货币代码(EUR)和 locale 是独立的
// locale 决定格式,currency 决定货币单位
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' })
.format(1234.56);  // "€1,234.56"(en-US 格式,但是欧元)

字符串排序(Collation)

这是最容易被忽视但影响最大的 locale 敏感行为。

为什么排序会出问题

// JavaScript 默认排序:按 Unicode 码点
['banana', 'apple', 'cherry'].sort();
// ['apple', 'banana', 'cherry']  ✅ 巧合正确
// 但带重音字符就出问题了
['résumé', 'resume', 'résume'].sort();
// ['resume', 'résumé', 'résume']  ❌ 可能不符合法语习惯
// 德语排序:ä 应该紧跟 a
['Äpfel', 'arm', 'Boot', 'bar'].sort();
// ['Boot', 'arm', 'bar', 'Äpfel']  ❌ 按码点排

使用 Intl.Collator 正确排序

// 德语排序(ä 视为 a 的变体)
const deCollator = new Intl.Collator('de-DE');
['Äpfel', 'arm', 'Boot', 'bar'].sort(deCollator.compare);
// ['Äpfel', 'arm', 'bar', 'Boot']  ✅
// 中文排序:按拼音
const zhCollator = new Intl.Collator('zh-CN', { sensitivity: 'base' });
['张', '李', '王', '赵'].sort(zhCollator.compare);
// ['李', '王', '张', '赵'](拼音顺序)
// sensitivity 选项
// 'base': 忽略重音和大小写(最宽松)
// 'accent': 区分重音,忽略大小写
// 'case': 忽略重音,区分大小写
// 'variant': 区分重音和大小写(最严格)

数据库排序规则

-- MySQL:按中文拼音排序
SELECT name FROM products
ORDER BY CONVERT(name USING gbk) COLLATE gbk_chinese_ci;
-- PostgreSQL:设置 locale-aware 排序
CREATE TABLE products (
name TEXT COLLATE "zh-CN-x-icu"  -- 需要 pg_icu 扩展
);
-- 常用排序规则
-- utf8mb4_zh_0900_as_cs:中文按拼音
-- utf8mb4_unicode_ci:Unicode 通用不区分大小写
-- utf8mb4_bin:按字节排序(最快,但无语言规则)

正则表达式与 locale

// Unicode 属性转义(需要 /u 标志)
/\p{Script=Han}/u.test('中');    // true(汉字)
/\p{Script=Latin}/u.test('a');   // true(拉丁字母)
/\p{Script=Arabic}/u.test('ع');  // true(阿拉伯字母)
// 通用字母匹配(支持所有语言)
/\p{Letter}/u.test('ü');   // true
/\p{Letter}/u.test('中');  // true
/\p{Letter}/u.test('1');   // false
// 旧的 \w 不匹配非 ASCII 字母
/\w/.test('ü');  // false  ❌
/\p{L}/u.test('ü');  // true  ✅

文本方向(Bidi)

阿拉伯语、希伯来语、波斯语等从右向左书写(RTL)。同一段文本中混合 LTR 和 RTL 内容时,浏览器的双向文本算法(Unicode Bidi Algorithm)会处理文字方向。

<!-- 声明整个页面的文字方向 -->
<html dir="rtl" lang="ar">
<!-- 单个元素切换方向 -->
<p dir="ltr">English text in an RTL page</p>
<!-- auto:让浏览器根据内容自动判断 -->
<p dir="auto">مرحبا Hello مرحبا</p>

RTL 支持详见第 07 章。


度量单位系统

系统 国家/地区 长度 重量 温度
公制 大多数国家 km、m、cm kg、g °C
英制 美国、缅甸、利比里亚 miles、feet、inches pounds、ounces °F
混用 英国 距离用 miles,身高用 feet 体重混用 °C(官方)
// Intl.MeasureFormat(实验性,但支持度在提升)
// 目前建议用独立的单位转换 + Intl.NumberFormat
function formatTemperature(celsius, locale) {
const isUSCustomary = ['en-US', 'en-AS', 'en-GU', 'en-MH', 'en-MP', 'en-PR', 'en-VI'].includes(locale);
if (isUSCustomary) {
const fahrenheit = celsius * 9/5 + 32;
return `${Math.round(fahrenheit)}°F`;
}
return `${Math.round(celsius)}°C`;
}
formatTemperature(22, 'zh-CN');  // "22°C"
formatTemperature(22, 'en-US');  // "72°F"

locale 对服务端的影响

服务端代码同样受 locale 影响,尤其是在容器/云环境中:

# 查看系统 locale
locale
# 输出可能是:
# LANG=en_US.UTF-8
# LC_TIME=en_US.UTF-8
# LC_NUMERIC=en_US.UTF-8
# ...
# 设置 locale(不同 Linux 发行版不同)
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

Python 的 locale 陷阱

import locale
# 获取当前系统 locale
locale.getlocale()  # ('en_US', 'UTF-8')
# 设置为德语格式
locale.setlocale(locale.LC_NUMERIC, 'de_DE.UTF-8')
locale.format_string('%.2f', 1234567.89)  # '1234567,89'(德语小数点)
# ⚠️ 危险:setlocale 是全局的,会影响多线程
# 生产代码应避免调用 setlocale,改用 babel 库
from babel.numbers import format_number
format_number(1234567.89, locale='de_DE')  # '1.234.567,89'

工程实践总结

数据类型 存储/传输格式 显示格式化
数字 JSON number(无格式) Intl.NumberFormat
日期 ISO 8601 UTC(2026-03-22T00:00:00Z Intl.DateTimeFormat
货币金额 JSON number + 货币代码(分开传输) Intl.NumberFormat currency
字符串排序 数据库 collation 或 Intl.Collator Intl.Collator
文字方向 HTML dir 属性 + CSS logical properties 见第 07 章

进入第 02 章:掌握 locale 基础后,开始学习如何在 React/Next.js 和 Vue 中搭建完整的 i18n 框架。

react-intl 与 next-i18next 选型实战