Cultura ASP.NET MVC 5 en ruta y url

He traducido mi sitio web mvc, que funciona muy bien. Si selecciono otro idioma (holandés o inglés), el contenido se traduce. Esto funciona porque configuro la cultura en la sesión.

Ahora quiero mostrar la cultura seleccionada (= cultura) en la url. Si es el idioma predeterminado, no se debe mostrar en la url, solo si no es el idioma predeterminado, debe mostrarse en la url.

p.ej:

Para cultura por defecto (holandés):

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

Para cultura no predeterminada (inglés):

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

Mi problema es que siempre veo esto:

site.com/ nl / foo / bar / 5 incluso si hice clic en inglés (ver _Layout.cs). Mi contenido está traducido en inglés, pero el parámetro de ruta en la url permanece en “nl” en lugar de “en”.

¿Cómo puedo resolver esto o qué estoy haciendo mal?

Intenté en el archivo.asx global configurar RouteData pero no ayuda.

  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 (donde dejo que el usuario cambie el idioma)

 // ...  // ... 

CultureController: (= donde establecí la sesión que uso en GlobalAsax para cambiar CurrentCulture y 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"); } } } 

Hay varios problemas con este enfoque, pero se reduce a ser un problema de flujo de trabajo.

  1. Usted tiene un CultureController cuyo único propósito es redirigir al usuario a otra página en el sitio. Tenga en cuenta que RedirectToAction enviará una respuesta HTTP 302 al navegador del usuario, que le indicará que busque la nueva ubicación en su servidor. Este es un viaje de ida y vuelta innecesario a través de la red.
  2. Está utilizando el estado de la sesión para almacenar la cultura del usuario cuando ya está disponible en la URL. El estado de la sesión es totalmente innecesario en este caso.
  3. Está leyendo los HttpContext.Current.Request.UserLanguages del usuario, que pueden ser diferentes de la cultura que solicitaron en la URL.

El tercer problema se debe principalmente a una visión fundamentalmente diferente entre Microsoft y Google sobre cómo manejar la globalización.

La vista (original) de Microsoft era que se debía usar la misma URL para cada cultura y que los UserLanguages de UserLanguages del navegador deberían determinar qué idioma debería mostrar el sitio web.

La opinión de Google es que cada cultura debe estar alojada en una URL diferente . Esto tiene más sentido si lo piensas. Es deseable que cada persona que encuentre su sitio web en los resultados de búsqueda (SERP) pueda buscar el contenido en su idioma nativo.

La globalización de un sitio web debería verse como contenido en lugar de personalización: estás transmitiendo una cultura a un grupo de personas, no a una persona en particular. Por lo tanto, normalmente no tiene sentido utilizar las características de personalización de ASP.NET, como el estado de la sesión o las cookies para implementar la globalización; estas características impiden que los motores de búsqueda indexen el contenido de sus páginas localizadas.

Si puede enviar al usuario a una cultura diferente simplemente enrutando a una nueva URL, hay mucho menos de qué preocuparse: no necesita una página separada para que el usuario seleccione su cultura, simplemente incluya un enlace en el encabezado. o pie de página para cambiar la cultura de la página existente y luego todos los enlaces cambiarán automáticamente a la cultura que el usuario haya elegido (porque MVC reutiliza automáticamente los valores de ruta de la solicitud actual ).

Reparar los problemas

Antes que nada, deshazte del CultureController y el código en el método Application_AcquireRequestState .

CultureFilter

Ahora, dado que la cultura es una preocupación transversal, establecer la cultura del hilo actual se debe hacer en un IAuthorizationFilter . Esto garantiza que la cultura se establece antes de que se use ModelBinder en 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); } } 

Puede establecer el filtro globalmente registrándolo como un filtro global.

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

Selección de idioma

Puede simplificar la selección del idioma vinculando la misma acción y controlador para la página actual e incluyéndola como una opción en el encabezado o pie de página de su _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 se mencionó anteriormente, todos los demás enlaces de la página pasarán automáticamente una cultura del contexto actual, por lo que permanecerán automáticamente dentro de la misma cultura. No hay ninguna razón para pasar la cultura explícitamente en esos casos.

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

Con el enlace de arriba, si la URL actual es /Home/Contact , el enlace que se generará será /Home/About . Si la URL actual es /en/Home/Contact , el enlace se generará como /en/Home/About .

Cultura predeterminada

Finalmente, llegamos al corazón de tu pregunta. La razón por la que su cultura predeterminada no se genera correctamente es porque el enrutamiento es un mapa bidireccional e independientemente de si está haciendo coincidir una solicitud entrante o generando una URL saliente, la primera coincidencia siempre gana. Al construir su URL, la primera coincidencia es DefaultWithCulture .

Normalmente, puede solucionar esto simplemente invirtiendo el orden de las rutas. Sin embargo, en su caso eso causaría que las rutas entrantes fallaran.

Entonces, la opción más simple en su caso es construir una restricción de ruta personalizada para manejar el caso especial de la cultura predeterminada al generar la URL. Simplemente devuelve falso cuando se suministra el cultivo predeterminado y hará que el marco de enrutamiento .NET omita la ruta DefaultWithCulture y se mueva a la siguiente ruta registrada (en este caso, por 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 + "$"); } } } 

Todo lo que queda es agregar la restricción a su configuración de enrutamiento. También debe eliminar la configuración predeterminada para el cultivo en la ruta DefaultWithCulture ya que solo desea que coincida cuando haya un cultivo en la URL de todos modos. La ruta Default , por otro lado, debería tener una cultura porque no hay forma de pasarla a través de la 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 } ); 

AttributeRouting

NOTA: Esta sección se aplica solo si está utilizando MVC 5. Puede omitir esto si está utilizando una versión anterior.

Para AttributeRouting, puede simplificar las cosas al automatizar la creación de 2 rutas diferentes para cada acción. MapMvcAttributeRoutes ajustar un poco cada ruta y agregarlas a la misma estructura de clases que utiliza MapMvcAttributeRoutes . Desafortunadamente, Microsoft decidió hacer los tipos internos, por lo que requiere Reflection para instanciarlos y poblarlos.

RouteCollectionExtensions

Aquí solo usamos la funcionalidad incorporada de MVC para escanear nuestro proyecto y crear un conjunto de rutas, luego insertamos un prefijo URL de ruta adicional para el cultivo y el CultureConstraint antes de agregar las instancias a nuestro MVC RouteTable.

También hay una ruta separada que se crea para resolver las URL (de la misma manera que lo hace 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); } } 

Entonces solo se trata de llamar a este método en lugar 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 } ); } } 

Ajuste cultural predeterminado

Publicación increíble de NightOwl888. Sin embargo, falta algo: las rutas de los atributos de generación de URL normales (no localizadas), que se agregan a través de la reflexión, también necesitan un parámetro de cultura predeterminado; de lo contrario, se obtiene un parámetro de consulta en la URL.

? culture = nl

Para evitar esto, estos cambios deben hacerse:

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

Y para atribuir el registro de rutas:

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

Mejor solución

Y en realidad, después de un tiempo, necesité agregar la traducción de url, así que busqué más, y parece que no hay necesidad de hacer el hacking de reflexión descrito. Los chicos de ASP.NET lo pensaron, hay una solución mucho más limpia; en su lugar, puedes extender un DefaultDirectRouteProvider así:

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

Hay una solución basada en esto, incluida la traducción de URL aquí: https://github.com/boudinov/mvc-5-routing-localization