23、本地化测试:字符串资源测试、RTL布局测试、区域设置测试
本地化测试,说白了就是检查你的App在不同语言、不同地区下能不能正常工作。我见过太多App,中文界面完美无瑕,一切换到阿拉伯语就界面错乱、文字截断、甚至直接崩溃。嗯,这章我们就来聊聊怎么用Compose UI测试把这些坑都填上。
核心要点:本地化测试不是简单的翻译检查,而是验证字符串资源是否正确加载、RTL布局是否正常渲染、区域设置是否影响业务逻辑。这三者缺一不可。
23.1 字符串资源测试:别让翻译"跑偏"
字符串资源测试,听起来简单,但坑不少。我个人习惯在测试中直接验证资源ID对应的字符串值,而不是硬编码中文或英文。
23.1.1 基础字符串加载测试
@Test
fun testStringResourceLoaded() {
// 使用ComposeTestRule获取上下文
composeTestRule.activity.apply {
val expected = getString(R.string.app_name)
assertEquals("我的应用", expected)
}
}
// 更推荐的方式:通过Compose的stringResource
@Test
fun testStringInCompose() {
composeTestRule.setContent {
val greeting = stringResource(R.string.greeting)
Text(text = greeting)
}
composeTestRule.onNodeWithText("你好,世界").assertExists()
}
我在项目中遇到过一个问题:某个翻译人员把占位符写错了,导致运行时崩溃。后来我加了一个测试,专门检查所有带占位符的字符串格式是否正确。
23.1.2 占位符参数验证
@Test
fun testStringWithPlaceholder() {
composeTestRule.setContent {
val message = stringResource(R.string.welcome_message, "张三")
Text(text = message)
}
// 验证占位符被正确替换
composeTestRule.onNodeWithText("欢迎,张三").assertExists()
}
// 批量测试所有语言
@Config(locales = ["en", "zh", "ar", "ja"])
@Test
fun testAllLocalesPlaceholder() {
composeTestRule.setContent {
val message = stringResource(R.string.order_count, 5)
Text(text = message)
}
// 不同语言下,数字5应该出现在正确位置
composeTestRule.onNode(hasText(containsString("5"))).assertExists()
}
小技巧:我建议在CI中跑一个自动化脚本,遍历所有strings.xml文件,检查占位符数量是否一致。比如中文有2个%s,英文也必须有2个。这个脚本能提前发现80%的翻译问题。
23.2 RTL布局测试:镜像世界的挑战
RTL(Right-to-Left)布局,说白了就是让界面从右往左排列。阿拉伯语、希伯来语都用这种布局。你想想看,一个左对齐的按钮,在RTL下应该变成右对齐。如果没处理好,用户体验会非常糟糕。
23.2.1 检测布局方向
@Test
fun testRtlLayout() {
composeTestRule.setContent {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Text(text = if (isRtl) "RTL模式" else "LTR模式")
}
// 模拟RTL环境
composeTestRule.activity.apply {
resources.configuration.uiMode =
Configuration.UI_MODE_NIGHT_NO
// 设置布局方向为RTL
window.decorView.layoutDirection = View.LAYOUT_DIRECTION_RTL
}
}
我曾经接手过一个项目,RTL下所有图标都反了。原因很简单:用了start和end属性,但图标本身没有镜像翻转。嗯,这里要注意:图标也需要根据布局方向做镜像处理。
23.2.2 使用Semantics验证RTL
@Test
fun testRtlSemantics() {
composeTestRule.setContent {
Row(
modifier = Modifier.semantics {
testTag = "rtlRow"
}
) {
Text("左")
Text("右")
}
}
// 在RTL下,语义顺序应该反转
composeTestRule.onNodeWithTag("rtlRow")
.assert(hasContentDescription("右 左"))
}
避坑指南:我曾经在测试RTL时,发现Compose的Row默认不会自动反转子元素顺序。你需要显式设置layoutDirection或者使用Arrangement来适配。否则测试结果会跟实际表现不一致。
23.3 区域设置测试:数字、日期、货币的"方言"
区域设置(Locale)影响的可不只是语言。数字格式、日期格式、货币符号、甚至排序规则都会变。我见过一个App,在美国显示"$1,000.50",到了德国变成了"$1.000,50"——用户直接懵了。
23.3.1 数字格式化测试
@Test
fun testNumberFormatByLocale() {
composeTestRule.setContent {
val formatted = remember {
NumberFormat.getNumberInstance(Locale.GERMANY)
.format(1234567.89)
}
Text(text = formatted)
}
// 德国格式:1.234.567,89
composeTestRule.onNodeWithText("1.234.567,89").assertExists()
}
@Config(locales = ["de"])
@Test
fun testGermanLocale() {
composeTestRule.setContent {
val price = stringResource(R.string.price, 1234.56)
Text(text = price)
}
// 验证德国价格格式
composeTestRule.onNodeWithText("1.234,56 €").assertExists()
}
23.3.2 日期与货币测试
@Test
fun testDateFormatByLocale() {
val testDate = LocalDate.of(2024, 3, 15)
composeTestRule.setContent {
val formatted = remember {
DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(Locale.JAPAN)
.format(testDate)
}
Text(text = formatted)
}
// 日本格式:2024年3月15日
composeTestRule.onNodeWithText("2024年3月15日").assertExists()
}
// 货币测试
@Test
fun testCurrencyFormat() {
composeTestRule.setContent {
val formatted = remember {
NumberFormat.getCurrencyInstance(Locale.UK)
.format(99.99)
}
Text(text = formatted)
}
// 英国格式:£99.99
composeTestRule.onNodeWithText("£99.99").assertExists()
}
个人经验:我建议在测试中覆盖至少三种区域设置:en_US(美式)、de_DE(欧式)、ar_SA(阿拉伯)。这三个基本能覆盖大部分格式差异。如果App面向特定市场,再加对应的区域设置。
23.4 知识体系图:本地化测试全景
23.5 实战:组合测试场景
在实际项目中,这三个测试往往是组合出现的。我分享一个我常用的测试模板:
@RunWith(ParameterizedTest::class)
class LocalizationTest {
data class LocaleTestCase(
val locale: Locale,
val expectedDateFormat: String,
val expectedNumberFormat: String,
val isRtl: Boolean
)
companion object {
@JvmStatic
@ParameterizedTest.Parameters
fun data() = listOf(
LocaleTestCase(Locale.US, "3/15/24", "1,234.56", false),
LocaleTestCase(Locale.GERMANY, "15.03.24", "1.234,56", false),
LocaleTestCase(Locale("ar", "SA"), "15/03/24", "1٬234٫56", true)
)
}
@ParameterizedTest
@Test
fun testLocaleCombined(localeCase: LocaleTestCase) {
composeTestRule.setContent {
CompositionLocalProvider(
LocalLayoutDirection provides
if (localeCase.isRtl) LayoutDirection.Rtl
else LayoutDirection.Ltr
) {
Column {
Text(stringResource(R.string.date, localeCase.locale))
Text(stringResource(R.string.number, localeCase.locale))
}
}
}
// 验证布局方向
if (localeCase.isRtl) {
composeTestRule.onRoot().assert(hasLayoutDirection(LayoutDirection.Rtl))
}
}
}
重要提醒:参数化测试虽然强大,但不要贪多。我建议每个区域设置单独写一个测试类,而不是把所有语言塞到一个测试里。否则某个语言失败会导致整个测试套件变红,排查起来很麻烦。
本地化测试,说白了就是让你的App在全世界都能"说人话"。字符串资源保证翻译正确,RTL保证布局不崩,区域设置保证格式统一。这三板斧砍下去,大部分本地化问题都能提前发现。嗯,今天就聊到这里,希望这些经验对你有帮助。
公众号:蓝海资料掘金营,微信deep3321