V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
vifird
V2EX  ›  Flutter

从 0 开始设计 Flutter 独立 APP | 第二篇: 完整的国际化语言支持

  •  
  •   vifird · 2020-06-28 14:27:41 +08:00 · 2610 次点击
    这是一个创建于 1643 天前的主题,其中的信息可能已经有所发展或是发生改变。

    鉴于 Flutter 高性能渲染和跨平台的优势,闪点清单在移动端 APP 上,使用了完整的 Flutter 框架来开发。既然是完整 APP,架构搭建完全不受历史 Native APP 的影响,没有历史包袱的沉淀,设计也能更灵活和健壮。

    国际化语言的支持,是很多 APP 都有的一个强需求,APP 无论大小,只要还不想放弃国外的客户,一般就需要支持国际化。

    flutter 国际化

    官方支持

    Flutter 官方方案提供了国际化的基础支持,如 Flutter 内置组件的国际化、语言代理、Widget 使用语言包、语言设置回调等,并支持自定义第三方类来扩展,可以参考 Flutter 国际化文档。 官方支持代码示例:

    class DemoLocalizations {
      DemoLocalizations(this.locale);
    
      final Locale locale;
    
      static DemoLocalizations of(BuildContext context) {
        return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
      }
    
      static Map<String, Map<String, String>> _localizedValues = {
        'en': {
          'title': 'Hello World',
        },
        'es': {
          'title': 'Hola Mundo',
        },
      };
    
      String get title {
        return _localizedValues[locale.languageCode]['title'];
      }
    }
    

    官方方案的缺陷

    官方的支持有几个缺陷:

    1. 依赖于 BuildContext 对象,在非 Widget 中调用时,需要层层传递 BuildContext 对象,或存储全局 BuildContext 对象。
    2. 在 MaterialApp 初始化前无法使用国际化(原因也是依赖于 BuildContext 对象)。
    3. 语言包定义推荐使用 Map 方式,无法利用静态语言的优势(语法提示、错误检查等);而为语言包每个属性自定义类和类字段,成本较高、使用和更新灵活性差。

    i18n 介绍

    鉴于 Flutter 官方支持的缺陷,我们调研了很多第三方库,最终发现了 i18n,并在此基础上、结合 Flutter 官方支持和自身封装,实现了更灵活易用的方案。

    flutter 国际化

    基础使用

    i18n 使用 yaml 格式来定义语言包,同时提供构建脚本一键生成 Dart 语言包 Class 。如下:

    lib/messages.i18n.yaml
    
    button:
      save: Save
      load: Load
    users:
      welcome(String name): "Hello $name!"
      logout: Logout
    

    该配置会生成几个 Class:Messages 、ButtonMessages 、UserMessages,生成后的 Dart 文件使用方式如下:

    Messages m = Messages();
    debugPrint(m.users.logout);
    debugPrint(m.users.welcome('World'));
    

    生成的 Dart 文件预览(开发时无需关心):

    class Messages {
        const Messages();
        ButtonMessages get button => ButtonExampleMessages(this);
        UsersMessages get users => UsersExampleMessages(this);
    }
    class ButtonMessages {
        final Messages _parent;
        const ButtonMessages(this._parent);
        String get save => "Save";
        String get load => "Load";
    }
    class UsersMessages {
        final Messages _parent;
        const UsersMessages(this._parent);
        String get logout => "Logout";
        String welcome(String name) => "Hello $name!";
    }
    

    进阶功能

    下面讲解一些进阶用法。

    函数定义

    i18n 支持函数定义,并支持传参,如上述的welcome函数:

    debugPrint(m.users.welcome('World'));
    

    参数定义基本没有限制,可以随意定义参数个数和类型。

    内置函数

    i18n 支持了一些内置函数,用于做不同语言解析的体验优化,如:plural 、cardinal 、ordinal 。具体规则和使用,可以参考这里: http://cldr.unicode.org/index/cldr-spec/plural-rules

    使用 Dart 字符串模板

    Dart 字符串模板是非常强大的,而在 i18n 中,你可以使用字符串模板(这点非常赞),如:

    count(int cnt): "You have created $cnt ${_plural(cnt, one:'invoice', many:'invoices')}."
    

    前置编译

    i18n 依然依赖了 Dart 官方提供的 builder_runner 工具,来从 yaml 文件生成 Dart 文件,使用方式: flutter pub run build_runner build

    flutter 国际化

    语言包使用

    前置编译后,每个语言包会生成 N 个 Class (语言包的每一个分类或组合会生成一个 Class 文件),然后会生成一个根 Class,我们可以直接使用根 Class (当然也可以使用任何一个分类层级的 Class )。

    比如两个语言包文件: AppMessages.i18n.yamlAppMessages_en.i18n.yaml(未加语言后缀的,会认为是默认语言包,因此 AppMessages.i18n.yaml 是默认语言包),会生成 2 个根 Dart Class: class AppMessagesclass AppMessages_en extends AppMessages

    AppMessages_en自动继承自AppMessages,因此我们可以直接使用AppMessages类型来存储语言包,并在语言切换时重新为其实例化对应的子类:

    AppMessages appMessages = new AppMessages();
    
    resetLocalLang(String localeName) {
      switch (localeName) {
        case 'en':
          appMessages = AppMessages_en();
          break;
        case 'zh':
        default:
          appMessages = AppMessages();
          break;
      }
    }
    

    然后你可以在任意地方使用语言包:

    debugPrint('Load Button: ${appMessages.button.load}');
    
    FlatButton(
      child: Text(appMessages.button.save),
      onPressed: () {
        /// 干点什么
      },
    )
    

    Flutter 集成

    集成到 Flutter,依然要依赖于官方的支持,在 MaterialApp 中设置和监听本地语言包:

    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        localeResolutionCallback:
            (Locale locale, Iterable<Locale> supportedLocales) {
          /// Local changed
        },
        localizationsDelegates: [
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
        ],
        supportedLocales: ['zh', 'en']
            .map((loc) => new Locale(loc))
            .toList(growable: false),
        /// ...
      );
    }
    

    结尾

    国际化支持,是一个移动端 APP 框架层的基础能力,设计原则应该是使用无感知、灵活易扩展;但维护成本是难免有增加的,比如每次改文案要所有语言包同时更改。

    讲到这里,还并没有完成基础框架的搭建,后面我们会讲解更多的 Flutter 架构设计内容,比如:通知、分享、UI 设计等等。


    持续分享闪点清单在 Flutter 上的开发经验。闪点清单,一款悬浮清单软件:

    闪点清单,一款悬浮清单软件

    5 条回复    2020-06-29 22:59:49 +08:00
    sunbreak
        1
    sunbreak  
       2020-06-28 16:24:37 +08:00
    有意向来头条么? https://v2ex.com/t/684705,
    blakejia
        2
    blakejia  
       2020-06-28 16:29:57 +08:00
    阿拉伯语言,可以支持么?从右到左
    sunbreak
        3
    sunbreak  
       2020-06-29 05:08:47 +08:00
    @blakejia 支持的
    vifird
        4
    vifird  
    OP
       2020-06-29 22:58:44 +08:00
    @blakejia 可以的
    vifird
        5
    vifird  
    OP
       2020-06-29 22:59:49 +08:00
    @sunbreak 暂时还没有哇🤩
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1766 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 16:29 · PVG 00:29 · LAX 08:29 · JFK 11:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.