应用程序依赖于由类表示 ResourceManager 的 .NET Framework 资源管理器来检索本地化资源。 资源管理器假设使用中心辐射状模型来打包和部署资源。 核心程序集包含不可本地化的可执行代码以及用于单一文化的资源,称为中性文化或默认文化。 默认区域性是应用程序的回退区域性;如果找不到已本地化的资源,则使用默认区域性的资源。 每条轮辐均连接到一个附属程序集,该附属程序集包含单个区域性的资源,但不包含任何代码。
此模型有几个优点:
- 部署应用程序后,可以逐步添加新文化的资源。 由于后续开发区域性特定的资源可能需要大量的时间,因此,可以先发布主应用程序,并在以后提供区域性特定的资源。
- 无需重新编译应用程序,即可更新和更改应用程序的附属程序集。
- 应用程序只需加载那些包含特定区域性所需资源的附属程序集。 这可以显著减少系统资源的使用。
但是,此模型也有缺点:
- 必须管理多个资源集。
- 测试应用程序的初始成本会增加,因为必须测试多个配置。 请注意,从长远来看,使用多个卫星测试一个核心应用程序比测试和维护多个并行国际版本要容易、更便宜。
资源命名约定
打包应用程序的资源时,必须使用公共语言运行时所需的资源命名约定对其进行命名。 运行时按其文化名称标识资源。 每个文化都有一个独特的名称,这通常是与语言关联的双字母、小写的文化名称的组合,如果需要,则为与国家或地区关联的双字母、大写的子文化名称。 次文化名称跟在文化名称后面,用短划线(-)分隔。 例如,ja-JP 在日本使用的日语,en-US 在美国使用的英语,de-DE 在德国使用的德语,或 de-AT 在奥地利使用的德语。 请参阅 Windows 支持的语言/区域名称列表中的“语言”标记列。 文化名称遵循 BCP 47 定义的标准。
注释
两字母区域性名称有一些例外,如表示中文(简体)的 zh-Hans
。
资源回退过程
用于打包和部署资源的中枢轮辐式模型可使用回退过程来定位相应的资源。 如果应用程序请求不可用的已本地化资源,则公共语言运行时会在区域层次结构中查找与用户应用程序请求最匹配的相应回退资源,并仅在无法选择其他手段时引发异常。 在层次结构的每个级别,如果找到适当的资源,运行时会使用它。 如果未找到资源,搜索将继续在下一级别。
若要提高查找性能,请将属性 NeutralResourcesLanguageAttribute 应用于主程序集,并向其传递将用于主程序集的中性语言的名称。
.NET Framework 资源回退过程
.NET Framework 资源回退过程涉及以下步骤:
小窍门
您可能可以使用 <relativeBindForResources> 配置元素来优化资源回退机制和运行时探测资源程序集的过程。 有关详细信息,请参阅 优化资源回退过程。
运行时首先检查 全局程序集缓存 中是否存在与应用程序请求的区域性匹配的程序集。
全局程序集缓存可以存储由许多应用程序共享的资源程序集。 这样,就无需在所创建的每个应用程序的目录结构中包含特定资源集。 如果运行时找到对程序集的引用,则会在程序集中搜索请求的资源。 如果在程序集中找到了该项,则使用请求的资源。 如果找不到条目,它将继续搜索。
运行时接下来会检查当前正在执行的程序集目录,查找与请求的区域性相匹配的子目录。 如果找到该子目录,它将搜索该子目录以找到请求的区域性的有效附属程序集。 然后,运行时将搜索卫星程序集以查找请求的资源。 如果程序在程序集里找到资源,它就会使用该资源。 如果找不到资源,它会继续搜索。
接下来,运行时将查询 Windows Installer,以确定是否要按需安装附属程序集。 如果是这样,它将处理安装、加载程序集并搜索它或请求的资源。 如果程序在程序集里找到资源,它就会使用该资源。 如果找不到资源,它会继续搜索。
运行时引发 AppDomain.AssemblyResolve 事件以指示找不到附属程序集。 如果选择对事件进行处理,事件处理程序可以返回对其资源将用于查找的附属程序集的引用。 否则,事件处理程序将
null
返回并继续搜索。运行时接下来再次搜索全局程序集缓存,这次是搜索所请求文化的父程序集。 如果全局程序集缓存中存在父层程序集,运行时将搜索父层程序集中请求的资源。
父区域性被定义为合适的回退区域性。 将父区域性视为回退候选项,因为提供任何资源都比引发异常更可取。 此过程还允许重复使用资源。 仅当子无需本地化所请求的资源时,才应在父级别包含特定资源。 例如,如果您为
en
(中性英语)、en-GB
(英国口语的英语)和en-US
(在美国口述的英语)提供卫星程序集,则en
卫星将包含通用术语,并且en-GB
卫星en-US
只能为不同术语提供替代。运行时接下来检查当前正在执行的程序集的目录,以查看它是否包含父目录。 如果存在父目录,则运行时将搜索该目录,寻找父区域性的有效附属程序集。 如果找到程序集,运行时将在程序集内搜索请求的资源。 如果找到资源,则使用它。 如果找不到资源,它会继续搜索。
接下来,运行时将查询 Windows Installer,以确定是否要按需安装父级附属程序集。 如果是这样,它将处理安装、加载程序集并搜索它或请求的资源。 如果程序在程序集里找到资源,它就会使用该资源。 如果找不到资源,它会继续搜索。
运行时引发事件 AppDomain.AssemblyResolve 以指示找不到适当的回退资源。 如果选择对事件进行处理,事件处理程序可以返回对其资源将用于查找的附属程序集的引用。 否则,事件处理程序将
null
返回并继续搜索。接下来,如前前面三个步骤所述,运行时将通过许多可能的级别搜索父程序集。 每个区域性只有一个父区域性(由 CultureInfo.Parent 属性定义),但一个父区域性可能还有其自己的父区域性。 如果区域性的 Parent 属性返回 CultureInfo.InvariantCulture,父区域性搜索将停止;对于资源回退,固定区域性不被视为父区域性,也不被视为具有资源的区域性。
如果区域性是最初指定的,且已搜索所有父级,但仍未找到资源,则使用默认(回退)区域性的资源。 通常,默认文化的资源包含在主应用程序程序集。 但是,可指定 Satellite 特性的 Location 属性的值为 NeutralResourcesLanguageAttribute,以指定资源的最终回退位置是附属程序集,而不是主程序集。
注释
默认资源是唯一可以使用主程序集编译的资源。 除非通过使用 NeutralResourcesLanguageAttribute 属性指定附属程序集,否则为最终回退(最终父级)。 因此,我们建议始终在主程序集中包含一组默认资源。 这有助于防止引发异常。 通过包含默认资源文件,可为所有资源提供回退,并确保始终向用户呈现至少一种资源,即使该资源不是特定于区域性的。
最后,如果运行时找不到默认(回退)区域性的资源,将引发 MissingManifestResourceException 或 MissingSatelliteAssemblyException 异常,指示找不到该资源。
例如,假定应用程序请求本地化为墨西哥西班牙语(es-MX
区域性)所需的资源。 运行时首先在全局程序集缓存中搜索匹配 es-MX
的程序集,但找不到它。 然后,运行时将搜索当前正在执行的程序集的目录以获取目录 es-MX
。 如果失败,运行时会再次搜索全局程序集缓存区,以查找反映适当回退文化的父程序集(在本例中为 es
西班牙语)。 如果未找到父程序集,运行时将在所有可能的父程序集级别中搜索 es-MX
文化,直到找到相应的资源。 如果未找到资源,运行时会使用默认文化的资源。
优化 .NET Framework 资源回退过程
在下列情况下,可以优化运行时在附属程序集中搜索资源的过程:
附属程序集部署在与代码程序集相同的位置。 如果代码程序集安装在 全局程序集缓存中,则附属程序集也会安装在全局程序集缓存中。 如果代码程序集安装在一个目录中,则附属程序集安装在该目录的特定于区域性的文件夹中。
卫星程序集并不是按需安装的。
应用程序代码不处理事件 AppDomain.AssemblyResolve 。
可通过在应用程序配置文件中包含 <relativeBindForResources> 元素并将其 enabled
属性设为 true
,优化附属程序集探测,如下例所示。
<configuration>
<runtime>
<relativeBindForResources enabled="true" />
</runtime>
</configuration>
用于卫星组件的优化探针是一项可选择加入的功能。 也就是说,运行时遵循 资源回退过程中 记录的步骤,除非应用程序配置文件中存在 <relativeBindForResources> 元素,并且其 enabled
属性设置为 true
。 如果出现这种情况,附属程序集探测过程将进行如下修改:
运行时使用父级代码程序集的位置来探测卫星程序集。 如果父程序集安装在全局程序集缓存中,运行时会在缓存中查找,而不会在应用程序的目录中查找。 如果父程序集安装在应用程序目录中,运行时将只在应用程序目录中查找,而不会在全局程序集缓存中查找。
运行时不会查询 Windows Installer 以按需安装卫星程序集。
如果特定资源程序集的探测失败,运行时不会引发 AppDomain.AssemblyResolve 该事件。
.NET Core 资源回退过程
.NET Core 资源回退过程涉及以下步骤:
运行时尝试加载请求的区域性的附属程序集。
检查当前正在执行的程序集目录中是否有与请求的文化相匹配的子目录。 如果找到该子目录,它将搜索该子目录以找到请求的区域性的有效附属程序集并加载该程序集。
注释
在具有区分大小写的文件系统(即 Linux 和 macOS)的操作系统上,区域性名称子目录搜索区分大小写。 子目录名称必须与 CultureInfo.Name 的大小写完全匹配(例如,
es
或es-MX
)。注释
如果程序员从 AssemblyLoadContext中派生了自定义程序集加载上下文,则情况很复杂。 如果将执行程序集加载到自定义上下文中,运行时会将卫星程序集加载到自定义上下文中。 此文档的详细信息范围不足。 请参阅 AssemblyLoadContext。
如果未找到附属程序集,AssemblyLoadContext 引发 AssemblyLoadContext.Resolving 事件以指示找不到附属程序集。 如果选择对事件进行处理,事件处理程序可以加载引用并返回对附属程序集的引用。
如果仍未找到卫星程序集,AssemblyLoadContext 会使 AppDomain 触发一个 AppDomain.AssemblyResolve 事件,表明无法找到该卫星程序集。 如果选择对事件进行处理,事件处理程序可以加载引用并返回对附属程序集的引用。
如果找到卫星程序集,运行时会搜索该程序集请求的资源。 如果程序在程序集里找到资源,它就会使用该资源。 如果找不到资源,它会继续搜索。
注释
要在附属程序集中查找资源,运行时将搜索 ResourceManager 为当前 CultureInfo.Name 请求的资源文件。 在资源文件中,它搜索请求的资源名称。 如果未找到任一项,资源将被视为未找到。
接下来,运行时通过许多潜在级别搜索父区域性程序集,每次均重复步骤 1 和 2。
父区域性被定义为合适的回退区域性。 将父区域性视为回退候选项,因为提供任何资源都比引发异常更可取。 此过程还允许重复使用资源。 仅当子无需本地化所请求的资源时,才应在父级别包含特定资源。 例如,如果你为
en
(中性英语)、en-GB
(英国口述的英语)和en-US
(在美国口述的英语)提供卫星程序集,则en
卫星包含通用术语,并且en-GB
卫星en-US
只为不同术语提供替代。每个区域性只有一个父区域性(由 CultureInfo.Parent 属性定义),但一个父区域性可能还有其自己的父区域性。 当文化的Parent属性返回CultureInfo.InvariantCulture时,父文化的搜索将停止。 对于资源回退,固定区域性不被视为父区域性,也不被视为具有资源的区域性。
如果区域性是最初指定的,且已搜索所有父级,但仍未找到资源,则使用默认(回退)区域性的资源。 通常,默认文化的资源包含在主应用程序程序集。 但是,您可以为Satellite属性指定Location值,以表明资源的最终回退位置是附属程序集,而不是主程序集。
注释
默认资源是唯一可以使用主程序集编译的资源。 除非通过使用 NeutralResourcesLanguageAttribute 属性指定附属程序集,否则为最终回退(最终父级)。 因此,我们建议始终在主程序集中包含一组默认资源。 这有助于防止引发异常。 通过包括默认资源文件,可以为所有资源提供备选方案,并确保用户始终至少有一个资源存在,即使该资源不是文化特定的。
最后,如果运行时找不到默认(回退)区域性的资源文件,将引发 MissingManifestResourceException 或 MissingSatelliteAssemblyException 异常,指示找不到该资源。 如果找到资源文件,但请求的资源不存在,则请求将
null
返回。
最终回退到附属程序集
可选择从主程序集中删除资源,并指定运行时应加载对应于特定区域性的附属程序集中的最终回退资源。 若要控制回退过程,请使用 NeutralResourcesLanguageAttribute(String, UltimateResourceFallbackLocation) 构造函数并为参数提供一个值 UltimateResourceFallbackLocation ,该值指定 Resource Manager 是应从主程序集还是从附属程序集中提取回退资源。
以下 .NET Framework 示例使用 NeutralResourcesLanguageAttribute 属性将应用程序的回退资源存储在法语(fr
)语言的卫星程序集。 该示例有两个基于文本的资源文件,用于定义名为单个字符串资源 Greeting
。 第一个 resources.fr.txt包含法语资源。
Greeting=Bon jour!
第二个资源,ru.txt,包含俄语资源。
Greeting=Добрый день
这两个文件通过从命令行运行 资源文件生成器(resgen.exe) 编译为 .resources 文件。 对于法语资源,命令为:
resgen.exe resources.fr.txt
对于俄语资源,命令为:
resgen.exe resources.ru.txt
对于法语资源,从命令行运行程序集连接器 (al.exe),将 .resources 文件嵌入动态链接库,如下所示:
al /t:lib /embed:resources.fr.resources /culture:fr /out:fr\Example1.resources.dll
而对于俄语资源,则为如下所示:
al /t:lib /embed:resources.ru.resources /culture:ru /out:ru\Example1.resources.dll
应用程序源代码位于名为Example1.cs或Example1.vb的文件中。 它包括 NeutralResourcesLanguageAttribute 属性,用于指示默认应用程序资源位于 fr 子目录中。 它实例化资源管理器,检索Greeting
资源的值,并将其显示到控制台上。
using System;
using System.Reflection;
using System.Resources;
[assembly:NeutralResourcesLanguage("fr", UltimateResourceFallbackLocation.Satellite)]
public class Example
{
public static void Main()
{
ResourceManager rm = new ResourceManager("resources",
typeof(Example).Assembly);
string greeting = rm.GetString("Greeting");
Console.WriteLine(greeting);
}
}
Imports System.Reflection
Imports System.Resources
<Assembly: NeutralResourcesLanguage("fr", UltimateResourceFallbackLocation.Satellite)>
Module Example
Public Sub Main()
Dim rm As New ResourceManager("resources", GetType(Example).Assembly)
Dim greeting As String = rm.GetString("Greeting")
Console.WriteLine(greeting)
End Sub
End Module
然后,可以从命令行编译 C# 源代码,如下所示:
csc Example1.cs
Visual Basic 编译器的命令非常相似:
vbc Example1.vb
由于主程序集中没有嵌入的资源,因此无需使用 /resource
开关进行编译。
当你在语言为非俄语的系统上运行该示例时,它会显示以下输出:
Bon jour!
建议的打包替代方法
时间或预算限制可能会限制你为应用程序支持的每个子文化创建一组资源。 但可以为所有相关子区域性可用的父区域性创建单个附属程序集。 例如,可以提供一个英语附属程序集(en),该程序集由请求特定于区域的英语资源的用户检索,并为请求区域特定的德语资源的用户提供单个德语附属程序集(de)。 例如,对德国德语 (de-DE)、奥地利德语 (de-AT) 和瑞士德语 (de-CH) 的请求均会回退到德语附属程序集 (de)。 默认资源是最终回退,因此应该是大多数应用程序用户请求的资源,因此请仔细选择这些资源。 此方法部署在区域性上不太具体的资源,但可以显著降低应用程序的本地化成本。