2、核心概念:语言环境(Locale)、语言代码与地区代码、字符编码(Unicode/UTF-8)、资源文件

好,咱们正式开始啃这块硬骨头。做国际化,说白了就是让你的软件能「说人话」——不同国家的人打开你的应用,看到的是他们熟悉的语言、日期格式、数字写法。这背后有四个核心概念,我一个个拆开讲。

2.1 语言环境(Locale)—— 你的软件该「入乡随俗」

Locale 是什么?我习惯把它理解成一个「文化包」。它不光管语言,还管这个地区的人怎么看时间、怎么写钱、怎么排序。

举个例子。同样是 2025 年 3 月 15 日:

  • 美国人写:3/15/2025
  • 中国人写:2025/3/15
  • 德国人写:15.3.2025

你看,光一个日期就有三种写法。Locale 就是帮你的软件自动识别「该用哪种写法」的机制。

Locale 的标准格式:

语言代码_地区代码,比如 zh_CNen_USde_DE

注意:下划线前面是语言,后面是地区。zh_CN 和 zh_TW 虽然都是中文,但一个用简体字,一个用繁体字,日期格式也不一样。

我在项目中遇到过一个问题:有个同事把 Locale 设成了 zh 而不是 zh_CN。结果在台湾地区测试时,日期格式全乱了。嗯,这里要注意——只写语言代码不写地区代码,系统会取默认地区,但默认地区不一定是你想要的。

2.2 语言代码与地区代码 —— 别搞混了

这两个东西经常被混为一谈,但它们的职责完全不同。

代码 含义 示例
语言代码 表示语言本身 zh(中文)、en(英语)、ja(日语)
地区代码 表示使用该语言的地理区域 CN(中国)、US(美国)、JP(日本)

语言代码遵循 ISO 639-1 标准,两个字母。地区代码遵循 ISO 3166-1 标准,也是两个字母。组合起来就是 Locale。

我曾经踩过一个坑:把 en_GB(英国英语)和 en_US(美国英语)混用了。结果英国用户看到「color」而不是「colour」,直接投诉说我们不专业。你想想看,一个拼写差异就能让用户觉得你不够用心。

我的建议:在资源文件命名时,始终使用完整的 Locale 格式。比如 messages_zh_CN.properties,而不是 messages_zh.properties。这样能避免很多歧义。

2.3 字符编码(Unicode/UTF-8)—— 乱码的终结者

说到字符编码,我脑子里立刻浮现出当年被 GB2312 支配的恐惧。那时候做项目,中文显示出来全是「锟斤拷」——嗯,老程序员都懂的梗。

为什么会有乱码?说白了就是编码和解码用了不同的规则。你拿 UTF-8 编码的文件,用 GBK 去读,不出乱码才怪。

Unicode 是什么? 它是一个字符集,给全世界所有字符分配了一个唯一的编号。比如「中」字的编号是 U+4E2D。但它不规定怎么存储。

UTF-8 是什么? 它是 Unicode 的一种存储方式。它用变长字节来存储字符,英文用 1 个字节,中文用 3 个字节。好处是兼容 ASCII,而且没有字节序问题。

铁律:所有国际化项目,一律使用 UTF-8 编码。没有例外。

我记得有一次帮一个团队排查问题,他们的资源文件是 UTF-8 编码,但编译脚本里写死了 -encoding GBK。结果编译出来的程序,中文全变成问号。折腾了两天才找到原因。从那以后,我要求团队在项目根目录放一个 .editorconfig 文件,强制所有文件用 UTF-8。

注意:Windows 的记事本保存 UTF-8 文件时会加 BOM(字节顺序标记)。这会导致某些解析器报错。我建议用 VS Code 或 Notepad++,保存为「UTF-8 无 BOM」格式。

2.4 资源文件 —— 翻译的「仓库」

资源文件是什么?说白了就是存放翻译文本的文件。你的代码里不要写死任何用户可见的字符串,全部放到资源文件里。

常见的资源文件格式:

  • .properties(Java 生态):键值对形式,简单直接
  • .json(前端/Node.js):结构清晰,支持嵌套
  • .resx(.NET 生态):XML 格式,支持注释
  • .po(GNU gettext):开源项目常用

举个例子,Java 的 messages.properties

# 登录页面
login.title=欢迎登录
login.username=用户名
login.password=密码
login.button=登录
login.error.invalid=用户名或密码错误

对应的英文版 messages_en_US.properties

login.title=Welcome
login.username=Username
login.password=Password
login.button=Sign In
login.error.invalid=Invalid username or password

代码里怎么用?很简单:

// 不要这样写
String msg = "欢迎登录";

// 要这样写
String msg = resourceBundle.getString("login.title");

这样,当用户切换到英文环境时,程序自动加载 messages_en_US.properties,显示的就是英文。

我的经验:资源文件的键名要有层次感。用点号分隔,比如 login.titlelogin.error.invalid。这样翻译人员一看就知道这个文本用在哪里。我曾经见过有人用 a1b2 这种键名,结果翻译回来根本对不上号,那叫一个痛苦。

还有一个坑:资源文件里不要拼接字符串。比如:

// 错误做法
String msg = "你好," + userName + ",欢迎回来";

// 正确做法
// 资源文件里写:greeting.welcome=你好,{0},欢迎回来
String msg = MessageFormat.format(resourceBundle.getString("greeting.welcome"), userName);

为什么?因为不同语言的语序不一样。中文说「你好,张三,欢迎回来」,英文说「Welcome back, Zhang San」。如果你硬拼接,翻译人员根本没法调整语序。

好了,这四个核心概念讲完了。你想想看,它们其实是一条链:Locale 决定了用哪个语言和地区,语言代码和地区代码组成了 Locale 的标识,Unicode/UTF-8 保证了所有字符都能正确显示,资源文件则存放了所有需要翻译的文本。缺一个,国际化就做不完整。