使用 .NET Framework 中的嵌入式和外部资源的自定义语言

· 5 分钟阅读

简介

在工作中,我们遇到了一个正常的问题,我确信很多开发人员都必须处理这个问题,即来自向其客户提供我们服务的客户的语言和自定义语言。

例如,假设基本语言使用非正式语言,而客户需要正式语言,因为他们的客户是老年人。

另请注意,该解决方案适用于多种语言,我们将使用英语和西班牙语。

我的母语不是英语,对于教程中的任何错误,我深表歉意。如果您发现错误并想要修复它们,您可以在 this repo 上打开拉取请求,我很乐意批准它!

资源

资源是扩展名为 .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>

我们将为两种不同的语言创建资源:英语西班牙语,因此文件的命名法将为:resources.ISOLANGUAGECODE.resx,例如:resource.es.resx 为西班牙语,resource.resx 为英语,如果我们稍后要添加德语,文件将被命名为 resource.de.resx

嵌入资源

内嵌资源,项目编译时会添加到dll里面。

这是一个带有嵌入资源的图像,如您所见,文件 Resource.resx 在编译中看不到。

图片来自 Gyazo

外部资源

另一方面,外部资源或未嵌入的资源是编译后将添加到文件夹中的资源。

资源文件 (.resx) 将位于 Properties 文件夹内

图片来自 Gyazo

构建项目

让我们从 .NET Framework 中的控制台项目开始。

创建嵌入资源文件

添加一个带有几个键的资源文件,并确保它是嵌入式资源,稍后我们将使用它来更新外部资源。

图片来自 Gyazo

创建外部资源文件

为了将外部资源与嵌入资源分开,我们将在一个带有名称的文件夹中添加外部资源,以便以后可以轻松访问它们。

提示:要在 Properties 文件夹内添加文件夹,请在外部创建并将其移至内部,Visual Studio 不允许您创建它

执行与嵌入资源相同的操作,然后转到文件的 propertiesAdvancedBuild action 内部,并将其更改为:Content 并将 Copy to Output Dictionary 更改为 Copy 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.John` 而不是 `Properties.Doe`。 

此外,当应用程序已经部署时,这使得更改变得更容易,因为您可以轻松更改 app.config。

## 访问嵌入的资源文件

使用 .NET Framework 相当简单,因此为了访问嵌入式属性,我们只需使用 `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

这就是问题所在。

那是因为,如果我们正在为很多客户开发应用程序,那么改变我们为每个客户调用资源文件的方式是一个坏主意。

您能想象吗? 为每个客户端编译应用程序并将 John 更改为 Doe,然后更改为其他内容。 这太疯狂了!

解决方案

我们的团队领导想到了一个非常好的方法,就像后备系统一样,我们必须有一个资源的基本模型,然后对于每个客户端都有一个资源文件,它将用他们的资源更新文件,最后我们得到一个资源列表。

如果客户不需要自定义资源,我们将使用基础资源;如果客户需要,我们将使用他们的资源。

要将其放入清单中,我们必须这样做:

  • 找到一种方法将所有键/值映射到嵌入资源的字典。
  • 找到一种方法将所有键/值映射到外部资源的字典
  • 混合两个文件并为每种语言提供一个字典
  • 创建一个访问字典并返回值的方法

图表

图片来自 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)
        {
            ...
        }
    }

请注意,我们正在实现属性的延迟加载,这有助于提高性能并使其加载字典一次。

  • GetDictionaryFromEmbedded:从嵌入资源返回字典。
  • GetDictionaryFromFile:从外部资源返回字典。
  • OverwriteDictionary:混合两个字典并返回一个字典。
  • 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;
}

两件事:- 请注意,它需要一个名为 embedded 的参数,该参数是您可以在设计器中看到的文件的名称,在我们的例子中是:resources-demo.Properties.Resource

  • 我们还有一个名为 cultreInfoCode 的参数,它是要选择的语言的代码。对我们来说幸运的是,.NET Framework 为我们完成了这项工作,我们不需要做任何事情,只需设置我们想要英语或西班牙语,它就会在 resource.es.resxresource.resx 之间进行选择

从外部资源到字典

从文件获取有点困难,但并不难,我们必须获取可执行文件的当前位置,连接资源文件的位置,然后将其解析为字典。

但首先您必须添加对 System.Windows.Forms 的引用,才能访问 ResXResourceReader

图片来自 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"

混合字典

我们已经完成了,首先,请记住将 System.Configuration 添加到引用中,以便您可以访问 app.settings

 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}";
    }
}

请注意,如果要添加更多语言,则必须修改开关。

添加代码到属性 getter

由于我们现在拥有所有方法,因此我们可以修改公共属性的 getter 来获取值。

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. 然后我们将它们混合并返回值。

所有这些都将在延迟加载后完成一次。

测试

与代码相关的所有内容都已完成,现在让我们测试一下,为了从字典中获取值,我们必须调用返回值的方法 CustomResources.GetText(string key)

更新整个资源文件

此测试是我们想要更新资源文件的整个键/值的情况,正如您在图片中看到的,我们具有相同的键但不同的值。

我们将测试 John,为了进行设置,我们将 app.config 设置为 <add key="CustomResources.Folder" value="John" />

现在让我们检查我们的基础资源文件(Properties/Resource.es.resx):

图片来自 Gyazo

然后是我们的外部资源文件(Properties/John/Resource.es.resx):

图片来自 Gyazo

一切设置完毕后,我们运行控制台应用程序,让我们停止到属性的 get 部分并检查所有内容

folderIdentifier 具有应用程序设置的值:

图片来自 Gyazo

baseResources 具有基础资源、嵌入资源的值:

图片来自 Gyazo

customResources 具有文件夹 John 内的外部资源的值:图片来自 Gyazo

最后,_ResourcesSpanish 具有从基础资源到外部资源的混合值。

图片来自 Gyazo

只更新一个

现在让我们测试相同但不同的场景,只需更新一个密钥并让另一个相同。

图片来自 Gyazo

如您所见,这些文件对于 Action_greeting 具有相同的值含义,但对于 Action_cancel 具有不同的值,因此它应该仅更新 Action_cancel

图片来自 Gyazo

缺少资源文件

如果您不提供外部资源文件,那根本不重要,因为我们期望有一个文件,如果失败,它将返回一个空字典,并且当混合两个字典时,它将最终得到基本字典。

嵌入资源中缺少对

如果外部资源文件中有一对,默认情况下不会将其添加到最终字典中,您可以通过在混合两个字典时调用方法 OverwriteDictionary() 来更改这一点,并将参数 addIfDoesntExist 设置为 true

不同的语言

正如你所看到的,我们没有指定任何语言,因为所有这些都是由调用 GetText(string key, string language) 的函数 GetText(string key) 完成的,参数 languageTwoLetterISOLanguageName 填充,它返回线程的当前语言。

就我而言,我将西班牙语作为默认语言,这就是为什么它总是以西班牙语显示,但我们也可以尝试使用英语。

让我们编写一些代码并构建一些东西来检查西班牙语和英语。

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

前两个值来自 resources.resx,均为英语,最后两个值均来自西班牙语,并且这些值是从 resources.es.resx 检索的。

就是这样

在本教程中,我们找到了一种混合嵌入式和外部语言资源的方法,这是我们团队中遇到的问题的解决方案,从那时起它就一直运行没有任何问题。

您可以在[此处](https://github.com/emimontesdeoca/resources-demo查看源代码。

如果您有任何问题,请随时通过 @emimontesdeocaa 发推文给我,我会在有时间时回复您。