国际化与本地化:资源文件、字符串格式化、日期/数字格式化
国际化(i18n)和本地化(l10n),说白了就是让你的 App 能说「人话」——不同国家的人都能看懂的话。我早年做海外项目时,第一次接触这玩意儿,心想不就是翻译几个字符串吗?结果踩了一堆坑。嗯,今天咱们就把这事彻底聊透。
1. 资源文件:你的多语言「字典」
Kotlin/Android 的资源文件放在 res/values 目录下。默认语言用 strings.xml,其他语言加后缀,比如 values-zh、values-ja。
核心原则:所有用户可见的字符串,必须放在资源文件里。硬编码是万恶之源。
举个例子,你的默认 strings.xml:
<resources>
<string name="app_name">MyApp</string>
<string name="welcome">Welcome, %s!</string>
<string name="order_count">You have %d orders.</string>
</resources>
中文版 values-zh/strings.xml:
<resources>
<string name="app_name">我的应用</string>
<string name="welcome">欢迎,%s!</string>
<string name="order_count">您有 %d 个订单。</string>
</resources>
我在项目中遇到过最坑的事:有人把「登录」直接写死在代码里,结果日语版上线后,登录按钮还是中文。你想想看,用户看到会怎么想?
2. 字符串格式化:别把变量拼死在字符串里
很多新手喜欢这样写:
val msg = "Hello, " + userName + "!"
千万别!不同语言的语序完全不同。比如英语说「You have 3 orders」,日语说「3件の注文があります」。你硬拼的话,翻译根本没法做。
正确做法是用 getString 配合占位符:
// 在 Activity 或 Fragment 中
val msg = getString(R.string.welcome, userName)
// 在 ViewModel 或其他非 Context 类中
// 需要传入 Resources 对象
val msg = resources.getString(R.string.order_count, orderCount)
我的习惯:在资源文件中,占位符尽量用 %s 和 %d,别用 %1$s 这种索引形式——除非你需要重复引用同一个参数。索引形式虽然灵活,但可读性差,翻译人员容易搞错。
还有一个细节:复数形式。英语有单复数之分(1 order vs 2 orders),但中文没有。Android 提供了 <plurals> 标签:
<plurals name="orders">
<item quantity="one">%d order</item>
<item quantity="other">%d orders</item>
</plurals>
使用时:
val text = resources.getQuantityString(R.plurals.orders, count, count)
我曾经在阿拉伯语项目里吃过亏——阿拉伯语的复数规则有 6 种形态,one、two、few、many、other、zero。嗯,当时我直接用了默认的英语规则,结果 QA 提了一堆 bug。
3. 日期/数字格式化:每个国家都有自己的「脾气」
日期格式,各国差异巨大。美国用 MM/dd/yyyy,中国用 yyyy-MM-dd,日本用 yyyy年MM月dd日。数字也是:美国写 1,234.56,德国写 1.234,56。
Kotlin 里用 java.time 包(Java 8+)或者 java.text 包来处理。我个人强烈推荐 java.time,因为它更现代、线程安全。
日期格式化示例
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.Locale
fun formatDate(date: LocalDate, locale: Locale): String {
// 使用用户当前 locale
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(locale)
return date.format(formatter)
}
// 使用
val date = LocalDate.of(2024, 3, 15)
println(formatDate(date, Locale.US)) // Mar 15, 2024
println(formatDate(date, Locale.CHINA)) // 2024年3月15日
println(formatDate(date, Locale.JAPAN)) // 2024/03/15
数字格式化示例
import java.text.NumberFormat
import java.util.Locale
fun formatNumber(number: Double, locale: Locale): String {
val formatter = NumberFormat.getNumberInstance(locale)
formatter.maximumFractionDigits = 2
return formatter.format(number)
}
fun formatCurrency(amount: Double, locale: Locale): String {
val formatter = NumberFormat.getCurrencyInstance(locale)
return formatter.format(amount)
}
// 使用
println(formatNumber(1234.56, Locale.US)) // 1,234.56
println(formatNumber(1234.56, Locale.GERMANY)) // 1.234,56
println(formatCurrency(99.99, Locale.US)) // $99.99
println(formatCurrency(99.99, Locale.JAPAN)) // ¥100
注意:不要用 SimpleDateFormat 和 String.format 做国际化格式化。前者线程不安全,后者依赖默认 locale,容易出问题。我见过一个线上 bug:某用户的手机 locale 是阿拉伯语,结果日期格式全乱了,因为阿拉伯语是从右往左写的。
4. 知识体系总览
下面这张图,帮你把国际化与本地化的核心脉络理清楚:
5. 实战中的避坑指南
说了这么多理论,来点实际的。我这些年踩过的坑,总结成几条:
- 不要用
Locale.getDefault()做格式化——用户可能改了系统语言,但你的 App 需要保持自己的语言设置。我建议在 Application 初始化时,保存用户选择的 Locale,全局使用。 - 字符串拼接是大忌——我曾经接手过一个项目,代码里到处都是
"您有" + count + "条消息"。后来要加日语支持,直接崩溃。因为日语里「您有3条消息」要说成「3件のメッセージがあります」,语序完全不同。 - RTL 布局要提前规划——阿拉伯语、希伯来语是从右往左写的。如果你的布局用了
start/end而不是left/right,那基本不用改。但如果你用了leftMargin硬编码,那就等着哭吧。 - 测试要覆盖所有语言——不要只测英语和中文。我见过一个 App,德语下日期格式显示正常,但土耳其语下月份名全乱了,因为土耳其语有特殊的字符映射。
我的小技巧:在开发阶段,可以写一个测试用例,遍历所有支持的 Locale,检查每个字符串是否都能正确格式化。这样能提前发现很多问题。
6. 总结
国际化与本地化,说白了就是「尊重用户」。尊重他们的语言、他们的日期习惯、他们的数字写法。技术上并不难,难的是细心和全局思维。
嗯,今天的内容就到这里。记住:资源文件是基础,字符串格式化是桥梁,日期/数字格式化是细节。三者缺一不可。
公众号:蓝海资料掘金营,微信deep3321