一起养成写作习惯!这是我参与「日新计划 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];
}
});
}
这样总体来说就比小完善了,如果你有什么意见或建议,欢迎提出 ~
评论(0)