Xamarin逆引きTips

Xamarin逆引きTips

MvvmCrossで文字列をローカライズ(多言語化)するには?

2015年8月12日

MvvmCrossでのiOS/Androidアプリ開発において、ViewModelの文字列リソースを多言語化してローカライズする方法を解説する。

  • このエントリーをはてなブックマークに追加

 iOS/Androidアプリをストアに公開すると、日本だけではなく、英語圏やそれ以外の多くの国々で使用されることになるため、多言語化(国際化)とローカライズ(まとめてローカライズと表記)の重要性も高い。MvvmCrossにもローカライズ機構が用意されており、これを使用することで、プラットフォーム間で共通した方法によりローカライズを行うことができる。今回はMvvmCrossのJsonLocalizationプラグインを用いてローカライズを行う方法を解説する。

MvvmCrossの標準ローカライズ機能

 MvvmCrossをプロジェクトに導入すると、Cirrious.MvvmCross.Localizationアセンブリが標準で追加される。この中にはローカライズされたテキストを提供するオブジェクトのインターフェースであるIMvxTextProviderと、これを利用してローカライズリソースに対してバインディングを行うためのMvxLanguageBinderMvxLanguageConverterが含まれている。

 IMvxTextProviderはパラメーターに渡された型とリソースキーを基に、対応する文字列リソースを返すインターフェースだ。このインターフェースを独自に実装することで、ローカライズ機能を作ることもできるが、リファレンス実装としてJsonLocalizationプラグインが用意されている。このプラグインを使用すれば、ViewModelごとにリソースキーと文字列を定義したJSONファイルを用意することでリソースの定義を行えるようになる。

JsonLocalizationプラグインを用いたローカライズの実施

 JsonLocalizationプラグインは、JSONファイルからの文字列リソースの読み込みの機能は提供しており、アプリの起動中に言語を切り替えるといった要望に対しても柔軟に対応できるように考慮されている半面、実際に使用できるようにするための組み込みに多少の準備を必要とする。

 今回のアプリでは、JsonLocalizationプラグインを使ったローカライズの一例として、起動時に日本語に設定されている端末では日本語が、それ以外の場合は英語が表示されるように実装する。

プロジェクトの準備

 「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。このとき、ソリューションの作成時に[Create a project within solution directory.]にチェックを入れておくことを忘れないようにしていただきたい。

 次に「Tips:MvvmCrossでWebBrowserプラグインを使用するには?」でWebBrowserプラグインを追加する際と同様の手順で、NuGetから以下のプラグインを各プロジェクトに追加する。

  • MvvmCross - File Plugin
     (MvvmCross.HotTuna.Plugin.File
  • MvvmCross - ResourceLoader Plugin
     (MvvmCross.HotTuna.Plugin.ResourceLoader
  • MvvmCross - Json Plugin
     (MvvmCross.HotTuna.Plugin.Json
  • MvvmCross - JsonLocalisation Plugin
     (MvvmCross.HotTuna.Plugin.JsonLocalisation

ローカライズリソースの作成

 ローカライズするためのリソースを定義するファイルを作成する。iOS/Androidで共通のファイルを使うことから、一度、プロジェクト外のところでファイルを作成しておき、プロジェクトにはリンクという形でそれらのファイルを含めるとよい。

 まず、ソリューションファイル(.slnファイル)と同じフォルダーを開き、LanguageResourceというフォルダーを作成する。このフォルダーの中にViewModelごとにデフォルト定義となるJSONファイルを作成し、各言語のサブフォルダーを作成してその中にその言語へローカライズしたJSONファイルを作成することとなる(なお、今回の手順を用いる場合、アプリに含まれる全てのViewModelに対してJSONファイルを作成しておく必要がある。作成しない場合、実行時のログ出力にファイルが見つからない旨の警告が出力される)。

 まずは、この中にデフォルト定義となる英語のファイルを作成する。FirstViewModel.jsonというファイルを作成し、以下のような内容とする。

JSON
{
  "Test": "Test Text",
  "Sample1": "Sample 1 Text",
  "Sample2": "Sample 2 Text"
}
デフォルト(英語)リソースの内容 (FirstViewModel.json)

キーにリソースキーを持ち、値に表示されるテキストが指定されたシンプルなJSONとなっている。

 次に、日本語リソースを作成する。LanguageResourceフォルダーの中にja-JPフォルダーを作成し、先ほど作成したFirstViewModel.jsonja-JPフォルダーにコピーして、コピーしたファイルの内容を以下のように変更する。

JSON
{
  "Test": "テストテキスト",
  "Sample1": "サンプルテキスト1",
  "Sample2": "サンプルテキスト2"
}
日本語リソースの内容 (ja-JP/FirstViewModel.json)

キーは同じだが、表示される内容である値の方は日本語になっている。

 ここまでの操作で、ソリューションフォルダーの内容は以下のみのような構造になっているはずだ。

図1 作成した文字列リソースの配置

LanguageResourceフォルダーの中にja-JPフォルダーを作成する。

 作成したLanguageResourceフォルダーをプロジェクトに含める。

iOSアプリ

 iOSアプリのプロジェクトは、Resourcesというフォルダーがプロジェクトにデフォルトで存在する。この中に、先ほど作ったLanguageResourceフォルダーをリンクとして追加すればよい。Xamarin Studioで行う場合は、Resourcesフォルダーを右クリックし、(表示されるコンテキストメニューから)[追加]-[Add Existing Folder]を選択する。ファイル選択のダイアログでLanguageResourceフォルダーを選択すると、どのファイルを含めるかの選択が表示される。作成したJSONファイルにチェックを入れて[OK]ボタンをクリックする。次に表示されるダイアログは「どのように追加するか」を選択するものとなっているので、3つ目の選択肢である[Add a link to the file]を選択する(図2)。

図2 プロジェクトにリンクでファイルを追加する操作

ドラッグ&ドロップが使用できない場合は、追加先のフォルダーを右クリックし、[追加]-[Add Existing Folder]を使用してもよい。

Androidアプリ

 同様に、AndroidはAssetsフォルダーに対してLanguageResourceフォルダーのリンクを作成する。

TextProviderBuilderの実装

 JsonLocalizationプラグインでローカライズリソースを扱うには、MvxTextProviderBuilderクラスのサブクラスとしてTextProviderBuilderクラスを実装し、MvvmCrossの初期化処理の中に含める必要がある。MvxTextProviderBuilderは、ローカライズされたJSONファイルを読み込み、IMvxTextProviderに提供する。

 まず、Coreプロジェクトを右クリックし、[追加]-[新しいフォルダー]の順にポイントしてServicesフォルダーを作成する。次に、このフォルダーを右クリックして[追加]-[新しいファイル]を開いて新規作成のダイアログを表示し、[General]の中から[空のクラス]を選択し、「TextProviderBuilder」という名前を入力し、[新規]ボタンをクリックしてファイルを作成する。作成されたTextProviderBuilder.csファイルを以下のように編集する。

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Cirrious.CrossCore.IoC;
using Cirrious.MvvmCross.Plugins.JsonLocalisation;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Localization;
using Cirrious.CrossCore;

namespace LocalizeSample.Core.Services
{
  public class TextProviderBuilder : MvxTextProviderBuilder
  {
    public const string GeneralNamespace = "LocalizeSample";  // 1
    const string RootFolderForResources = "LanguageResource"; // 2

    public TextProviderBuilder()
      : base(GeneralNamespace, RootFolderForResources)
    {
    }

    protected override IDictionary<string, string> ResourceFiles // 3
    {
      get
      {
        return this.GetType() // 4
          .GetTypeInfo()
          .Assembly
          .CreatableTypes()
          .Where(t => t.Name.EndsWith("ViewModel"))
          .ToDictionary(t => t.Name, t => t.Name);
      }
    }
  }

  public static class TextProviderClassExtension
  {
    public static string GetText(this MvxViewModel viewModel, string name) // 5
    {
      var loader = Mvx.Resolve<IMvxTextProvider>();
      var type = viewModel.GetType();
      return loader.GetText(TextProviderBuilder.GeneralNamespace, type.Name, name);
    }

    public static IMvxLanguageBinder CreateTextSource(this MvxViewModel viewModel) // 6
    {
      return new MvxLanguageBinder(TextProviderBuilder.GeneralNamespace, viewModel.GetType().Name); 
    }
  }
}
TextProviderBuilderクラスの実装(TextProviderBuilder.cs)

 MvxTextProviderBuilderクラスは、コンストラクターにネームスペース(1)とローカライズリソースが入っているフォルダーの名前(2)を指定する。IMvxTextProviderの仕組み上はネームスペースで分類できるようになっているものの、JsonLocalizatonプラグインを使用する場合は固定の値を使用するようになっている。ローカライズリソースが入っているフォルダーの名前については、先ほど作成したローカライズリソースを含むフォルダーの名前であるLanguageResourceを指定している。

 また、このクラスは抽象クラスとなっており、これを継承するクラスは、アプリが使用するローカライズリソースのJSONファイル名の一覧であるResourceFilesプロパティを実装する必要がある(3)。今回は、Coreプロジェクトに含まれている全ての型から、“ViewModel”で終わるクラスを列挙して使用している(4)。

 また、今回はTextProviderを簡単に扱えるよう、MvxViewModelに対する拡張メソッドも定義する。GetText拡張メソッド(5)はViewModelに対応するローカライズリソースからテキストを取得するメソッド、CreateTextSource拡張メソッド(6)はLanguageバインディングで使用するTextSourceプロパティの値を作成するものだ。

 MvxTextProviderBuilderを実装したら、これをIoCコンテナーに登録する必要がある。CoreプロジェクトのApp.csファイルを開き、以下のように編集する。

C#
using System.Globalization;
using Cirrious.CrossCore;
using Cirrious.CrossCore.IoC;
using Cirrious.MvvmCross.Localization;
using Cirrious.MvvmCross.Plugins.JsonLocalisation;
using LocalizeSample.Core.Services;

namespace LocalizeSample.Core
{
  public class App : Cirrious.MvvmCross.ViewModels.MvxApplication
  {
    public override void Initialize()
    {
      CreatableTypes()
        .EndingWith("Service")
        .AsInterfaces()
        .RegisterAsLazySingleton();

      // この行を追加
      SetupTextBuilder();

      RegisterAppStart<ViewModels.FirstViewModel>();
    }

    // ----- ▼▼▼ ここから追加 ▼▼▼ -----
    void SetupTextBuilder()
    {
      var builder = new TextProviderBuilder(); // 1

      if (CultureInfo.CurrentUICulture.Name == "ja-JP")
      {
        builder.LoadResources(CultureInfo.CurrentUICulture.Name); // 2
      }

      var textProvider = builder.TextProvider;
      Mvx.RegisterSingleton<IMvxTextProviderBuilder>(builder); // 3
      Mvx.RegisterSingleton<IMvxTextProvider>(textProvider);   // 4
    }
    // ----- ▲▲▲ ここまで追加 ▲▲▲ -----
  }
}
MvxTextProviderBuilderをIoCコンテナーに登録(App.cs)

 まず、先ほど実装したTextProviderBuilderクラスのインスタンスを作成する(1)。

 TextProviderBuilderは、デフォルトでLanguageResourceフォルダー直下のリソースがロードされた状態で作成されるが、ローカライズ済みのリソースが必要な場合はLoadResourcesメソッドでLanguageResourceフォルダー内にあるサブフォルダーの名前を指定することで切り替えることができる。今回は、UIカルチャがja-JP(日本語)の場合に、ja-JPフォルダーの内容を読み込むようにしている(2)。

 TextProviderBuilderにリソースが読み込まれたら、TextProviderBuilderTextProviderプロパティからIMvxTextProviderオブジェクトを取得し、Mvx.RegisterSingletonメソッドを使ってTextProviderBuilder3)とIMvxTextProvider4)をそれぞれIoCコンテナーに登録する。

 これでローカライズリソースの準備が整った。

ViewModel内でローカライズリソースを取得する

 まず、FirstViewModelのデフォルトテキストをローカライズする。FirstViewModel.csファイルを以下のように編集する。

C#
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Localization;
using LocalizeSample.Core.Services;

namespace LocalizeSample.Core.ViewModels
{
  public class FirstViewModel 
    : MvxViewModel
  {
    // ----- ▼▼▼ ここから追加 ▼▼▼ -----
    public FirstViewModel()
    {
      _hello = this.GetText("Test"); // 1
    }
    // ----- ▲▲▲ ここまで追加 ▲▲▲ -----

    private string _hello = "Hello MvvmCross";
    public string Hello
    { 
      get { return _hello; }
      set { _hello = value; RaisePropertyChanged(() => Hello); }
    }
  }
}
ViewModel内でローカライズリソースを取得する(FirstViewModel.cs)

 コンストラクターで、ローカライズ値“Test”に設定されている値を取得して、_helloフィールドに直接代入している(1)。GetTextメソッドは先ほどTextProviderBuilderとあわせて実装した拡張メソッドだ。

実行例

 この状態でアプリを実行すると、端末の言語設定が英語の場合は英語で「Test Text」が、日本語の場合は「テストテキスト」が入力された状態で起動する。

図3 ViewModelのデフォルト値をローカライズしたアプリ

端末に設定されている言語が日本語の場合は日本語に、それ以外の場合は英語が入力した状態で起動するようになった。

ローカライズリソースにバインディングする

 ローカライズする文字列リソースに対してViewModel内での処理が不要な場合、Viewからローカライズリソースに対して直接バインディングすることもできる。そのためには、ViewModelにTextSourceという名前でIMvxLanguageBinder型のプロパティを実装する必要がある。

 先ほどのFirstViewModelに実装するのであれば、以下のようになる。

C#
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Localization;
using LocalizeSample.Core.Services;

namespace LocalizeSample.Core.ViewModels
{
  public class FirstViewModel 
    : MvxViewModel
  {
    public FirstViewModel()
    {
      _hello = this.GetText("Test");
    }

    // ----- ▼▼▼ ここから追加 ▼▼▼ -----
    private IMvxLanguageBinder _textSource = null;
    public IMvxLanguageBinder TextSource         // 1
    {
      get { 
        if (_textSource == null)
        {
          _textSource = this.CreateTextSource(); // 2
        }
        return _textSource;
      }
    }
    // ----- ▲▲▲ ここまで追加 ▲▲▲ -----

    private string _hello = "Hello MvvmCross";
    public string Hello
    { 
      get { return _hello; }
      set { _hello = value; RaisePropertyChanged(() => Hello); }
    }
  }
}
ローカライズリソースにバインディングする(FirstViewModel.cs)

 TextSourceプロパティをViewModelに実装した(1)。

 このプロパティでは、最初にアクセスされたときにIMvxLanguageBinderオブジェクトを作成するようにしているが(2)、これもTextProviderBuilderを作る際に用意したCreateTextSource拡張メソッドを使用して作成するようにしている。

 もし、アプリケーション共通で基底となるViewModelを用意する場合、その基底のViewModelにのみTextSourceプロパティを実装する形にするのもよい。

iOSアプリ

 次に、iOSアプリにバインディング先のラベルを作成して、リソースを表示する。TouchプロジェクトのFirstViewクラスを以下のように編集する。

C#
using Cirrious.MvvmCross.Binding.BindingContext;
using Cirrious.MvvmCross.Touch.Views;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;

namespace LocalizeSample.Touch.Views
{
  [Register("FirstView")]
  public class FirstView : MvxViewController
  {
    public override void ViewDidLoad()
    {
      View = new UIView { BackgroundColor = UIColor.White };
      base.ViewDidLoad();

      // ios7 layout
      if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
      {
         EdgesForExtendedLayout = UIRectEdge.None;
      }
         
      var label = new UILabel(new CGRect(10, 10, 300, 40));
      Add(label);
      var textField = new UITextField(new CGRect(10, 50, 300, 40));
      Add(textField);

      // ----- ▼▼▼ ここから追加 ▼▼▼ -----
      var sampleLabel1 = new UILabel(new CGRect(10, 90, 300, 40));
      Add(sampleLabel1);
      var sampleLabel2 = new UILabel(new CGRect(10, 130, 300, 40));
      Add(sampleLabel2);

      this.BindLanguage(sampleLabel1, "Sample1");              // 1
      this.BindLanguage(sampleLabel2, v => v.Text, "Sample2"); // 2
      // ----- ▲▲▲ ここまで追加 ▲▲▲ -----

      var set = this.CreateBindingSet<FirstView, Core.ViewModels.FirstViewModel>();
      set.Bind(label).To(vm => vm.Hello);
      set.Bind(textField).To(vm => vm.Hello);
      set.Apply();
    }
  }
}
iOSアプリに、バインディング先のラベルを作成して、リソースを表示する(FirstView.cs)

 TextSourceプロパティにバインディングしてローカライズする場合は、BindLanguage拡張メソッドを使用する。ラベルなど、デフォルト・バインディング・プロパティが文字列のビューウィジェットの場合は、バインディング先のオブジェクトと、バインディングするリソースキーを指定することで、バインディングが完了する(1)。

 デフォルト・バインディング・プロパティ以外のプロパティにバインディングする場合など、バインディング先のプロパティを変える場合は、第2引数に対象となるプロパティを参照することで対応できる(2)。

 なお、BindLanguageの操作はBindingSetの外で行っても構わない。

Androidアプリ

 Androidの場合は、local:MvxBind属性の代わりにlocal:MvxLang属性を使用してバインディングを定義する。Droidプロジェクトの[Resources]-[layout]フォルダーにあるFirstView.axmlファイルを開き、以下のように編集する。

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:local="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <EditText
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text Hello" />
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text Hello" />
  <!-- ----- ▼▼▼ ここから追加 ▼▼▼ ----- --> 
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxLang="Text Sample1" /><!-- 1 -->
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxLang="Text Sample2" />
  <!-- ----- ▲▲▲ ここまで追加 ▲▲▲ ----- --> 
</LinearLayout>
Androidアプリに、バインディング先のテキストビューを作成して、リソースを表示する(FirstView.axml)

 言語リソースにバインディングする際は、local:MvxLang属性を使用する。バインディング先のプロパティ名と、プロパティの代わりに参照するリソースキーを指定する(1)。

実行例

 この状態でアプリを実行すると、追加したラベルに端末の言語設定が英語の場合は英語で「Sample 1 Text」「Sample 2 Text」が、日本語の場合は「サンプルテキスト1」「サンプルテキスト2」が表示された状態で起動する。

図4 ローカライズ値へ直接バインディングしたアプリ

単純にローカライズしたテキストを表示するだけであれば、TextSourceプロパティだけ実装し、ローカライズリソースに対して直接バインディングできる。

※以下では、本稿の前後を合わせて5回分(第56回~第60回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

56. Xamarin.FormsでAzureモバイルサービスによるToDoアプリを作成するには?

ひな型プロジェクトが用意されているXamarin.iOSやXamarin.Androidではなく、Xamarin.FormsからAzureモバイルサービスを活用する基本的な方法を、簡単なToDoアプリを題材に解説する。

57. MvvmCrossで画像をバインディングするには?

MvvmCrossでのiOS/Androidアプリ開発において、画像のURLをViewへバインディングできるMvxImageViewの使い方を説明する。

58. 【現在、表示中】≫ MvvmCrossで文字列をローカライズ(多言語化)するには?

MvvmCrossでのiOS/Androidアプリ開発において、ViewModelの文字列リソースを多言語化してローカライズする方法を解説する。

59. MvvmCrossでViewModelからViewにイベントを通知するには?(Messengerパターン)

MvvmCrossでのiOS/Androidアプリ開発において、ViewModelからViewにイベントを通知するMessengerパターンの実装方法を紹介する。

60. XamarinのUIやコードの実行結果を簡単に確認できる「Sketches」を使うには?

Xamarin.Formsのレイアウトなどを、ビルドすることなくREPL環境で手軽に確認できるSketchesの使い方を紹介する。

サイトからのお知らせ

Twitterでつぶやこう!