Flutter的国际化问题详解
Internationalization vs Localization
Internationalization(国际化,简写为i18n), Localization(本地化,简写为l10n)。两者有稍微些许的差别,国际化为了适配不同的语言和不同的地区的文化,国际化是在开发阶段。本地化是为了让应用展示地更适合本地人的文化,以及个人习惯。本地化是在进入某个市场前做得适配工作,为了遵守当地的法律法规,功能做的简单调整。
国际化语言简单适配
如果国际化语言种类少,需要翻译的字符串条目少,则可以使用下面的方式;否则,需要使用后面用到的 gen-l10n方式。
import 'dart:async';
import 'package:flutter/foundation.dart' show SynchronousFuture;
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class DemoLocalizations {
DemoLocalizations(this.locale);
final Locale locale;
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations)!;
}
//1
static const _localizedValues = <String, Map<String, String>>{
'en': {
'title': 'Hello World',
},
'es': {
'title': 'Hola Mundo',
},
};
//2
static List<String> languages() => _localizedValues.keys.toList();
//3
String get title {
return _localizedValues[locale.languageCode]!['title']!;
}
}
// #docregion delegate
//4
class DemoLocalizationsDelegate
extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
//5
@override
bool isSupported(Locale locale) =>
DemoLocalizations.languages().contains(locale.languageCode);
@override
Future<DemoLocalizations> load(Locale locale) {
// Returning a SynchronousFuture here because an async "load" operation
// isn't needed to produce an instance of DemoLocalizations.
return SynchronousFuture<DemoLocalizations>(DemoLocalizations(locale));
}
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
}
// #enddocregion delegate
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//8
title: Text(DemoLocalizations.of(context).title),
),
body: Center(
child: Text(DemoLocalizations.of(context).title),
),
);
}
}
class Demo extends StatelessWidget {
const Demo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (context) => DemoLocalizations.of(context).title,
//6
localizationsDelegates: const [
DemoLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
//7
supportedLocales: const [
Locale('en', ''),
Locale('es', ''),
],
// Watch out: MaterialApp creates a Localizations widget
// with the specified delegates. DemoLocalizations.of()
// will only find the app's Localizations widget if its
// context is a child of the app.
home: const DemoApp(),
);
}
}
void main() {
runApp(const Demo());
}
国际化语言简单适配,有8点注释说明对应上面代码中的数字标记的注释。
- 声明一个类,定义需要支持的语言种类集合,以及需要翻译的所有字符值名称以及对应的字符串之间的映射。
- 定义需要支持的语言种类的集合。
- 每一个字符串创建一个属性或者方法,查找某种语言的字符串的值。
- 创建对应的代理类继承自LocalizationsDelegate类。
- 重写LocalizationsDelegate子类(步骤4声明类)的isSupported方法,定义支持的语言种类。
- MaterialApp的构造方法中localizationsDelegates参数值中添加步骤4声明类的实例。
- MaterialApp的构造方法中supportedLocales参数设置支持的语音。
- 使用步骤1声明类的of的静态方法,获取步骤1声明类的实例,调用步骤3或许相应的字符串,例如:DemoLocalizations.of(context).title。此方法如果运行在MaterialApp的构造方法之前调用,会提示空指针错误。可以在MaterialApp的子Widget中的build方法中调用。
使用gen-l10n适配国际化中语言
语言翻译是国际化过程中常见的问题,我们需要根据系统设置的语言不同,我们的应用显示不同的语言。国际化过程中不得不提Locale这个类,它的构造方法包括两个参数,第一个参数是语言,第二个参数是国家或地区,语言必须设置,国家和地区可以是可选的。例如,简体中文和繁体中文是有区别的,通过l10n.yaml 文件,你可以配置gen-l10n工具。例如:
创建app_zh_CN.arb为简体中文(中国大陆),创建app_zh_TW.arb为繁体中文 (台湾地区)。保证每个arb文件中包括完整的,合适的字符串翻译。 步骤如下所示:
- intl package添加为依赖。
intl: any
flutter_localizations:
sdk: flutter
- 另外,在pubspec.yaml文件中,flutter pacage启用generate标志(也就是generate属性设置为true)。
- 在项目根目录下面新建i10n.yaml配置文件,内容如下:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
use-escaping: true
4.在项目根目录/lib/l10n 中,添加 app_en.arb模板文件。以及其他需要支持的语言文件。 5. 现在,运行 flutter run 命令,你将在 项目根目录/.dart_tool/flutter_gen/gen_l10n 中看到生成的文件。同样的,你可以在应用没有运行的时候运行,在命令行运行flutter gen-l10n命令来生成本地化文件。如下图所示:
生成的app_locationlizations_en.dart代码如下:
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get helloWorld => 'Hello World!';
@override
String hello(String userName) {
return 'Hello $userName';
}
@override
String nWombats(num count) {
final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact(
locale: localeName,
);
final String countString = countNumberFormat.format(count);
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$countString wombats',
one: '1 wombat',
zero: 'no wombats',
);
return '$_temp0';
}
@override
String pronoun(String gender) {
String _temp0 = intl.Intl.selectLogic(
gender,
{
'male': 'he',
'female': 'she',
'other': 'they',
},
);
return '$_temp0';
}
@override
String numberOfDataPoints(int value) {
final intl.NumberFormat valueNumberFormat = intl.NumberFormat.compactCurrency(
locale: localeName,
decimalDigits: 2
);
final String valueString = valueNumberFormat.format(value);
return 'Number of data points: $valueString';
}
@override
String get nationalFlag => 'images/england.png';
@override
String helloWorldOn(DateTime date) {
final intl.DateFormat dateDateFormat = intl.DateFormat.yMd(localeName);
final String dateString = dateDateFormat.format(date);
return 'Hello World on $dateString';
}
@override
String get helloWorldWonderful => 'Hello! Isn\'t this a \nwonderful day?';
}
- 注意nationalFlag这个属性,可以理解为图片的国际化。当然也可以使用方法,代码如下的:
String getIconPath(Locale locale) {
switch (locale.languageCode) {
case 'es':
return 'assets/icons/zh/icon.png'; // 中国icon
case 'en':
default:
return 'assets/icons/en/icon.png'; // Default to English icon
}
}
7.在调用MaterialApp的构造函数时候,添加 import语句,导入app_localizations.dart和AppLocalizations.delegate。代码使用:
import 'package:flutter/material.dart';
// #docregion app-localizations-import
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// #enddocregion app-localizations-import
// #docregion localization-delegates-import
import 'package:flutter_localizations/flutter_localizations.dart';
// #enddocregion localization-delegates-import
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// AppLocalizations.of(context)!.helloWorld; NullPointerException
// #docregion material-app
return const MaterialApp(
title: 'Localizations Sample App',
localizationsDelegates: [
AppLocalizations.delegate, // Add this line
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
Locale('en'), // English
Locale('zh'), // Spanish
],
home: MyHomePage(),
);
// #enddocregion material-app
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
// #docregion internationalized-title
appBar: AppBar(
// The [AppBar] title text should update its message
// according to the system locale of the target platform.
// Switching between English and Spanish locales should
// cause this text to update.
title: Text(AppLocalizations.of(context)!.helloWorld),
),
// #enddocregion internationalized-title
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Add the following code
Localizations.override(
context: context,
// locale: const Locale('en'),
// Using a Builder here to get the correct BuildContext.
// Alternatively, you can create a new widget and Localizations.override
// will pass the updated BuildContext to the new widget.
child: Builder(
builder: (context) {
// #docregion placeholder
// Examples of internationalized strings.
return Column(
children: <Widget>[
// Returns 'Hello John'
Text(AppLocalizations.of(context)!.hello('John')),
// Returns 'no wombats'
Text(AppLocalizations.of(context)!.nWombats(0)),
// Returns '1 wombat'
Text(AppLocalizations.of(context)!.nWombats(1)),
// Returns '5 wombats'
Text(AppLocalizations.of(context)!.nWombats(5)),
// Returns 'he'
Text(AppLocalizations.of(context)!.pronoun('male')),
// Returns 'she'
Text(AppLocalizations.of(context)!.pronoun('female')),
// Returns 'they'
Text(AppLocalizations.of(context)!.pronoun('other')),
Text(AppLocalizations.of(context)!.numberOfDataPoints(15)),
Text(AppLocalizations.of(context)!.helloWorldWonderful),
Image.asset(AppLocalizations.of(context)!.nationalFlag, width: 60, height: 60,),
Text(AppLocalizations.of(context)!.helloWorldOn(DateTime.utc(1959, 7, 9)))
],
);
// #enddocregion placeholder
},
),
),
],
),
),
);
}
}
void main() {
runApp(const MyApp());
}
注意以下几点:
- Localizations.override方法可以指定本地化语言参数,如locale: const Locale('en')设置为英语。
- localizationsDelegates参数集合里需要增加AppLocalizations.delegate。
- 利用AppLocalizations.of(context)方法,最终会通过下面的方法找到最终的实现实例,然后调用相关的属性或者方法。
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en': return AppLocalizationsEn();
case 'zh': return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'
);
}
可以简单地看一下运行效果。
Android中的字符串会通过Context的getString方法传递String Id来直接获取,语言变化的时候也会重新加载,重新刷新页面,显示新的语言。Flutter字符串的国际化和Android是不一样的,Flutter会把字符串的国际化,不同的语音生成不同的类,类里面有相应的方法或者属性。
总结
国际化是在软件推广到不同国家或者地区需要做的事情,我们应该尊重本地区的宗教信仰以及文化习惯,同时也需要遵守当地的法律法规,做相应的调整。希望文章对您有帮助,如果文中有问题,希望您不吝指教。
转载自:https://juejin.cn/post/7392806046699175947