做为一个 Android 开发者,Flutter 上来就让我把各种字符串写在 widget 里,其实我内心是拒绝的。硬编码是不可能硬编码的。国际化又不会,就是只能去看看文档,才能学点新姿式这样子。看了文档以后,以为国际化这部分,仍是有点麻烦的,我以为有必要拎出来单独写写。git
我的但愿能把应用的字符串资源独立出来,以方便管理。至于支持多语言这种,反而是顺带完成的结果。本文以实用优先,由于我认为这部份内容是每一个应用都须要使用的。github
首先简单认识一下 Flutter 国际化相关的知识点。json
添加 flutter_localizations
依赖,让 Flutter 知道咱们须要使用国际化相关的包。Flutter 自带的 widget 中,也用到了一些字符串资源,好比,showSearch()
方法打开的搜索栏提示。而这个包能够提供英文以外的,被 Flutter 内部默认使用的国际化字符串资源。app
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
复制代码
而后在建立 App 时,加入 LocalizationsDelegate
,国际化的内容就由这些类来提供。GlobalMaterialLocalizations.delegate
提供了 Material 组件库所使用的字符串资源;GlobalWidgetsLocalizations.delegate
则定义了在当前的语言中,文字默认的排列方向。less
以后咱们定义了本身的国际化内容后,也须要加入到这个列表的头部。ide
还要声明要支持什么语言,supportedLocales
这里添加了英文和中文两种。若是说用户的语言不在这个列表内,则会默认使用列表第一项指定的语言。假如你对这个规则不满意,可使用 localeResolutionCallback
参数来自定义本身想要的规则。函数
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class ThisApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
const Locale('zh', 'CN'),
],
title: 'App Title',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
复制代码
如今,咱们将一些自带的国际化资源加入到了应用中,Flutter 自身已经可以使用它们了。但咱们要怎么使用它们呢?工具
经过 MaterialLocalizations.of(context)
获取到 MaterialLocalizations
的实例,而后访问里面的字符串。好比上面的 title 一行,能够替换为:优化
onGenerateTitle: (context) => MaterialLocalizations.of(context).closeButtonLabel,
复制代码
注意这里将 title 替换成 onGenerateTitle 了,由于此时还在初始化 App 中,没法获取到 context,更没法经过 context 获取字符串了。ui
如今来考虑怎么将咱们本身的国际化加入到其中。也就是,须要在 localizationsDelegates
中加入本身的 LocalizationsDelegate
。
查看文档,LocalizationsDelegate
须要一个泛型参数。参考官方的文档,可知这里指定的类型就是咱们存放字符串的类。在这里,有两种选择:第一是基于 map 的,很是简单的实现;第二个则是经过 Dart 语言中专门负责国际化的 intl 包来实现。接下来咱们按次来看看。
class SimpleLocalizations {
SimpleLocalizations(this.locale);
final Locale locale;
static SimpleLocalizations of(BuildContext context) {
return Localizations.of<SimpleLocalizations>(context, SimpleLocalizations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'app_name': 'App Name',
'hello_world': 'Hello World',
},
'zh': {
'app_name': '应用名',
'hello_world': '你好世界',
},
};
Map<String, String> get _stringMap {
return _localizedValues[locale.languageCode];
}
String get helloWorld {
return _stringMap['hello_world'];
}
String get appName {
return _stringMap['app_name'];
}
}
复制代码
从上面的代码能够看到,这种方法的原理很是简单,就是将全部字符串放进 map,而后经过应用的 Locale
来取出对应语言的字符串。使用时,就是 SimpleLocalizations.of(context).helloWorld
这样来引用字符串。
其对应的 LocalizationsDelegate
以下:
class SimpleLocalizationsDelegate extends LocalizationsDelegate<SimpleLocalizations> {
const SimpleLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
@override
Future<SimpleLocalizations> load(Locale locale) {
return SynchronousFuture<SimpleLocalizations>(SimpleLocalizations(locale));
}
@override
bool shouldReload(SimpleLocalizationsDelegate old) => false;
}
复制代码
只要将这个 SimpleLocalizationsDelegate
加入到上面的 delegates 列表中,国际化就算完成了。
回看一下整个流程,并不算复杂,须要经手部分的原理也很是简单,只是一个 map 的使用。使用这个方法能够将整个应用的字符串都集中到一块儿管理。可是,维护起来仍是很不方便。
接下来看看基于 intl 包的实现方法是怎么样的。
第一步,添加依赖:
dependencies:
intl: ^0.15.7
dev_dependencies:
intl_translation: ^0.17.3
复制代码
经过查看官方的例子,能够知道 Intl.message()
方法是咱们管理字符串的关键。因而去看相关的文档,会发现——嗯,没有卵用(甚至没解释每一个参数有什么做用)。
接下来仍是同样添加一个跟 SimpleLocalizations
差很少类:
class IntlLocalizations {
static IntlLocalizations of(BuildContext context) {
return Localizations.of<IntlLocalizations>(context, IntlLocalizations);
}
String get appName {
return Intl.message('App Name');
}
String get helloWorld {
return Intl.message('Hello world');
}
}
复制代码
从命令行中运行 flutter pub pub run intl_translation:extract_to_arb --output-dir=你想要的输出目录 IntlLocalizations所在文件
。这一操做将会在指定目录里生成一个名为 intl_messages.arb 的文件,内容大体以下:
{
"@@last_modified": "2019-02-17T15:57:00.554988",
"App Name": "App Name",
"@App Name": {
"type": "text",
"placeholders": {}
},
"Hello world": "Hello world",
"@Hello world": {
"type": "text",
"placeholders": {}
}
}
复制代码
将这个文件复制一份,命名为 intl_en.arb,做为英文版本使用。接着再复制一份,命名为 intl_zh.arb 做为中文版本使用。将 intl_zh.arb 的内容修改成对应中文的内容:
{
"@@last_modified": "2019-02-17T15:57:00.554988",
"App Name": "应用名",
"@App Name": {
"type": "text",
"placeholders": {}
},
"Hello world": "你好世界",
"@Hello world": {
"type": "text",
"placeholders": {}
}
}
复制代码
若是须要其余语言的版本,请自行添加并修改。
再来输入一段长长的命令行:flutter pub pub run intl_translation:generate_from_arb --output-dir=输出目录 --no-use-deferred-loading IntlLocalizations所在文件 全部arb文件
。这样会生成几个 messages_
开头的 dart 文件。能够自行查看一下里面的内容,我如今的 Dart 水平还比较菜,就先不分析其中的原理了。
其中名为 messages_all.dart 的文件里,生成了 initializeMessages(String localeName)
这个方法,将会在下面的步骤中使用到。
在 IntlLocalizations
中添加以下的方法:
static Future<IntlLocalizations> load(Locale locale) {
final name =
locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
return IntlLocalizations();
});
}
复制代码
IntlLocalizations
就准备完毕了。而后,开始实现 delegate,内容很简单:
class IntlLocalizationsDelegate extends LocalizationsDelegate<IntlLocalizations> {
const IntlLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
@override
Future<IntlLocalizations> load(Locale locale) {
return IntlLocalizations.load(locale);
}
@override
bool shouldReload(IntlLocalizationsDelegate old) => false;
}
复制代码
以后就是正常使用流程了,这里不赘诉。回想整个流程,真正国际化的内容在 arb 文件中,对于集中管理字符串来讲,比使用 map 仍是好一点。可是整个流程仍是显得异常麻烦,尤为是两次长得过度的命令行,明显应该由工具来改进。我相信 Flutter/Dart 团队应该会在这一点上作出优化。
那么,有没有一款工具能够解救咱们呢?您好,有的。
Android Studio(IDEA)上有一款名为 flutter_i18n 的插件,能够帮助简化这个过程。其原理是经过 arb 文件来自动生成所须要的代码。
插件的使用很是简单,安装后会出现一个新的按钮。一旦你按下这个按钮——boom——插件就会根据 res/values
文件夹(Android 开发者以为很亲切)中的 arb 文件,在 lib/generated
中生成 Dart 代码。
那么咱们的重心就放在了 arb 文件上。Arb 文件全称是 Application Resource Bundle,是基于 JSON 的 balabala 接下去的我也不想接着说了,由于并不实用。仍是来看下 Flutter 国际化中切实相关的部分。
虽然咱们知道了 arb 文件是类 JSON 格式,但咱们还并不清楚文件里具体须要什么样的内容。这里咱们经过 Intl.message()
方法再从新认识一下。
String get appName {
return Intl.message(
'App Name',
desc: 'Name for the application',
name: 'IntlLocalizations_appName',
);
}
String hello(String name) {
return Intl.message(
'Hello $name',
name: 'IntlLocalizations_hello',
desc: 'Say hello to someone',
args: [name],
locale: 'en',
examples: const {'name': 'Someone'},
meaning: 'What is this?',
skip: false,
);
}
复制代码
这里有两个更为详细的实现,其中 hello
方法将所有的参数都赋值了,以方便观察经过 intl_translation
包处理后的 arb 文件会是什么样的。
不过这以前简单介绍一下 Intl.message()
的部分参数。
name
参数必须与函数名一致,或者是类名_方法名
这个形式——建议使用后者避免冲突;args
就是重复一遍参数;name
和 args
能够省略;desc
参数就是描述这个字符串的字符串,必须是一个字符串字面量;examples
是参数的示例;desc
和 examples
在运行时不会被使用,但会被提取出来做为额外的信息提供给翻译人员做为参考;skip
若是为 true,那么这条记录就不会被提取出来;而后咱们再运行一下那个很长的命令行,将其处理成 arb 文件看看:
{
"@@last_modified": "2019-02-18T21:31:28.750455",
"IntlLocalizations_appName": "App Name",
"@IntlLocalizations_appName": {
"description": "Name for the application",
"type": "text",
"placeholders": {}
},
"IntlLocalizations_hello": "Hello {name}",
"@IntlLocalizations_hello": {
"description": "Say hello to someone",
"type": "text",
"placeholders": {
"name": {
"example": "Someone"
}
}
}
}
复制代码
首先,meaning
彷佛没有用处。其核心就是 "IntlLocalizations_appName": "App Name"
这样的一条一条的记录。以 @ 开头的部分,并不会真正在程序中使用,而是给翻译人员做为参考使用的。
这么一来,咱们接下来就能够在 res/values
文件夹中建立须要的 arb 文件了。这个插件还提供了快捷建立 arb 文件的功能,只须要在 res/values
目录右键选择 New -> Arb File 就能够选择这个 arb 文件的 locale 了。
须要注意的是,在这个插件中,若是字符串内须要包含变量,使用的语法是 $var_name
,而不是上面例子里使用大括号的形式。
这里我建立了两个 arb 文件:
// strings_en.arb
{
"appName": "App Name",
"hello": "Hello $name"
}
// strings_zh_CN.arb
{
"appName": "应用名",
"hello": "你好${name}"
}
复制代码
使用插件生成代码后,将 delegate 加入到应用的列表中,使用时也只要直接利用 S
这个类名来引用就好:
MaterialApp(
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
const Locale('zh', 'CN'),
],
onGenerateTitle: (context) => S.of(context).appName,
...
复制代码
使用了这个插件以后,国际化就算得上方便了。生成的代码也能够稍微看一眼,或许有你用获得的其余方法。
最后提醒一句,因为生成代码是由插件完成的,因此依赖中的 intl_translation
能够删掉了。
可能有的人会问,不使用 IDEA 的开发者,有没有什么更好的选择呢?或许有。如今还有一个名为 rosetta 的库,致力于解决 Flutter 国际化太过复杂的问题。我尝试过,但并无跑通正常的流程,没法更多评价。有兴趣的朋友能够试试看。
到此,这篇指南就结束了,但愿能对一些人有帮助。