[Flutter] flutter_i18nによる多言語化対応

# 概要

Flutterでの標準的な多言語化対応は[flutter_localizations](https://flutter.dev/docs/development/accessibility-and-localization/internationalization)を使用するものです。
しかし、flutter_localizationsはOSデフォルトの言語を扱う分にはよいのですが、アプリ内で言語の切り替えを行うのには少し微妙というか、アプリ内で言語を切り替えた後の再描画（画面の更新）がやりづらく感じました。
⇛追記: そこまでやりづらくもなかった。[flutter_localizationsによる多言語化対応（アプリ内で言語切り替え）](https://qiita.com/satoshii/items/eeaaaaeff93ab94fa03c)

調べたところ、[flutter_i18n](https://pub.dev/packages/flutter_i18n)というパッケージでは再描画の仕組みが元々用意されているようだったので、これを使ってみることにします。

```dart
await FlutterI18n.refresh(context, newLocale);
```

これだけで対応するロケールでの再描画が行えます。

![スクリーンショット 2021-10-04 18.14.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141950/91ce1cf1-3f98-71e2-effb-3711b28f4454.png)

なお、flutter_i18nは現在のバージョンが0.30.0ということもあり、まだ枯れていない部分もあると思われるのでそこは注意してください。

# 使いかた
大まかな手順は

- 翻訳リソースの作成
- pubspec.yamlの修正
- MaterialAppにlocalizationsDelegatesを設定
- 翻訳部分の修正
- 言語切替時に再描画

という感じになります。

## 翻訳リソースの作成

翻訳リソースをウェブから取ったりもできるようですが、ここでは単にローカルのファイルから取ります。
プロジェクト配下に以下のようにファイルを作成します。

```
assets/flutter_i18n/ja.json
assets/flutter_i18n/en.json
```

ディレクトリ名はこれがデフォルトですが "i18n" や "locales" などでもよいようです。
ファイル形式はJSON、YAML、TOML、XMLから選べるようですが、ここではJSONにします。
ファイル名にも国コードを含むかなど命名規則がありますが、ここでは単純に言語コードだけ設定しています。

<!--
```sh:シェル操作だとこんな感じ
mkdir -p assets/flutter_i18n
touch assets/flutter_i18n/{en,ja}.json
```
-->

翻訳ファイルの詳しい書き方についてはドキュメントに記載がなかったのですが、少なくともJSONで次のような書き方はできるようです。

```json:ja.json
{
  "hello": "こんにちは",
  "helloName": "こんにちは{name}さん"
}
```

上記の設定に対して、次のような呼び出しができます。

```dart
I18nText("hello")
I18nText("helloName", translationParams: {"name": yourName})
```

複数形（Plural）の設定などもっと複雑なこともできるようです。


## pubspec.yamlの修正

pubspec.yamlに、依存性とアセットを追加します。

```yaml:pubspec.yaml
dependencies:
  flutter_i18n: ^0.30.0

flutter:
  assets:
    - assets/flutter_i18n/en.json
    - assets/flutter_i18n/ja.json
```

修正後は

```sh
flutter pub get
```

を忘れず行ってください。（IDEが勝手にやってくれるとは思いますが。）


## MaterialAppにlocalizationsDelegatesを設定

実際にはflutter_localizationsの場合と同様に`localeResolutionCallback`や`supportedLocales`の設定も必要になると思いますが、一旦最小構成のみ示します。

```dart:main.dart
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_i18n/flutter_i18n.dart';

:

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        FlutterI18nDelegate(
          // 国コードも使用する場合は useCountryCode: true を指定
          translationLoader: FileTranslationLoader(),
          // ここはサンプルそのまま
          missingTranslationHandler: (key, locale) {
            print("--- Missing Key: $key, languageCode: ${locale?.languageCode}");
          },
        ),
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate
      ],
      // RTLサポートが必要な場合（RTLとは？Right-to-Leftの話？）
      // builder: FlutterI18n.rootAppBuilder(),

      :

    );
  }
```

## 翻訳部分の修正

翻訳対象の`Text`を以下のように置き換えていきます。

```dart
I18nText("hello")
I18nText("helloName", translationParams: {"name": yourName})
```

`Text`でなく`String`が必要な場合は次のようにして取れます。

```dart
FlutterI18n.translate(context, "hello")
```

## 言語切替時に再描画

最後に、本題の再描画です。

```dart
// 英語に切り替えた際のイベント
onPressed: () async {
  await FlutterI18n.refresh(context, Locale("en", ""));
  setState(() {});
}
```

実際には切り替えた言語を設定に保存する処理などもあると思いますが、基本的にはこれだけです。

# サンプルアプリ

参考にサンプルアプリの全体を貼っておきます。

```json:ja.json
{
  "languageSettings": "言語設定",
  "hello": "こんにちは"
}
```

```json:en.json
{
  "languageSettings": "Language Settings",
  "hello": "Hello"
}
```

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_i18n/flutter_i18n.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        FlutterI18nDelegate(
          translationLoader: FileTranslationLoader(),
          missingTranslationHandler: (key, locale) {
            print("--- Missing Key: $key, languageCode: ${locale?.languageCode}");
          },
        ),
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate
      ],
      title: 'i18n sample',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HelloPage(),
    );
  }
}

class HelloPage extends StatefulWidget {
  const HelloPage({Key? key}) : super(key: key);

  @override
  _HelloPageState createState() => _HelloPageState();
}

class _HelloPageState extends State<HelloPage> {

  SimpleDialogOption _changeLocaleDialogOption(
      BuildContext context,
      String text,
      String languageCode) {
    return SimpleDialogOption(
      child: Text(text),
      onPressed: () async {
        await FlutterI18n.refresh(context, Locale(languageCode, ''));
        setState(() {});
        Navigator.pop(context);
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title : I18nText('hello')
      ),
      body: Center(
        child: TextButton(
          child: I18nText('languageSettings'),
          onPressed: () => showDialog(
            context: context,
            builder: (context) {
              return SimpleDialog(
                title: I18nText('languageSettings'),
                children: <Widget>[
                  _changeLocaleDialogOption(context, 'English', 'en'),
                  _changeLocaleDialogOption(context, '日本語', 'ja'),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}
```