Home » ASP » ASP.MVC » Asp.Net MVC localized url without having to specify the language in it

Asp.Net MVC localized url without having to specify the language in it

Often we see people using for multi language website the possibility to change the language by adding a property into the session of the user which tell the server to change the culture and culture ui to something else than english. This is fine because resources files handle multi languages if the culture is set correctly. An other way to do it, is to allow the user to have the culture into the url like the following example : http://yourwebsite.com/fr-ca/controllerName/actionName. I personally dislike this approach. It has the advantage to give the possibility to send an url to someone and to have it in the correct language, this can’t be done with the previous solution of allowing the user to change the language and set it to the session. But, it has the disadvantage to tell that the language is French and still having the text in the url in English. The solution is suggest is that if you have public page that these one should control the language with an url that is already in the desired language. You should also let the user select the language and set the client language into a session which will tell the server how to display the url. This solution gives you good SEO url with named url in the good language and allow the user to control the language if this one want to change it.

At the end, what we want is to have url like this : http://yourwebsite.com/Compte/Identification for French website and for English : http://yourwebsite.com/Account/Login

Everything start by changing the routing. This can be done by opening the file RouteConfig.cs that is inside your web project under the folder App_Start.

Several things need to be done. First, we need to specify which culture will be used by you application. Then, you will have to use a custom structure to specify every controllers and actions with the translated values. This is required to associate every language word to an existing controller and existing action. Finally, the last step is to create a binder that will allow you to use the new mechanism to translate route. This is an example of how the routeConfig.cs looks for a brand new Asp.Net MVC5 application with the Home and Account controller translated.

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        var cultureEN = CultureInfo.GetCultureInfo("en-US");
        var cultureFR = CultureInfo.GetCultureInfo("fr-FR");

        var translationTables = new List<ControllerTranslation>{ 
            new ControllerTranslation("Home"
                    , new List<Translation>{
                                new Translation(cultureEN, "Home")
                            ,new Translation(cultureFR, "Demarrer")
                    }
                ,new List<ActionTranslation>{
                        new ActionTranslation("About"
                        , new List<Translation>{
                                new Translation(cultureEN, "About")
                            ,new Translation(cultureFR, "Infos")
                        })
                        , new ActionTranslation("Home"
                        , new List<Translation>{
                                new Translation(cultureEN, "Home")
                            ,new Translation(cultureFR, "Demarrer")
                        })
                    , new ActionTranslation("Contact"
                        , new List<Translation>{
                                new Translation(cultureEN, "Contact")
                            ,new Translation(cultureFR, "InformationSurLaPersonne")
                        })
                })
            ,new ControllerTranslation("Account"
                    ,
                    new List<Translation>{
                                new Translation(cultureEN, "Account")
                            ,new Translation(cultureFR, "Compte")
                    }
                    ,
                    new List<ActionTranslation>
                    {
                        new ActionTranslation("Login"
                        , new List<Translation>{
                                new Translation(cultureEN, "Login")
                            ,new Translation(cultureFR, "Authentification")
                        })
                        , 
                        new ActionTranslation("Register"
                        , new List<Translation>{
                                new Translation(cultureEN, "Register")
                            ,new Translation(cultureFR, "Enregistrement")
                        })

                    }
            )
        };

        routes.Add("LocalizedRoute", new TranslatedRoute(
            "{controller}/{action}/{id}",
            new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
            translationTables,
            new MvcRouteHandler()));

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

As you can see, it can be very exhaustive to define every actions of every controller. However, this configuration can be saved in a database and loaded once for the application if you desire.

We are using a lot of classes that you will need. First of all, let’s define all classes that are used to structure the controllers and actions translation. I have separated all classes in 4 files.

RoutingClasses

public class ControllerTranslation
{
    public string ControllerName { get; set; }
    public List<Translation> Translation { get; set; }
    public List<ActionTranslation> ActionTranslations { get; set; }

    public ControllerTranslation(string controllerName, List<Translation> translation, List<ActionTranslation> actionsList)
    {
        this.ControllerName = controllerName;
        this.Translation = translation;
        this.ActionTranslations = actionsList;
    }
}

This is the main class that has the controller name, which is required to be able to bind the translated name for the real code name. It contains a translation list which contains for every cultures the new name which is used in the url. Then, it contains a list of actions. This way, we have a well structured a logically separated structure.

public class ActionTranslation
{
    public string ActionName { get; set; }
    public List<Translation> Translation { get; set; }

    public ActionTranslation(string actionName, List<Translation> translation)
    {
        this.ActionName = actionName;
        this.Translation = translation;
    }
}

ActionTranslation is almost the same as ControllerTranslation. The only difference it that it doesn’t contain a list of class.

public class Translation
{
    public CultureInfo CultureInfo { get; set; }
    public string TranslatedValue { get; set; }
    public Translation(CultureInfo culture, string translatedValue)
    {
        CultureInfo = culture;
        TranslatedValue = translatedValue;
    }
}

Finally, the Translation class contain a culture information that is associated to a string that is translated in the language of the culture.

Take note that you could also have a AreaTranslation that would have a Controller class list. This example is extensible for more level of url structure without problem. For the simplicity, this article concentrate its effort for Controller and Action only.

The last class is here to define a new Route. In the RouteConfig.cs class, the route is defined, before the default route.

 routes.Add("LocalizedRoute", new TranslatedRoute(
               "{controller}/{action}/{id}",
               new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
               translationTables,
               new MvcRouteHandler()));

The third parameter takes the controller list that has all actions translated. A route is a class that inherit from System.Web.Routing.Route class. This class allow you to override two important methods. RouteData GetRouteData(HttpContextBase httpContext) and VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values). The first one is used when a request is done to the server. This mean that is takes the localized words of the controller and action of the URL and translated them with your list of controller defined previously in the RouteConfig.cs. The second one is used by Html helper to create new link. This mean it takes the real controller and action name and translated them to create localized link for your application. This is awesome because not only your url are in the good language but also all your link everywhere in your application is automatically translated.

public class TranslatedRoute : Route
{

    public List<ControllerTranslation> Controllers { get; private set; }

    public TranslatedRoute(string url, RouteValueDictionary defaults, List<ControllerTranslation> controllers, IRouteHandler routeHandler)
        : base(url, defaults, routeHandler)
    {
        this.Controllers = controllers;
    }

    public TranslatedRoute(string url, RouteValueDictionary defaults, List<ControllerTranslation> controllers, RouteValueDictionary constraints, IRouteHandler routeHandler)
        : base(url, defaults, constraints, routeHandler)
    {
        this.Controllers = controllers;
    }

    /// <summary>
    /// Translate URL to route
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        RouteData routeData = base.GetRouteData(httpContext);
        if (routeData == null) return null;

        string controllerFromUrl = routeData.Values["controller"].ToString();
        string actionFromUrl = routeData.Values["action"].ToString();
        var controllerTranslation = this.Controllers.FirstOrDefault(d => d.Translation.Any(rf=>rf.TranslatedValue == controllerFromUrl));
        var controllerCulture = this.Controllers.SelectMany(d => d.Translation).FirstOrDefault(f => f.TranslatedValue == controllerFromUrl).CultureInfo;
        if (controllerTranslation != null)
        {
            routeData.Values["controller"] = controllerTranslation.ControllerName;
            var actionTranslation = controllerTranslation.ActionTranslations.FirstOrDefault(d => d.Translation.Any(rf => rf.TranslatedValue == actionFromUrl));
            if (actionTranslation != null)
            {
                  
                routeData.Values["action"] = actionTranslation.ActionName;
                    
            }
            System.Threading.Thread.CurrentThread.CurrentCulture = controllerCulture;
            System.Threading.Thread.CurrentThread.CurrentUICulture = controllerCulture;
        }
            

        return routeData;
    }

    /// <summary>
    /// Used in Html helper to create link
    /// </summary>
    /// <param name="requestContext"></param>
    /// <param name="values"></param>
    /// <returns></returns>
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {

        var requestedController = values["controller"];
        var requestedAction = values["action"];
        var controllerTranslation = this.Controllers.FirstOrDefault(d => d.Translation.Any(rf => rf.TranslatedValue == requestedController));
        var actionTranslation = controllerTranslation.ActionTranslations.FirstOrDefault(d => d.Translation.Any(rf => rf.TranslatedValue == requestedAction));
        var controllerTranslatedName = controllerTranslation.Translation.FirstOrDefault(d => d.CultureInfo == System.Threading.Thread.CurrentThread.CurrentCulture).TranslatedValue;
        if (controllerTranslatedName != null)
            values["controller"] = controllerTranslatedName;
        var actionTranslate = controllerTranslation.ActionTranslations.FirstOrDefault(d => d.Translation.Any(rf => rf.TranslatedValue == requestedAction));
        if (actionTranslate != null)
        {
            var actionTranslateName = actionTranslate.Translation.FirstOrDefault(d => d.CultureInfo == System.Threading.Thread.CurrentThread.CurrentCulture).TranslatedValue;
            if (actionTranslateName != null)
                values["action"] = actionTranslateName;
        }
        return base.GetVirtualPath(requestContext, values);
    }
}

For both method, if we do not find the controller name or action name, it falls back to the default name. This way, nothing crash. I have also added two lines that change the thread language. This is not require if you do not want to change the language of the whole application but it seems logical to do it. If you send an url in French, you certainly want to have the whole page to use French resources.

TranslatedUrlAndLinks

You can have the complete source code on GitHub.

If you like my article, think to buy my annual book, professionally edited by a proofreader. directly from me or on Amazon. I also wrote a TypeScript book called Holistic TypeScript

16 Responses so far.

  1. […] contains information to localize your Asp.Net MVC web application. I have already covered how to change the language from the url without having to use the culture code in the url. This post goal is to detect the default language of the user browser for a default language and to […]

  2. Michel says:

    Hi Patrick,

    Great sample to explain how to use a custom route for localizations!

    I have been putting together some samples for myself to figure out how to localize a MVC application.

    Seems that Microsoft does not really offer any great samples themselves 🙂

    Had to figure out how to localize the validation’s (and the unobtrusive version), and now thanks to you i can also localize the routes!

  3. Rohit says:

    Hello,
    How you are changing the language ? If i want to see french content then what should I do ?

    Regards,
    Rohit

    • Hi Rohit,

      AS you can see in the GetRouteData, the Thread language is change.

      System.Threading.Thread.CurrentThread.CurrentCulture = controllerCulture;
      System.Threading.Thread.CurrentThread.CurrentUICulture = controllerCulture;

      If you want to see French content, you have to navigate to a French route.

  4. Michael says:

    Hi Patrick,

    thanks for the great article, but how exactly would I register the routes when I want todo the same thing with areas?

    I already got my AreaTranslation class and also implemented the translation inside the TranslatedRoute class, but it always detects the area as controller when parsing the RouteValues.

    • This is not in the article but if you understood the article you should be able to see how to do it. Here is few hints.

       routes.Add("LocalizedRoute", new TranslatedRoute(
                  "{controller}/{action}/{id}",
                  new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
                  translationTables,
                  new MvcRouteHandler()));
      

      The Add Route needs to add “Area” in the RouteValueDictionary, thus the translationTables will need a new field to have the localized area’s name. You will have instead of 1 file to add route to have multiple ones. That is not a problem. The code routes.Add will be in multiple files for each area registration, thus having also 1 translation table per area too. That is about it.

  5. Greg says:

    Hi,

    thanks for this tutorial.
    How can I globally change language by something like the “language switcher” (dropdown list, etc.) by putting it in the layout page?

    To main goal is to define language list in one class / database or other place, and then just add new resource files if necessary.

    • You can create a Html control (comboxbox, list, flag icons, whatever you want) that will add in the query string the language. You can also just redirect to the main page which contain a localized url. From there, you can do multiple things. If you want to save the user preference you can setup this in the User classes (provided for you if you are using Asp.Net MVC default template with Entity Framework) or you can create your own field. That said, this is not required too. I normally have different domain name and French people go through a specific domain, and English people go to the other one. I can dynamically figure out the language. Am I clear?

  6. lenovo says:

    Wow that was unusual. I just wrote an really long comment but after I clicked submit my comment didn’t appear.
    Grrrr… well I’m not writing all that over again. Anyways, just wanted to say wonderful blog!

  7. DK says:

    Hi How do it in ASP.net Core 2.2 do i need to add the code in routes.MapRoute or is there a simplier way?

    • I haven’t played with Asp.Net Core 2.2 and I wouldn’t be in position to guide on a migration path. The code in this blog post is 6 years old hence might not be compatible with the latest framework.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.