Flutterで多言語化対応する(Dart intl toolsの使い方)

Flutterに入門しました。
サンプルアプリを動かしてみたあと、「とりあえず多言語化対応はどうやるのかな」とAndroidのvalues/strings.xmlみたいなものを念頭に置きながら調べ始めて、最終的に到達した結論を書き残しておきます。
三日ぐらいしかいじってないので、ぼけてるところがあればツッコミお願いします。

# Flutterで多言語化対応するには

まず、Flutter/Dartの開発系には文字列リソースという基本的な仕組みはないようです。
Flutter公式では以下のガイドラインが掲載されています。
https://flutter.io/tutorials/internationalization/

リンク先をざっと眺めて欲しいのですが、つまるところMapにKVペアを突っ込んで対応する文字列を返すコードを書け、と言うことになります。

さすがにこれはいくら何でもアレでしょう、ということで、Dart intl toolsというものが用意されていることがAppendixに書いてあります。本稿では、このツールの使い方を説明します。

# Dart intl toolsとは

Dartコマンドラインで動く文字列リソースの加工ツールです。
- dartファイル内の国際化リソースをARBファイルに抽出する。
- ARBファイルを元に対応するdartコードを生成する。

の二つのツールからなっています。

# その前に

Android StudioのNew Flutter Projectで作るサンプルアプリを元に進めていきます。
完成品はこちらに([github](https://github.com/jiro-aqua/flutter-localization))

まず、パッケージをプロジェクトに追加します。

```pubspec.yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl:
  intl_translation:
```
Dependenciesセクションに上の四行を追記します。
 
次に、lib/l10nディレクトリを掘り、以下のファイルを追加します。

```Strings.dart
import 'dart:async';

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

import 'messages_all.dart';

class Strings {
  static Future<Strings> load(Locale locale) {
    final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);
    // ignore: strong_mode_uses_dynamic_as_bottom
    return initializeMessages(localeName).then((Null _) {
      Intl.defaultLocale = localeName;
      return new Strings();
    });
  }

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

  static final Strings instance = new Strings();

  // TODO 追加すべき文言
  String get title => Intl.message('Flutter Demo Home Page', name: "title");
  String get message => Intl.message('You have pushed the button this many times:', name: "message");
}
```
この時点で、importやinitializeMessages()が解決しませんが、問題ありません。
TODOコメントの後が、必要に応じて追加すべき文言となります。

# ARBファイルのエクスポート

プロジェクトのルートで以下のコマンドを実行します。

```
$ flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/l10n/Strings.dart
```
これによって、lib/l10n/Strings.dart 内のIntl.message()呼び出しが抽出されて、intl_message.arbが出力されます。

```intl_messages.arb
{
  "@@last_modified": "2018-03-04T02:14:41.057019",
  "title": "Flutter Demo Home Page",
  "@title": {
    "type": "text",
    "placeholders": {}
  },
  "message": "You have pushed the button this many times:",
  "@message": {
    "type": "text",
    "placeholders": {}
  }
}
```
これを元に以下の様なファイルを作成します。

```intl_ja.arb
{
  "title": "フラッターデモホームページ",
  "message": "ボタンを押した回数がここに表示されます"
}
```

# ARBからdartコードの生成

ここで、以下のコマンドを実行します。

```
$ flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/Strings.dart lib/l10n/intl_messages.arb lib/l10n/intl_ja.arb
```

これによって、以下のファイルが生成されます。

- messages_all.dart
- messages_ja.dart
- messages_messages.dart

これらのファイルは DO NOT EDIT とあるので、編集してはいけません。
この時点で、先のビルドエラーは消えるはずです。

# 呼び出し側の設定

以下の修正を行います。

```main.dart
import 'dart:async';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/Strings.dart';
```
```main.dart
class _MyLocalizationsDelegate extends LocalizationsDelegate<Strings> {
  const _MyLocalizationsDelegate();

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

  @override
  Future<Strings> load(Locale locale) => Strings.load(locale);

  @override
  bool shouldReload(_MyLocalizationsDelegate old) => false;
}
```
```main.dart
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      localizationsDelegates: [
        const _MyLocalizationsDelegate(),
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en', ''),
        const Locale('ja', ''),
      ],
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
```

MaterialAppのパラメータに、localizationsDelegatesとsupportedLocalesを追加します。

ここまでで準備は終わり。文字列の呼び出しを修正します。

```main.dart
 appBar: new AppBar(
   title: new Text(Strings.of(context).title),
 ),
```

```main.dart
 new Text(
   Strings.of(context).message,
 ),
```
Strings.of(context).\[id\] の形で文字列を呼び出すことが出来ます。

実行するとこんな感じになります。日本語表記／英語表記
<img src="https://github.com/jiro-aqua/flutter-localization/blob/master/ss_ja.png?raw=true" width=25%> <img src="https://github.com/jiro-aqua/flutter-localization/blob/master/ss_default.png?raw=true" width=25%>

# 文言の管理
この後の文言の管理手順としては
1. Strings.dart に項目を追加
2. ARBに変換
3. intl_ja.arb に日本語翻訳を追加
4. dartを生成
5. 呼び出す
となります。
全部手作業でやるよりは少しはマシになったと思われます。

# 困った時は

Package追加した時、Android Studioの上の方に'get package'と出るので、クリックしましょう。
その際にエラーが出る場合は、FLUTTER_ROOTの設定し忘れかもしれません。
2018/03/04現在のFlutter インストールガイドではFLUTTER_ROOTへの言及がないので、設定しておきましょう。
(Flutterをインストールしたディレクトリを設定します)

参考) flutter projectでpub runコマンドが打てない時の対処法
https://qiita.com/konifar/items/d93e20ac0461e58a5247

# 参考リンク

Internationalizing Flutter Apps (公式ローカライゼーションのガイド)
https://flutter.io/tutorials/internationalization/#loading-and-retrieving

github dart-lang/intl_translation (ツールのソース)
https://github.com/dart-lang/intl_translation

github konifar/droidkaigi2018-flutter (現時点で日本語化されている数少ないFlutterアプリ)
https://github.com/konifar/droidkaigi2018-flutter

本記事のサンプルソース
https://github.com/jiro-aqua/flutter-localization

# 免債

Windows/Android環境でしか動かしていません。

# 感想

当分はポチポチ遊びながら、プロダクト投入可能なラインを探っていくことになるのでしょう。面白そうなので、今後に期待したいと思います。

以上です。