Dart/Flutter での多言語対応あれこれ

公式ドキュメントだけでは分かりにくいところを詳しく解説

mono 
mono 
May 1, 2019 · 31 min read

この記事でDateTimeに触れましたが、その流れでintlパッケージを紹介し、さらに後半ではFlutterの多言語対応のいろはを解説していきます。

パッケージ名の intl は、Internationalization(i18n, 国際化)の短縮名です。日本語のみ対応のアプリでは不要かというとそうではなく、諸々のデータを日本語として正しく表記するためにも必要です。

例えば、ローカルタイムで2019年5月1日を表すDateTimeを文字列にすると次のようになります。

ISO8601文字列ではなく「2019年5月1日」と表示したい場合はどうすればよいでしょうか?適当なサンプルアプリなどでは以下でも良いでしょう。

ただ、サービス・アプリをリリースする場合、以下などの理由により、基本的には上のような書き方よりも国際化対応のライブラリ・フレームワークを使っておくのがベターです(これを踏まえて本当に不要であると判断できるならそれでも良いです)。

  • 自前でDateTimeから所望の文字列への変換処理を書くよりも、すでに用意されている仕組みを使った方が簡単かつ的確
  • ベタ書きだと多言語対応が困難になる(はじめは一言語で良いと思っていてもあとから多言語対応したくなったらその時に大きな余計なコストがかかる)

というわけで、Dartの国際化対応用のパッケージである intl を紹介していきます。 このパッケージは主に次のクラスから構成されています。

  • Intl: localeの設定など全体に関わること
  • DateFormat: 日時のフォーマット
  • NumberFormat: 数値のフォーマット
  • BidiFormatter: 文字列の流れる方向のright-to-left (RTL) と left-to-right (LTR)の対応用

ちなみに、Flutterの場合はflutter_localizationsに依存すると、それ経由でintlが使えるようになります。

以下、完全に網羅はせず一般的なアプリ開発に必要そうなところをかいつまんでいきますので、より詳細はドキュメントも併読ください。

デフォルトlocaleの取得・設定

Intl.defaultLocale でデフォルトlocaleの取得・設定ができます。

素のDartのデフォルトでは en_US (米国向けの英語)となっています。

日本語に対応したい場合は、以下のように設定します。

ただ、このまま次のような、指定したロケールの文言リソースが必要なメソッドを使うと、

次のような例外が発生してしまいます。

initializeDateFormatting をあらかじめ呼んでおく必要があり、次のようにすると成功します 👍

Flutterの場合、上記処理を自前で書くのでは無くフレームワークに従った書き方とするのが良いです。詳しくは記事の後半で説明します。

また、Intlクラスは、Intl.message という多言語対応に大事なメソッドがありますが、少しややこしいので後回しにします。

というわけで、まずはDateFormatとNumberFormatの2種類のFormatクラスをなぞっていきます。

DateFormat

DateTimeをUI上に適切に表示する際などに、DateFormatを使います。

Web APIとのやり取りでのフォーマットやパースで使うこともありますが、DateTimeだけでISO8601文字列とUNIX時間を扱えるので、それで済む場合はDateFormatは不要です。

formatメソッドを使います。

DateFormatの生成は3通り書き方があって、例えばシンプルに年だけ表示したい場合は次のようになります。

下ほど推奨されている書き方で、基本的には最後の名前付きコンストラクターを使うのが良いです。

もう少し複雑な指定をする例は次のようになります。日にちと時刻を一緒に表示したい場合は、addPatterかそれをラップしたメソッド( add_XXX )で繋げます。

定数とパターン文字列の対応は以下のようになっています。

Image for post
Image for post

また、コンストラクタには、デフォルト以外のlocaleを指定することもできます。

それぞれの指定で実際にどういう文字列になるかについては、日本語リソースは次のあたりに載っています(書いて試してみる方が手っ取り早いかもしれませんが)。

また、上記のパターンに適したものが無い場合、次のSymbolを用いて細かい指定も可能です。ただ、基本的には上記パターンから近いものを選んだ方がどの言語でもきちんと一般的な表記になってくれてメンテナンスも楽なので、個別指定はどうしてもという時のみにした方が良いと思います。

Image for post
Image for post
Image for post
Image for post

DateTimeから文字列への変換と似た要領で、formatメソッドの代わりにparseメソッドを使います。

省略可能な第2引数にtrueをセットするか parseUtc メソッドでUTC扱いになります。

Dart の DateTime の扱いあれこれ で触れましたが、DartではDateTimeにローカル or UTCのタイムゾーン情報が含まれていて、DateFormatは単純にそれに従います。

次の例を見ると分かりやすいはずです。

つまり、渡すDateTime型のタイムゾーン情報がローカルとUTCのいずれになっているかを意識する必要があるということです。

DateTimeクラスの作りとして、明示的にUTCとしない限りは基本的にローカルタイムゾーンとなり、アプリ上の表示としてもそのままで適切なことが多いはずです。一方、サーバーAPIなどに日時を文字列で渡す場合は大抵UTCが適切なはずでそのあたりは充分注意が必要です。

ちなみに、FirestoreのTimestamp型とDateTimeの変換には、Timestamp.fromDate()およびTimestamp.toDate()メソッドを使いますが、それはUNIX時間で扱っているので注意は不要です。また、Timestamp.toDate()で受け取れるDateTimeはローカルタイムゾーンになっていてそのままUI表示用に扱う際に都合が良いです。

ちなみに、ローカルとUTC以外のタイムゾーンはDartの標準パッケージだけでは扱えず、対応が必要な場合は自前実装するか timezone などの外部パッケージに頼るしかありません。一般的なアプリでは不要なことが多い気がしますが、例えば世界時計作る場合などには必要になってきますね。

例えばTwitterでは1週間以内のツイートについて、15分前にされたものの場合は「15分」などと想定時刻表記になります。

DateFormatにこれ用のメソッドは用意されているものの、”NOT YET IMPLEMENTED.”のコメントがあり、結果は常に空文字となっていて、残念ながら現時点では使えません。

多言語対応など含めると対応の手間がかなり大きいからだと思います。対応Issueは古めでまったく動きが無いのであまり期待しない方が良さそうです。

というわけで、代わりの非公式パッケージとしては、 timeago が良さそうでした。

ちょっと日本語リソースが怪しいですが、forkして適宜改変(+プルリク)すれば良いと思います。

翻訳例は以下のでもサイトで確認できるようになっています。

モバイルアプリ開発で、よくパース処理などで大量のレスポンスを処理した時に日時のフォーマッタークラスを必要な度に生成してそれが原因で無駄に長い処理時間がかかってしまう例を目にしているので、Dartの場合どうなのかを調べてみました。

結果は以下のようになって、Dartでもボトルネックになり得ることが分かりました。

1万件などの大量の処理をしないとしても、使い回すことによって増える手間もデメリットも特に無く、それで少しでもパフォーマンスを良くできるなら、使い回すに越したことないと思っています。
(スレッドセーフでない場合少し扱いに注意が必要ですが、DartのDateFormatは時刻情報をフィールドに持っていたりしてないように見えるので多分大丈夫のはずです。さらにDartはシングルスレッドイベントループでIsolate間のメモリ共有もなされないため問題になりそうなケースが思い付きません🤔)

NumberFormat

次にNumberFormatクラスを見ていきます。

数値をベタ出力すると、次のようになってしまいますが、

デフォルトのコンストラクタ(NumberFormat()) では、”ja_JP”の場合は次のように整数部分をコンマ3桁区切り・小数部分を最大3桁表示に整えてくれます。

ちなみに、ドイツなど主に欧州の国ではコンマとピリオドの使い方が逆だったりすることがあります。

このように、とりあえずNumberFormat()を使っておけば、ロケールに応じて数値を「良い感じ」の文字列に整えてくれます。

NumberFormat.decimalPattern() でも同じ結果になるように見えましたが、差が出るパターンに気づいたら追記します。

任意のパターンにすることももちろんできて、例えば小数部分を1桁にしたい場合は次のようになります。

細かい指定方法は以下の通りです。

  • 0 A single digit
  • # A single digit, omitted if the value is zero
  • . Decimal separator
  • - Minus sign
  • , Grouping separator
  • E Separates mantissa and expontent
  • + - Before an exponent, to say it should be prefixed with a plus sign.
  • % - In prefix or suffix, multiply by 100 and show as percentage
  • ‰ (\u2030) In prefix or suffix, multiply by 1000 and show as per mille
  • ¤ (\u00A4) Currency sign, replaced by currency name
  • ' Used to quote special characters
  • ; Used to separate the positive and negative patterns (if both present)

日本語の場合は以下のような設定が組み込まれていて、

Image for post
Image for post
https://github.com/dart-lang/intl/blob/4b385517037485e3243d6915453215af315add27/lib/number_symbols_data.dart#L1076

次のように名前付きコンストラクタ経由でも使えます。

それぞれ、さらにオプション引数を指定して少しカスタマイズもできたりしますが、それでも足りない時のみ任意のパターンを指定するのが良いです。

DateFormatと同様にformatの逆の操作をしたい場合はparseメソッドを使います。

それでは、次にFlutterの多言語対応のいろはを説明していきます。

Flutterでの多言語対応

Flutterでの多言語対応は公式ドキュメントにしっかり載っています。

ただ、少し分かりにくかったので自分なりにまとめてみます。

サンプルリポジトリは以下です(ちょっと荒削りなサンプルですが)。

ローカライズ対応は特に、 https://github.com/mono0926/intl_sample/commit/c5b4d9ac2e99c984e9ad8e416d2e82dbb89d5ba2 のコミットに集中しています。

多言語対応は以下のように行いますが、前者については説明済みなので、後者について焦点を当てます。

  • DateFormat/NumberFormatなどを用いて日時・数値などを適切にフォーマット
  • それらのフォーマット結果も併用しつつUIの文言を用意する

まずは、以下のパッケージをインストールします。

  • flutter_localizations: Flutterのローカライズ全般を担う(intlにも依存している)
  • intl_translation: 多言語対応用のファイル生成(とりあえず1言語のみの対応であれば必須ではない)

以下のようなお決まりのクラスを用意します。

クラス名は適当で良いですが、短めにすることをお勧めします。なぜなら、文言を次のように取得する時に書きやすくなるからです。

ファイルの置き場所も適当で良いですが、以下 lib/l10n 以下にローカライズ関連のファイルをすべて置くことを前提に説明していきます。

次に上で用意した L10n クラスをFlutterのローカライズフレームワークに渡す役割を担うクラスを、LocalizationsDelegate<T>を継承して作成します。

これもほぼ定型コードですが、 isSupported で返すロケールは必要なものだけ指定します。

この時点では、 L10n クラスの以下でコンパイルエラーがあるはずです。

とりあえず1言語のみの対応ではこれをコメントアウトしても問題ないです。

ただ、Intl.messageを正しく使えているかの検証にもなるので、複数言語対応の以下のフローもなぞっておいた方が無難です。

というわけで、複数言語対応の場合はこの手順が必要です。

dev_dependenciesに指定したintl_translationを使って、arbファイルの生成とそれを元に多言語対応に必要なクラスを生成します。

「# このタイミングで、必要に応じて、メインの言語以外のarbファイルを用意」はどうやって差分を継続的に翻訳していくかが悩ましいところでしたが、BabelEditというツールがとても良さそうです。

また、「# arbファイル群から多言語対応に必要なクラスを生成」はそうやって翻訳した後に実行するので、実際には別スクリプト・タイミングでの実行になりますが、記事では説明の便宜上まとめてしまっています。

話は戻って、上のスクリプトを実行すると、コンパイルエラーも無くなるはずです。

以下のハイライトされているものが生成されたファイルです。

Image for post
Image for post

対象ファイルに含まれる Intl.message が intl_messages.arb ファイルに抽出されます。

上のスクリプトでは一気に実行してしまっていますが、最後のこのコマンドの前に、必要に応じて intl_en.arb などを雛形を元に作っておきます。

今回の例の場合、例えば次のような intl_en.arb などを用意することになって、 messages_en.dart が生成され、プログラムではそれが利用されます。

ローカライズフレームワークに従いつつもとりあえず1言語のみの対応とする場合は、上のスクリプトを実行するだけでも良いですし、多言語対応クラスが無かったら L10n クラスであてた文言がそのまま使われるためarbファイル周りの作業は無くとも動きます。

ちなみに、arbファイルの仕様はこちらに載っています。

以下の指定が必須です。

GlobalMaterialLocalizations.delegateとGlobalWidgetsLocalizations.delegateはFlutterフレームワークに含まれるWidgetの多言語対応およびRTL/LTRの判定のために必要で、多言語対応する場合は自分で用意したクラスと一緒にこれも含めるのが一般的です。

記事の冒頭で紹介したDateFormatの言語リソースロードの initializeDateFormatting 相当の処理もこの指定をすると内部で呼ばれます。

端末での言語優先順にアプリで対応している言語を探索して、マッチしたものが採用されます。

例えば、アプリ側で ['ja', 'en'] を対応している時に次のように、iOS端末設定の優先順がドイツ語・日本語・英語の順となっていた場合は、日本語( ja )が選ばれます。

Image for post
Image for post

また、iOSの場合、Xcodeでの言語指定でも追加しておかないとFlutter側に言語候補が伝わらないので注意です。

Image for post
Image for post

なお、採用されるロケールはデフォルトでは上記のようになっているものの、MaterialAppの次のプロパティを指定することでその確定の方式を自前で書くことができます。

端末の言語設定だけに頼らずにアプリ内設定でもロケールを選択できるようにしたい場合などはこれらも使うことになるはずです。

Intl.message の細かい使い方

以上で一通りなぞり終えたので、Intl.messageについて細々とした補足をしていきます。

「フォロワー: 1.4K」のような表記をしたい場合は外部から引数を渡す必要があって、次のように書きます。メソッドの引数を args 引数にも渡す必要があります。

利用例は次のようになります。

2つのメソッドに分けている理由は、普通に次のように1メソッドで書くと、

arbファイル化する時に次のようなエラーが出てしまうからです。Intl.messageを扱うメソッド内での自由な加工処理はできないのです。

ちなみに、 name は文言リソースのキーになるため、誤ったものを指定するとarbコマンド(intl_translation:extract_to_arb)で次のようなエラーメッセージが出て該当文言がスキップされてしまうので注意です。getterおよびメソッド名と同一にしましょう。

このように、Intl.messageの扱いには注意するべき点がいくつかあり、とりあえず1言語対応の場合も誤用をしていないかのチェックとして、たまにarb変換してエラーが出ないかチェックすると良いと思います。
(いざ複数言語対応する際に出たエラーに一括対処でも良いと思いますが。)

以下の特別な関数が用意されています。

plural: 複数形対応

次のように書くと、

次のように数に応じて文言を変えられます。複数形の概念がある英語などではほぼ必須の対応ですね。

Intl.messageの引数にも入れられるようで、下の例はうまく動いたものの他の文字列を含める( 'I have ${Intl.plural(...` のようにする)と出力結果がおかしくなってしまって、きちんと機能してないように見えました🤔

gender (性別対応)

次のように書くと、

次のように性別に応じて文言を変えられます。

select (条件による場合分け)

次のように書くと、

次のように引数に応じてメッセージを場合分けできます。

引数はObject型で良いですが、enumにすると便利です。

Dart/Flutterの多言語対応の解説は以上となります。

特に、Flutterの多言語対応は第一印象では面倒な印象でしたが、今では標準のiOSアプリよりはやりやすく、R.swiftSwiftGen 使った場合よりは面倒、という程度の印象です。

Flutterでも便利なパッケージやプラグインがあって、それら使うとより対応しやすくなるかもしれません。以下の一連のツイートでそれらについても触れたので、良かったらご覧ください。

標準の仕組みを知った上で、その標準の仕組みに従った作りのパッケージを使うと良いと思っていて、その考えに合う良いものを見つけたら使いたいと思いつつ、今のところは外部パッケージ無しで済ませています。

Flutter 🇯🇵

Flutterに関する日本語記事を書いていきます🇯🇵

mono 

Written by

mono 

Software Engineer(Flutter/Dart, Firebase/GCP, iOS/Swift, etc.) / Freelance / https://mono0926.com/page/job/

Flutter 🇯🇵

Flutterに関する日本語記事を書いていきます🇯🇵

mono 

Written by

mono 

Software Engineer(Flutter/Dart, Firebase/GCP, iOS/Swift, etc.) / Freelance / https://mono0926.com/page/job/

Flutter 🇯🇵

Flutterに関する日本語記事を書いていきます🇯🇵

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store