實用的 Flutter 國際化指南

做爲一個 Android 開發者,Flutter 上來就讓我把各種字符串寫在 widget 裏,其實我內心是拒絕的。硬編碼是不可能硬編碼的。國際化又不會,就是隻能去看看文檔,才能學點新姿式這樣子。看了文檔以後,以爲國際化這部分,仍是有點麻煩的,我以爲有必要拎出來單獨寫寫。git

我的但願能把應用的字符串資源獨立出來,以方便管理。至於支持多語言這種,反而是順帶完成的結果。本文以實用優先,由於我認爲這部份內容是每一個應用都須要使用的。github

切入點

首先簡單認識一下 Flutter 國際化相關的知識點。json

添加 flutter_localizations 依賴,讓 Flutter 知道咱們須要使用國際化相關的包。Flutter 自帶的 widget 中,也用到了一些字符串資源,好比,showSearch() 方法打開的搜索欄提示。而這個包能夠提供英文以外的,被 Flutter 內部默認使用的國際化字符串資源。app

dependencies:
flutter:
 sdk: flutter
flutter_localizations:
 sdk: flutter
複製代碼

而後在建立 App 時,加入 LocalizationsDelegate,國際化的內容就由這些類來提供。GlobalMaterialLocalizations.delegate 提供了 Material 組件庫所使用的字符串資源;GlobalWidgetsLocalizations.delegate 則定義了在當前的語言中,文字默認的排列方向。less

以後咱們定義了本身的國際化內容後,也須要加入到這個列表的頭部。ide

還要聲明要支持什麼語言,supportedLocales 這裏添加了英文和中文兩種。若是說用戶的語言不在這個列表內,則會默認使用列表第一項指定的語言。假如你對這個規則不滿意,可使用 localeResolutionCallback 參數來自定義本身想要的規則。函數

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

class ThisApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en', 'US'),
        const Locale('zh', 'CN'),
      ],
      title: 'App Title',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}
複製代碼

如今,咱們將一些自帶的國際化資源加入到了應用中,Flutter 自身已經可以使用它們了。但咱們要怎麼使用它們呢?工具

經過 MaterialLocalizations.of(context) 獲取到 MaterialLocalizations 的實例,而後訪問裏面的字符串。好比上面的 title 一行,能夠替換爲:優化

onGenerateTitle: (context) => MaterialLocalizations.of(context).closeButtonLabel,
複製代碼

注意這裏將 title 替換成 onGenerateTitle 了,由於此時還在初始化 App 中,沒法獲取到 context,更沒法經過 context 獲取字符串了。ui

自定義的國際化內容

如今來考慮怎麼將咱們本身的國際化加入到其中。也就是,須要在 localizationsDelegates 中加入本身的 LocalizationsDelegate

查看文檔,LocalizationsDelegate 須要一個泛型參數。參考官方的文檔,可知這裏指定的類型就是咱們存放字符串的類。在這裏,有兩種選擇:第一是基於 map 的,很是簡單的實現;第二個則是經過 Dart 語言中專門負責國際化的 intl 包來實現。接下來咱們按次來看看。

基於 Map

class SimpleLocalizations {
  SimpleLocalizations(this.locale);

  final Locale locale;

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

  static Map<String, Map<String, String>> _localizedValues = {
    'en': {
      'app_name': 'App Name',
      'hello_world': 'Hello World',
    },
    'zh': {
      'app_name': '應用名',
      'hello_world': '你好世界',
    },
  };

  Map<String, String> get _stringMap {
    return _localizedValues[locale.languageCode];
  }

  String get helloWorld {
    return _stringMap['hello_world'];
  }

  String get appName {
    return _stringMap['app_name'];
  }
}
複製代碼

從上面的代碼能夠看到,這種方法的原理很是簡單,就是將全部字符串放進 map,而後經過應用的 Locale 來取出對應語言的字符串。使用時,就是 SimpleLocalizations.of(context).helloWorld 這樣來引用字符串。

其對應的 LocalizationsDelegate 以下:

class SimpleLocalizationsDelegate extends LocalizationsDelegate<SimpleLocalizations> {
  const SimpleLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);

  @override
  Future<SimpleLocalizations> load(Locale locale) {
    return SynchronousFuture<SimpleLocalizations>(SimpleLocalizations(locale));
  }

  @override
  bool shouldReload(SimpleLocalizationsDelegate old) => false;
}
複製代碼

只要將這個 SimpleLocalizationsDelegate 加入到上面的 delegates 列表中,國際化就算完成了。

回看一下整個流程,並不算複雜,須要經手部分的原理也很是簡單,只是一個 map 的使用。使用這個方法能夠將整個應用的字符串都集中到一塊兒管理。可是,維護起來仍是很不方便。

基於 intl

接下來看看基於 intl 包的實現方法是怎麼樣的。

第一步,添加依賴:

dependencies:
 intl: ^0.15.7

dev_dependencies:
 intl_translation: ^0.17.3
複製代碼

經過查看官方的例子,能夠知道 Intl.message() 方法是咱們管理字符串的關鍵。因而去看相關的文檔,會發現——嗯,沒有卵用(甚至沒解釋每一個參數有什麼做用)。

接下來仍是同樣添加一個跟 SimpleLocalizations 差很少類:

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

  String get appName {
    return Intl.message('App Name');
  }

  String get helloWorld {
    return Intl.message('Hello world');
  }
}
複製代碼

從命令行中運行 flutter pub pub run intl_translation:extract_to_arb --output-dir=你想要的輸出目錄 IntlLocalizations所在文件。這一操做將會在指定目錄裏生成一個名爲 intl_messages.arb 的文件,內容大體以下:

{
  "@@last_modified": "2019-02-17T15:57:00.554988",
  "App Name": "App Name",
  "@App Name": {
    "type": "text",
    "placeholders": {}
  },
  "Hello world": "Hello world",
  "@Hello world": {
    "type": "text",
    "placeholders": {}
  }
}
複製代碼

將這個文件複製一份,命名爲 intl_en.arb,做爲英文版本使用。接着再複製一份,命名爲 intl_zh.arb 做爲中文版本使用。將 intl_zh.arb 的內容修改成對應中文的內容:

{
  "@@last_modified": "2019-02-17T15:57:00.554988",
  "App Name": "應用名",
  "@App Name": {
    "type": "text",
    "placeholders": {}
  },
  "Hello world": "你好世界",
  "@Hello world": {
    "type": "text",
    "placeholders": {}
  }
}
複製代碼

若是須要其餘語言的版本,請自行添加並修改。

再來輸入一段長長的命令行:flutter pub pub run intl_translation:generate_from_arb --output-dir=輸出目錄 --no-use-deferred-loading IntlLocalizations所在文件 全部arb文件。這樣會生成幾個 messages_ 開頭的 dart 文件。能夠自行查看一下里面的內容,我如今的 Dart 水平還比較菜,就先不分析其中的原理了。

其中名爲 messages_all.dart 的文件裏,生成了 initializeMessages(String localeName) 這個方法,將會在下面的步驟中使用到。

IntlLocalizations 中添加以下的方法:

static Future<IntlLocalizations> load(Locale locale) {
  final name =
    locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
  final localeName = Intl.canonicalizedLocale(name);
  return initializeMessages(localeName).then((_) {
    Intl.defaultLocale = localeName;
    return IntlLocalizations();
  });
}
複製代碼

IntlLocalizations 就準備完畢了。而後,開始實現 delegate,內容很簡單:

class IntlLocalizationsDelegate extends LocalizationsDelegate<IntlLocalizations> {
  const IntlLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);

  @override
  Future<IntlLocalizations> load(Locale locale) {
    return IntlLocalizations.load(locale);
  }

  @override
  bool shouldReload(IntlLocalizationsDelegate old) => false;
}
複製代碼

以後就是正常使用流程了,這裏不贅訴。回想整個流程,真正國際化的內容在 arb 文件中,對於集中管理字符串來講,比使用 map 仍是好一點。可是整個流程仍是顯得異常麻煩,尤爲是兩次長得過度的命令行,明顯應該由工具來改進。我相信 Flutter/Dart 團隊應該會在這一點上作出優化。

flutter_i18n

那麼,有沒有一款工具能夠解救咱們呢?您好,有的。

Android Studio(IDEA)上有一款名爲 flutter_i18n 的插件,能夠幫助簡化這個過程。其原理是經過 arb 文件來自動生成所須要的代碼。

插件的使用很是簡單,安裝後會出現一個新的按鈕。一旦你按下這個按鈕——boom——插件就會根據 res/values 文件夾(Android 開發者以爲很親切)中的 arb 文件,在 lib/generated 中生成 Dart 代碼。

那麼咱們的重心就放在了 arb 文件上。Arb 文件全稱是 Application Resource Bundle,是基於 JSON 的 balabala 接下去的我也不想接着說了,由於並不實用。仍是來看下 Flutter 國際化中切實相關的部分。

雖然咱們知道了 arb 文件是類 JSON 格式,但咱們還並不清楚文件裏具體須要什麼樣的內容。這裏咱們經過 Intl.message() 方法再從新認識一下。

String get appName {
  return Intl.message(
    'App Name',
    desc: 'Name for the application',
    name: 'IntlLocalizations_appName',
  );
}

String hello(String name) {
  return Intl.message(
    'Hello $name',
    name: 'IntlLocalizations_hello',
    desc: 'Say hello to someone',
    args: [name],
    locale: 'en',
    examples: const {'name': 'Someone'},
    meaning: 'What is this?',
    skip: false,
  );
}
複製代碼

這裏有兩個更爲詳細的實現,其中 hello 方法將所有的參數都賦值了,以方便觀察經過 intl_translation 包處理後的 arb 文件會是什麼樣的。

不過這以前簡單介紹一下 Intl.message() 的部分參數。

  • name 參數必須與函數名一致,或者是類名_方法名這個形式——建議使用後者避免衝突;
  • args 就是重複一遍參數;
  • 若是方法沒有參數,那麼 nameargs 能夠省略;
  • desc 參數就是描述這個字符串的字符串,必須是一個字符串字面量;
  • examples 是參數的示例;
  • descexamples 在運行時不會被使用,但會被提取出來做爲額外的信息提供給翻譯人員做爲參考;
  • skip 若是爲 true,那麼這條記錄就不會被提取出來;
  • 其餘的文檔裏並無提。

而後咱們再運行一下那個很長的命令行,將其處理成 arb 文件看看:

{
  "@@last_modified": "2019-02-18T21:31:28.750455",
  "IntlLocalizations_appName": "App Name",
  "@IntlLocalizations_appName": {
    "description": "Name for the application",
    "type": "text",
    "placeholders": {}
  },
  "IntlLocalizations_hello": "Hello {name}",
  "@IntlLocalizations_hello": {
    "description": "Say hello to someone",
    "type": "text",
    "placeholders": {
      "name": {
        "example": "Someone"
      }
    }
  }
}
複製代碼

首先,meaning 彷佛沒有用處。其核心就是 "IntlLocalizations_appName": "App Name" 這樣的一條一條的記錄。以 @ 開頭的部分,並不會真正在程序中使用,而是給翻譯人員做爲參考使用的。

這麼一來,咱們接下來就能夠在 res/values 文件夾中建立須要的 arb 文件了。這個插件還提供了快捷建立 arb 文件的功能,只須要在 res/values 目錄右鍵選擇 New -> Arb File 就能夠選擇這個 arb 文件的 locale 了。

須要注意的是,在這個插件中,若是字符串內須要包含變量,使用的語法是 $var_name,而不是上面例子裏使用大括號的形式。

這裏我建立了兩個 arb 文件:

// strings_en.arb
{
  "appName": "App Name",
  "hello": "Hello $name"
}
// strings_zh_CN.arb
{
  "appName": "應用名",
  "hello": "你好${name}"
}
複製代碼

使用插件生成代碼後,將 delegate 加入到應用的列表中,使用時也只要直接利用 S 這個類名來引用就好:

MaterialApp(
    localizationsDelegates: [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
    ],
    supportedLocales: [
        const Locale('en', 'US'),
        const Locale('zh', 'CN'),
    ],
    onGenerateTitle: (context) => S.of(context).appName,
    ...
複製代碼

使用了這個插件以後,國際化就算得上方便了。生成的代碼也能夠稍微看一眼,或許有你用獲得的其餘方法。

最後提醒一句,因爲生成代碼是由插件完成的,因此依賴中的 intl_translation 能夠刪掉了。

其餘與總結

可能有的人會問,不使用 IDEA 的開發者,有沒有什麼更好的選擇呢?或許有。如今還有一個名爲 rosetta 的庫,致力於解決 Flutter 國際化太過複雜的問題。我嘗試過,但並無跑通正常的流程,沒法更多評價。有興趣的朋友能夠試試看。

到此,這篇指南就結束了,但願能對一些人有幫助。

相關文章
相關標籤/搜索