Click here to Skip to main content
Click here to Skip to main content

Routing a Localized ASP.NET MVC Application

, 26 Aug 2010
Rate this:
Please Sign up or sign in to vote.
How to use URL to create a localized ASP.NET MVC application

Introduction

Localizing a general ASP.NET application is easy to implement. All we have to do is assign a specified culture info to Thread.CurrentThread.CurrentUICulture and use resource to display the localized text. There are two common ways to do this task:

Manually change CurrentUICulture in Application.BeginRequest event

The implementation will look something like this:

protected void Application_BeginRequest(object sender, EventArgs e) 
{ 
    var cultureName = HttpContext.Current.Request.UserLanguages[0]; 
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(cultureName); 
} 

Let ASP.NET set the UI culture automatically based on the values that are sent by a browser

We get it by changing web.config. The configuration will look like this:

<system.web>
  <globalization enableClientBasedCulture="true" uiCulture="auto:en"/>
</system.web>

The problem is that culture info is stored in UserLanguages which is defined by browser. If user wants to change to another language, he must go to "Internet Options" (IE for example) to change the Language Preference. This technique also has a problem for search engines because a search engine cannot classify two different language versions. It causes our page rank to not be promoted. Generally, we solve this problem by using URL to define culture (we will call it as localized URL). So, a URL will be change from http://domain/action/ to http://domain/[culture-code]/action (for example: http://domain/vi-VN/action) Then, in Application.BeginRequest event, we can change UICulture like this:

protected void Application_BeginRequest(object sender, EventArgs e) 
{ 
    var cultureName = 
	HttpContext.Current.Request.RawUrl.SubString(1, 5); // RawUrl is 
						// "/domain/vi-VN/action"
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(cultureName); 
}

Everything seems to be OK until the birth of ASP.NET MVC. Routing technique which is used by ASP.NET MVC framework causes problem for this localization technique because routing engine needs to parse a URL to create the right controller. Appending a prefix before the real URL will make a wrong result for the routing engine. So, we need a way to combine both of ASP.NET MVC and localized URL.

Background

In order to make ASP.NET MVC work, we have to map one (or more) routes into Routes collection of RouteTable. When you create an ASP.NET MVC application, VS.NET generates some code in global.asax.cs (.vb or something else depending on your language). The code will look like:

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      routes.MapRoute(
          "Default",                                              // Route name
          "{controller}/{action}/{id}",                           // URL with parameters
          new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
      );
    }

    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      RegisterRoutes(RouteTable.Routes);
    }
}

MapRoute function will create a System.Web.Routing.Route instance and add it to routes collection. A route will determine whether a URL is matched with its definition and which controller will process the URL. So, we can add our code to solve localized URL's problem.

Using the Code

Next, we need to create a new route which can handle localized URL. The new class will parse URL to generate RouteData (which contains controller, action and other parameters). Here is the point. After extracting the culture code to set UICulture, we'll simple rewrite the path. So, our request will be performed as normal.

public override RouteData GetRouteData(HttpContextBase httpContext)
{
    string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
    //string old = virtualPath;
    if (virtualPath.IsLocalizedUrl())
    {
        string cultureCode = virtualPath.Substring(2, 5);
        CultureInfo culture = null;
        try
        {
            culture = CultureInfo.GetCultureInfo(cultureCode);
        }
        catch { }
        if (culture != null)
        {
            System.Threading.Thread.CurrentThread.CurrentUICulture = culture;
        }
        //In ASP.NET Development Server, an URL like 
        //"http://localhost/Blog.aspx/Categories/BabyFrog" will return 
        //"~/Blog.aspx/Categories/BabyFrog" as AppRelativeCurrentExecutionFilePath.
        //However, in II6, the AppRelativeCurrentExecutionFilePath is "~/Blog.aspx"
        //It seems that IIS6 think we're process Blog.aspx page.
        //So, I'll use RawUrl to re-create 
        //an AppRelativeCurrentExecutionFilePath like ASP.NET Development Server.
        virtualPath = "~" + httpContext.Request.RawUrl;

        virtualPath = string.Concat("~/", 
		(virtualPath.Length <= 8 ? string.Empty : virtualPath.Substring(8)));
        httpContext.RewritePath(virtualPath, true);
    }
    RouteData data = base.GetRouteData(httpContext);
    return data;
}

IsLocalizedUrl is an extension which determines whether a URL is localized URL. The code is here:

public static bool IsLocalizedUrl(this string url)
{
    int length = url.Length;
    if (length < 7)
        return false;
    else if (length == 7)
        return url[4] == '-';
    else
        return url.Length > 7 && url[4] == '-' && url[7] == '/';
}

The localized URL works well now. However, there is another problem with this implementation. When user uses Html.ActionLink (or some other similar functions), the generated URL won't contains culture code. The reason is that VirtualPath of processed URL doesn't contains culture code (which is removed for localization purpose). So, we need to override GetVirtualPath function to make our expected virtual path.

public override VirtualPathData GetVirtualPath
	(RequestContext requestContext, RouteValueDictionary values)
{
    VirtualPathData data = base.GetVirtualPath(requestContext, values);

    if (data != null)
    {
        string rawUrl = requestContext.HttpContext.Request.RawUrl;
        if (rawUrl.IsLocalizedUrl(true))
        {
            data.VirtualPath = string.Concat(rawUrl.GetLocalizedCultureCode(), 
				"/", data.VirtualPath);
        }
    }
    return data;
}    

Everything is almost done here. There is only one small problem left: How to use register an LocalizedRoute into RouteTable. Of course, we can create a new instance of LocalizedRoute and add it to RouteTable.Routes but that code will look ugly. Fortunately, we have a better way: function extensions, which is also used by standard Route.

public static class RegexRouteCollectionExtensions
{
    //Override for localized route
    public static Route MapRoute(this RouteCollection routes, string name, 
	string url, object defaults)
    {
        return routes.MapRoute(name, url, defaults, null, null);
    }
    public static Route MapRoute(this RouteCollection routes, string name, 
	string url, object defaults, object constraints)
    {
        return routes.MapRoute(name, url, defaults, constraints, null);
    }
    public static Route MapRoute(this RouteCollection routes, string name, 
	string url, object defaults, object constraints, string[]namespaces)
    {
        if (routes == null)
        {
            throw new ArgumentNullException("routes");
        }
        if (url == null)
        {
            throw new ArgumentNullException("url");
        }
        LocalizedRoute item = new LocalizedRoute(url, new MvcRouteHandler())
        {
            Defaults = new RouteValueDictionary(defaults),
            Constraints = new RouteValueDictionary(constraints),
            DataTokens = new RouteValueDictionary()
        };
        if ((namespaces != null) && (namespaces.Length > 0))
        {
            item.DataTokens["Namespaces"] = namespaces;
        }
        routes.Add(name, item);
        return item;
    }
}    

Working with Area

It's very easy to make it works with MVC's Area. All you need is replace AreaRegistrationContext.MapRoute() function with by a similar version of RegexRouteCollectionExtensions.MapRoute() function. Because AreaRegistrationContext.MapRoute() function is not an extension method, we have to use another name for this task. So, I choose AreaRegistrationContext.MapLocalizedRoute. The code will look like:

public static Route MapLocalizedRoute(this AreaRegistrationContext context, 
    string name, string url, object defaults, object constraints, string[] namespaces)
{
    if ((namespaces == null) && (context.Namespaces != null))
    {
        namespaces = context.Namespaces.ToArray();
    }

    Route route = context.Routes.MapRoute(name, url, defaults, constraints, namespaces);
    route.DataTokens["area"] = context.AreaName;
    bool flag = (namespaces == null) || (namespaces.Length == 0);
    route.DataTokens["UseNamespaceFallback"] = flag;
    return route;
}

The above code is exactly the implementation of AreaRegistrationContext.MapRoute. However, context.Routes.MapRoute won't use System.Web.Mvc.RouteCollectionExtensions.MapRoute() function, it uses our RegexRouteCollectionExtensions.MapRoute() function. We also have to replace all overloads of AreaRegistrationContext.MapRoute.

Next, we just go to our AreaRegistration and replace MapRoute() function by our MapLocalizedRoute() function.

Before

public override void RegisterArea(AreaRegistrationContext context)
{
    context.MapRoute(
        "RtfRenderer_default",
        "RtfRenderer/{controller}/{action}/{id}",
        new { action = "Index", id = UrlParameter.Optional }
    );
}

After

public override void RegisterArea(AreaRegistrationContext context)
{
    context.MapLocalizedRoute(
        "RtfRenderer_default",
        "RtfRenderer/{controller}/{action}/{id}",
        new { action = "Index", id = UrlParameter.Optional }
    );
}

Points of Interest

Actually, in my real application, I use the following code to map my route:

routes.MapRoute(
    "Default",                                              // Route name
    "{controller}" + Config.RoutingExtension + "/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
);
routes.MapRoute(
    "Default2",                                              // Route name
    "{controller}/{action}/{id}",                            // URL with parameters
    new { controller = "Home", action = "Index", id = "" }   // Parameter defaults
);

Config.RoutingExtension is a string constant (I set it as ".aspx"). The reason is that in IIS6, a URL like http://domain/action/id won't be performed by ASP.NET. By default, ASP.NET just handles some extension like .aspx, .ashx, .axd and .mvc (in case we install MVC framework in server). So, our request will be ignored by ASP.NET engine. There is a work-around solution for this problem. We can make our URL into http://domain/action.aspx/id. So, it'll be recognized by the ASP.NET engine.

Note

The attached project is in VS.NET 2010 RC. However, the code should work with VS.NET 2008. All code is in ASP.NET MVC 2.0. I have not checked it yet, but I think it can't work well with ASP.NET MVC 1.0. Tell me the result if you try this with MVC 1.0. Smile | :)

History

  • April 13, 2010: First release
  • August 26, 2010: Make it work with MVC's Areas

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Đỗ Hồng Ngọc
Software Developer GrapeCity
Vietnam Vietnam
MCP, MCAD, MCSD, MCTS for Windows Form, MCTS for Web
 
You can reach me at qFotoEfex
 


Comments and Discussions

 
GeneralMy vote of 5 Pinmemberchfeng200822-Mar-11 16:56 
GeneralMVC3 Pinmemberdjkofi1-Feb-11 0:40 
GeneralMy vote of 5 PinmemberTawani Anyangwe15-Sep-10 9:44 
GeneralRe: My vote of 5 PinmemberWhiteRose161121-Oct-10 23:15 
GeneralLocalize with Areas Pinmembermltr198524-Aug-10 22:15 
GeneralRe: Localize with Areas PinmemberWhiteRose161125-Aug-10 5:11 
GeneralRe: Localize with Areas PinmemberWhiteRose161125-Aug-10 19:30 
GeneralRe: Localize with Areas Pinmembermltr198526-Aug-10 23:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 26 Aug 2010
Article Copyright 2010 by Đỗ Hồng Ngọc
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid