65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 国际化:构建全球化 Windows 和 Web 应用程序的开发者指南:第 11 章 - 自定义区域设置

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (21投票s)

2006 年 8 月 14 日

43分钟阅读

viewsIcon

132212

.NET Framework 中的自定义区域设置代表着一个巨大的飞跃,为开发人员开辟了新的、令人兴奋的可能性。新的区域设置被 .NET Framework 识别为一等公民,一旦注册,它们就与其他任何区域设置一样有效。

1.jpg

作者 Guy Smith-Ferrier
标题 .NET 国际化:构建全球化 Windows 和 Web 应用程序的开发者指南
出版社 Addison-Wesley
出版日期 2006 年 8 月 7 日
ISBN 0321341384
价格 US$ 44.99
页数 672

引言

CultureInfo 类是 .NET 国际化解决方案的核心。在第 6 章“全球化”中,您已经了解到,在 .NET Framework 2.0 中,可用区域设置的列表是 .NET Framework 已知区域设置与操作系统已知区域设置的组合。而在 .NET Framework 1.1 中,可用区域设置的列表仅仅是 .NET Framework 已知区域设置。如果所需的国家/语言组合是可用区域设置之一,并且该组合的信息对您的应用程序是正确的,那么这些区域设置就足够了。但是,存在许多国家/语言组合是不可用的,而其中一些可用组合可能不包含对您的应用程序而言正确的信息。因此,在 .NET Framework 2.0 中引入了自定义区域设置。自定义区域设置是由应用程序开发人员而非 Microsoft 定义的区域设置。创建后,.NET Framework 会将其视为一等公民,自定义区域设置与其他任何区域设置一样有效。在本章中,我们将探讨如何创建、注册/注销以及部署自定义区域设置。对于 .NET Framework 1.1 应用程序,情况则不那么复杂。在 .NET Framework 1.1 中可以创建自定义区域设置,但结果却不尽如人意。本章末尾将对此主题进行介绍。

自定义区域设置的用途

自定义区域设置有许多用途,并且完全有可能从 Internet 下载免费和商业的自定义区域设置。在本节中,我们将探讨您可能想要创建自己的自定义区域设置的几个原因。

第一个也是最简单的原因是更新现有区域设置中过时或不理想的信息。在第 6 章“CultureInfo 类”一节中,我曾指出,现有区域设置中的某些信息,例如货币信息,会随着时间的推移而变得不正确。.NET Framework 2.0 有一个新的基线区域设置数据,用于纠正许多过去的错误,以反映发布时世界的情况(例如,土耳其(土耳其)的货币已从 TL (土耳其里拉) 更新为 YTL (新土耳其里拉))。此外,还可以通过 Windows Update 来保持区域设置信息的最新。在几乎所有情况下,由于过时信息而需要更新区域设置信息的可能性都很低。但是,总会有例外情况,而且总会有现有信息变得不理想(而不是不正确)的时候。自定义区域设置允许您创建一个具有相同名称和 LCID 的“替换”区域设置,但具有不同的属性值。我们在此创建的第一个自定义区域设置就是这样的区域设置。

另一个常用的自定义区域设置用途是支持在已知使用国家/地区之外使用的已知语言。例如,西班牙语在美国广泛使用,但 .NET Framework 没有 es-US (西班牙语(美国)区域设置。表 11.1 显示了这些区域设置的一些示例。

表 11.1 在已知国家/地区之外使用语言的自定义区域设置示例

区域设置名称

区域设置英文名称

该地区使用该语言的用户近似数量

es-US

西班牙语(美国)

22,400,000

hi-GB

印地语(英国)

1,300,000

pa-CA

旁遮普语(加拿大)

300,000

zh-CA

中文(加拿大)

870,000

zh-US

中文(美国)

2,000,000

考虑到世界上有近 200 个国家和近 7000 种语言,Microsoft 无法支持国家和语言的所有可能组合。我们可以为这些“缺失”的国家/语言组合创建“补充”自定义区域设置。本章中的 西班牙语(美国)自定义区域设置就是这样一个例子。这种情况同样适用于世界各地的各种侨民社区。例如,法国和西班牙有大量英国侨民,这产生了对 英语(法国)英语(西班牙)自定义区域设置的需求。

这个主题的一个变体是为 .NET Framework(或 Windows)当前不支持的国家/地区或语言创建自定义区域设置。表 11.2 显示了一些示例。

表 11.2 不支持的国家/地区或语言的自定义区域设置示例

区域设置名称

区域设置英文名称

该地区使用该语言的用户近似数量

bn-BD

孟加拉语(孟加拉国)

125,000,000

eo

世界语

2,000,000

fj-FJ

斐济语(斐济)

364,000

gd-GB

盖尔语(英国)

88,892

tlh-KX

克林贡语(克林贡)(“tlh”是分配给“tlhIngan Hol”的 ISO 代码,“tlhIngan Hol”是克林贡语的名称)

431,892,000,000

la

拉丁语

?

tl-PH

他加禄语(菲律宾)

14,000,000

另一个同样重要的自定义区域设置用途是支持伪本地化。在第 9 章“机器翻译”的“选择用于伪本地化的区域设置”一节中,我介绍了 PseudoTranslator 类,该类执行从拉丁语系语言到同一语言的带重音版本的伪本地化。这样做的好处是,可以测试本地化过程,开发人员和测试人员仍然可以使用本地化应用程序,而无需学习另一种语言。在第 9 章的实现中,我们劫持了一个现有的区域设置作为伪本地化区域设置。在本章中,我们将创建一个专门用于支持伪本地化的自定义区域设置。

最后,自定义区域设置的另一个常见用途是支持商业方言。在这种情况下,您希望以单一语言(例如英语)发货应用程序,但一个客户或一组客户使用的词语和短语与另一个客户或另一组客户使用的词语和短语不同。这种情况比听起来要普遍。例如,会计行业就面临这种困境,因为“practice”和“site”这两个词对不同的人来说有不同的含义。您可以为特定客户创建自定义区域设置。例如,您可以创建一个 英语(美国,Sirius Minor Publications)自定义区域设置来服务 Sirius Minor Publications 客户,以及一个 英语(美国,Megadodo Publications)自定义区域设置来服务 Megadodo Publications 客户。这两个区域设置的父级都将是 英语(美国)或仅 英语,以便大多数文本对所有英语客户都是通用的。Sirius Minor Publications 将拥有使用其自己商业方言的资源,同样,Megadodo Publications 也将拥有使用其自己商业方言的资源。对开发人员的好处是,应用程序只有一个代码库,同时又能满足各个客户的需求。

使用 CultureAndRegionInfoBuilder

创建自定义区域设置包括两个步骤:

  1. 定义自定义区域设置
  2. 注册自定义区域设置

这两个步骤都使用 .NET Framework 2.0 的 CultureAndRegionInfoBuilder 类来实现。我们将从一个简单的创建替换区域设置的示例开始,以了解整个过程。稍后我们将回到这个主题,以创建更复杂的自定义区域设置。

在此示例中,该区域设置是对 en-GB 英语(英国)区域设置的替换。此区域设置的目的是更改默认的 ShortTimePattern 以包含 AM/PM 后缀(类似于 en-US ShortTimePattern)。ShortTimePattern 是 .NET Framework 属性,不属于 Win32 数据,因此无法在“区域和语言选项”对话框中设置此值。

以下代码创建了一个替换自定义区域设置并注册它:

// create a CultureAndRegionInfoBuilder for a
// replacement for the en-GB culture
CultureAndRegionInfoBuilder builder =
  new CultureAndRegionInfoBuilder("en-GB",
  CultureAndRegionModifiers.Replacement);

// the en-GB's short time format
builder.GregorianDateTimeFormat.ShortTimePattern = "HH:mm tt";

// register the custom culture
builder.Register();

CultureAndRegionInfoBuilder 构造函数接受两个参数:自定义区域设置名称和用于标识新区域设置类型的枚举。替换区域设置使用 Register 方法注册。注册后,此计算机上的所有 .NET Framework 2.0 应用程序将使用修改后的 en-GB 区域设置,而不是原始设置,而无需更改这些应用程序。

安装/注册自定义区域设置

CultureAndRegionInfoBuilderRegister 方法执行两个操作:

  • 在系统的 Globalization 文件夹中创建一个 NLP 文件
  • 在注册表中向 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Nls\IetfLanguage 添加一个条目

NLP 文件是自定义区域设置的二进制表示。没有针对此文件格式的 API,因此您必须将其视为黑盒子。该文件放置在 %WINDIR%\Globalization 目录下,并命名为与自定义区域设置相同的名称(例如,c:\Windows\Globalization\en-GB.NLP)。

注册表条目为静态 CultureInfo 方法提供了自定义区域设置的 IetfLanguage 名称。键是自定义区域设置的 IetfLanguage,值是共享相同 IetfLanguage 的自定义区域设置名称的以分号分隔的列表。在示例中调用 Register 后,将出现一个键为“en-GB”且值为“en-GB”的条目,这表明 en-GB 自定义区域设置的 IetfLanguage 是“en-GB”。

这种方法适用于在您自己的计算机上注册自定义区域设置,但它不够通用。如果您想在用户计算机上创建三个自定义区域设置(例如,en-GBfr-FRes-ES),您必须要么创建一个名为 CreateAndRegisterAllThreeCultures 的应用程序,要么创建三个独立的应用程序,例如 Create_enGB_CultureCreate_frFR_CultureCreate_esES_Culture。更好的解决方案是创建一个单一的自定义区域设置注册程序,并向其传递自定义区域设置文件。在本书的源代码中,您会找到名为 RegisterCustomCulture 的控制台应用程序,它就是为此目的而创建的。RegisterCustomCulture 接受一个或多个 LDML 自定义区域设置文件进行注册。LDML 是 Locale Data Markup Language,定义在 Unicode 技术标准 #35 中。它是一种可扩展的 XML 格式,用于交换结构化的区域设置数据,也是 Microsoft 选择用于导入和导出自定义区域设置的格式。尽管 LDML 由 Unicode Consortium 明确定义,但其使用存在广泛差异。如果您打算使用非 CultureAndRegionInfoBuilder 创建的 LDML 文件,请准备好修改 LDML,以便 CultureAndRegionInfoBuilder 可以消耗它。可以使用 CultureAndRegionInfoBuilder.Save 方法创建 LDML 文件,因此之前的示例可以重写如下:

CultureAndRegionInfoBuilder builder =
  new CultureAndRegionInfoBuilder("en-GB",
  CultureAndRegionModifiers.Replacement);

builder.GregorianDateTimeFormat.ShortTimePattern = "HH:mm tt";

builder.Save("en-GB.ldml");

此代码将成为应用程序构建过程的一部分,从而生成 en-GB.ldml 文件,该文件又会成为应用程序安装过程的一部分。可以通过使用 CultureAndRegionInfoBuilder.CreateFromLdml 方法轻松加载该文件。

CultureAndRegionInfoBuilder builder =
  CultureAndRegionInfoBuilder.CreateFromLdml("en-GB.ldml");

builder.Register();

此处显示了 RegisterCustomCulture 控制台应用程序的重要部分:

static void Main(string[] args)
{
  Console.WriteLine("RegisterCustomCulture registers custom" +
    " cultures for the .NET Framework from LDML/XML files");
  Console.WriteLine("");
  if (args.GetLength(0) == 0)
    // no parameters were passed – show the syntax
    ShowSyntax();
  else if (AllFilesExist(args))
  {
    // file parameters are all good – register the cultures
    RegisterCustomCultures(args);
  }
}

private static void RegisterCustomCultures(
  string[] customCultureFiles)
{
  foreach (string customCultureFile in customCultureFiles)
  {
    if (customCultureFile.StartsWith("/u:") ||
      customCultureFile.StartsWith("/U:"))
    {
      // unregister the culture
      string customCultureName =
        customCultureFile.Substring(3);

      CultureAndRegionInfoBuilder.Unregister(
        customCultureName);

      Console.WriteLine("{0} custom culture unregistered",
        customCultureName);
    }
    else
    {
      // register the culture
      CultureAndRegionInfoBuilder builder =
        CultureAndRegionInfoBuilder.CreateFromLdml(
        customCultureFile);

      builder.Register();

      Console.WriteLine(
        "{0} custom culture registered", customCultureFile);
    }
  }
  Console.WriteLine("");
  Console.WriteLine("Registration complete.");
}

RegisterCustomCulture 应用程序仅遍历每个命令行参数。如果参数以 "/u:" 开头,则尝试注销现有的自定义区域设置;否则,它会尝试将参数作为 LDML 文件加载,然后进行注册。

值得注意的是,由于 Register 方法会写入注册表和系统的 Globalization 文件夹,因此任何使用它的代码都需要管理员权限才能执行。这意味着,如果您打算部署使用自定义区域设置的应用程序,创建自定义区域设置的应用程序(例如 RegisterCustomCulture.exe)必须具有管理员权限(但是,仅创建 CultureInfo 对象而不进行注册则不需要额外权限)。如果您使用 ClickOnce 部署 Windows 窗体应用程序,则应使用 ClickOnce 引导程序创建自定义区域设置,因为 ClickOnce 应用程序本身不会获得管理员权限。

卸载/注销自定义区域设置

可以使用静态 CultureAndRegionInfoBuilder.Unregister 方法注销自定义区域设置。

CultureAndRegionInfoBuilder.Unregister("en-GB");

此方法尝试撤销 Register 方法的两个步骤(删除注册表项并尝试删除 NLP 文件)。删除 NLP 文件的尝试可能会成功,也可能不会。Unregister 方法会检查自定义区域设置是否被其他自定义区域设置引用。在此过程中,它可能会打开 NLP 文件本身,从而导致自身失败。这就是为什么即使在重新启动计算机后,仍有可能尝试注销自定义区域设置但仍然失败的原因。在这种情况下,Unregister 方法会将文件的扩展名重命名为“tmp0”(例如,“en-GB.tmp0”)。之后不会进行清理,因此临时文件会无限期地保留在 Globalization 文件夹中。如果您的应用程序在启动时注册了自定义区域设置,然后在应用程序关闭时注销,这一点非常重要。另请注意,Unregister 需要管理员权限。

公共自定义区域设置和命名约定

您使用 .NET Framework 2.0 创建的自定义区域设置都是公共的。这意味着它们可供在安装了它们的那台计算机上运行的所有 .NET Framework 2.0 应用程序的所有用户使用。在功能上,不存在私有自定义区域设置的概念。让我们稍微考虑一下这意味着什么。注册表项是公共的;NLP 文件放置在公共位置;区域设置名称是公共的。这意味着您创建的区域设置与其他人创建的区域设置生活在同一个空间中。我们以前曾遇到过 DLL 的这种情况,通常称为 DLL 冲突。欢迎来到自定义区域设置冲突。

这里的问题是,当您创建自定义区域设置并将其安装在计算机上时,您不知道是否有人已经创建了一个具有相同名称的区域设置,或者将来是否会有人创建具有相同名称的区域设置。这对于替换区域设置尤其是一个问题,例如在第一个示例中。新的 en-GB 区域设置只是修改了短时间格式。如果其他人(可能来自另一家公司)已经在同一台计算机上创建了一个 en-GB 区域设置,那么您尝试注册 en-GB 区域设置将失败,因为该名称的自定义区域设置已存在。此时,您有两个选择:

  • 不要安装您的区域设置。尊重原始应用程序的 en-GB 区域设置,并希望它不会阻止您的应用程序正常工作。
  • 继续安装您的自定义区域设置,覆盖现有自定义区域设置。

第一种方法代表了乐观主义的定义。第二种方法将使您获得在 DLL 冲突场景中覆盖现有 DLL 的供应商所获得的声誉。或者,考虑如果您的应用程序首先安装在一台计算机上,会发生什么情况。一切正常,直到第二个应用程序用其对同一区域设置的定义覆盖您的自定义区域设置。该应用程序将正常运行。您的应用程序的最佳情况是它将失败。最坏的情况更有可能发生:您的应用程序将继续运行,但结果将是不正确的。

存在一些有限的解决方案,具体取决于您是创建替换自定义区域设置还是补充自定义区域设置。让我们从补充自定义区域设置开始。补充自定义区域设置是 .NET Framework 和操作系统尚未见过的新区域设置。最好的解决方案是通过避免问题来解决问题(这通常是我对任何问题的首选解决方案)。解决方案在于使用一种将唯一性内置到名称中的命名约定。一个简单的解决方案是将公司名称作为后缀添加到区域设置名称中。因此,如果您为孟加拉国使用的孟加拉语(即“bn-BD”)创建了一个补充自定义区域设置,而您的公司是 Acme Corporation,那么您将区域设置命名为“bn-BD-Acme”。或者,您可以采取一种更确定但完全不可读的解决方案,即添加 GUID 作为后缀,例如“bn-BD-b79a80f4-2e22-4af5-9b79-e362304b-5b10”(请注意,GUID 已被分割成最多八个字符的块)。命名约定解决方案还具有未来可扩展性的好处。变化是必然的。Microsoft 将向 Windows 添加新区域设置。如果 Microsoft 将 bn-BD 区域设置添加到 Windows 中,那么创建自定义“bn-BD”区域设置的代码将会在 CultureAndRegionInfoBuilder 构造函数中抛出异常。

CultureAndRegionInfoBuilder builder =
  new CultureAndRegionInfoBuilder("bn-BD",
  CultureAndRegionModifiers.None);

如果将区域设置名称添加后缀以使其唯一,则它不会与新区域设置或其他公司的自定义区域设置发生冲突。这种命名的缺点是,它严重滥用了 IETF 标签,而后缀替换了该标签。您必须决定哪种权衡的危害较小。

关于补充自定义区域设置名称,IETF 定义了一个前缀(“x-”或“X-”),应将其用于所谓的“私有”区域设置(例如,“x-bn-BD”)。不要被“private”一词混淆,这些区域设置仍然对所有 .NET Framework 2.0 应用程序公开可用。区别在于,通过在区域设置名称前加上“x-”前缀,就表明该区域设置是用于“私有”使用的,即一个或少量应用程序使用。此前缀还解决了如果 Microsoft 引入了相同语言/地区区域设置的问题,就不会发生冲突(因为 Microsoft 的区域设置将没有“x-”前缀)。前缀解决方案代表了一种折衷。它解决了部分问题。当然,如果另一个应用程序尝试安装相同语言/地区区域设置并使用相同的名称(包括前缀),则仍然会发生冲突。

如果您正在创建替换区域设置,例如 en-GB,您的选择非常有限。如果它确实要成为一个替换区域设置,更改名称就不是一个选项。一种选择是在 Internet 上设置或查找一个公共注册表来存放替换自定义区域设置。如果存在这样的注册表,则可以用来跟踪对现有区域设置的更改请求,并提供一个“标准”替换区域设置,供行为良好的应用程序达成一致。“标准”替换区域设置将是所有商定更改的总和。这种合作解决方案是乐观的,且不能保证,只能被视为“君子协议”。或者,您也可以直接用您的自定义区域设置覆盖对方的替换区域设置。在调用 CultureAndRegionInfoBuilder.Register 之前,您将添加以下代码:

try
{
  CultureAndRegionInfoBuilder.Unregister("en-GB");
}
catch (ArgumentException)
{
}

此代码尝试注销任何现有的 en-GB 区域设置,并忽略由现有的 en-GB 替换区域设置引起的任何异常。如果您选择此方法,请准备好收到一些抱怨。唯一有保证的解决方案是使用补充自定义区域设置而不是替换自定义区域设置,并使用前面建议的命名约定来避免冲突。然后,自定义区域设置将被命名为“x-en-GB”或“en-GB-Acme”,而不是“en-GB”。这种解决方案的明显缺点是自定义区域设置不再是替换自定义区域设置。这意味着您的应用程序需要采取某些步骤来确保使用 x-en-GBen-GB-Acme 区域设置,而不是 en-GB 区域设置。

无论您如何解决这个问题,都应该注意自定义区域设置名称的限制。自定义区域设置名称的最大长度为 84 个字符,名称中的每个“标签”最多为 8 个字符。“标签”是由连字符(“-”)或下划线(“_”)分隔的字母和数字块。因此,“en-GB-AcmeSoftware”这个名称无效,因为“AcmeSoftware”标签长 12 个字符。您可以通过使用连字符或下划线分隔单词来解决此问题(例如,“en-GB-Acme-Software”或“en-GB-Acme_Software”)。

补充替代自定义区域设置

“补充替代”自定义区域设置听起来确实有些矛盾。我使用这个术语来描述一个补充自定义区域设置,该区域设置用于替换现有区域设置,而不实际替换它。在“公共自定义区域设置和命名约定”一节中,我讨论了替换自定义区域设置的问题,并提出了一种解决方案:与其创建替换自定义区域设置,不如创建一个与所需替换自定义区域设置在各个方面都相同的全新补充自定义区域设置。创建与现有自定义区域设置相同的自定义区域设置可以通过 LoadDataFromCultureInfoLoadDataFromRegionInfo 方法轻松实现。这是创建 en-GB-Acme 补充替代自定义区域设置的代码:

CultureInfo cultureInfo = new CultureInfo("en-GB");
RegionInfo regionInfo = new RegionInfo(cultureInfo.Name);

CultureAndRegionInfoBuilder builder =
  new CultureAndRegionInfoBuilder("en-GB-Acme",
  CultureAndRegionModifiers.None);

// load in the data from the existing culture and region
builder.LoadDataFromCultureInfo(cultureInfo);
builder.LoadDataFromRegionInfo(regionInfo);

// make custom changes to the culture
builder.GregorianDateTimeFormat.ShortTimePattern = "HH:mm tt";

builder.Register();

LoadDataFromCultureInfoLoadDataFromRegionInfo 方法分别从 CultureInfoRegionInfo 对象中的数据设置 CultureAndRegionInfoBuilder 属性。表 11.3 和 11.4 显示了这些方法设置的属性。

表 11.3 CultureAndRegionInfoBuilder.LoadDataFromCultureInfo 设置的属性

CultureAndRegionInfoBuilder 属性

AvailableCalendars

CultureInfo.OptionalCalendars(仅限特定区域设置)

CompareInfo

CultureInfo.CompareInfo(仅限补充)

ConsoleFallbackUICulture

CultureInfo.GetConsoleFallbackUICulture()

CultureEnglishName

CultureInfo.EnglishName

CultureNativeName

CultureInfo.NativeName

GregorianDateTimeFormat

CultureInfo.DateTimeFormat(仅限特定区域设置)

IetfLanguageTag

CultureInfo.IetfLanguageTag

IsRightToLeft

CultureInfo.TextInfo.IsRightToLeft

KeyboardLayoutId

CultureInfo.KeyboardLayoutId

NumberFormat

CultureInfo.NumberFormat(仅限特定区域设置)

Parent

CultureInfo.Parent

TextInfo

CultureInfo.TextInfo(仅限补充)

ThreeLetterISOLanguageName

CultureInfo.ThreeLetterISOLanguageName

ThreeLetterWindowsLanguageName

CultureInfo. ThreeLetterWindowsLanguageName(仅限补充)

TwoLetterISOLanguageName

CultureInfo.TwoLetterISOLanguageName

表 11.4 CultureAndRegionInfoBuilder.LoadDataFromRegionInfo 设置的属性

CultureAndRegionInfoBuilder 属性

CurrencyEnglishName

RegionInfo.CurrencyEnglishName

CurrencyNativeName

RegionInfo.CurrencyNativeName

GeoId

RegionInfo.GeoId

IsMetric

RegionInfo.IsMetric

ISOCurrencySymbol

RegionInfo.ISOCurrencySymbol

RegionEnglishName

RegionInfo.EnglishName

RegionNativeName

RegionInfo.NativeName

ThreeLetterISORegionName

RegionInfo.ThreeLetterISORegionName

ThreeLetterWindowsRegionName

RegionInfo.ThreeLetterWindowsRegionName(仅限补充)

TwoLetterISORegionName

RegionInfo.TwoLetterISORegionName

请注意,CompareInfoTextInfoThreeLetterWindowsLanguageNameThreeLetterWindowsRegionName 属性仅在区域设置为补充区域设置时(在本例中是这样)才由这些方法设置。对于替换区域设置,这些属性在 CultureAndRegionInfoBuilder 构造函数中设置,并且被视为不可变的。因此,如果您为替换区域设置分配这些属性的值,它们将抛出异常。这就是为什么您无法创建一个仅更改默认排序顺序的替换自定义区域设置的原因。以下代码尝试为 es-ES (西班牙语(西班牙)) 创建替换区域设置,而唯一的区别是排序顺序为 Traditional0x0000040A)而不是默认的 International

CultureAndRegionInfoBuilder builder =
  new CultureAndRegionInfoBuilder("es-ES",
  CultureAndRegionModifiers.Replacement);

builder.CompareInfo = CompareInfo.GetCompareInfo(0x0000040A);

builder.Register();

赋给 CompareInfo 会抛出 NotSupportedException。因此,使用补充自定义区域设置而不是替换区域设置的好处是,这些属性可以具有与原始区域设置不同的值。

除了表 11.3 中的公共属性外,LoadDataFromCultureInfo 方法还为 DurationFormatsFontSignaturePaperSize 设置内部值。这些值用于 Save 方法创建的 LDML 文件。LoadDataFromCultureInfo 方法是设置这些属性的唯一方法。

生成的补充自定义区域设置不具备替换自定义区域设置的完整功能。一个区别在于 CultureInfo.DisplayName 属性的行为。此属性内置了一定程度的智能。DisplayName 属性会为内置 .NET Framework 和 Windows 区域设置返回 CurrentCulture 的区域设置名称。这意味着当 CurrentCulture 为“en-US”时,fr-FR 区域设置的 DisplayName 是“French (France)”,但当 CurrentCulture 为“fr-FR”和“de-DE”时,分别为“Français (France)”和“Französisch (Frankreich)”,并且已安装了法语和德语 .NET Framework 语言包。替换区域设置会采用相同的功能,因为 .NET Framework 可以识别该区域设置是已知的。对于补充自定义区域设置,相同的功能不可用,因为 .NET Framework 无法也“不应该”猜测正确的 DisplayName。因此,补充自定义区域设置的 DisplayName 与其本地名称相同。表 11.5 显示了 tr-TR (土耳其语(土耳其)) 自定义区域设置的行为差异。

表 11.5 替换和补充自定义区域设置的 CultureInfo.DisplayName 行为差异

当前区域设置

tr -TR 替换区域设置显示名称

tr -TR 补充区域设置显示名称

en

US Turkish (Turkey)

Türkçe (Türkiye)

tr

TR Türkçe (Türkiye)

Türkçe (Türkiye)

RegionInfo.DisplayName 的行为差异同样适用于 RegionInfo.DisplayName

自定义区域设置 Locale ID

补充自定义区域设置与替换自定义区域设置的另一个区别是它们的区域设置 ID(即 CultureInfo.LCID)。CultureAndRegionInfoBuilder.LCID 是只读的。替换自定义区域设置使用与其替换的区域设置相同的区域设置 ID。这很有用,因为它意味着原始区域设置没有后门。在下面的示例中,这两行都将导致相同的 CultureInfo

CultureInfo cultureInfo1 = new CultureInfo("en-GB");
// The LCID for en-GB is 2057
CultureInfo cultureInfo2 = new CultureInfo(2057);

在几乎所有情况下,这种行为都是可取的。但是,这意味着无法创建原始被替换区域设置的 CultureInfo,即使您想这样做。如果绝对有必要,您需要将替换自定义区域设置保存到 LDML 文件,将其注销,创建原始 CultureInfo 对象,提取所需信息,然后再次加载 LDML 文件并注册替换自定义区域设置。

所有补充自定义区域设置都具有相同的区域设置 ID:0x10004096)。因此,“bn-BD(孟加拉语(孟加拉国))的区域设置 ID 为 4096en-GB-Acme 的区域设置 ID 也为 4096。考虑以下两个区域设置的相等性测试:

CultureInfo cultureInfo1 = new CultureInfo("bn-BD");
CultureInfo cultureInfo2 = new CultureInfo("en-GB-Acme");
if (! cultureInfo1.Equals(cultureInfo2))
  MessageBox.Show("CultureInfo objects are not the same");

CultureInfo.Equals 方法报告这些区域设置不相等,即使它们的 LCID 相同。在 .NET Framework 2.0 中,两个 CultureInfo 对象被视为相等,如果它们是同一个对象,或者它们的 NamesCompareInfo 对象相同。这与 .NET Framework 1.1 的实现形成对比,后者仅仅基于 LCID 的比较,而不是对象引用或名称。

另请注意,由于所有补充自定义区域设置共享相同的 LCID,因此无法使用 LCID 创建补充自定义区域设置。以下代码将导致 ArgumentException("Culture ID 4096 (0x1000) is not a supported culture"

CultureInfo cultureInfo1 = new CultureInfo(4096);

这就是为什么您应该将 LCID 视为遗留功能或与 Win32 API 一起使用。您还应该从这一点得出结论,如果您在数据库或配置文件中存储区域设置标识符,则您的方法应始终能够存储区域设置名称而不是区域设置 LCID 以用于自定义区域设置。回想一下第 6 章“备用排序顺序”一节,.NET Framework 2.0 支持使用字符串标识符(例如,“es-ES_tradnl”)而不是 LCID 来创建具有备用排序顺序的区域设置;显而易见,在使用 .NET Framework 2.0 时,您应始终使用字符串而不是整数来存储区域设置标识符。如果您想在应用程序中强制执行此操作,请参见第 13 章“使用 FxCop 测试国际化”中的“CultureInfo 不能从 LCID 构造”和“RegionInfo 不能从 LCID 构造”Fxcop 规则。

在我们离开备用排序顺序的主题之前,值得指出的是,由于自定义区域设置机制基于区域设置名称而不是区域设置 LCID,因此无法为具有备用排序顺序的区域设置创建替换自定义区域设置。但是,您可以为备用排序顺序创建一个“补充替代”自定义区域设置。

// create the es-ES culture with the Traditional sort order
CultureInfo cultureInfo = new CultureInfo("es-ES-Tradnl");
RegionInfo regionInfo = new RegionInfo(cultureInfo.Name);

CultureAndRegionInfoBuilder builder =
  new CultureAndRegionInfoBuilder("es-ES-Tradnl-Acme",
  CultureAndRegionModifiers.None);

// load in the data from the existing culture and region
builder.LoadDataFromCultureInfo(cultureInfo);
builder.LoadDataFromRegionInfo(regionInfo);

// make custom changes to the culture
...
...

builder.Register();

自定义区域设置的父级和子级

如您所知,CultureInfo 对象存在一个层次结构,其中特定区域设置(例如,“en-US”)回退到中性区域设置(例如,“en”),后者又回退到不变区域设置。这种层次结构通过 CultureInfo.Parent 属性体现出来。自定义区域设置适合此层次结构,但它们不受限于只有三个级别的区域设置的现有模式,也不受限于特定区域设置具有父级中性区域设置的概念。让我们看两个例子。第一个是 en-GB 自定义区域设置的层次结构,其中 Parent 属性未在代码中显式设置,而是由 LoadDataFromCultureInfo 方法处理:

BuildCulture("English (United Kingdom) Acme", 
  "en-GB-Acme", "en-GB");

BuildCulture("English (United Kingdom) Acme Child", 
  "en-GB-Acme-Child", "en-GB-Acme");

BuildCulture("English (United Kingdom) Acme Grandchild",
  "en-GB-Acme-GrandC", "en-GB-Acme-Child");


private void BuildCulture(string englishName,
  string cultureName, string loadFromCultureName)
{
  CultureInfo cultureInfo = new CultureInfo(loadFromCultureName);

  RegionInfo regionInfo = new RegionInfo(cultureInfo.Name);

  CultureAndRegionInfoBuilder builder =
    new CultureAndRegionInfoBuilder(cultureName,
    CultureAndRegionModifiers.None);

  // add data from the culture
  builder.LoadDataFromCultureInfo(cultureInfo);
  // add data from the region
  builder.LoadDataFromRegionInfo(regionInfo);
  // set the culture's English name
  builder.CultureEnglishName = englishName;

  builder.Register();

}

此代码的结果可能与您期望的不符。图 11.1 显示了生成的层次结构。

图 11.1ParentLoadDataFromCultureInfo 设置时,自定义区域设置的层次结构

LoadDataFromCultureInfo 方法将 Parent 属性设置为 CultureInfo.Parent,因此在第一次调用 BuildCulture 时,en-GB-Acme 的父级是 en (英语)。在第二次调用 BuildCulture 时,en-GB-Acme-Child 的父级也是 en (英语),因为它继承了 en-GB-Acme 的父级。如果您希望创建父级是正在读取数据的区域设置的层次结构,您必须显式设置 CultureAndRegionInfoBuilderParent。在调用 LoadDataFromCultureInfo 之后添加以下行:

builder.Parent = cultureInfo;

结果是图 11.2 中所示的层次结构。

图 11.2Parent 被显式设置时,自定义区域设置的层次结构

现在让我们从另一个角度来看这个问题。CultureInfo.CreateSpecificCulture 方法根据特定区域设置(在这种情况下,它仅返回相同的特定区域设置)或中性区域设置来创建特定区域设置。因此,如果您将法语区域设置传递给 CreateSpecificCulture,它将返回一个新的区域设置 French (France);同样,德语返回 German (Germany)。这对自定义区域设置开发人员很有意义,因为这种行为无法指定。这有多重要可能取决于您创建的是替换自定义区域设置还是补充自定义区域设置。如果您为“en”创建了一个替换自定义区域设置,您将无法将特定区域设置从“en-US”更改为,例如,“en-GB”。这可能是一个非常有用的行动方向。考虑到您正在为英国的诺丁汉森林足球俱乐部创建一个网站。如果用户的浏览器语言设置为“en”,那么使用 CultureInfo.CreateSpecificCulture 对您来说是没有帮助的,因为它将返回“en-US”的区域设置,这对于几乎所有您的访问者来说都是错误的(而“en-GB”会更合适)。对于加拿大的多伦多枫叶队网站也是如此,如果从 French 调用 CreateSpecificCulture,它将返回 French (France) 而不是更有用的 French (Canada)

同样,如果您为例如孟加拉语(“bn”)创建一个补充自定义区域设置,您将无法指定特定区域设置是什么(例如,“Bengali (Bangladesh)”)。

自定义区域设置的支持

不仅 .NET Framework 2.0 支持自定义区域设置,Microsoft 的 .NET Framework 2.0 开发工具也支持。 .NET Framework 2.0 使您能够使用 CultureInfo.GetCultures 获取自定义区域设置列表。

foreach (CultureInfo cultureInfo in
  CultureInfo.GetCultures(CultureTypes.UserCustomCulture))
{
  listBox1.Items.Add(
    cultureInfo.Name + " (" + cultureInfo.DisplayName + ")");
}

CultureTypes 的值为 UserCustomCulture。您可以使用自定义区域设置的 CultureTypes 属性来测试它是否是一个自定义区域设置。

CultureInfo cultureInfo = new CultureInfo("en-GB");
if ((CultureTypes.UserCustomCulture & cultureInfo.CultureTypes)
  != (CultureTypes)0)
  Text = "User Custom Culture";
else
  Text = "Not User Custom Culture";

Visual Studio 2005 的窗体设计器也支持自定义区域设置。当您通过将 Form.Localizable 设置为 true 来本地化窗体时,Form.Language 组合框将包含自定义区域设置。

组合框是通过 CultureInfo.DisplayName 填充的。请记住,对于补充自定义区域设置,CultureInfo.DisplayName 始终是 CultureInfo.NativeName,而不是 CultureInfo.EnglishName,因此您的自定义区域设置可能不在您期望的排序列表中。

与 Visual Studio 2005 一样,WinRes(Windows 资源本地化编辑器)也支持自定义区域设置,并允许打开和保存自定义区域设置的窗体资源。

ClickOnce 在 Visual Studio 和 Mage(Manifest Generation and Editing Tool)中都支持自定义区域设置。在 Visual Studio 中,在 ClickOnce Publish 属性(在解决方案资源管理器中,双击属性,然后选择“发布”选项卡)中,单击“选项...”按钮;您可以设置“发布语言”(参见图 11.3)。Mage 也以同样的方式支持自定义区域设置。

图 11.3 将 ClickOnce 发布语言设置为自定义区域设置

如果您希望 ClickOnce 引导程序使用自定义区域设置的语言,则必须在 Bootstrapper\Engine 文件夹下创建一个名为自定义区域设置名称(例如,“bn-BD”)的新文件夹,其中包含一个带有翻译字符串的 setup.xml 文件。您可以从 Bootstrapper\Engine\en 文件夹复制 setup.xml 作为自定义区域设置的起点。

对自定义区域设置的支持仅限于 .NET Framework。因此,“区域和语言选项”对话框不包含自定义区域设置。如果您以此作为设置用户 CurrentCultureCurrentUICulture 偏好的方式,则用户将无法使用补充自定义区域设置。同样,其他非基于 .NET Framework 2.0 的工具将不会识别自定义区域设置,因此,例如,可能无法使用某些第三方翻译工具。

ASP.NET 应用程序可以使用自定义区域设置而无需进行任何修改。如果用户在浏览器中将语言偏好设置为自定义区域设置,并且 CultureUICulture 标签设置为 Auto,则将自动使用自定义区域设置。此外,您可以通过在网站管理工具文件夹中创建新的 resx 文件,轻松地为您的自定义区域设置本地化 ASP.NET 2 网站管理工具。有关更多详细信息,请参阅第 5 章“ASP.NET 特性”。

补充自定义区域设置

补充区域设置是 .NET Framework 和操作系统中新出现的区域设置。本章中介绍了许多补充自定义区域设置的示例。我们将从最大的挑战开始:从头开始创建补充自定义区域设置,而没有任何现有的 CultureInfoRegionInfo 可供参考。在此示例中,我们将创建一个孟加拉国(也称为 Bangla)的区域设置。第二个示例是创建一个全新的补充自定义区域设置,它是一个伪本地化自定义区域设置。

孟加拉语(孟加拉国)自定义区域设置

在撰写本文时,孟加拉语(孟加拉国)区域设置(我们将其标记为“bn-BD”)既不为 .NET Framework 所知,也不为任何版本的 Windows 所知。然而,Windows Vista 包含了区域设置中性的孟加拉语,但仅在 Windows Vista 中可用,并且不是特定区域设置。但是,正如已经提到的,这种情况很可能不会持续下去,并且“bn-BD”区域设置将来可能会出现在某个版本的 Windows 中。尽管如此,这些未来的事件并不能使此示例失效。请考虑在此时,您将不得不在迫使所有用户升级到新版本的 Windows(不一定可行)和使用一个适用于所有 Windows 版本的自定义区域设置之间做出选择。后者是更实际的选择。关于您的区域设置命名约定的相同警告也适用于这种情况,因此,尽管您可能希望“个性化”您的 bn-BD 区域设置名称(例如,“bn-BD-Acme”),但在此示例中我出于简单性使用了“bn-BD”。最后,如果您在 Windows Vista 之前的任何版本中运行此示例,您应该安装对复杂脚本的支持才能看到孟加拉语脚本。

以下代码创建了 孟加拉语(孟加拉国)自定义区域设置:

public static void RegisterBengaliBangladeshCulture()
{
  CreateBengaliBangladeshCultureAndRegionInfoBuilder().Register();
}
public static CultureAndRegionInfoBuilder
       CreateBengaliBangladeshCultureAndRegionInfoBuilder()
{
  CultureAndRegionInfoBuilder builder =
    new CultureAndRegionInfoBuilder("bn-BD",
    CultureAndRegionModifiers.None);

  // there is no neutral Bengali culture to set the parent to
  builder.Parent = CultureInfo.InvariantCulture;

  builder.CultureEnglishName = "Bengali (Bangladesh)";
  builder.CultureNativeName = " (Bldesh)";
  builder.ThreeLetterISOLanguageName = "ben";
  builder.ThreeLetterWindowsLanguageName = "ben";
  builder.TwoLetterISOLanguageName = "bn";

  builder.RegionEnglishName = "Bangladesh";
  builder.RegionNativeName = "Bldesh";
  builder.ThreeLetterISORegionName = "BGD";
  builder.ThreeLetterWindowsRegionName = "BGD";
  builder.TwoLetterISORegionName = "BD";

  builder.IetfLanguageTag = "bn-BD";

  builder.IsMetric = true;
  builder.KeyboardLayoutId = 1081;
  builder.GeoId = 0x17; // Bangladesh

  builder.GregorianDateTimeFormat =
    CreateBangladeshDateTimeFormatInfo();

  builder.NumberFormat = CreateBangladeshNumberFormatInfo();
  builder.CurrencyEnglishName = "Bangladesh Taka";
  builder.CurrencyNativeName = "Bangladesh Taka";
  builder.ISOCurrencySymbol = "BDT";

  builder.TextInfo = CultureInfo.InvariantCulture.TextInfo;

  builder.CompareInfo = CultureInfo.InvariantCulture.CompareInfo;

  return builder;
}

bn-BD 的父级是不变区域设置。您可能希望分两步创建此区域设置,首先创建一个中性孟加拉语区域设置,然后创建一个特定的 孟加拉语(孟加拉国)区域设置。有一些值,您应该查找标准值:

  • 区域设置名称 bn-BD 显然至关重要,您应该查找用于此目的的现有代码(如果有)。语言代码列表可以在 此处找到。或者,可以从 此处购买官方 ISO 列表(搜索“639”)。国家/地区代码列表可以在 此处找到。或者,可以从 此处购买官方 ISO 列表(搜索“3166”)。
  • GeoId 值可从 Microsoft 的地理位置表中获得。如果您的地理区域未在此表中列出,您将不得不留空 ID 或选择一个未使用的数字(当然,该数字随后可能被用于完全不同的地理区域,这将使您的选择无效)。

CultureAndRegionInfoBuilder.NumberFormatInfoCreateBangladeshNumberFormatInfo 方法分配。

private static NumberFormatInfo CreateBangladeshNumberFormatInfo()
{
  NumberFormatInfo numberFormatInfo = new NumberFormatInfo();
  numberFormatInfo.CurrencyDecimalDigits = 2;
  numberFormatInfo.CurrencyDecimalSeparator = ".";
  numberFormatInfo.CurrencyGroupSeparator = ",";
  numberFormatInfo.CurrencyGroupSizes = new int[] { 3, 2 };
  numberFormatInfo.CurrencyNegativePattern = 12;
  numberFormatInfo.CurrencyPositivePattern = 2;
  numberFormatInfo.CurrencySymbol = "BDT";
  numberFormatInfo.DigitSubstitution = DigitShapes.None;
  numberFormatInfo.NaNSymbol = "NaN";
  numberFormatInfo.NativeDigits = new string[]
    { };
  numberFormatInfo.NegativeInfinitySymbol = "-Infinity";
  numberFormatInfo.NegativeSign = "-";
  numberFormatInfo.NumberDecimalDigits = 2;
  numberFormatInfo.NumberDecimalSeparator = ".";
  numberFormatInfo.NumberGroupSeparator = ",";
  numberFormatInfo.NumberGroupSizes = new int[] { 3, 2 };
  numberFormatInfo.NumberNegativePattern = 1;
  numberFormatInfo.PercentDecimalDigits = 2;
  numberFormatInfo.PercentDecimalSeparator = ".";
  numberFormatInfo.PercentGroupSeparator = ",";
  numberFormatInfo.PercentGroupSizes = new int[] { 3, 2 };
  numberFormatInfo.PercentNegativePattern = 0;
  numberFormatInfo.PercentPositivePattern = 0;
  numberFormatInfo.PercentSymbol = "%";
  numberFormatInfo.PerMilleSymbol = "‰";
  numberFormatInfo.PositiveInfinitySymbol = "Infinity";
  numberFormatInfo.PositiveSign = "+";
  return numberFormatInfo;
}

CultureAndRegionInfoBuilder.DateTimeFormatInfoCreateBangladeshDateTimeFormatInfo 方法分配。

private static DateTimeFormatInfo CreateBangladeshDateTimeFormatInfo()
{
  Calendar calendar =
    new GregorianCalendar(GregorianCalendarTypes.Localized);

  DateTimeFormatInfo dateTimeFormatInfo = new DateTimeFormatInfo();

  dateTimeFormatInfo.Calendar = calendar;

  dateTimeFormatInfo.AbbreviatedDayNames = new string[]
    { };
  dateTimeFormatInfo.DayNames = new string[] { "",
     };
  dateTimeFormatInfo.ShortestDayNames = new string[]
    { };

  dateTimeFormatInfo.AbbreviatedMonthNames = new string[]
    { ,
    , "" };
  dateTimeFormatInfo.MonthNames = new string[]
    { ,
     };

  dateTimeFormatInfo.AbbreviatedMonthGenitiveNames =
    new string[] { ,
    , "" };
  dateTimeFormatInfo.MonthGenitiveNames = new string[] { 
    ,
    "" };

  dateTimeFormatInfo.AMDesignator = "";
  dateTimeFormatInfo.CalendarWeekRule = CalendarWeekRule.FirstDay;
  dateTimeFormatInfo.DateSeparator = "-";
  dateTimeFormatInfo.FirstDayOfWeek = DayOfWeek.Monday;
  dateTimeFormatInfo.FullDateTimePattern = "dd MMMM yyyy HH:mm:ss";
  dateTimeFormatInfo.LongDatePattern = "dd MMMM yyyy";
  dateTimeFormatInfo.LongTimePattern = "HH:mm:ss";
  dateTimeFormatInfo.MonthDayPattern = "dd MMMM";
  dateTimeFormatInfo.PMDesignator = "";
  dateTimeFormatInfo.ShortDatePattern = "dd-MM-yyyy";
  dateTimeFormatInfo.ShortTimePattern = "HH:mm";
  dateTimeFormatInfo.TimeSeparator = ":";
  dateTimeFormatInfo.YearMonthPattern = "MMMM, yyyy";

  return dateTimeFormatInfo;
}

注意 - 必须先将 Calendar 对象分配给 DateTimeFormatInfo.Calendar 属性,然后再分配星期和月份名称,因为设置 Calendar 属性会重置这些值。


现在,孟加拉语(孟加拉国)区域设置可以像其他任何 .NET Framework 区域设置一样使用。

伪本地化自定义区域设置

伪本地化自定义区域设置是另一个自定义区域设置,它是从零开始创建的,不依赖于任何现有的区域设置或区域信息。此自定义区域设置的目的是支持第 9 章中描述的伪本地化,开发人员和测试人员可以使用非开发人员自己的区域设置,可以测试应用程序是否已全球化和本地化,并且仍然能够使用应用程序而无需学习另一种语言。此处未显示伪本地化自定义区域设置的完整代码,因为它与上一个示例相同,只是值不同。

伪本地化自定义区域设置值本身的重要性仅在于它们不能与现有区域设置的值相同。这允许开发人员和测试人员观察到全球化和本地化正在发生。这比乍一看要棘手。第一个问题是,在选择合适的语言和区域代码用于伪本地化区域设置时,您应该避免使用现有的代码。您可能会考虑使用“ps-PS”(表示 Pseudo (Pseudo)),但“ps”语言代码和“PS”区域代码已被占用。请参阅孟加拉语(孟加拉国)自定义区域设置中的链接,以避免选择已被占用的标识符。我选择了“pd-PD”,因为在撰写本文时这些代码仍然是免费的。但是,为了确保您选择的未来安全性,最安全的解决方案是选择一个不符合 ISO 规范的代码(例如,“p1-P1”使用数字,这是不符合这些规范的)。使用这种方法,您可以确保如果它不符合规范,那么代码将永远不会被其他人使用。

伪本地化区域设置的许多值都很容易发明。

builder.CultureEnglishName = "PseudoLanguage (PseudoRegion)";
builder.CultureNativeName =
  "[!!! () !!!]";
builder.ThreeLetterISOLanguageName = "psd";
builder.ThreeLetterWindowsLanguageName = "psd";
builder.TwoLetterISOLanguageName = "pd";

builder.RegionEnglishName = "PseudoRegion";
builder.RegionNativeName = "[!!! !!!]";
builder.ThreeLetterISORegionName = "PSD";
builder.ThreeLetterWindowsRegionName = "PSD";
builder.TwoLetterISORegionName = "PD";

builder.IetfLanguageTag = "pd-PD";

但是,您需要找到正确的平衡:您必须使用与英语足够不同的值,以清楚地表明应用程序未使用的默认区域设置,但您必须使用足够可理解的值,以使应用程序仍然可用。请考虑以下两个货币字符串,它们使用 123456789.123456.ToString("C") 转换为字符串。

$123,456,789.12
1'2'3'4'5'6'7'8'9@1235 ~

第一个使用“en-US”区域设置,第二个使用“pd-PD”区域设置。第二个清楚地表明应用程序已全球化,但它仍然可识别为货币吗?小数分隔符是“@”而不是“.”;千位分隔符是“'”而不是“,”;千位分组大小是 1 而不是 3;小数位数是 4 而不是 2;货币符号是“~”而不是“$”;并且货币符号放在右侧而不是左侧。在测试全球化方面,这得分 10 分,但应用程序仍然可用吗?

我还采取了这样的态度,即 DateTimeFormatInfo 中使用的星期和月份名称不应进行伪本地化。例如:

dateTimeFormatInfo.DayNames = new string[] {
  "*Sunday*", "*Monday*", "*Tuesday*", "*Wednesday*",
  "*Thursday*", "*Friday*", "*Saturday*" };

(名称由星号分隔。)您可能期望名称是这样的“伪本地化”:

dateTimeFormatInfo.DayNames = new string[] {
  "", "", "", "",
  "", "", "" };

这样做的原因是,我希望能够清楚地看到星期和月份名称是从适当的 DateTimeFormatInfo 对象中获取的,而不是从资源程序集中获取的。换句话说,如果用户看到 "",,您可以确信应用程序*已经*本地化了,但您不知道它是*如何*本地化的。文本可能同样来自对 ResourceManager.GetString("Sunday") 的调用,并且无法通过视觉方式区分,如果 DateTimeFormatInfo 中的文本与伪本地化资源相同。

有了伪本地化区域设置后,您可能希望更新第 9 章中介绍的 PseudoTranslation 类,以使用新的区域设置而不是以前劫持的区域设置。

public class PseudoTranslation
{
  private static CultureInfo cultureInfo =
    new CultureInfo("pd-PD");
  public static CultureInfo CultureInfo
  {
    get {return cultureInfo;}
    set {cultureInfo = value;}
  }
}

区域设置生成器应用程序示例 (CultureSample)

Visual Studio 2005 文档中的一个示例应用程序称为 Culture Builder Application Sample(也称为 CultureSample),它专门用于创建自定义区域设置。启动文档并搜索“Culture Builder Sample”。打开 CultureSampleCS.slnCultureSampleVB.sln Windows 窗体应用程序并生成它,您将获得 CultureBuilderSample.exe,这是一个用于构建自定义区域设置的 UI(参见图 11.4)。

图 11.4 用于构建自定义区域设置的 CultureBuilderSample 应用程序

单击“New Culture”。输入区域设置名称后,您可以使用对话框(参见图 11.5)指定区域设置的格式选项,该对话框模仿“区域和语言选项”的“自定义”对话框。

图 11.5 用于构建自定义区域设置的 CultureBuilderSample 应用程序

单击“OK”保存自定义区域设置。CultureBuilderSample 也可用于合并区域设置和创建替换区域设置。

合并区域设置

创建自定义区域设置的常见原因之一是创建语言和区域的组合,其中语言和区域是已知的,但尚未配对。创建此类组合区域设置的好处是,您可以引用对目标市场重要的语言和区域,但这些语言和区域在 .NET Framework 或操作系统中未定义。表 11.1 显示了一些示例组合,“es-US”(西班牙语(美国))是最受欢迎的之一。CultureAndRegionInfoBuilderHelper 类(包含在此书的源代码中)可以处理组合两个区域设置的繁重工作,并可按如下方式使用:

CultureAndRegionInfoBuilder builder = 
  CultureAndRegionInfoBuilderHelper.
  CreateCultureAndRegionInfoBuilder(
  new CultureInfo("es-ES"), new RegionInfo("en-US"));

builder.Register();

CultureAndRegionInfoBuilderHelper.CreateCultureAndRegionInfoBuilder 方法根据“语言”CultureInfo(“es-ES”)和“区域”RegionInfo(“en-US”)创建一个新的 CultureAndRegionInfoBuilder。然后使用新对象来注册区域设置或保存区域设置。CreateCultureAndRegionInfoBuilder 有各种重载,可以接受相同主题的变体。

“拼接”两个区域设置的过程并不像您想象的那么直接。表 11.6 显示了 CultureAndRegionInfoBuilder 属性,以及它们的来源和使用 Spanish (United States) 示例的实际值。

表 11.6 CultureAndRegionInfoBuilder 属性和 Spanish (United States) 区域设置的值

CultureAndRegionInfoBuilder 属性

es-US 值

AvailableCalendars

US CultureInfo. OptionalCalendars

 

CompareInfo

Spanish CultureInfo.CompareInfo

 

ConsoleFallbackUICulture

Spanish CultureInfo. GetConsole-FallbackUICulture()

 

CultureEnglishName

Spanish Neutral CultureInfo. TwoLetterISOLanguageName, US RegionInfo.EnglishName

Spanish (United States)

CultureName

Spanish CultureInfo.Tw oLetterISOLanguageName, US RegionInfo. TwoLetterISORegionName

es-US

CultureNativeName

Spanish Neutral CultureInfo.NativeName, US RegionInfo.DisplayName(西班牙语)

español (Estados Unidos)

CultureTypes

N/A(只读)

—(只读)

CurrencyEnglishName

US RegionInfo. CurrencyEnglishName

US Dollar

CurrencyNativeName

US RegionInfo. CurrencyDisplayName(西班牙语)

US Dollar

GeoId

US RegionInfo.Geold

244 (US)

GregorianDateTimeFormat

US CultureInfo.DateTimeFormat

US DateTimeFormat(带西班牙语名称)

IetfLanguageTag

Spanish CultureInfo. TwoLetterISOLanguageName, US RegionInfo. TwoLetterISORegionName

es-US

IsMetric

US RegionInfo.IsMetric

false

ISOCurrencySymbol

US RegionInfo.ISOCurrencySymbol

USD

IsRightToLeft

Spanish CultureInfo.TextInfo. IsRightToLeft

false

KeyboardLayoutId

Spanish Neutral CultureInfo.KeyboardLayoutId

1034

LCID

N/A(只读)

ox1000 (4096)

NumberFormat

US CultureInfo.NumberFormat

US CultureInfo. NumberFormat

Parent

Spanish Neutral CultureInfo

es

RegionEnglishName

US RegionInfo.EnglishName

United States

RegionName

N/A(只读)

—(只读)

RegionNativeName

US RegionInfo.DisplayName(西班牙语)

estados Unidos

TextInfo

Spanish Neutral CultureInfo.TextInfo

Spanish Neutral CultureInfo. TextInfo

ThreeLetterISOLanguageName

Spanish CultureInfo. ThreeLetterISOLanguageName

spa

ThreeLetterISORegionName

US RegionInfo. ThreeLetterISORegionName

USA

ThreeLetterWindows LanguageName

Spanish CultureInfo. ThreeLetterWindows LanguageName

ESN

ThreeLetterWindowsRegionName

US RegionInfo. ThreeLetterWindowsRegionName

USA

TwoLetterISOLanguageName

Spanish CultureInfo. TwoLetterISOLanguageName

es

TwoLetterISORegionName

US RegionInfo. TwoLetterISORegionName

US

新区域设置是语言和区域的组合,但文化中使用的许多名称需要本地化。虽然新区域设置使用该区域的日历,但该日历的星期和月份名称必须是指定语言(即西班牙语),而不是日历来源的语言(即英语)。LoadDataFromRegionInfo 方法在这种情况下非常有用,但 LoadDataFromCultureInfo 则不然。CultureAndRegionInfoBuilderHelper.CreateCultureAndRegionInfoBuilder 方法如下所示:

public static CultureAndRegionInfoBuilder
  CreateCultureAndRegionInfoBuilder(
  CultureInfo languageCultureInfo,
  RegionInfo regionInfo,
  string cultureName)
{
  if (cultureName == null || cultureName == String.Empty)
    // the culture name is blank so construct a default name
    cultureName =
      languageCultureInfo.TwoLetterISOLanguageName + "-" +
      regionInfo.TwoLetterISORegionName;

  CultureInfo languageNeutralCultureInfo =
    GetNeutralCulture(languageCultureInfo);

  CultureInfo regionCultureInfo = new CultureInfo(regionInfo.Name);

  CultureAndRegionInfoBuilder builder = 
    new CultureAndRegionInfoBuilder(
    cultureName, CultureAndRegionModifiers.None);

  builder.LoadDataFromCultureInfo(regionCultureInfo);
  builder.LoadDataFromRegionInfo(regionInfo);

  builder.Parent = languageNeutralCultureInfo;

  builder.CompareInfo = languageCultureInfo.CompareInfo;
  builder.TextInfo = languageCultureInfo.TextInfo;

  builder.IetfLanguageTag = cultureName;

  builder.RegionNativeName = GetNativeRegionName(
    regionInfo, languageCultureInfo);

  builder.CultureEnglishName =
    languageNeutralCultureInfo.EnglishName + " (" + 
    regionInfo.EnglishName + ")";

  builder.CultureNativeName =
    languageNeutralCultureInfo.NativeName + " (" +
    builder.RegionNativeName + ")";

  builder.CurrencyNativeName = GetNativeCurrencyName(
    regionInfo, languageCultureInfo);

  // copy the native month and day names
  DateTimeFormatInfo builderDtfi =
    builder.GregorianDateTimeFormat;

  DateTimeFormatInfo languageDtfi =
    languageCultureInfo.DateTimeFormat;

  builderDtfi.AbbreviatedDayNames =
    languageDtfi.AbbreviatedDayNames;

  builderDtfi.AbbreviatedMonthGenitiveNames =
    languageDtfi.AbbreviatedMonthGenitiveNames;

  builderDtfi.AbbreviatedMonthNames =
    languageDtfi.AbbreviatedMonthNames;

  builderDtfi.DayNames = languageDtfi.DayNames;

  builderDtfi.MonthGenitiveNames = languageDtfi.MonthGenitiveNames;

  builderDtfi.MonthNames = languageDtfi.MonthNames;

  builderDtfi.ShortestDayNames = languageDtfi.ShortestDayNames;

  builder.KeyboardLayoutId =
    languageNeutralCultureInfo.KeyboardLayoutId;

  builder.ThreeLetterISOLanguageName =
    languageNeutralCultureInfo.ThreeLetterISOLanguageName;

  builder.ThreeLetterWindowsLanguageName =
    languageNeutralCultureInfo.ThreeLetterWindowsLanguageName;

  builder.TwoLetterISOLanguageName =
    languageNeutralCultureInfo.TwoLetterISOLanguageName;

  return builder;
}

两个方法,GetNativeRegionNameGetNativeCurrencyName,尝试获取区域名称和货币名称的本地版本。它们都通过更改 CurrentCulture 为需要本地名称的语言(即西班牙语)来工作,然后获取属性。如果安装了相应的 .NET Framework 语言包,将返回正确的本地名称;否则,本地名称将是英文名称,您需要在注册或保存区域设置之前手动更新这些值。GetNativeCurrencyName 方法如下所示(GetNativeRegionName 相同,只是属性名称不同,并且它尝试获取区域的 DisplayName,因为 DisplayName 是本地化的)。

protected static string GetNativeCurrencyName(
  RegionInfo regionInfo, CultureInfo languageCultureInfo)
{
  string nativeName;
  CultureInfo oldCultureInfo =
    Thread.CurrentThread.CurrentUICulture;
  try
  {
    // attempt to change the UI culture
    Thread.CurrentThread.CurrentUICulture = languageCultureInfo;
    // get the new name (if a corresponding language pack is
    // installed then this yields a true native name)
    nativeName = regionInfo.CurrencyNativeName;
  }
  catch (Exception)
  {
    // it was not possible to change the UI culture
    nativeName = regionInfo.CurrencyNativeName;
  }
  finally
  {
    Thread.CurrentThread.CurrentUICulture = oldCultureInfo;
  }
  return nativeName;
}

导出操作系统特定区域设置

自定义区域设置的另一个用途是在不同操作系统之间统一支持的区域设置。回想一下,.NET Framework 2.0 中可用区域设置的列表取决于代码运行的操作系统。例如,Windows XP Professional Service Pack 2 拥有的区域设置比 Windows 2000 Professional 多得多。如果您的应用程序需要使用仅在新版本 Windows 中可用的区域设置,您的第一反应可能是将客户端升级到该 Windows 版本。然而,一个更简单的解决方案是从拥有该区域设置的 Windows 版本导出所需区域设置到没有该区域设置的机器上。例如,您可以从 Windows XP Professional Service Pack 2 导出 Welsh (United Kingdom) 区域设置到,比如说,Windows 2000 Professional(其中此区域设置是未知的)。当新版本的 Windows 发布并且您垂涎其新区域设置但又不想升级开发机器时,这种方法尤其有用。

这个过程被包装在 CultureAndRegionInfoBuilderHelper.Export 方法中,可以这样调用:

CultureAndRegionInfoBuilderHelper.Export(
  new CultureInfo("cy-GB"), "cy-GB.ldml", 
                  "en-GB", "en-GB");

静态 Export 方法接受四个参数:要导出的 CultureInfo、导出定义的文件的文件名、导出的区域设置应使用的文本信息区域设置以及导出的区域设置应使用的排序区域设置。导出方法从一些容易识别的代码开始,这些代码只是创建一个新的 Culture AndRegionInfoBuilder 对象并从现有区域设置加载其值:

RegionInfo regionInfo = new RegionInfo(cultureInfo.Name);

CultureAndRegionInfoBuilder builder =
  new CultureAndRegionInfoBuilder(cultureInfo.Name,
  CultureAndRegionModifiers.Replacement);

builder.LoadDataFromCultureInfo(cultureInfo);
builder.LoadDataFromRegionInfo(regionInfo);

builder.Save(ldmlFilename);

请注意,导出的区域设置起初看起来像是替换区域设置,但这只是一个伎俩,以便允许在已有该区域设置的机器上保存该区域设置。但是,导出的区域设置文件(例如,cy-GB.ldml)不能立即在目标机器上使用。首先需要解决一个问题。如果您打开导出的 LDML 文件,您会发现两条行会阻止在目标机器上创建自定义区域设置:

<msLocale:textInfoName type="cy-GB" />
<msLocale:sortName type="cy-GB" />

这些行分别定义了文本信息和排序顺序。这些行的问题在于它们引用了目标机器上不存在的文本信息和排序定义。这些行必须更改为目标机器上存在的文本信息和排序顺序。Export 方法的其余部分就是做这个。结果是这些行被更改:

<msLocale:textInfoName type="en-GB" />
<msLocale:sortName type="en-GB" />

当然,这意味着这些导出的自定义区域设置的文本信息和排序顺序将不完全正确,但由于无法为自定义区域设置定义新的文本信息和排序顺序,这是我们必须接受的一个限制。

公司特定方言

正如本章开头“自定义区域设置的用途”中所述,创建使用特定于单个公司或一组公司的词汇的资源集可能很有用。CreateChildCultureAndRegionInfoBuilder 方法可以做到这一点,并可以按如下方式使用:

CultureAndRegionInfoBuilder builder =
  CultureAndRegionInfoBuilderHelper.
  CreateChildCultureAndRegionInfoBuilder(
  new CultureInfo("en-US"),
  "en-US-Sirius",
  "English (United States) (Sirius Minor Publications)",
  "English (United States) (Sirius Minor Publications)",
  "United States (Sirius Minor Publications)",
  "United States (Sirius Minor Publications)");

builder.Register();

该方法接受一个要继承的区域设置(例如,“en-US”),接受新区域设置的名称以及各种字符串以设置各种名称属性。它返回一个 CultureAndRegionInfoBuilder 对象,可用于注册区域设置。CreateChildCultureAndRegionInfoBuilder 方法如下所示:

public static CultureAndRegionInfoBuilder
  CreateChildCultureAndRegionInfoBuilder(
  CultureInfo parentCultureInfo, string cultureName,
  string cultureEnglishName, string cultureNativeName,
  string regionEnglishName, string regionNativeName)
{
  RegionInfo parentRegionInfo =
    new RegionInfo(parentCultureInfo.Name);

  CultureAndRegionInfoBuilder builder =
    new CultureAndRegionInfoBuilder(cultureName,
    CultureAndRegionModifiers.None);

  // load the culture and region data from the parent
  builder.LoadDataFromCultureInfo(parentCultureInfo);
  builder.LoadDataFromRegionInfo(parentRegionInfo);

  builder.Parent = parentCultureInfo;
  builder.CultureEnglishName = cultureEnglishName;
  builder.CultureNativeName = cultureNativeName;
  builder.RegionEnglishName = regionEnglishName;
  builder.RegionNativeName = regionNativeName;

  return builder;
}

扩展 CultureAndRegionInfoBuilder 类

在第 6 章的“扩展 CultureInfo 类”一节中,我展示了一个 CultureInfoEx 类,它扩展了 .NET Framework 的 CultureInfo 类。这个 CultureInfoEx 可以用来存储有关区域设置的额外信息;给出的示例添加了邮政编码格式信息,可用作数据输入的掩码。如果您喜欢自定义区域设置的概念,并且也喜欢扩展 CultureInfo 类的概念,那么自然的扩展就是将两者结合起来,并拥有扩展的自定义区域设置。不幸的是,自定义区域设置体系结构是一个封闭的体系结构,这种情况不受支持。许多障碍阻止自定义区域设置体系结构被扩展:

  • CultureAndRegionInfoBuilder 是密封的,因此不能从中继承。
  • 读取和写入 LDML 文件的 CultureXmlReaderCultureXmlWriter 类都是内部且密封的;因此,它们不能被继承,甚至不能被访问。
  • NLP 文件格式是二进制的且是专有的。

为了解决这些限制,您必须在自定义区域性体系结构之上实现一个层。其核心思想是创建一个CultureAndRegionInfoBuilderEx类,该类封装了CultureAndRegionInfoBuilder类。新类将是CultureAndRegionInfoBuilder类的副本,并将所有属性和方法从“伪”CultureAndRegionInfoBuilderEx类重定向到CultureAndRegionInfoBuilder类。Register方法会将附加的CultureInfoEx信息保存到LDML文件(例如,“bn-BD.ldml”)的私有区域,然后将该文件安装在Windows的Globalization文件夹中。Unregister方法将删除/重命名附加文件。Save方法会将附加信息写入LDML文件,而CreateFromLdml方法将从LDML文件加载附加信息。最后,CultureInfoEx构造函数将检查该区域性是否为自定义区域性,如果是,将从关联的附加信息文件加载附加信息。

自定义区域性和.NET Framework语言包

.NET Framework从操作系统和框架资源中获取所需的资源。特别是,像异常消息、PrintPreviewDialogCultureInfo.DisplayNameRegionInfo.DisplayName这样的资源都是从与CultureInfo.CurrentUICulture匹配的.NET Framework语言包中获取的。当然,对于补充性自定义区域性,不存在这样的语言包,因此资源会回退到英语。您对此几乎无能为力。虽然技术上可以为自己的语言创建.NET Framework语言包,但这样做没有价值,因为您无法使用用于签名.NET Framework程序集的相同密钥来签名程序集。如果您的自定义.NET Framework语言包没有使用相同的密钥,ResourceManager将无法将您的语言包卫星程序集与.NET Framework中的回退程序集匹配。因此,任何此类自定义.NET Framework语言包都将被忽略。

如果使用ClickOnce部署Windows Forms应用程序,这会产生连锁反应,因为ClickOnce界面的大部分内容是从.NET Framework语言包中获取的(请参阅第4章“Windows Forms特定功能”中的“ClickOnce”部分)。由于您无法创建自己的.NET Framework语言包,因此无法以自定义区域性的语言提供ClickOnce用户界面(ClickOnce引导程序对话框除外)。

.NET Framework 1.1和Visual Studio 2003中的自定义区域性

对于.NET Framework 1.1中的自定义区域性,其功能远不如.NET Framework 2.0,以至于如果您能够升级到.NET Framework 2.0,我建议您这样做。假设这不可能,请继续阅读。

在.NET Framework 1.1中,自定义区域性是一个新类,它继承自CultureInfo类,并在构造函数中将必要的CultureInfo属性设置为它们的相关值。 .NET Framework SDK包含一个此类自定义区域性的示例,位于<SDK>\v1.1\Samples\Technologies\Localization\CustomCulture。要使用新的自定义区域性,您必须使用其自身的构造函数进行构建。例如,如果您的自定义区域性类名为BengaliBangladeshCulture,则使用此方法进行构建:

CultureInfo cultureInfo = new BengaliBangladeshCulture();

无法使用区域性名称(例如,“bn-BD”)来构造它,因为.NET Framework 1.1支持的区域性列表是硬编码的。同样,Visual Studio 2003和WinRes 1.1使用.NET Framework提供的列表;因此,无法让它们识别自定义区域性,所以这两个工具对于维护自定义区域性的资源来说都是无用的。

我们现在在哪里?

.NET Framework中的自定义区域性代表着向前迈出的巨大一步,为开发人员开辟了令人兴奋的新可能性。新的区域性被.NET Framework识别为一等公民,一旦注册,就与其他任何区域性一样有效。通过此功能,我们可以替换现有区域性,为以前未知或仅在某些操作系统上识别的区域性创建新区域性,创建新的语言/区域组合,并支持客户特定的方言。然而,自定义区域性实现并非没有限制,应谨慎避免“自定义区域性地狱”。扩展自定义区域性体系结构需要付出努力,而且(并非不合理地)不支持自定义区域性的语言包。话虽如此,唯一remaining的限制是我们想象力的范围。

© Pearson Education 版权所有。保留所有权利。

© . All rights reserved.