背景
作为前10的出海项目,公司本地化团队最近要求做产品的国际化适配
我的第一反应是多语言适配,出海的10余款产品中,除了UI样式根据国家审美标准有不同的实现外,就只有多语言了,再多一点的话就是阿拉伯地区的适配他们的文字是从右向左的,习惯也是。
(图片来源网络) 左边是非阿语布局
对于已经做的滚瓜烂熟的操作显然不是他们此次的需求。
一、国际化调研
网上的资料,提到国际化基本都是多语言适配和阿语布局适配,然而国际化适配除了这两项操作外还需要支持CLDR和ICU库,当我查阅官网发现,Android 这边,系统API已经做了两个库的集成操作, 已经利用ICU、CLDR提供Unicode 和其他国际化支持,
比如:
- 数字分割符,在德语上是"."分割,中文是","
de:111.111.111
zh:111,111,111
复制代码
- 日期:
zh:2022年8月12号
es: 2022/8/12
ar: أ 10 غسطس 2022
复制代码
- 缩写
zh: 1.6万
es: 1.6W
ar: 1.6 مليار
复制代码
等等很多场景
在Android 这边分为两种情况
1.1 Android 6.0(API 级别 23)及更低版本
1.2 Android 7.0(API 级别 24)及更高版本
简单来讲,Android 在7.0及以上支持了ICU4J, 我们可以通过操作ICU4J来满足我们的需求,在它以下,我们想支持国际化就需要引入ICU库,或者使用系统其他API实现
存在问题:
- Android 7.0级以上版本具备ICU包,后续版本对ICU的支持程度不一样,重点在ICU版本上;
- Android 对于ICU的包的开放是选择性的,可能存在不支持的场景;
- ICU库的使用需要单独引入ICU库,可能会增加包体积;
解决:
- 如果使用引入ICU库,后续系统版本迭代,6.0及以下系统用户减少,迭代版本支持开放ICU的场景会逐渐增多,夸张点可能全部开放,则需要移除引用;
- 如果使用系统7.0开放的ICU库,则不能兼容6.0以下版本
ICU4J API 地址 www.apiref.com/android-zh/…
二、实现
2.1 i18n (国际化缩写) 工具类设计
(代码处处是设计啊)
综合考虑,将设计一套兼容SDK,主旨就一句话:让应用使用和国际化 API 分开,大家采用面向抽象编程,具体实现由SDK分版本实现
小小设计,但是事情办的妥妥的昂
2.2 部分代码
2.2.1. 抽象工厂
abstract class I18nFactory<out T : I18nService> {
abstract fun createI18Api(context: Context): T
}
复制代码
2.2.2. 抽象接口(API,核心就是面向接口编程,不考虑具体实现端)
interface I18nService {
// .....
/**
* 任意自定义排序
* @param source 数据源
* @param sourceComparator 原始数据
* @param targetComparator 目标数据
* @param local 默认Locale.getDefault()
*/
fun <T> sortCustomOrderCollator(
source: MutableList<T>,
sourceComparator: (T) -> String,
targetComparator: (T) -> String,
local: Locale? = null
): List<T>
/**
* 获取泰国佛教日历
*/
fun getThaiBuddhistCalendar(time: Long): String?
//...
/**
* 兼容7.0 以下代码
* @param androidN 7.0 及以上
* @param androidM 7.0 以下
*/
fun compatibleAndroidMMethod(
androidN: (PPI18nService) -> String,
androidM: () -> String
): String {
return ""
}
/**
* 指定时间 月/日 时分
*/
fun getMonthDayHour24MinuteFormatTime(time: Long): String
/**
* KM
*/
fun getKilometerData(location: String, local: Locale? = null): String
/**
* 时间单位
*/
fun createRelativeTimeStringByRelativeTime(relativeTime: Long, local: Locale? = null): String
fun createCommentRelativeTime(serverTime: Long): String?
}
复制代码
2.2.3. 实现(7.0以上 使用ICU4J实现)
@RequiresApi(Build.VERSION_CODES.N)
internal class AndroidNServiceImpl(val context: Context) : I18nService {
private val MINUTE = (60 * 1000L)
private val HOUR = (60 * 60 * 1000L)
private val DAY = (24 * 60 * 60 * 1000L)
//...具体实现省略
}
复制代码
2.2.4. 创建创建ICU4J API 的工厂
internal class I18nAndroidNFactory : I18nFactory<AndroidNServiceImpl>() {
companion object {
fun create(): I18nAndroidNFactory {
return PPI18nAndroidNFactory()
}
}
@RequiresApi(Build.VERSION_CODES.N)
override fun createI18Api(context: Context): AndroidNServiceImpl {
return AndroidNServiceImpl(context)
}
}
复制代码
2.2.5. 写完个版本实现之后,对外暴露API
object I18nUtils {
lateinit var i18nApi: I18nService //各使用端操作此API即可
/**
* 初始化
* @param context 全局context
*/
@JvmStatic
fun init(context: Context) {
i18nApi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
I18nAndroidNFactory.create().createI18Api(context)
} else {
I18nAndroidMFactory.create().createI18Api(context)
}
}
}
复制代码
三、使用
3.1 Application 初始化
I18nUtils.init(this)
复制代码
3.2 使用
I18nUtils.i18nApi.getMonthDayHour24MinuteFormatTime(时间戳)
复制代码
特殊位置还可以这样用
I18nUtils.i18nApi.compatibleAndroidMMethod(
androidN = { api ->
//正常操作
},
androidM = {
//保持优化前逻辑
}
)
复制代码
四、国际化适配过程中遇到的问题
阿语语言中的数字显示问题,
比如: ١٬٦٦٠٬١١٩٬٩٨٥
我们可以用unicode码换算成阿拉伯数字
public static String arabicToDecimal(String number) {
char[] chars = new char[number.length()];
for (int i = 0; i < number.length(); i++) {
char ch = number.charAt(i);
if (ch >= 0x0660 && ch <= 0x0669) {
ch -= 0x0660 - '0';
} else if (ch >= 0x06f0 && ch <= 0x06F9) {
ch -= 0x06f0 - '0';
}
chars[i] = ch;
}
return new String(chars);
}
复制代码
也可以通过设置locale处理
private fun getLocale(local: Locale?): Locale {
var tmpLocal = local ?: Locale.getDefault()
if (tmpLocal.language.startsWith("ar")) {
tmpLocal = Locale("ar-u-nu-latn")// 注意这里
}
return tmpLocal
}
复制代码
phabricator.wikimedia.org/T64725 维基百科就是这么处理的