[Flutter] Androidのstrings.xmlの感覚で言語リソースを扱う手順 [手作業での多言語対応]

# はじめに

Flutterの言語リソースを手作業で扱う手順を自分なりに整理してみました。
最初に書いておくと、実運用ではこの手順をやるより、<a href="https://github.com/localizely/flutter-intl-plugin-sample-app">flutter intl</a> pluginを使うのが良いと思います。
正直、Androidのstrings.xmlの感覚で言語リソースを扱えるようにするには
pluginを導入したほうが早いと思いますが、
手作業の手順もできれば試して覚えておくと良いと思います。

以前は手作業だとちょっと複雑で覚えられなかったのでAndroid Studio pluginの<a href="https://plugins.jetbrains.com/plugin/10128-flutter-i18n">flutter i18n</a>を使っていたのですが、
最新のAndroid Studioバージョン(4.0から？)に対応しなくなって困ってました。
手作業での実装手順を覚えないといけないかな・・・と思っていましたが、
<a href="https://github.com/localizely/flutter-intl-plugin-sample-app">flutter intl</a>のpluginが同じように手軽に使えそうです。
これもサポートが切れたら困りますが、今回、手作業の手順もまとめたので万が一そうなっても今後は大丈夫でしょう。

- flutter i18n pluginは2019/10 で更新が止まっている
- flutter intl pluginは2020/1 に初回リリース、2020/6に最終更新

となっていて、ひとまずflutter intl pluginに乗り換えで良さそうです。

ここではpluginの使い方ではなく、手作業(Dart intl tools)での多言語化(国際化)対応の手順を記載します。

# 初回手順概要

英語(en)と日本語(ja)をサポートするリソースをベースとして作成する手順で記載しています。
他にサポートする言語がある場合は後から追加します。

1. Flutterプロジェクトを用意
2. 依存関係のパッケージをpubspec.yamlファイルに追加
3. 言語リソース用のファイルを格納するディレクトリを作成
4. ベースとなるlocalizationのdartファイルを用意("l10n.dart"とする)
5. l10n.dartから多言語対応用のファイルを生成("intl_messages.arb")
6. 各言語向けリソースとなるlib/l10n/intl_en.arb、lib/l10n/intl_ja.arbを用意
7. intl_xx.arbから多言語対応用のファイルを生成
8. 言語リソースを使用するdartファイルにて、多言語実装する

今回の例では以下の配置構成としている。通常はl10n.dartとintl_xx.arbファイルが運用時の編集対象となる。
lib/intl/l10n.dart → 多言語化のベースとして必要なファイル
lib/intl/generated　→　コマンドで生成されるファイルの格納場所
lib/l10n/intl_xx.arb　→　各言語用リソースの格納先

# 初回手順実行

## 1. Flutterプロジェクトを用意

例としてflutter_l10n_sample

```powershell:コマンド
flutter create flutter_l10n_sample
```

## 2. 依存関係のパッケージをpubspec.yamlファイルに追加

以下のようにflutter_localizations、intl、intl_translationを追加。

```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl:
  intl_translation:
```

・flutter pub getコマンドでパッケージ情報を更新しておく

```powershell:コマンド
flutter pub get
```

## 3. 言語リソース用のファイルを格納するディレクトリを作成

```powershell:コマンド
cd flutter_l10n_sample
mkdir lib/intl/generated
mkdir lib/l10n
```

## 4. ベースとなるlocalizationのdartファイルを用意("l10n.dart"とする)

"\<ProjectRoot\>/lib/intl/l10n.dart" に新規ファイル作成する。
'generated/messages_all.dart'が見つからないエラーになるが、後で自動生成するため無視する。
「TODO START」～「TODO END」の2か所は、実際の運用で適宜更新する。

```l10n.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'generated/messages_all.dart';

class S {
  S();

  static S current;

  static const AppLocalizationDelegate delegate = AppLocalizationDelegate();

  static Future<S> load(Locale locale) {
    final name = (locale.countryCode?.isEmpty ?? false)
        ? locale.languageCode
        : locale.toString();
    final localeName = Intl.canonicalizedLocale(name);
    return initializeMessages(localeName).then((_) {
      Intl.defaultLocale = localeName;
      S.current = S();

      return S.current;
    });
  }

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

  //TODO START: add your resource
  String get greeting_message {
    return Intl.message(
      'Hello',
      name: 'greeting_message',
      desc: '',
      args: [],
    );
  }

  String greeting_message_with_name(Object name) {
    return Intl.message(
      'Hello $name',
      name: 'greeting_message_with_name',
      desc: '',
      args: [name],
    );
  }
  //TODO END: add your resource
}

class AppLocalizationDelegate extends LocalizationsDelegate<S> {
  const AppLocalizationDelegate();

  //TODO START: customize your support language
  List<Locale> get supportedLocales {
    return const <Locale>[
      Locale.fromSubtags(languageCode: 'en'),
      Locale.fromSubtags(languageCode: 'ja'),
    ];
  }
  //TODO END: customize your support language

  @override
  bool isSupported(Locale locale) => _isSupported(locale);
  @override
  Future<S> load(Locale locale) => S.load(locale);
  @override
  bool shouldReload(AppLocalizationDelegate old) => false;

  bool _isSupported(Locale locale) {
    if (locale != null) {
      for (var supportedLocale in supportedLocales) {
        if (supportedLocale.languageCode == locale.languageCode) {
          return true;
        }
      }
    }
    return false;
  }
}
```

## 5. l10n.dartから多言語対応用のファイルを生成("intl_messages.arb")

```powershell:コマンド
flutter pub pub run intl_translation:extract_to_arb --locale=messages --output-dir=lib/intl/generated lib/intl/l10n.dart
```

"lib/intl/generated/intl_messages.arb"にファイルが生成・出力される。
l10n.dartを入力として、「--output-dir」で指定したディレクトリにintl_messages.arbが生成・出力される。

## 6. 各言語向けリソースとなるlib/l10n/intl_en.arb、lib/l10n/intl_ja.arbを用意

```intl_en.arb
{
  "_locale": "en",
  "greeting_message": "Hello",
  "greeting_message_with_name": "Hello {name}"
}
```

```intl_ja.arb
{
  "_locale": "ja",
  "greeting_message": "こんにちは",
  "greeting_message_with_name": "こんにちは {name}"
}
```

## 7. intl_xx.arbから多言語対応用のファイルを生成

```powershell:コマンド
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/intl/generated --no-use-deferred-loading lib/intl/l10n.dart lib/intl/generated/intl_messages.arb lib/l10n/intl_en.arb lib/l10n/intl_ja.arb
```

"lib/intl/generated"に以下ファイルが生成・出力される。
messages_all.dart
messages_en.dart
messages_ja.dart
messages_messages.dart

## 8. 言語リソースを使用するdartファイルにて、多言語実装する

・MaterialAppにてローカリゼーションを設定する。

```main.dart
import 'package:flutter_localizations/flutter_localizations.dart';
import 'intl/l10n.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
//　・・・
      // アプリのローカリゼーション設定
      localizationsDelegates: [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      // アプリのサポートするロケール種別設定
      supportedLocales: S.delegate.supportedLocales,
//　・・・
    );
  }
}
```

・言語リソースを使う

```main.dart
            Text(S.of(context).greeting_message),
            Text(S.of(context).greeting_message_with_name("Taro")),
```

```powershell:コマンド
# コマンドから実行。またはIDEから実行。
flutter run
```

ここまでやって、やっと言語リソース定義を使える。

<img width="300" alt="l10n対応したリソースの英語と日本語表示" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428857/e78b437f-6cb5-0f55-532d-305b73620218.gif">

# 2回目以降の言語リソース変更手順

## 言語リソース変更時

intl_xx.arb(サポートする言語ロケール数分)を編集する。

* コマンドで言語リソースを再生成

```
flutter pub pub run intl_translation:extract_to_arb --locale=messages --output-dir=lib/intl/generated lib/intl/l10n.dart
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/intl/generated --no-use-deferred-loading lib/intl/l10n.dart lib/intl/generated/intl_messages.arb lib/l10n/intl_en.arb lib/l10n/intl_ja.arb
```

## 言語リソース追加時

* l10n.dartとintl_xx.arb(サポートする言語ロケール数分)を編集する。
* コマンドで言語リソースを再生成

* dartからリソースを参照する

## 言語リソース削除時

* dartからリソースの参照箇所を削除する
* l10n.dartとintl_xx.arb(サポートする言語ロケール数分)を編集する。
* コマンドで言語リソースを再生成

## サポートする言語を追加時

* l10n.dartのsupportedLocalesにサポートするlanguageCodeを追加
* intl_xx.arb(サポートする言語に対応するlanguageCodeのファイル)を用意する
* コマンドで言語リソースを再生成

# 終わりに

まだそれほどflutterを使い込んでいないですが、
flutterのUI widgetのコーディングは覚えてくるとサクサクできます。
でも多言語化はstrings.xmlのイメージで公式ページで紹介されているMapやintl toolsを使った手順などを見ると、手番が多くてちょっと面倒です。
それでもpluginを使うとほとんど6と8の作業だけで済むのでplugin使うのが便利です。
pluginの使い方は記事にするほどでもないと思うのでたぶん書かないです。(このintl toolを手動で使う手順ほど整理しておくことが思いつかない。)
<a href="https://github.com/localizely/flutter-intl-plugin-sample-app">flutter intl</a> のVS code、Android Studioそれぞれ使用するIDEのpluginページに記載の
Getting started、General Usage Instructionsに記載のInitialize部分まで実施したら、後は割と直感的に使えると思います。

# 参考

公式：https://flutter.dev/docs/development/accessibility-and-localization/internationalization
Dart intl tools：https://qiita.com/jiro-aqua/items/fd9896682ca09018fdd3
Dart intl tools実行時に出るlocaleのメッセージ：https://qiita.com/sekitaka_1214/items/2114d1870cfe607b4d87
l10nとi18n：https://ja.stackoverflow.com/questions/21726/internationalization-%E3%81%A8-localization-%E3%81%AE%E9%81%95%E3%81%84
l10nとi18n：https://ja.wikipedia.org/wiki/%E5%9B%BD%E9%9A%9B%E5%8C%96%E3%81%A8%E5%9C%B0%E5%9F%9F%E5%8C%96