Sử dụng localizations và locale codes trong Flutter 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 tùy nền tảng, vì vậy Adapty sử dụng một tiêu chuẩn nội bộ thống nhất trên 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 một phiên bản điều chỉnh nhỏ của tiêu chuẩn BCP 47: 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, các 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

Một paywall được xây dựng trong Paywall Builder được chuyển phát 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ã localization của flow theo từng ký tự một: không thay đổi kiểu chữ, không thay thế dấu gạch dưới (_) bằng dấu gạch ngang (-), và không dự phòng về language subtag. Với một flow có localization pt-br, chỉ có pt-br khớp: pt-BR, pt_BR, và pt-PT đều không khớp.

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

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

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

Truyền mã ngôn ngữ chính xác như cách nó được cấu hình trong dashboard — các subtag viết thường được phân tách bằng dấu gạch ngang. Đừng truyền trực tiếp định danh locale của hệ thống: Platform.localeName trả về pt_BRPlatformDispatcher.instance.locale.toLanguageTag() trả về pt-BR, cả hai đều sẽ fallback về bản địa hóa 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à 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 bằng dấu gạch ngang (-)
  2. Adapty tì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 phần chuỗi trước dấu gạch ngang đầu tiên (pt trong pt-br) và tìm bản địa hóa khớp với chuỗi đó
  4. Nếu vẫn không tìm thấy, Adapty trả về nội dung theo locale mặc định của onboarding

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

Triển khai 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 cùng với tất cả các bản địa hóa của nó, và Adapty sẽ áp dụng một bản khi flow view được xây dựng. Tham số locale của getFlowgetFlowForDefaultAudience không có tác dụng đối với các flow; tham số này đã bị deprecated và sẽ ghi ra cảnh báo.

  • Flow được tạo trong builder: SDK không đọc ngôn ngữ 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 vào dưới dạng đối số locale của createFlowView hoặc AdaptyUIFlowPlatformView. Đối số này là tùy chọn — bỏ qua nó và flow sẽ hiển thị bằng en, hoặc bằng ngôn ngữ mặc định của flow khi flow không có bản dịch en.

AdaptyUIFlowView.locale báo cáo localization mà view được xây dựng với. Nó yêu cầu Flutter SDK 4.0.3 với các bản phát hành native iOS 4.0.2 và Android 4.0.1, và là null với các native SDK cũ hơn.

  • Paywall tùy chỉnh (remote config): getFlow trả về mọi localization đã được cấu hình trong flow.remoteConfigs. Mỗi mục có mã locale và nội dung config (chuỗi data, hoặc dictionary đã được parse). Chọn mục phù hợp với người dùng, với cơ chế fallback của riêng bạn:

final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID');
final config = flow.remoteConfigs.firstWhereOrNull((c) => c.locale == 'en') ??
    flow.remoteConfig; // the first remote config, if present
// read your values from config?.dictionary

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 locale, 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 vài tình huống mà mã ngôn ngữ (locale code) trở nên quan trọng — ví dụ, khi bạn cần lấy đúng paywall phù hợp với ngôn ngữ hiện tại của ứng dụng.

Vì mã ngôn ngữ khá phức tạp và có thể khác nhau tùy từng 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 ngôn ngữ mong muốn — và điều gì xảy ra tiếp theo — để 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 chuẩn BCP 47 được chỉnh sửa nhẹ: mỗi mã bao gồm các subtag viết thường, ngăn cách nhau 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ữ

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 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. Sau đó 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 từ pt-br) và tìm kiếm bản dịch khớ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 tìm hiểu về bản địa hóa, có thể bạn đã làm việc với các file chuỗi đã được bản địa hóa trong dự án của mình. Trong trường hợp đó, chúng tôi khuyến nghị bạn thêm một cặp key-value với mã locale Adapty tương ứng vào mỗi file cho từng bản địa hóa. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như ví dụ dưới đây:

// 1. Modify your app_en.arb, app_es.arb, app_pt_br.arb files

/*
app_en.arb
*/
"adapty_paywalls_locale": "en",

/*
app_es.arb
*/
"adapty_paywalls_locale": "es",

/*
app_pt_br.arb
*/
"adapty_paywalls_locale": "pt-br",

// 2. Extract and use the locale code
final locale = AppLocalizations.of(context)!.adapty_paywalls_locale;
// pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method

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

Triển khai bản địa hóa theo 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) mà không cần định nghĩa rõ ràng mã ngôn ngữ cho từng bản địa hóa. Điều đó có nghĩa là trích xuất mã ngôn ngữ từ các đối tượng khác mà nền tảng của bạn cung cấp, như sau:

final locale = Localizations.localeOf(context).languageCode;
// pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method

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ữ ưu tiên và locale hiện tại không giống nhau. Nếu muốn bản địa hóa được chọn đúng, bạn cần dựa vào logic của Apple — hoạt động tự động nếu bạn dùng cách tiếp cận được khuyến nghị với các file string đã bản địa hóa — hoặc tự tái tạo lại logic đó.
  2. Rất khó dự đoán chính xác những gì server của Adapty sẽ nhận được. Ví dụ: trên iOS, có thể lấy được một locale như ar_OM@numbers='latn' từ thiết bị và gửi lên server. Khi đó, thay vì nhận được bản địa hóa ar-om như mong muốn, bạn sẽ nhận được ar — điều này có thể nằm ngoài kỳ vọng. Should you decide to use this approach anyway — make sure you’ve covered all the relevant use cases.