Cultura ASP.NET MVC 5 em rota e url

Traduzi meu site mvc, que está funcionando muito bem. Se eu selecionar outro idioma (holandês ou inglês), o conteúdo será traduzido. Isso funciona porque eu defini a cultura na session.

Agora quero mostrar a cultura selecionada (= cultura) no URL. Se for o idioma padrão, ele não deverá ser mostrado na URL, apenas se não for o idioma padrão, ele deverá ser exibido no URL.

por exemplo:

Para cultura padrão (holandês):

site.com/foo site.com/foo/bar site.com/foo/bar/5 

Para cultura não padrão (inglês):

 site.com/en/foo site.com/en/foo/bar site.com/en/foo/bar/5 

Meu problema é que sempre vejo isso:

site.com/ nl / foo / bar / 5 mesmo se eu clicar em inglês (consulte _Layout.cs). Meu conteúdo é traduzido em inglês, mas o parâmetro de rota na URL permanece em “nl” em vez de “en”.

Como posso resolver isso ou o que estou fazendo errado?

Eu tentei no global.asax para definir o RouteData, mas não ajuda.

  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 = "[az]{2}" } );// or maybe: "[az]{2}-[az]{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 (onde eu deixo o usuário mudar de idioma)

 // ...  // ... 

CultureController: (= onde eu configurei a Session que eu uso no GlobalAsax para mudar o CurrentCulture e o 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"); } } } 

Existem vários problemas com essa abordagem, mas tudo se resume a ser um problema de stream de trabalho.

  1. Você tem um CultureController cuja única finalidade é redirect o usuário para outra página no site. Lembre-se de que o RedirectToAction enviará uma resposta HTTP 302 ao navegador do usuário, informando-o para procurar o novo local em seu servidor. Esta é uma viagem de ida e volta desnecessária pela rede.
  2. Você está usando o estado da session para armazenar a cultura do usuário quando ele já estiver disponível na URL. O estado da session é totalmente desnecessário neste caso.
  3. Você está lendo o HttpContext.Current.Request.UserLanguages do usuário, que pode ser diferente da cultura que eles solicitaram na URL.

A terceira questão é principalmente por causa de uma visão fundamentalmente diferente entre a Microsoft e o Google sobre como lidar com a globalização.

A visão (original) da Microsoft era de que a mesma URL deveria ser usada para cada cultura e que as linguagens de usuário do navegador deveriam determinar qual idioma o site deveria exibir.

A opinião do Google é que toda cultura deve ser hospedada em um URL diferente . Isso faz mais sentido se você pensar sobre isso. É desejável que todas as pessoas que encontrarem seu site nos resultados da pesquisa (SERPs) possam pesquisar o conteúdo em seu idioma nativo.

A globalização de um site deve ser vista como conteúdo e não como personalização – você está transmitindo uma cultura para um grupo de pessoas, não para uma pessoa individual. Portanto, normalmente não faz sentido usar quaisquer resources de personalização do ASP.NET, como estado de session ou cookies, para implementar a globalização – esses resources impedem que os mecanismos de pesquisa indexem o conteúdo de suas páginas localizadas.

Se você puder enviar o usuário para uma cultura diferente simplesmente roteando-os para um novo URL, há muito menos motivos para se preocupar – você não precisa de uma página separada para o usuário selecionar sua cultura, simplesmente include um link no header ou rodapé para alterar a cultura da página existente e, em seguida, todos os links alternarão automaticamente para a cultura escolhida pelo usuário (porque o MVC reutiliza automaticamente os valores de rota da solicitação atual ).

Corrigindo os problemas

Primeiro de tudo, livre-se do CultureController e do código no método Application_AcquireRequestState .

CultureFilter

Agora, como a cultura é uma preocupação transversal, definir a cultura do segmento atual deve ser feito em um IAuthorizationFilter . Isso garante que a cultura seja definida antes que o ModelBinder seja usado no MVC.

 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); } } 

Você pode definir o filtro globalmente, registrando-o como um filtro global.

 public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new CultureFilter(defaultCulture: "nl")); filters.Add(new HandleErrorAttribute()); } } 

Seleção de idioma

Você pode simplificar a seleção do idioma vinculando à mesma ação e ao controlador da página atual e incluindo-a como uma opção no header ou no rodapé da página em seu _Layout.cshtml .

 @{ var routeValues = this.ViewContext.RouteData.Values; var controller = routeValues["controller"] as string; var action = routeValues["action"] as string; } 
  • @Html.ActionLink("Nederlands", @action, @controller, new { culture = "nl" }, new { rel = "alternate", hreflang = "nl" })
  • @Html.ActionLink("English", @action, @controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" })

Como mencionado anteriormente, todos os outros links na página serão automaticamente transmitidos a uma cultura do contexto atual, para que eles permaneçam automaticamente dentro da mesma cultura. Não há razão para passar a cultura explicitamente nesses casos.

 @ActionLink("About", "About", "Home") 

Com o link acima, se o URL atual for /Home/Contact , o link gerado será /Home/About . Se o URL atual for /en/Home/Contact , o link será gerado como /en/Home/About .

Cultura Padrão

Finalmente, chegamos ao cerne da sua pergunta. O motivo pelo qual sua cultura padrão não está sendo gerada corretamente é porque o roteamento é um mapa bidirecional e, independentemente de você estar correspondendo a uma solicitação recebida ou de gerar uma URL de saída, a primeira correspondência sempre vence. Ao criar seu URL, a primeira correspondência é DefaultWithCulture .

Normalmente, você pode consertar isso simplesmente invertendo a ordem das rotas. No entanto, no seu caso, isso causaria falha nas rotas recebidas.

Portanto, a opção mais simples no seu caso é criar uma restrição de rota personalizada para manipular o caso especial da cultura padrão ao gerar a URL. Você simplesmente retorna false quando a cultura padrão é fornecida e fará com que a estrutura de roteamento .NET pule a rota DefaultWithCulture e vá para a próxima rota registrada (nesse caso, 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 + "$"); } } } 

Tudo o que resta é adicionar a restrição à sua configuração de roteamento. Você também deve remover a configuração padrão para cultura na rota DefaultWithCulture , já que você só deseja que ela corresponda quando houver uma cultura fornecida no URL mesmo assim. A rota Default , por outro lado, deve ter uma cultura, porque não há maneira de passá-lo através do 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: "[az]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); 

AtributoRouting

NOTA: Esta seção se aplica somente se você estiver usando o MVC 5. Você pode pular esta se você estiver usando uma versão anterior.

Para AttributeRouting, você pode simplificar as coisas automatizando a criação de duas rotas diferentes para cada ação. Você precisa ajustar cada rota um pouco e adicioná-las à mesma estrutura de class que o MapMvcAttributeRoutes usa. Infelizmente, a Microsoft decidiu tornar os tipos internos, de modo que requer o Reflection para instanciá-los e preenchê-los.

RouteCollectionExtensions

Aqui, usamos apenas a funcionalidade incorporada do MVC para varrer nosso projeto e criar um conjunto de rotas, em seguida, inserir um prefixo de URL de rota adicional para a cultura e o CultureConstraint antes de adicionar as instâncias à nossa MVC RouteTable.

Há também uma rota separada que é criada para resolver as URLs (da mesma forma que o AttributeRouting faz isso).

 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); } } 

Então é só uma questão de chamar esse método em vez de 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: "[az]{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: "[az]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } 

Correção de cultura padrão

Incrível post de NightOwl888. No entanto, há algo faltando – as rotas de atributo de geração de URL normais (não localizadas), que são adicionadas por reflection, também precisam de um parâmetro de cultura padrão, caso contrário, você obtém um parâmetro de consulta no URL.

? culture = nl

Para evitar isso, essas alterações devem ser feitas:

 using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Web; using System.Web.Mvc; using System.Web.Mvc.Routing; using System.Web.Routing; namespace Endpoints.WebPublic.Infrastructure.Routing { public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object defaults, object constraints) { MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); } public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary defaults, 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 //FIX: needed for default culture on normal attribute route var newDefaults = new RouteValueDictionary(defaults); route.Defaults.ToList().ForEach(x => newDefaults.Add(x.Key, x.Value)); var routeWithNewDefaults = new Route(route.Url, newDefaults, route.Constraints, route.DataTokens, route.RouteHandler); var linkGenerationRoute = CreateLinkGenerationRoute(routeWithNewDefaults); 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); } } } 

E para atribuir o registro de rotas:

  RouteTable.Routes.MapLocalizedMvcAttributeRoutes( urlPrefix: "{culture}/", defaults: new { culture = "nl" }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[az]{2}") } ); 

Melhor solução

E, na verdade, depois de algum tempo, eu precisei adicionar tradução de URL, então eu investiguei mais, e parece que não há necessidade de fazer o reflexo de hacking descrito. Os caras do ASP.NET pensaram nisso, existe uma solução muito mais limpa – em vez disso, você pode estender um DefaultDirectRouteProvider assim:

 public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string defaultCulture) { var routeProvider = new LocalizeDirectRouteProvider( "{culture}/", defaultCulture ); routes.MapMvcAttributeRoutes(routeProvider); } } class LocalizeDirectRouteProvider : DefaultDirectRouteProvider { ILogger _log = LogManager.GetCurrentClassLogger(); string _urlPrefix; string _defaultCulture; RouteValueDictionary _constraints; public LocalizeDirectRouteProvider(string urlPrefix, string defaultCulture) { _urlPrefix = urlPrefix; _defaultCulture = defaultCulture; _constraints = new RouteValueDictionary() { { "culture", new CultureConstraint(defaultCulture: defaultCulture) } }; } protected override IReadOnlyList GetActionDirectRoutes( ActionDescriptor actionDescriptor, IReadOnlyList factories, IInlineConstraintResolver constraintResolver) { var originalEntries = base.GetActionDirectRoutes(actionDescriptor, factories, constraintResolver); var finalEntries = new List(); foreach (RouteEntry originalEntry in originalEntries) { var localizedRoute = CreateLocalizedRoute(originalEntry.Route, _urlPrefix, _constraints); var localizedRouteEntry = CreateLocalizedRouteEntry(originalEntry.Name, localizedRoute); finalEntries.Add(localizedRouteEntry); originalEntry.Route.Defaults.Add("culture", _defaultCulture); finalEntries.Add(originalEntry); } return finalEntries; } private 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 RouteEntry CreateLocalizedRouteEntry(string name, Route route) { var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized"; return new RouteEntry(localizedRouteEntryName, route); } } 

Existe uma solução baseada nisso, incluindo a tradução do URL aqui: https://github.com/boudinov/mvc-5-routing-localization