第1章:国际化与本地化测试——让App说“人话”

国际化与本地化测试,说白了就是让你的App能跟不同国家、不同语言的人“好好聊天”。我做了这么多年Android开发,见过太多App在中文环境下跑得飞起,一换到阿拉伯语就界面崩成马赛克。嗯,今天我们就来聊聊怎么避免这种尴尬。

1.1 多语言资源测试:别让翻译“翻车”

多语言资源测试,核心就是检查你的strings.xml文件有没有“缺斤少两”。我个人习惯在项目里维护一个资源覆盖检查清单

  • 所有values/strings.xml中的key,在values-zh/strings.xml里都有对应翻译
  • 翻译内容没有截断、没有乱码
  • 占位符%s%d的数量和顺序一致

核心测试方法:

// 使用AndroidX Test的LocaleManager切换语言
@RunWith(AndroidJUnit4::class)
class LocalizationTest {
    
    @Test
    fun testChineseStringResources() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        val config = Configuration(context.resources.configuration)
        config.setLocale(Locale.CHINESE)
        val localizedContext = context.createConfigurationContext(config)
        
        val appName = localizedContext.getString(R.string.app_name)
        assertEquals("我的应用", appName)
    }
    
    @Test
    fun testEnglishStringResources() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        val config = Configuration(context.resources.configuration)
        config.setLocale(Locale.ENGLISH)
        val localizedContext = context.createConfigurationContext(config)
        
        val appName = localizedContext.getString(R.string.app_name)
        assertEquals("My App", appName)
    }
}

避坑指南:我曾经在项目中遇到过一个坑——某个翻译人员把%1$s写成了%1s,少了个美元符号,结果App在英文环境下直接崩溃。所以,我建议你写一个自动化脚本,遍历所有语言文件夹,检查占位符格式是否一致。

1.2 RTL布局测试:从右往左的“镜像世界”

RTL(Right-to-Left)布局,主要针对阿拉伯语、希伯来语等从右往左书写的语言。你想想看,一个习惯了从左往右看的人,突然看到界面镜像翻转,是不是会懵?

测试RTL布局,我一般分三步走:

  1. 静态检查:在布局文件中使用start/end代替left/right
  2. 动态切换:在开发者选项中强制开启RTL模式
  3. 自动化测试:用Espresso模拟RTL环境下的点击操作
// 强制切换RTL布局的测试方法
@Test
fun testRtlLayout() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    val config = Configuration(context.resources.configuration)
    config.setLocale(Locale("ar")) // 阿拉伯语
    config.layoutDirection = View.LAYOUT_DIRECTION_RTL
    val rtlContext = context.createConfigurationContext(config)
    
    // 检查布局方向
    val textView = TextView(rtlContext)
    textView.text = "مرحبا"
    assertEquals(View.LAYOUT_DIRECTION_RTL, textView.layoutDirection)
}

注意:RTL布局下,ViewPager的滑动方向也会反转。我曾经遇到过一个Bug——阿拉伯语用户往左滑,页面却往右翻,用户体验极差。解决方案是在ViewPageronPageScrolled回调里判断布局方向,手动调整滑动逻辑。

1.3 区域设置测试:数字、日期、货币的“方言”

区域设置测试,说白了就是检查你的App在不同国家/地区下,数字、日期、货币的显示格式是否正确。比如:

  • 美国:$1,234.56,日期格式MM/dd/yyyy
  • 德国:1.234,56 €,日期格式dd.MM.yyyy
  • 日本:¥1,234,日期格式yyyy/MM/dd

我习惯用java.text.NumberFormatjava.text.DateFormat来做格式化测试:

@Test
fun testCurrencyFormat() {
    val usLocale = Locale.US
    val deLocale = Locale.GERMANY
    
    val usFormat = NumberFormat.getCurrencyInstance(usLocale)
    val deFormat = NumberFormat.getCurrencyInstance(deLocale)
    
    assertEquals("$1,234.56", usFormat.format(1234.56))
    assertEquals("1.234,56 €", deFormat.format(1234.56))
}

@Test
fun testDateFormat() {
    val usLocale = Locale.US
    val jpLocale = Locale.JAPAN
    
    val date = Date(2024, 0, 15) // 2024年1月15日
    val usDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, usLocale)
    val jpDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, jpLocale)
    
    assertEquals("1/15/24", usDateFormat.format(date))
    assertEquals("2024/01/15", jpDateFormat.format(date))
}

个人经验:测试货币格式时,别忘了检查货币符号的位置。有些语言(比如法语)货币符号在数字后面,而且有空格。我曾在项目中因为没注意这个细节,导致法国用户看到的金额显示为1234,56€(正确应该是1 234,56 €)。

1.4 字符串占位符测试:别让“填空”变成“填坑”

字符串占位符测试,核心是检查%s%d%1$s等占位符在翻译后是否还能正确替换。我见过最离谱的Bug是:英文版Hello, %s!,中文翻译成了你好,%s!,但阿拉伯语翻译把%s写成了%S(大写S),结果直接报错。

测试方法很简单:

@Test
fun testStringPlaceholders() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    
    // 测试英文占位符
    val enConfig = Configuration(context.resources.configuration)
    enConfig.setLocale(Locale.ENGLISH)
    val enContext = context.createConfigurationContext(enConfig)
    val enGreeting = enContext.getString(R.string.greeting, "John")
    assertEquals("Hello, John!", enGreeting)
    
    // 测试中文占位符
    val zhConfig = Configuration(context.resources.configuration)
    zhConfig.setLocale(Locale.CHINESE)
    val zhContext = context.createConfigurationContext(zhConfig)
    val zhGreeting = zhContext.getString(R.string.greeting, "张三")
    assertEquals("你好,张三!", zhGreeting)
    
    // 测试阿拉伯语占位符
    val arConfig = Configuration(context.resources.configuration)
    arConfig.setLocale(Locale("ar"))
    val arContext = context.createConfigurationContext(arConfig)
    val arGreeting = arContext.getString(R.string.greeting, "أحمد")
    assertEquals("مرحبا، أحمد!", arGreeting)
}

注意:如果占位符有多个(比如%1$s%2$d),一定要检查翻译后的顺序。有些语言会把主语和宾语的位置调换,比如英语说%1$s gave %2$d books to %3$s,日语可能变成%3$sに%2$d冊の本を%1$sが渡しました。占位符的顺序变了,但索引不能变。

知识体系总览

下面这张图概括了国际化与本地化测试的核心知识点:

国际化与本地化测试 多语言资源测试 • strings.xml key完整性 • 翻译内容无截断/乱码 • 占位符格式一致性 RTL布局测试 • start/end替代left/right • 开发者选项强制RTL • ViewPager滑动方向 区域设置测试 • 数字格式(千分位/小数点) • 日期格式(MM/dd vs dd/MM) • 货币符号位置 字符串占位符测试 • %s/%d格式正确性 • 多占位符顺序一致性 • 翻译后索引不变

国际化与本地化测试,看似琐碎,但每一个细节都直接影响用户体验。我个人建议你把这些测试用例写成一个参数化测试类,用@RunWith(Parameterized::class)批量跑所有语言环境。这样每次发版前跑一遍,心里踏实。

最后一个小技巧:adb shell setprop persist.sys.locale命令可以快速切换设备的系统语言,配合自动化测试脚本,能省下大量手动测试的时间。我在团队里就是这么干的,效果拔群。

公众号:蓝海资料掘金营,微信deep3321