创建支持双语的 ASP.NET MVC 3 应用程序 – 第 2 部分






4.82/5 (28投票s)
本文扩展了第 1 部分,允许通过 URL 以轻量级的方式覆盖区域设置。
简介和回顾
第 1 部分 概述了如何使用资源文件来国际化 MVC3 应用程序,使其支持双语。它还引用了三个对我很有帮助的网站,这些网站构成了本文各部分的基础。如果您不熟悉第 1 部分,或者没有在 MVC 应用程序或一般情况下使用过 resx 文件,那么回顾一下(尤其是简介)可能会有所帮助,因为我不会在这一部分重复涵盖其内容。此外,我还要重申第 1 部分的注意事项,即需要对 MVC 3 中的以下内容有基本了解:路由策略、控制器操作和视图;Route
类;RouteHandlers
的作用;RouteConstraint
的功能。为了清晰和简洁起见,我不会深入解释这些内容。与第 1 部分不同,本文并非面向 MVC3 初学者,因为其中一些方面不可避免地涉及技术性。但我希望初学者能够坚持下去,并通过剖析代码来加深对 MVC 3 的理解!
在第 1 部分中,MVC 3 应用程序能够根据用户浏览器中设置的“语言偏好”显示英语(默认)或阿拉伯语。本文将介绍如何添加通过 URL 覆盖此设置的功能;此外,还将介绍如何手动更改语言,并最后讨论所用方法的优缺点。
第 2 部分应用程序概述
首先需要决定 URL 的路由策略,MVC 3 的默认路由是:http://MyHost/Contoller/Action/Id。
“Controller”是控制器类,“Action”是将在该控制器上调用的方法,还有一个可选的第三部分,即 ID。这些的默认值分别是“Home”和“Index”;在默认应用程序中,该方法不需要 ID。有必要决定将语言标识符放在 URL 的何处。由于 URL 已经从通用到具体,我们将遵循相同的模式:http://MyHost/Culture/Contoller/Action。
如果缺少区域设置,则策略是默认使用浏览器的语言设置,使应用程序像本文第 1 部分一样工作,使用浏览器默认设置。MSDN 具有类似的路由:区域设置的格式为“en-us”或“en-sa”。本文忽略了区域设置格式的第二部分,因此语言可以是“ar-zz”或“en-xx”,它仍然会显示相应的语言版本。由于我们不关心语言子区域,因此我也允许使用两字母 ISO 代码的短格式“en”和“ar”,我们将默认使用短格式。以下是一些示例 URL:
URL | 结果 |
https:// | 调用 Home/Index(默认),并使用浏览器的语言 |
https:///ar | 调用 Home/Index(默认),指定阿拉伯语,覆盖浏览器语言 |
https:///Home/Index | 与第一个 URL 相同,但明确设置了 Controller 和 Action |
https:///ar/Home/Index | 与第二个 URL 相同,但明确设置了 Controller 和 Action |
https:///en/Home/Index | 与上一个相同,但用英语覆盖浏览器语言 |
https:///en-gb/Home/Index | 与上一个相同(有效的子区域设置) |
https:///en-zz/Home/Index | 与上一个相同(无效的子区域设置,但仍然用英语覆盖) |
将语言放在前面可以使 URL 的其余部分保持“原样”,因此可以兼容绝大多数现有的路由策略。这里有一个陷阱,它并不明显:默认策略有一个可选的最终参数 ID,因此以下 URL 有可能以相同的方式被匹配
- https:///Home/Index/1
- https:///ar/Home/Index
可能会以以下方式被错误匹配
URL | 语言 | 控制器 (Controller) | 操作 | ID |
https:///Home/Index/1 | [默认] | Home | 目录 | 1 |
https:///ar/Home/Index | ar | Home | 目录 | N/A |
https:///ar/Home/Index | [默认] | ar | Home | 目录 |
最坏的情况是最后一种:MVC 3 框架在尝试使用名为“ar”的控制器时会抛出错误。为了确定给定 URL 的正确场景,我们将创建一个 RouteConstraint
,如果 URL 的第一部分是语言标识符,它将有效地返回 true。代码将匹配“XX”或“XX-XX”的模式,其中 X 是任何字母。约束代码不会检查语言代码是否有效。如果语言代码无效或不受支持(即不是英语或阿拉伯语),则会显示站点的默认设置(英语)。这一设计决策部分是为了提高健壮性;如果未做出此决定,而用户实际尝试使用语言代码“xx-xx”,则网站会抛出错误,因为它会将“xx-xx”识别为控制器,而该控制器不存在。在示例代码中,有一个已注释掉的方法(带有简单的说明),如果您觉得只匹配支持的区域设置更适合您的情况,可以使用它。请注意,我们确实会失去一点灵活性:我们不能创建名称与我们的模式匹配的控制器(尽管命名为“en”或“ar-jo”的控制器类并不具描述性 :)。
如果您对路由策略感到困惑,请查看下面的“结果(到目前为止!)”部分,它显示了各种 URL 的意图!
我们将如何实现这些?
这涉及几个步骤
- 添加一个路由,该路由接收默认路由并在其前面加上区域设置值,以确保应用程序在全球化时正常工作。它将使用正常的应用程序默认值。路由处理程序将添加一个约束,以确保在使用添加到 URL 的匹配我们模式的区域设置时使用全球化类。
- 创建一个路由处理程序来调用设置 UI 和主线程区域设置的代码。除此之外,它应该与默认的
MvcRouteHandler
具有相同的功能。 - 将有一个区域设置管理器,维护一个字典,其中两字母 ISO 代码是映射到代表要使用的语言的
CultureInfo
实例的键。它将负责设置线程的区域设置,并且可以由约束来约束仅支持的区域设置。管理器类还应使多语言(而不是双语)支持更容易实现。 - 用于将区域设置的模式匹配从其他类中抽象出来的代码。
请注意,该设计旨在“不干涉”,并尽可能自成一体。要为第 1 部分中的应用程序(或几乎任何 MVC 3 应用程序)添加全球化 URL 支持:
- 添加对包含上述类的全球化支持库的引用。
- 通过 MVC 应用程序的 global.asax 注册一个步骤 1 中描述的路由实例。
保留默认处理程序,以便在未指定区域设置代码时(即路由约束失败),应用程序仅使用浏览器的默认语言,回退到本文第 1 部分中的行为。
代码
名为 MvcGlobalisationSupport 的类库已添加到第 1 部分的解决方案中,其中包含上述类。我将描述将机制整合进去的代码,然后深入解释每个类做什么,并描述一些设计决策/权衡。
现在,我们只需要将原始注册代码替换为 Application_Start()
中的以下内容:
public static void RegisterRoutes(RouteCollection routes)
{
const string defautlRouteUrl = "{controller}/{action}/{id}";
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
RouteValueDictionary defaultRouteValueDictionary = new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
Route defaultRoute = new Route(defautlRouteUrl,
defaultRouteValueDictionary, new MvcRouteHandler());
routes.Add("DefaultGlobalised",
new GlobalisedRoute(defaultRoute.Url, defaultRoute.Defaults));
routes.Add("Default", new Route(defautlRouteUrl,
defaultRouteValueDictionary, new MvcRouteHandler()));
}
应用程序中定义了默认路由(没有区域设置),这将由 GlobalisedRoute
(前面加上“{culture}/”)和默认回退路由使用。通常会添加对资源的忽略子句。接下来,创建一个默认值字典,其中包含与“正常”应用程序相同的值。请注意,代码没有为区域设置添加默认值。这是可能的,但会导致全局化版本始终被调用,覆盖浏览器默认语言。这会提供较差的用户体验(尽管在重构后会使应用程序更简单),所以我们不会提供默认值。
全局化路由被创建并首先添加到路由集合中,以便其中创建的全局化约束可以确定 URL 是否匹配包含区域设置值的“全局化”版本。如果非全局化默认路由首先添加,它会将任何提供的区域设置匹配为 controller
值,并且由于它会尝试调用不存在的控制器(基于提供的区域设置值)而发生错误。请注意,全局化路由在其构造函数中接收非全局化路由和默认值,因此它可以将区域设置值添加到路由的开头,使路由的其余部分和默认值与正常策略相同。
GlobalisedRoute
GlobalisedRoute 类抽象了本可以轻松添加到 global.asax 中的代码,这有助于保持 global.asax 的整洁,并将大部分全球化支持工作保留在一个单独的程序集中。
public class GlobalisedRoute : Route
{
public const string CultureKey =
"culture"; static string CreateCultureRoute(string unGlobalisedUrl)
{
return string.Format("{{" + CultureKey + "}}/{0}", unGlobalisedUrl);
} public GlobalisedRoute(string unGlobalisedUrl, RouteValueDictionary defaults) :
base(CreateCultureRoute(unGlobalisedUrl),
defaults,
new RouteValueDictionary(new { culture = new CultureRouteConstraint() }),
new GlobalisationRouteHandler())
{
}
}
此类创建路由 {culture}/{controller}/{action}
,并添加 CultureRouteConstraint
以确保仅在提供格式有效的区域设置代码时才调用全局化处理程序。它还实例化 GlobalisationRouteHandler
以供使用。我还决定不重载构造函数来镜像基类中的构造函数;实现基类构造函数的版本似乎没有多大意义,因为我们在类中提供了值。如果您将此代码投入生产,可能需要与基类类似的构造函数。
GlobalisationRouteHandler
路由处理程序负责创建 IHttpandler
以向请求提供响应。
public class GlobalisationRouteHandler : MvcRouteHandler
{
string CultureValue
{
get
{
return (string)RouteDataValues[GlobalisedRoute.CultureKey];
}
}
RouteValueDictionary RouteDataValues { get; set; }
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
RouteDataValues = requestContext.RouteData.Values;
CultureManager.SetCulture(CultureValue);
return base.GetHttpHandler(requestContext);
}
}
这些属性只是为了提供对路由中区域设置部分存储值的更清晰的访问。CultureManager.SetCulture(CultureValue);
完成了第一个实际工作,它使用区域设置管理器来设置 UI 和主线程的区域设置。最后,它调用基类 [默认] 处理程序的 GetHttpHandler
方法,确保应用程序的路由处理方式与非全局化应用程序相同。请注意,我在上面的代码片段中省略了构造函数。
CultureRouteConstraint
GlobalisedRoute
假定主机名之后的 URL 的第一部分是区域设置值,此约束由它添加,以匹配它是否符合本文开头提到的格式(“xx”或“xx-xx”)。
public class CultureRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (!values.ContainsKey(parameterName))
return false;
string potentialCultureName = (string)values[parameterName];
return CultureFormatChecker.FormattedAsCulture(potentialCultureName);
}
}
此类提取潜在的“区域设置”值,并使用格式检查器返回 true(如果它与模式匹配)。通过返回 true,将使用 GlobalisedRoute
,否则将使用 global.asax 中指定的默认路由。如应用程序概述中所述,区域设置可能不受支持甚至无效,它只需要具有正确的格式。示例代码中有一个第二个已注释掉的 Match
方法,可以替换上面的方法,以仅允许匹配受支持的区域设置。
CultureManager 和 CultureFormatChecker
这些代码可以在下载文件中找到,它们并不复杂,因此描述这些类的主要功能比描述代码本身要简单得多。
CultureManager
- 有一个私有的“
SupportedCultures
”字典,将两字母 ISO 代码映射到等效的CultureInfo
对象。 - 一个私有属性,返回默认区域设置的
CultureInfo
;当在 URL 中请求格式化但无效的区域设置时使用。 - 一个公共
SetCulture(string code)
方法,它尝试从字典中查找区域设置。它会忽略大小写,并将解析到短的两字母形式。如果区域设置受支持,它会将 UI 和主线程设置为该区域设置;如果不支持,则将其设置为默认区域设置。通常,只有在 URL 包含有效但不受支持的区域设置(例如“fr-fr”/“fr”)或无效但格式正确的区域设置字符串(例如“xx-yy”/“xx”)时,区域设置才应仅正常设置为默认值。
CultureFormatChecker
这是最简单的类,它的单个方法 bool FormattedAsCulture(string code)
返回 true(如果它匹配一个匹配“xx”或“xx-xx”的正则表达式,其中 x 是字母字符)。对于喜欢这些东西的人来说,实际的正则表达式是:^([a-zA-Z]{2})(-[a-zA-Z]{2})?$
。
结果(到目前为止!)
首先,为了确认在没有 URL 覆盖行为的情况下,应用程序仍然使用浏览器默认语言(如果您不知道如何操作,请参阅第 1 部分!),以下是浏览器默认设置为英语的页面。
浏览器默认设置为阿拉伯语
到目前为止一切顺利!从现在开始,浏览器默认设置为阿拉伯语(图片宽度减小以适应页面和 URL!)。这里在 URL 中指定了一个区域设置,使用了短格式。
以及长格式
现在,我们测试一下当 URL 中指定了不受支持的区域设置时,应用程序是否会默认使用英语。
最后,一个基本的测试,证明应用程序仍然通过手动指定控制器和操作的默认值来处理路由。
为 UI 添加切换语言的支持
正如在第 1 部分中简要讨论过的,强制用户使用其浏览器的默认语言并不是理想的做法。例如,他们可能身在国外,并使用锁定的公共机房的计算机;不太懂技术的用户可能不知道如何将其切换回他们能读懂的语言(相信我,我以前在技术支持部门工作过 :))。本最后一部分概述了如何添加一个返回当前页面“其他”语言版本的超链接。由于我们将显示相反的语言(英文页面上有一个阿拉伯语链接,阿拉伯语页面上有一个英语链接),因此我们需要将内容添加到我们的 resx 文件中。这些将是通用值,因此它们被添加到 Common.resx 和 Common.ar.resx 中。对于英语资源文件:
OtherLanguageKey |
ar |
OtherLanguageName |
عربي |
ReverseTextDirection |
rtl |
ReverseTextDirection
用于支持标记链接的 dir
属性,使其与包含的文本方向相同,对于阿拉伯语,是从右到左。这有助于保持浏览器选择行为的一致性。
下一步是添加一个链接,该链接转到当前页面,但带有相反区域设置的语言键。为此,向库中添加了一个静态类,该类提供了 HtmlHelper
的扩展方法,称为 GlobalisedRouteLink
。
public static class GlobalisationHtmlHelperExtensions
{
//Snip……
public static MvcHtmlString GlobalisedRouteLink(this HtmlHelper htmlHelper,
string linkText, string targetCultureName, RouteData routeData)
{
RouteValueDictionary globalisedRouteData = new RouteValueDictionary();
globalisedRouteData.Add(GlobalisedRoute.CultureKey, targetCultureName);
AddOtherValues(routeData, globalisedRouteData);
return htmlHelper.RouteLink(linkText, globalisedRouteData);
}
}
该帮助程序创建一个新的路由字典,并首先添加区域设置;AddOtherValues
方法(已省略)会遍历传递给该方法中的路由,添加剩余的值(如果区域设置已存在则跳过)。然后,它使用普通的 RouteLink
方法来生成完整的全局化链接。
集成
对于 ASPX 视图引擎,已添加导入指令。
<%@ Import Namespace=" InternationalizedMvcApplication.Resources" %>
<%@ Import Namespace="MvcGlobalisationSupport" %>
第一个语句消除了在从 Common.resx 添加资源时对命名空间进行限定的需要,第二个语句添加了包含 GlobalisedRouteLink
扩展方法的命名空间。然后,像这样将用于提供链接的代码添加到本文第 1 部分的 body 的顶部:
<div dir="<%= Common.ReverseTextDirection %>">
<%= Html.GlobalisedRouteLink(Common.OtherLanguageName,
Common.OtherLanguageKey, ViewContext.RouteData)%>
</div>
同样,对于 Razor,会添加一个 using
标记。
@using MvcGlobalisationSupport
@using InternationalizedMvcApplicationRazor.Resources;
然后将这个 div
添加到 body 的顶部。
<div dir="@Common.ReverseTextDirection">
@Html.GlobalisedRouteLink(Common.OtherLanguageName,
Common.OtherLanguageKey, ViewContext.RouteData)
</div>
Razor 用户应该注意,上面的标记是 Razor 和 ASPX 视图引擎在此文章中唯一的不同之处!
测试 UI 更改
首先,将浏览器默认语言设置为英语,然后运行项目。
生成的链接的 HTML 源代码如下:
<div dir="rtl">
<a href="https://codeproject.org.cn/ar">عربي</a>
</div>
请注意,我们已支持语言方向,并且帮助程序已生成引用。单击链接将导致:
这是英语链接的 HTML。
<div dir="ltr">
<a href="https://codeproject.org.cn/en">English</a>
</div>
请注意,语言已明确指定。精明的您会注意到,当前页面的控制器和操作未指定,MVC 3 框架足够智能,知道正在使用默认值,因此不会显式提供它们。要测试当使用除默认值之外的值时 URL 是否正确生成,请输入一个提供一个未使用的(但可用的!)ID 的值的 URL,例如 https:///ar/Home/Index/1。渲染时,将为浏览器生成所需的 HTML 以用于链接。
<div dir="ltr">
<a href="https://codeproject.org.cn/en/Home/Index/1">English</a>
</div>
大获成功!
关注点
- 这是一种策略,其他架构也适合!希望我的方法是清晰的(尽管解释起来很复杂……)。
- 我已经在我雇主的模拟应用程序中测试了此策略,该策略包含可选的 ID 参数以及其他控制器和操作。为了使代码保持简单,我在下载中没有这样做,但是您应该能够像在普通应用程序中一样添加新的操作和控制器。您唯一需要担心双语状态的情况是实际显示差异(无论如何都需要这些)或如果您想提供其他切换语言的方式。
- 此机制可以扩展到多语言应用程序。最大的问题可能是将语言链接替换为组合框,并弄清楚如何填充它。应该不需要太多工作(著名的最后几句话!)。可以使用语言的区域变化,但明智的做法是完成验证受支持语言所需的额外工作,并为不受支持的变体返回默认值,从而替换我简单的模式匹配机制。
- 可以通过在 web.config 中添加一个部分来指定支持哪些语言,提供一个两字母甚至四字母的代码列表。要做到这一点,开发人员需要替换
CultureManager
中硬编码的区域设置添加,并更改设置默认语言的代码。同样,用于填充语言切换机制的代码很可能是最大的难题。 - 如果需求足够,我将添加第三部分,使应用程序支持多语言。
结论
尽管我们已经进行了一些技术细节的深入探讨,并且需要大量工作,但从 Web 应用程序的角度来看,MVC 3 应用程序的全球化可以实现,而且非常轻巧。
- 添加 RESX 文件并在视图中使用它们,如第 1 部分所述。
- 添加此处所述的全球化支持程序集。
- 对 global.asax 进行更改。
- 添加一个控件以允许用户手动覆盖默认浏览器设置。
实际的繁重工作是在全球化支持程序集中完成的。MVC 应用程序全球化的整个体验远不如标准的 ASP.NET 应用程序那样流畅和直观。我希望随着 MVC 框架的成熟和普及(它似乎正在这样做),能够添加更好的国际化支持。
还请注意,语言是通过 URL 本身来区分的,而不是像有时在普通的 ASP 应用程序中那样通过参数,并且您可以轻松添加特定于语言的元数据。这两者可以与站点地图结合使用,以生成更好的特定语言搜索引擎排名。
一如既往,如果您有任何意见或反馈,请随时提出。
历史
如果您编辑本文,请在此处保留您所做更改或改进的运行更新。
- 2011 年 6 月 7 日:初稿。