如果您打算向世界各地的用户发布您的Flutter应用程序,您迟早需要对其进行本地化,使其支持多种语言。
幸运的是,Flutter已经提供了有用的API,使这项任务变得更容易,同时还有详细解释这一过程的文档。
从Flutter 2.5开始,我们可以使用新的骨架应用模板,该模板默认使用ARB文件生成本地化,这样我们就不必手工完成所有的设置步骤。
在这篇文章中,我们将回顾骨架应用模板并做一些改进。
结果将是一个简化的应用模板,它使用BuildContext 扩展来轻松访问我们小部件内部的本地化字符串。
入门。简化的 "骨架 "应用程序模板
作为参考,我们将从一个用骨架模板生成的新Flutter应用程序开始。
flutter create -t skeleton localization_riverpod_flutter
这个模板带有一个设置屏幕,可以用来在明模式和暗模式之间切换。
但由于我们想专注于本地化,我们可以删除所有的主题化逻辑,这样我们应用中的根部件就被简化了。
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'sample_feature/sample_item_details_view.dart';
import 'sample_feature/sample_item_list_view.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
restorationScopeId: 'app',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', ''),
],
onGenerateTitle: (BuildContext context) =>
AppLocalizations.of(context)!.appTitle,
onGenerateRoute: ...,
);
}
}
这是一个很好的起点。但有一个改进,我们可以立即进行。那就是替换这段代码。
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', ''),
],
换成这个。
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
为什么这是有效的代码?
嗯,答案就在AppLocalizations.localizationsDelegates 的文档中。
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
我们可以看到,localizationsDelegates 声明的委托列表与我们传递给MaterialApp 的委托完全相同。
最重要的是,我们最终得到了这个结果。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
restorationScopeId: 'app',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
onGenerateTitle: (BuildContext context) =>
AppLocalizations.of(context)!.appTitle,
onGenerateRoute: ...,
);
}
}
现在我们的 "简化 "设置已经准备好了,让我们看看还有什么可以改进的。🔍
访问AppLocalizations的问题
如果我们仔细看一下我们的MaterialApp ,我们可以发现这一行。
onGenerateTitle: (BuildContext context) =>
AppLocalizations.of(context)!.appTitle,
这给了我们一个提示,我们应该如何对我们的小部件使用本地化的字符串,这是一个多步骤的过程。
1.在ARB文件中添加本地化的字符串
首先,我们必须将本地化字符串添加到src/localizations/app_en.arb 文件中。
{
"appTitle": "localization_riverpod_flutter",
"@appTitle": {
"description": "The title of the application"
},
"itemDetails": "Item Details",
"@itemDetails": {
"description": "Title of the Item Details Page"
},
"moreInformationHere": "More Information Here",
"@moreInformationHere": {
"description": "Body of the Item Details Page"
}
}
2.生成AppLocalizations类
然后,我们可以运行flutter gen-l10n ,它将重新生成app_localizations.dart 文件,使用存储在l10n.yaml 中的配置(后面会有更多的介绍)。
这样,我们需要的本地化字符串将作为类型安全的值在AppLocalizations 类中被访问。
abstract class AppLocalizations {
...
/// The title of the application
///
/// In en, this message translates to:
/// **'localization_riverpod_flutter'**
String get appTitle;
/// Title of the Item Details Page
///
/// In en, this message translates to:
/// **'Item Details'**
String get itemDetails;
/// Body of the Item Details Page
///
/// In en, this message translates to:
/// **'More Information Here'**
String get moreInformationHere;
}
3.在我们的小部件中访问本地化的字符串
最后,我们可以在我们的部件中使用本地化的字符串。例子。
import 'package:flutter/material.dart';
// 1. Import this file
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SampleItemDetailsView extends StatelessWidget {
const SampleItemDetailsView({Key? key}) : super(key: key);
static const routeName = '/sample_item';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 2. Access the localized string
title: Text(AppLocalizations.of(context)!.itemDetails),
),
body: Center(
// 2. Access the localized string
child: Text(AppLocalizations.of(context)!.moreInformationHere),
),
);
}
}
在实践中,这最后一步引起了一些麻烦,导致了一个糟糕的开发工作流程。
有一次,如果我们输入AppLocalizations.of(context) ,并试图使用快速修复快捷键来添加缺失的导入,VSCode拒绝显示app_localizations.dart ,作为可用的选项之一--至少当我尝试这样做的时候是这样的。
VSCode不显示app_localizations.dart文件的导入选项
其次,每次我们想访问一个本地化的字符串时,我们都被迫使用! 操作符并输入AppLocalizations.of(context)!.something 。
如果我们检查AppLocalizations 类,我们会发现静态of 方法返回一个nullable对象。
abstract class AppLocalizations {
...
// the return type is nullable
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
}
这是有道理的,因为底层的Localizations widget是作为MaterialApp 的一部分被创建的,而且根据我们使用的context ,调用AppLocalizations.of(context) 可能会也可能不会给我们一个对象。
但是在实践中,我们所有的部件都是MaterialApp 的后裔,所以我们不应该在任何地方使用! 操作符!
如果你对
BuildContext,以及它与访问widget的关系感到困惑,请看这个关于BuildContext的Decoding Flutter视频。
一定会有更好的方法。
l10n.yaml文件
我们上面所说的是,我们需要运行flutter gen-l10n 来为我们的项目生成本地化。
我们可以运行flutter gen-l10n --help 来发现所有可用的选项。
或者我们可以编辑在创建骨架应用模板时为我们生成的l10n.yaml 文件。默认情况下,这个文件看起来像这样。
# l10n.yaml
arb-dir: lib/src/localization
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
为了解决我们的问题,我们可以添加这一行。
nullable-getter: false
然后,我们可以再次运行flutter gen-l10n 。而如果我们再次打开生成的AppLocalizations ,我们会发现有些东西已经改变了。
abstract class AppLocalizations {
...
// the return type is now non-nullable
static AppLocalizations of(BuildContext context) {
// note the ! at the end
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
}
这意味着,当我们访问小工具中的本地化字符串时,我们不再需要! 操作符。
Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).itemDetails),
),
body: Center(
child: Text(AppLocalizations.of(context).moreInformationHere),
),
)
然而,每次都输入AppLocalizations.of(context) ,有点长,我们可以进一步改进💪
BuildContext扩展的拯救
感谢Dart扩展的力量,我们可以编写这个小助手。
// app_localizations_context.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension LocalizedBuildContext on BuildContext {
AppLocalizations get loc => AppLocalizations.of(this);
}
然后,我们可以像这样更新我们的widget类。
import 'package:flutter/material.dart';
// 1. new import
import 'package:localization_riverpod_flutter/src/localization/app_localizations_context.dart';
class SampleItemDetailsView extends StatelessWidget {
const SampleItemDetailsView({Key? key}) : super(key: key);
static const routeName = '/sample_item';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 2. short-hand syntax
title: Text(context.loc.itemDetails),
),
body: Center(
// 2. short-hand syntax
child: Text(context.loc.moreInformationHere),
),
);
}
}
这样就好多了,因为我们现在可以导入app_localizations_context.dart ,而不是app_localizations.dart --VSCode也能顺利找到它了:
VSCode现在显示了我们BuildContext扩展的导入选项
而且我们可以输入context.loc.itemDetails ,而不是AppLocalizations.of(context).itemDetails ,这样更简短。
如果我们的应用程序中有几十个(或几百个)本地化的字符串,这就相当于给开发者带来了很多快乐!😀
总结
我们现在已经创建了一个简化的应用程序模板,使我们能够通过使用一个方便的BuildContext 扩展,更容易地在我们的小部件中进行字符串的本地化。
在这个过程中,我们还学会了如何配置l10n.yaml 文件以满足我们的需要。
你可以在GitHub的这个页面上找到这个应用模板的完整源代码。
在下一篇文章中,我们将看到如何在我们的小部件之外访问AppLocalizations 对象(使用Riverpod),这样我们就可以在我们的业务逻辑中使用本地化的字符串。
而关于国际化的更深入的介绍,以及如何为新的和现有的Flutter应用程序设置它,请参阅官方文档。
最后,这里有一个Twitter线程,总结了我们在这篇文章中涉及的主要步骤。
在你的小部件内访问本地化字符串的默认方式是使用`AppLocalizations`类。
然而,到处使用`!`运算符并不是好的做法。
而且,如果能有一些更 "轻量级 "的语法就更好了。
一个线程。🧵pic.twitter.com/6t1ackmS2c
- Andrea Bizzotto 💙(@biz84)2022年1月26日
欢迎在此添加你的评论。👆
编码愉快