ASP.NET MVC 5 культура в маршруте и URL-адресе

Я перевел свой веб-сайт 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/ п/Foo/бар/5 даже если я нажал на английский (см. _Layout.cs). Мой контент переведен на английский язык, но параметр маршрута в URL-адресе остается на "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");
      }
    }
  }

Ответ 1

Есть несколько проблем с этим подходом, но это сводится к проблеме рабочего процесса.

  • У вас есть CultureController, единственная цель которого - перенаправить пользователя на другую страницу на сайте. Имейте в виду, что RedirectToAction отправит HTTP 302-ответ на браузер пользователя, который скажет, что он ищет новое местоположение на вашем сервере. Это неоправданное круговое путешествие по сети.
  • Вы используете состояние сеанса для сохранения культуры пользователя, когда оно уже доступно в URL-адресе. В этом случае состояние сеанса совершенно не нужно.
  • Вы читаете HttpContext.Current.Request.UserLanguages у пользователя, который может отличаться от культуры, которую они запрашивали в URL-адресе.

Третий вопрос связан прежде всего с принципиально иным представлением Microsoft и Google о том, как справляться с глобализацией.

Microsoft (оригинальное) представление заключалось в том, что один и тот же URL-адрес должен использоваться для каждой культуры и что UserLanguages браузера должен определять, на каком языке должен отображаться веб-сайт.

Google выглядит так: каждая культура должна размещаться на другом URL. Это имеет смысл, если вы думаете об этом. Желательно, чтобы каждый человек нашел ваш сайт в результатах поиска (SERP), чтобы иметь возможность искать контент на своем родном языке.

Глобализация веб-сайта должна рассматриваться как контент, а не персонализация - вы передаете культуру группе людей, а не отдельному человеку. Поэтому обычно не имеет смысла использовать любые функции персонализации ASP.NET, такие как состояние сеанса или файлы cookie для реализации глобализации, - эти функции не позволяют поисковым системам индексировать содержимое ваших локализованных страниц.

Если вы можете отправить пользователя в другую культуру, просто направив их на новый URL-адрес, вам будет гораздо меньше беспокоиться - вам не нужна отдельная страница для выбора пользователем своей культуры, просто укажите ссылку в верхнем или нижнем колонтитуле изменить культуру существующей страницы, а затем все ссылки автоматически переключится на культуру, которую выбрал пользователь (поскольку MVC автоматически повторяет маршрут значения из текущего запроса).

Фиксирование проблем

Прежде всего, избавьтесь от CultureController и кода в методе Application_AcquireRequestState.

CultureFilter

Теперь, поскольку культура является сквозной проблемой, настройка культуры текущей нити должна выполняться в IAuthorizationFilter. Это гарантирует, что культура установлена ​​до использования ModelBinder в 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);
    }
}

Вы можете установить фильтр глобально, зарегистрировав его как глобальный фильтр.

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

Выбор языка

Вы можете упростить выбор языка, перенаправляя его к одному и тому же действию и контроллеру для текущей страницы и включив его в качестве опции в верхний или нижний колонтитул страницы в _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. Если текущий URL /en/Home/Contact, ссылка будет сгенерирована как /en/Home/About.

Культура по умолчанию

Наконец, мы дошли до сути вашего вопроса. Причина, по которой ваша культура по умолчанию не генерируется правильно, заключается в том, что маршрутизация представляет собой двухстороннюю карту и независимо от того, соответствует ли вам входящий запрос или генерирует исходящий URL-адрес, первое совпадение всегда выигрывает. При создании URL-адреса первое совпадение - DefaultWithCulture.

Обычно вы можете исправить это просто, изменив порядок маршрутов. Однако в вашем случае это приведет к сбою входящих маршрутов.

Итак, самым простым вариантом в вашем случае является создание настраиваемого ограничения маршрута для обработки специального случая культуры по умолчанию при создании URL-адреса, Вы просто возвращаете false, когда включена культура по умолчанию, и это приведет к тому, что инфраструктура .NET-маршрутизации пропустит маршрут DefaultWithCulture и переместится на следующий зарегистрированный маршрут (в этом случае 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 }
);

AttributeRouting

ПРИМЕЧАНИЕ. Этот раздел применяется только в том случае, если вы используете MVC 5. Вы можете пропустить это, если используете предыдущую версию.

Для AttributeRouting вы можете упростить процесс, автоматизируя создание двух разных маршрутов для каждого действия. Вам нужно немного настроить каждый маршрут и добавить их в ту же структуру классов, что использует MapMvcAttributeRoutes. К сожалению, Microsoft решила сделать типы внутренними, поэтому требуется, чтобы Reflection создавал и заполнял их.

RouteCollectionExtensions

Здесь мы просто используем встроенные функции MVC для сканирования нашего проекта и создания набора маршрутов, затем вставляем дополнительный URL-адрес маршрута для культуры и CultureConstraint перед добавлением экземпляров в наш MVC RouteTable.

Существует также отдельный маршрут, который создается для разрешения 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 }
        );
    }
}

Ответ 2

Невероятный пост от NightOwl888. Однако есть кое-что отсутствует - обычные (не локализованные) маршруты атрибутов URL-адреса, которые добавляются через отражение, также нуждаются в параметре культуры по умолчанию, иначе вы получите параметр запроса в URL-адресе.

? Culture = п

Чтобы избежать этого, эти изменения должны быть сделаны:

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

И для атрибуции регистрации маршрутов:

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