/MLang

Android 动态化多语言框架,支持语言包的动态下发、升级、删除,一处安装,到处使用

Primary LanguageJavaApache License 2.0Apache-2.0

MLang 动态化多语言框架

MLang 是 MultiLanguage 的简写,是一款动态化的多语言框架。

设计优雅

  • 语言包存储格式为 xml 格式,和 res 下的 strings.xml 一致
  • 零依赖,完全使用系统 api 和系统的 xml 解析器
  • 不持有 context,无内存泄漏
  • 静态方法 + 单例模式,一处安装,到处使用

动态化语言包

  • 动态下发语言包
  • 语言包的增加、升级、删除
  • 语言包内部任意字符串的增加、升级、删除
  • 自定义语言包的存储路径

完全兼容

  • 跟随系统语言
  • 时间格式跟随系统的 24 小时制
  • 处理各种语言的时区、时间格式化问题
  • 处理各种语言的复数格式化问题
  • 处理各种语言的阅读顺序问题(从左到右、从右到左)

1. 使用

使用字符串

// 本地和云端都存在的字符串
MyLang.getString("local_string", R.string.local_string)

// 云端存在 remote_string_only
// 但本地没有 R.string.remote_string_only,用 R.string.fallback_string 代替
MyLang.getString("remote_string_only", R.string.fallback_string)

使用语言包(语言包文件是 xml 格式,和 res 下的 strings.xml 一样。)

//应用一种语言(这里自动处理了语言包的升级、语言包内部字符串的升级)
MyLang.getInstance().applyLanguage(Context, LocaleInfo, force=true, init=false);

//删除一种语言
MyLang.getInstance().deleteLanguage(Context, LocaleInfo);

LocaleInfo 可以在以下地方找到

//1. 所有云端的语言包
MyLang.getInstance().remoteLanguages

//2. 所有下载到本地、可用的语言包
MyLang.getInstance().languages

//3. 所有非官方的语言包
MyLang.getInstance().unofficialLanguages

//4. 除内置支持的语言外,另外安装的云端的语言包
MyLang.getInstance().otherLanguages

2. 安装

2.1. 引入

//build.gradle
allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://github.com/LinXueyuanStdio/MLang/raw/main/dist/" }
    }
}

//app/build.gradle
implementation 'com.timecat.component:MLang:2.0.2'

2.2. 在 Application 中初始化,并监听系统语言的更改(如果跟随系统语言的话):

public class MyApplication extends Application {
    @SuppressLint("StaticFieldLeak")
    public static volatile Context applicationContext;
    public static volatile Handler applicationHandler;

    @Override
    public void onCreate() {
        super.onCreate();
        applicationContext = this;
        applicationHandler = new Handler(applicationContext.getMainLooper());
        MyLang.init(applicationContext);
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        MyLang.onConfigurationChanged(newConfig);
    }
}

其中建议自己新建一个静态类 MyLang 来代理 MLang。 这样有两个好处:

  1. 隔绝 MLang 的 api 变化,提高兼容性和稳定性。
  2. 使用更简洁。MLang 不持有 context,但每次获取字符串为空时,需要 context 来兜底,获取本地的字符串。在自己的 MyLang 默认提供 application Context,可以不用到处提供 context,更简洁。
public class MyLang {
    private static File filesDir;
    private static LangAction action;
    public static void init(@NonNull Context applicationContext) {
        filesDir = applicationContext.getCacheDir();
        action = new MyLangAction();
        getInstance();
    }
    public static MLang getInstance() {
        return MLang.getInstance(MyApplication.applicationContext, filesDir, action);
    }
    public static void onConfigurationChanged(@NonNull Configuration newConfig) {
        getInstance().onDeviceConfigurationChange(getContext(), newConfig);
    }
}

3. 设计

3.1. 单例模式接收 3 个参数,context,fileDir,action

  1. context:MLang 内部不持有该 context。该 context 用于注册时区广播(根据时区来格式化字符串中的时间)、 判断系统当前时间是否 24 小时制等等。
  2. filesDir:持久化语言包文件的存储地址。语言包文件是 xml 格式,和 res 下的 strings.xml 一样。
  3. action:action 包含了应用语言包、切换语言等等需要的所有回调,即 LangAction 接口。
MLang.getInstance(context, filesDir, action);

3.2. LangAction 接口定义了 2 个东西

  1. 当前语言的设置存储。 MLang 根据语言 id (string) 来识别当前语言。语言 id 需要持久化。 所以设计了下面两个方法,可以自行决定持久化的方式(SharedPreferences、MMKV、SQLite等等)。
    void saveLanguageKeyInLocal(String language);
    @Nullable String loadLanguageKeyInLocal();
  2. 必要的网络接口。
    void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback)
    void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback)
    void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback)

LangAction 的注释如下:

public interface LangAction {
    /**
     * SharedPreferences preferences = Utilities.getGlobalMainSettings();
     * SharedPreferences.Editor editor = preferences.edit();
     * editor.putString("language", language);
     * editor.commit();
     * @param language localeInfo.getKey() 语言 id
     */
    void saveLanguageKeyInLocal(String language);

    /**
     * SharedPreferences preferences = Utilities.getGlobalMainSettings();
     * String lang = preferences.getString("language", null);
     * @return @Nullable lang 语言 id
     */
    String loadLanguageKeyInLocal();

    /**
     * 在其他线程网络请求,在主线程或UI线程调用callback
     * 这里设计成这样,是因为这个方法里支持异步执行
     * 您需要在合适的时机手动调用 callback,且只能调用一次
     * @param lang_pack 语言包名字
     * @param lang_code 语言包版本名称
     * @param from_version 语言包版本号
     * @param callback @NonNull 在主线程或UI线程调用
     */
    void langpack_getDifference(String lang_pack, String lang_code, int from_version, GetDifferenceCallback callback);

    /**
     * 在其他线程网络请求,在主线程或UI线程调用callback
     * 这里设计成这样,是因为这个方法里支持异步执行
     * 您需要在合适的时机手动调用 callback,且只能调用一次
     * @param callback @NonNull 在主线程或UI线程调用
     */
    void langpack_getLanguages(GetLanguagesCallback callback);

    /**
     * 在其他线程网络请求,在主线程或UI线程调用callback
     * 这里设计成这样,是因为这个方法里支持异步执行
     * 您需要在合适的时机手动调用 callback,且只能调用一次
     * @param lang_code 语言包版本名称
     * @param callback @NonNull 在主线程或UI线程调用
     */
    void langpack_getLangPack(String lang_code, GetLangPackCallback callback);

    interface GetLanguagesCallback {
        /**
         * 必须在UI线程或者主线程调用
         * 所有可用的语言包
         * @param languageList 语言包列表
         */
        void onLoad(List<LangPackLanguage> languageList);
    }

    interface GetDifferenceCallback {
        /**
         * 必须在UI线程或者主线程调用
         * 如果服务端没有实现增量分发的功能,可以用完整的语言包代替
         * @param languageList 增量的语言包
         */
        void onLoad(LangPackDifference languageList);
    }

    interface GetLangPackCallback {
        /**
         * 必须在UI线程或者主线程调用
         * @param languageList 完整的语言包
         */
        void onLoad(LangPackDifference languageList);
    }

}

实现LangAction的一个示例如下:

public class MyLangAction implements LangAction {
   @Override
   public static void saveLanguageKeyInLocal(String language) {
       SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
       SharedPreferences.Editor editor = preferences.edit();
       editor.putString("language", language);
       editor.apply();
   }

   @Override
   @Nullable
   public static String loadLanguageKeyInLocal() {
       SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
       return preferences.getString("language", null);
   }
   @Override
   public void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback) {
       Server.request_langpack_getDifference(lang_pack, lang_code, from_version, new Server.GetDifferenceCallback() {
           @Override
           public void onNext(final LangPackDifference difference) {
               callback.onLoad(difference);
           }
       });
   }

   @Override
   public void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback) {
       Server.request_langpack_getLanguages(new Server.GetLanguagesCallback() {
           @Override
           public void onNext(final List<LangPackLanguage> languageList) {
               callback.onLoad(languageList);
           }
       });
   }

   @Override
   public void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback) {
       Server.request_langpack_getLangPack(lang_code, new Server.GetLangPackCallback() {
           @Override
           public void onNext(final LangPackDifference difference) {
               callback.onLoad(difference);
           }
       });
   }
}

3.3. 服务器语言包的结构

模拟的服务器数据

语言包实体

  • LangPackLanguage(name, version, ...)

语言包的数据

  • LangPackDifference(name, version, List<LangPackString>, ...)
  • LangPackString(key: String, value: String)
public class Server {
    public static LangPackLanguage chineseLanguage() {
        LangPackLanguage langPackLanguage = new LangPackLanguage();
        langPackLanguage.name = "chinese";
        langPackLanguage.native_name = "简体中文";
        langPackLanguage.lang_code = "zh";
        langPackLanguage.base_lang_code = "zh";
        return langPackLanguage;
    }
    public static LangPackDifference chinesePackDifference() {
        LangPackDifference difference = new LangPackDifference();
        difference.lang_code = "zh";
        difference.from_version = 0;
        difference.version = 1;
        difference.strings = chineseStrings();
        return difference;
    }
    public static ArrayList<LangPackString> chineseStrings() {
        ArrayList<LangPackString> list = new ArrayList<>();
        list.add(new LangPackString("LanguageName", "中文简体"));
        list.add(new LangPackString("LanguageNameInEnglish", "Chinese"));
        list.add(new LangPackString("local_string", "中文的云端字符串"));
        list.add(new LangPackString("remote_string_only", "本地缺失,云端存在的字符串"));
        return list;
    }
}

4. 进阶配置

MLang.isRTL = false; //是否从右到左阅读(默认 false)
MLang.is24HourFormat = false; //是否 24 小时制(默认 false)
MLang.USE_CLOUD_STRINGS = true; //是否使用云端字符串(默认 true)