关于多语言插件报错,我动手解析生成代码的这件事
起因

另外 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];
}
});
}
这样总体来说就比小完善了,如果你有什么意见或建议,欢迎提出 ~
转载自:https://juejin.cn/post/7091067482185662478



