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

使用简单的 RESX 文件本地化网站、JavaScript 和程序集

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2012年12月10日

CPOL

11分钟阅读

viewsIcon

34618

downloadIcon

277

ASP.NET 中国际化的历史和可用选项。

引言

我们中的许多人都经历过本地化在网站项目中带来的问题。或者项目带来的问题。

有很多不同的方法可以处理本地化,每种方法通常都有其优点和不便之处。 

让我们一起了解一下这些选项。

App_LocalResources   

使用经典的 App_LocalResources,您可以灵活地快速高效地组织文件。只需在要本地化的任何文件下方创建一个名为 App_LocalResources 的子文件夹,然后将 .resx 文件放入其中,并将其命名为与要本地化的文件相同,但以 .resx 结尾作为基本翻译,并为每种特定语言的翻译以 {culture}.resx 结尾。

在 .resx 文件中插入一个名为 Title 的键,以及一些值。 

然后开始使用您的翻译

<asp:Literal runat="server" Text="<%$Resources:Title %>" />
<asp:Button runat="server" Text="<%$Resources:Title %>" />

需要注意的是,为了让魔法生效,<%$Resources:Title %> 只在声明了 runat="server" 的元素上才有效。因此,在很多地方,我们将插入我们漂亮的 <asp:Literal />,这在某些情况下会产生非常难看的语法。

在使用 .aspx 文件的项目中,您可以做一些更有趣的事情,那就是使用不太为人所知的 meta:resourcekey

<asp:TextBox runat="server" meta:resourcekey="MyInput" /> 

这允许您在 resx 文件中本地化元素的全部属性范围,而只在 .aspx 文件中声明一个关键字。

正如您可能刚刚猜到的,第一部分是键标识符,第二部分是要应用该值的属性名称。这非常棒,因为它避免了将难看的语法引入您的 .aspx 文件。

这对于自定义控件来说可能非常有帮助,因为自定义控件有很多属性。例如,对于特殊的错误处理控件:tooShortErrorMsg、tooLongErrorMsg、numericErrorMsg、notPrettyEnoughErrorMsg……您明白了。

不便之处:   

虽然上面的方法非常酷,但您必须将所有要使用本地化的元素设置为 runat="server"。

您不能在 code-behind 代码、类或函数中使用这些翻译。

您也不能在 JavaScript 中使用它们……除非您愿意这样做: 

<%@ Page Language="C#" ContentType="text/javascript" %>

var translations = {
    Title: "<asp:Literal runat="server" 
             Text="<%$Resources: Title %>" />"
};

注意 Page 上的 ContentType 声明。即使这是一个很好的技巧,我真的不建议您这样做!

上述选项对于使用 .aspx 文件而不是 .cshtml 文件的 ASP.NET 和 MVC 项目都有效,因为一旦切换到 .cshtml 文件,我们就失去了使用 runat="server" 的能力。

App_GlobalResources 

这个很有意思,因为您的翻译包含在一个文件夹中,而不是分散在整个站点中。这并非坏事,因为在很多情况下,将翻译放在一个子文件夹中是相当方便的。

此文件夹位于网站的根目录。

另一个有趣的地方是,App_GlobalResources 实际上会生成已编译的类,这意味着您可以停止使用那些神奇的 runat="server" 标签,并可以访问真实的属性,甚至可以在您的 code-behind 中使用它们。

<%= Resources.Common.Title %>

<asp:Literal runat="server" Text="<%$Resources: Common, Title %>" />

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    Response.Write(Resources.Common.Title);
}

尽管如此,您仍然可以使用上面关于 runat="server" 标签的适用内容,只是现在您必须声明您的翻译位于哪个命名空间中。 

您会看到,添加到 App_GlobalResources 的每个文件都会生成一个同名类。我们之前添加了一个名为 common.resx 的文件,所以我们通过 Resources.Common.* 或 <$Resources: Common, * %> 来访问它。

您甚至可以进行一些小的 JavaScript 技巧,只是现在使用真实的属性。

<%@ Page Language="C#" ContentType="text/javascript" %>

var translations = {
        Title: "<%= Resources.Common.Title %>"
};

通过向 App_GlobalResources 添加不同名称的文件,您可以生成同样多的不同类,这对于按类别(通用、用户、注册等)组织您的翻译非常方便!

不便之处: 

App_GlobalResources 看起来很不错。

但是,将文件移到这里,您就失去了使用那个很酷的 meta:resourcekey 技巧的可能性。实际上,这是因为 App_GlobalResources 会将内容编译成类。在 .resx 文件中使用带有标点符号的键标识符(MyInput.Text)将成为非法语法。

本地化 JavaScript 文件仍然有点麻烦。

尽管 App_GlobalResources 允许您按类组织翻译,您可以在其中添加任意多的子文件夹,.NET 仍然只会为您生成一个扁平的命名空间。Resources.Something,仅此而已。在大多数情况下,这可能没问题,但在其他情况下,您可能会得到非常庞大的 .resx 文件,或者大量的 KEY 条目,其中一个键与另一个键非常相似,但又不完全相同,等等。

不幸的是,您的翻译仍然被锁定在您的网站中。除非您愿意将您的网站 .dll 引用添加到其他程序集中。也就是说,如果您的网站有一个生成的并且可以共享的 .dll,而旧式网站项目不是这种情况。

在继续研究专用程序集之前,先说一点

现在,如果您能够继续使用 App_LocalResources 的灵活性,同时又添加 App_GlobalResources 的优点,会怎样?

嗯,您可以!

这有点繁琐,但实际上很简单。您只需要选择代表基本文化的 .resx 文件,选择属性,然后将 "Custom Tool"(自定义工具)更改为 "PublicResXFileCodeGenerator"。

现在您可以神奇地做到这一点

<%= WebApplication1.App_LocalResources.Default_aspx.Title %>

好的,命名空间不是很漂亮,所以您可以采取的另一个步骤是将 "Custom Tool NameSpace"(自定义工具命名空间)设置为类似 "MyCompany" 的内容,这样您就可以通过以下方式调用它:

<%= MyCompany.Default_aspx.Title %>

您仍然会失去使用 meta:resourcekey 技巧的能力,并且仍然存在与与其他项目共享翻译相关的问题。如果您想与其他项目共享您的网站 .dll,您还需要将 "Build Action"(生成操作)设置为 "Embedded Resource"(嵌入式资源)。

这会告诉编译器将翻译嵌入到生成的 dll 中。而且您甚至不必将 .resx 文件推送到生产 Web 服务器上,因为它们现在直接包含在最终的 DLL 中。

但是,如您所见,如果您的站点有成百上千个本地化文件,这需要大量的手动操作,更不用说如果您忘记更改这些设置可能会发生的不可预见的错误。

独立程序集

那么,如果我们只需将所有翻译放在一个独立的 .dll 中,在那里我们可以用任意多的命名空间来组织我们的翻译,并且可以与我们的网站以及所有其他项目共享该 .dll,会怎样?

这样,我们就可以在网站中使用我们的翻译来本地化我们的网页,在我们的程序集中返回错误消息、本地化属性或 ViewModel。它在 .cshtml Razor 文件中也工作得很好,因为一切都已编译。

现在您可以做到这一切

<%= Resources.Models.User.Pseudo %>
 
@Resources.Models.User.Pseudo
 
public string Validate(User user)
{
    if (user == null) {
        return Resources.Models.User.NotExists;
    }
    
    return string.Empty;
}

不错!

但是……这里还有一个问题,就像上面一样,您必须将 "Custom Tool"(自定义工具)设置为 "PublicResXFileCodeGenerator",否则 .NET 默认会将您的翻译标记为 private。您仍然不能使用 meta:resourcekey 技巧,并且在 JavaScript 方面仍然存在相同的问题。

然而,您的翻译现在在一个独立的程序集中,您可以有效地组织您的命名空间,更不用说您能够与所有其他项目共享这些翻译的巨大优势,并且可以在 html、code-behind、函数和类中同样好地使用它们。

我们来个终极杀招!

正如您所见,我们在本地化方面有许多可用的选项,每种选项都有其优点和不便之处。

那么,我们是否真的可以拥有完美或接近完美的东西?嗯,算是吧。但前提是您愿意编写自定义解决方案。

隆重推出:T4 文本模板

T4 是一项很棒的技术,它实际上只是一个带有 .tt 扩展名的 Fancy .bat 文件。而且,您不必执行 DOS 命令,实际上可以实例化整个 .NET 框架。

然后……您就可以开始执行 WriteLines() 了。

虽然基础很简单,但它使您能够输出文本,并将该文本保存为 .cs 文件(或您喜欢的任何扩展名)。例如,您可以解析一个 .js 文件,对其进行最小化处理,然后将其结果输出到一个 .min.js 文件。

在我看来,我将获取项目中所有 .resx 文件的列表,为每个文件实例化一个 ResourceManager,以便能够从中提取 KEY 和一些额外信息,然后输出一个漂亮的 .cs 文件,其中包含所有命名空间、类和 KEY,以属性的形式,以便我们可以结合上述所有方法的优点。

此外,由于我们已经开始编写自定义代码,我们不妨添加一些附加功能,这样我们就可以做一些使用上述任何方法都无法实现的事情。

例如,我经常想用变量格式化错误消息。其他时候,我想替换翻译中的单词,但使用上述解决方案,这意味着我必须将文本分成多行,以便在短语中间插入一个 Pseudo,例如。为什么不直接说 "Hello {0}, how are you doing"?

当您的翻译中有域名或品牌名称,因为您正在处理一个通用项目,然后想将这些相同的翻译用于另一个客户的另一个项目时,会发生什么?复制所有翻译……为什么不直接写 "Sign up with {DOMAIN} and get the benefits of being a {BRAND} member for only {0}/month !",然后根据我们正在处理的客户替换 {DOMAIN} 和 {BRAND}?

哦,还有我们从一开始就面临的重大问题。本地化的JavaScript!我们已经看到到目前为止,每种解决方案的麻烦程度……当我们需要的翻译以 JSON 格式提供时,我们能否做到?

T4 使所有这些成为可能,并且可以做得更多,缺点是您必须自己编写代码。 

当然,既然我写了这篇文章,意味着我已经为您完成了大部分繁重的工作。剩下的就是将 T4ResX 添加到包含 .resx 文件的 "standalone assembly"(独立程序集)中。

然后您只需打开文件,然后按 CTRL+S 触发生成过程,T4ResX.cs 就会被创建。

此代码包含对我们常规翻译的快速访问,以及用于访问上述所有额外功能的辅助方法。

现在本地化 JavaScript 变得容易了,因为生成的代码包含一个小的(通过反射)方法,该方法能够搜索程序集中的属性,然后以 Dictionary<> 的形式返回它们。

Dictionary<string, Dictionary<string, string>> result = MyAssembly.Resources.Common.GetAsDictionary();

现在,一旦我们有了 Dictionary<namespace, Dictionary<key, value>,我们就只需要一种方法将其序列化为 JSON 并返回结果。

所以,现在我们只需要在我们的网站中设置一个小的辅助方法,该方法将返回字典作为 JSON,这样您就可以开始进行 script src = ,并享受真正的本地化 JavaScript 的好处了。

首先,我们需要一种返回 JSON 的方法,我们可以通过在项目中创建一个新类并声明一个小的扩展方法来做到这一点,如下所示:

using System.Web.Script.Serialization;
 
/// <summary>
/// Mark as partial, like this we can create supplementary extension methods in other files,
/// instead of filling this file with tons of junk until it becomes unreadable.
/// 
/// Yeah, i know the namespace is missing, it's on purpose so we can access our extension method from AnyWhere.
/// </summary>
public static partial class ExtensionMethods
{
    /// <summary>
    /// Make it static and initialized, we can reuse it when we need.
    /// </summary>
    public static readonly JavaScriptSerializer JavaScriptSerializer = new JavaScriptSerializer();
 

    /// <summary>
    /// Our nice JSON helper method
    /// </summary>
    public static string ToJson(this object value)
    {
        return JavaScriptSerializer.Serialize(value);
    }
}

在我们的控制器(如果使用 MVC 的话)中,设置一个小的辅助函数:

public ActionResult GetNameSpaceAsJs(string ns)
{
    return JavaScript(string.Format("var T4ResX = {{ Localization: {0}}};", 
        Localization.Utilities.GetResourcesByNameSpace(ns).ToJson()));
}

现在我们只需要将此添加到我们的 HTML 中,我们就有本地化 JavaScript 了!

<script src="https://codeproject.org.cn/home/GetNameSpaceAsJs?ns=OurNamespace"></script> 

>当然,我可以动态生成所有上述辅助方法,但目前我不希望在 .tt 文件中引用 System.Web。而且辅助方法手动设置起来相对较快。

几乎忘了!

您实际上如何在网站中切换区域设置?

我不会详细介绍,因为示例项目中有一些有趣的东西供您玩。不过,我将列出您的选项。

Web.config

<System.Web><Globalization>
  • 我们可以默认声明用户界面的区域设置 (uiCulture)
  • 以及影响日期/时间/数学函数的站点的低级部分 (culture)。
  • 我们还可以将区域设置设置为自动检测 (uiCulture="auto:en")。
    • 这会检测用户浏览器的首选区域设置。
  • 还有一个名为 EnableClientBasedCulture 的属性,它会从客户端浏览器的 HTTP Accept-Language Header 中提取区域设置。但我发现它不如单独设置前两个选项可靠。
    • 在代码中,您可以通过一个快捷方式读取 Accept-Language:Request.UserLanguages。
    • 但同样,这里也会遇到问题…… 
页面
  • 在 @Page 元素上,我们可以声明 UICulture 和/或 Culture。
代码
  • 我们可以将应用程序的 Thread 设置为我们想要的任何区域设置。
    • System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("en")
  • 就像上面的 @Page 一样,您也可以在 OnLoad() 事件中设置 UICulture 和/或 Culture。Page.UiCulture = "en";
  • 通过在 Page 中重写 InitializeCulture()。
    • 实际上,在 Page 中设置区域设置有点问题,所以这些是在这些情况下首选的方法。
  • 在 MVC 中,我们可以向我们的控制器添加自定义 Filter ?? 属性。
    • 例如 [LocalizedAttribute]
      • 这很好,但您可能会遇到问题,例如低级内容未被翻译,因为该属性起作用太晚了。
HttpModule
  • 我们可以创建一个自定义 HttpModule,根据各种输入变量(查询字符串、Cookie、用户偏好等)将应用程序的 Thread 设置为我们需要的任何区域设置。 
    • 这也是性能最高效的方法,并且触发足够早,因此您网站中的所有内容都会被本地化。
    • 嗯……还有 web.config 方法。 

希望阅读本文不会太枯燥,并且您在未来的本地化工作中玩得开心 Wink | <img src=

© . All rights reserved.