Ответ 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 }
);
}
}