Использование локализаций и кодов локали в Kotlin Multiplatform SDK

Почему это важно

Коды локалей используются, когда Adapty выбирает локализацию для флоу или онбординга, а также когда вы считываете Remote Config для кастомного пейвола.

Коды локалей непросты и могут отличаться от платформы к платформе, поэтому Adapty использует единый внутренний стандарт для всех поддерживаемых платформ. Понимание этого стандарта поможет вам предсказать, какую локализацию получит пользователь.

Стандарт кодов локалей в Adapty

Для кодов локалей Adapty использует немного модифицированный стандарт BCP 47: каждый код состоит из строчных подтегов, разделённых дефисами. Примеры: en (английский), pt-br (португальский (Бразилия)), zh (упрощённый китайский), zh-hant (традиционный китайский).

Сопоставление кодов локали

В SDK v4 флоу и онбординги сопоставляют коды локали по-разному: флоу локализуются SDK на устройстве, онбординги — сервером Adapty.

Флоу и пейволы Paywall Builder

Пейвол, созданный в Paywall Builder, в SDK v4 доставляется как флоу, поэтому правило ниже распространяется на оба случая.

Совпадение точное. SDK сравнивает переданный вами код с кодами локализации флоу посимвольно: регистр не меняется, подчёркивания (_) не заменяются дефисами (-), и откат до языкового тега не выполняется. Для флоу с локализацией pt-br совпадёт только pt-br: pt-BR, pt_BR и pt-PT — нет.

Когда код не соответствует ни одной локализации, флоу молча отображается в локализации по умолчанию — SDK не возвращает ошибку и не записывает предупреждение.

Когда код совпадает, Adapty объединяет найденную локализацию с локализацией по умолчанию: строки и ресурсы, которых нет в найденной локализации, берутся из локализации по умолчанию.

Пропуск кода языка — это не то же самое, что запрос локализации флоу по умолчанию: SDK подставляет фиксированный en. Флоу, у которого в качестве языка по умолчанию задан de, всё равно будет отображаться на en, если у него есть локализация en, и только при её отсутствии вернётся к de.

Warning

Передавайте код локали точно в том виде, в котором он настроен в дашборде — строчные буквы, подтеги разделены дефисами. Не передавайте платформенный идентификатор локали напрямую: на Android Locale.getDefault().toLanguageTag() возвращает pt-BR, на iOS NSLocale.currentLocale.localeIdentifier возвращает pt_BR. В обоих случаях используется локализация по умолчанию. Преобразуйте значение в приложении перед передачей.

Онбординги

Онбординги локализуются на стороне сервера, и серверные правила допускают другие форматы. Когда вы передаёте locale в getOnboarding:

  1. Строка локали приводится к нижнему регистру, а все символы подчёркивания (_) заменяются дефисами (-)
  2. Adapty ищет локализацию с полностью совпадающим кодом локали
  3. Если совпадение не найдено, Adapty берёт подстроку до первого дефиса (pt для pt-br) и ищет соответствующую локализацию
  4. Если совпадение снова не найдено, Adapty возвращает контент для локали онбординга по умолчанию

Таким образом, pt_BR, pt-BR и pt-br — все они указывают на одну и ту же локализацию онбординга.

Реализация локализаций

В SDK v4 не нужно передавать код локали при получении флоу — getFlow возвращает флоу со всеми его локализациями, а Adapty применяет нужную при создании представления флоу.

  • Флоу, созданные в конструкторе: SDK не читает локаль устройства, поэтому определите её в своём приложении и передайте как параметр locale в createFlowView. Параметр необязателен — если его не указать, флоу отобразится на en или на локали по умолчанию, если у флоу нет локализации en.

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

createNativeFlowView и составной элемент AdaptyUIFlowPlatformView принимают одинаковый необязательный параметр locale. view.locale возвращает локализацию, с которой было создано представление. Для параметра locale и view.locale требуется Kotlin Multiplatform SDK версии 4.0.1-beta.1 или выше.

  • Кастомные пейволы (Remote Config): getFlow возвращает все настроенные локализации в flow.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 хранит коды locale в формате, описанном в разделе Стандарт кода локали в Adapty. SDK не сопоставляет Remote Config с локалью автоматически — какую запись применять, решает само приложение.

Почему это важно

Есть несколько ситуаций, когда коды локали имеют значение — например, когда вы запрашиваете правильный пейвол для текущей локализации приложения.

Поскольку коды локали бывают сложными и могут различаться в зависимости от платформы, мы используем внутренний стандарт для всех поддерживаемых платформ. Именно из-за этой сложности важно понимать, что именно вы отправляете на наш сервер для получения правильной локализации и что происходит дальше — чтобы всегда получать ожидаемый результат.

Стандарт кодов локалей в Adapty

Для кодов локалей Adapty использует слегка изменённый стандарт BCP 47: каждый код состоит из подтегов в нижнем регистре, разделённых дефисами. Несколько примеров: en (английский), pt-br (португальский (Бразилия)), zh (упрощённый китайский), zh-hant (традиционный китайский).

Сопоставление кода локали

Когда Adapty получает вызов от клиентского SDK с кодом локали и начинает искать соответствующую локализацию пейвола, происходит следующее:

  1. Входящая строка локали приводится к нижнему регистру, а все символы подчёркивания (_) заменяются дефисами (-)
  2. Затем мы ищем локализацию с полностью совпадающим кодом локали
  3. Если совпадение не найдено, берётся подстрока до первого дефиса (pt для pt-br) и выполняется поиск совпадающей локализации
  4. Если совпадение снова не найдено, возвращается контент для локали пейвола по умолчанию

Таким образом, устройство iOS, отправившее 'pt_BR', устройство Android, отправившее pt-BR, и другое устройство, отправившее pt-br, получат одинаковый результат.

Если вы думаете о локализациях, скорее всего, вы уже работаете с локализованными строковыми ресурсами в своём проекте. В таком случае мы рекомендуем добавить пару ключ-значение с нужным кодом локали Adapty в каждый из ваших файлов ресурсов для соответствующих локализаций. А затем извлекать значение по этому ключу при вызове нашего SDK, например так:

// 1. Add the Adapty locale code to your Compose Multiplatform resources

/*
composeResources/values/strings.xml (default — English)
*/
<string name="adapty_paywalls_locale">en</string>

/*
composeResources/values-es/strings.xml (Spanish)
*/
<string name="adapty_paywalls_locale">es</string>

/*
composeResources/values-pt-rBR/strings.xml (Portuguese — Brazil)
*/
<string name="adapty_paywalls_locale">pt-br</string>

// 2. Extract and use the locale code

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.

Реализация локализаций: альтернативный подход

Можно получить похожий (но не идентичный) результат, не указывая явно коды локалей для каждой локализации. Для этого нужно извлекать код локали напрямую с устройства — но это потребует объявлений expect/actual, поскольку в commonMain нет общего API для работы с локалями:

// 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. Should you decide to use this approach anyway — make sure you’ve covered all the relevant use cases.