flutter多言語化 flutter_localizationsからの脱却

flutterの多言語かといえばflutter_localizationsですね！

https://qiita.com/kazutxt/items/936ebe7a21ede7e1ab20


こちらの記事にあるように、簡単に多言語化対応ができるflutter_localizations。
私も以前はflutter_localizationsを使用していたのですが.arbファイルが大きくなるにつれ以下の点から代替案を探す必要性があると感じていました。

<br>
・.arbファイルにコメントができない
<br>
・ファイルの分割が一筋縄ではいかない
<br>
・開発段階で実装漏れに気づけない<br>
(実装漏れのある箇所はデフォルトに設定した.arbから表示される)
<br>
<br>

ほかにいい方法も思いつかないので、シンプルにインターフェースを実装した各言語のクラスをproviderの初期化メソッド内で端末の言語設定に応じて出し分ける方法で実装しました。


まずはインターフェースを用意します。
```dart:interfaces/counter_page_text.dart
abstract interface class CounterPageText {
  String get counterPageTitle;

  String get countUpDescriptionText;
  String get saveCountButtonText;

  String get errorText;
}

```

<br>
各言語のcounterPageTextクラスでは必ずこのインターフェースをimprementsします。
後からページにテキストを増やしたいときはインターフェースから更新してあげると各言語ファイルで追加したテキストの未実装エラーが出てくれるので実装漏れが防げます。<br>
一つのファイルにすべてのテキストを実装すると見通しが悪くなるのでいい感じの粒度で分割してあげます。
<br>

```dart:interfaces/snackbar_text.dart
abstract interface class SnackbarText {
  //成功
  String get countSaveSuccess;
  //失敗
  String get countSaveFailed;
  String get countFetchFailed;
}
```
スナックバーに表示するテキスト<br><br><br>

```dart:interfaces/tooltip_text.dart
abstract interface class ToolTipText {
  String get floatingActionButtonTooltip;
}

```
tooltipのテキスト<br><br><br>

次は新しいフォルダjaに先のインターフェースをimplementsした日本語のファイルを実装します。<br>
```dart:ja/counter_page_text_ja.dart
class CounterPageTextJa implements CounterPageText {
  @override
  final String counterPageTitle = "カウンター";

  @override
  final String countUpDescriptionText = "ボタンを押すとカウントアップします";
  @override
  final String saveCountButtonText = "保存";
  @override
  final String errorText = "エラーが発生しました";
}

```

同様にtooltip, snackbarの実装が終わったら、それらをまとめるクラスを実装していきます。<br>

まずはインターフェースから

```dart:interfaces/localized_strings.dart
abstract interface class LocalizedStrings {
  CounterPageText get counterPage;
  SnackbarText get snackbar;
  ToolTipText get toolTip;
}
```

localizedStrings -> counterPage -> counterPageTitle
といったような入れ子構造になっています。
ここもインターフェースにすることで実装漏れが防げます。<br><br><br>
このインターフェースをimplementsした実体を実装します。

```dart:ja/localized_strings_ja.dart
class LocalizedStringsJa implements LocalizedStrings {
  @override
  final CounterPageText counterPage = CounterPageTextJa();
  @override
  final SnackbarText snackbar = SnackbarTextJa();
  @override
  final ToolTipText toolTip = ToolTipTextJa();
}

```

これで日本語のテキストを実装したクラスの実体が作成できました。<br>
日本語のクラスと同様に英語のクラスも同様に作成してあげます。
jaフォルダをそのままコピーして「ja」とあるところを「en」にして日本語を英語に翻訳してあげるだけで簡単に新規言語のファイルが作成できます。<br>
追加で言語を増やしたい時に楽です。


```dart:en/localized_strings_en.dart
class LocalizedStringsEn implements LocalizedStrings {
  @override
  final CounterPageText counterPage = CounterPageTextEn();
  @override
  final SnackbarText snackbar = SnackbarTextEn();
  @override
  final ToolTipText toolTip = ToolTipTextEn();
}

```
<br>

```dart:en/counter_page_text_en.dart
class CounterPageTextEn implements CounterPageText {
  @override
  final String counterPageTitle = "Counter";

  @override
  final String countUpDescriptionText = "Counts up when the button is pressed.";
  @override
  final String saveCountButtonText = "Save";
  @override
  final String errorText = "An error has occurred.";
}


```
<br>
これで言語ファイルは完成です。
あとは端末の言語設定にあわせてLocalizedStringsEnとLocalizedStringsJaを出し分けてあげます。
riverpodのproviderで提供してあげます。<br>

```dart:localized_strings_provider.dart
final localizedStringProvider = StateProvider<LocalizedStrings>((
  ref,
) {

//端末の言語設定を取得
//この方法だとcontextを使用せずに取得できる
  final Locale currentLocale =
      WidgetsBinding.instance.platformDispatcher.locale;

  final String lgCode = currentLocale.languageCode;
  switch (lgCode) {
    case 'ja':
      return LocalizedStringsJa();
    default:
      return LocalizedStringsEn();
  }
});

```
<br>
端末のlocaleを取得し言語コードごとに先ほど作成した言語ファイルを出し分けます。
端末の言語設定が日本語ならLocalizedStringsJaを、それ以外ならEnをprovideしてあげます。<br>


※languageCode一覧はこちら

https://api.flutter.dev/flutter/flutter_localizations/GlobalMaterialLocalizations-class.html

<br>
あとはページでConsumerWidgetのrefからこのproviderを呼び出してあげるだけです。

```dart:counter_page.dart
class CounterPage extends ConsumerWidget {
  const CounterPage({
    super.key,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    //providerの初期化
    //言語ファイル
    final localizedStrings = ref.watch(localizedStringProvider);
    
    final count = ref.watch(countProvider);

    return count.when(
      data: (count) => Scaffold(
          appBar: AppBar(
            title: Text(localizedStrings.counterPage.counterPageTitle),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                BigText(
                    text: localizedStrings.counterPage.countUpDescriptionText),
                //BigTextとSmallTextの縦の隙間を埋めるwidget
                Gap.h(RawInt.p32.h),
                SmallText(text: count.value.toString()),
              ],
            ),
          ),
          //countを増やすボタン
          floatingActionButton: CountUpFloatingActionButton(
            count: count,
          )),
      loading: () {
        return const Center(child: CircularProgressIndicator());
      },
      error: (error, stackTrace) =>
          Center(child: Text(localizedStrings.counterPage.errorText)),
    );
  }
}
```

以上になります。
新たにテキストを追加する際にinterfaceの更新もしなければいけないので手間といえば手間ですが、メンテナンス性が高まったように感じます。<br>