应用本地化
学会将App界面翻译成多语言、用资源限定符适配不同语言环境、格式化数字和日期、以及支持从右到左的布局。
学习目标
- 能沿“应用本地化”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“学会将App界面翻译成多语言、用资源限定符适配不同语言环境、格式化数字和日期、以及支持从右到左的布局。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用双视口、大字体、焦点顺序、状态语义和对比度截图独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
为什么需要应用本地化
想象你开发了一个记账 App,界面全是中文。一个巴西用户下载了它——他看不懂 "添加支出" 四个字,也不知道 "月收入" 是什么意思。他只给你打了一星差评,然后卸载。
不做本地化,你的 App 就自动放弃了所有非母语用户。Android 设备遍布全球 190+ 个国家——Google Play 上排名靠前的 App 无一例外都支持多语言。好消息是 Android 的资源框架从第一天开始就为多语言做了设计——你不需要改一行代码,只需要把字符串翻译一份放到对应目录。
用工厂流水线来类比:你的 Kotlin 代码是流水线机器本身——它只管逻辑(按钮点下去做什么)。而 strings.xml 是机器上可以随时替换的铭牌——中文铭牌装在中文产线,英文铭牌装在出口产线。机器不变,铭牌换一块就行。
资源限定符是怎么工作的
目录命名规则
Android 通过资源目录名称的后缀来区分不同配置。语言限定符的格式是 values-语言代码,如果需要区分地区则是 values-语言代码-r地区代码:
res/
values/ ← 默认(fallback)资源
strings.xml
values-zh/ ← 中文(任何地区)
strings.xml
values-zh-rCN/ ← 中文(中国大陆)
strings.xml
values-zh-rTW/ ← 中文(台湾)
strings.xml
values-en/ ← 英文(任何地区)
strings.xml
values-en-rUS/ ← 英文(美国)
strings.xml
values-ja/ ← 日语
strings.xml
values-fr/ ← 法语
strings.xml
语言代码遵循 ISO 639-1(两字母,如 zh、en、ja、fr),地区代码遵循 ISO 3166-1(如 CN、TW、US),前面加 r 前缀。目录名必须严格小写。
<Term def="系统在所有匹配的资源限定符中选择最具体的一个——比如同时有 values-zh-rCN 和 values-zh,用户用中文(中国大陆)时会优先选前者,用户用中文(台湾)时因没有 zh-rTW 而退到 zh">资源匹配规则</Term> 遵循 "最具体匹配" 原则:系统根据用户设备当前的 <Term def="语言+地区的组合标识(如 zh-CN、en-US),决定了日期格式、数字格式、字符排序等文化相关行为">Locale</Term> 设置,在所有可用的资源目录中选择最匹配的那个。优先级从高到低依次是:语言+地区完全匹配 → 仅语言匹配 → 默认值。
Locale 、屏幕密度等)去所有候选限定符目录里挑 最具体匹配 的那个:西班牙语设备命中 values-es/strings.xml、超高密度屏命中 drawable-xxhdpi/;任何一项匹配不到就回退默认 values/。代码里 @string/app_name 始终不变——换语言只是多加一个目录。在 Android Studio 中添加语言
Android Studio 提供了 Translations Editor(翻译编辑器)——这是最高效的添加多语言方式。操作路径:
- 右键
res/values/strings.xml→ Open Translations Editor - 点击左上角的地球图标 → 选择要添加的语言(比如 Chinese (zh) 或 French (fr))
- 编辑器中会出现新的一列——直接在表格里翻译每条字符串
- AS 自动在
res/下创建values-zh/strings.xml,只包含翻译过的条目
Translations Editor 还会自动标记哪些字符串还没有翻译(显示为红色),以及哪些翻译与默认值完全相同(可能是忘了翻译)。
strings.xml 里的字符串应该怎么写
把所有用户可见的文字都放进 strings.xml,永远不要在代码或布局 XML 里硬编码:
<!-- res/values/strings.xml(默认,通常是英文) -->
<resources>
<string name="app_name">ExpenseTracker</string>
<string name="add_expense">Add Expense</string>
<string name="monthly_summary">Monthly Summary</string>
<string name="delete_confirm">Are you sure you want to delete this entry?</string>
<string name="total_amount">Total: ¥%1$s</string>
</resources><!-- res/values-zh/strings.xml(中文翻译) -->
<resources>
<string name="app_name">记账助手</string>
<string name="add_expense">添加支出</string>
<string name="monthly_summary">月度汇总</string>
<string name="delete_confirm">确定要删除这条记录吗?</string>
<string name="total_amount">合计:¥%1$s</string>
</resources>注意 %1$s 这种带格式参数的字符串用法——%1 表示第一个参数,$s 表示字符串类型。不同语言的语序可能完全不同,用位置参数让翻译者可以自由调整顺序。比如中文 "合计:¥123" 和英文 "Total: ¥123" 语序一致,但日语可能是 "合計:123円"。
动手:给一个 App 加上中英文支持
猜一猜:如果你只加了
values-zh/strings.xml但没有加values-en/strings.xml,系统语言设为英文时 App 会显示什么?
1. 准备默认的 strings.xml
假设你有一个简单的 App,只有一个主页面显示问候语和按钮。先在 res/values/strings.xml 中写好默认(英文)字符串:
<resources>
<string name="app_name">My App</string>
<string name="greeting">Hello, welcome!</string>
<string name="btn_send">Send</string>
<string name="btn_cancel">Cancel</string>
</resources>布局文件 activity_main.xml 通过 @string/greeting 引用字符串——代码完全不碰文字内容:
代码逐段拆解
下面拆解一个完整的支持多语言 + 本地化格式的 Activity。
先定义好所有字符串资源:
<!-- res/values/strings.xml -->
<resources>
<string name="label_price">Price: %1$s</string>
<string name="label_date">Date: %1$s</string>
<string name="title_main">My Expenses</string>
<string name="btn_add">Add</string>
<string name="item_count">You have %1$d items</string>
</resources>
<!-- res/values-zh/strings.xml -->
<resources>
<string name="label_price">价格:%1$s</string>
<string name="label_date">日期:%1$s</string>
<string name="title_main">我的支出</string>
<string name="btn_add">添加</string>
<string name="item_count">你有 %1$d 笔记录</string>
</resources>然后是 Activity 的 Kotlin 代码——数字和日期都用 Locale 感知的方式格式化:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 从 strings.xml 取文字——永远不硬编码
supportActionBar?.title = getString(R.string.title_main)
// 数字用 NumberFormat 按当前 Locale 格式化
val price = 1234567.89
val currencyFormat = NumberFormat.getCurrencyInstance()
val formattedPrice = currencyFormat.format(price)
// 中文环境:¥1,234,567.89 英文环境:$1,234,567.89
tvPrice.text = getString(R.string.label_price, formattedPrice)
// 日期用 DateFormat 按当前 Locale 格式化
val today = Date()
val dateFormat = DateFormat.getDateInstance(DateFormat.LONG)
val formattedDate = dateFormat.format(today)
// 中文环境:2024年3月15日 英文环境:March 15, 2024
tvDate.text = getString(R.string.label_date, formattedDate)
// 带参数的字符串
val itemCount = 5
tvCount.text = getString(R.string.item_count, itemCount)
}
}public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 从 strings.xml 取文字
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(getString(R.string.title_main));
}
// 数字格式化
double price = 1234567.89;
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
String formattedPrice = currencyFormat.format(price);
tvPrice.setText(getString(R.string.label_price, formattedPrice));
// 日期格式化
Date today = new Date();
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG);
String formattedDate = dateFormat.format(today);
tvDate.setText(getString(R.string.label_date, formattedDate));
// 带参数的字符串
int itemCount = 5;
tvCount.setText(getString(R.string.item_count, itemCount));
}
}NumberFormat.getCurrencyInstance() 不带参数时自动使用用户设备的当前 Locale——中文设备显示 ¥,美国设备显示 $,日本设备显示 ¥。同理 getNumberInstance() 处理千位分隔符的差异。
DateFormat.getDateInstance(DateFormat.LONG) 根据 Locale 决定日期格式——中文 "2024年3月15日"、英文 "March 15, 2024"、德语 "15. März 2024"。不需要你手动拼接字符串。
RTL 布局支持: 阿拉伯语、希伯来语等语言是从右到左书写的。用 start/end 替代 left/right,系统自动根据语言方向翻转布局:
<!-- 所有支持 RTL 的 App 都应该这样写 -->
<!-- 而不是这样 -->
<!-- android:layout_marginLeft="16dp" ← LTR 语言没问题,RTL 语言就反了 -->复数字符串(Quantity Strings):不同语言的复数规则完全不同。中文 "1张照片" 和 "5张照片" 说法一样;英文 "1 photo" 和 "5 photos" 不同;俄语甚至有四种形式。用 <plurals> 而非 if-else:
<!-- res/values/strings.xml(英文) -->
<plurals name="photo_count">
<item quantity="one">%d photo</item>
<item quantity="other">%d photos</item>
</plurals>
<!-- res/values-zh/strings.xml(中文) -->
<plurals name="photo_count">
<item quantity="other">%d 张照片</item>
</plurals>
<!-- res/values-ru/strings.xml(俄语,有四种形式) -->
<plurals name="photo_count">
<item quantity="one">%d фотография</item>
<item quantity="few">%d фотографии</item>
<item quantity="many">%d фотографий</item>
<item quantity="other">%d фотографии</item>
</plurals>代码中通过 resources.getQuantityString() 使用复数字符串——系统自动根据数量选择正确的形式:
val count = 3
val text = resources.getQuantityString(
R.plurals.photo_count, count, count
)
// 英文:3 photos 中文:3 张照片 俄语:3 фотографии
tvPhoto.text = textint count = 3;
String text = getResources().getQuantityString(
R.plurals.photo_count, count, count
);
tvPhoto.setText(text);容易踩的坑
小结
- 资源限定符
values-语言代码让系统自动选择最匹配的字符串资源 - Android Studio 的 Translations Editor 可视化管理和翻译所有字符串
NumberFormat/DateFormat按当前 Locale 自动格式化数字和日期——不用手写格式串- 用
start/end替代left/right,一行代码不改就能兼容 RTL 语言 - 复数字符串用
<plurals>而非 if-else 拼接——不同语言有不同的复数规则
练习
问题 1:“第17章 Localization”覆盖哪些正式节点和项目主线?
问题 2:怎样建立本页最小可执行实验?
问题 3:为什么只在正常点击路径运行不能证明完成?
问题 4:怎样设计能推翻当前实现的反例?
问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?
问题 6:本页达到独立交接标准需要什么?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- Locale
语言+地区的组合标识(如 zh-CN、en-US)。决定了日期格式、数字格式、货币符号、字符排序规则等文化相关的行为。通过 Locale.getDefault() 获取当前系统设置。
- 资源匹配规则
系统在所有匹配的资源限定符中选择最具体的一个——比如同时有 values-zh-rCN 和 values-zh,用户用中文(中国大陆)时会优先选前者,用户用中文(台湾)时因没有 zh-rTW 而退到 zh。原则是"越具体越优先"。
- Configuration 变化
当设备配置(语言、屏幕方向、屏幕尺寸等)发生变化时,Android 系统默认会销毁并重新创建 Activity,以便加载与新配置匹配的资源。可以通过重写 onConfigurationChanged 或使用 ViewModel 来优化处理。
- 资源限定符
Android 资源目录名中的后缀(如 -zh 表示中文、-land 表示横屏、-xhdpi 表示超高密度屏幕),告诉系统在特定设备配置下使用对应目录里的资源。多个限定符可以用 - 连接,如 values-zh-rCN-land。
- RTL(从右到左)
Right-to-Left 的缩写。阿拉伯语、希伯来语、波斯语等语言的书写和阅读方向。Android 通过 start/end 属性和 android:supportsRtl 支持自动镜像布局。
“应用本地化”不使用未获授权的纸书正文;InformIT出版信息与授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。
为什么“应用本地化”必须回到可观察状态
“应用本地化”的学习结果不是记住类名,而是能预测“学会将App界面翻译成多语言、用资源限定符适配不同语言环境、格式化数字和日期、以及支持从右到左的布局。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。
第四版机制逐项深读
17. Localization
在“应用本地化”中,“17. Localization”让资源匹配器根据locale与配置选择候选;字符串参数、复数、日期、RTL和回退链必须由目标locale实测,不能拼接英语语序。
Localizing Resources
在“应用本地化”中,“Localizing Resources”的复用不能吞掉状态语义;pressed、focused、disabled与checked都要有可辨反馈。
Configuration Qualifiers
在“应用本地化”中,“Configuration Qualifiers”让资源匹配器根据locale与配置选择候选;字符串参数、复数、日期、RTL和回退链必须由目标locale实测,不能拼接英语语序。
Testing Alternative Resources
在“应用本地化”中,“Testing Alternative Resources”通过资源限定符和主题属性把内容与设备配置解耦;最终值由资源匹配和主题继承共同决定。
For the More Curious: More on Determining Device Size
在“应用本地化”中,“For the More Curious: More on Determining Device Size”用于推翻“学会将App界面翻译成多语言、用资源限定符适配不同语言环境、格式化数字和日期、以及支持从右到左的布局。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。
Challenge: Localizing Dates
在“应用本地化”中,“Challenge: Localizing Dates”让资源匹配器根据locale与配置选择候选;字符串参数、复数、日期、RTL和回退链必须由目标locale实测,不能拼接英语语序。
“应用本地化”验收回顾
“应用本地化”只有在双视口、大字体、焦点顺序、状态语义和对比度截图能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。