0%

关于 Flutter 项目的国际化,只需要做对这几步

最近刚把公司项目的国际化搞完,顺便记录下Flutter项目国际化的几个步骤,来供大家参考。

App 项目为什么需要国际化?

公司项目的国际化需求来得很突然,就是因为要参加国外的展会,方便展示给客户使用App的操作步骤。正常情况下国际化无非是通过将应用程序本地化为不同的语言和地区,可以使应用在全球范围内更具吸引力,从而扩大市场覆盖范围。当你的应用能够以用户熟悉和舒适的语言呈现时,吸引更多的用户成为可能,同时也提升用户的使用体验,或者是遵守当地法律法规及政策的要求。

国际化需要做哪些准备?

以下是示例项目所在的电脑的开发环境。

1
2
3
joe@wf ~ % flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel unknown, 3.7.1, on macOS 13.6 22G120 darwin-arm64 (Rosetta), locale zh-Hans-CN)

Dart SDK

1
2
joe@wf ~ % dart --version
Dart SDK version: 2.19.1 (stable) (Tue Jan 31 12:25:35 2023 +0000) on "macos_arm64

示例项目的 pubspec.yaml 中的 environment 配置:

1
2
environment:
sdk: ">=2.17.0 <3.0.0"

在你的 pubspec.yaml 文件中添加 intlintl_utils 依赖:

1
2
3
dev_dependencies:
intl: ^0.17.0
intl_utils: ^2.4.0

使用 intl_utils 第三方工具的目的是给官方 Dart Intl 库生成样板代码,并为 Dart 代码中的键添加自动完成的功能。

1
2
3
4
5
6
flutter_intl:
enabled: true
class_name: S
main_locale: en
output_dir: lib/language/generated
arb_dir: lib/language/l10n
  • output_dir:模板代码生成路径;
  • arb_dir:是 .arb 文件存放的路径。

每次编辑 .arb 文件保存后会自动生成模板代码保存到 output_dir 路径下面。

运行 flutter pub get 指令后,自动生成的文件目录如下:

image.png

根据需要创建 .arb 文件编辑好内容后,保存文件就会在 lib/language/generated 目录下自动生成模板代码,下面以 intl_zh.arbintl_en.arb 为例来修改。
image.png

修改项目入口的Widget

mian() 中的第一个 Widget -> build 函数中添加如下3个地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void main() {
runApp(App());
}

class App extends StatelessWidget {
App({Key? key}) : super(key: key)

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 技术实践',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ThemeMode.system,
onGenerateRoute: MyRoutes.router.generator,
initialRoute: MyRoutes.root,
debugShowCheckedModeBanner: false,
supportedLocales: S.delegate.supportedLocales, // 1
localizationsDelegates: const [ // 2
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
localeResolutionCallback:
(Locale? locale, Iterable<Locale> supportedLocales) { // 3
Locale currentLocale =
Locale.fromSubtags(languageCode: locale?.languageCode ?? "zh");
return supportedLocales.contains(currentLocale)
? currentLocale
: const Locale.fromSubtags(languageCode: "zh");
},
);
}
}

这里的localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales)函数是设备系统或者浏览器语言环境发生改变的时候的回调,其中 locale 参数是改变后的语言,supportedLocales 是项目中所有支持的语言环境,如果本地未找到支持的语言就默认显示中文,也就是 languageCode: "zh"

使用 S.of(context).{key} 替换项目中文案

这里的 key 就是我们在 .arb 的文件定义的 key。

1
2
3
4
5
6
7
8
9
10
11
{
"nav_tab": "Tab Pages",
"nav_news": "News List",
"nav_route": "Route Navigation And Parameter Transfer",
"nav_completer": "Use Completer And Compute in Flutter",
"nav_extension": "Chrome Extension",
"tab_home": "Home",
"tab_search": "Search",
"tab_favor": "Favor",
"tab_setting": "Setting"
}

intl_utils 帮我们生成了模板代码,使用的时候有代码提示。
image.png

如何在应用内部切换语言

在应用内部切换语言的时候,就需要用到全局的状态管理,如Provider,当应用内部收到切换语言的操作后,会通知所有依赖它的 Widget 进行刷新,达到显示更换后的语言,我们也来实现一下。

pubspec.yaml 中引入 provider 依赖:

1
2
dependencies:
provider: ^6.1.2

创建类 AppLanguageProvider 继承自 ChangeNotifier,用来记录当前的应用内的语言,以及当执行切换语言的操作时,调用 notifyListeners() 来刷新。

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:flutter/cupertino.dart';

enum LanguageCode { zh, en }

class AppLanguageProvider extends ChangeNotifier {
LanguageCode languageCode = LanguageCode.zh;

changeLanguage(LanguageCode languageCode) {
this.languageCode = languageCode;
notifyListeners();
}
}

AppLanguageProvider 挂在项目入口的 Widget 来作为管理全局语言环境状态,并在 Builder 回调中监听当前的 languageCode 的值(context.watch<AppLanguageProvider>().languageCode),再根据当前的 languageCode 值获得到的 Locale 赋值给 MaterialApp -> locale 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// 第一步
ChangeNotifierProvider(create: (_) {
return AppLanguageProvider();
}),
],
builder: (BuildContext context, Widget? child) {
// 第二步
LanguageCode languageCode =
context.watch<AppLanguageProvider>().languageCode;
return MaterialApp(
title: 'Flutter 技术实践',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ThemeMode.system,
onGenerateRoute: MyRoutes.router.generator,
initialRoute: MyRoutes.root,
debugShowCheckedModeBanner: false,
supportedLocales: S.delegate.supportedLocales,
// 第三步
locale: Locale.fromSubtags(languageCode: languageCode.name),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
localeResolutionCallback:
(Locale? locale, Iterable<Locale> supportedLocales) {
Locale currentLocale =
Locale.fromSubtags(languageCode: locale?.languageCode ?? "zh");
return supportedLocales.contains(currentLocale)
? currentLocale
: const Locale.fromSubtags(languageCode: "zh");
},
);
},
);
}
}

最后,在切换语言操作的地方执行如下代码:

1
Provider.of<AppLanguageProvider>(context, listen: false).changeLanguage(LanguageCode.zh);

最终效果:
ezgif-5-b8ef0d45fe.gif

好了,以上就是Flutter项目国际化的全部实现过程,示例代码和查看效果在 https://flutter.nnxkcloud.com 上,另外,如果有更好的实现方式,也欢迎留言交流一下。

欢迎关注我的公众号“Flutter技术实践”,原创技术文章第一时间推送。