本地化是 app 常见的需求,本文介绍如何在 flutter 中添加本地化支持,并提供详细的步骤说明以及可供复现的 demo
安装
项目做本地化通常是 intl 和 flutter_localizations 搭配使用。下面是项目的 pubspec.yaml 文件,除了安装前面提到的两个包外,还需要设置 generate: true 以供使用
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.19.0
flutter:
uses-material-design: true
generate: true
有一点需要注意的是,flutter_localizations 对使用的 intl 有版本限制,比如目前限定为 0.19.0。如果版本不匹配,你会遇到类似的错误:
Because test_drive depends on flutter_localizations from sdk which depends on intl 0.19.0, intl 0.19.0 is
required.
So, because test_drive depends on intl ^0.20.0, version solving failed
接着我们创建语言文件,计划支持两种语言,英文和简体中文
app_en.arb
{
"@@locale": "en",
"title": "Flutter Demo",
"message": "Hello World",
"greeting": "Hello {userName}",
"@greeting": {
"placeholders": {
"userName": {
"type": "String"
}
}
}
}
app_zh.arb
{
"@@locale": "zh",
"title": "Flutter 示例",
"message": "你好,世界",
"greeting": "你好,{userName}",
"@greeting": {
"placeholders": {
"userName": {
"type": "String"
}
}
}
}
这两个文件放在 lib 下的 l10n 目录,树形结构如下
lib
├── l10n
│ ├── app_en.arb
│ └── app_zh.arb
另外,需要注意的是,l10n 的首字母是 L 转小写(有些字体很难看出与 i 转大写后的 I 的区别),很容易与 i18n 混淆,实际上它俩是同样的命名规则,包含首尾以及中间字母的数量,l10n 的全程是 localization
@@locale 是一个特殊的 key,它用于指定 ARB(Application Resource Bundle) 文件中的语言环境,告诉本地化生成工具当前文件代表哪个语言;greeting 和 @greeting 组合在一起,提供了插值写法的示例,本地化生成工具会生成一个 greeting 方法,传入 userName 字符串来生成完整的字符串
接着就是使用 gen-l10n 命令了,它的作用是 Flutter 提供的一个工具,用于简化和自动化本地化(Localization)的过程
通过 command 传参比较麻烦,我们可以建一个 l10n.yaml 放在项目的根目录下
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
接着使用命令,会遇到这样的警告,其实是可以忽略的
Because l10n.yaml exists, the options defined there will be used instead.
To use the command line arguments, delete the l10n.yaml file in the Flutter project.
一般情况下,你不需要手动执行 flutter gen-l10n 命令,当你启动项目(flutter run)或构建项目(flutter build apk 或 flutter build ios)都会自动执行 gen-l10n 命令
然后你会在项目根目录下的 .dart_tool 看到有文件生成,自此,前期安装的准备工作完成
.dart_tool
├── flutter_gen
│ ├── gen_l10n
│ │ ├── app_localizations.dart
│ │ ├── app_localizations_en.dart
│ │ └── app_localizations_zh.dart
│ └── pubspec.yaml
使用(简单 demo)
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
static void setLocale(BuildContext context, Locale newLocale) {
final MyAppState state = context.findAncestorStateOfType<MyAppState>()!;
state.setLocale(newLocale);
}
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
late Locale _locale;
void setLocale(Locale locale) {
setState(() {
_locale = locale;
});
}
@override
void initState() {
super.initState();
_locale = const Locale('en'); // Set default locale
}
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: _locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)?.title ?? ''),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(AppLocalizations.of(context)?.message ?? ''),
Text(AppLocalizations.of(context)?.greeting(
Localizations.localeOf(context).languageCode == 'en'
? 'User'
: '用户') ??
''),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// Toggle between English and Chinese
Locale newLocale =
Localizations.localeOf(context).languageCode == 'en'
? const Locale('zh')
: const Locale('en');
MyApp.setLocale(context, newLocale);
},
child: const Text('切换语言'),
),
],
),
),
);
}
}
上面给出了一个简单的 demo,演示如何使用文本和切换语言,需要在 MaterialApp 里面配置,使用 key 在 AppLocalizations.of(context) 里面拿数据,通过更改 MaterialApp 的 locale 参数切换语言
优化
实际上,app 语言逻辑不会像上面演示的那么简单,通常会先判断系统的默认语言,如果系统语言是中文,我们肯定就不能默认英文了,然后如果用户在系统语言设置了中文的情况下,仍然设置 app 内的语言为英文,那我们肯定得设置成英文了,所以优先级应该是 用户设置 > 系统默认 > App 默认;因此,我们需要通过持久化存储把用户的设置给存储起来;我们可以建立一个 manager 来管理本地化需求
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocaleManager with ChangeNotifier {
Locale _locale = const Locale('en');
static const String _languageKey = 'language';
Locale get locale => _locale;
LocaleManager() {
_loadLocale();
}
void setLocale(Locale locale) async {
if (!AppLocalizations.supportedLocales.contains(locale)) return;
_locale = locale;
notifyListeners();
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString(_languageKey, locale.languageCode);
}
void _loadLocale() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? languageCode = prefs.getString(_languageKey);
if (languageCode != null) {
// 用户设置
_locale = Locale(languageCode);
} else {
final systemLanguageCode =
WidgetsBinding.instance.platformDispatcher.locale.languageCode;
final List<String> supportLanguageCodes = AppLocalizations
.supportedLocales
.map((locale) => locale.languageCode)
.toList();
if (supportLanguageCodes.contains(systemLanguageCode)) {
// 系统默认
_locale = Locale(systemLanguageCode);
} else {
// App 默认
_locale = const Locale('en');
}
}
notifyListeners();
}
}
上述代码在 _loadLocale 中执行了 用户设置 > 系统默认 > App 默认 的优先级处理,为什么使用 languageCode 呢?如果直接使用 Locale,会发现带有一些无关信息,比如 iOS 设置中文返回的是 zh_Hans_CN,很难兼顾处理。而使用 languageCode 就可以拿出 zh,方便我们匹配处理
然后就可以使用了,应用示例如下
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:test_drive/utils/locale_manager.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => LocaleManager(),
child: const MyApp(),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
final localeProvider = Provider.of<LocaleManager>(context);
return MaterialApp(
locale: localeProvider.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)?.title ?? ''),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(AppLocalizations.of(context)?.message ?? ''),
Text(AppLocalizations.of(context)?.greeting(
Localizations.localeOf(context).languageCode == 'en'
? 'User'
: '用户') ??
''),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// Toggle between English and Chinese
Locale newLocale =
Localizations.localeOf(context).languageCode == 'en'
? const Locale('zh')
: const Locale('en');
final provider =
Provider.of<LocaleManager>(context, listen: false);
provider.setLocale(newLocale);
},
child: const Text('切换语言'),
),
],
),
),
);
}
}