はじめに
Flutter での多言語対応は、公式ドキュメントで紹介されている arb ファイルを使った方法が一般的だと思います。
しかし、アプリケーションの規模が大きくなるにつれて arb ファイルが肥大化していき、管理が難しくなってしまいます。
そこで、管理しやすくするために arb ファイルを分割したくなりますが、残念ながら現状はサポートされていません。
2022 年 7 月 7 日に機能リクエストの Issue が作成され、2023 年 10 月 26 日にプルリクエストが提出されているため、近いうちにサポートされるかもしれません。
slang のようなパッケージを利用することで課題を解決することもできますが、すでに arb ファイルを使っている場合は学習コストも考えると少し敷居が高く感じてしまうかもしれません。
では、arb ファイルを分割して管理する方法はないのでしょうか。
いえ、実はあります。
マルチパッケージ構成にして、それぞれのパッケージで arb ファイルを使用することで、分割して管理することができます。
マルチパッケージ構成で arb ファイルの分割
最終的なサンプルコードを置いておきます。
マルチパッケージ構成
次のようなマルチパッケージ構成で進めていきます。
.
├── app # アプリケーション本体
└── packages
    ├── bar
    └── foo
Melos を使用すると、簡単にモノレポ形式のプロジェクトを管理することができるためオススメです。
arb ファイルの分割
公式ドキュメントに従って、多言語対応の下準備を行っていきます。
まずは、foo・bar パッケージの pubspec.yaml に依存関係を追加して、generate を有効にします。
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.18.1
  
flutter:
  generate: true
次に、それぞれのルートディレクトリに中身が空の l10n.yaml を作成しておきます。
lib/l10n ディレクトリを作成し、arb ファイルを配置します。
{
  "foo": "foo"
}
{
  "foo": "ふー"
}
{
  "bar": "bar"
}
{
  "bar": "ばー"
}
それぞれのパッケージに Widget を作成します。
library foo;
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class FooWidget extends StatelessWidget {
  const FooWidget({super.key});
  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context)!;
    return Text(l.foo);
  }
}
library bar;
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class BarWidget extends StatelessWidget {
  const BarWidget({super.key});
  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context)!;
    return Text(l.bar);
  }
}
最後に、アプリケーション本体で Widget を使用します。
import 'package:bar/bar.dart';
import 'package:flutter/material.dart';
import 'package:foo/foo.dart';
void main() {
  runApp(const MainApp());
}
class MainApp extends StatelessWidget {
  const MainApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              FooWidget(),
              BarWidget(),
            ],
          ),
        ),
      ),
    );
  }
}
これで、対応完了!!、、かと思いきや、次のようなエラーが発生します。
Launching lib/main.dart on iPhone 14 Pro in debug mode...
Running Xcode build...
Xcode build done.                                            9.5s
Failed to build iOS app
Error (Xcode): Invalid depfile: /xxx/arb-demo/app/.dart_tool/flutter_build/1d71dacafbf048692cb1384285002b76/kernel_snapshot.d
Could not build the application for the simulator.
Error launching application on iPhone 14 Pro.
このエラーは、foo・bar パッケージそれぞれで自動生成された AppLocalizations クラスを利用できないというものです。
どうやら .dart_tool へ自動生成されたクラスを使用している場合は、パッケージ間での参照ができないようです。
従って、.dart_tool ではなくパッケージ内に自動生成されたクラスを配置する必要があります。
foo・bar パッケージの l10n.yaml を次のように修正して
+ output-dir: lib/gen/l10n
+ synthetic-package: false
foo・bar パッケージの pubspec.yaml の generate を無効にします。
- flutter:
-   generate: true
foo・bar パッケージの Widget の import を次のように修正します。
- import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+ import 'package:xxx/gen/l10n/app_localizations.dart';
これで、対応完了!!、、、、、、、、かと思いきや、次のようなエラーが発生します。
======== Exception caught by widgets library =======================================================
The following _TypeError was thrown building FooWidget(dirty, dependencies: [_LocalizationsScope-[GlobalKey#ea537]]):
Null check operator used on a null value
...
...
...
======== Exception caught by widgets library =======================================================
The following _TypeError was thrown building BarWidget(dirty, dependencies: [_LocalizationsScope-[GlobalKey#ea537]]):
Null check operator used on a null value
...
...
...
これは、foo・bar パッケージそれぞれの AppLocalizations.of(context) が null を返しているためです。
自動生成した AppLocalizations を使用するためには MaterialApp の localizationsDelegates と supportedLocales に設定を追加する必要があります。
そのために、foo・bar パッケージそれぞれの AppLocalizations をアプリケーション本体で使用できるように l10n.dart というファイルを作成して export します。
export 'package:foo/gen/l10n/app_localizations.dart' show AppLocalizations;
アプリケーション本体を次のように修正します。
  import 'package:bar/bar.dart';
+ import 'package:bar/l10n.dart' as bar_l10n;
  import 'package:flutter/material.dart';
  import 'package:foo/foo.dart';
+ import 'package:foo/l10n.dart' as foo_l10n;
  
  void main() {
    runApp(const MainApp());
@@ -12,15 +14,32 @@ class MainApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return const MaterialApp(
-       home: Scaffold(
-         body: Center(
-           child: Row(
-             mainAxisAlignment: MainAxisAlignment.center,
-             children: [
-               FooWidget(),
-               BarWidget(),
-             ],
-           ),
+       localizationsDelegates: [
+         ...bar_l10n.AppLocalizations.localizationsDelegates,
+         ...foo_l10n.AppLocalizations.localizationsDelegates,
+       ],
+       supportedLocales: [
+         ...bar_l10n.AppLocalizations.supportedLocales,
+         ...foo_l10n.AppLocalizations.supportedLocales,
+       ],
+       home: HomeWidget(),
+     );
+   }
+ }
+ 
+ class HomeWidget extends StatelessWidget {
+   const HomeWidget({super.key});
+ 
+   @override
+   Widget build(BuildContext context) {
+     return const Scaffold(
+       body: Center(
+         child: Row(
+           mainAxisAlignment: MainAxisAlignment.center,
+           children: [
+             FooWidget(),
+             BarWidget(),
+           ],
          ),
        ),
      );
すると、無事に次のように表示されました 
| 英語 | 日本語 | 
|---|---|
|  |  | 
おわりに
いかがでしたでしょうか。
マルチパッケージ構成で arb ファイルを分割する方法を紹介しました。 この方法を使うことで arb ファイルの肥大化を防ぐだけでなく、より適切な範囲で文言の管理を行うことができます。
少し設定の手間は必要ですが、個人的におすすめの方法ですので、ぜひご活用していただければ幸いです。
