你真的以为“把中文翻成英文”就叫 i18n?那为啥一到夏令时你系统就开始装死?

张开发
2026/4/11 19:47:43 15 分钟阅读

分享文章

你真的以为“把中文翻成英文”就叫 i18n?那为啥一到夏令时你系统就开始装死?
你好欢迎来到我的博客我是【菜鸟不学编程】我是一个正在奋斗中的职场码农步入职场多年正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上我决定记录下自己的学习与成长过程也希望通过博客结识更多志同道合的朋友。️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等也会分享一些踩坑经历与面试复盘希望能为还在迷茫中的你提供一些参考。 我相信写作是一种思考的过程分享是一种进步的方式。如果你和我一样热爱技术、热爱成长欢迎关注我一起交流进步全文目录前言 I. Locale语言和区域不是一回事别把它当“翻译开关”1.1 创建 Locale别手写字符串拼来拼去1.2 Web 项目里 Locale 从哪来II. ResourceBundle 高级ListResourceBundle vs PropertyResourceBundle别只会 properties2.1 propertiesPropertyResourceBundle最常用但要注意编码编码坑properties 历史上不是 UTF-82.2 ListResourceBundle当你想把“文案”写成可编程结构III. 格式化类NumberFormat 和 DateFormat别再手动拼小数点和逗号了3.1 NumberFormat数字/货币/百分比3.2 DateFormat能用但更推荐 java.time 的 DateTimeFormatterIV. Unicode 支持UTF-8 处理和 surrogatesemoji 不是“一个 char”4.1 Surrogate代理对到底是啥4.2 UTF-8 处理输入输出别让默认编码背锅V. 时区与夏令时ZoneRules 和 DST真正让人破防的 i18n 核心‍5.1 正确姿势存 UTC展示用用户时区5.2 ZoneRules判断某天是否有 DST 变化5.3 DST 的经典坑本地时间可能“不存在”或“重复”VI. 项目多语言 Web 应用的 i18n 实现从“能展示”到“能长期维护”6.1 分层思路别把 i18n 写散了6.2 一个轻量 MessageServiceResourceBundle UTF-8 参数替换6.3 Web 请求里如何带 Locale ZoneId工程建议结尾i18n 的“及格线”不是翻译是一致性和可预期✅ 写在最后前言 国际化i18n这东西很多团队一开始都很自信“简单啊不就是搞个messages_en.properties吗”然后上线两个月用户截图来了德国人看到的数字小数点不对日本用户日期格式怪怪的纽约用户的时间比你早一天夏令时那天你们的定时任务执行了两次或者一次都没执行你才发现i18n 从来不是翻译是“让不同地区的人看到对的东西、在对的时间发生对的行为”。这篇我按你给的大纲来讲Locale、ResourceBundle的高级用法ListResourceBundle/PropertyResourceBundle、数字和日期格式化、Unicode 的坑尤其 surrogate、再到时区/DST 的工程实践最后落一个“多语言 Web 应用”的实现方案和代码示例。保证专业但不拧巴能直接落地。I.Locale语言和区域不是一回事别把它当“翻译开关”Locale表达的是语言 区域可选 变体可选。很多人只写en、zh但在真实世界里差别可大了en_USvsen_GB日期格式、拼写习惯、单位偏好都可能不同pt_BRvspt_PT葡语但词汇差异明显zh_CNvszh_TW简体/繁体是最直观的差别之一1.1 创建 Locale别手写字符串拼来拼去importjava.util.Locale;LocalezhCNLocale.SIMPLIFIED_CHINESE;// zh_CNLocalezhTWLocale.TRADITIONAL_CHINESE;// zh_TWLocaleenUSLocale.US;LocalefrFRLocale.FRANCE;LocalecustomLocale.forLanguageTag(pt-BR);// 推荐语言标签写法1.2 Web 项目里 Locale 从哪来常见来源优先级别死背按业务取舍用户个人设置profile 里存的语言偏好请求头Accept-Language浏览器/客户端会带URL 参数?lang...或路径前缀/en/...系统默认 Locale最后兜底工程建议让 Locale 决定展示格式和文案不要让 Locale 决定业务逻辑。业务逻辑用统一标准比如 UTC、ISO 格式、统一货币单位展示层再本地化。否则你会在“不同地区订单计算不一致”里哭出声。‍II.ResourceBundle高级ListResourceBundlevsPropertyResourceBundle别只会 propertiesJava 的 i18n 文案通常用ResourceBundle来加载。它支持两种主要形态PropertyResourceBundle基于.properties文件最常见ListResourceBundle基于 Java 类更灵活适合动态/复杂结构2.1 propertiesPropertyResourceBundle最常用但要注意编码文件命名messages.properties默认messages_zh_CN.propertiesmessages_en_US.properties加载示例importjava.util.*;ResourceBundlerbResourceBundle.getBundle(messages,Locale.forLanguageTag(zh-CN));Stringtitlerb.getString(app.title);编码坑properties 历史上不是 UTF-8过去很多工具链要求.properties用 ISO-8859-1非 ASCII 需要\uXXXX转义。现代 Java尤其 9对 UTF-8 支持更友好但团队环境不统一时容易踩雷。更稳的工程做法之一用自定义 Control 强制按 UTF-8 读取下面给代码这样你就不用把中文写成\u4F60\u597D那种“眼睛看了想辞职”的形式。importjava.io.*;importjava.nio.charset.StandardCharsets;importjava.util.*;publicclassUtf8ControlextendsResourceBundle.Control{OverridepublicResourceBundlenewBundle(StringbaseName,Localelocale,Stringformat,ClassLoaderloader,booleanreload)throwsIllegalAccessException,InstantiationException,IOException{StringbundleNametoBundleName(baseName,locale);StringresourceNametoResourceName(bundleName,properties);try(InputStreamisloader.getResourceAsStream(resourceName)){if(isnull)returnnull;try(ReaderreadernewInputStreamReader(is,StandardCharsets.UTF_8)){returnnewPropertyResourceBundle(reader);}}}}使用ResourceBundlerbResourceBundle.getBundle(messages,Locale.forLanguageTag(zh-CN),newUtf8Control());2.2 ListResourceBundle当你想把“文案”写成可编程结构比如你想支持更复杂的数据带占位符规则、或按业务动态生成ListResourceBundle会更舒服importjava.util.ListResourceBundle;publicclassmessages_zh_CNextendsListResourceBundle{OverrideprotectedObject[][]getContents(){returnnewObject[][]{{app.title,订单中心},{greeting,你好{0}},};}}它的好处可以写逻辑但别写太复杂不然维护很难编码天然是 UTF-8源文件IDE 重构更友好类名/包名可追踪缺点也很现实文案跟代码耦合更深不太适合给非开发同学维护所以大多数项目properties 为主ListResourceBundle 作为特殊场景补充。III. 格式化类NumberFormat和DateFormat别再手动拼小数点和逗号了i18n 里最常见的“翻车截图”不是翻译错是格式错。比如同样的数字1,234.56美国1 234,56法国常见风格你手写String.format(%.2f)就直接完蛋因为它不懂地区习惯。3.1 NumberFormat数字/货币/百分比importjava.text.NumberFormat;importjava.util.Locale;doublen1234.56;NumberFormatusNumberFormat.getNumberInstance(Locale.US);NumberFormatfrNumberFormat.getNumberInstance(Locale.FRANCE);System.out.println(us.format(n));// 1,234.56System.out.println(fr.format(n));// 1 234,56空格/逗号风格货币NumberFormatmoneyNumberFormat.getCurrencyInstance(Locale.JAPAN);System.out.println(money.format(1234.56));// 日本圆格式会按本地规则取整/符号3.2 DateFormat能用但更推荐 java.time 的 DateTimeFormatterDateFormat是老 API但你大纲里有它我给你讲清用法同时也告诉你更推荐的方向。importjava.text.DateFormat;importjava.util.*;DatenownewDate();DateFormatdfDateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM,Locale.UK);System.out.println(df.format(now));工程建议展示层日期时间优先用java.timeZonedDateTimeDateTimeFormatter更清晰也更不容易被 DST 坑DateFormat适合兼容老系统或简单展示IV. Unicode 支持UTF-8 处理和 surrogatesemoji 不是“一个 char”Unicode 这块最容易踩的坑就是你以为String.length()是“字符数”其实它是 UTF-16 code unit 数。然后你一遇到 emoji 或某些超出 BMP 的字符就开始数错、截断、乱码、甚至数据库写入失败。‍4.1 Surrogate代理对到底是啥在 Java 里String用 UTF-16 存储。对于某些字符比如 它需要两个char表示高代理 低代理。所以.length()可能是 2但它实际上是 1 个“用户感知的字符”正确按 Unicode code point 计数StringsAB;intcodePointss.codePointCount(0,s.length());System.out.println(codePoints);// 3正确截取按 code pointint[]cpss.codePoints().toArray();StringfirstTwonewString(cps,0,2);System.out.println(firstTwo);// A4.2 UTF-8 处理输入输出别让默认编码背锅工程里最稳的习惯是IO 一律显式指定 UTF-8不依赖平台默认编码它会在不同机器上“变脸”importjava.nio.charset.StandardCharsets;importjava.nio.file.*;PathpPath.of(messages.txt);Files.writeString(p,你好,StandardCharsets.UTF_8);StringreadFiles.readString(p,StandardCharsets.UTF_8);V. 时区与夏令时ZoneRules和 DST真正让人破防的 i18n 核心‍我说句可能不太好听但很真实的话如果你的系统涉及“跨地区时间”却还在用new Date() “服务器时区”那迟早要出事故。因为服务器可能在 UTC你用户在纽约你还要面对 DST夏令时那种“这一天少一小时/多一小时”的魔法。5.1 正确姿势存 UTC展示用用户时区数据库存时间建议InstantUTC 时间点或 epoch millis展示给用户转换到用户ZoneIdimportjava.time.*;InstantnowInstant.now();// UTC 时间点ZoneIduserZoneZoneId.of(America/New_York);ZonedDateTimeuserTimenow.atZone(userZone);System.out.println(userTime);5.2ZoneRules判断某天是否有 DST 变化importjava.time.*;ZoneIdzoneZoneId.of(Europe/Helsinki);ZoneRulesruleszone.getRules();InstantnowInstant.now();booleanisDstrules.isDaylightSavings(now);System.out.println(isDST isDst);ZoneOffsetoffsetrules.getOffset(now);System.out.println(offset offset);5.3 DST 的经典坑本地时间可能“不存在”或“重复”比如某天从 02:00 直接跳到 03:00那02:30这时间点在该地区不存在。反过来回拨时某个时间段会出现两次重复一小时。工程处理建议业务逻辑尽量用Instant或ZonedDateTime避免用“裸 LocalDateTime”去表示真实发生的时间点需要用户输入本地时间时要明确选择 DST 处理策略取早/取晚/报错VI. 项目多语言 Web 应用的 i18n 实现从“能展示”到“能长期维护”来点能落地的一个多语言 Web 应用 i18n我推荐这套分层6.1 分层思路别把 i18n 写散了Locale 解析层从请求里确定 Locale用户设置 Accept-Language 默认Message 层封装 ResourceBundle 读取 缺失 key 兜底格式化层数字/日期/货币/单位格式化统一出口时区层从用户配置或请求里确定 ZoneId展示时统一转换前后端一致性前端也要用同一套语言 key或由后端下发字典6.2 一个轻量 MessageServiceResourceBundle UTF-8 参数替换这里用MessageFormat做占位符替换{0}这种importjava.text.MessageFormat;importjava.util.*;publicclassMessageService{privatefinalStringbaseName;privatefinalResourceBundle.Controlcontrol;publicMessageService(StringbaseName){this.baseNamebaseName;this.controlnewUtf8Control();// 上面实现的 UTF-8 control}publicStringmsg(Localelocale,Stringkey,Object...args){try{ResourceBundlerbResourceBundle.getBundle(baseName,locale,control);Stringpatternrb.getString(key);returnMessageFormat.format(pattern,args);}catch(MissingResourceExceptione){// 缺 key 的兜底策略别直接报错给用户return??key??;}}}properties 里你可以写messages_en_US.propertiesapp.titleOrder Center greetingHello, {0}!messages_zh_CN.propertiesapp.title订单中心 greeting你好{0}调用MessageServicemsnewMessageService(messages);System.out.println(ms.msg(Locale.forLanguageTag(zh-CN),greeting,Britney));System.out.println(ms.msg(Locale.US,greeting,Britney));6.3 Web 请求里如何带 Locale ZoneId工程建议LocaleAccept-Language: zh-CN,zh;q0.9,en;q0.8ZoneId建议由用户设置保存profile或前端上报Intl.DateTimeFormat().resolvedOptions().timeZone后端拿到后文案按 Locale 取ResourceBundle时间按 ZoneId 将Instant转换展示数字/货币按 Locale 使用NumberFormat结尾i18n 的“及格线”不是翻译是一致性和可预期✅真正靠谱的 i18n 体系应该做到文案key 统一、缺失可兜底、可追踪数字/日期用 Locale 的格式化工具不手拼UnicodeUTF-8 显式处理别在 emoji 上翻车时间存 UTCInstant展示按 ZoneIdDST 要有明确策略工程化i18n 能扩展、能测试、能维护不靠“临时补丁” 写在最后如果你觉得这篇文章对你有帮助或者有任何想法、建议欢迎在评论区留言交流你的每一个点赞 、收藏 ⭐、关注 ❤️都是我持续更新的最大动力我是一个在代码世界里不断摸索的小码农愿我们都能在成长的路上越走越远越学越强感谢你的阅读我们下篇文章再见✍️ 作者某个被流“治愈”过的 Java 老兵 日期2026-01-07 本文原创转载请注明出处。

更多文章