AndroidJava/日本語と英語などで切り替える

日本語と他の言語をアプリ内で切り替える(2019-04-07)


今回のこのメモは、
国内に日本語のみのアプリをリリースする分には無用な話題です。


言語の自動切り替え

切替と言っても、翻訳してくれる訳でもなく、
あらかじめ用意した日本語、英語のテキストのどちらを取り出すか、というお話です。

src/res/values/strings.xml あたりに、

<string name="your_string_name">Hello World</string>

のようにあらかじめ書いておけば、

String text = getApplicationContext().getString(R.string.your_string_name); 

のようにtextへ"Hello World" を呼び出せますが、
これを各言語(日本語・英語)でそれぞれ用意しておけば、
Android端末の言語設定に合わせて、読み込むxmlファイルを変えてくれます。
つまり、

values/strings.xml    に、 your_string_name="Hello World"
values-ja/strings.xml に、 your_string_name="はろ〜、わぁるど"

を用意しておけば、getString時に自動で選んで読み込んでくれる、と。


ということで strings.xml の日本語版を用意する方法です。
AndroidStudio でリソースを新たに作ります。

  1. res/values ディレクトリを右クリックしてから
  2. [New]-[Values resource file]
  3. ファイル名を入れてOKを押す前に、
    「Available qualifiers」で「Locale」に、設定したい国(ja)を選びます。

自力で直接ファイルを作る場合は、方法の1つとして

  1. AndroidStudioの「res/values/strings.xml」 上で右クリック
  2. 「File Path」を選んで、values を選ぶ
  3. これで開発環境のエクスプローラ上でファイルが表示されると思うので、
    valuesディレクトリをコピーして、「values-ja」として保存する

ちなみに最初は後者の方法しか知りませんでした。
新規なら前者、既にあるファイルから複製するなら後者の方が早そうですね。

そして、
上記のように、values-ja を用意した後は、
日本語の場合が values-ja/strings.xml、
それ以外が values/strings.xml を読み込むことになります。
これを逆に、
英語の場合が values-en/strings.xml、
日本語の場合 values/strings.xml を読み込む、なんてやると、
フランスでもロシアでも、みんな日本語の「values/strings.xml」を読みに行ってしまうので注意しましょう。

(とはいえ、英語を標準語にしている国って、意外と少ないんですよね...
 もっと言えば、USA英語を使うのはUSAだけであって、他はイギリス英語だったり*1

日本語の判定

アプリ起動時の初期値は Locale.getDefault() で拾えそうです。

結局分からなかったのですが、
Locale.JAPAN なのか Locale.JAPANESE なのか、
いまいち分からず、Android端末に依存する雰囲気があります。
利用想定する端末でどちらを使っているのか調べた方が良さそうです。

...まぁ上記2つしか無いなら、両方で判定すればいいか、というのが現状の結論です。
具体例は後述します(isLocaleJpをご覧ください)

日本語、多言語への切り替え(ContextWrapperを使う)

上記の日本語の判定にも絡むのですが、

  • Localeをマルチリンガルユーザに対応した(API24以降)ことに伴い、Locale周りのAPIが若干変わっている
  • API25以降で、Configを変更する手段が変わった(無くなった?)らしい

ことから、

  • アプリ内の言語を変更する方法がバージョンによって変わる
  • 言語の確認方法も変わる

そのため、以前は Locale.setDefault やら Config.setLocale やらで済ました内容が
いまは Context.createConfigurationContext で変える。
(Contextが持っている Locale を見たり変えたりする?)
イメージになるようです。
とか書いている今も、Androidのバージョンが上がって仕様が変わっているかもしれません(おびえています)。

それらをふまえて?以下は実装例
先達のみなさんが既に解決されていたので、私もそれに習いました。
Log.dはてきとうに変えて下さい。

/*
 * Context を指定の言語で上書きするためのラッパー
 * Context context = MyContextWrapper.get(context, Locale.JAPANESE);
 * のように使う。そして、処理を分けたい時は
 * if(isLocaleJp(context)) 
 * のように日本語とそれ以外に分岐させる。
 */
 class MyContextWrapper extends ContextWrapper {
 
   MyContextWrapper(Context context){
       super(context);
   }
 
   static ContextWrapper get(Context context, Locale locale_update){
       Lg.d("Locale:" + locale_update);
       if(locale_update == null){
           locale_update = Locale.getDefault();
       }
 
       Configuration configuration = context.getResources().getConfiguration();
 
       Lg.d("Build.VERSION.SDK_INT:" + Build.VERSION.SDK_INT);
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
           Lg.d("Build.VERSION_CODES.N");
           configuration.setLocale(locale_update);
           LocaleList localeList = new LocaleList(locale_update);
           LocaleList.setDefault(localeList);
           configuration.setLocales(localeList);
           context = context.createConfigurationContext(configuration);
  
       }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
           Lg.d("Build.VERSION_CODES.JELLY_BEAN_MR1");
           configuration.setLocale(locale_update);
           context = context.createConfigurationContext(configuration);
       
       }else{
           Lg.d("else");
           configuration.locale = locale_update;
           context.getResources().updateConfiguration(configuration, null);
       }
       return new MyContextWrapper(context);
   }
   
   static boolean isLocaleJp(Context context){
       // 渡された Contextが日本語なら trueを返す
       Locale locale;
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
           Lg.d("Build.VERSION_CODES.N");
           locale = context.getResources().getConfiguration().getLocales().get(0);
 
       }else  {
           Lg.d("Build.VERSION_CODES.JELLY_BEAN_MR1");
           locale = context.getResources().getConfiguration().locale;
       }
       return (locale.equals(Locale.JAPAN) || locale.equals(Locale.JAPANESE));
   }
 }

以上のような例をふまえて、
以下のようにActivity 起動時に attachBaseContext を仕込んで、
狙った言語に変更する方法もあるようですね。

 @Override
   protected void attachBaseContext(Context context){
       super.attachBaseContext(MyContextWrapper.get(context, Locale.JAPANESE));
   }

私は(上のようにattachBaseContext を Overrideせずに)
OSで設定したDefault値で起動して、起動後にcontextを切り替えますが、
その場合は言語を切り替える「前に」
String text = context.getString(R.string.your_string_name);
のように取得済みである textについては、
切替後の contexを使って再度 getString で読み直すのを忘れないようにしましょう。

余談:ContextWrapperの置き換え後の注意

というより、ContextとActivityを「混ぜて」使っていた場合の注意です。

例として、アプリでダイアログを呼ぶ際に、こんなイメージだと思いますが。

new AlertDialog.Builder(context)

この引数の contextは、私は Activity自身を(thisで)渡していたのですが、
(ApplicationContextではなく、Activity を渡していたイメージ)
前述のような言語対応で context やらContextWrapper やらバタバタ変えた時に、
ダイアログ生成に失敗してしまいました。

Exception:android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application

AlerDialog.Builderで指定する引数は「the parent context」だとAPIリファレンスにも書いてあって、
(はい、知りませんでした)
これを AlertDialog.Builder(getApplicationContext) みたいにやっても動かないようです。

別の言い方をすれば、

Context-ContextWrapper-ContextThemeWrapper-Activity
Context-ContextWrapper-ContextThemeWrapper-Service

クラスの継承順で言えば上記なので、言語対応後のWrapperを渡しても
Activityとして必要な情報が入っていない?...あ、まずい、書いてて分からなくなってきた [worried]
とにかく、Context? Activity? 言語対応でWrapperへ置き換えた?
「どれ」を使っているか、間違えると動かなくなるかも、と。

いままで Activity や Service として渡していた部分を
言語変更のために ContextWrapper等で差し替える場合は気をつけましょう。


*1 「色」は colorはアメリカ英語、colourがイギリス英語