現在、英語と日本語のUI表示に対応する予定のWPFアプリの開発をしています。まずは英語でGUIを実装していました。GUIの画面構成の変化が少なくなってきたので、そろそろ英語と日本語の二つの言語に対応することにしました。さて、どうやってローカライズすればよいのでしょう。WPFアプリのローカライズはいろいろなところに情報はありますが、自分へのメモも兼ねて記録に残したいと思います。では、WPFアプリの文字列のローカライズです。
ひとつ断っておきますが、ここで扱うのは文字列のローカライズです。アプリのグローバリゼーション(Globalization for WPF(英語))やローカライズ可能なレイアウトにすること(Use Automatic Layout Overview(英語))などは終わっているものとします。これらが完了していないと、文字列をローカライズしたとしても、文字化けしたり、GUIのレイアウトが崩れたり、ローカライズした文字列が一部しか表示されなかったりします。
WPFアプリの文字列をローカライズする方法の代表的なものを挙げると以下のものがあります。
- LocBamlを使って後処理で対応する方法
- x:Uidを使う方法(Windowsストア向けアプリのローカライズと同じ方法
- リソースディクショナリを使う方法
- Resources.resxを使う方法
などです。それぞれの方法を順番に確認します。
1. LocBamlを使って後処理で対応する方法
旧来のWPFのローカライズ法であるx:UidとLocBamlツールを使った後処理による方法(How to: Localize an Application(英語))です。
大まかな手順は、
- UIをXAMLで構成する
- ローカライズ対象要素にx:UID属性を付与する。
- LocBamlツールを使ってx:UID属性がついた要素をcsvファイルとしてエクスポート
- csvファイルを各言語にローカライズ
- LocBamlツールを使ってローカライズしたcsvファイルからサテライトアセンブリを作る
です。
しかしこの方法はここでは扱いませんので、説明は省略します。詳細な情報はマイクロソフトのサイトを参照してください。また、ここは日本語の説明で参考になると思います。
2. x:Uidを使う方法
(Windowsストア向けアプリのローカライズと同じ方法)
x:Uidを使った方法について説明します。これは、1番で記載した旧来のWPFのローカライズ法のことではありません。ここで扱うx:Uidを使う方法は、XAMLを使用したWindowsストア向けアプリで利用されているローカライズ方法と同じ方法です。
大まかな手順は、
- UIをXAMLで構成する
- ローカライズしたいコントロールにx:Uid属性を追加する
- 一つのResources.reswに1言語の文字列リソースを定義する。名前は {Uidの値}.{属性名} の形式です
- 対応するUI言語分のResources.reswファイルを用意する
- Resources.reswを適切なフォルダ構成で配置する
です。
この方法では、例えばButtonコントロールをローカライズしたいときXAMLファイル上は以下のようにx:Uidを使って、ローカライズするUIのIDを指定します。
<Button x:Uid="buttonCreate" FontFamily="Segoe UI" Content="Create new package file"/>
そして、ローカライズ後の文字列はResources.reswファイルに設定します。Resources.reswファイルの名前のところは、コントロールのx:Uid属性に設定したIDとローカライズしたいそのコントロールの属性名(Attribute名)をドット(.)でつなぎます。たとえば、上記のButtonコントールの場合は、以下のような設定をします。
名前 | 値 | コメント |
---|---|---|
buttonCreate.Content | Create new package file | |
buttonCreate.FontFamily | Segoe UI |
名前 | 値 | コメント |
---|---|---|
buttonCreate.Content | パッケージファイルを作成 | |
buttonCreate.FontFamily | Meiryo UI |
このようにすることで、アプリの実行時にContent属性とFontFamily属性がResources.reswで設定したものに置き換わります。表示する文字列だけでなく、フォント名などもUI言語ごとに置き換えることができます。
言語の切り替えは、適切なフォルダ構成でResources.reswファイル配置することにより、ユーザーのUI言語設定に従って自動的に行われます。
また、アプリ実行時にはXAMLの属性に設定された文字列の代わりにResources.reswファイルに設定した文字列が優先して使われます。そのためXAMLファイル上の文字列は開発用の分かりやすい文字列に設定することができるので開発が楽になります。
この方法はWPFアプリでは使えない方法で、Windowsストア向けアプリの方法なのにここまで説明したのは、「同じ仕組みをWPFでも使えたらよいのになぁ。」と思ったからです。念のためNuGetを探したら、WPF Localization (Surviveplus.WpfLocalization) というのがありました。
WPF Localization
WPF XAML 要素を x:Uid 属性とリソースを使ってローカライズするためのライブラリです。
後発のWindows ストアアプリや Windows Phone アプリと違って、WPF XAML で画面に表示されるテキストをローカライズする方法は非常に複雑です。このライブラリを使用すると、Windows ストアアプリなどと同様に x:Uid とリソースを使用してローカライズすることが出来て、非常に簡単です。
※現バージョンでは対応しているプロパティに制限があります。詳しくは NuGetパッケージのインストール時に表示される readme.txt を参照ください。
いい感じの機能です。すぐにでもこれを使おうとしました。でも、ライセンス情報がよくわかりませんでした。また、対応状況(最後の一文)も気になりました。そのためreadme.txtを確認したところ、対応状況は以下の通り、
In this version, the following classes and properties are supported.
TexBlock.Text
TextBox.Text
Button.Content
CheckBox.Content, CheckBox.ToolTip
RadioButton.Content, RadioButton.ToolTip
良いコンセプトのパッケージですが、対応するコントロール・属性がかなり少ないです。ちょっと使えません。ソースコードがGitHubに公開されていれば、追加対応してもよいのですが公開されていません。
このパッケージの利用はあきらめるしかないです。残念です。
3. リソースディクショナリを使う方法
次に、リソースディクショナリ(Resource dictionary)を使う方法です。
大まかな手順は、
- UIをXAMLで構成する
- 一つのStringResource.xamlファイルに1言語の文字列リソースを定義する。キー名は任意の形式です
- 対応するUI言語毎のStringResource.xamlファイルを用意する
- StringResource.xamlを適切なフォルダ構成または適切な名前で配置する
- ローカライズしたい属性をStaticResourceまたはDynamicResourceで参照する
です。
XAMLには、ResourceDictionary
というタグがあります。このタグのコンテンツには、オブジェクトとオブジェクトのキーの一覧を定義することができます。ここに文字列を格納し、コントロールで側ではそれをStaticResourceまたはDynamicResoruceでオブジェクトキーを使って参照する方法です。言葉の説明よりXAMLコードを見たほうがわかりやすいので例を記載します。
例えばButtonコントロールをローカライズしたいとき以下のように設定します。
<Button FontFamily="{StaticResource buttonCreateFontFamily}" Content="{StaticResource buttonCreateContent}"/>
文字列を定義する側は以下のようにします。
Resources/StringResource.en-us.xaml <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib"> <sys:String x:Key="buttonCreateContent">Create new package file</sys:String> <sys:String x:Key="buttonCreateFontFamily">Segoe UI</sys:String> </ResourceDictionary>
Resources/StringResource.ja.xaml <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib"> <sys:String x:Key="buttonCreateContent">パッケージファイルを作成</sys:String> <sys:String x:Key="buttonCreateFontFamily">Meiryo UI</sys:String> </ResourceDictionary>
現在のUI言語に合わせて、読み込むResourceDirectoryを切り替える必要があります。たとえば以下のようなコードで読み込みできます。
var cultureCode = "ja"; var dictionary = new ResourceDictionary(); dictionary.Source = new Uri(@"Resources/StringResource." + cultureCode + @".xaml", UriKind.Relative); this.Resources.MergedDictionaries.Add(dictionary);
ここでは具体的な切り替える方法の詳細は省略しますが、cultureCode は固定値ではなく、GetUserPreferredUILanguages()などから取得し、適切な言語を選択するのがよいです。
4. Resources.resxを使う方法
Visual StudioでC#プロジェクトを作成した時、プロジェクトに存在するResources.resxファイルを使う方法です。私も最終的にこの方法を使いました。
大まかな手順は、
- UIをXAMLで構成する
- 一つのResources.resxファイルに1言語の文字列リソースを定義する。キー名は任意の形式です
- 対応するUI言語毎のResources.resxファイルを用意する
- Resources.resxを適切なフォルダ構成、ファイル名で配置する
- ローカライズしたい属性をBindingまたはx:Staticを使用して参照する
です。
Resources.resxファイルを対応するUI言語の数だけ用意します。これを適切なフォルダ構成、ファイル名で配置することにより言語切り替えは自動的に行われます。
コントロールで側ではそれをBindingまたはx:Staticで参照する方法です。言葉の説明よりXAMLコードを見たほうがわかりやすいので例を記載します。
この方法では、例えばButtonコントロールをローカライズしたいときXAMLファイル上は以下のようにローカライズ文字列を参照します。
<Button FontFamily="{x:Static properties:Resources.buttonCreateFontFamily}" Content="{x:Static properties:Resources.buttonCreateContent}"/> (別の場所でpropertiesの名前空間の定義が必要です)
または、
<Button FontFamily="{Binding buttonCreateFontFamily, Source={StaticResource resources}}" Content="{Binding buttonCreateContent, Source={StaticResource resources}}"/> (別の場所でresourcesのオブジェクトの定義が必要です)
二つの方法の違いですが、前者の方法の場合は、オブジェクトのプロパティを直接参照しており、コンパイル時に名前解決がされるので、名前などが間違っていた場合、コンパイル時に検出されます。
後者の方法の場合は、XAML Designerの「データバインディングを作成」ダイアログ(Create Data Binding Dialog)が使えるのが良い点でもあります。存在するインスタンスやプロパティをダイアログ上で選んで設定することができます。
文字列を参照するのにどちらの方法を使うかですが好きなほうを使ってください。ただ、後者の方法を用いた場合、アプリの起動時にXAMLのパースで例外が発生することがあるようです。なぜだかわかりませんがApp.xaml.csなどのファイルを変更(改行を追加して削除(実質的には変更なし)などの変更でよいです)してコンパイルしなおすと回復するようです。もし、csファイルを変更しても回復しない場合は場合は、前者の方法に切り替えてみてください。
私の場合は、普段Data Binding作成ダイアログを使うことはありません。間違いがあった時にアプリの実行時ではなく、コンパイル時にエラーとして指摘してくれるほうが嬉しいので、前者を好んで使います。
ローカライズの文字列はResources.resxファイルに設定します。Resources.resxファイルの名前のところは、コントロールで参照した名前と同じものを指定します。たとえば、上記のButtonコントールの場合は、以下のような設定をします。
名前 | 値 | コメント |
---|---|---|
buttonCreateContent | Create new package file | |
buttonCreateFontFamily | Segoe UI |
名前 | 値 | コメント |
---|---|---|
buttonCreateContent | パッケージファイルを作成 | |
buttonCreateFontFamily | Meiryo UI |
このようにすることで、アプリの実行時にContent属性とFontFamily属性がResources.resxで設定したものに置き換わります。表示する文字列だけでなく、フォント名などもUI言語ごとに置き換えることができます。
結局、私はこの方法を使いました。
Resources.resxを使う方法の具体的手順
では、具体的な手順を詳細に説明します。
Visual Studio 2017で、C#のWPFアプリ(.NET Framework)プロジェクトを作成し、メインウィンドウにボタンを二つほど追加します。このボタンを多言語化してみます。
プロジェクト作成直後からResources.resxファイルがあります。これにベースとなる言語の文字列を定義します。多言語化する場合は、ベースとする言語は通常は英語にします。なぜなら、このベースとする言語は、対応しているUI言語がない場合にも使用されるからです。
例えば、英語と日本語のみUI言語を用意しているアプリをドイツ語環境で起動したとき、対応するUI言語がないのでベースとなるUI言語で表示されます。このとき、日本語で表示されたら欧州人はほとんどの場合理解できません。そのため、ベースとするUI言語は、国際言語である英語にします。
そのため、追加のResources.resxファイルは日本語用を追加します。追加するファイルの名前は、言語コードを追加して、Resources.ja.resxとします。言語コードは、日本語の場合はjaもしくはja-JPとなりますが、ここでは、jaとします。
まず、元からあるベース言語(英語)のResources.resxの設定のうちアクセス修飾子をInternalからPublicに変更します。サンプルとして3つの文字列を追加しています。
次に、日本語用のResources.ja.resxを追加します。このファイルの設定のアクセス修飾子は「コード生成なし」のままでよいです。サンプルとして3つの文字列を日本語化して追加しています。
もし、コントロールの文字列の設定をBinding/StaticResourceを使って行うならApp.xamlにインスタンスを定義します。また、名前空間の定義も追加します。
コントロールでの文字列リソースの参照は、以下の図のように行います。
⇒(赤)は、x:Staticで参照する場合の例です。名前空間の定義も追加しています。
⇒(青)は、Binding/StaticResoruceで参照する場合の例です。
これでWPFアプリのローカライズ(日本語・英語の両対応)は完了です。実際にアプリをビルドして、実行してみました。
まずは日本語環境での例です。「設定」の「地域と言語」で優先言語を日本語に変更すると、アプリも日本語で表示されます。
次に英語環境での例です。「設定」の「地域と言語」で優先言語を英語に変更すると、アプリも英語で表示されます。
最後にドイツ語環境での例です。「設定」の「地域と言語」で優先言語をドイツ語に変更すると、アプリは対応していないので、フォールバックとしてベース言語である英語で表示されます。
3つの言語を試してみましたが、どれも想定通りの動きをしています。
WPFアプリのローカライズの例でした。
次回へ続く
遅ればせながら、本日、こちらのページで紹介していただいていることを教えてもらいましたので、NuGetパッケージを更新・公開させていただきました。
http://tech.surviveplus.net/archives/1271