目录
1. 国际化Material组件库
2. 国际化开发人员的UI(Localizations)
国际化:支持多种语言。为应用程序支持的每种语言设置本地化值,如文本(语言差异)、布局(阅读方向差异)、图片(国旗)。
默认情况下,flutterSDK(为了尽可能小而且简单)中的组件仅提供美国英语值的本地化资源(文本)。为Material组件库添加其他语言的支持,须添加一个名为“flutter_localizations”的包依赖,然后在MaterialApp中进行一些配置。
/*
flutter_localizations 包(Material组件库的本地化实现)包含:
GlobalMaterialLocalizations和GlobalWidgetsLocalizations的本地化接口的多语言实现。
*/
Material组件库和开发人员的UI都需要进行国际化:
1. Material组件库
比如:日历组件默认在任何环境下都会以英文显示,所以需要国际化。
需要依赖flutter_localizations包。
2. 开发人员的UI。
需要实现Localizations。
/*
iOS需要在info.plist中添加Localizations项(在其中添加语言,默认有一个英文)
*/
简介
- 获取当前区域Locale
Locale类(包括语言和国家两个标志)用来标识用户的语言环境。
Locale('zh', 'CN') // 中文简体
获取应用的当前区域Locale
// Localizations 组件一般位于widget树中其它业务组件的顶部,它的作用是定义区域Locale以及设置子树依赖的本地化资源。
// 如果系统的语言环境发生变化,WidgetsApp将创建一个新的Localizations 组件并重建它,这样子树中通过Localizations.localeOf(context) 获取的Locale就会更新。
Locale myLocale = Localizations.localeOf(context);
// 返回:zh_CN
myLocale.toString()
- 监听系统语言切换
当更改系统语言设置时,APP中的Localizations组件会重新构建,Localizations.localeOf(context) 获取的Locale就会更新,最终界面会重新build达到切换语言的效果。
因为Localizations组件内部使用了InheritedWidget ,当子组件的build函数引用了InheritedWidget时会创建对InheritedWidget的隐式依赖关系,因此当InheritedWidget发生更改时(即Localizations的Locale设置发生更改时)将重建所有依赖它的子组件。
这个过程是隐式完成的,并没有主动去监听系统语言切换。
但是有时需要在系统语言发生改变时做一些事,比如系统语言切换为一种APP不支持的语言时,需要设置一个默认的语言,这时就需要 监听locale改变事件。
可以通过MaterialApp的以下2种方法来监听locale改变的事件
方法1. localeResolutionCallback回调函数:
Locale Function(Locale locale, Iterable supportedLocales)
方法2. localeListResolutionCallback(推荐)回调函数:
// 和localeResolutionCallback唯一的不同就在第一个参数类型
Locale Function(List locales, Iterable supportedLocales)
说明:
1. locale:当前手机系统设置的语言区域。
当应用启动时或用户动态改变系统语言设置时此locale即为系统的当前locale。
如果locale为null,则表示Flutter未能获取到设备的Locale信息,所以在使用locale之前一定要先判空。
注意:当开发者手动指定APP的locale时(MaterialApp的locale属性),那么此locale参数代表开发者指定的locale,将忽略系统locale,不再会因为设备语言改变而发生变化。
2. supportedLocales:当前应用支持的locale列表,MaterialApp通过supportedLocales属性注册的。
3. 返回值:一个Locale(应用最终使用的Locale)
通常会在不支持的语言区域时返回一个默认的Locale(默认使用语言)。
4. locales
用户在较新的Android系统手机设置中可以设置一个语言列表,应用通常的处理方式就是按照列表的顺序依次尝试加载相应的Locale,如果某一种语言加载成功则会停止。
在Flutter中,应该优先使用localeListResolutionCallback,不必担心Android系统的差异性,如果在低版本的Android系统中,Flutter会自动处理这种情况,这时Locale列表只会包含一项。
- 引用本地化值
通过Localizations.of(context,type)来引用本地化值。
Localizations组件用于加载和查找应用当前语言下的本地化值或资源。
本地化值由Localizations组件的LocalizationsDelegates列表加载。 每个委托必须定义一个异步load() 方法,以生成封装了一系列本地化值的对象。通常这些对象为每个本地化值定义一个方法。
在大型应用程序中,不同模块或Package可能会与自己的本地化值捆绑在一起, 这就是为什么要用Localizations组件 管理对象表的原因。 要使用由LocalizationsDelegate的load方法之一产生的对象,可以指定一个BuildContext和对象的类型来找到它。
例如:Material组件库的本地化字符串由MaterialLocalizations类定义,此类的实例由MaterialApp类提供的LocalizationDelegate创建。通过如下方式获取到:
Localizations.of(context, MaterialLocalizations);
这个特殊的Localizations.of()表达式会经常使用,所以MaterialLocalizations类提供了一个便捷方法:
static MaterialLocalizations of(BuildContext context) {
return Localizations.of(context, MaterialLocalizations);
}
使用:MaterialLocalizations.of(context).国际化字段名
1. 国际化Material组件库
第一步: 在pubspec.yaml文件中添加flutter_localizations依赖包(支持十几种语言),并下载
dependencies:
flutter_localizations:
sdk: flutter
第二步:配置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 组件支持多语言。GlobalMaterialLocalizations.delegate是一个产生GlobalMaterialLocalizations实例的LocalizationsDelegate。
GlobalWidgetsLocalizations.delegate定义组件默认的文本方向,从左到右或从右到左。
3. supportedLocales接收一个Locale数组,表示应用支持的语言列表。
当没有精确匹配(语言和地区同时匹配)时,使用语言。
如果没有匹配则使用supportedLocales列表项的第一个。
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(context, DemoLocalizations);
}
// Locale相关值,title为应用标题
String get title {
return isZh ? "Flutter应用" : "Flutter APP";
}
//... 其它的值
}
/*
方式2
class DemoLocalizations {
DemoLocalizations(this.locale);
final Locale locale;
static DemoLocalizations of(BuildContext context) {
return Localizations.of(context, DemoLocalizations);
}
static Map> _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 {
const DemoLocalizationsDelegate();
// 是否支持某个Local
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
// Flutter会调用此类加载相应的Locale资源类
@override
Future load(Locale locale) {
print("$locale");
return SynchronousFuture(
DemoLocalizations(locale.languageCode == "zh")
);
}
// shouldReload的返回值决定当Localizations组件重新build时,是否调用load方法重新加载Locale资源。一般情况下,Locale资源只应该在Locale切换时加载一次,不需要每次在Localizations重新build时都加载,所以返回false即可。
// 事实上,无论shouldReload返回true还是false,每当Locale改变时Flutter都会再调用load方法加载新的Locale。
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
/*
class DemoLocalizationsDelegate extends LocalizationsDelegate {
const DemoLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
@override
Future load(Locale locale) {
return new SynchronousFuture(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(context, DemoLocalizations);
}
static Map> _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 {
const DemoLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
@override
Future load(Locale locale) {
return new SynchronousFuture(new DemoLocalizations(locale));
}
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
当要支持的语言不是两种而是8种甚至20几种时,如果为每个文本属性都要分别去判断到底是哪种Locale从而获取相应语言的文本将会是一件非常复杂的事。而且通常情况下翻译人员并不是开发人员,能不能像i18n或l10n标准那样可以将翻译单独保存为一个arb文件交由翻译人员去翻译,翻译好之后开发人员再通过工具将arb文件转为代码呢。
解决:使用Intl包
使用Intl包的好处:
1. 轻松实现国际化
2. 将字符串文本分离成单独的文件,方便开发人员和翻译人员分工协作。
2. 使用Intl包
第一步:添加依赖、创建必要目录
添加依赖
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",
"@@locale":"zh_CH",
"title": "Flutter应用",
"@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 load(Locale locale) {
final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
// intl_translation命令会生成对应的方法
// initializeMessages()用来加载翻译的字符串
return initializeMessages(localeName).then((b) {
Intl.defaultLocale = localeName;
return new DemoLocalizations();
});
}
static DemoLocalizations of(BuildContext context) {
return Localizations.of(context, DemoLocalizations);
}
// 添加属性
String get title {
return Intl.message( // Intl.message用来查找
'Flutter APP',
name: 'title',
desc: 'Title for the Demo application',
);
}
/*
// 添加方法
String helloTitle(String name) {
return Intl.message( // Intl.message用来查找
'hello $name',
name: 'helloTitle',
desc: 'helloTitle',
args:[name],
);
}
*/
// 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 {
const DemoLocalizationsDelegate();
// 是否支持某个Local
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
// Flutter会调用此类加载相应的Locale资源类
@override
Future load(Locale locale) {
return DemoLocalizations.load(locale);
}
// 当Localizations Widget重新build时,是否调用load重新加载Locale资源.
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
第三步: 生成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
}
}
}
}
// 中文简体:intl_zh_Hans_CN.arb
// const Locale.fromSubtags(languageCode: 'zh',scriptCode: 'Hans',countryCode: 'CN'),
这个是默认的Locale资源文件,如果现在要支持中文大陆,只需要在该文件同级目录创建一个"intl_zh_CN.arb"文件,然后将"intl_messages.arb"的内容拷贝到"intl_zh_CN.arb"文件,接下来将英文翻译为中文即可,翻译后的"intl_zh_CN.arb"文件内容如下:
{
"@@last_modified": "2018-12-10T15:46:20.897228",
"@@locale":"zh_CN",
"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 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(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 {
const DemoLocalizationsDelegate();
//是否支持某个Local
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
// Flutter会调用此类加载相应的Locale资源类
@override
Future load(Locale locale) {
return DemoLocalizations.load(locale);
}
// 当Localizations Widget重新build时,是否调用load重新加载Locale资源.
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
static DemoLocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
常见问题
- 默认语言区域不对
在一些非大陆行货渠道买的一些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 locales, Iterable supportedLocales) {
// 判断当前locale是否为英语系国家,如果是直接返回Locale('en', 'US')
}