1 依赖

默认情况下,Flutter SDK中的组件只提供了美国英语本地化资源,而要添加对其他语言的支持,则必须添加一个名为”flutter_localizations” 的依赖包。

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

2 配置

依赖好flutter_localization库之后,需要指定MaterialApp的localizationsDelegates和supportedLocales。

import 'package:flutter_localizations/flutter_localizations.dart';

new MaterialApp(
 localizationsDelegates: [
   // 本地化的代理类
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // 美国英语
    const Locale('zh', 'CN'), // 中文简体
    //其它Locales
  ],
  // ...
)

localizationsDelegates列表中的元素,是生成本地化值集合的工厂 。
GlobalMaterialLocalizations.delegate 为Material组件提供了本地化的字符串和其他值,可以使Material组件支持多语言。
GlobalWidgetsLocalizations.delegate 定义了组件默认的文本方向,从左到右或者从右到左。
supportedLocales 用于接受一个Locale数组,表示我们的应用所支持的语言列表。

3 获取当前区域的Locale

Locale类是用来标示用户的语言环境的,它包含语言和国家两个标志,如:

const Locale('zh', 'CN') // 中文简体

一般可以通过如下方式来获取应用当前区域的Locale

Locale myLocale = Localizations.localeOf(context);

Localizations组件一般位于Widget树中其他业务组件的顶部,它的作用是定义区域Locale以及设置子树依赖的本地化资源,如果系统语言发生了变化那么WidgetsApp将创建一个新的Localizations组件并重新构建它,这样,子树中通过Localizations.localeOf(context) 获取的Locale就会更新。

4 实现Localizations

如果想要在自己的UI中实现国际化,则必须实现两个类:一个是Delegate类(代理类),另一个是Localizations类(本地化资源类)。

//Locale资源类
class DemoLocalizations {
  DemoLocalizations(this.isZh);
  //是否为中文
  bool isZh = false;
  //为了使用方便,我们定义一个静态方法
  static DemoLocalizations of(BuildContext context) {
    return Localizations.of(context, DemoLocalizations);
  }
  //Locale相关值,title为应用标题
  String get title {
    return isZh ? "Flutter应用" : "Flutter APP";
  }
  //... 其它的值  
}

DemoLocalizations 会根据当前的语言返回不同的文本,如上面的标题title,我们可以在此类中定义所有需要支持对语言的文本,而DemoLocalizations类的实类则会在代理类Delegate类的load方法中创建。

5 实现 Delegate类

delegate类的职责主要是在Locale发生改变时,加载新的Locale资源,所以他有一个load方法,Delegate类需要继承自LocalizationsDelegate类,实现相应的接口

//Locale代理类
class DemoLocalizationsDelegate extends LocalizationsDelegate {
  const DemoLocalizationsDelegate();

  //是否支持某个Local
  @override
  bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);

  // Flutter会调用此类加载相应的Locale资源类
  @override
  Future load(Locale locale) {
    print("xxxx$locale");
    return SynchronousFuture(
        DemoLocalizations(locale.languageCode == "zh")
    );
  }

  @override
  bool shouldReload(DemoLocalizationsDelegate old) => false;
}

shouldReload的返回值决定当Localizations组件重新build时,是否调用load方法重新加载Locale资源。一般情况下,Locale资源只应该在Locale切换时加载一次,不需要每次在Localizations重新build时都加载,所以返回false即可,不过无论返回false还是true,只要Locale发生改变时Flutter都会再调用load方法加载新的Locale

6 在应用中添加国际化支持

因为我们已经定义好了localizations类和delegate类,所以接下来首先要做的就是注册DemoLocalizationsDelegate类,然后再通过DemoLocalizations.of(context)来动态获取当前Locale文本。

只需要在MaterialApp或WidgetsApp的localizationsDelegates列表中添加我们的Delegate实例即可完成注册:

localizationsDelegates: [
 // 本地化的代理类
 GlobalMaterialLocalizations.delegate,
 GlobalWidgetsLocalizations.delegate,
 // 注册我们的Delegate
 DemoLocalizationsDelegate()
],

接下来我们可以在Widget中使用Locale值:

return Scaffold(
  appBar: AppBar(
    //使用Locale title  
    title: Text(DemoLocalizations.of(context).title),
  ),
  ... //省略无关代码
 )

这样,当在美国英语和中文简体之间切换系统语言时,APP的标题将会分别为“Flutter APP”和“Flutter应用”。

以上就是国际化的基本过程以及原理,但这个实例的严重不足之处就是在DemoLocalizations类中需要手动判断当前语言Locale去获取对应的title,如果我们应用中支持的语言超过两种以上时,这种方法就变得复杂而很难维护了,对此,我们就要寻找更好的实现方法来解决了,而利用Intl包就是一种很好的方法。

7 使用Intl包

使用Intl包,我们不仅可以非常轻松的实现国际化,而且还可以将字符串文本分离成单独的文件,方便开发和维护,而使用Intl包,就需要添加两个依赖。

dependencies:
  #...省略无关项
  intl: ^0.15.8 
dev_dependencies:
   #...省略无关项
  intl_translation: ^0.17.2

intl_translation 包主要包含了一些工具,它在开发阶段主要主要的作用是从代码中提取要国际化的字符串到单独的arb文件和根据arb文件生成对应语言的dart代码,而intl包主要是引用和加载intl_translation生成后的dart代码。

8 创建必要目录

首先,在项目根目录下创建一个i10n-arb目录,该目录可用于保存我们接下来通过intl_translation命令生成的arb文件,一个简单的arb文件内容如下:

{
  "@@last_modified": "2018-12-10T15:46:20.897228",
  "@@locale":"zh_CH",
  "title": "Flutter应用",
  "@title": {
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {}
  }
}

我们根据"@@locale"字段可以看出这个arb对应的是中文简体的翻译,里面的title字段对应的正是我们应用标题的中文简体翻译。@title字段是对title的一些描述信息,主要是用来给我们翻译不同语言用的。
接下来,我们在lib目录下创建一个i10n的目录,该目录用于保存从arb文件生成的dart代码文件。

9 实现Intl包下的Localizations和Delegate类

与前文中的步骤类似,我们仍然要实现Localizations和Delegate类,不同的是,现在我们在实现时要使用intl包的一些方法(有些是动态生成的)

import 'dart:async';
import 'package:intl/intl.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_xes_localizations/i10n/messages_all.dart'; @1

class AppStrings {
  AppStrings(Locale locale) : _localeName = locale.toString();

  final String _localeName;

  static Future load(Locale locale) {
    return initializeMessages(locale.toString())  @2
        .then((Object _) {
      return new AppStrings(locale);
    });
  }

  static AppStrings of(BuildContext context) {
    return Localizations.of(context, AppStrings);
  }

  String title() {
    return Intl.message(
      'Localization Demo',
      name: 'title',
      desc: '应用标题',
      locale: _localeName,
    );
  }

  String click() => Intl.message(
    'Click',
    name: 'click',
    desc: '点击',
    locale: _localeName,
  );

  String helloFromDemo() => Intl.message(
    'Hello~',
    name: 'helloFromDemo',
    desc: '一句问候',
    locale: _localeName,
  );
}

注释@1 中"messages_all.dart"文件是通过intl_translation工具从arb文件生成的代码,所以在第一次运行生成命令之前,此文件不存在。注释@2处的initializeMessages()方法和"messages_all.dart"文件一样,是同时生成的。

import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_xes_localizations/i10n/localization_intl.dart';

class AppLocalizationsDelegate extends LocalizationsDelegate {
  @override
  Future load(Locale locale) {
    return AppStrings.load(locale);
  }

  @override
  bool isSupported(Locale locale) =>
      ['zh', 'en'].contains(locale.languageCode); // 支持的类型要包含App中注册的类型

  @override
  bool shouldReload(AppLocalizationsDelegate old) => false;
}

10 生成arb文件

定义好localization和delegate两个类后,可以通过intl_translation包的工具来提取代码中字符串到一个arb文件运行如下命令

flutter pub pub run intl_translation:extract_to_arb --output-dir=i10n-arb  lib/i10n/localization_intl.dart

运行此命令后,会将我们之前通过Intl API标识的属性和字符串提取到“i10n-arb/intl_messages.arb”文件中,其内容如下:

{
  "@@last_modified": "2020-04-02T01:11:18.369750",
  "title": "Localization Demo",
  "@title": {
    "description": "应用标题",
    "type": "text",
    "placeholders": {}
  },
  "click": "Click",
  "@click": {
    "description": "点击",
    "type": "text",
    "placeholders": {}
  },
  "helloFromDemo": "Hello~",
  "@helloFromDemo": {
    "description": "一句问候",
    "type": "text",
    "placeholders": {}
  }
}

这个是默认的Locale资源文件,如果我们现在要支持中文简体,只需要在该文件同级目录创建一个"intl_zh_CN.arb"文件,然后将"intl_messages.arb"的内容拷贝到"intl_zh_CN.arb"文件,接下来将英文翻译为中文即可,翻译后的"intl_zh_CN.arb"文件内容如下

{
  "@@last_modified": "2020-02-08T18:08:36.651992",
  "title": "国际化示例App",
  "@title": {
    "description": "应用标题",
    "type": "text",
    "placeholders": {}
  },
  "click": "点击",
  "@click": {
    "description": "点击",
    "type": "text",
    "placeholders": {}
  },
  "helloFromDemo": "你好呀~",
  "@helloFromDemo": {
    "description": "一句问候",
    "type": "text",
    "placeholders": {}
  }
}

我们必须要翻译title和click以及 helloFromDemo字段,description是该字段的说明,通常给翻译人员看,代码中不会用到。
有两点需要说明:
如果某个特定的arb中缺失某个属性,那么应用将会加载默认的arb文件(intl_messages.arb)中的相应属性,这是Intl的托底策略。
每次运行提取命令时,intl_messages.arb都会根据代码重新生成,但其他arb文件不会,所以当要添加新的字段或方法时,其他arb文件是增量的,不用担心会覆盖。

11 根据arb文件生成对应的dart代码

flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/i10n --no-use-deferred-loading lib/i10n/localization_intl.dart i10n-arb/intl_*.arb

这句命令在首次运行时会在"lib/i10n"目录下生成多个文件,对应多种Locale,这些代码便是最终要使用的dart代码,内容如下:

import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';

final messages = new MessageLookup();

typedef String MessageIfAbsent(String messageStr, List args);

class MessageLookup extends MessageLookupByLibrary {
  String get localeName => 'messages';

  final messages = _notInlinedMessages(_notInlinedMessages);
  static _notInlinedMessages(_) =>  {
    "click" : MessageLookupByLibrary.simpleMessage("Click"),
    "helloFromDemo" : MessageLookupByLibrary.simpleMessage("Hello~"),
    "title" : MessageLookupByLibrary.simpleMessage("Localization Demo")
  };
}

12 在Flutter应用中的应用

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lwdflutter/i10n/delegate_init.dart';
import 'package:lwdflutter/i10n/localization_intl.dart';
import 'package:lwdflutter/test/change_page.dart';

void main() => runApp(new MyAppss());

class MyAppss extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),

      localizationsDelegates: [
        AppLocalizationsDelegate(), // 我们定义的代理
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],

      supportedLocales: [
        // 支持的语言类型
        const Locale('zh', ''),
        const Locale('en', 'US'), // English
      ],

      onGenerateTitle: (context) {
        return AppStrings.of(context).title();
      },

      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State {

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(AppStrings.of(context).title()),
      ),

      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            new Text(
              AppStrings.of(context).title(),
            ),
          ],
        ),
      ),
  }
}

上面就是用Intl包对APP进行国际化的整体流程,我们可以发现,其中8,9只在第一次需要,而我们开发时的主要的工作都是在第10步。由于最后两步在第10步完成后每次也都需要,所以我们可以将最后两步放在一个shell脚本里,当我们完成第10步或完成arb文件翻译后只需要分别执行该脚本即可。我们在根目录下创建一个intl.sh的脚本,内容为:

flutter pub pub run intl_translation:extract_to_arb --output-dir=i10n-arb  lib/i10n/localization_intl.dart

flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/i10n --no-use-deferred-loading lib/i10n/localization_intl.dart i10n-arb/intl_*.arb

创建好intl.sh 脚本后,需要赋予权限,在Terminal中输入如下命令:

chmod +x intl.sh

这样以后每次新增对应的文字之后,只需要在Localizations类中去添加,然后执行init.sh 脚本之后,就会生成对应的intl_messagea.arb文件了

./intl.sh

原创: IT烟酒僧