{"id":21024015,"url":"https://github.com/linxueyuanstdio/mlang","last_synced_at":"2025-05-15T08:33:07.174Z","repository":{"id":50306746,"uuid":"306546871","full_name":"LinXueyuanStdio/MLang","owner":"LinXueyuanStdio","description":"Android 动态化多语言框架，支持语言包的动态下发、升级、删除，一处安装，到处使用","archived":false,"fork":false,"pushed_at":"2023-01-30T06:23:16.000Z","size":930,"stargazers_count":112,"open_issues_count":1,"forks_count":12,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-03T06:51:11.379Z","etag":null,"topics":["android"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LinXueyuanStdio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-10-23T06:21:17.000Z","updated_at":"2025-02-28T16:04:17.000Z","dependencies_parsed_at":"2023-02-16T04:45:23.710Z","dependency_job_id":null,"html_url":"https://github.com/LinXueyuanStdio/MLang","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinXueyuanStdio%2FMLang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinXueyuanStdio%2FMLang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinXueyuanStdio%2FMLang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinXueyuanStdio%2FMLang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LinXueyuanStdio","download_url":"https://codeload.github.com/LinXueyuanStdio/MLang/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254304720,"owners_count":22048451,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android"],"created_at":"2024-11-19T11:22:13.642Z","updated_at":"2025-05-15T08:33:02.994Z","avatar_url":"https://github.com/LinXueyuanStdio.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MLang 动态化多语言框架\n\n`MLang` 是 MultiLanguage 的简写，是一款动态化的多语言框架。\n\n设计优雅\n- [x] 语言包存储格式为 xml 格式，和 res 下的 strings.xml 一致\n- [x] 零依赖，完全使用系统 api 和系统的 xml 解析器\n- [x] 不持有 context，无内存泄漏\n- [x] 静态方法 + 单例模式，一处安装，到处使用\n\n动态化语言包\n- [x] 动态下发语言包\n- [x] 语言包的增加、升级、删除\n- [x] 语言包内部任意字符串的增加、升级、删除\n- [x] 自定义语言包的存储路径\n\n完全兼容\n- [x] 跟随系统语言\n- [x] 时间格式跟随系统的 24 小时制\n- [x] 处理各种语言的时区、时间格式化问题\n- [x] 处理各种语言的复数格式化问题\n- [x] 处理各种语言的阅读顺序问题（从左到右、从右到左）\n\n## 1. 使用\n\n使用字符串\n```java\n// 本地和云端都存在的字符串\nMyLang.getString(\"local_string\", R.string.local_string)\n\n// 云端存在 remote_string_only\n// 但本地没有 R.string.remote_string_only，用 R.string.fallback_string 代替\nMyLang.getString(\"remote_string_only\", R.string.fallback_string)\n```\n使用语言包（语言包文件是 xml 格式，和 res 下的 strings.xml 一样。）\n```java\n//应用一种语言（这里自动处理了语言包的升级、语言包内部字符串的升级）\nMyLang.getInstance().applyLanguage(Context, LocaleInfo, force=true, init=false);\n\n//删除一种语言\nMyLang.getInstance().deleteLanguage(Context, LocaleInfo);\n```\n`LocaleInfo` 可以在以下地方找到\n```java\n//1. 所有云端的语言包\nMyLang.getInstance().remoteLanguages\n\n//2. 所有下载到本地、可用的语言包\nMyLang.getInstance().languages\n\n//3. 所有非官方的语言包\nMyLang.getInstance().unofficialLanguages\n\n//4. 除内置支持的语言外，另外安装的云端的语言包\nMyLang.getInstance().otherLanguages\n```\n\n## 2. 安装\n\n2.1. 引入\n\n```java\n//build.gradle\nallprojects {\n    repositories {\n        google()\n        jcenter()\n        maven { url \"https://github.com/LinXueyuanStdio/MLang/raw/main/dist/\" }\n    }\n}\n\n//app/build.gradle\nimplementation 'com.timecat.component:MLang:2.0.2'\n```\n\n2.2. 在 Application 中初始化，并监听系统语言的更改（如果跟随系统语言的话）：\n```java\npublic class MyApplication extends Application {\n    @SuppressLint(\"StaticFieldLeak\")\n    public static volatile Context applicationContext;\n    public static volatile Handler applicationHandler;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        applicationContext = this;\n        applicationHandler = new Handler(applicationContext.getMainLooper());\n        MyLang.init(applicationContext);\n    }\n\n    @Override\n    public void onConfigurationChanged(@NonNull Configuration newConfig) {\n        super.onConfigurationChanged(newConfig);\n        MyLang.onConfigurationChanged(newConfig);\n    }\n}\n```\n\n其中建议自己新建一个静态类 MyLang 来代理 MLang。\n这样有两个好处：\n1. 隔绝 MLang 的 api 变化，提高兼容性和稳定性。\n2. 使用更简洁。MLang 不持有 context，但每次获取字符串为空时，需要 context 来兜底，获取本地的字符串。在自己的 MyLang 默认提供 application Context，可以不用到处提供 context，更简洁。\n\n```java\npublic class MyLang {\n    private static File filesDir;\n    private static LangAction action;\n    public static void init(@NonNull Context applicationContext) {\n        filesDir = applicationContext.getCacheDir();\n        action = new MyLangAction();\n        getInstance();\n    }\n    public static MLang getInstance() {\n        return MLang.getInstance(MyApplication.applicationContext, filesDir, action);\n    }\n    public static void onConfigurationChanged(@NonNull Configuration newConfig) {\n        getInstance().onDeviceConfigurationChange(getContext(), newConfig);\n    }\n}\n```\n\n## 3. 设计\n\n### 3.1. 单例模式接收 3 个参数，context，fileDir，action\n1. context：MLang 内部不持有该 context。该 context 用于注册时区广播（根据时区来格式化字符串中的时间）、 判断系统当前时间是否 24 小时制等等。\n2. filesDir：持久化语言包文件的存储地址。语言包文件是 xml 格式，和 res 下的 strings.xml 一样。\n3. action：action 包含了应用语言包、切换语言等等需要的所有回调，即 `LangAction` 接口。\n\n```java\nMLang.getInstance(context, filesDir, action);\n```\n\n### 3.2. `LangAction` 接口定义了 2 个东西\n1. 当前语言的设置存储。\n   MLang 根据语言 id (string) 来识别当前语言。语言 id 需要持久化。\n   所以设计了下面两个方法，可以自行决定持久化的方式（SharedPreferences、MMKV、SQLite等等）。\n   ```java\n   void saveLanguageKeyInLocal(String language);\n   @Nullable String loadLanguageKeyInLocal();\n   ```\n2. 必要的网络接口。\n   ```java\n   void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback)\n   void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback)\n   void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback)\n   ```\n\n`LangAction` 的注释如下：\n\n```java\npublic interface LangAction {\n    /**\n     * SharedPreferences preferences = Utilities.getGlobalMainSettings();\n     * SharedPreferences.Editor editor = preferences.edit();\n     * editor.putString(\"language\", language);\n     * editor.commit();\n     * @param language localeInfo.getKey() 语言 id\n     */\n    void saveLanguageKeyInLocal(String language);\n\n    /**\n     * SharedPreferences preferences = Utilities.getGlobalMainSettings();\n     * String lang = preferences.getString(\"language\", null);\n     * @return @Nullable lang 语言 id\n     */\n    String loadLanguageKeyInLocal();\n\n    /**\n     * 在其他线程网络请求，在主线程或UI线程调用callback\n     * 这里设计成这样，是因为这个方法里支持异步执行\n     * 您需要在合适的时机手动调用 callback，且只能调用一次\n     * @param lang_pack 语言包名字\n     * @param lang_code 语言包版本名称\n     * @param from_version 语言包版本号\n     * @param callback @NonNull 在主线程或UI线程调用\n     */\n    void langpack_getDifference(String lang_pack, String lang_code, int from_version, GetDifferenceCallback callback);\n\n    /**\n     * 在其他线程网络请求，在主线程或UI线程调用callback\n     * 这里设计成这样，是因为这个方法里支持异步执行\n     * 您需要在合适的时机手动调用 callback，且只能调用一次\n     * @param callback @NonNull 在主线程或UI线程调用\n     */\n    void langpack_getLanguages(GetLanguagesCallback callback);\n\n    /**\n     * 在其他线程网络请求，在主线程或UI线程调用callback\n     * 这里设计成这样，是因为这个方法里支持异步执行\n     * 您需要在合适的时机手动调用 callback，且只能调用一次\n     * @param lang_code 语言包版本名称\n     * @param callback @NonNull 在主线程或UI线程调用\n     */\n    void langpack_getLangPack(String lang_code, GetLangPackCallback callback);\n\n    interface GetLanguagesCallback {\n        /**\n         * 必须在UI线程或者主线程调用\n         * 所有可用的语言包\n         * @param languageList 语言包列表\n         */\n        void onLoad(List\u003cLangPackLanguage\u003e languageList);\n    }\n\n    interface GetDifferenceCallback {\n        /**\n         * 必须在UI线程或者主线程调用\n         * 如果服务端没有实现增量分发的功能，可以用完整的语言包代替\n         * @param languageList 增量的语言包\n         */\n        void onLoad(LangPackDifference languageList);\n    }\n\n    interface GetLangPackCallback {\n        /**\n         * 必须在UI线程或者主线程调用\n         * @param languageList 完整的语言包\n         */\n        void onLoad(LangPackDifference languageList);\n    }\n\n}\n```\n\n实现`LangAction`的一个示例如下：\n\n```java\npublic class MyLangAction implements LangAction {\n   @Override\n   public static void saveLanguageKeyInLocal(String language) {\n       SharedPreferences preferences = getContext().getSharedPreferences(\"language_locale\", Context.MODE_PRIVATE);\n       SharedPreferences.Editor editor = preferences.edit();\n       editor.putString(\"language\", language);\n       editor.apply();\n   }\n\n   @Override\n   @Nullable\n   public static String loadLanguageKeyInLocal() {\n       SharedPreferences preferences = getContext().getSharedPreferences(\"language_locale\", Context.MODE_PRIVATE);\n       return preferences.getString(\"language\", null);\n   }\n   @Override\n   public void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback) {\n       Server.request_langpack_getDifference(lang_pack, lang_code, from_version, new Server.GetDifferenceCallback() {\n           @Override\n           public void onNext(final LangPackDifference difference) {\n               callback.onLoad(difference);\n           }\n       });\n   }\n\n   @Override\n   public void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback) {\n       Server.request_langpack_getLanguages(new Server.GetLanguagesCallback() {\n           @Override\n           public void onNext(final List\u003cLangPackLanguage\u003e languageList) {\n               callback.onLoad(languageList);\n           }\n       });\n   }\n\n   @Override\n   public void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback) {\n       Server.request_langpack_getLangPack(lang_code, new Server.GetLangPackCallback() {\n           @Override\n           public void onNext(final LangPackDifference difference) {\n               callback.onLoad(difference);\n           }\n       });\n   }\n}\n```\n### 3.3. 服务器语言包的结构\n\n[模拟的服务器数据](https://github.com/LinXueyuanStdio/MLang/blob/main/app/src/main/java/com/timecat/ui/Server.java)\n\n语言包实体\n- `LangPackLanguage(name, version, ...)`\n\n语言包的数据\n- `LangPackDifference(name, version, List\u003cLangPackString\u003e, ...)`\n- `LangPackString(key: String, value: String)`\n\n```java\npublic class Server {\n    public static LangPackLanguage chineseLanguage() {\n        LangPackLanguage langPackLanguage = new LangPackLanguage();\n        langPackLanguage.name = \"chinese\";\n        langPackLanguage.native_name = \"简体中文\";\n        langPackLanguage.lang_code = \"zh\";\n        langPackLanguage.base_lang_code = \"zh\";\n        return langPackLanguage;\n    }\n    public static LangPackDifference chinesePackDifference() {\n        LangPackDifference difference = new LangPackDifference();\n        difference.lang_code = \"zh\";\n        difference.from_version = 0;\n        difference.version = 1;\n        difference.strings = chineseStrings();\n        return difference;\n    }\n    public static ArrayList\u003cLangPackString\u003e chineseStrings() {\n        ArrayList\u003cLangPackString\u003e list = new ArrayList\u003c\u003e();\n        list.add(new LangPackString(\"LanguageName\", \"中文简体\"));\n        list.add(new LangPackString(\"LanguageNameInEnglish\", \"Chinese\"));\n        list.add(new LangPackString(\"local_string\", \"中文的云端字符串\"));\n        list.add(new LangPackString(\"remote_string_only\", \"本地缺失，云端存在的字符串\"));\n        return list;\n    }\n}\n```\n\n## 4. 进阶配置\n```java\nMLang.isRTL = false; //是否从右到左阅读（默认 false）\nMLang.is24HourFormat = false; //是否 24 小时制（默认 false）\nMLang.USE_CLOUD_STRINGS = true; //是否使用云端字符串（默认 true）\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinxueyuanstdio%2Fmlang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flinxueyuanstdio%2Fmlang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinxueyuanstdio%2Fmlang/lists"}