Flutter Intl 远程管理,动态化更新语言包
背景
之前有个需求做Flutter的多语言国际化嘛,然后立马想起有Intl,看着文档唰唰唰写完了,自测,嗯,没有问题,转头就扔去测试。
然后开会领导问我,“你这个文案都是在哪里管理啊?” “emmm,项目代码内,本地憋” “嗯?那我要修改怎么办?” “改完发版咯” “那不行啊!那文案有问题我还等你发版啊?文案你自己管理吗?” “emmm,好吧”
行吧,看来不能简单的摸鱼了。
分析
首先分析一下,一个简单的intl结构如下
-generated
--intl
---messages_all.dart
---messages_en.dart
---messages_zh.dart
--l10n.dart
-l10n
--intl_en.arb
--intl_zh.arb
generated
中的为自动生成的文件,l10n
中则为我们的多语言文件,intl_**.arb
的内容基本是键值对的json
(不考虑有‘@’注释辅助信息的情况)。
看起来好像是无法处理,因为重点在generated
中,而generated
都是自动生成的,即使修改,再次运行命令就会被覆盖。
不妨往下接着看看,首先l10n.dart
中有两个类,一个为S
,即我们获取Localizations.of()
拿到文案的类;一个是AppLocalizationDelegate
。
S
我是使用flutter_intl
自动生成的,除非想自己写S
,不然不大可能修改S
来另外管理。以下是一个基本的S
class S {
S();
static S? _current;
static S get current {
assert(_current != null,
'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.');
return _current!;
}
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = S();
S._current = instance;
return instance;
});
}
static S of(BuildContext context) {
final instance = S.maybeOf(context);
assert(instance != null,
'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?');
return instance!;
}
static S? maybeOf(BuildContext context) {
return Localizations.of<S>(context, S);
}
/// `test title`
String get test {
return Intl.message(
'test title',
name: 'test',
desc: '',
args: [],
);
}
}
而AppLocalizationDelegate
就不同了,它基本是固定的,主要功能是注册到MaterialApp
的localizationsDelegates
,然后我们就可以在Localizations
拿到对应的翻译文案。所以我们可以考虑编写自己的Delegates。
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
const AppLocalizationDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'zh'),
];
}
@override
bool isSupported(Locale locale) => _isSupported(locale);
@override
Future<S> load(Locale locale) => S.load(locale);
@override
bool shouldReload(AppLocalizationDelegate old) => false;
bool _isSupported(Locale locale) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
return false;
}
}
简单来说,就是定义支持的语言列表,以及load怎么加载得到对应的S
。
那么这个load就是重点信息了,我们怎么把我们自己的资源加载进去呢?看看intl
是怎么做的,即S.load()
。
static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
// 获取intl可识别格式的localeName
final localeName = Intl.canonicalizedLocale(name);
// 加载对应localeName的翻译Messages
return initializeMessages(localeName).then((_) {
// 加载成功后设置defaultLocale,并把S实例返回
Intl.defaultLocale = localeName;
final instance = S();
S._current = instance;
return instance;
});
}
上面我简单写了下注释,重点即是initializeMessages
,跟着继续来到messages_all.dart
。
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) async {
// 验证Locale是否支持是否有效
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new Future.value(false);
}
// 可以异步等待操作
var lib = _deferredLibraries[availableLocale];
await (lib == null ? new Future.value(false) : lib());
// 实例化MessageLookup()
initializeInternalMessageLookup(() => new CompositeMessageLookup());
// 找到对应Locale的Messages并add到messageLookup中
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new Future.value(true);
}
大致描述了下功能,可以看到,官方已经预留了异步加载的位置,也就是说他的设计是支持远程的。异步远程找到地方了,那么重点就是messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
,远程拉取的资源,还需要添加到messageLookup
中,因为最终Localizations
获取的文案,就是messageLookup
中的。
abstract class MessageLookup {
String? lookupMessage(String? messageText, String? locale, String? name,
List<Object>? args, String? meaning,
{MessageIfAbsent? ifAbsent});
void addLocale(String localeName, Function findLocale);
}
lookupMessage
即是取得对应Message
的方法。
先跟着_findGeneratedMessagesFor
走,跳过下中间的检查是否有效的步骤,最终到_findExact
。
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
case 'zh':
return messages_zh.messages;
default:
return null;
}
}
很明显就是根据localName
获取对应的MessageLookupByLibrary
,而看到messages_en.messages
的内容,就有点头大了,它是自动生成的固定写死的类。
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"test": MessageLookupByLibrary.simpleMessage("test title")
};
}
而MessageLookupByLibrary
提供对应的Messages
返回给_findExact
。
至此我们对于intl如何加载翻译的整个流程就大致了解了。
准备冻手!
首先看看messages_en.messages
这种东西怎么处理,有复杂的_notInlinedMessages
,甚至有占位符还会有各个方法。
static String m16(percent) => "${percent}%";
但让我们无视它们。重点其实就是messages
和localeName
。localeName
在_findExact
中可以给到;messages
的话,简单来说就是map
。那么,一个简单通用的MessageLookupByLibrary
就可以这样定义:
/// intl 可解析的结构体
class MessageLookup extends MessageLookupByLibrary {
MessageLookup(this.messages, this.localeName);
final Map<String, dynamic> messages;
final String localeName;
}
而messages
的value
,并不是简简单单的String
,而是Function()
,然后返回的翻译。那也是说我们需要对远程的资源(arb即json),进行解析,得到对应的格式,然后再赋给messages
。
其实就是差不多下面的格式:
([v1, v2, v3]) {
return str;
}
注意你并不确定它具体有多少个入参。它实际是Function.apply()
。
/// Evaluate the translated message and return the translated string.
String? evaluateMessage(translation, List<dynamic> args) {
return Function.apply(translation, args);
}
OK,那Messages
就解决了。那接下来就是顺水推舟,编写我们自己的Delegate
,然后注册到MaterialApp
的localizationsDelegates
中。
在load
中,使用我们已经修改好的initializeDynamicMessages
static Future<S> _load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeDynamicMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = S();
return instance;
});
}
注册到MaterialApp
。
localizationsDelegates: const [
/// Delegate 注册
AppLocalizationDynamicDelegate.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
/// 支持的语言列表
supportedLocales:
AppLocalizationDynamicDelegate.delegate.supportedLocales,
还有支持的语言列表也要改为我们自己的。调整了supportedLocales
,也需要GlobalMaterialLocalizations.delegate
这几个让系统的文案也支持这几个语言。
然后就能够让我们的国际化使用我们的远程资源了,是不是很简单。
结束
然后再补充一点点的功能,就实现了Flutter Intl的远程动态化,可以远程管理我们的文案翻译。这是我写的一个插件dynamic_intl,然后也会传到pub.dev,怎么用可以看看README,里面也有个简单的Demo。
转载自:https://juejin.cn/post/7103774763524784158