.NET Framework の埋め込みリソースと外部リソースを使用したカスタム言語

· 5分で読める

はじめに

仕事中に、多くの開発者が対処しなければならない通常の問題を思いつきました。それは、クライアントにサービスを提供している言語とクライアントのカスタム言語です。

たとえば、基本言語が非公式言語を使用していて、クライアントが高齢者であるため、正式な言語を望んでいるとします。

また、このソリューションは複数の言語に対応しており、英語とスペイン語を使用することにも注意してください。

私の母国語は英語ではありません。チュートリアル内の間違いをお詫び申し上げます。間違いを見つけて修正したい場合は、このリポジトリ でプル リクエストを開いていただければ、喜んで承認します。

リソース

リソースは拡張子 .resx を持つ XML ファイルであり、キー/値構造を持ち、次のようになります。

<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="Action_cancel" xml:space="preserve">
    <value>Finish</value>
  </data>
  <data name="Action_greeting" xml:space="preserve">
    <value>Hello</value>
  </data>
</root>

英語スペイン語という 2 つの異なる言語のリソースを作成するので、ファイルの命名法は resources.ISOLANGUAGECODE.resx になります。たとえば、スペイン語の場合は resource.es.resx、英語の場合は resource.resx となります。後でドイツ語を追加する場合に備えて、ファイルの名前は resource.de.resx となります。

埋め込みリソース

埋め込みリソースは、プロジェクトがコンパイルされると、dll 内に追加されます。

これはリソースが埋め込まれた画像です。ご覧のとおり、ファイル Resource.resx はコンパイルでは表示されません。

Gyazo からの画像

外部リソース

一方、外部リソースまたは埋め込まれていないリソースは、コンパイル後にフォルダーに追加されるリソースです。

リソース ファイル (.resx) は Properties フォルダー内にあります。

Gyazo からの画像

プロジェクトのビルド

.NET Framework のコンソール プロジェクトから始めましょう。

埋め込みリソースファイルの作成

いくつかのキーを含むリソース ファイルを追加し、それが埋め込みリソースであることを確認します。これは、後で外部リソースで更新するために使用します。

Gyazo からの画像

外部リソースファイルの作成

外部リソースを埋め込みリソースから分離するために、後で簡単にアクセスできるように、名前を付けて外部リソースをフォルダー内に追加します。

ヒント: プロパティ フォルダー内にフォルダーを追加するには、フォルダーの外に作成して内部に移動します。Visual Studio では作成できません

埋め込みリソースに対して行ったのと同じことを行い、ファイルの properties 内にある AdvancedBuild action に移動し、それを Content に変更し、Copy to Output DictionaryCopy if newer に変更します。

Gyazo からの画像

これは次のようになります。

Gyazo からの画像

app.config へのキーの追加

私たちはクールな開発者であり、すべてをうまく仕上げることを好み、各クライアントのコードを変更することはありません。そのため、リソース ファイルを検索するフォルダーの名前を持つキーを appconfig に追加しましょう。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
  <appSettings>
    <add key="CustomResources.Folder" value="John" />
  </appSettings>
</configuration>
```したがって、後でリソースを検索するときは、`Properties.Doe` ではなく `Properties.John` を検索します。 

また、app.config を簡単に変更できるため、アプリケーションがすでにデプロイされている場合でも変更が容易になります。

## 埋め込みリソース ファイルへのアクセス

.NET Framework の使用は非常に簡単なので、Embedded プロパティにアクセスするには `Properties` オブジェクトを使用するだけです。このオブジェクトにはリソース ファイルがプロパティとして含まれ、そのオブジェクト内には `resx` ファイルにあるすべてのキー/値が含まれます。

```csharp
static void Main(string[] args)
{
    var hello = Properties.Resource.Action_greeting;
    var bye = Properties.Resource.Action_cancel;

    Console.WriteLine($"Action_greeting value: {hello}, Action_cancel value: {bye}");

    Console.Read();
}

これを実行すると、次のようなものが表示されるはずです。

Action_greeting value: Hello, Action_cancel value: Cancel

外部リソース ファイルへのアクセス

この部分はほぼ同じで、Properties オブジェクトによってリソース ファイルにアクセスできます。

var hello = Properties.Resource.Action_greeting;
var bye = Properties.Resource.Action_cancel;

var johnhello = Properties.John.Resource.Action_greeting;
var johnbye = Properties.John.Resource.Action_cancel;

var doehello = Properties.Doe.Resource.Action_greeting;
var doebye = Properties.Doe.Resource.Action_cancel;

問題

このメソッドを使用する主な問題は、すべてのキーがオブジェクト内のプロパティであるため、前に見たようにそれを呼び出す必要があることです。 John のリソース ファイルのキー Action_greeting を呼び出したい場合は、次の Properties.John を使用してから Resource.Action_greeting を使用する必要があります。

まさにそこに問題があります。

それは、多数のクライアント向けにアプリケーションを開発している場合、クライアントごとにリソース ファイルの呼び出し方法を変更するのは悪い考えだからです。

想像できますか? クライアントごとにアプリケーションをコンパイルし、 JohnDoe に変更し、さらに別の値に変更します。 それは非常識です!

解決策

私たちのチームリーダーは、フォールバック システムのような非常に良い方法を考えました。リソースの基本モデルが必要です。そして、クライアントごとに、そのリソースでファイルを更新するリソース ファイルが必要です。これで、最終的に 1 つのリソースのリストが得られます。

クライアントがカスタム リソースを望まない場合はベース リソースを使用し、クライアントが必要な場合はクライアントのリソースを使用します。

これをチェックリストに入れるには、次のことを行う必要があります。

  • すべてのキー/値を埋め込みリソースの辞書にマップする方法を見つけます。
  • すべてのキー/値を外部リソースの辞書にマッピングする方法を見つける
  • 両方のファイルを混合し、言語ごとに 1 つの辞書を作成します
  • 辞書にアクセスして値を返すメソッドを作成します。

Gyazo からの画像

コードを書いてみましょう

まず、リソース ファイルの取得からそれらの混合、値の返しまでのすべてのロジックが含まれる分離クラスを作成しましょう。そのクラスは CustomResources と呼ばれます。

これは次のようなものです:

    class CustomResources
    {

        private static Dictionary<string, string> _ResourcesEnglish;
        private static Dictionary<string, string> ResourcesEnglish;

        private static Dictionary<string, string> _ResourcesSpanish;
        private static Dictionary<string, string> ResourcesSpanish;


        private static Dictionary<string, string> OverwriteDictionary(Dictionary<string, string> currentDictionary, Dictionary<string, string> newDictionary, bool addIfDoesntExist = false)
        {
            ...
        }

        private static Dictionary<string, string> GetDictionaryFromEmbedded(string embedded, string cultureInfoCode)
        {
            ...
        }

        private static Dictionary<string, string> GetDictionaryFromFile(string file)
        {
            ...
        }

        public static string GetText(string key)
        {
            ...
        }

        public static string GetText(string key, string language)
        {
            ...
        }
    }

プロパティの遅延読み込みを実装していることに注意してください。これにより、パフォーマンスが向上し、辞書が 1 回読み込まれるようになります。

  • GetDictionaryFromEmbedded: 埋め込みリソースから辞書を返します。
  • GetDictionaryFromFile: 外部リソースから辞書を返します。
  • OverwriteDictionary: 2 つの辞書を混合し、1 つの辞書を返します。
  • GetText: キーを指定して値を返します

埋め込みリソースから辞書へ

XML ファイルからすべてのプロパティを取得し、辞書を返す必要があります。

private static Dictionary<string, string> GetDictionaryFromEmbedded(string embedded, string cultureInfoCode)
{
    Dictionary<string, string> res = new Dictionary<string, string>();

    try
    {
        ResourceManager rm = new ResourceManager(embedded, Assembly.GetExecutingAssembly());
        var resourceSet = rm.GetResourceSet(new CultureInfo(cultureInfoCode), true, true);

        var resourceDictionary = resourceSet.Cast<DictionaryEntry>()
                            .ToDictionary(r => r.Key.ToString(),
                                          r => r.Value.ToString());
        res = resourceDictionary;
    }
    catch (Exception e)
    {
        string a = e.Message;
        // Error getting resource file
    }

    return res;
}

2 つのこと:- embedded というパラメーターが必要であることに注意してください。パラメーターはデザイナーに表示されるファイルの名前であり、この場合は resources-demo.Properties.Resource です。

  • また、cultreInfoCode というパラメータもあります。これは、選択される言語のコードです。幸いなことに、.NET Framework がその仕事をしてくれます。何もする必要はありません。英語またはスペイン語のいずれかを設定するだけで、resource.es.resx または resource.resx のいずれかが選択されます。

外部リソースから辞書へ

ファイルからの取得は少しハック的ですが難しくはありません。実行可能ファイルの現在の場所を取得し、リソース ファイルの場所を連結して、それを解析して辞書に変換する必要があります。

ただし、ResXResourceReader にアクセスするには、まず System.Windows.Forms への参照を追加する必要があります。

Gyazo からの画像

次に GetDictionaryFromFile メソッドに進みます。

private static Dictionary<string, string> GetDictionaryFromFile(string file)
{
    Dictionary<string, string> res = new Dictionary<string, string>();
    string currentPath = (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) + file).Replace("file:\\", "");

    try
    {
        using (ResXResourceReader resxReader = new ResXResourceReader(currentPath))
        {
            foreach (DictionaryEntry entry in resxReader)
            {
                res.Add((string)entry.Key, (string)entry.Value);
            }
        }
    }
    catch (Exception e)
    {
        string a = e.Message;
    }

    return res;
}

ファイルパラメータには、実行可能ファイルからリソースファイルまでの場所を入力する必要があります。この場合は "\\Properties\\John\\Resource.resx" です。

辞書の混合

これでほぼ完了です。まず、app.settings にアクセスできるように System.Configuration を参照に追加することを忘れないでください。

 private static Dictionary<string, string> OverwriteDictionary(Dictionary<string, string> currentDictionary, Dictionary<string, string> newDictionary, bool addIfDoesntExist = false)
{
    var identifier = ConfigurationManager.AppSettings["CustomResources.Folder"];
    if (String.IsNullOrEmpty(identifier))
        return currentDictionary;

    foreach (var item in newDictionary)
    {
        try
        {
            currentDictionary[item.Key] = item.Value;
        }
        catch (Exception)
        {
            if(addIfDoesntExist)
            currentDictionary.Add(item.Key, item.Value);
        }
    }

    return currentDictionary;
}

辞書からテキストを取得する

言語を選択するプライベート メソッドを呼び出すパブリック メソッドを作成します。

private static string GetText(string key, string language)
{
    try
    {
        switch (language)
        {
            case "es":
                return ResourceSpanish[key];
            case "en":
            default:
                return ResourceEnglish[key];
        }
    }
    catch (Exception)
    {
        return $"No value with key: {key} and language: {language}";
    }
}
public static string GetText(string key)
{
    try
    {
        return GetText(key, Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName);
        }
    catch (Exception)
    {
        return $"No value with key: {key}";
    }
}

さらに言語を追加する場合は、スイッチを変更する必要があることに注意してください。

プロパティゲッターにコードを追加する

これですべてのメソッドが揃ったので、パブリック プロパティのゲッターを変更して値を取得できます。

private static Dictionary<string, string> _ResourcesEnglish;
private static Dictionary<string, string> ResourcesEnglish
{
    get
    {
        if (_ResourcesEnglish == null)
        {
            var folderIndentifier = ConfigurationManager.AppSettings["CustomResources.Folder"]; ;
            var baseResources = GetDictionaryFromEmbedded("resources-demo.Properties.Resource");
            var customResources = GetDictionaryFromFile($"\\Properties\\{folderIndentifier}\\Resource.resx", "en");
            _ResourcesEnglish = OverwriteDictionary(baseResources, customResources);
        }

        return _ResourcesEnglish;
    }
}

private static Dictionary<string, string> _ResourcesSpanish;
private static Dictionary<string, string> ResourcesSpanish
{
    get
    {
        if (_ResourcesSpanish == null)
        {
            var folderIndentifier = ConfigurationManager.AppSettings["CustomResources.Folder"]; ;
            var baseResources = GetDictionaryFromEmbedded("resources-demo.Properties.Resources");
            var customResources = GetDictionaryFromFile($"\\Properties\\{folderIndentifier}\\Resources.es.resx", "es");
            _ResourcesSpanish = OverwriteDictionary(baseResources, customResources);
        }

        return _ResourcesSpanish;
    }
}
  1. まず、app.settings から識別子を取得します。
  2. 次に、基本リソース、つまり埋め込みリソースを取得します。
  3. その後、カスタム リソースを取得します。そのためにはフォルダー名 (識別子) が必要です。
  4. 次に、それらを混合して値を返します。

遅延読み込みなので、これはすべて 1 回で実行されます。

テスト

コードに関連するすべてが完了したので、テストしましょう。辞書から値を取得するには、値を返すメソッド CustomResources.GetText(string key) を呼び出す必要があります。

リソース ファイル全体を更新する

このテストは、リソース ファイルのキー/値全体を更新する場合です。図からわかるように、キーは同じですが値が異なります。

John をテストします。それを設定するために、app.config を <add key="CustomResources.Folder" value="John" /> に設定します。

次に、基本リソース ファイル (Properties/Resource.es.resx) を確認してみましょう。

Gyazo からの画像

次に、外部リソース ファイル (Properties/John/Resource.es.resx):

Gyazo からの画像

OK、すべての設定が完了しました。コンソール アプリケーションを実行します。プロパティの get 部分に戻ってすべてを確認しましょう。

folderIdentifier には appseting の値があります。

Gyazo からの画像

baseResources には、基本リソース、つまり埋め込まれたリソースの値が含まれます。

Gyazo からの画像

customResources には、フォルダー John 内の外部リソースの値があります。Gyazo からの画像

そして最後に、_ResourcesSpanish には、基本リソースから外部リソースまで混合された値が含まれます。

Gyazo からの画像

1 つだけ更新しています

次に、同じものを異なるシナリオでテストしてみましょう。一方のキーを更新し、もう一方のキーを同じにするだけです。

Gyazo からの画像

ご覧のとおり、ファイルは Action_greeting については同じ値の意味を持ちますが、Action_cancel については異なる値を持っているため、Action_cancel のみを更新する必要があります。

Gyazo からの画像

リソース ファイルがありません

外部リソース ファイルを提供しない場合でも、ファイルがあることが期待されているため、失敗すると空の辞書が返され、両方の辞書を混合すると基本の辞書が使用されるため、まったく問題ありません。

埋め込みリソースにペアがありません

外部リソース ファイルにペアがある場合、デフォルトでは最終的な辞書には追加されません。両方の辞書を混合するときにメソッド OverwriteDictionary() を呼び出し、パラメータ addIfDoesntExisttrue に設定することで変更できます。

さまざまな言語

ご覧のとおり、言語を指定していません。これはすべて、GetText(string key, string language) を呼び出す関数 GetText(string key) によって行われており、パラメーター language には、スレッドの現在の言語を返す TwoLetterISOLanguageName が代入されています。

私の場合はスペイン語をデフォルト言語として設定しているため、常にスペイン語で表示されますが、英語を使用してみることもできます。

少しコーディングして、スペイン語と英語の両方をチェックするものを構築しましょう。

static void Main(string[] args)
{

    // System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
    System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
    WriteText("Action_greeting");
    WriteText("Action_cancel");

    // System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");
    System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
    WriteText("Action_greeting");
    WriteText("Action_cancel");

    Console.Read();
}

private static void WriteText(string key) {
    Console.WriteLine($"[Culture: {System.Threading.Thread.CurrentThread.CurrentCulture}] Key: {key} -> value: {CustomResources.GetText(key)}");
}

これを実行すると、出力は次のようになります。

[Culture: en-US] Key: Action_greeting -> value: Hello
[Culture: en-US] Key: Action_cancel -> value: Finish
[Culture: es-ES] Key: Action_greeting -> value: Hola
[Culture: es-ES] Key: Action_cancel -> value: Terminar

Gyazo からの画像

最初の 2 つの値は英語の resources.resx からのもので、最後の 2 つの値はスペイン語で、値は resources.es.resx から取得されます。

#それだけです

このチュートリアルでは、言語に埋め込みリソースと外部リソースの両方を混合する方法を見つけました。これはチーム内で抱えていた問題の解決策であり、それ以来問題なく実行されています。

ソースコードはここで確認できます。

ご質問がございましたら、@emimontesdeocaa までお気軽にツイートしてください。時間ができたら折り返しご連絡いたします。