8

I am using the following article Addition to ASP.NET MVC Localization - Using routing to support multi-culture routes.

If you look at section "Registering routes" you will see that current routes are updated (in the "RegisterRoutes" method) with the "{culture}" segment.

The difference is that I want to keep the current routes and add a duplicate for each one with a "{culture}" segment, so for a route like "foo/bar" I would get a duplicate "{culture}/foo/bar".

You can see I'm also making sure the new route comes first .

public static void MapMvcMultiCultureAttributes(this RouteCollection routes, bool inheritedRoutes = true, string defaultCulture = "en-US", string cultureCookieName = "culture")
{
    routes.MapMvcAttributeRoutes(inheritedRoutes ? new InheritedRoutesProvider() : null);

    var multiCultureRouteHandler = new MultiCultureMvcRouteHandler(defaultCulture, cultureCookieName);

    var initialList = routes.ToList();
    routes.Clear();

    foreach (var routeBase in initialList)
    {
        var route = routeBase as Route;
        if (route != null)
        {
            if (route.Url.StartsWith("{culture}"))
            {
                continue;
            }

            var cultureUrl = "{culture}";
            if (!String.IsNullOrWhiteSpace(route.Url))
            {
                cultureUrl += "/" + route.Url;
            }

            var cultureRoute = routes.MapRoute(null, cultureUrl, null, new
            {
                culture = "^\\D{2,3}(-\\D{2,3})?$"
            });

            cultureRoute.Defaults = route.Defaults;
            cultureRoute.DataTokens = route.DataTokens;

            foreach (var constraint in route.Constraints)
            {
                cultureRoute.Constraints.Add(constraint.Key, constraint.Value);
            }

            cultureRoute.RouteHandler = multiCultureRouteHandler;
            route.RouteHandler = multiCultureRouteHandler;
        }

        routes.Add(routeBase);
    }
}

The "InheritedRoutesProvider" looks like this:

private class InheritedRoutesProvider : DefaultDirectRouteProvider
{
    protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(ActionDescriptor actionDescriptor)
    {
        return actionDescriptor.GetCustomAttributes(typeof(IDirectRouteFactory), true)
            .Cast<IDirectRouteFactory>()
            .ToArray();
    }
}

My controller looks like this:

public class MyBaseController: Controller
{
    [HttpGet]
    [Route("bar")]
    public virtual ActionResult MyAction(){
    {
        return Content("Hello stranger!");
    }
}

[RoutePrefix("foo")]
public class MyController: MyBaseController
{
}

My "RegisterRoutes" method looks like this:

public static void RegisterRoutes(RouteCollection routes)
{
     routes.MapMvcMultiCultureAttributes();
     routes.LowercaseUrls = true;
}

Now, if I do:

  • /foo/bar - WORKS!
  • /en-US/foo/bar - HttpException A public action method 'MyAction' was not found on controller 'MyController'
Dan
  • 1,555
  • 2
  • 14
  • 30

3 Answers3

1

I can give you an example of how I would do it. That example you are using is quite old.

  1. Implement in your controllers (use inheritance) the BeginExecuteCore method as below:

    protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
    {
        string cultureName = RouteData.Values["culture"] as string;
    
        if (cultureName == null)
            cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ?
                    Request.UserLanguages[0] :  // obtain it from HTTP header AcceptLanguages
                    null;
    
        // Validate culture name
        cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
    
        if (RouteData.Values["culture"] as string != cultureName)
        {
            // Force a valid culture in the URL
            RouteData.Values["culture"] = cultureName.ToLower(); // lower case too
    
            // Redirect user
            Response.RedirectToRoute(RouteData.Values);
        }
    
        // Modify current thread's cultures            
        Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
        Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
    
        return base.BeginExecuteCore(callback, state);
    }
    
  2. Add some routes

            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
            routes.MapRoute(
                name: "Custom",
                url: "{controller}/{action}/{culture}",
                defaults: new { culture = CultureHelper.GetDefaultCulture(), controller = "Coordinate", action = "Index" }
    
  3. Implement a culture helper class

public static class CultureHelper { private static readonly List _cultures = new List { "listOfClutures" };

    public static bool IsRighToLeft()
    {
        return System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.IsRightToLeft;
    }

    public static string GetImplementedCulture(string name)
    {
        if (string.IsNullOrEmpty(name))
            return GetDefaultCulture(); // return Default culture
        if (!CultureInfo.GetCultures(CultureTypes.SpecificCultures).Any(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
            return GetDefaultCulture(); // return Default culture if it is invalid
        if (_cultures.Any(c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
            return name; // accept it

        var n = GetNeutralCulture(name);
        foreach (var c in _cultures)
            if (c.StartsWith(n))
                return c;
        return GetDefaultCulture(); // return Default culture as no match found
    }

    public static string GetDefaultCulture()
    {
        return "en-GB"; // return Default culture, en-GB
    }
    public static string GetCurrentCulture()
    {
        return Thread.CurrentThread.CurrentCulture.Name;
    }
    public static string GetCurrentNeutralCulture()
    {
        return GetNeutralCulture(Thread.CurrentThread.CurrentCulture.Name);
    }
    public static string GetNeutralCulture(string name)
    {
        if (!name.Contains("-")) return name;

        return name.Split('-')[0]; // Read first part only. E.g. "en", "es"
    }

    public static List<KeyValuePair<string, string>> GetImplementedLanguageNames()
    {
        List<KeyValuePair<string, string>> languageNames = new List<KeyValuePair<string, string>>();

        foreach (string culture in _cultures)
        {
            languageNames.Add(new KeyValuePair<string, string>(culture, CultureInfo.GetCultureInfo(culture).EnglishName));
        }

        languageNames.Sort((firstPair, nextPair) =>
        {
            return firstPair.Value.CompareTo(nextPair.Value);
        });

        string currCulture = GetCurrentCulture();
        languageNames.Remove(new KeyValuePair<string, string>(currCulture, CultureInfo.GetCultureInfo(currCulture).EnglishName));
        languageNames.Insert(0, new KeyValuePair<string, string>(currCulture, CultureInfo.GetCultureInfo(currCulture).EnglishName));

        return languageNames;
    }

    public static string GetDateTimeUsingCurrentCulture(DateTime dateToConvert)
    {
        CultureInfo ci = new CultureInfo(GetCurrentCulture());
        return dateToConvert.ToString(ci.DateTimeFormat.ShortDatePattern + ' ' + ci.DateTimeFormat.ShortTimePattern);
    }
}
Rober
  • 726
  • 8
  • 27
  • Thank you for your example, but I don't want to redirect in case the current route is different from the specified one (or empty) and I also want the {culture} segment to be the first segment and work with any kind of route (normal route or route attribute). So the user will be able to access my route with the culture specified or not (with no redirect). – Dan Nov 06 '15 at 15:17
0

I don't think you'll be able to achieve this with attribute routing because the RouteCollection passed to RegisterRoutes and its subsequent RouteData is quite different and uses internal classes that you can't get to.

I got as far as determining that RouteData expects a key called "MS_DirectRouteMatches" whose value is a collection of RouteData. Google MS_DirectRouteMatches if you want to dig further.

Ultimately, I assume this isn't intended to be an extension point.

You will get more success if you stick to conventional routing. I found your code worked pretty well in this scenario, but I expect you already knew that. Sorry I couldn't give you a better solution.

Phil
  • 125
  • 1
  • 7
0

From what I can see it's not using the parent ActionResult. I'm not sure why this is happening. You could override the ActionResult in the derived class, but to me it seems that there's something amiss in the inheritance.. a fault in how you are managing your classes.

public class MyBaseController: Controller
{
    [HttpGet]
    [Route("bar")]
    public virtual ActionResult MyAction(){
    {
        return Content("Hello stranger!");
    }
}

So assuming this will not work:

[RoutePrefix("foo")]
public class MyController: MyBaseController
{

    [HttpGet]
    [Route("foo")]
    public override ActionResult MyAction(){
    {
        return Content("Hello stranger!");
    }
}

I'd suggest doing this:

[RoutePrefix("foo")]
public class MyController: MyBaseController
{

    [HttpGet]
    [Route("foo")]
    public  ActionResult MyAction(){
    {
        return Content("Hello stranger!");
    }
}

This at least will tell you if the inheritance is flawed and you can review your code.

Another way around this would be to use the one controller with two methods for the differing formats.