Xamarin.Android でアプリの言語を動的に切り替える

複数言語のリソースを用意しておいて、システムの言語を変えると、アプリで使用される言語リソースも変わるわけですが、システム設定に関係なく、アプリ内で言語選択をしたい。

つまり、

これ。 Kotlin でもできたので、どうせならということで Xamarin.Android でもやってみました。

できあがり

こんな感じの成果になります。

Untitled.gif

方法

1. 多言語用のリソースファイルを用意する

Android の仕様に従って values/values-ja-rJP/String.xml を用意します。 ファイルを追加した後で、Build Action が「AndroidResource」になっている事を確認してください。

values/String.xml (英語ってかデフォルト):

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">ResourceTest</string>

    <string name="welcome">WELCOME</string>
    <string name="to_japanese">To Japanese</string>
    <string name="to_english">To English</string>
</resources>

values-ja-rJP/String.xml (日本語):

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">ResourceTest</string>

    <string name="welcome">ようこそ</string>
    <string name="to_japanese">日本語にする</string>
    <string name="to_english">英語にする</string>
</resources>

2. MainActivity を実装する

とりあえずざっと。

MainActivity.cs:

using Android.App;
using Android.Widget;
using Android.OS;
using Android.Content;
using Android.Content.Res;
using Java.Util;
using System.Linq;
using System;

namespace ResourceTest
{
    [Activity(Label = "ResourceTest", MainLauncher = true, Icon = "@mipmap/icon")]
    public class MainActivity : Activity
    {
        protected override void AttachBaseContext(Context baseContext)
        {
            var pref = baseContext.GetSharedPreferences("mypref", FileCreationMode.Private);

            var locale = pref.GetString("locale", string.Empty);
            var newLocale = Locale.GetAvailableLocales().FirstOrDefault(
                l => string.Equals(l.ToString(), locale, StringComparison.OrdinalIgnoreCase))
                                  ?? Locale.Default;

            var configuration = baseContext.Resources.Configuration;
            configuration.Locale = newLocale;
            var newContext = new ContextWrapper(
                baseContext.CreateConfigurationContext(configuration));


            base.AttachBaseContext(newContext);
        }

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.Main);

            FindViewById<TextView>(Resource.Id.textWelcome).Text = Resources.GetString(Resource.String.welcome);

            var pref = this.GetSharedPreferences("mypref", FileCreationMode.Private);

            FindViewById<Button>(Resource.Id.buttonToEnglish).Click += (sender, e) => 
            {
                var editor = pref.Edit();
                editor.PutString("locale", "en_US");
                editor.Commit();
                Restart();
            };

            FindViewById<Button>(Resource.Id.buttonToJapanese).Click += (sender, e) =>
            {
                var editor = pref.Edit();
                editor.PutString("locale", "ja_JP");
                editor.Commit();
                Restart();
            };
        }

        private void Restart()
        {
            var intent = new Intent(this, typeof(MainActivity));
            this.Finish();
            this.StartActivity(intent);
        }
    }
}

簡単に説明すると 「AttachBaseContext() を override して、そこで任意の Locale に変えた Context にすげ替え」ています。

「任意の Locale」は、2つのボタンを押したときにそれぞれ ja_JPen_US を SharedPreference に保存しておき、Activity を再起動します。

再起動直後に AttachBaseContext() が呼ばれるので、そこで SharedPreference に記憶された Locale を読み出しています。

おまけ

1st try では、SharedPref を使うのを面倒くさがって、Application クラスに記憶させとく作戦でしたが、失敗しました。その原因は AttachBaseContext()OnCreate() よりも先に呼ばれ、さらに AttachBaseContext() の時点では Activity.Applicationnull になっているためでした。