Budaya ASP.NET MVC 5 di rute dan url

Sep 25 2015

Saya telah menerjemahkan situs web MVC saya, yang berfungsi dengan baik. Jika saya memilih bahasa lain (Belanda atau Inggris) konten akan diterjemahkan. Ini berhasil karena saya menetapkan budaya dalam sesi tersebut.

Sekarang saya ingin menunjukkan budaya yang dipilih (= budaya) di url. Jika ini adalah bahasa default, itu tidak boleh ditampilkan di url, hanya jika itu bukan bahasa default, itu harus ditampilkan di url.

misalnya:

Untuk budaya default (Belanda):

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

Untuk budaya non-default (bahasa Inggris):

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

Masalah saya adalah saya selalu melihat ini:

site.com/ nl / foo / bar / 5 meskipun saya mengklik bahasa Inggris (lihat _Layout.cs). Konten saya diterjemahkan ke dalam bahasa Inggris tetapi parameter rute di url tetap di "nl" bukan "en".

Bagaimana saya bisa mengatasi ini atau apa yang saya lakukan salah?

Saya mencoba di global.asax untuk mengatur RouteData tetapi tidak membantu.

  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 (di mana saya membiarkan pengguna mengubah bahasa)

// ...
                            <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: (= di mana saya mengatur Sesi yang saya gunakan di GlobalAsax untuk mengubah CurrentCulture dan 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");
      }
    }
  }

Jawaban

133 NightOwl888 Sep 29 2015 at 15:54

Ada beberapa masalah dengan pendekatan ini, tetapi intinya adalah masalah alur kerja.

  1. Anda memiliki tujuan CultureControlleryang hanya untuk mengarahkan pengguna ke halaman lain di situs. Perlu diingat bahwa RedirectToActionrespons HTTP 302 akan dikirim ke browser pengguna, yang akan memberitahukannya untuk mencari lokasi baru di server Anda. Ini adalah perjalanan bolak-balik yang tidak perlu melintasi jaringan.
  2. Anda menggunakan status sesi untuk menyimpan budaya pengguna ketika sudah tersedia di URL. Status sesi sama sekali tidak diperlukan dalam kasus ini.
  3. Anda membaca HttpContext.Current.Request.UserLanguagesdari pengguna, yang mungkin berbeda dari budaya yang mereka minta di URL.

Masalah ketiga terutama karena pandangan yang sangat berbeda antara Microsoft dan Google tentang cara menangani globalisasi.

Pandangan (asli) Microsoft adalah bahwa URL yang sama harus digunakan untuk setiap budaya dan bahwa UserLanguagesbrowser harus menentukan bahasa apa yang harus ditampilkan situs web.

Pandangan Google adalah bahwa setiap budaya harus dihosting di URL yang berbeda . Ini lebih masuk akal jika Anda memikirkannya. Setiap orang yang menemukan situs web Anda di hasil pencarian (SERP) diharapkan dapat mencari konten dalam bahasa asli mereka.

Globalisasi situs web harus dilihat sebagai konten daripada personalisasi - Anda menyiarkan budaya ke sekelompok orang, bukan individu. Oleh karena itu, biasanya tidak masuk akal untuk menggunakan fitur personalisasi ASP.NET seperti status sesi atau cookie untuk menerapkan globalisasi - fitur ini mencegah mesin telusur mengindeks konten halaman lokal Anda.

Jika Anda dapat mengarahkan pengguna ke budaya yang berbeda hanya dengan mengarahkan mereka ke URL baru, tidak ada yang perlu dikhawatirkan - Anda tidak memerlukan halaman terpisah bagi pengguna untuk memilih budaya mereka, cukup sertakan tautan di tajuk atau footer untuk mengubah budaya halaman yang ada dan kemudian semua tautan akan secara otomatis beralih ke budaya yang telah dipilih pengguna (karena MVC secara otomatis menggunakan kembali nilai rute dari permintaan saat ini ).

Memperbaiki Masalah

Pertama-tama, singkirkan CultureControllerdan kode dalam Application_AcquireRequestStatemetode.

CultureFilter

Sekarang, karena budaya adalah perhatian lintas sektoral, pengaturan budaya utas saat ini harus dilakukan di IAuthorizationFilter. Ini memastikan budaya disetel sebelum ModelBinderdigunakan di 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);
    }
}

Anda dapat menyetel filter secara global dengan mendaftarkannya sebagai filter global.

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

Pilihan bahasa

Anda dapat menyederhanakan pemilihan bahasa dengan menautkan ke tindakan dan pengontrol yang sama untuk halaman saat ini dan memasukkannya sebagai opsi di header atau footer halaman di _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>

Seperti yang disebutkan sebelumnya, semua tautan lain di laman secara otomatis akan melewati budaya dari konteks saat ini, sehingga tautan tersebut secara otomatis akan tetap berada dalam budaya yang sama. Tidak ada alasan untuk menyebarkan budaya secara eksplisit dalam kasus tersebut.

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

Dengan tautan di atas, jika URL saat ini adalah /Home/Contact, tautan yang dihasilkan akan menjadi /Home/About. Jika URL saat ini adalah /en/Home/Contact, tautan akan dibuat sebagai /en/Home/About.

Budaya Default

Akhirnya, kami membahas inti dari pertanyaan Anda. Alasan budaya default Anda tidak dibuat dengan benar adalah karena perutean adalah peta 2 arah dan terlepas dari apakah Anda mencocokkan permintaan yang masuk atau membuat URL keluar, kecocokan pertama selalu menang. Saat membuat URL Anda, yang pertama cocok adalah DefaultWithCulture.

Biasanya, Anda dapat memperbaikinya hanya dengan membalik urutan rute. Namun, dalam kasus Anda itu akan menyebabkan rute masuk gagal.

Jadi, opsi paling sederhana dalam kasus Anda adalah membuat batasan rute kustom untuk menangani kasus khusus dari budaya default saat membuat URL. Anda cukup mengembalikan false ketika budaya default disediakan dan itu akan menyebabkan kerangka kerja perutean .NET melewati DefaultWithCulturerute dan pindah ke rute terdaftar berikutnya (dalam kasus ini 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 + "$");
        }
    }
}

Yang tersisa hanyalah menambahkan batasan ke konfigurasi perutean Anda. Anda juga harus menghapus pengaturan default untuk budaya di DefaultWithCulturerute karena Anda hanya ingin itu cocok ketika ada budaya yang disediakan di URL. The Defaultrute di sisi lain harus memiliki budaya karena tidak ada cara untuk lulus melalui 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

CATATAN: Bagian ini hanya berlaku jika Anda menggunakan MVC 5. Anda dapat melewati ini jika Anda menggunakan versi sebelumnya.

Untuk AttributeRouting, Anda dapat menyederhanakan berbagai hal dengan mengotomatiskan pembuatan 2 rute berbeda untuk setiap tindakan. Anda perlu mengubah sedikit setiap rute dan menambahkannya ke struktur kelas yang sama yang MapMvcAttributeRoutesdigunakan. Sayangnya, Microsoft memutuskan untuk membuat tipe internal sehingga memerlukan Refleksi untuk membuat instance dan mengisinya.

RouteCollectionExtensions

Di sini kami hanya menggunakan fungsionalitas bawaan MVC untuk memindai proyek kami dan membuat serangkaian rute, lalu memasukkan awalan URL rute tambahan untuk budaya dan CultureConstraintsebelum menambahkan instance ke MVC RouteTable kami.

Ada juga rute terpisah yang dibuat untuk menyelesaikan URL (dengan cara yang sama seperti 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);
    }
}

Maka itu hanya masalah memanggil metode ini, bukan 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 }
        );
    }
}
11 PlamenBoudinov Jun 02 2016 at 18:51

Perbaikan budaya default

Pos luar biasa oleh NightOwl888. Namun ada sesuatu yang hilang - rute atribut pembuatan URL normal (tidak dilokalkan), yang ditambahkan melalui refleksi, juga memerlukan parameter budaya default, jika tidak, Anda mendapatkan parameter kueri di URL.

? budaya = nl

Untuk menghindari ini, perubahan ini harus dilakukan:

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

Dan untuk mengatribusikan pendaftaran rute:

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

Solusi yang lebih baik

Dan sebenarnya, setelah beberapa waktu, saya perlu menambahkan terjemahan url, jadi saya menggali lebih dalam, dan tampaknya tidak perlu melakukan peretasan refleksi yang dijelaskan. Orang-orang ASP.NET memikirkannya, ada solusi yang jauh lebih bersih - sebagai gantinya Anda dapat memperpanjang DefaultDirectRouteProvider seperti ini:

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

Ada solusi berdasarkan ini, termasuk terjemahan url di sini: https://github.com/boudinov/mvc-5-routing-localization