ASP.NET MVC 5文化路由和网址

juF*_*uFo 50 .net c# asp.net-mvc asp.net-mvc-routing asp.net-mvc-5

我翻译了我的mvc网站,这个网站运行得很好.如果我选择其他语言(荷兰语或英语),内容将被翻译.这是有效的,因为我在会话中设置了文化.

现在我想在网址中显示所选文化(=文化).如果它是默认语言,则不应在网址中显示,只有当它不是默认语言时才应在网址中显示.

例如:

对于默认文化(荷兰语):

site.com/foo
site.com/foo/bar
site.com/foo/bar/5
Run Code Online (Sandbox Code Playgroud)

对于非默认文化(英语):

site.com/en/foo
site.com/en/foo/bar
site.com/en/foo/bar/5
Run Code Online (Sandbox Code Playgroud)

我的问题是我总是看到这个:

site.com/ nl/foo/bar/5即使我点击了英文(参见_Layout.cs).我的内容是用英文翻译的,但网址中的路由参数仍然是"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 }
      );
    }
Run Code Online (Sandbox Code Playgroud)

的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);
      }
    }
Run Code Online (Sandbox Code Playgroud)

_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>
// ...
Run Code Online (Sandbox Code Playgroud)

CultureController :( =我设置我在GlobalAsax中使用的Session来更改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");
      }
    }
  }
Run Code Online (Sandbox Code Playgroud)

Nig*_*888 126

这种方法存在一些问题,但归结为工作流问题.

  1. CultureController的唯一目的是将用户重定向到网站上的另一个页面.请记住,RedirectToAction将向用户的浏览器发送HTTP 302响应,该响应将告诉它查找服务器上的新位置.这是整个网络不必要的往返.
  2. 您正在使用会话状态来存储URL中已有的用户文化.在这种情况下,会话状态完全没有必要.
  3. 您正在阅读HttpContext.Current.Request.UserLanguages用户,这可能与他们在URL中请求的文化不同.

第三个问题主要是因为微软和谷歌之间关于如何处理全球化的根本不同观点.

微软(原创)的观点是,应该为每种文化使用相同的URL UserLanguages,并且浏览器应该确定网站应该显示的语言.

谷歌认为,每种文化都应该托管在不同的网址上.如果你仔细想想,这就更有意义了.在搜索结果(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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以通过将过滤器注册为全局过滤器来全局设置过滤器.

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

语言选择

您可以通过链接到当前页面的相同操作和控制器并将其作为选项包含在您的页面页眉或页脚中来简化语言选择_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>
Run Code Online (Sandbox Code Playgroud)

如前所述,页面上的所有其他链接将自动从当前上下文传递文化,因此它们将自动保持在同一文化中.在这些情况下,没有理由明确传递文化.

@ActionLink("About", "About", "Home")
Run Code Online (Sandbox Code Playgroud)

使用上面的链接,如果当前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 + "$");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

剩下的就是将约束添加到路由配置中.您还应该删除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 }
);
Run Code Online (Sandbox Code Playgroud)

AttributeRouting

注意:本节仅适用于您使用MVC 5.如果您使用的是以前的版本,则可以跳过此步骤.

对于AttributeRouting,您可以通过为每个操作自动创建2个不同的路径来简化操作.您需要稍微调整每个路径并将它们添加到使用的相同类结构中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);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,只需要调用此方法而不是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 }
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢!这不仅可以给出答案,还可以让我更好地了解内部运作.我已经学到了一些额外的想法和使用像RouteConstraint这样的新课程!希望每个答案都这么好! (5认同)
  • 您只需要确保为每个操作创建默认路径和本地化路由,并且通常需要在映射任何`MapRoute`路由之前注册`routes.MapMvcAttributeRoutes()`.在这种情况下,您可以在`routes.LowercaseUrls = true;`之后立即调用它.您需要使用`IInlineConstraintResolver`来添加自定义约束[如此处所示](http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc -5.aspx#路由约束).您还应该使用属性路由的`Order`属性来确保它们以正确的顺序注册. (2认同)
  • @ A.Sideris - 使用上述解决方案,搜索引擎将使用它支持的每种语言为站点编制索引.许多(如果不是大多数)用户将通过他们的母语搜索找到该网站,因此他们在到达网站时无需更改语言.为网站添加书签/存储URL的所有用户都将以自己的语言返回.可能会以不同语言看到网站的唯一流量是那些在浏览器中手动输入"somesite.com"的流量,这可能是网站总流量的很小一部分. (2认同)
  • @ A.Sideris - 如果您还选择合理的默认语言(对于网站的大部分流量),需要手动更改为自己语言的用户数量可以忽略不计.请记住,一些"超级用户"会知道在URL的末尾添加`/ en`.您也可以使用`subdomain`(en.somesite.com)而不是每种语言的备用路径,这可能会使其更直观.但我不担心那些必须手动更改的人 - 只需通过使用标志图标而不是文本来识别选择控件,并将其放在可见位置. (2认同)

Pla*_*nov 10

默认文化修复

由NightOwl888发布的令人难以置信的帖子.但是缺少一些东西 - 正常(非本地化)的URL生成属性路由(通过反射添加)也需要默认的culture参数,否则您将在URL中获得查询参数.

?文化= NL

为了避免这种情况,必须进行以下更改:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并归属路线注册:

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

好的解决方案

实际上,经过一段时间后,我需要添加url翻译,所以我挖掘了更多,并且似乎没有必要进行描述的反射黑客攻击.ASP.NET人员考虑过它,有更清晰的解决方案 - 相反,你可以像这样扩展一个DefaultDirectRouteProvider:

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<RouteEntry> GetActionDirectRoutes(
                ActionDescriptor actionDescriptor,
                IReadOnlyList<IDirectRouteFactory> factories,
                IInlineConstraintResolver constraintResolver)
    {
        var originalEntries = base.GetActionDirectRoutes(actionDescriptor, factories, constraintResolver);
        var finalEntries = new List<RouteEntry>();

        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);
    }
}
Run Code Online (Sandbox Code Playgroud)

有一个基于此的解决方案,包括网址翻译:https://github.com/boudinov/mvc-5-routing-localization