多语言适配陷阱:当图片和设备名都需要国际化时

背景

我们的老年关怀应用需要支持简体中文、繁体中文和英文三种语言。字符串资源的国际化是常规操作——在 values/values-zh-rTW/values-en/ 下分别放对应的 strings.xml 就行。但当涉及到图片资源动态拼接的设备名时,多语言适配就会出现一些不那么明显的陷阱。

本文分享了两个实际遇到的案例:引导图片的多语言切换,以及TTS语音播报中设备名的本地化问题。

案例一:引导图片的多语言适配

问题

心电检测引导页有两张说明图片 dialog_4dialog_5,原本只有简体中文版本。产品要求增加繁体(dialog_4_tw/dialog_5_tw)和英文(dialog_4_en/dialog_5_en)版本,根据当前语言展示对应图片。

新增的图片资源已放到 mipmap-xxhdpi 目录,但XML布局里硬编码了简体图片:

<ImageView
    android:id="@+id/iv_check_hint"
    android:src="@mipmap/dialog_4" />

<ImageView
    android:id="@+id/iv_check_hint2"
    android:src="@mipmap/dialog_5" />

思路误区:能不能用资源限定符?

Android 支持通过资源限定符自动匹配不同语言版本的资源,比如:

mipmap-zh-rTW/dialog_4.webp    // 繁体
mipmap-en/dialog_4.webp        // 英文
mipmap/dialog_4.webp           // 默认(简体)

系统会根据当前Locale自动选择对应目录下的资源,XML里写 @mipmap/dialog_4 就行。

但这条路走不通,原因有两个:

  1. 文件格式不一致:简体版是 .webp,繁体和英文版是 .png,资源限定符目录要求同密度下文件名一致
  2. 另一个入口也需要同一套图片:引导弹窗 HealthPulseDeviceGuideDialog 的代码里也引用了这些图片,两个入口需要保持一致

解决方案:代码中动态选择

在Fragment和Dialog中根据 LanguageSP().language 动态选择图片资源:

// Fragment中
private fun setupGuideImages() {
    val (hint1Res, hint2Res) = when (LanguageSP().language) {
        "zh_TW" -> R.mipmap.dialog_4_tw to R.mipmap.dialog_5_tw
        "en" -> R.mipmap.dialog_4_en to R.mipmap.dialog_5_en
        else -> R.mipmap.dialog_4 to R.mipmap.dialog_5
    }
    viewBinding.ivCheckHint.setImageResource(hint1Res)
    viewBinding.ivCheckHint2.setImageResource(hint2Res)
}
// Dialog中
private val guideItems: List<HealthPulseDeviceGuideItem> by lazy {
    val dialog4Images = when (LanguageSP().language) {
        "zh_TW" -> listOf(R.mipmap.dialog_4_tw, R.mipmap.dialog_5_tw)
        "en" -> listOf(R.mipmap.dialog_4_en, R.mipmap.dialog_5_en)
        else -> listOf(R.mipmap.dialog_4, R.mipmap.dialog_5)
    }
    listOf(
        HealthPulseDeviceGuideItem(
            R.string.health_pulse_guide_title_4,
            R.string.health_pulse_guide_content_4,
            dialog4Images
        )
    )
}

XML保留默认值

XML中的 android:src 保留为简体中文图片作为默认值。代码运行时会覆盖;如果代码路径未执行(异常情况),至少显示简体图片,不至于空白:

<ImageView
    android:id="@+id/iv_check_hint"
    android:src="@mipmap/dialog_4" />  <!-- 默认值,代码会覆盖 -->

案例二:TTS设备名的本地化

问题

设备连接成功后,应用会语音播报"XX已连接",其中XX是设备名。字符串模板已经三语翻译:

<!-- values/strings.xml -->
<string name="health_pulse_connected_auto_check">%1$s已连接</string>
<!-- values-zh-rTW/strings.xml -->
<string name="health_pulse_connected_auto_check">%1$s已連接</string>
<!-- values-en/strings.xml -->
<string name="health_pulse_connected_auto_check">%1$s is connected</string>

看起来没问题?实际测试发现,英文模式下播报的是"脉搏健康监测仪 is connected"——设备名是中文,"已连接"是英文,中英混杂。

根因

传入的 deviceName 来自服务端 HealthAccessoryModel.name 字段,这是服务端返回的中文产品名,不是本地化字符串:

// 原来的逻辑
private fun getPulseIntroDeviceName(): String {
    return pulseHealthDevice?.nickname?.ifBlank {
        pulseHealthDevice?.name.orEmpty()  // ← 服务端中文产品名
    }?.ifBlank {
        getString(R.string.pulse_health_monitor)  // 本地化兜底
    } ?: getString(R.string.pulse_health_monitor)
}

优先级是:nickname(用户自定义)→ name(服务端中文)→ 本地化兜底。

问题出在第二步:name 是服务端存储的产品名,永远是中文(如"脉搏健康监测仪")。当用户没有设置 nickname 时,就会用这个中文名,导致英文模式下出现混杂。

解决方案:跳过服务端name

private fun getPulseIntroDeviceName(): String {
    // 优先用户自定义昵称(用户自己输入的语言无需本地化),否则用本地化产品名
    // 跳过服务端 name 字段(中文产品名,非本地化)
    return pulseHealthDevice?.nickname?.takeIf { it.isNotBlank() }
        ?: getString(R.string.pulse_health_monitor)
}

private fun getPortableEcgIntroDeviceName(): String {
    return portableEcgDevice?.nickname?.takeIf { it.isNotBlank() }
        ?: getDefaultEcgDeviceName()
}

优先级改为:nickname(用户自定义)→ 本地化兜底,跳过服务端 name

设计取舍

为什么保留 nickname?因为昵称是用户自己输入的——如果用户给设备起名叫"爷爷的监护仪",那不管app设什么语言,都应该播报这个名字。用户输入什么语言就用什么语言,不需要本地化。

为什么跳过 name?因为 name 是服务端产品名,是硬编码的中文,无法本地化。与其在客户端做"中文产品名 → 英文产品名"的映射表(维护成本高且容易漏),不如直接用已经三语翻译好的字符串资源。

语言值约定

项目中用 LanguageSP 存储当前语言,有三个值:

class LanguageSP {
    var language: String by SharePreferences("language", "zh_cn") //默认中文
}

// 语言常量
const val LANGUAGE_SIMPLIFIED = "zh_cn"   // 简体中文
const val LANGUAGE_TRADITIONAL = "zh_TW"  // 繁体中文
const val LANGUAGE_ENGLISH = "en"         // 英文

注意简体是 "zh_cn"(小写),繁体是 "zh_TW"(大小写混合)。这个不一致容易导致bug,使用时必须严格匹配。

收获与思考

1. 字符串国际化只是冰山一角

做多语言适配时,最容易想到的是字符串资源——放三个 strings.xml 就行。但实际项目中,图片、音频、TTS播报文本、服务端返回的数据都可能包含语言相关的硬编码。每一类都需要单独考虑本地化策略。

2. 资源限定符不是万能的

Android的资源限定符机制很强大,能自动根据Locale/density/屏幕尺寸选择资源。但它有局限性:

  • 同一资源名在不同限定符目录下必须文件格式一致
  • 动态创建的资源(如Dialog代码中构造的列表)无法享受自动匹配
  • 如果图片来自不同渠道(设计切图格式不统一),限定符方案可能不适用

代码中 when(language) 的方式虽然"原始",但灵活性最高——可以处理任意格式、任意命名的资源,也方便在多个入口复用同一套逻辑。

3. 服务端数据不是本地化的终点

这个案例揭示了一个容易被忽略的问题:服务端返回的数据可能包含硬编码的中文HealthAccessoryModel.name 看起来是个普通的字符串字段,但它的值是服务端写死的产品名。做多语言适配时,不能只看客户端代码,还要审查所有数据来源——服务端API返回的字段、本地缓存的SP数据、第三方SDK的回调数据,都可能是中文字符串的藏身之处。

4. "兜底"设计要考虑所有路径

原来的 getPulseIntroDeviceName 有三级fallback:nickname → name → 本地化字符串。看起来很健壮,但第二级(name)实际上是个"陷阱"——它永远不会为空(服务端总会返回产品名),所以第三级的本地化字符串永远不会被触发。修复后的代码去掉了这个"陷阱",让fallback路径真正有效。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容