Sử dụng localization và mã ngôn ngữ trong Kotlin Multiplatform SDK

Tại sao điều này quan trọng

Mã ngôn ngữ (locale code) được dùng khi Adapty chọn bản dịch cho một flow hoặc onboarding, và khi bạn đọc Remote Config cho một paywall tùy chỉnh.

Mã ngôn ngữ khá phức tạp và có thể khác nhau giữa các nền tảng, vì vậy Adapty dựa trên một tiêu chuẩn nội bộ thống nhất cho mọi nền tảng được hỗ trợ. Hiểu rõ tiêu chuẩn đó giúp bạn dự đoán được bản dịch nào người dùng sẽ nhận được.

Tiêu chuẩn mã ngôn ngữ tại Adapty

Đối với mã ngôn ngữ, Adapty sử dụng tiêu chuẩn BCP 47 được chỉnh sửa nhẹ: mỗi mã bao gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số ví dụ: en (tiếng Anh), pt-br (tiếng Bồ Đào Nha (Brazil)), zh (tiếng Trung giản thể), zh-hant (tiếng Trung phồn thể).

Khớp mã ngôn ngữ

Trong SDK v4, flow và onboarding khớp mã ngôn ngữ theo cách khác nhau: flow được bản địa hóa bởi SDK trên thiết bị, còn onboarding được bản địa hóa bởi máy chủ Adapty.

Flow và paywall Paywall Builder

Paywall được xây dựng trong Paywall Builder sẽ được phân phối dưới dạng flow trong SDK v4, vì vậy quy tắc dưới đây áp dụng cho cả hai.

Kết quả khớp là chính xác tuyệt đối. SDK so sánh mã bạn truyền vào với mã bản địa hóa của flow từng ký tự một: không thay đổi chữ hoa/thường, không thay thế dấu gạch dưới (_) bằng dấu gạch ngang (-), và không tự động dùng subtag ngôn ngữ thay thế. Với một flow có bản địa hóa pt-br, chỉ có pt-br mới khớp: pt-BR, pt_BR, và pt-PT đều không khớp.

Khi mã ngôn ngữ không khớp với bất kỳ localization nào, flow sẽ tự động hiển thị theo ngôn ngữ mặc định — SDK không trả về lỗi và cũng không ghi cảnh báo.

Khi mã ngôn ngữ khớp, Adapty sẽ hợp nhất localization đó với localization mặc định: các chuỗi văn bản và tài nguyên mà localization được khớp không định nghĩa sẽ được lấy từ localization mặc định.

Bỏ qua mã ngôn ngữ không đồng nghĩa với việc yêu cầu bản địa hóa mặc định của flow: SDK sẽ thay thế bằng en cố định. Một flow có ngôn ngữ mặc định là de vẫn hiển thị bằng en nếu flow đó có bản địa hóa en, và chỉ dự phòng về de khi không có bản địa hóa en.

Warning

Truyền mã ngôn ngữ chính xác như cấu hình trên dashboard — các subtag viết thường, phân cách bằng dấu gạch ngang. Đừng truyền trực tiếp identifier ngôn ngữ của nền tảng: trên Android, Locale.getDefault().toLanguageTag() trả về pt-BR; trên iOS, NSLocale.currentLocale.localeIdentifier trả về pt_BR. Cả hai đều sẽ fallback về bản ngôn ngữ mặc định. Hãy chuyển đổi giá trị trong ứng dụng của bạn trước khi truyền vào.

Onboardings

Onboarding được bản địa hóa trên server, và các quy tắc server chấp nhận nhiều định dạng khác nhau. Khi bạn truyền locale vào getOnboarding:

  1. Chuỗi locale được chuyển thành chữ thường và tất cả dấu gạch dưới (_) được thay thế bằng dấu gạch ngang (-)
  2. Adapty tìm kiếm bản địa hóa có mã locale khớp hoàn toàn
  3. Nếu không tìm thấy, Adapty lấy chuỗi con trước dấu gạch ngang đầu tiên (pt cho pt-br) và tìm kiếm bản địa hóa khớp
  4. Nếu vẫn không tìm thấy, Adapty trả về nội dung theo ngôn ngữ mặc định của onboarding

Bằng cách này, pt_BR, pt-BR, và pt-br đều phân giải thành cùng một bản địa hóa onboarding.

Triển khai các bản địa hóa

Trong SDK v4, bạn không cần truyền mã ngôn ngữ khi tải một flow — getFlow trả về flow với tất cả các bản địa hóa, và Adapty sẽ áp dụng một bản khi flow view được xây dựng.

  • Flow được xây dựng trong builder: SDK không tự đọc ngôn ngữ của thiết bị, vì vậy hãy xác định ngôn ngữ trong ứng dụng của bạn và truyền nó vào tham số locale của createFlowView. Tham số này là tùy chọn — nếu bỏ qua, flow sẽ hiển thị bằng en, hoặc bằng ngôn ngữ mặc định của nó khi flow không có bản địa hóa en.

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

createNativeFlowView và composable AdaptyUIFlowPlatformView đều nhận tham số locale tùy chọn. view.locale báo cáo localization mà view đã được xây dựng với. Cả tham số localeview.locale đều yêu cầu Kotlin Multiplatform SDK 4.0.1-beta.1 trở lên.

  • Paywall tùy chỉnh (remote config): getFlow trả về tất cả các localization đã được cấu hình trong flow.remoteConfigs. Mỗi mục là một AdaptyRemoteConfig với mã locale và một dataMap. Hãy chọn mục phù hợp với người dùng, kèm theo fallback của riêng bạn:

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 lưu trữ các mã locale đó theo định dạng được mô tả trong Tiêu chuẩn mã locale tại Adapty. SDK không so khớp Remote Config với một locale cụ thể, vì vậy việc áp dụng mục nào là tùy thuộc vào ứng dụng của bạn.

Tại sao điều này quan trọng

Có một số trường hợp mà locale code phát huy vai trò quan trọng — ví dụ, khi bạn cần lấy đúng paywall theo ngôn ngữ hiện tại của ứng dụng.

Vì locale code khá phức tạp và có thể khác nhau giữa các nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ thống nhất cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng bản địa hóa, và điều gì xảy ra tiếp theo — để đảm bảo bạn luôn nhận được kết quả như kỳ vọng.

Tiêu chuẩn mã ngôn ngữ tại Adapty

Đối với mã ngôn ngữ, Adapty sử dụng tiêu chuẩn BCP 47 được chỉnh sửa một chút: mỗi mã gồm các subtag viết thường, ngăn cách nhau bằng dấu gạch ngang. Ví dụ: en (tiếng Anh), pt-br (tiếng Bồ Đào Nha (Brazil)), zh (tiếng Trung giản thể), zh-hant (tiếng Trung phồn thể).

Khớp mã ngôn ngữ

Khi Adapty nhận được lệnh gọi từ SDK phía client với mã ngôn ngữ và bắt đầu tìm kiếm bản dịch tương ứng của một paywall, quá trình diễn ra như sau:

  1. Chuỗi ngôn ngữ đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (_) được thay thế bằng dấu gạch ngang (-)
  2. Tiếp theo, chúng tôi tìm kiếm bản dịch có mã ngôn ngữ khớp hoàn toàn
  3. Nếu không tìm thấy kết quả khớp, chúng tôi lấy chuỗi con trước dấu gạch ngang đầu tiên (pt cho pt-br) và tìm kiếm bản dịch phù hợp
  4. Nếu vẫn không tìm thấy kết quả khớp, chúng tôi trả về nội dung theo ngôn ngữ mặc định của paywall

Bằng cách này, một thiết bị iOS gửi 'pt_BR', một thiết bị Android gửi pt-BR, và một thiết bị khác gửi pt-br đều sẽ nhận được cùng một kết quả.

Nếu bạn đang cân nhắc về bản địa hóa, khả năng cao là bạn đã làm việc với các tệp tài nguyên chuỗi đã được bản địa hóa trong dự án của mình. Nếu vậy, chúng tôi khuyến nghị bạn đặt một cặp key-value với mã locale Adapty tương ứng vào từng tệp tài nguyên cho mỗi bản địa hóa. Sau đó, lấy giá trị của key đó khi gọi SDK của chúng tôi, như sau:

// 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 ->
        // the requested paywall
    }.onError { error ->
        // handle the error
    }
}

Bằng cách đó, bạn có thể đảm bảo rằng mình hoàn toàn kiểm soát được bản dịch nào sẽ được tải về cho từng người dùng trong ứng dụng.

Nếu bạn không sử dụng Compose Multiplatform resources, ý tưởng tương tự cũng áp dụng cho bất kỳ thư viện localization nào bạn đang dùng (ví dụ: moko-resources) — hãy lưu mã ngôn ngữ Adapty dưới dạng chuỗi trong từng resource bundle của mỗi ngôn ngữ, rồi đọc nó trước khi gọi SDK.

Triển khai bản địa hóa: cách khác

Bạn có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống nhau) mà không cần định nghĩa tường minh mã ngôn ngữ cho từng bản địa hóa. Cách này sẽ trích xuất mã ngôn ngữ trực tiếp từ thiết bị — điều này yêu cầu khai báo expect/actual, vì không có API ngôn ngữ dùng chung trong commonMain:

// 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
    }
}

Lưu ý rằng chúng tôi không khuyến nghị cách tiếp cận này vì một số lý do:

  1. Trên iOS, ngôn ngữ ưa thích của người dùng và locale khu vực của thiết bị không giống nhau. NSLocale.currentLocale.localeIdentifier trả về locale khu vực, có thể khác với ngôn ngữ mà người dùng thực sự đọc ứng dụng của bạn. Các ứng dụng iOS sử dụng file chuỗi đã bản địa hóa dựa vào logic phân giải của Apple để kết hợp cả hai — điều này hoạt động tốt ngay từ đầu với cách tiếp cận được khuyến nghị ở trên.
  2. Rất khó dự đoán chính xác thiết bị sẽ trả về gì và liệu nó có khớp với một bản địa hóa trong Adapty hay không. Locale của thiết bị có thể bao gồm các phần mở rộng hoặc mã vùng mà bạn chưa cấu hình trong Adapty, trong trường hợp đó SDK sẽ dự phòng về kết quả khớp với subtag đầu tiên hoặc cuối cùng là về en.

Should you decide to use this approach anyway — make sure you’ve covered all the relevant use cases.