GeminiでFlutterアプリの多言語化対応やってみた

# はじめに
こんにちは。OREOです。
本記事ではGemini APIを活用したモバイルアプリの多言語対応の実装について取り扱いたいと思います。

Flutter公式も推奨しているライブラリ”flutter_localizations”とGeminiAPIを組み合わせるような実装の流れになります。

# 導入手順
Geminiについて触れる前にFLutterプロジェクトの多言語化対応, すなわちflutter_localizationsの導入から解説を行なっていきます。

## flutter_localizations
### 1. パッケージのインストール
`pubspec.yaml`内のdepencies配下に下記の3つの記述を追加してください。
```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  flutter_localization: ^0.2.0 # 追加
  intl: ^0.19.0　　　# 追加

# 省略~~~~~~~
flutter:
  generate: true # 追加

```
パッケージのインストールについてはターミナルで下記を叩くだけでも大丈夫です。
```terminal
$ flutter pub add flutter_localizations --sdk=flutter
$ flutter pub add intl
```

### 2. 言語設定ファイルの作成
プロジェクトのルートディレクトリに下記の内容で`l10n.yaml`を追加してください。

```l10n.yaml
arb-dir: lib/l10n # ARBファイルのパスの指定
template-arb-file: app_ja.arb # デフォルトの言語のARBファイルの指定
output-class: L10n # 言語を呼び出す時のクラス名
output-localization-file: app_localizations.dart # 自動生成されるローカライゼーションファイル名
```

ちなみにl10nはlocalizationの略語のようです。

### 3. 言語ファイルの作成
２の工程で指定したlib/l10nディレクトリ内に言語ファイルを作成します。
```app_ja.arb
{
  "@@locale": "ja",
  "home": "ホーム",
  "@home": {
    "description": "home"
  },
  "setting": "設定",
  "@setting": {
    "description": "setting"
  },
  "profile": "プロフィール",
  "@profile": {
    "description": "profile"
  },
  "news": "お知らせ",
  "@news": {
    "description": "news"
  },
  "login": "ログイン",
  "@login": {
    "description": "login"
  },
  "logout": "ログアウト",
  "@logout": {
    "description": "logout"
  }
}

```

### 4. localizationsファイルの自動生成
ターミナルで下記のファイルコマンドを叩きます。
```terminal
$ flutter gen-l10n
```

すると`.dart_tool/flutter_gen/gen_l10n`配下に`app_localizations.dart`と`app_localizations_ja.dart`が生成されます。

```app_localizations.dart
abstract class L10n {
  L10n(String locale)
      : localeName = intl.Intl.canonicalizedLocale(locale.toString());

  final String localeName;

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

  static const LocalizationsDelegate<L10n> delegate = _L10nDelegate();

  /// A list of this localizations delegate along with the default localizations
  /// delegates.
  ///
  /// Returns a list of localizations delegates containing this delegate along with
  /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
  /// and GlobalWidgetsLocalizations.delegate.
  ///
  /// Additional delegates can be added by appending to this list in
  /// MaterialApp. This list does not have to be used at all if a custom list
  /// of delegates is preferred or required.
  static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
      <LocalizationsDelegate<dynamic>>[
    delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ];

// 以下略

```

``` app_localizations_ja.dart
import 'app_localizations.dart';

/// The translations for Japanese (`ja`).
class L10nJa extends L10n {
  L10nJa([String locale = 'ja']) : super(locale);

  @override
  String get home => 'ホーム';

  @override
  String get setting => '設定';

  @override
  String get profile => 'プロフィール';

  @override
  String get news => 'お知らせ';

  @override
  String get login => 'ログイン';

  @override
  String get logout => 'ログアウト';
}

```

### 5. MaterialAppに設定を追加
MaterialAppに以下の２行を追記します。
```main.dart
 @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      localizationsDelegates: L10n.localizationsDelegates, // 追加
      supportedLocales: L10n.supportedLocales,　//　追加
      theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
      darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
      routerConfig: goRouter,
    );
  }
```

L10nを使用するために
`import 'package:flutter_gen/gen_l10n/app_localizations.dart';`を*MaterialApp*を呼び出しているdartファイルへ追記します。

:::note warn
main.dartファイルである必要はないです。MaterialAppを呼び出しているところに追記してください。
:::

### 6. アプリ側から呼び出し
4の工程で自動生成したファイルから文字列を呼び出します。
```dart
  final l10n = L10n.of(context)!;
    final menuList = [
      l10n.profile,
      l10n.news,
      l10n.news,
      l10n.login,
      l10n.logout
    ];
```
ソースコード全体は下記です。
```page1.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class Page1 extends StatelessWidget {
  const Page1({super.key});

  @override
  Widget build(BuildContext context) {
    // l10nファイルの読み込みを行っている部分
    final l10n = L10n.of(context)!;
    final menuList = [
      l10n.profile,
      l10n.news,
      l10n.news,
      l10n.login,
      l10n.logout
    ];
    Widget listTile(String title) {
      return Column(
        children: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  title,
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
                IconButton(
                  onPressed: () {},
                  icon: const Icon(Icons.arrow_forward_ios_outlined),
                )
              ],
            ),
          ),
          const Divider(
            thickness: 1.0,
            color: Color.fromARGB(255, 183, 180, 180),
          ),
        ],
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: const Center(child: Text('Gemini翻訳テスト')),
        backgroundColor: Colors.amberAccent,
        actions: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: IconButton(
              icon: const Icon(Icons.flag),
              onPressed: () {},
            ),
          ),
        ],
      ),
      drawer: Drawer(
        child: ListView(
          children: [
            const Divider(
              thickness: 1.0,
              color: Color.fromARGB(255, 183, 180, 180),
            ),
            ...menuList.map(
              (e) => listTile(e),
            )
          ],
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: 0,
        selectedFontSize: 10,
        unselectedFontSize: 10,
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: const Icon(Icons.home),
            activeIcon: Container(
                key: const Key('bottom_navigation_bar_home'),
                margin: const EdgeInsets.fromLTRB(0, 0, 0, 4),
                child: const Icon(Icons.home)),
            label: l10n.home,
            backgroundColor: Colors.red,
          ),
          BottomNavigationBarItem(
            icon: const Icon(Icons.settings),
            activeIcon: Container(
                key: const Key('bottom_navigation_bar_settings'),
                margin: const EdgeInsets.fromLTRB(0, 0, 0, 4),
                child: const Icon(Icons.settings)),
            label: l10n.setting,
          ),
        ],
        selectedItemColor: Theme.of(context).colorScheme.primary,
        onTap: (int index) {
          // context.replace(locationNames[index]);
        },
      ),
    );
  }
}

```

|初期画面|ハンバーガーメニュー表示時|
|--|--|
|![simulator_screenshot_6712063C-5DB6-43A2-84FC-A773156F2126.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/2890a8a1-6456-1a8f-b053-aa543633ee8c.png)|![simulator_screenshot_BBAF0732-51E7-4AEC-B265-51D4DB03B030.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/f8e21210-5cb4-ceec-c6b0-4455171b51a1.png)|



これでflutter_localizationsの動作確認がきました。

## Geminiの導入
次にGeminiを導入し多言語対応を行なっていきたいと思います。

### 1. Gemini API トークンを生成
[Google AI for Developers](https://ai.google.dev/tutorials/setupに従って)のページからトークンを生成してください。

![スクリーンショット 2024-05-22 23.09.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/68a294f8-f57c-f11f-fddc-094773cf0489.png)

### 2. arb_translateをアクティブ化
ターミナルで下記のコマンドを叩き，　arb_translateを有効化してください。
```terminal
$ dart pub global activate arb_translate
```

### 3. トークンの設定
1の工程で取得したトークンを下記のコマンドを叩き, 環境変数に設定してください。
```terminal
$ export ARB_TRANSLATE_API_KEY={your-api-key}
```

非推奨ではありますが，　l10n.yamlに追記する方法もあります。
```l10n.yaml
arb-dir: lib/l10n # ARBファイルを格納するディレクトリを指定します。
template-arb-file: app_ja.arb # テンプレートのARBファイルを指定します。デフォルトの言語のARBファイルを設定すると良いと思います。 
output-class: L10n # 手順3-4で自動生成されるクラス名。設定しないとAppLocalizationsになります。
output-localization-file: app_localizations.dart # 手順3-4で自動生成される言語ファイル名。ローカライゼーションを使うときはこのファイルをインポートします。
arb-translate-api-key: your-api-key # 追記
```

### 4. 追加したい言語の空のarbファイルを作成
`lib/l10n`に追加したい言語の言語キーを用いて空ファイルを作成してください。
例1）　英語の場合: app_en.arb
例2)　インドネシア語の場合: app_id.arb

### 5. arbファイルをGeminiで自動生成
次にターミナルで下記のコマンドを叩きます。
```terminal
$ arb_translate
```

すると4の工程で作成したからファイルに翻訳したものをGeminiが自動で追記してくれます。
```app_en.arb
{
  "home": "Home",
  "setting": "Settings",
  "profile": "Profile",
  "news": "News",
  "login": "Login",
  "logout": "Logout"
}
```

```app_id.arb
{
  "home": "Beranda",
  "setting": "Setelan",
  "profile": "Profil",
  "news": "Berita",
  "login": "Login",
  "logout": "Logout"
}
```

### ６. localizationsファイルの自動生成
ターミナルで下記のファイルコマンドを叩きます。
```terminal
$ flutter gen-l10n
```
すると英語とインドネシア語のlocalizationsファイルが生成されます。（flutter_localizationsの4の工程と同じ）

```app_localizations_en.dart
import 'app_localizations.dart';

/// The translations for English (`en`).
class L10nEn extends L10n {
  L10nEn([String locale = 'en']) : super(locale);

  @override
  String get home => 'Home';

  @override
  String get setting => 'Settings';

  @override
  String get profile => 'Profile';

  @override
  String get news => 'News';

  @override
  String get login => 'Login';

  @override
  String get logout => 'Logout';
}

```

```app_localizations_id.dart
import 'app_localizations.dart';

/// The translations for Indonesian (`id`).
class L10nId extends L10n {
  L10nId([String locale = 'id']) : super(locale);

  @override
  String get home => 'Beranda';

  @override
  String get setting => 'Setelan';

  @override
  String get profile => 'Profil';

  @override
  String get news => 'Berita';

  @override
  String get login => 'Login';

  @override
  String get logout => 'Logout';
}

```

### 7. アプリ言語を変えて実行
初期画面
|日本語|英語|インドネシア語|
|-|-|-|
|![simulator_screenshot_6712063C-5DB6-43A2-84FC-A773156F2126.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/2890a8a1-6456-1a8f-b053-aa543633ee8c.png)|![simulator_screenshot_6427A179-646F-4E4C-A74F-61D9252F69EA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/72db32d6-5d03-e0c0-3c5f-602a8286394d.png)|![simulator_screenshot_A93C4199-0E2B-4E2E-BB84-AA73A0CE4260.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/b45edd0f-231f-7af7-7e70-c296783b3f4a.png)|

ハンバーガーメニュー表示時
|日本語|英語|インドネシア語|
|-|-|-|
|![simulator_screenshot_BBAF0732-51E7-4AEC-B265-51D4DB03B030.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/f8e21210-5cb4-ceec-c6b0-4455171b51a1.png)|![simulator_screenshot_FE94B237-579C-4361-8954-2A20C042B52A.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/2090fdf9-628f-0463-2f39-5292bd62fd63.png)|![simulator_screenshot_36B747F1-05C8-47DD-9492-61AA6D65837A.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/0fda28b0-2888-6cd9-447d-433ef391b73b.png)|

これにてGeminiを用いた多言語化対応が実装できました！

# さいごに
Gemini✖️Flutter についての発表などがあったGoogle I/O 2024。AIともうまくつきあいながら開発効率上げていきたいですね。Gemini関連でまたなにかあれば書きたいと思います！

# 参考文献
https://leancode.co/blog/flutter-app-localization-with-ai