多言語対応(C#、WPF編)

Windows Formsの多言語対応はそういくつも方法がある訳ではありません、以前紹介した方法かちょっとしたバリエーション違い・・程度ではないかと思っています。

一方WPFの場合は表示部(View)をXAMLで構成する都合、そのXAMLのUI要素をどう多言語対応するかにはいくつかの方法が出てきます。私が知り得たところを少しご紹介します。

1.LocBAMLを使う方法

MSDN他、MCTS(70-511)という.NET4.0(WinForms+WPF)の資格試験があったのですが、その試験の自習教材の中でWPFのローカライズ方法としてこのLocBamlを使う方法が紹介されています。この意味でMicrosoftがWPFアプリケーションの多言語対応として推奨(?)想定(?)しているのはこの方法のように感じます。この方法の大まかな手順は、

  1. UIをXAMLで構成する。
  2. MSBuildを使ってローカライズ対象要素にx:UID属性を付与する。
  3. x:UID属性がついた要素をLocBamlツールを使ってcsvとしてエキスポート
  4. csvファイルをローカライズ、ビルドしてサテライトアセンブリを作る

というものです。LocBamlは自分でビルドが必要なツールであることや.NET4.0以降ではそのままでは動作しないとやりにくいところがあるようですが、こちらのサイトでやり方が説明されています。作業が面倒で一部自動化のツール用意が必要など、ローカライズの主流ではないように感じます。

2.Resource.resxを使う方法

VisualStudioのC#プロジェクトに存在するResource.resxを使う方法です。Windows Formsの多言語対応時と文言定義場所位置が同じため、WinFormsアプリとWPFアプリを両方提供する場合には便利そうです。日本のブロガーによく紹介されているようです、この方法の大まかな手順は、

  1. UIをXAMLで構成する
  2. Resource.resxに言語リソースを定義する。
  3. 対応言語分resxファイルの追加、文言定義を繰り返す。
  4. XAMLから文言をStaticResourceで参照する

もともとCodeProjectで紹介された方法のようですが、こちらのサイトでとてもわかりやすく説明されています。

3.リソースディクショナリを使う方法

先に補足ですが、WPFではPenやBrushといったオブジェクトをリソースとして定義できますが、それらをリソースディクショナリという別ファイルにすることが可能です。これは上記2の方法で出てきたResource.resxファイルとは別モノのWPF特有ですのでご注意ください。

リソースディクショナリにはString型、つまり文字列もリソースとして定義可能ですのでこのリソースディクショナリに文言をXAMLで定義しておき、UI要素からリソースとして参照する方法があります。海外のブロガーがよく紹介されているようです、大まかな手順は、

  1. UIをXAMLで構成する
  2. プロジェクトに新しい項目としてリソースディクショナリを追加します。ここにsystem:String型の要素として文字列リソースを定義します。
  3. 対応言語分リソースディクショナリを作成します、このとき同一文言に対しては手順1で割り当てたKeyを合わせておくことが必要です。
  4. 手順1のXAMLのUI要素から必要な文字列をStaticResource or DynamicResourceで参照します。

これはこちらのサイトで紹介されています。2の方法との違いは、resxファイルにリソース定義するかxamlファイルにリソース定義するかの違いです。resxファイルはVisualStudioにエディタがありますが、文言翻訳作業を考えると私はこれがメリットには感じず、XamlのほうがReader/Writerがある分運用しやすく感じます。

ということで3の方法とその運用で使いそうな内容についてちょっとご紹介します。まずUIをXAMLで定義し、翻訳必要な文言を別リソースファイルとして定義するとこのような形になります。

MainWindow.xaml

<Window x:Class="MultiLanguageWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="179" Width="534">
    <Grid>
        <Label Content="{DynamicResource language}" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0" Name="label1" VerticalAlignment="Top" />
        <ComboBox Height="24" HorizontalAlignment="Left" Margin="100,12,0,0" Name="languageComboBox" VerticalAlignment="Top" Width="120" SelectedIndex="0" SelectionChanged="languageComboBoxSelectionChanged">
            <ComboBoxItem>日本語</ComboBoxItem>
            <ComboBoxItem>English</ComboBoxItem>
            <ComboBoxItem>Deutsch</ComboBoxItem>
        </ComboBox>
        <Button Content="{DynamicResource convert}" Height="23" HorizontalAlignment="Left" Margin="396,85,0,0" Name="convertButton" VerticalAlignment="Top" Width="88" Click="convertButton_Click" />
        <Label Content="{DynamicResource input}" Height="28" HorizontalAlignment="Left" Margin="12,66,0,0" Name="label2" VerticalAlignment="Top" />
        <TextBox Height="24" HorizontalAlignment="Left" Margin="100,70,0,0" Name="inputFileTextBox" VerticalAlignment="Top" Width="290" />
        <Label Content="{DynamicResource output}" Height="28" HorizontalAlignment="Left" Margin="12,100,0,0" Name="label3" VerticalAlignment="Top" />
        <TextBox Height="24" HorizontalAlignment="Left" Margin="100,100,0,0" Name="outputFileTextBox" VerticalAlignment="Top" Width="290" />
    </Grid>
</Window>

リソースの実体はそれぞれ以下のような形で定義します。

StringResource.ja-jp.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">言語</system:String>
    <system:String x:Key="update">更新</system:String>
    <system:String x:Key="convert">変換</system:String>
    <system:String x:Key="input">入力</system:String>
    <system:String x:Key="output">出力</system:String>
    <system:String x:Key="file">ファイル</system:String>
</ResourceDictionary>

StringResource.en-us.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">Language</system:String>
    <system:String x:Key="update">Update</system:String>
    <system:String x:Key="convert">Convert</system:String>
    <system:String x:Key="input">Input</system:String>
    <system:String x:Key="output">Output</system:String>
    <system:String x:Key="file">File</system:String>
</ResourceDictionary>

StringResource.de-DE.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">Sprache</system:String>
    <system:String x:Key="update">Aktualisierung</system:String>
    <system:String x:Key="convert">Umwandlung</system:String>
    <system:String x:Key="input">Eingabe</system:String>
    <system:String x:Key="output">Ausgabe</system:String>
    <system:String x:Key="file">Akte</system:String>
</ResourceDictionary>

japan

上のようなUIで、コードビハインドに以下のような簡単な処理を用意しておくと

MainWindow.xaml.cs

namespace MultiLanguageWpfApp
{
    using System;
    using System.Windows;

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            SetLanguage("ja-jp");
        }
        private void SetLanguage(string cultureCode)
        {
            var dictionary = new ResourceDictionary();
            dictionary.Source = new Uri(@"Resources/StringResource." + cultureCode + @".xaml", UriKind.Relative);
            this.Resources.MergedDictionaries.Add(dictionary);
        }
        private void languageComboBoxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
        {
            string[] culcureCodes = { "ja-jp", "en-us", "de-DE" };
            SetLanguage(culcureCodes[languageComboBox.SelectedIndex]);
        }
        // xaml-xls変換
        private void convertButton_Click(object sender, RoutedEventArgs e)
        {
            // 後述
        }
    }
}

ドイツ語

コンボボックスアイテム変更で動的な言語切り替えが可能となります。

最後に。私が考える翻訳作業では、ビルド用にはXAMLリソースディクショナリが必要ですが、翻訳業者に提出する資料としたり、文字数制限チェックをしたりと、ベースはExcelファイルの方が都合よさそうです。となるとExcelファイルは読み書きがほしくなるのでNPOIを使って、XAML(リソースディクショナリ)⇔Excel間の変換に以下のようなクラスを使います。

XamlResourceConverter.cs

namespace MultiLanguageWpfApp
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Collections;
    using System.Windows;
    using System.Windows.Markup;
    using NPOI.HSSF.UserModel;
    using NPOI.SS.UserModel;

    public class XamlResourceConverter
    {
        private const int targetColumnIndex = 1;
        private static readonly string sheetName = "sheet1";

        // コンストラクタを外部から隠ぺい
        private XamlResourceConverter() { }

        // ID, 文言のペア
        private Dictionary<string, string> stringTable = new Dictionary<string, string>();

        // Dictionaryから
        public XamlResourceConverter(Dictionary<string, string> table)
        {
            stringTable = table;
        }

        // XAMLファイルから読み込み
        public static XamlResourceConverter FromXamlFile(string xamlResourceFile)
        {
            Dictionary<string, string> table = new Dictionary<string, string>();
            using (FileStream inputStream = new FileStream(xamlResourceFile, FileMode.Open))
            {
                ResourceDictionary xamlTop = XamlReader.Load(inputStream) as ResourceDictionary;
                if (xamlTop == null)
                {
                    throw new InvalidDataException();
                }
                foreach (DictionaryEntry item in xamlTop)
                {
                    table.Add(item.Key as String, item.Value as String);
                }
            }
            return new XamlResourceConverter(table);
        }

        // XLSファイルから読み込み
        public static XamlResourceConverter FromExcelFile(string excelFile)
        {
            Dictionary<string, string> table = new Dictionary<string, string>();
            using (FileStream inputStream = new FileStream(excelFile, FileMode.Open, FileAccess.Read))
            {
                HSSFWorkbook book = new HSSFWorkbook(inputStream);
                ISheet sheet = book.GetSheet(sheetName);
                for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++)
                {
                    IRow row = sheet.GetRow(rowIndex);
                    table.Add(row.GetCell(0).StringCellValue, row.GetCell(1).StringCellValue);
                }
            }
            return new XamlResourceConverter(table);
        }

        // XAMLファイルへの書き込み
        public void SaveToXaml(string xamlResourceFile)
        {
            var resourceDictionary = new ResourceDictionary();
            foreach (var item in stringTable)
            {
                resourceDictionary.Add(item.Key, item.Value);
            }
            var xaml = XamlWriter.Save(resourceDictionary);
            File.WriteAllText(xamlResourceFile, xaml);
        }

        // XLSファイルへの書き込み
        public void SaveToExcel(string excelFile)
        {
            using (FileStream outputStream = new FileStream(excelFile, FileMode.OpenOrCreate))
            {
                // NPOIを使ってExcel出力
                var book = new HSSFWorkbook();
                var sheet = book.CreateSheet(sheetName);
                int rowIndex = 0;
                foreach (KeyValuePair<string,string> item in stringTable)
                {
                    var row = sheet.CreateRow(rowIndex++);
                    var cell1 = row.CreateCell(0, NPOI.SS.UserModel.CellType.String);
                    var cell2 = row.CreateCell(targetColumnIndex, NPOI.SS.UserModel.CellType.String);
                    cell1.SetCellValue(item.Key);
                    cell2.SetCellValue(item.Value);
                }
                book.Write(outputStream);
            }
        }
    }
}

NPOIはxlsxは扱えないみたいで、xls限定になります。他にもWPFの多言語対応化は方法があるようですが、TPOで適宜判断ください。

多言語対応(C#、WPF編)」への1件のフィードバック

  1. 言語を動的に切り替えるGUIの検討をしていたので、参考になりました。ありがとうございます。
    切り替える度にMergedDictionariesの要素数が増えるのが、ちょっと気になりました。ご参考になれば。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中