国际化与多语言
Android 国际化:strings.xml 多语言配置、locale 资源目录(values-zh / values-en)、运行时切换语言(Configuration / ContextWrapper)、RTL 布局支持、日期与数字的本地化格式化。
学习目标
- 理解 Android 资源限定符与 locale 目录命名
- 能够用 strings.xml 提供多语言文案
- 掌握运行时切换语言并持久化
- 了解 RTL 布局与 start/end 属性
- 能够使用 NumberFormat 与 DateFormat 进行本地化
学习目标
- 理解 Android 资源限定符与 locale 目录命名;
- 能够用
strings.xml提供多语言文案; - 掌握运行时切换语言并持久化;
- 了解 RTL 布局与
start/end属性; - 能够使用
NumberFormat与DateFormat进行本地化。
国际化与多语言
资源限定符
Android 资源系统通过在 res/ 下创建带限定符后缀的目录,让系统在不同条件下加载不同资源。语言相关的限定符是 BCP 47 语言标签:
| 目录 | 语言 / 区域 |
|---|---|
values/ |
默认(fallback) |
values-en/ |
英语 |
values-zh/ |
中文(不分简繁) |
values-zh-rCN/ |
简体中文 |
values-zh-rTW/ |
繁体中文 |
values-ja/ |
日语 |
values-de/ |
德语 |
规则:
- 语言用小写两字母(
en、zh),区域用大写两字母加r前缀(rCN); - 多个限定符用
-连接,必须按 MCC -> 语言 -> 区域 -> … -> UI 模式 的固定顺序,例如values-zh-rCN-night。
strings.xml 多语言
默认资源 res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MyApp</string>
<string name="welcome">Welcome</string>
<string name="hello_user">Hello, %1$s!</string>
<plurals name="items_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
</resources>
中文 res/values-zh/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">我的应用</string>
<string name="welcome">欢迎</string>
<string name="hello_user">你好,%1$s!</string>
<plurals name="items_count">
<item quantity="other">%d 项</item>
</plurals>
</resources>
简体中文 res/values-zh-rCN/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="welcome">欢迎光临</string>
</resources>
英文 res/values-en/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="welcome">Welcome</string>
</resources>
占位符与复数
字符串里用 %1$s、%2$d 占位。Java 里通过 getString(int, Object...) 格式化:
String msg = getString(R.string.hello_user, "Alice"); // Hello, Alice!
复数 plurals 通过 getQuantityString(int, int, ...) 使用:
int count = 3;
String text = getResources().getQuantityString(R.plurals.items_count, count, count);
// 英文:3 items;中文:3 项
注意:第一个 count 决定复数规则,第二个 count 才是占位符参数。
字符串拼接的误区
i18n 的禁忌是 字符串拼接:
// ❌ 反例
String text = "Welcome, " + name + "!"; // 不同语言语序不同
正确做法用占位符:
String text = getString(R.string.welcome_user, name);
因为不同语言的语序不同。英语可能是 “Hello, Alice!”,日语是 “Aliceさん、こんにちは!”,占位符位置可能完全不同。%1$s 让翻译者重新排列。
运行时切换语言
老方案:updateConfiguration
Android 24 之前的标准做法:
@Deprecated
public static Context wrapLegacy(Context context, Locale locale) {
Configuration config = new Configuration(context.getResources().getConfiguration());
config.setLocale(locale);
context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
return context;
}
updateConfiguration 在 API 25 弃用,且只对当前 Resources 生效。
新方案:createConfigurationContext(API 17+)
public class LocaleHelper {
public static Context wrap(Context context, Locale locale) {
Configuration config = new Configuration(context.getResources().getConfiguration());
config.setLocale(locale);
// LocaleList 兼容 API 24+
LocaleList localeList = new LocaleList(locale);
LocaleList.setDefault(localeList);
config.setLocales(localeList);
return context.createConfigurationContext(config);
}
}
通过 ContextWrapper 透传
为了让切换生效到整个 Activity,覆盖 attachBaseContext:
public abstract class BaseLocaleActivity extends AppCompatActivity {
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(LocaleHelper.wrap(newBase, getSavedLocale(newBase)));
}
private Locale getSavedLocale(Context context) {
SharedPreferences sp = context.getSharedPreferences("lang_prefs", MODE_PRIVATE);
String lang = sp.getString("lang", Locale.getDefault().getLanguage());
if ("zh".equals(lang)) return Locale.SIMPLIFIED_CHINESE;
if ("en".equals(lang)) return Locale.US;
if ("ja".equals(lang)) return Locale.JAPAN;
return Locale.getDefault();
}
}
切换语言后重建:
public class LanguageActivity extends BaseLocaleActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_language);
findViewById(R.id.btn_zh).setOnClickListener(v -> setLanguage("zh"));
findViewById(R.id.btn_en).setOnClickListener(v -> setLanguage("en"));
findViewById(R.id.btn_ja).setOnClickListener(v -> setLanguage("ja"));
}
private void setLanguage(String lang) {
getSharedPreferences("lang_prefs", MODE_PRIVATE)
.edit().putString("lang", lang).apply();
// 让所有 Activity 重新 attachBaseContext
recreate();
// 重建其他栈中 Activity
// 一种简易做法是发广播或用 EventBus 通知栈顶 Activity
}
}
Application 级别初始化
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LocaleHelper.wrap(base, getSavedLocale(base)));
}
private Locale getSavedLocale(Context context) {
SharedPreferences sp = context.getSharedPreferences("lang_prefs", MODE_PRIVATE);
String lang = sp.getString("lang", Locale.getDefault().getLanguage());
if ("zh".equals(lang)) return Locale.SIMPLIFIED_CHINESE;
if ("en".equals(lang)) return Locale.US;
return Locale.getDefault();
}
}
注意:Android 7.0+ 官方建议用 ContextWrapper + attachBaseContext 透传,而不是修改全局 Configuration,因为后者只对当前进程生效。
重建栈中 Activity
切换语言时通常希望所有栈中 Activity 都重新加载文案。简易方案是用 LocalBroadcastManager 或 ViewModel 标记,让 onResume 时调用 recreate()。一个常用做法是遍历 ActivityManager 获取当前任务栈并重建:
public class LanguageActivity extends BaseLocaleActivity {
private void setLanguage(String lang) {
getSharedPreferences("lang_prefs", MODE_PRIVATE)
.edit().putString("lang", lang).apply();
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
}
}
通过 FLAG_ACTIVITY_CLEAR_TOP 把目标 Activity 之上的 Activity 都清掉,从而让用户重新进入主流程拿到新语言。
RTL 布局支持
阿拉伯语、希伯来语等是从右向左书写(RTL)。Android 提供 RTL 支持:
启用 RTL
AndroidManifest.xml 中:
<application
android:supportsRtl="true">
<!-- ... -->
</application>
用 start/end 替代 left/right
<!-- ❌ 写死方向 -->
<View
android:layout_marginLeft="16dp"
android:layout_alignParentLeft="true" />
<!-- ✓ 自动镜像 -->
<View
android:layout_marginStart="16dp"
android:layout_alignParentStart="true" />
start 在 LTR 时等于 left,在 RTL 时等于 right,自动适配。
代码方向判断
public class RtlHelper {
public static boolean isRtl(Context context) {
return TextUtils.getLayoutDirectionFromLocale(
context.getResources().getConfiguration().getLocales().get(0))
== View.LAYOUT_DIRECTION_RTL;
}
}
Drawable 自动镜像
带方向性的图标(如返回箭头)应放 res/drawable-ldrtl/:
res/drawable/ic_back.xml (LTR 指向左)
res/drawable-ldrtl/ic_back.xml (RTL 指向右)
或对 ImageView 调 setImageAutoMirror(true)(部分组件支持)。
RTL Padding 调整
setPadding 不支持自动镜像,需用 setPaddingRelative(start, top, end, bottom):
textView.setPaddingRelative(16, 8, 16, 8);
日期与数字本地化
NumberFormat
double pi = 3.14159;
double amount = 1234567.89;
// 按当前 Locale 格式化数字
NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
String us = nf.format(pi); // 3.142
String cn = NumberFormat.getNumberInstance(Locale.CHINA).format(pi); // 3.142
// 货币
NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.US);
String usd = currency.format(amount); // $1,234,567.89
NumberFormat yuan = NumberFormat.getCurrencyInstance(Locale.CHINA);
String rmb = yuan.format(amount); // ¥1,234,567.89
// 百分比
NumberFormat pct = NumberFormat.getPercentInstance(Locale.US);
String text = pct.format(0.25); // 25%
DateFormat
long timestamp = System.currentTimeMillis();
// 按当前 Locale 默认格式
String localized = DateFormat.getDateInstance(DateFormat.LONG, Locale.US)
.format(new Date(timestamp));
// October 9, 2026
String localizedCn = DateFormat.getDateInstance(DateFormat.LONG, Locale.CHINA)
.format(new Date(timestamp));
// 2026年10月9日
// Android 资源格式
// res/values/strings.xml: <string name="date_format">MM/dd/yyyy</string>
SimpleDateFormat sdf = new SimpleDateFormat(
getString(R.string.date_format), Locale.US);
String text = sdf.format(new Date(timestamp)); // 10/09/2026
icu4j / libphonenumber
更复杂的本地化(电话号码、复数规则、单位换算)可以引入:
dependencies {
implementation "com.googlecode.libphonenumber:libphonenumber:8.13.45"
}
常见坑与最佳实践
- 拼接字符串而非占位符:
"Total: " + count在不同语序语言下读不通。务必用getString(R.string.total, count)。 - 资源限定符顺序错误:
values-night-zh-rCN是错的,正确顺序是values-zh-rCN-night。系统按固定优先级匹配,乱写可能匹配不到。 recreate后用户输入丢失:切换语言时 EditText 内容、滚动位置可能丢失。可以用ViewModel保存状态,或对 Activity 声明android:configChanges="locale"自行处理。- 遗留
updateConfiguration:API 25 弃用,且只对当前Resources生效。新代码用createConfigurationContext+ContextWrapper。 - 复数规则只写
other:英文有one/other两套,俄语有one/few/many/other,阿拉伯语有 6 套。只写other在英语单数下显示1 items,需要每套规则都补全。 Locale.getDefault在后台被改:应用语言切换后Locale.getDefault仍是系统值,应自己维护Locale变量。left/right不支持 RTL:写布局时养成用start/end的习惯,或对老项目统一替换。- Drawable 不镜像:图标在 RTL 下应该镜像。忘了
drawable-ldrtl资源会让阿拉伯用户看到反向的箭头。 - 日期格式硬编码:直接
SimpleDateFormat("yyyy-MM-dd")在不同地区显示样式不一致。考虑用DateFormat.getDateInstance(DateFormat.SHORT, locale)。 - 混淆
Locale.CHINA与Locale.SIMPLIFIED_CHINESE:前者是 region(带国家),后者只有语言。资源匹配时values-zh-rCN用Locale.SIMPLIFIED_CHINESE也可能匹配(因语言相同),但更精确的是用new Locale("zh", "CN")。
章节小结
本章覆盖了 Android 国际化体系:资源限定符(values-<lang>-r<region>)、strings.xml 多语言文案与 plurals 复数、占位符 %1$s 替代字符串拼接、运行时切换语言(createConfigurationContext + ContextWrapper 透传)、RTL 布局(start/end、drawable-ldrtl、setPaddingRelative)以及日期/数字本地化(NumberFormat / DateFormat / SimpleDateFormat)。核心要点:所有用户可见文案都从 strings.xml 取,资源按 locale 提供替代版本,RTL 用 start/end 替代 left/right,不要直接拼接字符串。
下一章预告
下一章将进入屏幕适配领域:dp 与 sp 原理、smallestWidth 限定符、布局别名、小屏 / 大屏 / 折叠屏适配、WindowMetrics API、ConstraintLayout 响应式布局。