Cultura ASP.NET MVC 5 in rotta e url

Ho tradotto il mio sito Web mvc, che funziona perfettamente. Se seleziono un’altra lingua (olandese o inglese) il contenuto viene tradotto. Questo funziona perché ho impostato la cultura nella sessione.

Ora voglio mostrare la cultura selezionata (= cultura) nell’URL. Se è la lingua predefinita, non dovrebbe essere mostrata nell’url, solo se non è la lingua predefinita dovrebbe mostrarla nell’URL.

per esempio:

Per cultura predefinita (olandese):

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

Per cultura non predefinita (inglese):

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

Il mio problema è che vedo sempre questo:

site.com/ nl / foo / bar / 5 anche se ho fatto clic su inglese (vedi _Layout.cs). Il mio contenuto è tradotto in inglese, ma il parametro del percorso nell’URL rimane su “nl” anziché su “en”.

Come posso risolvere questo o cosa sto sbagliando?

Ho provato in global.asax per impostare RouteData ma non aiuta.

  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 (dove ho lasciato che l’utente cambi lingua)

 // ...  // ... 

CultureController: (= dove imposto la sessione che uso in GlobalAsax per modificare CurrentCulture e 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"); } } } 

Ci sono diversi problemi con questo approccio, ma si riduce ad essere un problema di stream di lavoro.

  1. Hai un controllo CultureController cui unico scopo è quello di redirect l’utente a un’altra pagina del sito. Tieni presente che RedirectToAction invierà una risposta HTTP 302 al browser dell’utente, che gli dirà di cercare la nuova posizione sul tuo server. Questa è un’inutile andata e ritorno attraverso la rete.
  2. Stai utilizzando lo stato di sessione per archiviare la cultura dell’utente quando è già disponibile nell’URL. Lo stato della sessione è totalmente inutile in questo caso.
  3. Stai leggendo HttpContext.Current.Request.UserLanguages dall’utente, che potrebbe essere diverso dalla cultura richiesta nell’URL.

Il terzo problema è principalmente dovuto a una visione fondamentalmente diversa tra Microsoft e Google su come gestire la globalizzazione.

La vista (originale) di Microsoft era che lo stesso URL dovrebbe essere usato per ogni cultura e che le UserLanguages del browser dovrebbero determinare quale lingua deve essere visualizzata sul sito.

L’opinione di Google è che ogni cultura dovrebbe essere ospitata su un URL diverso . Questo ha più senso se ci pensi. È auspicabile che ogni persona che trova il proprio sito Web nei risultati di ricerca (SERP) sia in grado di cercare il contenuto nella propria lingua madre.

La globalizzazione di un sito web dovrebbe essere vista come contenuto piuttosto che come personalizzazione: stai trasmettendo una cultura a un gruppo di persone, non a una singola persona. Di conseguenza, in genere non ha senso utilizzare le funzionalità di personalizzazione di ASP.NET come lo stato della sessione o i cookie per implementare la globalizzazione: queste funzionalità impediscono ai motori di ricerca di indicizzare il contenuto delle pagine localizzate.

Se puoi inviare l’utente a una cultura diversa semplicemente indirizzandoli a un nuovo URL, c’è molto meno da preoccuparsi – non è necessaria una pagina separata per consentire all’utente di selezionare la propria cultura, basta includere un link nell’intestazione o il piè di pagina per modificare la cultura della pagina esistente e quindi tutti i collegamenti passeranno automaticamente alla cultura scelta dall’utente (poiché MVC riutilizza automaticamente i valori della rotta dalla richiesta corrente ).

Risolvere i problemi

Prima di tutto, elimina CultureController e il codice nel metodo Application_AcquireRequestState .

CultureFilter

Ora, dal momento che la cultura è una preoccupazione trasversale, l’impostazione della cultura del thread corrente dovrebbe essere fatta in un IAuthorizationFilter . Ciò garantisce che la cultura sia impostata prima che ModelBinder venga utilizzato in 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); } } 

È ansible impostare il filtro globalmente registrandolo come filtro globale.

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

Selezione della lingua

È ansible semplificare la selezione della lingua tramite il collegamento alla stessa azione e al controller per la pagina corrente e includendola come opzione nell’intestazione o nel piè di pagina della pagina _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" })

Come accennato in precedenza, a tutti gli altri link della pagina verrà automaticamente passata una cultura dal contesto corrente, in modo che rimangano automaticamente nella stessa cultura. Non c’è motivo di trasmettere la cultura esplicitamente in questi casi.

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

Con il link sopra, se l’URL corrente è /Home/Contact , il link che verrà generato sarà /Home/About . Se l’URL corrente è /en/Home/Contact , il collegamento verrà generato come /en/Home/About .

Cultura predefinita

Finalmente, arriviamo al cuore della tua domanda. Il motivo per cui la cultura predefinita non viene generata correttamente è perché il routing è una mappa a 2 vie e indipendentemente dal fatto che si corrisponda a una richiesta in entrata o che si generi un URL in uscita, vince sempre la prima corrispondenza. Quando si DefaultWithCulture l’URL, la prima corrispondenza è DefaultWithCulture .

Normalmente, puoi sistemarlo semplicemente invertendo l’ordine dei percorsi. Tuttavia, nel tuo caso ciò causerebbe il fallimento delle rotte in arrivo.

Quindi, l’opzione più semplice nel tuo caso è quella di build un vincolo del percorso personalizzato per gestire il caso speciale della cultura predefinita durante la generazione dell’URL. È sufficiente restituire false quando viene fornita la cultura predefinita e il framework di routing .NET farà saltare la rotta DefaultWithCulture e passerà alla successiva route registrata (in questo 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 + "$"); } } } 

Non resta che aggiungere il vincolo alla configurazione del routing. Dovresti anche rimuovere l’impostazione predefinita per cultura nella rotta DefaultWithCulture poiché desideri che corrisponda solo quando esiste una cultura fornita nell’URL. L’itinerario Default altra parte dovrebbe avere una cultura perché non c’è modo di passarlo attraverso l’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: questa sezione si applica solo se si utilizza MVC 5. È ansible saltare questo se si utilizza una versione precedente.

Per AttributeRouting, puoi semplificare le cose automatizzando la creazione di 2 percorsi diversi per ogni azione. È necessario modificare leggermente ogni percorso e aggiungerli alla stessa struttura di class utilizzata da MapMvcAttributeRoutes . Sfortunatamente, Microsoft ha deciso di rendere i tipi interni così da richiedere a Reflection di istanziarli e popolarli.

RouteCollectionExtensions

Qui utilizziamo semplicemente le funzionalità incorporate di MVC per analizzare il nostro progetto e creare una serie di percorsi, quindi inserire un prefisso URL del percorso aggiuntivo per la cultura e CultureConstraint prima di aggiungere le istanze al nostro MVC RouteTable.

C’è anche una rotta separata che viene creata per risolvere gli URL (nello stesso modo in cui AttributeRouting lo fa).

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

Quindi si tratta solo di chiamare questo metodo al posto di 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 } ); } } 

Correzione cultura predefinita

Post incredibile di NightOwl888. Tuttavia, manca qualcosa: le normali (non localizzate) route degli attributi di generazione URL, che vengono aggiunte tramite la reflection, richiedono anche un parametro cultura predefinito, altrimenti si ottiene un parametro di query nell’URL.

? La cultura = nl

Per evitare questo, è necessario apportare queste modifiche:

 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 per attribuire la registrazione dei percorsi:

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

Soluzione migliore

E in effetti, dopo un po ‘di tempo, avevo bisogno di aggiungere la traduzione dell’URL, quindi ho approfondito di più, e sembra che non sia necessario fare l’hacking della riflessione descritto. I ragazzi di ASP.NET ci hanno pensato, c’è una soluzione molto più pulita – invece puoi estendere un DefaultDirectRouteProvider in questo modo:

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

C’è una soluzione basata su questo, inclusa la traduzione dell’URL qui: https://github.com/boudinov/mvc-5-routing-localization