【Flutter】Googleスプレッドシートを使った自動生成型多言語化パッケージを作った

こんにちは。広瀬マサルです。

みなさん多言語化ってどうしてますか？

僕もいろいろなフレームワークで様々な多言語化パッケージを使ってきましたがどうもイマイチな結果になることが多かったです。

なので、自分で新しいFlutterパッケージを作ってみました。

一言で簡単に言うと

**Googleスプレッドシートから自動でコード生成し直感的に使える多言語化パッケージ**

です。

翻訳を行うにあたって非常に便利なGoogleスプレッドシートから[build_runner](https://pub.dev/packages/build_runner)を使ってコードジェネレーションを行いタイプセーフに翻訳済みのテキストを入力できます。

テキストの入力も非常に直感的に使えるようにしているので、もしかしたらこれまでの多言語化パッケージでは味わえなかった文字入力のスムーズさを出せるかもしれません。（あくまで可能性です）

使い方をまとめたので興味ある方はぜひ使ってみてください！

# katana_localization

https://pub.dev/packages/katana_localization

https://pub.dev/packages/katana_localization_builder

# はじめに

多言語化パッケージは、基本的に下記のような特徴を備えているかと思います。

- 特定のキーとそれに対応した翻訳テキスト定義したファイルを作成
- 実装時に特定のキーを指定して現在のロケールにおける一致したテキストを抽出して表示

このとき、僕は下記の現象に悩まされてました。

- 実装時にどのキーが定義されているかが定義ファイルを毎回確認する必要がある
    - どのキーが定義されているかがわからないので違うキーで重複して同じ翻訳テキストを定義することがあった
- キーをタイポして翻訳が正常に行われないことがある
- 翻訳をお願いするにあたってファイルを共有して構造を説明するのが面倒
- 翻訳を自動翻訳で済ませたい場合があるがそれを行うのも面倒
- BuildContextを使わないとアクセスできない場合がある
- 翻訳テキスト中に変数を入れたい場合に大抵の場合、後から値を注入する形になるので分かりづらい。
    
    ```dart
    print(
      sprintf(localize("%s of the %s, by the %s, for the %s"), ["Government", "people", "people", "people"])
    );
    ```
    

なので上記を解消するため下記の機能を有するパッケージを作成しました。

- Googleスプレッドシートで翻訳データを作成する
    - 翻訳をお願いするときにシートを共有するだけで良い
    - キーと翻訳テキストが同一行にあるので、キーとテキストの対応がわかりやすい。そのため翻訳を依頼するときに説明が不要
    - `GOOGLETRANSLATE`関数を用いることで簡単に自動翻訳が可能
- [build_runner](https://pub.dev/packages/build_runner)を使ってコードジェネレーションを行う
    - IDEのサジェスト機能により用意されているキーを確認することができる
    - タイプセーフで入力できるのでタイポしない
- **変数を利用する場合メソッドチェーンを利用することで文章の順番を崩さずに変数の入力をすることが可能**
    - 下記のように書けます。
    
    ```dart
    print(
      l().$("Goverment").ofThe.$("People").byThe.$("People").forThe.$("People")
    );
    ```
    
- 言語を変更が容易。変更を検知しアプリ自体の描画を再更新できる。
- BuildContextを使わずともどこからでもアクセスが可能。

# インストール

[build_runner](https://pub.dev/packages/build_runner)を用いたコードジェネレーションを行うため下記のパッケージをインポートします。

```dart
flutter pub add katana_localization
flutter pub add --dev build_runner
flutter pub add --dev katana_localization_builder
```

# 事前準備

事前にGoogleスプレッドシートを利用可能にします。

1. [こちらのテンプレート](https://docs.google.com/spreadsheets/d/1bw7IXEr7BGkZ4U6on0OuF7HQkTMgDSm6u5ThpBkDPeo/edit?usp=sharing)からスプレッドシートを自分のGoogleドライブにコピーします。
    - `ファイル` -> `コピーの作成`からコピーが可能です。
2. コピーしたスプレッドシート内で`ファイル` -> `共有` -> `他のユーザーと共有`をクリックします。
3. `（作成したスプレッドシート名）を共有`ウィンドウ内で、`一般的なアクセス`を`リンクを知っている全員`に変更します。

### Googleスプレッドシートの記述方法

Googleスプレッドシートは一番左の列が`キー`となり、各行のキーに対応する翻訳テキストを2行目にあるロケールに従い記載します。

![Untitled](https://raw.githubusercontent.com/mathrunet/flutter_masamune/master/packages/katana_localization/.github/images/gss01.png)

キーのプレフィックスに`#`をつけるとその行は読み込まれません。コメント等にお使いください。

### 変数を用いる場合

キーに`{変数名}`を入力することでそこに変数を埋め込むことができます。

翻訳テキストにも同じ`{変数名}`を記載することで同じ変数名に対応する値が埋め込まれて表示されます。

![Untitled](https://raw.githubusercontent.com/mathrunet/flutter_masamune/master/packages/katana_localization/.github/images/gss02.png)

# 実装

## 定義ファイル作成

`localization.dart`などのDartファイルを作成します。

`part ‘元のファイル名.localize.dart’;`でPartファイルをインポートします。

そこに`GoogleSpreadSheetLocalize`のアノテーションを付与したクラスを作成します。

クラス名は自由ですが必ず`_$（定義したクラス名）`をextendsしてください。

`GoogleSpreadSheetLocalize`のパラメーターには上で用意したGoogleスプレッドシートのURL（例：https://docs.google.com/spreadsheets/d/1bw7IXEr7BGkZ4U6on0OuF7HQkTMgDSm6u5ThpBkDPeo/edit#gid=551986808 ）を**そのまま**コピーして貼り付けてください。

配列として複数のURLを貼り付けることができます。その場合すべてのスプレッドシートのデータが適用されます。（後に読み込まれたデータが優先）

基本的な翻訳をベースのスプレッドシートとして指定し、その上からアプリごとに異なる翻訳データを追加で指定することが可能です。

通常はダウンロードしたスプレッドシートの内容はキャッシュされますが、`version`をインクリメントすることにより新しい内容に更新することができます。

さらにそのクラスを利用するためのトップレベルのフィールドを定義します。**フィールド名は短いほうが後で利用しやすくなります。**

```dart
// localization.dart
import 'package:katana_localization/katana_localization.dart';

part ‘localization.localize.dart’;

@GoogleSpreadSheetLocalize(
  [
    "https://docs.google.com/spreadsheets/d/1bw7IXEr7BGkZ4U6on0OuF7HQkTMgDSm6u5ThpBkDPeo/edit#gid=551986808",
  ],
  version: 1,
)
class AppLocalize extends _$AppLocalize { }

final l = AppLocalize();
```

## MaterialAppへの翻訳データの登録

上記で作成した`AppLocalize`クラスのオブジェクトを利用して`LocalizeScope`と`MaterialApp`を定義します。

LocalizeScopeの`localize`に先程作成した`AppLocalize`のオブジェクトを渡し、`builder`に`MaterialApp`を配置します。

MaterialAppの`locale`、`localizationsDelegates`、`supportedLocales`、`localeResolutionCallback`に`AppLocalize`のメソッドやフィールドを渡してください。

```dart
// main.dart

import 'package:flutter/material.dart';
import 'package:katana_localization/katana_localization.dart';

import 'localization.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return LocalizeScope(
      localize: l,
      builder: (context, localize) {
        return MaterialApp(
          locale: localize.locale,
          localizationsDelegates: localize.delegates(),
          supportedLocales: localize.supportedLocales(),
          localeResolutionCallback: localize.localeResolutionCallback(),
          title: "Test App",
        );
      },
    );
  }
}

```

# コードジェネレーション

下記のコマンドを入力することで自動でコード生成を行います。

```bash
flutter pub run build_runner build --delete-conflicting-outputs
```

連続でコマンドを実行する場合、キャッシュが残って翻訳が反映されないことがあるのでその際は事前に下記のコマンドを入力してください。

```bash
flutter pub run build_runner clean
```

# 利用方法

## 翻訳テキストの取得

`AppLocalize`のオブジェクトをメソッドとして実行することで翻訳オブジェクトを取得することができます。

翻訳オブジェクトには、Googleスプレッドシートで定義されたキーがそのままフィールドやメソッドとして定義されているため下記のようにして翻訳テキストを取得することができます。

```dart
print( l().user ); // English: User、日本語: ユーザー
```

変数を利用する場合は`$(入力値)`のメソッドが用意されているのでそこに値を入力します。

また、その入力値にも翻訳オブジェクトを指定することが可能です。

```dart
print( l().$( l().saving ).hasBeenCompleted );
// English: Saving has been completed.
// 日本語: 保存が完了しました。

print(
  l().$(
    l().$( l().saving ).of.$( l().data )
  ).hasBeenCompleted
);
// English: Saving of Data has been completed.
// 日本語: データの保存が完了しました。
```

## 現在の言語の取得と変更

現在の言語の取得はAppLocalizeの`locale`プロパティで可能です。

また、言語の変更はAppLocalizeの`setCurrentLocale`メソッドで可能です。

Googleスプレッドシートで定義したロケールで利用可能な言語のみに変更されます。

それ以外は変更されません。

`setCurrentLocale`が実行されロケールの変更に成功した場合、`LocalizeScope`が再ビルドされます。また、AppLocalize自体がChangeNotifierであるため`addListener`で変更を監視することでその他の場所でも変更を検知した処理を実装することが可能です。

# おわりに

自分で使う用途で作ったものですが実装の思想的に合ってそうならぜひぜひ使ってみてください！

また、[こちら](https://github.com/mathrunet/flutter_masamune)にソースを公開しているのでissueやPullRequestをお待ちしてます！

また仕事の依頼等ございましたら、私の[Twitter](https://twitter.com/mathru)や[Webサイト](https://mathru.net)で直接ご連絡をお願いいたします！

https://mathru.net/ja/contact

# GitHub Sponsors

スポンサーを随時募集してます。ご支援お待ちしております！

https://github.com/sponsors/mathrunet