Flutter の i18n パッケージは slang が良かった件
株式会社Awarefy CTO の池内です。
スマホアプリ「AIメンタルパートナー アウェアファイ」は、Internationalization(以下、i18n)として日本語に加え英語の UI をサポートしています。
i18n を実現するためのパッケージとして、これまで easy_localization
を利用していました。
easy_localization
は優れたパッケージなのですが、運用していく中で次のような課題が生まれていました。
- 言語ファイル(翻訳ファイル)のフォーマットが JSON であること
- 正確には、YAML 対応もあるが、Type-Safe に扱うためのコード生成方式の場合 JSON しかサポートがないこと
- 生成されるコードの命名規則に問題があったこと(詳細は割愛。構造化した JSON の場合のキー名がアンダースコア連結で出力されるイメージ)
JSON ではなく YAML で、というのは要求としては強くあり、easy_localization
を fork していくことも検討したのですが、命名規約などはパッケージの思想的な部分もあるかなと思い、別のパッケージに乗り換えることにしました。そこで選定したのが slang
でした。
i18n パッケージに求めるもの
Flutter の i18n 対応は個人的にも Awarefy アプリが初めてだったのですが、運用していくなかで、下記は必須だなと感じるようになりました。
-
MUST Type-Safe であること(具体的には、Dart のコードとして扱えること)
- コード生成のフローが入ることを忌避される方もいると思うのですが、Type-Safe の恩恵のほうが大きいと思います
-
MUST JSON ではないフォーマットで管理できること、例えば YAML
- 知ってた、という話ではあるのですが、コメントが書けない、末尾カンマの有無によるsyntax error といった JSON の仕様は複数人で管理する言語ファイルのフォーマットとしては相性が悪いです。コメントが書ける。カンマを気にしなくてもいい。そう、YAML ならね。
-
WANT 言語ファイルの分割管理ができること
- 言語ファイル、少なからず画面の実装に依存するので、画面ごとなくなったり、機能毎なくなったりなどしたときに、どこまで削除できるかどうかの判断がむつかしく、ややもすると放置してしまうことになります。画面毎やコンテクス毎にファイルを分割できると、全体を削除できる可能性があるので、メンテナンスが容易になります
slang の使い方
slang
は前述の i18n パッケージが備えていてほしい要件をすべて満たし、なおかつシンプルなAPIを提供してくれる使い勝手のよいパッケージです。
README が詳しいため重ねて説明することはほとんどないのですが、例えば次のような日本語向け、英語向けの言語ファイルを用意します。
welcome:
title: ようこそ!
welcome:
title: Welcome!
設定ファイル(後述)を配備し、コード生成を行います。
dart run slang
コマンドの実行に成功するとファイル translations.g.dart
が生成されます。あとはこれを利用するだけです。
import 'package:my_project/i18n/translations.g.dart';
import 'package:flutter/material.dart';
class SampleWidget extends StatelessWidget {
const SampleWidget({super.key});
Widget build(BuildContext context) {
return Container(
child: Text(t.welcome.title), // <-- ようこそ! or Welcome! と表示される
);
}
}
easy_localization
の場合、コード生成を利用しない場合は 'welcome.title'.tr()
、コード生成を利用した場合でも MyTranslations.welcome_title.tr()
のように記述する必要があり、それと比べると slang
の記法はシンプルで間違いが起きにくく、.
で階層構造を意識できるのも有り難いなと思っています。じっさい使い始めるにはもう少しだけ手順が必要ですが、それでもシンプルな部類だとは思います。
slang 設定 Tips
すぐに使い始められる slang
ですが、設定がかなり細かく行える点も気に入っています。全ての項目を詳しく理解できているわけではないですが、ざっとこのような形で設定します。
base_locale: en-US
fallback_strategy: base_locale
input_directory: assets/i18n # 元となる言語ファイルの置き場
input_file_pattern: .i18n.yaml
output_directory: lib/i18n # コード生成のディレクトリ
output_file_name: translations.g.dart
output_format: single_file
locale_handling: true
flutter_integration: true # Dart プロジェクトで使う場合のみ false
namespaces: false
translate_var: t # Widget からアクセスする場合の変数名
enum_name: AppLocale # AppLocale.jaJp のような enum コードが生成される、その enum名
class_name: Translations # Translations.of(context) のようにアクセスする場合のクラス名
translation_class_visibility: private
key_case: camel # 生成されるコードの case。Dart のお作法にのっとり、camel を指定
key_map_case: camel
param_case: pascal
string_interpolation: double_braces
flat_map: false
translation_overrides: false
timestamp: true
statistics: true
pluralization:
auto: cardinal
default_parameter: n
例としてこのような設定ファイルを用意し、各項目をカスタマイズすることができます。
YAML内では snake_case
で記述しつつ、生成されるコードは camelCase
にする、といったことができるのがごく個人的にはポイントが高いところです。
おまけ : 言語ファイル、まとめて置くか、近くに置くか
slang
の検証がひととおりできたところで チームで相談し、乗り換えすることを決めました。ここまで紹介したように、さしたる学習コストなく運用していけるパッケージだと思っています。
しかし、どのようなパッケージを使ったとしても共通の課題となることとして、言語ファイルの記述ルールをどうしていくか、ということがあると思います。
common:
button:
ok: 了解
cancel: キャンセル
error:
not_found: リソースが見つかりません
home:
title: ホーム画面
description: ここはアプリのホーム画面です
setting:
theme_config:
dark: ダークモード
light: ライトモード
共通テキストは共通テキストに、特定の画面固有の場合は画面固有の場所へ、、といったあたりが妥当な整理の方法なのかなと思いつつ、みなさまどのようにされているのか気になるところです(識者求む)。
そんな逡巡のなか、いっそ、特に特定の画面固有のテキストについては、Widget の近くに配備してしまうのはどうかというアイデアも生まれています。この場合のメリットとしては、Widget 群を破棄することになったときに一緒に破棄できること、デメリットとしては言語ファイル自体が散らばってしまうことでしょうか。特に、i18n を引き受けるチームが開発メンバーの外にいる場合は、管理が難しくなるかも知れません。
※ 下記、言語ファイルを Widget の近くに配置するアイデア
.
└── screen
├── home
│ ├── home.dart
│ ├── home.en-US.i18n.yaml
│ └── home.ja-JP.i18n.yaml
└── setting
├── settings.dart
├── settings.en-US.i18n.yaml
└── settings.ja-JP.i18n.yaml
さておき、Type-Safe に扱えることと YAML で管理できるようになったことで、チーム内には平和が訪れていますとさ。めでたしめでたし。
以上 slang
の紹介でした。
Discussion