使用BuildContext扩展简化Flutter的本地化工作

430 阅读7分钟

如果您打算向世界各地的用户发布您的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

欢迎在此添加你的评论。👆

编码愉快