Flutter如何优雅地实现国际化

本人初结识Flutter,文章内容如有理解错误,欢迎指正。

为了让不同国家的用户都可以使用我们开发的应用,在应用上架之前需要让应用能够支持多种语言,即应用的国际化。

应用的国际化主要涉及语言和地区差异性配置两个方面,它们是应用程序的组成部分之一。在Flutter开发中,实现国际化大都采用的方案是Intl(如熟悉此方案可跳过)

Intl方案

1.添加依赖项

pubspec.yaml添加依赖项flutter_localizations,然后运行一下flutter packages get

dependencies:
  flutter:
    sdk: flutter
# 添加下面的依赖项
  flutter_localizations:
    sdk: flutter
  intl: ^0.17.0
  intl_translation: ^0.17.10+1

2.编辑dart文件

新建app_strings.dart文件

import 'dart:async';

import 'package:intl/intl.dart';
import 'package:flutter/widgets.dart';

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

  final String _localeName;

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

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

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

3.生成arb文件

进入项目目录,运行intl的命令。

$ flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/app_strings.dart

生成l10n/intl_messages.arb,内容如下:

{
  "@@last_modified": "2018-07-15T22:13:19.218221",
  "title": "Localization Demo",
  "@title": {
    "description": "应用标题",
    "type": "text",
    "placeholders": {}
  },
  "click": "Click",
  "@click": {
    "description": "点击",
    "type": "text",
    "placeholders": {}
  },
 }

4.新增和修改arb文件

前面生成了l10n/intl_messages.arb,我们可以把它当成模板。复制粘贴一下,同目录下得到intl_en.arbintl_zh.arb。如intl_zh.arb:

{
  "@@last_modified": "2018-07-15T22:13:19.218221",
  "title": "国际化示例App",
  "@title": {
    "description": "应用标题",
    "type": "text",
    "placeholders": {}
  },
  "click": "点击",
  "@click": {
    "description": "点击",
    "type": "text",
    "placeholders": {}
  },
 }

5.根据arb生成dart文件

flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n \
   --no-use-deferred-loading lib/app_strings.dart lib/l10n/intl_*.ar

此时在app_strings.dart中添加对l10n/intl_messages.arb的引用。

6.创建locallization代理

创建localizations_delegate.dart。新建AppLocalizationsDelegate类继承LocalizationsDelegate

import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:localization_demo/app_strings.dart';

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

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

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

7.MaterialApp中添加本地代理和语言类型

class MyApp 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('en', 'US'), // English
        const Locale('zh', ''),
      ],
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

注意事项:

代理isSupported方法中的语言类型最好是和App中supportedLocales的一致。

简单介绍了一下Intl方案实现国际化,由于不是本文的重点,有疑问大家可以google,通过上面的介绍使用,我们总结一下Intl方案的缺点:

  • 实现步骤过多,繁琐。
  • 中间使用命令生成的arb文件需要复制粘贴,工作量大。
  • 需要创建arb,dart,代理等一系列文件,每添加一种语言的支持就得修改所有的文件,严重违反开闭原则

实践了Intl方案后,一直在想能不能像iOS一样直接通过配置几个json文件的方式来实现国际化呢,GetX的发现验证了我的猜想。

getX方案

GetX的使用场景有很多,比如状态管理,路由,监听,国际化等等。

文章的重点来说国际化:

1.添加依赖项

pubspec.yaml添加依赖项flutter_localizations,然后运行一下flutter packages get

get: ^4.3.8

2.自定义翻译

创建一个翻译类并扩展 Translations :

import 'package:get/get.dart';

class Messages extends Translations {
  @override
  Map<String, Map<String, String>> get keys => {
        'zh_CN': {
          'hello': '你好 世界',
        },
        'de_DE': {
          'hello': 'Hallo Welt',
        }
      };
}

3.在GetMaterialApp中定义语言和翻译

return GetMaterialApp(
    translations: Messages(), // 你的翻译
    locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
    fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
);

4.使用翻译

Text('title'.tr);

5.改变语言

var locale = Locale('en', 'US');
Get.updateLocale(locale);

至此,getX已实现国际化,但上面的实现有两个问题:

  • 项目中使用的字符串过多,则翻译类代码量很大,阅读不友好。
  • 使用翻译硬编码严重。

为了解决上面的问题,我们来进一步优化方案,其实对于程序员来说,第一个问题很好解决,不同语言的翻译提取一个单独的文件去搞就可以了,主要来看第二个问题(硬编码问题):

GetX Cli

GetX Cli是一个命令行脚本,它可以做到:

  • 创建项目
  • 项目工程化
  • 生成Model
  • 生成page
  • 生成view
  • 生成controller
  • 自定义controller模板
  • 生成翻译文件

本文主要介绍其翻译文件的相关内容:

1.安装get_cli

pub global activate get_cli 
# or
flutter pub global activate get_cli

1.生成国家化文件

在 assets/locales 目录创建 json 格式的语言文件

zh_CN.json:

{
  "buttons": {
    "login": "登录",
    "sign_in": "注册",
    "logout": "注销",
    "sign_in_fb": "用 Facebook 登录",
    "sign_in_google": "用 Google 登录",
    "sign_in_apple": "用 Apple 登录"
  }
}

en_US.json:

{
  "buttons": {
    "login": "Login",
    "sign_in": "Sign-in",
    "logout": "Logout",
    "sign_in_fb": "Sign-in with Facebook",
    "sign_in_google": "Sign-in with Google",
    "sign_in_apple": "Sign-in with Apple"
  }
}

2.运行命令行

get generate locales assets/locales

输出:

abstract class AppTranslation {
  static Map<String, Map<String, String>> translations = {
    'en_US' : Locales.en_US,
    'zh_CN' : Locales.zh_CN,
  };
}
abstract class LocaleKeys {
  static const buttons_login = 'buttons_login';
  static const buttons_sign_in = 'buttons_sign_in';
  static const buttons_logout = 'buttons_logout';
  static const buttons_sign_in_fb = 'buttons_sign_in_fb';
  static const buttons_sign_in_google = 'buttons_sign_in_google';
  static const buttons_sign_in_apple = 'buttons_sign_in_apple';
}

abstract class Locales {

  static const en_US = {
   'buttons_login': 'Login',
   'buttons_sign_in': 'Sign-in',
   'buttons_logout': 'Logout',
   'buttons_sign_in_fb': 'Sign-in with Facebook',
   'buttons_sign_in_google': 'Sign-in with Google',
   'buttons_sign_in_apple': 'Sign-in with Apple',
  };
  static const zh_CN = {
   'buttons_login': 'Entrar',
   'buttons_sign_in': 'Cadastrar-se',
   'buttons_logout': 'Sair',
   'buttons_sign_in_fb': '用 Facebook 登录',
   'buttons_sign_in_google': '用 Google 登录',
   'buttons_sign_in_apple': '用 Apple 登录',
  };

}

3.在GetMaterialApp中使用

GetMaterialApp(
      ...
      translationsKeys: AppTranslation.translations,
      ...
)

4.使用翻译

Text(LocaleKeys.buttons_login.tr);

到此,硬编码和代码分离的问题已解决。

源码

getX是如何实现国际化的,为什么不需要设置代理,点击xx.tr查看源码:

extension Trans on String {
  String get tr {
    // Returns the key if locale is null.
    if (Get.locale?.languageCode == null) return this;
    /*
    是从translations map查找,是一个map嵌套map的结构,外层key为${Get.locale!.languageCode}_${Get.locale!.countryCode}
    内层key:this
    */
    if (Get.translations.containsKey(
            "${Get.locale!.languageCode}_${Get.locale!.countryCode}") &&
        Get.translations[
                "${Get.locale!.languageCode}_${Get.locale!.countryCode}"]!
            .containsKey(this)) {
      return Get.translations[
          "${Get.locale!.languageCode}_${Get.locale!.countryCode}"]![this]!;

      // Checks if there is a callback language in the absence of the specific
      // country, and if it contains that key.
    } else if (Get.translations.containsKey(Get.locale!.languageCode) &&
        Get.translations[Get.locale!.languageCode]!.containsKey(this)) {
      return Get.translations[Get.locale!.languageCode]![this]!;
    } else if (Get.fallbackLocale != null) {
      final fallback = Get.fallbackLocale!;
      final key = "${fallback.languageCode}_${fallback.countryCode}";

      if (Get.translations.containsKey(key) &&
          Get.translations[key]!.containsKey(this)) {
        return Get.translations[key]![this]!;
      }
      if (Get.translations.containsKey(fallback.languageCode) &&
          Get.translations[fallback.languageCode]!.containsKey(this)) {
        return Get.translations[fallback.languageCode]![this]!;
      }
      return this;
    } else {
      return this;
    }
  }

举个例子来简单说明一下上面代码的逻辑:

{
  'zh_CN':{
    'buttons_sign_in_fb': '用 Facebook 登录',
  }
}
  1. languageCode_countryCode当key,去查找出:
{
    'buttons_sign_in_fb': '用 Facebook 登录',
}
  1. buttons_sign_in_fb当key,查找到具体的显示信息,而translations是在GetMaterialApp传进来的,查看get_material_app.dart:
 if (locale != null) Get.locale = locale;
 if (fallbackLocale != null) Get.fallbackLocale = fallbackLocale;
//Get接口添加了LocalesIntl extension,里面实现了addTranslations
if (translations != null) {
     Get.addTranslations(translations!.keys);
 } else if (translationsKeys != null) {
     Get.addTranslations(translationsKeys!);
}
注意: 单纯使用getX,在GetMaterialApp传入的是translations,而使用getX+getX Cli 传入的是 translationsKeys。

项目实践

方案虽然落地,但在项目开发中,还是会遇到各种问题,比如我们现有的项目,翻译文件是由翻译人员提供的,其格式为.xlsx,那么由.xlsx到json的转换就是一个必备的过程。

贴出一段项目里的数据:

截屏2021-09-01 下午6.33.11

excel和json的自动化转换

pandas

Pandas是一个强大的分析结构化数据的工具集;它的使用基础是Numpy(提供高性能的矩阵运算);用于数据挖掘和数据分析,同时也提供数据清洗功能。

Pandas提供了两大利器:

  • DataFrame:是Pandas中的一个表格型的数据结构,包含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型等),DataFrame即有行索引也有列索引,可以被看做是由Series组成的字典。
  • Series:是一种类似于一维数组的对象,是由一组数据以及一组与之相关的数据标签(即索引)组成。仅由一组数据也可产生简单的Series对象。
应用

pandas是python的一个package

//pip是python的包管理工具,如果出现pip commond not found,是因为你当前的python环境为python3,命令改为pip3 install pandas
pip install pandas
脚本具体实现分为两部分:
  • excel=>json
import pandas as pd
def excel_to_json(filename):
    # read_excel读取excel文件,同时设置列标签为'zh', 'en', 'ja', 'TW', 'de', 'es'
    df = pd.read_excel(filename, names=['zh', 'en', 'ja', 'TW', 'de', 'es'])
    # 取出每个Series转为json格式
    df.zh.to_json('jsons/zh.json')
    df.en.to_json("jsons/en.json")
    df.ja.to_json("jsons/ja.json")
    df.TW.to_json("jsons/tw.json")
    df.de.to_json("jsons/de.json")
    df.es.to_json("jsons/es.json")
  • json=>excel
def json_to_excel(filename):
    # 取出目录下所有的json格式文件
    #     json_files = glob.glob("*.json")
    jsons = ['jsons/zh.json','jsons/en.json','jsons/ja.json','jsons/tw.json','jsons/de.json','jsons/es.json']
    xlsx_file = []
    for json_str in jsons:
      language_str = json_str.split(".")[0]
      # 读取json文件
      file = open(json_str)
      text = file.read()
      text = json.loads(text)
      # 转为DataFrame
      df = json_normalize(text)
      xlsx_str = language_str+'.xlsx'
      # .T行列转置
      df.T.to_excel(xlsx_str)
      xlsx_file.append(xlsx_str)

    li =[]
    for i in xlsx_file:
          # index_col表示哪列当列号,默认是0-rows
          li.append(pd.read_excel(i,index_col=0))
    writer = pd.ExcelWriter(filename)
    # merge只能两两合并
    # axis:0 行合并 1列合并
    connact = pd.concat(li, axis=1,)
    #重新标记列标签
    connact.columns=['Chinese','English','Japanese','Traditional','German','Spanish']
    connact.to_excel(writer)
    writer.save()

    #删除临时文件
    for i in xlsx_file:
      if os.path.exists(i):
          os.remove(i)

注意:

  1. 执行脚本时出现read_excel报错等信息是因为没有安装openpyxl package,使用pip install openpyxl即可。
  2. 在使用GetX Cli时出现null类型无法转string的问题,在read_excel添加na_filter=False。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇