在 Kotlin Multiplatform SDK 中使用本地化和语言区域代码

为什么这很重要

当 Adapty 为流程或用户引导选择本地化内容时,以及当你为自定义付费墙读取远程配置时,语言区域代码就会发挥作用。

语言区域代码较为复杂,且因平台而异,因此 Adapty 在其支持的所有平台上统一采用一套内部标准。了解该标准有助于你预测用户最终会收到哪个本地化版本。

Adapty 的语言区域代码标准

Adapty 的语言区域代码采用略经修改的 BCP 47 标准:每个代码由小写子标签组成,以连字符分隔。示例:en(英语)、pt-br(葡萄牙语(巴西))、zh(简体中文)、zh-hant(繁体中文)。

语言区域代码匹配

在 SDK v4 中,流程和用户引导的语言区域代码匹配方式有所不同:流程由 SDK 在设备端完成本地化,而用户引导则由 Adapty 服务器完成本地化。

流程与付费墙编辑工具构建的付费墙

在 SDK v4 中,通过付费墙编辑工具构建的付费墙以流程的形式下发,因此以下规则同时适用于两者。

匹配规则为精确匹配。SDK 会将你传入的代码与流程的本地化代码逐字符进行比较:不会转换大小写,不会将下划线(_)替换为连字符(-),也不会回退到语言子标签。对于包含 pt-br 本地化的流程,只有 pt-br 能匹配:pt-BRpt_BRpt-PT 均无法匹配。

当代码未匹配任何本地化语言时,流程会静默地以其默认语言环境渲染——SDK 不会返回错误,也不会记录警告。

当代码匹配时,Adapty 会将匹配的本地化语言与默认本地化语言合并:匹配的本地化语言中未定义的字符串和资源将从默认本地化语言中获取。

省略语言代码并不等同于请求流程的默认本地化:SDK 会固定替换为 en。如果一个流程的默认语言是 de,但该流程存在 en 本地化版本,仍会以 en 渲染;只有在没有 en 本地化版本时,才会回退到 de

Warning

请严格按照看板中配置的格式传递语言代码——使用连字符分隔的小写子标签。不要直接传递平台原生的语言标识符:在 Android 上,Locale.getDefault().toLanguageTag() 返回 pt-BR;在 iOS 上,NSLocale.currentLocale.localeIdentifier 返回 pt_BR。两者都会回退到默认本地化。请在应用中转换该值后再传递。

用户引导

用户引导在服务器端进行本地化,服务器规则支持多种格式。当你向 getOnboarding 传入 locale 时:

  1. locale 字符串会被转换为小写,所有下划线(_)替换为连字符(-
  2. Adapty 查找与完整 locale 代码完全匹配的本地化版本
  3. 若未找到匹配项,Adapty 截取第一个连字符前的子字符串(如 pt-brpt),并查找匹配的本地化版本
  4. 若仍未找到匹配项,Adapty 返回该用户引导默认语言的内容

这样,pt_BRpt-BRpt-br 都能解析到同一个用户引导的本地化内容。

实现本地化

在 SDK v4 中,获取流程时无需传递语言区域代码——getFlow 会返回包含所有本地化内容的流程,Adapty 在构建流程视图时会自动应用相应的本地化。

  • 在编辑工具中构建的流程:SDK 不会读取设备语言区域,因此需要在应用中自行解析,并通过 createFlowViewlocale 参数传入。该参数为可选项——省略时,流程将以 en 渲染;若流程没有 en 本地化版本,则使用其默认语言区域

    import com.adapty.kmp.AdaptyUI
    
    AdaptyUI.createFlowView(flow = flow, locale = "es")
        .onSuccess { view ->
            view.present()
        }
        .onError { error ->
            // handle the error
        }

createNativeFlowViewAdaptyUIFlowPlatformView 可组合项接受相同的可选 locale 参数。view.locale 报告视图构建时所使用的本地化语言。locale 参数和 view.locale 均需要 Kotlin Multiplatform SDK 4.0.1-beta.1 或更高版本。

  • 自定义(远程配置)付费墙getFlowflow.remoteConfigs 中返回所有已配置的本地化语言。每个条目都是一个 AdaptyRemoteConfig,包含 locale 代码和 dataMap。请选择与用户匹配的条目,并自行设置回退逻辑:

Adapty.getFlow("YOUR_PLACEMENT_ID")
    .onSuccess { flow ->
        val config = flow.remoteConfigs.firstOrNull { it.locale == "en" }
            ?: flow.remoteConfigs.firstOrNull()
        // read your values from config?.dataMap
    }
    .onError { error ->
        // handle the error
    }

Adapty 以 Adapty 的语言区域代码标准 中描述的格式存储这些 locale 代码。SDK 不会自动将远程配置与语言区域进行匹配,因此应用应自行决定使用哪条配置。

为什么这很重要

在某些场景下,语言区域代码会发挥关键作用——例如,当你需要根据应用的当前本地化语言获取对应的付费墙时。

由于语言区域代码较为复杂,且在不同平台之间可能存在差异,我们在所有支持的平台上统一采用内部标准。也正因为这些代码较为复杂,你需要清楚地了解自己向服务器发送的内容,以及后续的处理逻辑——这样才能始终获取到预期的本地化结果。

Adapty 的语言代码标准

Adapty 的语言代码采用经过轻微修改的 BCP 47 标准:每个代码由小写子标签组成,以连字符分隔。示例:en(英语)、pt-br(葡萄牙语(巴西))、zh(简体中文)、zh-hant(繁体中文)。

语言区域代码匹配

当 Adapty 从客户端 SDK 接收到包含语言区域代码的请求,并开始为付费墙查找对应的本地化内容时,会依次执行以下步骤:

  1. 将传入的语言区域字符串转换为小写,并将所有下划线(_)替换为连字符(-
  2. 查找与完整语言区域代码完全匹配的本地化内容
  3. 如果未找到匹配项,则截取第一个连字符之前的子字符串(例如 pt-brpt),并再次查找匹配的本地化内容
  4. 如果仍未找到匹配项,则返回该付费墙默认语言区域的内容

这样,发送了 'pt_BR' 的 iOS 设备、发送了 pt-BR 的 Android 设备,以及发送了 pt-br 的另一台设备,都会得到相同的结果。

如果你在考虑本地化的问题,很可能你的项目中已经在处理本地化字符串资源了。如果是这样,我们建议在每个对应语言的资源文件中,添加一个键值对,将 Adapty 的语言区域代码作为其值。然后在调用 SDK 时,提取该键对应的值,示例如下:

// 1. 将 Adapty 语言代码添加到你的 Compose Multiplatform 资源中

/*
composeResources/values/strings.xml(默认 — 英语)
*/
<string name="adapty_paywalls_locale">en</string>

/*
composeResources/values-es/strings.xml(西班牙语)
*/
<string name="adapty_paywalls_locale">es</string>

/*
composeResources/values-pt-rBR/strings.xml(葡萄牙语 — 巴西)
*/
<string name="adapty_paywalls_locale">pt-br</string>

// 2. 提取并使用语言代码

suspend fun fetchPaywall() {
    val locale = getString(Res.string.adapty_paywalls_locale)
    Adapty.getPaywall(
        placementId = "YOUR_PLACEMENT_ID",
        locale = locale
    ).onSuccess { paywall ->
        // 请求到的付费墙
    }.onError { error ->
        // 处理错误
    }
}

这样,您就能完全掌控应用中每位用户获取到的本地化内容。

如果您没有使用 Compose Multiplatform 资源,同样的思路也适用于您所使用的任何本地化库(例如 moko-resources)——将 Adapty 语言区域代码作为字符串存储在每个语言包的资源文件中,并在调用 SDK 前读取该值。

实现本地化:另一种方式

你可以不为每个本地化显式定义语言区域代码,同样能达到类似(但不完全相同)的效果。这种方式是直接从设备上提取语言区域代码——由于 commonMain 中没有共享的语言区域 API,因此需要用到 expect/actual 声明:

// commonMain
expect fun currentLocaleTag(): String

// androidMain
actual fun currentLocaleTag(): String = Locale.getDefault().toLanguageTag()

// iosMain
actual fun currentLocaleTag(): String = NSLocale.currentLocale.localeIdentifier

// commonMain — pass the locale code to Adapty

suspend fun fetchPaywall() {
    Adapty.getPaywall(
        placementId = "YOUR_PLACEMENT_ID",
        locale = currentLocaleTag()
    ).onSuccess { paywall ->
        // the requested paywall
    }.onError { error ->
        // handle the error
    }
}

请注意,由于以下几个原因,我们不建议使用此方法:

  1. 在 iOS 上,用户的首选语言与设备的地区语言环境并不相同。NSLocale.currentLocale.localeIdentifier 返回的是地区语言环境,可能与用户实际阅读应用时使用的语言不一致。使用本地化字符串文件的 iOS 应用依赖 Apple 的解析逻辑来综合两者——这在上述推荐方案中可以开箱即用。
  2. 很难预测设备会返回什么,以及它是否与 Adapty 的某个本地化配置匹配。设备语言环境可能包含你未在 Adapty 中配置的扩展或地区代码,在这种情况下,SDK 会回退到第一个子标签匹配,最终回退到 en。 如果你仍然决定采用这种方式,请确保覆盖了所有相关的使用场景。