Flutterで多言語化する方法


# 2021/1/8追記
現在はarbファイルからdartファイルを生成してくれるようになっていて結構楽になったようです。

# 以下は当時の手順

[公式のチュートリアル](https://flutter.io/tutorials/internationalization/)を参考にして、Flutterアプリのテキストのローカライズ方法を説明します。


`flutter_localizations, intl, intl_translation`を依存関係に追加します。

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  # 追記ここから
  flutter_localizations:
    sdk: flutter
  intl:
  intl_translation:
  # 追記ここまで
```

次のコマンドで依存関係を解決します。

```bash
cd <プロジェクトのルートディレクトリ>
flutter pub get
```

次のコマンドを実行して、`intl_messages.arb` `messages_all.dart` `messages_messages.dart` を生成します。 `intl_messages.arg`がjson形式の翻訳定義ファイルになりますが、今は翻訳対象のリソースが指定されていないので大した内容はありません。このコマンドを一度実行するのは`dart`ファイルを生成したいためです。

```bash
mkdir -p lib/l10n
flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/main.dart
```


プログラムで使用されている`Intl.message`からリソースファイルを生成するため、先に翻訳リソースを使用する部分を記述しておきます。このシンプルな例では `Hello World`を出力する`title`という名前のリソースが生成されます。
そのために`lib/main.dart`に以下のクラスを追加します。

```dart:lib/main.dart
class DemoLocalizations {
  static Future<DemoLocalizations> load(Locale locale) {
    final String name = locale.countryCode.isEmpty
        ? locale.languageCode
        : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);
    return initializeMessages(localeName).then((_) {
      Intl.defaultLocale = localeName;
      return new DemoLocalizations();
    });
  }

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

  String get title {
    return Intl.message(
      'Hello World',
      name: 'title',
      desc: 'Title for the Demo application',
    );
  }
}
```


そしてもう一度arbファイルの生成コマンドを実行します。

```bash
flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/main.dart
```

すると`l10n/intl_messages.arb`は次のような内容になります。`title`の定義が追加されました。

```json:l10n/intl_messages.arb
{
  "@@last_modified": "2018-04-06T20:58:49.424203",
  "title": "Hello World",
  "@title": {
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {}
  }
}
```

この`l10n/intl_messages.arb`をコピーして`l10n/intl_en.arb`と`l10n/intl_ja.arb`を作ります。
内容はそれぞれ次のようになります。単純に翻訳しているだけですね。

```json:l10n/intl_en.arb
{
  "title": "Hello World",
  "@title": {
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {}
  }
}
```

```json:l10n/intl_ja.arb
{
  "title": "こんにちは世界",
  "@title": {
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {}
  }
}
```

ここまでファイルの準備ができたら以下のコマンドを実行します。

```bash
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/main.dart lib/l10n/intl_*.arb
```

すると `l10n/messages_en.dart` と `l10n/messages_ja.dart` という2つのdartファイルが生成されます。
`l10n/messages_ja.dart`の内容は次のようになっており、先ほど作成したリソースファイルがdartのクラスとして生成されたことがわかります。

```dart:l10n/messages_ja.dart
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ja locale. All the
// messages from the main program should be duplicated here with the same
// function name.

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

final messages = new MessageLookup();

// ignore: unused_element
final _keepAnalysisHappy = Intl.defaultLocale;

// ignore: non_constant_identifier_names
typedef MessageIfAbsent(String message_str, List args);

class MessageLookup extends MessageLookupByLibrary {
  get localeName => 'ja';

  final messages = _notInlinedMessages(_notInlinedMessages);
  static _notInlinedMessages(_) => <String, Function> {
    "title" : MessageLookupByLibrary.simpleMessage("こんにちは世界")
  };
}
```

次に`MaterialApp`のインスタンス生成部分を次のように変更します。

```dart
    return new MaterialApp(
      title: "AppName",
      home: new Invite(),
      // ここから追記
      localizationsDelegates: [
        const DemoLocalizationsDelegate(), 
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale("en", "US"),
        const Locale("ja", "JP")
      ],
      // ここまで追記
    );
```

`DemoLocalizationsDelegate`は今回のローカライズのためのDelegateです。
`GlobalMaterialLocalizations.delegate` は多分組み込みのローカライズのもの？日付のフォーマットなどが管理されている。
`GlobalWidgetsLocalizations`はウィジェットをローカライズするために必要？右から書く言語とかに対応するためにいるのか？
`supportedLocaled`は対応言語のリストですかね。
このあたり必要性がよくわかっていないのですが、書かなかったら動かなかったので、必要なんでしょう。

`main.dart`には先ほどのDelegateも追加しておきます。

```dart:lib/main.dart
class DemoLocalizationsDelegate
    extends LocalizationsDelegate<DemoLocalizations> {
  const DemoLocalizationsDelegate();

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

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

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

ここまでやってリビルドするとローカライズされたテキストを出力することができます。

|英語|日本語|
|---|---|
|<img src="https://qiita-image-store.s3.amazonaws.com/0/18273/8bf815b7-097d-d1d9-3810-40134ed7dab7.png" width="300">|<img src="https://qiita-image-store.s3.amazonaws.com/0/18273/603fd76a-4978-b9fc-9d2a-1b5fa5290cda.png"  width="300">|

# 感想
現時点では手順が少し面倒に感じますね。
ただintlを使用しないで直接マップをコードに書く方法もあったりして、選択するライブラリ次第では改善出来るのかもしれません。

そして使用する画面毎にクラスを分けたほうがいいのか。巨大な1つの言語リソース用のクラスを作っておくのか悩ましいところです。
クラス定義して関数まで作っておく必要があるのが、ちょっとしんどい。それをもとにarb作って、またdartのクラスが生成されてって感じなので。

まぁまずは使い続けてみることで自分なりの解決策が見つかるかもしれないので、気長に使ってみます。