Flutterの多言語ファイルを管理する

# Flutterの多言語ファイルの管理は難しい

Flutterの多言語は管理がやや複雑です。
具体的な説明や実装方法は詳しい記事がありますので、こちらに譲ります。
[Flutterで多言語化対応する(Dart intl toolsの使い方)](https://qiita.com/jiro-aqua/items/fd9896682ca09018fdd3)

簡単にまとめると、
> 1. Dart intl toolsをpubspec.yamlに追加
> 2. 文字列管理クラス(ここではstrings.dart)を用意
> 3. 翻訳する文字列をdartファイルに記述
> 4. 翻訳ファイルであるarbをコマンドで出力
> 5. 他の言語のarbを作成
> 6. arbファイルからdartファイルをコマンドで生成

となります。
また、文言の追加や修正となった場合、[3]の手順から処理を繰り返す必要がある上に、翻訳ファイルの一覧性もなく、管理しづらいという難点がありました。

# Spread Sheetで翻訳データを管理する

そこで、翻訳データをスプレッドシートで管理し、FlutterプロジェクトへはGoogle App Scriptを使って必要なファイルを自動出力するようにしてみました。
[Flutter多言語翻訳管理シート](https://docs.google.com/spreadsheets/d/1dU5UR3neGr0jw8qyMS0K-AdO3rLTBB7-sptw_e68FQs/edit#gid=0)
[Flutter多言語翻訳管理シートのスクリプト](https://script.google.com/d/1OWHAI_mGUeBQwiDMGqu2NTKE5PuD53xHVLYBPni04n3q7cp6ZIj4kXT5/edit?usp=sharing)
GASのコードはこちらにも置いてあります
https://github.com/shcahill/FlutterStringResourceScript

ちなみに参考にした大元のスクリプトはこちらです。
[Android iOS Drive Export](https://github.com/aurelhubert/android-ios-drive-export)

# Flutterプロジェクト側の準備

## intlパッケージの追加

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  # 多言語対応
  flutter_localizations:
    sdk: flutter
  intl:
  intl_translation: ^0.17.5
  ...
```

## `strings.dart`の追加

l10フォルダを作り、そこに以下の様な`strings.dart`を用意します。

```dart:strings.dart
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'messages_all.dart';
import 'strings_impl.dart';

/// 文字列リソース管理クラス
class Strings with StringsImpl {
  static Future<Strings> load(Locale locale) {
    final String name = '${locale.languageCode}_${locale.countryCode}';
    final String localeName = Intl.canonicalizedLocale(name);

    return initializeMessages(localeName).then((_) {
      Intl.defaultLocale = localeName;
      return Strings();
    });
  }

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

  static final Strings instance = Strings();
}
```

この時点では`messages_all.dart`や`StringsImpl`などが解決しませんが、必要なファイルはGASによる自動生成と、コマンドでの生成になるので、今はスルーしておいてOKです。
ちなみに、以下のようなdartファイルがGASで出力されます。

```dart:strings_impl.dart(GASによる自動生成)
import 'package:intl/intl.dart';

// DO NOT EDIT. THIS FILE IS GENERATED by https://docs.google.com/spreadsheets/d/1dU5UR3neGr0jw8qyMS0K-AdO3rLTBB7-sptw_e68FQs/edit
mixin StringsImpl {
  /// region Common
  String get settings => Intl.message('Setting', name: "settings");
  String get next => Intl.message('next', name: "next");
  String get back => Intl.message('back', name: "back");
  /// endregion

  /// region XXX画面
  String fromTo(String from, String to) => Intl.message('Go from {from} to {to}', name: "fromTo", args: [from, to]);
  String count(String count) => Intl.message('{count}', name: "count", args: [count]);
  /// endregion
}
```

これは、多言語対応のロジックと翻訳部分にファイルを分けることで、GASで出力するファイルの責務を翻訳部分だけに集中させるためです。こうすることで、GASが出力すべきファイルは翻訳内容である`strings_impl.dart`のみになり、変更が入る可能性のあるロジック部分をGASで気にする必要がなくなります。

# Spread Sheetの準備

## 新規シート作成

[Flutter多言語翻訳管理シート](https://docs.google.com/spreadsheets/d/1dU5UR3neGr0jw8qyMS0K-AdO3rLTBB7-sptw_e68FQs/edit#gid=0)をベースに個人のSpread Sheetを用意します。
その後、`ツール > スクリプトエディタ`を開き、下記に用意したスクリプトとhtmlをコピーします。
[Flutter多言語翻訳管理シートのスクリプト](https://script.google.com/d/1OWHAI_mGUeBQwiDMGqu2NTKE5PuD53xHVLYBPni04n3q7cp6ZIj4kXT5/edit?usp=sharing)

## Spread SheetのメニューにExportメニューを追加する
menu.gsのonOpen()を実行すると以下の画面になります。
<img width="941" alt="スクリーンショット 2019-07-17 13.40.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202533/a140450a-e6d5-1c8a-74bc-de313c9edf05.png">
`許可を確認`を選択
<img width="1030" alt="スクリーンショット 2019-07-17 13.40.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202533/374de80c-e8b9-d3a4-6732-fe7ab02fb625.png">
アカウントを選択すると、怪しげな画面が表示されますが、ダイアログ下部の`詳細`をクリックして内容を表示します。
<img width="1032" alt="スクリーンショット 2019-07-17 13.40.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202533/1884197d-bf89-b76e-9dd4-bca245cec499.png">
`menu(安全ではないページ)に移動`をクリックします
(表示されない場合は英語表示だと該当の項目が表示されるかもしれません)
<img width="1035" alt="スクリーンショット 2019-07-17 13.41.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202533/fbbb12b0-f1c6-9c40-2a20-8602a7237c37.png">
`許可`をクリック
<img width="882" alt="スクリーンショット 2019-07-17 13.41.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202533/5dd67ae6-6c5b-2062-1036-1ddd4ddcccae.png">
Spread Sheetのメニューに`エクスポート`メニューが追加されています。

## 多言語翻訳ファイルの出力

追加された`エクスポート`メニューを実行してしばらく待つと、以下の様になります。
（スクショは`All`を選択した場合です。実行時間はちょっと長いです）
<img width="968" alt="スクリーンショット 2019-08-04 22.48.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202533/9cfc6b5b-2bd9-e9cd-73ef-de1190798128.png">

Flutter/Android/iOSそれぞれzipで落とせる様になっています。こちらをDLして展開したら、そのままプロジェクトにコピペできるような構成になっています。

<img width="316" alt="スクリーンショット 2019-08-04 22.52.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202533/22d1016b-1b80-4643-fbb4-c7e95f9034e6.png">

## コマンドからdartファイルの生成
GASでは`strings_impl.dart`,`xxx.arb`が生成されるため、最後にコマンドでdartファイルを生成する必要があります。（Android/iOSはリソースファイルを差し替えるだけ。）

```
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/strings_impl.dart lib/l10n/intl_messages.arb lib/l10n/intl_ja.arb lib/l10n/intl_ko.arb lib/l10n/intl_zh-cn.arb lib/l10n/intl_zh-tw.arb
```

以上でビルドエラーも解消し、動く様になると思います。

# 使い方の補足

## 言語の追加方法

サンプルでは日英中(簡体/繁体)韓の5カ国語ですが、追加や変更はいくらでもできます。
必要な分だけ横に言語を追加してください。

## 翻訳データの追加

`FlutterKey`(A列)、`AndroidKey`(B列)、`iOSKey`(C列)にkeyが記載されている場合のみ、そのOS用のリソースを出力します。そのため、必要な翻訳データの部分にはkeyを入力するようにしてください。keyがそのままコードからアクセスするものになります。(例えばAndroidだったらstrings.xmlのリソース名になります)

## regionタグの出力

Android Studioのregionタグに対応しています。
`<XXX画面>`の様に行を追加しておくと、以下の様に出力されます。（`<`でタグを判別しています）

```dart
  /// region XXX画面
  ...
  /// endregion
```

## 複雑な文字列の出力

たとえば、`東京から大阪まで行く`という文字列があって、`東京`と`大阪`の部分が可変だった場合の記述は以下のようになります。
`{from}から{to}まで行く`
すると、dart側のコードは以下のように出力されます。

```dart
String fromTo(String from, String to) => Intl.message('Go from {from} to {to}', name: "fromTo", args: [from, to]);
```

あとは使う側は以下のようになります。

```dart
Strings.of(context).fromTo("東京", "大阪");
```

## TODO

一通り自分が必要な機能は作りましたが、スクリプトは初心者のため、以下の機能は断念しました…
また、パフォーマンスの問題であったり、コードの美しさには自信がありませんので、どなたか教えていただけるととても助かります。

- 変数の受け取りは、Stringしか受け付けられない(Flutter)
- Android/iOS部分は手抜き
- 単数系・複数系の対応ができていない
- 女性名詞・男性名詞などに対応していない
- 実行速度が遅い
- スクリプト実行後、zipのDLが自動で出来ない
    - 試してみたものの、Blob周りで詰まってしまいました…
- 同じkey(A列またはB/C列のこと)が登録されていてもエラー検出できない