如果App的用户使用的是不同语言,那进行国际化是必要的。国际化主要包括文案的国际化(不同的语言展示不同的文案)和布局的国际化(从左到右还是从右到左布局)。不同语言涉及的业务逻辑的差别(eg. 法语跳转到法语对应网站,韩语跳到韩语对应的网页)一般不被归为国际化的内容,属于业务逻辑的范畴。
我们公司的产品用户涵盖了欧美、日韩和以色列等国家,每个版本发版前的一个块大的任务就是针对不同的语言进行布局和文案的适配,所以国际化还是很重要的一块内容。
案例
为了说明如何实现国际化,我们先建一个工程,然后将main.dart
中的代码替换成下面的代码:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatelessWidget {
@override
Widget build(context) {
return Scaffold(
appBar: AppBar(title: Text("国际化案例")),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(
3,
(index) => ElevatedButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2022));
},
child: Text("按钮 ${index + 1}"))),
),
),
drawer: Drawer(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("这是抽屉",
style: TextStyle(color: Colors.red, fontSize: 30)),
SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text("关闭抽屉"),
),
],
),
),
),
);
}
}
这段代码的功能很简单:
-
Home
页面有三个按钮,按照顺序水平排列,点击按钮会弹出Flutter官方的时间选择器DatePicker
; - 点击左上角会弹出
Drawer
, 有一个行文字和一个按钮,点击按钮Drawer
消失。
这个App在用户体验上有一些问题:
- 非中文语言的手机用户,他们不认识中文,所以需要将App中的中文(eg. 按钮,国际化按钮,关闭抽屉等)替换成他们手机对应的语言;
- 对于像以色列,阿拉伯语言的手机用户,他们的布局是从右往左的,目前从左往右的布局不符合他们的使用习惯;
- 即使是中文手机用户,弹出来的时间选择器上的文字是英文的,对中文用户也是不友好的。
Flutter官方提供的国际化
本着谁开发谁负责的原则,Flutter官方需要为他们提供的Widget提供国际化的支持。事实上他们也确实有提供支持方案。
添加依赖
dependencies:
flutter_localizations: //添加的
sdk: flutter //添加的
在pubspec.yaml
文件中加入依赖,然后执行flutter pub get
。
修改代码
- 引入头文件
import 'package:flutter_localizations/flutter_localizations.dart';
; - 给
MaterialApp
设置localizationsDelegates
和supportedLocales
;
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// 1 设置localizationsDelegates
localizationsDelegates: [
GlobalWidgetsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// 2 设置 supportedLocales 表示支持的国际化语言
supportedLocales: [
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'he'),
Locale.fromSubtags(languageCode: 'zh'),
],
home: Home(),
);
}
}
localizationsDelegates
的参数介绍:GlobalWidgetsLocalizations
主要是对布局方向进行国际化,GlobalMaterialLocalizations
主要是对Material Widgets进行了国际化,GlobalCupertinoLocalizations
是对Cupertino Widgets进行了国际化。不需要理由,写上这三个基本上系统的Widget就都支持国际化了。
supportedLocales
的参数介绍:en
代表英文,zh
代表中文,he
代表希伯来文(以色列)。这个参数需要根据实际情况设置,我这里设置这三个语言只是案例需要。如果用户的手机语言不是上述三种,譬如法语,那就使用默认的语言(英文)。
效果
经过这两步设置后,Flutter官方Widget的国际化已经实现完成,让我们看下效果:
- 手机语言是英文的效果:
抽屉从左往右弹出,一排按钮从左往右排列, 时间选择器上的文字是英文。
- 手机语言是中文的效果:
抽屉从左往右弹出,一排按钮从左往右排列, 时间选择器上的文字是中文。
- 手机语言是希伯来文的效果:
抽屉从右往左弹出,一排按钮从右往左排列, 时间选择器上的文字变成了希伯来文,时间选择器的内容也是从右往左排列。
自定义国际化实现
上面的效果还有一些瑕疵,不管切换什么手机语言,一些内容都是显示的中文,这是因为我们写死的是中文。这些中文文字根据手机语言显示对应的语言的文案才是最完美的实现。我们接下来的任务就是实现这个逻辑:
新建多语言Json文件
在根目录新建assets/json
文件夹,在此文件夹下新建i18n.json
文件,文件内容如下:
{
"en": {
"title": "Localization Demo",
"button": "Button",
"drawer_tip": "This is the Drawer",
"close_drawer": "Close Drawer"
},
"zh": {
"title": "国际化案例",
"button": "按钮",
"drawer_tip": "这是抽屉",
"close_drawer": "关闭抽屉"
},
"he": {
"title": "הדגמת לוקליזציה",
"button": "לַחְצָן",
"drawer_tip": "מְגֵרָה",
"close_drawer": "סגור מגירה"
}
}
en
,zh
和he
三种语言下都有title
,button
,drawer_tip
和close_drawer
四个文案。
引入文件
在pubspec.yaml
文件中引入assets/json/
文件夹下的所有文件,当然也包括i18n.json
这个文件:
flutter:
assets: //添加的
- assets/json/ //添加的
添加国际化代码
- 新建
app_localizations.dart
文件, 文件内容:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AppLocalizations {
// 1
final Locale locale;
AppLocalizations(this.locale);
// 2
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static Map<String, Map<String, String>> _localizedStrings = {};
// 3
Future loadJson() async {
final jsonString = await rootBundle.loadString("assets/json/i18n.json");
Map<String, dynamic> map = json.decode(jsonString);
_localizedStrings = map.map((key, value) => MapEntry(key, value.cast<String, String>()));
}
// 4
String get title => _localizedStrings[this.locale.languageCode]["title"];
String get button => _localizedStrings[this.locale.languageCode]["button"];
String get drawerTip =>
_localizedStrings[this.locale.languageCode]["drawer_tip"];
String get closeDrawer =>
_localizedStrings[this.locale.languageCode]["close_drawer"];
}
locale
是系统确定的,会从外部传进来,AppLocalizations
需要根据这个locale
来找到对应的语言的文案;of
只是封装了一个对外的方法,方便找到AppLocalizations
对象来使用。看到of
方法猜测国际化也是依赖于InheritedWiget来实现的;loadJson
是从JSON文件来加载国际化文件,然后将结果赋值给_localizedStrings
;- 实现了
title
,button
,drawerTip
和closeDrawer
的get方法。
- 新建
app_localization_delegate.dart
文件, 文件内容:
import 'package:flutter/material.dart';
import 'package:localization_demo/i18n/app_localizations.dart';
class APPLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
// 1.
static APPLocalizationDelegate delegate = APPLocalizationDelegate();
// 2.
@override
bool isSupported(Locale locale) {
return ["en", "zh", "he"].contains(locale.languageCode);
}
// 3
@override
Future<AppLocalizations> load(Locale locale) async {
final appLocalizations = AppLocalizations(locale);
await appLocalizations.loadJson();
return appLocalizations;
}
// 4
@override
bool shouldReload(APPLocalizationDelegate old) {
return false;
}
}
delegate
方法是实例化方法,起这个名字就是为了和系统的方法一致;isSupported
是判断是否支持locale
这个语言的国际化,支持就返回true
,否则返回false
;load
就是如果支持locale
这个语言的国际化,就去加载国际化资源,我们这儿的实现是让AppLocalizations
去加载JSON文件;shouldReload
是在用到国际化资源时是否需要重新加载国际化资源,默认是不需要。
国际化代码的使用
在main.dart
中使用前面实现的国际化的代码:
import 'package:localization_demo/i18n/app_localization_delegate.dart';
import 'package:localization_demo/i18n/app_localizations.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
GlobalWidgetsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
// 1. 修改地方
APPLocalizationDelegate.delegate
],
supportedLocales: [
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'he'),
Locale.fromSubtags(languageCode: 'zh'),
],
home: Home(),
);
}
}
class Home extends StatelessWidget {
@override
Widget build(context) {
return Scaffold(
// 2. 修改地方
appBar: AppBar(title: Text(AppLocalizations.of(context).title)),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(
3,
(index) => ElevatedButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2022));
},
// 3. 修改地方
child: Text("${AppLocalizations.of(context).button} ${index + 1}"))),
),
),
drawer: Drawer(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 4. 修改地方
Text(AppLocalizations.of(context).drawerTip, style: TextStyle(color: Colors.red, fontSize: 30)),
SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
// 5. 修改地方
child: Text(AppLocalizations.of(context).closeDrawer),
),
],
),
),
),
);
}
}
- 在
MaterialApp
的localizationsDelegates
中加入APPLocalizationDelegate.delegate
;- 用到文案的地方都用
AppLocalizations.of(context)
。
效果
- 手机语言是英文的效果:
- 手机语言是中文的效果:
- 手机语言是希伯来文的效果:
Flutter Intl 插件
上面我们实现了文案的国际化,为了更加简便,我们可以使用Flutter Intl插件来实现国际化。
Flutter Intl插件安装
初始化
VS Code
和Android Studio
都有Flutter Intl插件,由于我使用的是VS Code
,加上Android Studio
插件的安装和使用也很简单,本文仅介绍VS Code
上该插件的使用。
VS Code
安装Flutter Intl插件
利用Flutter Intl插件初始化国际化的相关文件
使用快捷键调出命令行工具(Mac电脑是Shift+Command+p
),然后选择Flutter Intl: Initialize
命令(第一次用可能看不到这个命令,也可以直接输入,我最近使用过所以这个命令在最上面),敲击回车确认。然后我们可以看到执行了flutter pub get
命令,然后在项目中生成了一堆新的文件。
生成的新的文件包括generated和l10n两个文件夹,然后还在pubspec.yaml
文件中加入了配置:
修改intl_en.arb
我们将en
语言下的文案放在intl_en.arb
这个文件中:
{
"title": "Localization Demo",
"button": "Button",
"drawer_tip": "This is the Drawer",
"close_drawer": "Close Drawer"
}
修改后记得执行flutter pub get
。
修改main.dart
import 'generated/l10n.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
GlobalWidgetsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
// 1. 修改地方
S.delegate,
],
// 2. 修改
supportedLocales: S.delegate.supportedLocales,
home: Home(),
);
}
}
class Home extends StatelessWidget {
@override
Widget build(context) {
return Scaffold(
// 3 修改地方
appBar: AppBar(title: Text(S.of(context).title)),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(
3,
(index) => ElevatedButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2022));
},
// 4. 修改
child: Text("${S.of(context).button} ${index + 1}"))),
),
),
drawer: Drawer(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 5. 修改
Text(S.of(context).drawer_tip, style: TextStyle(color: Colors.red, fontSize: 30)),
SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
// 6. 修改
child: Text(S.of(context).close_drawer),
),
],
),
),
),
);
}
}
- 在MaterialApp的
localizationsDelegates
中加入S.delegate
,supportedLocales
修改为S.delegate.supportedLocales
;- 所有使用的地方改成
S.of(context)
。
到目前为止英文en
的国际化就弄好了。
添加其他国际化的语言
利用Flutter Intl插件添加新的国际化语言
使用快捷键调出命令行工具,然后选择Flutter Intl: Add locale
命令(和前面类似,你可能直接看不到这个命令,可以在输入框中输入Intl, 这样会出现提示);
会出现输入框,在输入框中输入zh
,然后敲击回车。
等待一会儿,会自动生成两个和zh
相关的文件:messages_zh.dart
和intl_zh.arb
修改intl_zh.arb
文件
将中文的文案放在这个文件内:
{
"title": "国际化案例",
"button": "按钮",
"drawer_tip": "这是抽屉",
"close_drawer": "关闭抽屉"
}
最后不要忘了执行flutter pub get
。
he
的实现方式类似,不再重复介绍了。
intl_he.arb
文件内容如下:
{
"title": "הדגמת לוקליזציה",
"button": "לַחְצָן",
"drawer_tip": "מְגֵרָה",
"close_drawer": "סגור מגירה"
}
目前为止,所有需要做的工作就完成了,非常简单。
占位符传参
有时候文案中的某些部分最开始是不确定的,在运行的时候才能确定。譬如文案中有价格,但是这个价格不是固定的,这时候就需要先用一个占位符占位,然后在运行的时候用真实的数据替换掉这个占位符。
我们案例中的button
文案我们替换为为button {seq}
, לַחְצָן{seq}
和按钮 {seq}
。
在使用的时候我们可以改为Text("${S.of(context).button(index + 1)}")))
,这样的效果和前面的一样。
这种方式的好处由于不同语言表达方式不一样,不同语言翻译出来后的占位符的位置可以是任意的。
总结
Flutter官方提供的国际化方案对布局的国际化做的非常友好,文案的国际化在Flutter Intl插件的加持下也非常简单。