Flutter系列十二: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消失。
1.gif

这个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插件的加持下也非常简单。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容