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

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

* [java - Android N change language programmatically - Stack Overflow](https://stackoverflow.com/questions/39705739/android-n-change-language-programmatically/40849142#40849142)

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

## できあがり

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

![Untitled.gif](https://qiita-image-store.s3.amazonaws.com/0/8227/9a6e1a7b-0acd-85a6-eb2f-7f63dfc0a1b2.gif)

## 方法

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

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

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

```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
<?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:**

```csharp
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_JP``、``en_US`` を SharedPreference に保存しておき、Activity を再起動します。

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

## おまけ

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