国际化ICU4J 适配及SDK设计

背景

作为前10的出海项目,公司本地化团队最近要求做产品的国际化适配

我的第一反应是多语言适配,出海的10余款产品中,除了UI样式根据国家审美标准有不同的实现外,就只有多语言了,再多一点的话就是阿拉伯地区的适配他们的文字是从右向左的,习惯也是。

image.png (图片来源网络) 左边是非阿语布局

对于已经做的滚瓜烂熟的操作显然不是他们此次的需求。

一、国际化调研

网上的资料,提到国际化基本都是多语言适配和阿语布局适配,然而国际化适配除了这两项操作外还需要支持CLDR和ICU库,当我查阅官网发现,Android 这边,系统API已经做了两个库的集成操作, 已经利用ICU、CLDR提供Unicode 和其他国际化支持,

比如:

  1. 数字分割符,在德语上是"."分割,中文是","
de:111.111.111
zh:111,111,111
复制代码
  1. 日期:
zh:2022年8月12号
es: 2022/8/12
ar: أ 10 غسطس 2022
复制代码
  1. 缩写
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库,可能会增加包体积;

解决:

  1. 如果使用引入ICU库,后续系统版本迭代,6.0及以下系统用户减少,迭代版本支持开放ICU的场景会逐渐增多,夸张点可能全部开放,则需要移除引用;
  2. 如果使用系统7.0开放的ICU库,则不能兼容6.0以下版本

ICU4J API 地址 www.apiref.com/android-zh/…

二、实现

2.1 i18n (国际化缩写) 工具类设计

(代码处处是设计啊)

综合考虑,将设计一套兼容SDK,主旨就一句话:让应用使用和国际化 API 分开,大家采用面向抽象编程,具体实现由SDK分版本实现

小小设计,但是事情办的妥妥的昂

image.png

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 维基百科就是这么处理的

image.png

猜你喜欢

转载自juejin.im/post/7130872374617964580