Flutter了解之国际化
目录
1. 国际化Material组件库
国际化:支持多种语言。
为应用程序支持的每种语言设置本地化值,如文本(语言差异)、布局(阅读方向差异)、图片(国旗)。
为了尽可能小而且简单,flutter包中仅提供美国英语值的MaterialLocalizations和WidgetsLocalizations接口的实现(实现类分别称为DefaultMaterialLocalizations和DefaultWidgetsLocalizations)。
所以需要为Material组件库和开发人员的UI进行国际化:
1. Material组件库
比如:日历组件默认在任何环境下都会以英文显示,所以需要国际化。
需要依赖flutter_localizations包。flutter_localizations包包含GlobalMaterialLocalizations和GlobalWidgetsLocalizations的本地化接口的多语言实现。
2. 开发人员的UI。
需要实现Localizations。
获取当前区域Locale
Locale类(包括语言和国家两个标志)用来标识用户的语言环境
const Locale('zh', 'CN') // 中文简体
获取应用的当前区域Locale:
// Localizations 组件一般位于widget树中其它业务组件的顶部,它的作用是定义区域Locale以及设置子树依赖的本地化资源。
// 如果系统的语言环境发生变化,WidgetsApp将创建一个新的Localizations 组件并重建它,这样子树中通过Localizations.localeOf(context) 获取的Locale就会更新。
Locale myLocale = Localizations.localeOf(context);
监听系统语言切换
当更改系统语言设置时,APP中的Localizations组件会重新构建,Localizations.localeOf(context) 获取的Locale就会更新,最终界面会重新build达到切换语言的效果。因为Localizations内部使用了InheritedWidget ,当子组件的build函数引用了InheritedWidget时会创建对InheritedWidget的隐式依赖关系,因此当InheritedWidget发生更改时(即Localizations的Locale设置发生更改时)将重建所有依赖它的子组件。
但是这个过程是隐式完成的,我们并没有主动去监听系统语言切换,但是有时需要在系统语言发生改变时做一些事,比如系统语言切换为一种APP不支持的语言时,需要设置一个默认的语言,这时就需要监听locale改变事件。
可以通过localeResolutionCallback或localeListResolutionCallback回调来监听locale改变的事件:
1. localeResolutionCallback的回调函数签名:
Locale Function(Locale locale, Iterable<Locale> supportedLocales)
说明:
1. 参数locale的值为当前的当前的系统语言设置,当应用启动时或用户动态改变系统语言设置时此locale即为系统的当前locale。如果locale为null,则表示Flutter未能获取到设备的Locale信息,所以在使用locale之前一定要先判空。当开发者手动指定APP的locale时,那么此locale参数代表开发者指定的locale,此时将忽略系统locale
2. supportedLocales 为当前应用支持的locale列表,是开发者在MaterialApp中通过supportedLocales属性注册的。
3. 返回值是一个Locale,此Locale为Flutter APP最终使用的Locale。通常在不支持的语言区域时返回一个默认的Locale。
2. localeListResolutionCallback的回调函数签名:
Locale Function(List<Locale> locales, Iterable<Locale> supportedLocales)
和localeResolutionCallback唯一的不同就在第一个参数类型,前者接收的是一个Locale列表,而后者接收的是单个Locale。
在较新的Android系统中,用户可以设置一个语言列表,这样一来,支持多语言的应用就会得到这个列表,应用通常的处理方式就是按照列表的顺序依次尝试加载相应的Locale,如果某一种语言加载成功则会停止。
在Flutter中,应该优先使用localeListResolutionCallback,不必担心Android系统的差异性,如果在低版本的Android系统中,Flutter会自动处理这种情况,这时Locale列表只会包含一项。
如果手动指定了应用locale为美国英语,则不会因为设备语言改变而发生变化。
MaterialApp(
locale: const Locale('en', 'US'), // 手动指定locale
)
引用本地化值
通过Localizations.of(context,type)来引用本地化值。
本地化值由Localizations的 LocalizationsDelegates 列表加载 。 每个委托必须定义一个异步load() 方法,以生成封装了一系列本地化值的对象。通常这些对象为每个本地化值定义一个方法。
在大型应用程序中,不同模块或Package可能会与自己的本地化值捆绑在一起, 这就是为什么要用Localizations 管理对象表的原因。
便捷方法
Material 组件库的本地化字符串由MaterialLocalizations类定义,此类的实例由MaterialApp类提供的LocalizationDelegate创建。通过如下方式获取到:
Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
便捷方法
static MaterialLocalizations of(BuildContext context) {
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}
方便使用:MaterialLocalizations.of(context).国际化字段名
1. 国际化Material组件库
1. 首先需要添加flutter_localizations依赖包(支持十几种语言)到pubspec.yaml文件中,并下载:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
2. 配置MaterialApp(指定localizationsDelegates和supportedLocales)
import 'package:flutter_localizations/flutter_localizations.dart';
new MaterialApp(
localizationsDelegates: [
// 本地化的代理类
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // 美国英语
const Locale('zh', 'CN'), // 中文简体
//其它Locales
],
// ...
)
说明:
1. 多数应用程序都是通过MaterialApp(对WidgetsApp进行了包装)为入口,低级别的WidgetsApp为入口时也可以使用相同的类和逻辑进行国际化。与MaterialApp类为入口的应用不同, 对基于WidgetsApp类为入口的应用程序进行国际化时,不需要GlobalMaterialLocalizations.delegate。
2. localizationsDelegates列表中的元素是生成本地化值集合的工厂。 每个委托必须定义一个异步load() 方法,以生成封装了一系列本地化值的对象。
GlobalMaterialLocalizations.delegate 为Material 组件库提供的本地化的字符串和其他值,它可以使Material 组件支持多语言。
GlobalWidgetsLocalizations.delegate定义组件默认的文本方向,从左到右或从右到左。
3. supportedLocales接收一个Locale数组,表示应用支持的语言列表。当没有精确匹配(语言和地区同时匹配)时,使用语言。如果没有匹配则使用supportedLocales列表项的第一个。
4. 全局本地化delegates构造相应类的特定于语言环境的实例。例如,GlobalMaterialLocalizations.delegate是一个产生GlobalMaterialLocalizations实例的LocalizationsDelegate。
2. 国际化开发人员的UI(Localizations)
示例
第一步:实现Localizations类(提供本地化资源值,如文本)
// Locale资源类
// 会根据当前的语言来获取本地化资源值。可以将所有需要支持多语言的文本都在此类中定义,该类的实例会在Delegate类的load方法中创建。
class DemoLocalizations {
DemoLocalizations(this.isZh);
// 是否为中文
bool isZh = false;
// 为了使用方便,定义一个静态方法
static DemoLocalizations of(BuildContext context) {
// MaterialApp组件内部嵌套了Localizations组件,通过多第三步配置MaterialApp的localizationsDelegates,会将DemoLocalizationsDelegate传给Localizations组件
// 获取DemoLocalizations实例
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
// Locale相关值,title为应用标题
String get title {
return isZh ? "Flutter应用" : "Flutter APP";
}
//... 其它的值
}
/*
class DemoLocalizations {
DemoLocalizations(this.locale);
final Locale locale;
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'Hello World',
},
'es': {
'title': 'Hola Mundo',
},
};
String get title {
return _localizedValues[locale.languageCode]['title'];
}
}
*/
第二步:实现Delegate类(在Locale改变时会从DemoLocalizations中加载新的本地化资源值)
// Locale代理类
// Delegate类需要继承自LocalizationsDelegate类,实现相应的接口,有一个load方法。
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
// 是否支持某个Local
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
// Flutter会调用此类加载相应的Locale资源类
@override
Future<DemoLocalizations> load(Locale locale) {
print("$locale");
return SynchronousFuture<DemoLocalizations>(
DemoLocalizations(locale.languageCode == "zh")
);
}
@override
// shouldReload的返回值决定当Localizations组件重新build时,是否调用load方法重新加载Locale资源。一般情况下,Locale资源只应该在Locale切换时加载一次,不需要每次在Localizations重新build时都加载,所以返回false即可。事实上,无论shouldReload返回true还是false,每当Locale改变时Flutter都会再调用load方法加载新的Locale。
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
/*
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
@override
Future<DemoLocalizations> load(Locale locale) {
return new SynchronousFuture<DemoLocalizations>(new DemoLocalizations(locale));
}
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
*/
第三步:配置MaterialApp的localizationsDelegates
在MaterialApp或WidgetsApp的localizationsDelegates列表中添加Delegate实例即可完成注册
localizationsDelegates: [
// 本地化的代理类
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
// 注册我们的Delegate
DemoLocalizationsDelegate(), // 或DemoLocalizationsDelegate.delegate
],
第四步:在Widget中使用本地化资源值
return Scaffold(
appBar: AppBar(
// 使用Locale title
title: Text(DemoLocalizations.of(context).title),
),
... //省略无关代码
)
完整代码如下
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart' show SynchronousFuture;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.yellow,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home:MyHomePage(),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
DemoLocalizationsDelegate.delegate,
],
supportedLocales: [
const Locale('zh', 'CH'),
const Locale('en', 'US'),
],
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(DemoLocalizations.of(context).title),
),
body: new Center(
child: new Text(DemoLocalizations.of(context).content),
),
);
}
}
class DemoLocalizations {
DemoLocalizations(this.locale);
final Locale locale;
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'Home',
'content': 'Hello World'
},
'zh': {
'title': '首页',
'content': '世界 你好'
},
};
String get title {
return _localizedValues[locale.languageCode]['title'];
}
String get content {
return _localizedValues[locale.languageCode]['content'];
}
}
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
@override
Future<DemoLocalizations> load(Locale locale) {
return new SynchronousFuture<DemoLocalizations>(new DemoLocalizations(locale));
}
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
![](https://img.haomeiwen.com/i5111884/b3b53a75d3bbe552.png)
![](https://img.haomeiwen.com/i5111884/87cbacf747998df1.png)
当要支持的语言不是两种而是8种甚至20几种时,如果为每个文本属性都要分别去判断到底是哪种Locale从而获取相应语言的文本将会是一件非常复杂的事。
2. 使用Intl包(轻松实现国际化)
好处:
1. 轻松实现国际化
2. 将字符串文本分离成单独的文件,方便开发人员和翻译人员分工协作。
第一步:添加依赖、创建必要目录
添加依赖
dependencies:
#...省略无关项
intl: ^0.15.7
dev_dependencies:
#...省略无关项
intl_translation: ^0.17.2
说明:
1. intl_translation包主要包含了一些工具,它在开发阶段主要主要的作用是从代码中提取要国际化的字符串到单独的arb文件和根据arb文件生成对应语言的dart代码。
2. intl包主要是引用和加载intl_translation生成后的dart代码。
创建必要目录
在项目根目录下创建一个l10n-arb目录,该目录保存接下来通过intl_translation命令生成的arb文件。
在lib目录下创建一个l10n的目录,该目录用于保存从arb文件生成的dart代码文件。
/*
arb文件示例(通过intl_translation命令自动生成)JSON格式:
{
"@@last_modified": "2020-09-23T12:54:51.602843",
"title": "Flutter APP",
"@title": {
"description": "Title for the Demo application",
"type": "text",
"placeholders": {}
}
}
*/
第二步:实现Localizations类(添加需要国际化的属性)和Delegate类
在lib/l10n目录下新建一个“localization_intl.dart”的文件
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
// import 'messages_all.dart'; // intl_translation从arb文件生成的dart代码
// 可以在DemoLocalizations类中添加需要国际化的属性或方法
class DemoLocalizations {
static Future<DemoLocalizations> load(Locale locale) {
final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
// initializeMessages()方法和"messages_all.dart"文件一样,是同时生成的。
// initializeMessages()用来加载翻译的字符串
// return initializeMessages(localeName).then((b) {
// Intl.defaultLocale = localeName;
// return new DemoLocalizations();
// });
}
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
// 添加属性
String get title {
return Intl.message( // Intl.message用来查找
'Flutter APP',
name: 'title',
desc: 'Title for the Demo application',
);
}
// 添加方法
// Intl.plural方法可以在howMany值不同时输出不同的提示信息
remainingEmailsMessage(int howMany) => Intl.plural(howMany,
zero: 'There are no emails left',
one: 'There is $howMany email left',
other: 'There are $howMany emails left',
name: "remainingEmailsMessage",
args: [howMany],
desc: "How many emails remain after archiving.",
examples: const {'howMany': 110, 'userName': 'Fred'});
}
// Locale代理类
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
//是否支持某个Local
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
// Flutter会调用此类加载相应的Locale资源类
@override
Future<DemoLocalizations> load(Locale locale) {
//
return DemoLocalizations.load(locale);
}
// 当Localizations Widget重新build时,是否调用load重新加载Locale资源.
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
}
第三步: 生成arb文件、根据arb文件生成dart代码
生成arb文件
提取localization_intl.dart代码中的字符串到一个arb文件(通intl_translation包的工具),运行如下命名:
flutter pub pub run intl_translation:extract_to_arb --output-dir=l10n-arb lib/l10n/localization_intl.dart
运行此命令后,会将之前通过Intl API标识的属性和字符串提取到“l10n-arb/intl_messages.arb”文件中,看看其内容:
{
"@@last_modified": "2020-09-23T12:54:51.602843",
"title": "Flutter APP",
"@title": {
"description": "Title for the Demo application",
"type": "text",
"placeholders": {}
},
"remainingEmailsMessage": "{howMany,plural, =0{There are no emails left}=1{There is {howMany} email left}other{There are {howMany} emails left}}",
"@remainingEmailsMessage": {
"description": "How many emails remain after archiving.",
"type": "text",
"placeholders": {
"howMany": {
"example": 110
}
}
}
}
这个是默认的Locale资源文件,如果现在要支持中文简体,只需要在该文件同级目录创建一个"intl_zh_CH.arb"文件,然后将"intl_messages.arb"的内容拷贝到"intl_zh_CN.arb"文件,接下来将英文翻译为中文即可,翻译后的"intl_zh_CN.arb"文件内容如下:
{
"@@last_modified": "2018-12-10T15:46:20.897228",
"@@locale":"zh_CH",
"title": "Flutter应用",
"@title": {
"description": "Title for the Demo application",
"type": "text",
"placeholders": {}
},
"remainingEmailsMessage": "{howMany,plural, =0{没有未读邮件}=1{有{howMany}封未读邮件}other{有{howMany}封未读邮件}}",
"@remainingEmailsMessage": {
"description": "How many emails remain after archiving.",
"type": "text",
"placeholders": {
"howMany": {
"example": 42
}
}
}
}
必须要翻译title和remainingEmailsMessage字段,description是该字段的说明,通常给翻译人员看,代码中不会用到。
注意:
1. 如果某个特定的arb中缺失某个属性,那么应用将会加载默认的arb文件(intl_messages.arb)中的相应属性,这是Intl的托底策略。
2. 每次运行提取命令时,intl_messages.arb都会根据代码重新生成,但其他arb文件不会,所以当要添加新的字段或方法时,其他arb文件是增量的,不用担心会覆盖。
3. arb文件是标准的。通常会将arb文件交给翻译人员,当他们完成翻译后,再通过下面的步骤根据arb文件生成最终的dart代码。
生成dart代码
根据arb生成dart文件:
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/localization_intl.dart l10n-arb/intl_*.arb
在首次运行时会在"lib/l10n"目录下生成多个文件,对应多种Locale,这些代码便是最终要使用的dart代码。
优化第三步
在根目录下创建一个intl.sh的脚本,内容为:
flutter pub pub run intl_translation:extract_to_arb --output-dir=l10n-arb lib/l10n/localization_intl.dart
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/localization_intl.dart l10n-arb/intl_*.arb
授予执行权限:
chmod +x intl.sh
执行intl.sh
./intl.sh
完整代码
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/localization_intl.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.yellow,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home:MyHomePage(),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
DemoLocalizationsDelegate.delegate,
],
supportedLocales: [
const Locale('zh', 'CH'),
const Locale('en', 'US'),
],
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(DemoLocalizations.of(context).title),
),
body: new Center(
child: new Text(DemoLocalizations.of(context).content),
),
);
}
}
localization_intl.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'messages_all.dart'; // intl_translation从arb文件生成的dart代码
class DemoLocalizations {
static Future<DemoLocalizations> load(Locale locale) {
final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
// initializeMessages()方法和"messages_all.dart"文件一样,是同时生成的。
// initializeMessages()用来加载翻译的字符串
return initializeMessages(localeName).then((b) {
Intl.defaultLocale = localeName;
return new DemoLocalizations();
});
}
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
// Intl.message用来查找
String get title {
return Intl.message(
'Home',
name: 'title',
desc: 'Title for the Demo application',
);
}
String get content {
return Intl.message(
'Hello world',
name: 'content',
desc: 'Content for the Demo application',
);
}
remainingEmailsMessage(int howMany) => Intl.plural(howMany,
zero: 'There are no emails left',
one: 'There is $howMany email left',
other: 'There are $howMany emails left',
name: "remainingEmailsMessage",
args: [howMany],
desc: "How many emails remain after archiving.",
examples: const {'howMany': 110, 'userName': 'Fred'});
}
// Locale代理类
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
//是否支持某个Local
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
// Flutter会调用此类加载相应的Locale资源类
@override
Future<DemoLocalizations> load(Locale locale) {
return DemoLocalizations.load(locale);
}
// 当Localizations Widget重新build时,是否调用load重新加载Locale资源.
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
![](https://img.haomeiwen.com/i5111884/46a938a0d9999a5f.png)
常见问题
默认语言区域不对
在一些非大陆行货渠道买的一些Android和iOS设备,会出现默认的Locale不是中文简体的情况。这属于正常现象,但是为了防止设备获取的Locale与实际的地区不一致,所有的支持多语言的APP都必须提供一个手动选择语言的入口。
对应用标题进行国际化
MaterialApp有一个title属性,用于指定APP的标题。在Android系统中,APP的标题会出现在任务管理器中。所以也需要对title进行国际化。
但是问题是很多国际化的配置都是在MaterialApp上设置的,我们无法在构建MaterialApp时通过Localizations.of来获取本地化资源,如:
MaterialApp(
title: DemoLocalizations.of(context).title, //不能正常工作!
localizationsDelegates: [
// 本地化的代理类
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
DemoLocalizationsDelegate() // 设置Delegate
],
);
运行后,DemoLocalizations.of(context).title会报错,这里DemoLocalizations.of(context)会返回null,这里的context找不到MaterialApp,继而找不到DemoLocalizationsDelegate。
只需要设置一个onGenerateTitle回调即可:
MaterialApp(
onGenerateTitle: (context){
// 此时context在Localizations的子树中
return DemoLocalizations.of(context).title;
},
localizationsDelegates: [
DemoLocalizationsDelegate(),
...
],
);
为英语系的国家指定同一个locale
英语系的国家非常多,如美国、英国、澳大利亚等,这些英语系国家虽然说的都是英语,但也会有一些区别。如果我们的APP只想提供一种英语(如美国英语)供所有英语系国家使用
localeListResolutionCallback中来做兼容:
localeListResolutionCallback:
(List<Locale> locales, Iterable<Locale> supportedLocales) {
// 判断当前locale是否为英语系国家,如果是直接返回Locale('en', 'US')
}