我已经翻译了我的mvc网站,该网站运行良好。如果我选择其他语言(荷兰语或英语),则会翻译内容。之所以有效,是因为我在会议中设定了文化。
现在,我想在URL中显示所选的区域性。如果它是默认语言,则不应在url中显示,仅当它不是默认语言时,才应在url中显示。
例如:
对于默认区域性(荷兰语):
site.com/foo site.com/foo/bar site.com/foo/bar/5
对于非默认文化(英语):
site.com/en/foo site.com/en/foo/bar site.com/en/foo/bar/5
我的问题 是我总是看到以下内容:
site.com/ nl / foo / bar / 5,即使我单击了英语(请参见_Layout.cs)。我的内容已翻译成英文,但网址中的route参数保留在“ nl”而不是“ en”。
我该如何解决或我做错了什么?
我尝试在global.asax中设置RouteData,但无济于事。
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("favicon.ico"); routes.LowercaseUrls = true; routes.MapRoute( name: "Errors", url: "Error/{action}/{code}", defaults: new { controller = "Error", action = "Other", code = RouteParameter.Optional } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = "[a-z]{2}" } );// or maybe: "[a-z]{2}-[a-z]{2} routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
Global.asax.cs:
protected void Application_Start() { MvcHandler.DisableMvcResponseHeader = true; AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } protected void Application_AcquireRequestState(object sender, EventArgs e) { if (HttpContext.Current.Session != null) { CultureInfo ci = (CultureInfo)this.Session["Culture"]; if (ci == null) { string langName = "nl"; if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0) { langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2); } ci = new CultureInfo(langName); this.Session["Culture"] = ci; } HttpContextBase currentContext = new HttpContextWrapper(HttpContext.Current); RouteData routeData = RouteTable.Routes.GetRouteData(currentContext); routeData.Values["culture"] = ci; Thread.CurrentThread.CurrentUICulture = ci; Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name); } }
_Layout.cs(允许用户更改语言)
// ... <ul class="dropdown-menu" role="menu"> <li class="@isCurrentLang("nl")">@Html.ActionLink("Nederlands", "ChangeCulture", "Culture", new { lang = "nl", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "nl" })</li> <li class="@isCurrentLang("en")">@Html.ActionLink("English", "ChangeCulture", "Culture", new { lang = "en", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "en" })</li> </ul> // ...
CultureController:(=我在GlobalAsax中设置用于更改CurrentCulture和CurrentUICulture的会话的位置)
public class CultureController : Controller { // GET: Culture public ActionResult Index() { return RedirectToAction("Index", "Home"); } public ActionResult ChangeCulture(string lang, string returnUrl) { Session["Culture"] = new CultureInfo(lang); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } }
这种方法存在多个问题,但是归结为工作流程问题。
CultureController
RedirectToAction
HttpContext.Current.Request.UserLanguages
第三个问题主要是由于微软和Google在如何处理全球化方面存在根本不同的看法。
微软的(原始)观点是,每种文化都应使用相同的URL UserLanguages,而浏览器的URL 应该确定网站应显示的语言。
UserLanguages
Google的观点是,每种文化都应托管在不同的URL上。如果您考虑一下,这更有意义。希望每个在搜索结果(SERP)中找到您的网站的人都能够以其母语搜索内容。
网站的全球化应该被视为 内容 而不是个性化-您是在向 一群 人而不是个人传播一种文化。因此,使用ASP.NET的任何个性化功能(例如会话状态或cookie)来实现全球化通常是没有意义的- 这些功能会阻止搜索引擎索引本地化页面的内容。
如果您可以简单地通过将用户路由到新的URL来使用户具有不同的文化,则无需担心- 您不需要单独的页面供用户选择其文化,只需在标题中包含一个链接或页脚更改现有页面的区域性,然后所有链接将自动切换到用户选择的区域性(因为MVC 自动重用当前请求中的路由值)。
首先,摆脱CultureController和Application_AcquireRequestState方法中的代码。
Application_AcquireRequestState
现在,由于文化是一个跨领域的问题,因此应在中设置当前线程的文化IAuthorizationFilter。这样可以确保ModelBinder在MVC中使用之前先设置区域性。
IAuthorizationFilter
ModelBinder
using System.Globalization; using System.Threading; using System.Web.Mvc; public class CultureFilter : IAuthorizationFilter { private readonly string defaultCulture; public CultureFilter(string defaultCulture) { this.defaultCulture = defaultCulture; } public void OnAuthorization(AuthorizationContext filterContext) { var values = filterContext.RouteData.Values; string culture = (string)values["culture"] ?? this.defaultCulture; CultureInfo ci = new CultureInfo(culture); Thread.CurrentThread.CurrentCulture = ci; Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name); } }
您可以通过将其注册为全局过滤器来全局设置过滤器。
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new CultureFilter(defaultCulture: "nl")); filters.Add(new HandleErrorAttribute()); } }
您可以通过链接到当前页面的相同动作和控制器,并将其作为选项包含在页面的页眉或页脚中来简化语言选择_Layout.cshtml。
_Layout.cshtml
@{ var routeValues = this.ViewContext.RouteData.Values; var controller = routeValues["controller"] as string; var action = routeValues["action"] as string; } <ul> <li>@Html.ActionLink("Nederlands", @action, @controller, new { culture = "nl" }, new { rel = "alternate", hreflang = "nl" })</li> <li>@Html.ActionLink("English", @action, @controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" })</li> </ul>
如前所述,页面上的所有其他链接将自动从当前上下文传递一种区域性,因此它们将自动停留在同一区域性中。在这些情况下,没有理由明确地传递文化。
@ActionLink("About", "About", "Home")
使用上述链接,如果当前URL为/Home/Contact,则生成的链接将为/Home/About。如果当前网址为/en/Home/Contact,则链接将生成为/en/Home/About。
/Home/Contact
/Home/About
/en/Home/Contact
/en/Home/About
最后,我们深入您的问题。无法正确生成默认区域性的原因是,路由是2向映射,并且无论您是匹配传入的请求还是生成传出的URL,始终都会赢得第一个匹配项。构建网址时,第一个匹配项是DefaultWithCulture。
DefaultWithCulture
通常,您可以简单地通过反转路由顺序来解决此问题。但是,在这种情况下,这将导致传入路由失败。
因此,您遇到的最简单的选择是建立一个自定义路由约束,以在生成URL时处理默认区域性的特殊情况。提供默认区域性时,您只需返回false,这将导致.NET路由框架跳过该DefaultWithCulture路由并移至下一个注册的路由(在本例中为Default)。
Default
using System.Text.RegularExpressions; using System.Web; using System.Web.Routing; public class CultureConstraint : IRouteConstraint { private readonly string defaultCulture; private readonly string pattern; public CultureConstraint(string defaultCulture, string pattern) { this.defaultCulture = defaultCulture; this.pattern = pattern; } public bool Match( HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if (routeDirection == RouteDirection.UrlGeneration && this.defaultCulture.Equals(values[parameterName])) { return false; } else { return Regex.IsMatch((string)values[parameterName], "^" + pattern + "$"); } } }
剩下的就是将约束添加到路由配置中。您还应该删除DefaultWithCulture路由中的区域性默认设置,因为无论如何,只要URL中提供了区域性,您都只希望使其匹配。Default另一方面,该路由应具有一种文化,因为无法通过URL传递该路由。
routes.LowercaseUrls = true; routes.MapRoute( name: "Errors", url: "Error/{action}/{code}", defaults: new { controller = "Error", action = "Other", code = UrlParameter.Optional } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } );
注意: 本部分仅在使用MVC 5时适用。如果使用早期版本,则可以跳过此部分。
对于AttributeRouting,您可以通过自动为每个动作创建2条不同的路线来简化操作。您需要对每条路线进行一些微调,并将它们添加到使用的相同类结构中MapMvcAttributeRoutes。不幸的是,Microsoft决定将这些类型设为内部类型,因此需要Reflection实例化并填充它们。
MapMvcAttributeRoutes
在这里,我们只使用MVC的内置功能来扫描我们的项目并创建一组路由,然后为区域性插入一个附加的路由URL前缀,然后再将CultureConstraint实例添加到我们的MVC RouteTable中。
CultureConstraint
还创建了一个单独的路由来解析URL(与AttributeRouting进行路由的方式相同)。
using System; using System.Collections; using System.Linq; using System.Reflection; using System.Web.Mvc; using System.Web.Mvc.Routing; using System.Web.Routing; public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object constraints) { MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(constraints)); } public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary constraints) { var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc"); var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc"); FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance); var subRoutes = Activator.CreateInstance(subRouteCollectionType); var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes); // Add the route entries collection first to the route collection routes.Add((RouteBase)routeEntries); var localizedRouteTable = new RouteCollection(); // Get a copy of the attribute routes localizedRouteTable.MapMvcAttributeRoutes(); foreach (var routeBase in localizedRouteTable) { if (routeBase.GetType().Equals(routeCollectionRouteType)) { // Get the value of the _subRoutes field var tempSubRoutes = subRoutesInfo.GetValue(routeBase); // Get the PropertyInfo for the Entries property PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries"); if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable))) { foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes)) { var route = routeEntry.Route; // Create the localized route var localizedRoute = CreateLocalizedRoute(route, urlPrefix, constraints); // Add the localized route entry var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute); AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry); // Add the default route entry AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry); // Add the localized link generation route var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute); routes.Add(localizedLinkGenerationRoute); // Add the default link generation route var linkGenerationRoute = CreateLinkGenerationRoute(route); routes.Add(linkGenerationRoute); } } } } } private static Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints) { // Add the URL prefix var routeUrl = urlPrefix + route.Url; // Combine the constraints var routeConstraints = new RouteValueDictionary(constraints); foreach (var constraint in route.Constraints) { routeConstraints.Add(constraint.Key, constraint.Value); } return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler); } private static RouteEntry CreateLocalizedRouteEntry(string name, Route route) { var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized"; return new RouteEntry(localizedRouteEntryName, route); } private static void AddRouteEntry(Type subRouteCollectionType, object subRoutes, RouteEntry newEntry) { var addMethodInfo = subRouteCollectionType.GetMethod("Add"); addMethodInfo.Invoke(subRoutes, new[] { newEntry }); } private static RouteBase CreateLinkGenerationRoute(Route innerRoute) { var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc"); return (RouteBase)Activator.CreateInstance(linkGenerationRouteType, innerRoute); } }
然后,只需调用此方法即可MapMvcAttributeRoutes。
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // Call to register your localized and default attribute routes routes.MapLocalizedMvcAttributeRoutes( urlPrefix: "{culture}/", constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }