一起养成写作习惯!这是我参与「日新计划 4 月更文挑战」的第5天,点击查看活动详情。
起因
今天本想用 Flutter Intl
插件来玩玩 多语言
,不知道是 AndroidStudio
版本问题,还是什么,没想到添加语言时一直报错。不就是生成几个类,解析一下资源文件嘛,自己动手丰衣足食。再加上之前写个一个简单的多语言解析 ,刚好借此来稍微完善一下。
另外 Flutter Intl
插件的工作方式会实时监听 arb
文件的变化,生成代码。我并不喜欢这种时时监听的感觉,还是觉得写个小脚本,想跑就跑,又快又便捷。 自己把握核心逻辑,这样就不必看插件的 “脸色”
。
一、 使用介绍
代码已经开源,在 【toly1994328/i18n_builder】 中可获取脚本源码,同时这也是一个非常精简的多语言切换示例。
如何使用
- 1.把这个脚本文件
拷贝
到你项目文件夹, - 2.在命令行中,进入
script/i18n_builder
文件,运行dart run.dart .
即可生成默认的文件。
cd script/i18n_builder # 进入脚本文件夹
dart run.dart . # 在 lib 下创建名为 I18n 的相关文件
如果不想通过命令行,在 run.dart
中直接点运行也是可以的。
2. 定制化参数
有两个可定制的参数,分别是生成文件的文件夹,以及调用名。通过命令行可指定参数:
cd script/i18n_builder # 进入脚本文件夹
dart run.dart -D=lib,src,app -N=S # 在 lib/src/app 下创建名为 S 的相关文件
比如上面的命令可以指定在 lib/src/app
生成文件,并且调用的类为 S
。也就是说,在代码中通过下面语句进行访问属性: 默认的调用类是 I18n
,你可以自由指定:
S.of(context).XXX
如果直接运行,可以在此进行指定:
3.资源说明
字符资源通过 json
的形式给出,如果你想添加一个新语言,只需要提供 languageCode_countryCode.json
的文件即可。
其中支持 参数变量
,使用 {变量名}
进行设定。另外还支持变量的默认参数
,通过 {变量名=默认参数}
进行指定:
I18n.of(context).info2(count: '$_counter')
I18n.of(context).info2(count: '$_counter',user: 'toly')
一、支持多语言的流程
我们先来看一下对于 Flutter
来说,该如何支持多语言。如下所示,先给一个最精简的案例实现:
中文 | 英文 |
---|---|
1. 准备工作
首先在 pubspec.yaml
文件中添加 flutter_localizations
的依赖:
dependencies:
#...
flutter_localizations:
sdk: flutter
在使用时我们需要在 MaterialApp
中配置三个参数:
-
tag1
: 代理类列表。其中I18nDelegate
是自定义的代理(通过脚本生成
)。 -
tag2
: 语言支持的列表。 -
tag3
: 当前支持的语言。
MaterialApp(
//...
localizationsDelegates: [ // tag1
...GlobalMaterialLocalizations.delegates,
I18nDelegate.delegate
],
supportedLocales: I18nDelegate.delegate.supportedLocales, // tag2
locale: const Locale('zh', 'CH'), // tag3
);
多语言切换的功能实现其实非常简单,修改 tag3
处的 locale
参数即可。所以关键还是代理类的实现。
2. 代理类的书写
其中 supportedLocales
表示当前支持的语言:
///多语言代理类
class I18nDelegate extends LocalizationsDelegate<I18N> {
I18nDelegate._();
final List<Locale> supportedLocales = const [ // 当前支持的语言
Locale('zh', 'CH'),
Locale('en', 'US'),
];
@override
bool isSupported(Locale locale) => supportedLocales.contains(locale);
///加载当前语言下的字符串
@override
Future<I18N> load(Locale locale) {
return SynchronousFuture<I18N>(I18N(locale));
}
@override
bool shouldReload(LocalizationsDelegate<I18N> old) => false;
///代理实例
static I18nDelegate delegate = I18nDelegate._();
}
在 I18N
类中进行文字的获取构造,其实整个流程还是非常简洁易懂的:
class I18N {
final Locale locale;
I18N(this.locale);
static const Map<String, Map<String,String>> _localizedValues = {
'en_US': {
"title":"Flutter Demo Home Page",
"info":"You have pushed the button this many times:",
"increment":"Increment:",
}, //英文
'zh_CH': {
"title":"Flutter 案例主页",
"info":"你已点击了多少次按钮: ",
"increment":"增加",
}, //中文
};
static I18N of(BuildContext context) {
return Localizations.of(context, I18N);
}
get title => _localizedValues[locale.toString()]!['title'];
get info => _localizedValues[locale.toString()]!['info'];
get increment => _localizedValues[locale.toString()]!['increment'];
}
3. 使用方式
使用方式也非常简洁,通过 .of
的方式从上下文中获取 I18N
对象,再获取对应的属性即可。
I18N.of(context).title
从这里也可以看出,本质上这也是通过 InheritedWidget
组件实现的。多语言的关键类是 Localization
组件,其中使用了 _LocalizationsScope
组件。
二、如何自己写脚本
本着代码本身就是字符串的理念,我们只要根据资源来生成上面所述的字符串即可。这里考虑再三,还是用 json
记录数据。文件名使用 languageCode_countryCode
来标识,比如 zh_CH
标识简体中文,zh_HK
标识繁体中文。另外如果不知道对应的 语言代码表
,稍微搜索一下就行了。
1. 文件夹的解析
先来根据资源文件解析处需要支持的 Local
信息与 Attr
属性信息,如下所示:
先定义如下的实体类,用于收录信息。其中 ParserResult
类是最终的解析结果:
class LocalInfo {
final String languageCode;
final String? countryCode;
LocalInfo({required this.languageCode, this.countryCode});
}
class AttrInfo {
final String name;
AttrInfo({required this.name});
}
class ParserResult {
final List<LocalInfo> locals;
final List<AttrInfo> attrs;
final String scriptPath;
ParserResult({required this.locals, required this.attrs,required this.scriptPath});
}
在 Parser
类中,遍历 data
文件,通过文件名来收集 Local
,核心逻辑通过 _parserLocal
方法实现。然后读取第一个文件来对属性进行收集,核心逻辑通过 _parserAttr
方法实现。
class Parser {
Future<ParserResult> parserData(String scriptPath) async {
Directory dataDir =
Directory(path.join(scriptPath, 'script', 'i18n_builder', 'data'));
List<FileSystemEntity> files = dataDir.listSync();
List<LocalInfo> locals = [];
List<AttrInfo> texts = [];
for (int i = 0; i < files.length; i++) {
if (files[i] is File) {
File file = files[i] as File;
locals.add(_parserLocal(file.path));
if (i == 0) {
String fileContent = await file.readAsString();
Map<String, dynamic> decode = json.decode(fileContent);
decode.forEach((key, value) {
texts.add(_parserAttr(key,value.toString()));
});
}
}
}
return ParserResult(locals: locals, attrs: texts,scriptPath: scriptPath);
}
}
如下是 _parserLocal
和 _parserAttr
的实现:
// 解析 LocalInfo
LocalInfo _parserLocal(String filePath) {
String name = path.basenameWithoutExtension(filePath);
String languageCode;
String? countryCode;
if (name.contains('_')) {
languageCode = name.split('_')[0];
countryCode = name.split('_')[1];
} else {
languageCode = name;
}
return LocalInfo(
languageCode: languageCode,
countryCode: countryCode,
);
}
// 解析属性
AttrInfo _parserAttr(String key, String value){
return AttrInfo(name: key);
}
2.根据分析结果进行代码生成
现在 食材
算是准备完毕了,下面来对它们进行加工。主要目标就是点击运行,可以在指定文件夹内生成相关代码,如下所示:
如下通过 Builder
类来维护生成代码的工作,其中 dir
用于指定生成文件的路径, caller
用于指定调用类。比如之前的是 I18n.of(context)
,如果用 Flutter Intl
的话,可能习惯于S.of(context)
。其实就是在写字符串时改个名字而已,暴露出去,使用者可以更灵活地操作。
class Builder {
final String dir;
final String caller;
Builder({
required this.dir,
this.caller = 'I18n',
});
void buildByParserResult(ParserResult result) async {
await _ensureDirExist();
await _buildDelegate(result);
print('=====${caller}_delegate.dart==文件创建完毕==========');
await _buildCaller(result);
print('=====${caller}.dart==文件创建完毕==========');
await _buildData(result);
print('=====数据文件创建完毕==========');
}
另外 buildByParserResult
方法负责根据解析结构生成文件,就是字符串的拼接而已,这里就不看贴了。感兴趣的可以自己去源码里看 【i18n_builder】
三、支持字符串解析
有时候,我们是希望支持变量的,这也就表示需要对变量进行额外的解析,这也是为什么之前 _parserAttr
单独抽出来的原因。比如下面的 info2
中有两个参数,可以通过 正则表达式
进行匹配。
1. 属性信息的优化
下面对 AttrInfo
继续拓展,增加 args
成员,来记录属性名列表:
class AttrInfo {
final String name;
List<String> args;
AttrInfo({required this.name});
}
2. 解析的处理
正则表达式已经知道了,解析一下即可。代码如下:
// 解析属性
AttrInfo _parserAttr(String key, String value){
RegExp regExp = RegExp(r'{(?<tag>.*?)}');
List<String> args = [];
List<RegExpMatch> allMatches = regExp.allMatches(value).toList();
allMatches.forEach((RegExpMatch match) {
String? arg = match.namedGroup('tag');
if(arg!=null){
args.add(arg);
}
});
print("==$key==$args");
return AttrInfo(name: key,args: args);
}
然后对在文件对应的属性获取时,生成如下字符即可:
这样在使用该属性时,就可以传递参数,使用及效果如下:
Text(
I18n.of(context).info2(user: 'toly', count: '$_counter'),
),
中文 | 英文 |
---|---|
3.支持默认参数
在解析时,通过校验 {=}
号,提供默认参数。
在生产代码是对于有 =
的参数,使用可空处理,如果有默认值,通过正则解析出默认值,进行设置:
4. 支持命令行
为了更方便使用,可以通过命令行的方式来使用。
cd script/i18n_builder # 进入脚本文件夹
dart run.dart -D=lib,src,app -N=S # 在 lib/src/app 下创建名为 S 的相关文件
需要额外进行的就是对入参字符串列表的解析:
main(List<String> args) async {
...
if(args.isNotEmpty){
scriptPath = Directory.current.parent.parent.path;
args.forEach((element) {
if(element.contains("-D")){
String dir = element.split('=')[1];
List<String> dirArgs = dir.split(',');
String p = '';
dirArgs.forEach((d) {
p = path.join(p,d);
});
distDir= p;
}
if(element.contains("-N")){
caller = element.split('=')[1];
}
});
}
这样总体来说就比小完善了,如果你有什么意见或建议,欢迎提出 ~