阅读 357

Flutter国际化

如果App的用户使用的是不同语言,那进行国际化是必要的。国际化主要包括文案的国际化(不同的语言展示不同的文案)和布局的国际化(从左到右还是从右到左布局)。不同语言涉及的业务逻辑的差别(eg. 法语跳转到法语对应网站,韩语跳到韩语对应的网页)一般不被归为国际化的内容,属于业务逻辑的范畴。

我们公司的产品用户涵盖了欧美、日韩和以色列等国家,每个版本发版前的一个块大的任务就是针对不同的语言进行布局和文案的适配,所以国际化还是很重要的一块内容。

案例

为了说明如何实现国际化,我们先建一个工程,然后将main.dart中的代码替换成下面的代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(context) {
    return Scaffold(
      appBar: AppBar(title: Text("国际化案例")),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: List.generate(
              3,
              (index) => ElevatedButton(
                  onPressed: () {
                    showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime(2020),
                        lastDate: DateTime(2022));
                  },
                  child: Text("按钮 ${index + 1}"))),
        ),
      ),
      drawer: Drawer(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text("这是抽屉",
                  style: TextStyle(color: Colors.red, fontSize: 30)),
              SizedBox(
                height: 20,
              ),
              ElevatedButton(
                onPressed: () => Navigator.of(context).pop(),
                child: Text("关闭抽屉"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
复制代码

这段代码的功能很简单:

  1. Home页面有三个按钮,按照顺序水平排列,点击按钮会弹出Flutter官方的时间选择器DatePicker
  2. 点击左上角会弹出Drawer, 有一个行文字和一个按钮,点击按钮Drawer消失。

示例

这个App在用户体验上有一些问题:

  1. 非中文语言的手机用户,他们不认识中文,所以需要将App中的中文(eg. 按钮国际化按钮关闭抽屉等)替换成他们手机对应的语言;
  2. 对于像以色列,阿拉伯语言的手机用户,他们的布局是从右往左的,目前从左往右的布局不符合他们的使用习惯;
  3. 即使是中文手机用户,弹出来的时间选择器上的文字是英文的,对中文用户也是不友好的。

Flutter官方提供的国际化

本着谁开发谁负责的原则,Flutter官方需要为他们提供的Widget提供国际化的支持。事实上他们也确实有提供支持方案。

添加依赖
dependencies:
  flutter_localizations:  //添加的
    sdk: flutter          //添加的
复制代码

pubspec.yaml文件中加入依赖,然后执行flutter pub get

修改代码
  1. 引入头文件import 'package:flutter_localizations/flutter_localizations.dart';
  2. MaterialApp设置localizationsDelegatessupportedLocales;
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 1 设置localizationsDelegates
      localizationsDelegates: [
        GlobalWidgetsLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      // 2 设置 supportedLocales 表示支持的国际化语言
      supportedLocales: [
        Locale.fromSubtags(languageCode: 'en'),
        Locale.fromSubtags(languageCode: 'he'),
        Locale.fromSubtags(languageCode: 'zh'),
      ],
      home: Home(),
    );
  }
}
复制代码

localizationsDelegates的参数介绍:GlobalWidgetsLocalizations主要是对布局方向进行国际化,GlobalMaterialLocalizations主要是对Material Widgets进行了国际化,GlobalCupertinoLocalizations是对Cupertino Widgets进行了国际化。不需要理由,写上这三个基本上系统的Widget就都支持国际化了。

supportedLocales的参数介绍:en代表英文,zh代表中文,he代表希伯来文(以色列)。这个参数需要根据实际情况设置,我这里设置这三个语言只是案例需要。如果用户的手机语言不是上述三种,譬如法语,那就使用默认的语言(英文)。

效果

经过这两步设置后,Flutter官方Widget的国际化已经实现完成,让我们看下效果:

  • 手机语言是英文的效果:

en

抽屉从左往右弹出,一排按钮从左往右排列, 时间选择器上的文字是英文。

  • 手机语言是中文的效果:

zh

抽屉从左往右弹出,一排按钮从左往右排列, 时间选择器上的文字是中文。

  • 手机语言是希伯来文的效果:

he

抽屉从右往左弹出,一排按钮从右往左排列, 时间选择器上的文字变成了希伯来文,时间选择器的内容也是从右往左排列。

自定义国际化实现

上面的效果还有一些瑕疵,不管切换什么手机语言,一些内容都是显示的中文,这是因为我们写死的是中文。这些中文文字根据手机语言显示对应的语言的文案才是最完美的实现。我们接下来的任务就是实现这个逻辑:

新建多语言Json文件

在根目录新建assets/json文件夹,在此文件夹下新建i18n.json文件,文件内容如下:

{
    "en": {
      "title": "Localization Demo",
      "button": "Button",
      "drawer_tip": "This is the Drawer",
      "close_drawer": "Close Drawer"
    },
    "zh": {
      "title": "国际化案例",
      "button": "按钮",
      "drawer_tip": "这是抽屉",
      "close_drawer": "关闭抽屉"
    },
    "he": {
      "title": "הדגמת לוקליזציה",
      "button": "לַחְצָן",
      "drawer_tip": "מְגֵרָה",
      "close_drawer": "סגור מגירה"
    }
}
复制代码

enzhhe三种语言下都有titlebuttondrawer_tipclose_drawer四个文案。

引入文件

pubspec.yaml文件中引入assets/json/文件夹下的所有文件,当然也包括i18n.json这个文件:

flutter:

  assets:  //添加的
    - assets/json/  //添加的
复制代码
添加国际化代码
  1. 新建app_localizations.dart文件, 文件内容:
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class AppLocalizations {

  // 1
  final Locale locale;

  AppLocalizations(this.locale);

  // 2
  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations);
  }

  static Map<String, Map<String, String>> _localizedStrings = {};

  // 3
  Future loadJson() async {
    final jsonString = await rootBundle.loadString("assets/json/i18n.json");
    Map<String, dynamic> map = json.decode(jsonString);
    _localizedStrings = map.map((key, value) => MapEntry(key, value.cast<String, String>()));
  }

  // 4
  String get title => _localizedStrings[this.locale.languageCode]["title"];

  String get button => _localizedStrings[this.locale.languageCode]["button"];

  String get drawerTip =>
      _localizedStrings[this.locale.languageCode]["drawer_tip"];

  String get closeDrawer =>
      _localizedStrings[this.locale.languageCode]["close_drawer"];

}
复制代码
  1. locale是系统确定的,会从外部传进来,AppLocalizations需要根据这个locale来找到对应的语言的文案;
  2. of只是封装了一个对外的方法,方便找到AppLocalizations对象来使用。看到of方法猜测国际化也是依赖于InheritedWiget来实现的;
  3. loadJson是从JSON文件来加载国际化文件,然后将结果赋值给_localizedStrings
  4. 实现了title,button,drawerTipcloseDrawerget方法
  1. 新建app_localization_delegate.dart文件, 文件内容:
import 'package:flutter/material.dart';
import 'package:localization_demo/i18n/app_localizations.dart';

class APPLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

  // 1.
  static APPLocalizationDelegate delegate = APPLocalizationDelegate();

  // 2.
  @override
  bool isSupported(Locale locale) {
    return ["en", "zh", "he"].contains(locale.languageCode);
  }

  // 3
  @override
  Future<AppLocalizations> load(Locale locale) async {
    final appLocalizations = AppLocalizations(locale);
    await appLocalizations.loadJson();
    return appLocalizations;
  }

  // 4
  @override
  bool shouldReload(APPLocalizationDelegate old) {
    return false;
  }

}
复制代码
  1. delegate方法是实例化方法,起这个名字就是为了和系统的方法一致;
  2. isSupported是判断是否支持locale这个语言的国际化,支持就返回true,否则返回false;
  3. load就是如果支持locale这个语言的国际化,就去加载国际化资源,我们这儿的实现是让AppLocalizations去加载JSON文件;
  4. shouldReload是在用到国际化资源时是否需要重新加载国际化资源,默认是不需要。
国际化代码的使用

main.dart中使用前面实现的国际化的代码:

import 'package:localization_demo/i18n/app_localization_delegate.dart';
import 'package:localization_demo/i18n/app_localizations.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        GlobalWidgetsLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        // 1. 修改地方
        APPLocalizationDelegate.delegate
      ],
      supportedLocales: [
        Locale.fromSubtags(languageCode: 'en'),
        Locale.fromSubtags(languageCode: 'he'),
        Locale.fromSubtags(languageCode: 'zh'),
      ],
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(context) {
    return Scaffold(
      // 2. 修改地方
      appBar: AppBar(title: Text(AppLocalizations.of(context).title)),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: List.generate(
              3,
              (index) => ElevatedButton(
                  onPressed: () {
                    showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime(2020),
                        lastDate: DateTime(2022));
                  },
                  // 3. 修改地方
                  child: Text("${AppLocalizations.of(context).button} ${index + 1}"))),
        ),
      ),
      drawer: Drawer(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              // 4. 修改地方
              Text(AppLocalizations.of(context).drawerTip, style: TextStyle(color: Colors.red, fontSize: 30)),
              SizedBox(
                height: 20,
              ),
              ElevatedButton(
                onPressed: () => Navigator.of(context).pop(),
                // 5. 修改地方
                child: Text(AppLocalizations.of(context).closeDrawer),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
复制代码
  1. MaterialApplocalizationsDelegates中加入APPLocalizationDelegate.delegate;
  2. 用到文案的地方都用AppLocalizations.of(context)
效果
  • 手机语言是英文的效果:

en

  • 手机语言是中文的效果:

zh

  • 手机语言是希伯来文的效果:

he

Flutter Intl 插件

上面我们实现了文案的国际化,为了更加简便,我们可以使用Flutter Intl插件来实现国际化。

Flutter Intl插件安装

初始化

VS CodeAndroid Studio都有Flutter Intl插件,由于我使用的是VS Code,加上Android Studio插件的安装和使用也很简单,本文仅介绍VS Code上该插件的使用。

VS Code安装Flutter Intl插件

安装

利用Flutter Intl插件初始化国际化的相关文件

使用快捷键调出命令行工具(Mac电脑是Shift+Command+p),然后选择Flutter Intl: Initialize命令(第一次用可能看不到这个命令,也可以直接输入,我最近使用过所以这个命令在最上面),敲击回车确认。然后我们可以看到执行了flutter pub get命令,然后在项目中生成了一堆新的文件。

initialize

生成的新的文件包括generatedl10n两个文件夹,然后还在pubspec.yaml文件中加入了配置:

new file

修改intl_en.arb

我们将en语言下的文案放在intl_en.arb这个文件中:

{
    "title": "Localization Demo",
    "button": "Button",
    "drawer_tip": "This is the Drawer",
    "close_drawer": "Close Drawer"
}
复制代码

修改后记得执行flutter pub get

修改main.dart
import 'generated/l10n.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        GlobalWidgetsLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        // 1. 修改地方
        S.delegate,
      ],
      // 2. 修改
      supportedLocales: S.delegate.supportedLocales,
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(context) {
    return Scaffold(
      // 3 修改地方
      appBar: AppBar(title: Text(S.of(context).title)),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: List.generate(
              3,
              (index) => ElevatedButton(
                  onPressed: () {
                    showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime(2020),
                        lastDate: DateTime(2022));
                  },
                  // 4. 修改
                  child: Text("${S.of(context).button} ${index + 1}"))),
        ),
      ),
      drawer: Drawer(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              // 5. 修改
              Text(S.of(context).drawer_tip, style: TextStyle(color: Colors.red, fontSize: 30)),
              SizedBox(
                height: 20,
              ),
              ElevatedButton(
                onPressed: () => Navigator.of(context).pop(),
                // 6. 修改
                child: Text(S.of(context).close_drawer),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
复制代码
  1. MaterialApplocalizationsDelegates中加入S.delegate,supportedLocales修改为S.delegate.supportedLocales;
  2. 所有使用的地方改成S.of(context)

到目前为止英文en的国际化就弄好了。

添加其他国际化的语言

利用Flutter Intl插件添加新的国际化语言

使用快捷键调出命令行工具,然后选择Flutter Intl: Add locale命令(和前面类似,你可能直接看不到这个命令,可以在输入框中输入Intl, 这样会出现提示);

Add locale

会出现输入框,在输入框中输入zh,然后敲击回车。

zh

等待一会儿,会自动生成两个和zh相关的文件:messages_zh.dartintl_zh.arb

结果

修改intl_zh.arb文件

将中文的文案放在这个文件内:

{
    "title": "国际化案例",
    "button": "按钮",
    "drawer_tip": "这是抽屉",
    "close_drawer": "关闭抽屉"
}
复制代码

最后不要忘了执行flutter pub get

he的实现方式类似,不再重复介绍了。

intl_he.arb文件内容如下:

{
    "title": "הדגמת לוקליזציה",
    "button": "לַחְצָן",
    "drawer_tip": "מְגֵרָה",
    "close_drawer": "סגור מגירה"
}
复制代码

目前为止,所有需要做的工作就完成了,非常简单。

占位符传参

有时候文案中的某些部分最开始是不确定的,在运行的时候才能确定。譬如文案中有价格,但是这个价格不是固定的,这时候就需要先用一个占位符占位,然后在运行的时候用真实的数据替换掉这个占位符。

我们案例中的button文案我们替换为为button {seq}, לַחְצָן{seq}按钮 {seq}

在使用的时候我们可以改为Text("${S.of(context).button(index + 1)}"))),这样的效果和前面的一样。

这种方式的好处由于不同语言表达方式不一样,不同语言翻译出来后的占位符的位置可以是任意的。

总结

Flutter官方提供的国际化方案对布局的国际化做的非常友好,文案的国际化在Flutter Intl插件的加持下也非常简单。

文章分类
iOS
文章标签