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)
使用语言包
//应用一种语言(这里自动处理了语言包的升级、语言包内部字符串的升级)
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。 这样有两个好处:
- 隔绝 MLang 的 api 变化,提高兼容性和稳定性。
- 使用更简洁。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
- context:MLang 内部不持有该 context。该 context 用于注册时区广播(根据时区来格式化字符串中的时间)、 判断系统当前时间是否 24 小时制等等。
- filesDir:持久化语言包文件的存储地址。语言包文件是 xml 格式,和 res 下的 strings.xml 一样。
- action:action 包含了应用语言包、切换语言等等需要的所有回调,即
LangAction
接口。
MLang.getInstance(context, filesDir, action);
LangAction
接口定义了 2 个东西
3.2. - 当前语言的设置存储。 MLang 根据语言 id (string) 来识别当前语言。语言 id 需要持久化。 所以设计了下面两个方法,可以自行决定持久化的方式(SharedPreferences、MMKV、SQLite等等)。
void saveLanguageKeyInLocal(String language); @Nullable String loadLanguageKeyInLocal();
- 必要的网络接口。
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(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)