A
第 33 章JAVA40 分钟

国际化与多语言

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"
}

常见坑与最佳实践

  1. 拼接字符串而非占位符:"Total: " + count 在不同语序语言下读不通。务必用 getString(R.string.total, count)。
  2. 资源限定符顺序错误:values-night-zh-rCN 是错的,正确顺序是 values-zh-rCN-night。系统按固定优先级匹配,乱写可能匹配不到。
  3. recreate 后用户输入丢失:切换语言时 EditText 内容、滚动位置可能丢失。可以用 ViewModel 保存状态,或对 Activity 声明 android:configChanges="locale" 自行处理。
  4. 遗留 updateConfiguration:API 25 弃用,且只对当前 Resources 生效。新代码用 createConfigurationContext + ContextWrapper。
  5. 复数规则只写 other:英文有 one / other 两套,俄语有 one / few / many / other,阿拉伯语有 6 套。只写 other 在英语单数下显示 1 items,需要每套规则都补全。
  6. Locale.getDefault 在后台被改:应用语言切换后 Locale.getDefault 仍是系统值,应自己维护 Locale 变量。
  7. left/right 不支持 RTL:写布局时养成用 start/end 的习惯,或对老项目统一替换。
  8. Drawable 不镜像:图标在 RTL 下应该镜像。忘了 drawable-ldrtl 资源会让阿拉伯用户看到反向的箭头。
  9. 日期格式硬编码:直接 SimpleDateFormat("yyyy-MM-dd") 在不同地区显示样式不一致。考虑用 DateFormat.getDateInstance(DateFormat.SHORT, locale)。
  10. 混淆 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 响应式布局。