flutterの多言語かといえばflutter_localizationsですね!
こちらの記事にあるように、簡単に多言語化対応ができるflutter_localizations。
私も以前はflutter_localizationsを使用していたのですが.arbファイルが大きくなるにつれ以下の点から代替案を探す必要性があると感じていました。
・.arbファイルにコメントができない
・ファイルの分割が一筋縄ではいかない
・開発段階で実装漏れに気づけない
(実装漏れのある箇所はデフォルトに設定した.arbから表示される)
ほかにいい方法も思いつかないので、シンプルにインターフェースを実装した各言語のクラスをproviderの初期化メソッド内で端末の言語設定に応じて出し分ける方法で実装しました。
まずはインターフェースを用意します。
abstract interface class CounterPageText {
String get counterPageTitle;
String get countUpDescriptionText;
String get saveCountButtonText;
String get errorText;
}
各言語のcounterPageTextクラスでは必ずこのインターフェースをimprementsします。 後からページにテキストを増やしたいときはインターフェースから更新してあげると各言語ファイルで追加したテキストの未実装エラーが出てくれるので実装漏れが防げます。
一つのファイルにすべてのテキストを実装すると見通しが悪くなるのでいい感じの粒度で分割してあげます。
abstract interface class SnackbarText {
//成功
String get countSaveSuccess;
//失敗
String get countSaveFailed;
String get countFetchFailed;
}
スナックバーに表示するテキスト
abstract interface class ToolTipText {
String get floatingActionButtonTooltip;
}
tooltipのテキスト
次は新しいフォルダjaに先のインターフェースをimplementsした日本語のファイルを実装します。
class CounterPageTextJa implements CounterPageText {
@override
final String counterPageTitle = "カウンター";
@override
final String countUpDescriptionText = "ボタンを押すとカウントアップします";
@override
final String saveCountButtonText = "保存";
@override
final String errorText = "エラーが発生しました";
}
同様にtooltip, snackbarの実装が終わったら、それらをまとめるクラスを実装していきます。
まずはインターフェースから
abstract interface class LocalizedStrings {
CounterPageText get counterPage;
SnackbarText get snackbar;
ToolTipText get toolTip;
}
localizedStrings -> counterPage -> counterPageTitle
といったような入れ子構造になっています。
ここもインターフェースにすることで実装漏れが防げます。
このインターフェースをimplementsした実体を実装します。
class LocalizedStringsJa implements LocalizedStrings {
@override
final CounterPageText counterPage = CounterPageTextJa();
@override
final SnackbarText snackbar = SnackbarTextJa();
@override
final ToolTipText toolTip = ToolTipTextJa();
}
これで日本語のテキストを実装したクラスの実体が作成できました。
日本語のクラスと同様に英語のクラスも同様に作成してあげます。
jaフォルダをそのままコピーして「ja」とあるところを「en」にして日本語を英語に翻訳してあげるだけで簡単に新規言語のファイルが作成できます。
追加で言語を増やしたい時に楽です。
class LocalizedStringsEn implements LocalizedStrings {
@override
final CounterPageText counterPage = CounterPageTextEn();
@override
final SnackbarText snackbar = SnackbarTextEn();
@override
final ToolTipText toolTip = ToolTipTextEn();
}
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.";
}
これで言語ファイルは完成です。 あとは端末の言語設定にあわせてLocalizedStringsEnとLocalizedStringsJaを出し分けてあげます。 riverpodのproviderで提供してあげます。
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();
}
});
端末のlocaleを取得し言語コードごとに先ほど作成した言語ファイルを出し分けます。 端末の言語設定が日本語ならLocalizedStringsJaを、それ以外ならEnをprovideしてあげます。
※languageCode一覧はこちら
あとはページでConsumerWidgetのrefからこのproviderを呼び出してあげるだけです。
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の更新もしなければいけないので手間といえば手間ですが、メンテナンス性が高まったように感じます。