Android 国际化之多语言适配小记

11,063 阅读7分钟

害,乱糟糟,总要去梳理.

面对未知的一切,陌生感突突的.

甲方要求实现 App 国际化多语言,正好抽个时间弄了下,害,被自己蠢到死,特意记录下.

如有不对,欢迎指正,一起交流~

效果演示

视频录制的不是太好,整体的效果出来了,大家见谅~

版本为别为: 6.0、8.0 以及 10.0

搞起来

简单说下需要注意的:

  • 国际化,多语言目录创建,资源配置;
  • Locale 资源获取以及本地缓存,缓存的目的是为了下次重新打开 App 依然是上次选择的语言;
  • Android 系统间不同的差异,例如 7.0 后不再是唯一默认语言,而是多种语言配置,具体差别如下所示:

好啦,直接上码~

网上看到大家再讨论这个 androidx 包下 appcompat 问题,这里也把我使用的版本贴出来:

  • implementation 'androidx.appcompat:appcompat:1.2.0'

一、创建对应的资源文件

方式有两种.如下:

  • 方式一:

右键 「res」,选择 「New」,「Android Resource File」:

按如下图进行选择配置语言表:

  • 方式二:

Android Studio 左侧选择「Resource Manager」,随后选择小地图 + 的标志,最后在列表中选择对应兼容的国家即可.

随后会为我们创建选择的国家的 values 目录以及 strings 文件,如下所示:

好了,到现在,基本的语言目录以及文件都已经创建好了,剩下的就是会有专人负责提供对应的翻译词.

当然,我司一贯的原则是,自己动手,丰衣足食.

提供了部分常用的、不错的在线翻译地址,如下:

二、贴心附上过程中使用的 MMKV Utils

记得去引用 MMKV 依赖以及初始化,地址如下:

个人使用的版本如下:

  • implementation 'com.tencent:mmkv:1.0.17'
/**
 * @author:HLQ_Struggle
 * @date:2020/4/13
 * @desc:基础数据缓存
 */

class MMKVPro<T>(
    private val mmkv: MMKV,
    private val key: String,
    private val defValue: T
) {

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        // 本地加密存储并支持多进程访问
        return mmkv.run {
            when (defValue) {
                is String -> getString(key, defValue)
                is Boolean -> getBoolean(key, defValue)
                is Long -> getLong(key, defValue)
                is Int -> getInt(key, defValue)
                is Float -> getFloat(key, defValue)
                else -> Unit
            }
        } as T
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        return mmkv.run {
            when (value) {
                is String -> putString(key, value)
                is Boolean -> putBoolean(key, value)
                is Long -> putLong(key, value)
                is Int -> putInt(key, value)
                is Float -> putFloat(key, value)
                else -> Unit
            }
        }
    }

}

/**
 * 移除 key
 */
fun removeKey(key: String) {
    MMKV.mmkvWithID(F_APP_CACHE, MMKV.MULTI_PROCESS_MODE, K_ENCRYPT).run {
        remove(key)
    }
}

三、准备多语言 utils

/**
 * @author HLQ_Struggle
 * @date 2021/02/26
 * @desc
 */

/**
 * Activity 更新语言资源
 */
fun getAttachBaseContext(context: Context): Context {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return setAppLanguageApi24(context)
    } else {
        setAppLanguage(context)
    }
    return context
}

/**
 * 设置应用语言
 */
@Suppress("DEPRECATION")
fun setAppLanguage(context: Context) {
    val resources = context.resources
    val displayMetrics = resources.displayMetrics
    val configuration = resources.configuration
    // 获取当前系统语言,默认设置跟随系统
    val locale = getAppLocale()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        configuration.setLocale(locale);
    } else {
        configuration.locale = locale;
    }
    resources.updateConfiguration(configuration, displayMetrics)
}

/**
 * 兼容 7.0 及以上
 */
@TargetApi(Build.VERSION_CODES.N)
private fun setAppLanguageApi24(context: Context): Context {
    val locale = getAppLocale()
    val resource = context.resources
    val configuration = resource.configuration
    configuration.setLocale(locale)
    configuration.setLocales(LocaleList(locale))
    return context.createConfigurationContext(configuration)
}

/**
 * 获取 App 当前语言
 */
private fun getAppLocale() = when (LocalDataStorage().multilingual) {
    0 -> { // 跟随系统
        getSystemLocale()
    }
    1 -> { // 中文
        Locale.CHINA
    }
    2 -> { // 英文
        Locale.ENGLISH
    }
    else -> Locale.ENGLISH
}

/**
 * 获取当前系统语言,如未包含则默认英文
 */
private fun getSystemLocale(): Locale {
    val systemLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)
    } else {
        Locale.getDefault()
    }
    return when (systemLocale.language) {
        Locale.CHINA.language -> {
            Locale.CHINA
        }
        Locale.ENGLISH.language -> {
            Locale.ENGLISH
        }
        else -> {
            Locale.ENGLISH
        }
    }
}

四、在选择多语言页面进行处理

当然这里我的思路是,本地缓存语言列表索引,然后后续根据 id 直接获取对应的语言即可.

点击确认时,进行缓存当前选择的

override fun onClick(v: View?) {
    when (v?.id) {
        R.id.tvDone -> {
            // 更新选择状态
            LocalDataStorage().multilingual = mAfterPosition
            setAppLanguage(this)
            reStartActivity()
        }
    }
}

private fun reStartActivity() {
    val intent = Intent(mSelfActivity, MainActivity::class.java)
    intent.flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
    startActivity(intent)
    // 取消其专场动画
    overridePendingTransition(0, 0)
}

五、Application 中 Configuration 处理

override fun onConfigurationChanged(newConfig: Configuration?) {
    super.onConfigurationChanged(newConfig)
    // ...
    setAppLanguage(this)
}

六、BaseActivity 处理

由于需要重建 Activity 去处理对应资源,所以这里个人是把它放在 BaseActivity 中去处理:

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(newBase?.let { getAttachBaseContext(it) })
}

七、优化项,资源文件更新

大家千万记得更新这个,如果做过 Apk 大小优化,八成都会限制 resConfigs 内容,避免打包时多处一些无用内容增加 Apk 大小. 大家千万记得更新这个,如果做过 Apk 大小优化,八成都会限制 resConfigs 内容,避免打包时多处一些无用内容增加 Apk 大小. 大家千万记得更新这个,如果做过 Apk 大小优化,八成都会限制 resConfigs 内容,避免打包时多处一些无用内容增加 Apk 大小.

我就是写完之后,怎么也不出效果,后来一看,好家伙,限制只有中文.当时的尴尬、无奈...

resConfigs "zh-rCN", "en"

好了,到此结束,当然,Android 不得不面对的多机型适配...

这里后续遇到在更新把~

多语言遇到的一些问题

1. 布局问题

这个的确让人蛮头疼的,尤其对于我们基建不完整的情况,能做的只能说是保证大部分的效果,尽量使用短称英文或者非中文.

同时这个也提醒我,如何在开发的过程中尽可能兼容后续呢?

可能也是经验把,慢慢努力.

2.TabLayout 英文模式下大写

切换后效果如下:

目前使用的 TabLayout 版本如下:

  • implementation 'com.google.android.material:material:1.2.1'

喏,设置个样式就好:

<style name="TabLayoutTextStyle" parent="TextAppearance.Design.Tab">
    <item name="android:textSize">@dimen/sp_18</item>
    <item name="textAllCaps">false</item>
</style>

后续遇到再补充吧.

2023.4.28 爬上来更新一波

最佳的公司开始搞海外,难免又开始国际化的支持,针对之前的方案,每个项目拆分归类有差别,这次遇到了几个小问题,继续更新一波~

我们目前仅支持英文和印尼,而印尼大多数文章都是需要写出如下:

  • value-id
  • drawable-id
  • ...

而实际获取设备国家语言则是 in,XDM 一定要自己先挨个获取下...

首先放出基础工具类:

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.os.LocaleList 
import java.util.Locale

/**
 * 多语言切换 Utils
 *
 * @author HeLiquan
 * @version 1.0.0
 * @date 2023/4/20 星期四
 * @see xxx
 * @since 1.0.0
 */
object MultiLanguageHelper {

    /**
     * Activity 更新语言资源
     */
    fun modifyContextLanguageConfig(context: Context): Context {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            setAppLanguageApi24(context)
        } else {
            setAppLanguage(context)
        }
        return context
    }

    /**
     * 设置应用语言
     */
    @SuppressLint("ObsoleteSdkInt")
    @Suppress("DEPRECATION")
    fun setAppLanguage(context: Context) {
        val resources = context.resources
        val displayMetrics = resources.displayMetrics
        val configuration = resources.configuration
        val locale = getAppLocalLanguage()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(locale)
        } else {
            configuration.locale = locale
        }
        resources.updateConfiguration(configuration, displayMetrics)
    }

    /**
     * 兼容 7.0 及以上
     */
    @TargetApi(Build.VERSION_CODES.N)
    private fun setAppLanguageApi24(context: Context): Context {
        val locale = getAppLocalLanguage()
        val resource = context.resources
        val configuration = resource.configuration
        configuration.setLocale(locale)
        configuration.setLocales(LocaleList(locale))
        return context.createConfigurationContext(configuration)
    }

    /**
     * 获取当前系统语言,如未包含则默认英文
     */
    fun getSystemLocale(): Locale {
        val local = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)
        } else {
            Locale.getDefault()
        }
        return when (local.language) {
            LANGUAGE_IN -> Locale(LANGUAGE_IN, COUNTRY_ID)
            else -> Locale.ENGLISH
        }
    }

    /**
     * 获取 App 当前语言 默认英语
     */
    fun getAppLocalLanguage(): Locale = when (getLocalLanguageConfig()) {
        LANGUAGE_IN -> Locale(LANGUAGE_IN, COUNTRY_ID)
        "" -> getSystemLocale()
        else -> Locale.ENGLISH
    }

    /**
     * 获取系统或者 App 本地语言
     */
    private fun getSystemAppLocalLanguage(): String? {
        val localLanguageConfig = getLocalLanguageConfig()
        val language = if (localLanguageConfig.isNullOrBlank()) {
            getSystemLocale().language
        } else {
            localLanguageConfig
        }
        return language
    }

    /**
     * 获取当前 App 语言
     */
    fun getAppCurrentLanguage(): String = when (getSystemAppLocalLanguage()) {
        LANGUAGE_IN -> LANGUAGE_NAME_IN
        else -> LANGUAGE_NAME_EN
    }

    /**
     * 获取共参 language
     */
    fun getPublicParamsLanguage(): String = when (val language = getSystemAppLocalLanguage()) {
        LANGUAGE_IN -> LANGUAGE_IN_ID
        else -> language ?: ""
    }

}

随后就是基本的常量定义:

/**
 * 语言常量类
 *
 * @author HeLiquan
 * @version 1.0.0
 * @date 2023/4/20 星期四
 * @see xxx
 * @since 1.0.0
 */

/**
 * 本地语言
 */
const val KEY_LOCAL_LANGUAGE = "local_language"

/**
 * 语言 - 印尼
 */
const val LANGUAGE_IN = "in"

/**
 * 语言 - 印尼 兼容 id
 */
const val LANGUAGE_IN_ID = "id"

/**
 * 语言 - 英语
 */
const val LANGUAGE_EN = "en"

/**
 * 语言国家 - 印尼
 */
const val COUNTRY_ID = "ID"

/**
 * 语言名字 英语 这里根据自己项目定义
 */
const val LANGUAGE_NAME_EN = "English"

/**
 * 语言名字 印尼 这里根据自己项目定义
 */
const val LANGUAGE_NAME_IN = "Bahasa Indo"

本地存储我们项目用的 MMKV,这里也简单给出一下:

/**
 * MMKV Helper
 *
 * @author HeLiquan
 * @version 1.0.0
 * @date 2023/4/28 星期五
 * @see xxx
 * @since 1.0.0
 */
object MMKVHelper {

    /**
     * 设置本地语言配置
     */
    fun setLocalLanguageConfig(language: String?) {
        KeyValue.put(KEY_LOCAL_LANGUAGE, language)
    }

    /**
     * 获取本地语言配置
     */
    fun getLocalLanguageConfig(): String? {
        return try {
            val language = KeyValue.getString(KEY_LOCAL_LANGUAGE)
            if (TextUtils.isEmpty(language)) {
                ""
            } else language
        } catch (e: Exception) {
            ""
        }
    }

}

BaseActivity 的修改:

/**
 * 修改基本配置
 *
 * @param newBase The new base context for this wrapper.
 */
@Override
protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(MultiLanguageHelper.INSTANCE.modifyContextLanguageConfig(newBase));
}

BaseApplication 修改:

/**
 * 修改基础配置信息
 *
 * @param base The new base context for this wrapper.
 */
@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(MultiLanguageHelper.INSTANCE.modifyContextLanguageConfig(base));
}

/**
 * 系统资源配置发生变化
 *
 * @param newConfig The new device configuration.
 */
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    MultiLanguageHelper.INSTANCE.modifyContextLanguageConfig(this);
}

/**
 * 偷天换日
 *
 * @return
 */
@Override
public Resources getResources() {
    Resources resources = super.getResources();
    Configuration configuration = new Configuration();
    configuration.setLocale(MultiLanguageHelper.INSTANCE.getAppLocalLanguage());
    resources.updateConfiguration(configuration, resources.getDisplayMetrics());
    return resources;
}

这里额外说明下,因为我们项目中获取资源的方式有两种:

  • 通过 Activity 的 context
  • 通过 Application 的 context

所以,仅仅修改 Activity 中的 context 并无卵用,实际还是需要替换 getResoutce() 才管用。

最后截取一张项目资源截图:

image.png

参考资料