言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

[海外対応] AndroidのLocale周りまとめ [多言語対応]

はじめに

複数の国や地域、あるいは言語に対応する場合に避けられないMulti Locale対応ですが、Android Frameworkのバージョンによってできること・しなくてはならないことが変わって来ます。 各APIでの大きな変化を時系列でまとめてご紹介します。 また、実際にMulti Localeに対応して来た中で遭遇したハマり所とその対策方法・tipsなども共有していきます。

ってDroidKaigi2018のCfPに応募したけどRejectされたのでブログにまとめます😇 「急にグローバル対応しなきゃいけなくなったけど何していいかわかんない!」みたいな人にざっと見ていただきたいような内容です。

それでは各APIで何が対応されたのかまとめます。ハマり所とその対策方法とかは別のエントリで書けたらいいな。

Locale周りの変遷

API Level 1 : Localeの基礎

LocaleクラスはAPI Level 1の時代から存在している。というかAndroidで独自に定義したものではなく、java.util.Localeを使っている。そもそもLocaleってなに?という話をすると、ドキュメントには

A Locale object represents a specific geographical, political, or cultural region.

と定義されている。単に国というわけではなく、地理的・政治的・文化的な特定の地域をさすものである。例を挙げると、日本だとja-JPになる。これはlanguageとcountry(region)を合わせたものだ。この頃はlanguage codeについてはISO 639-1、region codeについてはISO 3166-1で定義されているものが使える。後述するが、API Level 24以降ではIETF BCP 47をサポートする。

「地理的・政治的・文化的」という部分について補足すると、languageにおいて同じ中国語でも簡体字繁体字が別のものとして扱われていたり、regionの方も中国(本土)と香港と台湾が別のコードが割り当てられている例がわかりやすいかもしれない。

さて、話をAndroidに戻そう。LocaleオブジェクトはContextからgetResources()getConfiguration()と経由して、Configurationのpublicなメンバであるlocaleにアクセスすることができる。アプリやActivity独自でLocaleの設定を持ちたい場合は、ApplicationやActivityのContextのlocaleを書き換えることで言語切り替えを行うことができる。具体的にはいい感じにLocaleを書き換えたConfigurationオブジェクトを使って Resources#updateConfiguration()を呼んでやるとできる。(端末の言語設定に従う場合は端末で設定したLocaleがここに入ってくるので特に個別のアプリで対応する必要はない。)端末の言語設定で何が選択されているか知りたい時にはLocale#getDefault()が使える。

では各Localeごとのリソースをどう定義するか。ドキュメントはこの辺りを参照してほしいが、ざっくりいうとres/配下のディレクトリにconfig_qualifierを付与することでLocaleごとのリソースを定義することができる。ディレクトリ名のルールはこのようにハイフンつなぎ。

<resources_name>-<config_qualifier>

例を挙げると、 アメリカ・英語用の言語リソースは下記のように定義できる。valuesresources_nameenrUSconfig_qualifierだ。

values-en-rUS/strings.xml

上記の例のように、qualifierはハイフンで複数繋げることができる。Localeクラス同様languageは ISO 639-1、regionは ISO 3166-1で定義されたものが使える。ただし、regionの方は頭にrをつけなければならない。なぜかというとcase sensitiveではないので、ハイフンでqualifierを繋げた場合にregionを明示的に示すために必要なのだ。また、regionを指定する場合は必ずlanguageとセットで指定しなければならない。(region単体では使えない。)

例ではvalues/を示したが、values以外にももちろん使える。例えばdrawable/でも文字埋め込み画像の出し分けをするのに必要だし、xml/menu/でLocaleによって機能の出し分けをするのに使うことができる。

API Level 17 : RTL対応

ここからConfigurationクラスにsetLocale()メソッドが追加された。とは言え、このクラスのメンバであるlocaleはpublicのままだ。では直接localeの値を書き換えるのとsetLocale()メソッドでやっていることは何が違うかコード見てみよう。

     /**
      * Set the locale. This is the preferred way for setting up the locale (instead of using the
      * direct accessor). This will also set the userLocale and layout direction according to
      * the locale.
      *
      * @param loc The locale. Can be null.
      */
     public void setLocale(Locale loc) {
         locale = loc;
         userSetLocale = true;
         setLayoutDirection(locale);
     }

まずはlocaleの更新を行なっている。これは予想通りだろう。次にuserSetLocaleフラグをtrueにしている。このフラグはpublicだがhideアノテーションが付いている。これはActivityManagerServiceがsystem propertyにlocale情報を書き込む時に使われる。 そして最後のsetLayoutDirection()メソッド。これもAPI Level 17から追加されたAPIだ。何をしているかと言うと、どちらからどちらの方向にレイアウトするをLocaleから判断している。

「どちらからどちらにレイアウトするか」と聞いてピンと来ない人がいるかもしれない。我々が普段目にする文字は(横書きの)日本語であれば左から右方向に流れる。よく目にする英語もそうだ。しかしアラビア語ヘブライ語など一部の言語では横書きでも右から左に文字を記述する。右から左、なのでRight to Left, 頭文字をとってRTL言語と言われる。文字だけでなく、ボタンの位置などもRTL方向に配置する必要があるが、言語によってレイアウトを複数分けて用意する必要はない。例えばRelative Layoutの子要素として、TextViewとその右にButtonを置きたいと思ったらandroid:layout_toRightOf ではなくandroid:layout_toEndOfを使おう。これもAPI Level 17から追加されたattributeだ。このattributeを使っているとsetLocale()を使ってLocaleを更新した際に画面の再描画が走って適切なLayout Directionで表示されるようになる。

API Level 24 : Multi Locale

このAPIバージョンから java.util.Locale が変わってIETF BCP 47をサポートするようになった。これによりサポートされる言語が大幅に増えた。 また、Multi LocaleをサポートするようになったのもAPI Level 24からだ。

Multi Localeについては以前ブログにまとめた。

muumuutech.hatenablog.com

詳細は上記を読んでもらうとして、ざっくり概要を書くと「これまではユーザは言語を一つしか設定できなかったが、このバージョンからは複数の言語とその優先順位を設定でき、かつシステム側もいくつかLocaleを準備しておくことによってより良いマッチングを行うことができる」ものである。

いくつかの言語をユーザが設定できるという部分だが、これに伴いConfigurationクラスのAPIにいくつか変更が入った。具体的には単一のLocaleを引数に取る setLocale() 、またpublicメンバのlocale がdeperecatedになった。代わりに使用が推奨されるのがLocaleListクラスを引数に取るsetLocales()だ。

    public void setLocales(@Nullable LocaleList locales) {
         mLocaleList = locales == null ? LocaleList.getEmptyLocaleList() : locales;
         locale = mLocaleList.get(0);
         setLayoutDirection(locale);
     }

3行のシンプルなメソッドだ。Localeのリストを更新して、その優先順位トップのものを現在のLocaleとして設定、最後に現在のLocaleに応じたLayout Directionを設定する。

ちなみにLocaleListクラスがマッチングをやってくれる。ロジックはこの辺。以前blogにもちょっと書いた。

muumuutech.hatenablog.com

API Level 25 : Configurationの設定方法が変わった

Resources#updateConfiguration() がdeprecatedになった。代わりに何を使うのが推奨されているかというと、Context#createConfigurationContext()だ。(このAPIが追加されたのはAPI Level 17。)ただし、これは引数で指定したConfigurationが設定された新しいContextオブジェクトを返してくれるだけだ。ここで生成したContextをActivity#attachBaseContext()に渡してやると新しいConfiguration(と、これが持っているLocale)が設定される。

Twitterで教えてもらいました。感謝!)

API Level 26: Configurationの共有範囲が変わった

API Level 25でdeprecatedになったResources#updateConfiguration()だが、25までは一度コールするとその設定がapplication内で共有されていたが、API Level 26からは各activity/applicationで別の設定になった。

このあたりの記事が参考になった。

proandroiddev.com

最後に

以上でざっとLocale周りの変遷を書いた。最後の方とかLocaleともはや直接関係ないんじゃ…という気がしなくもないが、多言語、海外対応する際に必要になるケースも多いのではないかと思うので消さずに残しておく。

それにしてもAPI Level が上がれば上がるほどアプリ内で独自の言語設定させていかないぞ♡というGoogle先生のお気持ちが強まっているのかなと思わずにはいられない😇 😇 😇