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

Creating a Bilingual ASP.NET MVC 3 Application – Part 2

By , 30 Jan 2012
 

Introduction and recap

Part 1 outlined how to use resources to internationalize an MVC3 application so it is bilingual, it also acknowledges three sites I found useful which helped form the basis of the parts to this article. If you are unfamiliar with Part 1, or have not used resx files with an MVC application or in general, it might be worth reviewing (especially the introduction) as I will not re-cover its ground in this part. It is also re-iterating Part 1’s caveat about needing a basic understanding of the following in MVC 3: routing strategy, controllers actions and views; the Route class; the purpose of RouteHandlers; what RouteConstraints do. I will not be explaining any of these in great depth, in the interests of clarity and brevity. Unlike Part 1, this article is not targeted at MVC3 beginners as some of the aspects are unavoidably technical. I do hope that beginners persevere and find picking the code apart useful to their understanding of MVC 3 anyway!

In Part 1, the MVC 3 application was able to display English (as default) or Arabic according to the “Language Preference” set in the user’s browser. This article describes how to add the ability to override this using the URL; further, it will add how to change languages manually and end in a discussion of the pros and cons of the methodology used.

Part 2 Application overview

The first thing that must be decided is the routing strategy for the URL, the default for MVC 3 is: http://MyHost/Contoller/Action/Id.

“Controller” is the controller class, “Action” is the method to be called on that controller, there is an optional third part, ID. The defaults for these are “Home” and “Index”, respectively; in the default application, the ID is not needed for this method. It is necessary to decide where to put the language discriminator in the URL. As the URL already goes from general to specific, we shall follow the same pattern: http://MyHost/Culture/Contoller/Action.

If culture is missing, the strategy is to default to the brower’s language settings, making the application work like Part 1 of this article, using the browser default. MSDN has a similar routing: culture is in the format “en-us” or “en-sa”. This article ignores the second part of the culture format, so the language could be “ar-zz” or “en-xx” and it will still show the appropriate language version. Because we do not care about language sub-divisions, I will also allow the short form two letter ISO code “en” and “ar”, and we will use the short form by default. Here are some sample URLs:

URL Result
http://localhost Calls Home/Index (defaults) and uses the browser’s language
http://localhost/ar Calls Home/Index (defaults), specifies Arabic, overriding the browser language
http://localhost/Home/Index As per first URL, but explicitly sets Controller and Action
http://localhost/ar/Home/Index As per second URL, but explicitly sets Controller and Action
http://localhost/en/Home/Index As previous, but overrides browser language with English
http://localhost/en-gb/Home/Index As previous (valid sub-culture)
http://localhost/en-zz/Home/Index As previous (invalid sub-culture, but sill overrides with English)

Placing the language first has the benefit of leaving the rest of the URL “as is”, so the vast majority of existing routing strategies can be accommodated. There is one pitfall here, and it is not obvious: the default strategy has an optional final parameter ID so the following have the potential to be matched the same way:

  • http://localhost/Home/Index/1
  • http://localhost/ar/Home/Index

Could be broken matched in the following ways:

URL Language Controller Action Id
http://localhost/Home/Index/1 [Default] Home Index 1
http://localhost/ar/Home/Index ar Home Index N/A
http://localhost/ar/Home/Index [Default] ar Home Index

The worst cast scenario is the last: the MVC 3 framework will throw an error when it tries to use a controller called “ar”. To determine which scenario is correct for a given URL, we will create a RouteConstraint which effectively returns true if the first part of the URL is a language specification. The code will pattern match either “XX” or “XX-XX” where X is any letter. The constraint code will not check to see if the language code is valid. If the language code is invalid or not supported (i.e., not English or Arabic), the site’s default will be shown (English). This design decision was partly taken to increase robustness; if this decision was not made and the user actually tries the language code “xx-xx”, the website would throw an error as it would determine that “xx-xx” is the controller, which will not exist. In the sample code, there is a commented method (with simple instructions) to allow you to only match supported cultures if this fits your scenario better. Note that we do lose a little flexibility: we cannot create controllers with names that match our pattern (though a controller class named “en” or “ar-jo” is not very descriptive :).

If you are confused about the routing strategy, please take a look at the section “Results (So Far!)” below, it shows how most of the various URLs are intended to work!

How will we achieve these?

This involves several steps:

  1. Add a route that takes the default route and pre-pends it with a culture value, to try to ensure the application works as normal when globalised. It will use the normal application defaults. The route handler will add a constraint to ensure that the globalisation classes are used if a culture matching our pattern is added to the URL.
  2. Create a route handler to call the code that sets the culture of the UI and main threads. Other than that, it should have the same functionality as the default MvcRouteHandler.
  3. There will be a culture manager maintaining a dictionary where the two-letter ISO code is the key mapping on to a CultureInfo instance that represents the language to be used. It will do the work of setting the thread’s culture, and could be used by the constraint to constrain to only supported cultures. The manager class should also make multi-lingual (as opposed to bi-lingual) support easier to implement.
  4. Code to abstract the pattern matching of the culture away from the other classes.

Note that the design is intended to “get out of the way” and be as self-contained as possible. To add globalised URL support to the application as it was left in part 1 (or almost any MVC 3 application):

  • A reference to the globalisation support library containing the above classes is added.
  • An instance of the route described in step 1 is plumbed in via the MVC application’s global.asax.

The default handler is left in place so that if no culture code is specified (i.e., the route constraint fails), the application just uses the browser’s default language, falling back to the behaviour in part 1 of this article.

The code

A class library called MvcGlobalisationSupport was added to the solution from part 1, with the classes described above. I will describe the code plumbing the mechanism in, then drilling down to explain what each class does and describe some of the design decisions/trade-offs.

Now all we need do is replace the original registration code in Application_Start() with:

public static void RegisterRoutes(RouteCollection routes)
{
    const string defautlRouteUrl = "{controller}/{action}/{id}";
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    RouteValueDictionary defaultRouteValueDictionary = new RouteValueDictionary(
            new { controller = "Home", action = "Index", id = UrlParameter.Optional });
    Route defaultRoute = new Route(defautlRouteUrl, 
          defaultRouteValueDictionary, new MvcRouteHandler());
    routes.Add("DefaultGlobalised", 
               new GlobalisedRoute(defaultRoute.Url, defaultRoute.Defaults));
    routes.Add("Default", new Route(defautlRouteUrl, 
               defaultRouteValueDictionary, new MvcRouteHandler()));
}

The default route (without the culture) is defined for the application; this will be used by the GlobalisedRoute (pre-pended with “{culture}/”) and by the default fallback route. The ignore clause for resources is added as usual. Next a dictionary of default values is created with the same values as the “normal” application. Note that the code does not add a default for culture. It is possible to do this, but this will result in the globalised version always being called, overriding the browser default language. This would provide a poorer user experience (though it would make the application simpler after a refactor) so we will not supply the default.

The globalised route is created and added to the routes collection first so that globalised constraint created within it can determine whether the URL matches the “globalised” version containing a culture value. If the non-globalised default was added first, it would match any supplied culture as a controller value, and an error would occur as it tried to call a non-existent controller based on the culture value supplied. Note that the globalised route takes the unglobalised route and defaults in its constructor, so it can add a culture value at the beginning of the route, keeping the rest of the route and defaults the same as the normal strategy.

GlobalisedRoute

The globalised route class abstracts code that could have been easily added to global.asax, this helps keep the global.asax clear of code, and also keeps most of the globalisation support work in a separate assembly.

public class GlobalisedRoute : Route
{
    public const string CultureKey = 
           "culture"; static string CreateCultureRoute(string unGlobalisedUrl)
    {
        return string.Format("{{" + CultureKey + "}}/{0}", unGlobalisedUrl);
    }    public GlobalisedRoute(string unGlobalisedUrl, RouteValueDictionary defaults) :
        base(CreateCultureRoute(unGlobalisedUrl),
                defaults,
                new RouteValueDictionary(new { culture = new CultureRouteConstraint() }),
                new GlobalisationRouteHandler())
    {
    }
}

This class creates the route {culture}/{controller}/{action}, and adds CultureRouteConstraint to ensure that the globalised handler is only called if a culture code in a valid format is provided. It also instantiates the GlobalisationRouteHandler ready for use. I have also taken the decision not to overload the constructor to mirror those in the base class; implementing versions of the base class’ constructors does not seem to make much sense as we supply the values in this class. If you put this code into production, constructors similar to those in the base class might be necessary.

GlobalisationRouteHandler

The route handler does the work of creating an IHttpandler to provide the response to the request.

public class GlobalisationRouteHandler : MvcRouteHandler
{
    string CultureValue
    {
        get
        {
            return (string)RouteDataValues[GlobalisedRoute.CultureKey];
        }
    }
    RouteValueDictionary RouteDataValues { get; set; }
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        RouteDataValues = requestContext.RouteData.Values;
        CultureManager.SetCulture(CultureValue);
        return base.GetHttpHandler(requestContext);
    }
}

The properties are just there to provide clearer access to the value stored in the culture part of the route. CultureManager.SetCulture(CultureValue); does the first bit of real work, it uses the culture manager to set the UI and main thread cultures. Finally, it calls the base [default] handler’s GetHttpHandler method, ensuring the application’s route handling continues the same way as if the application is unglobalised. Note that I have left out the constructors in the snippet above.

CultureRouteConstraint

The first part of the URL after the host name is assumed to be the culture value by GlobalisedRoute, this constraint is added by it to pattern-match to see if it is in the format mentioned at the beginning of this article (“xx” or “xx-xx”).

public class CultureRouteConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext,
        Route route, 
        string parameterName, 
        RouteValueDictionary values, 
        RouteDirection routeDirection)
    {
        if (!values.ContainsKey(parameterName))
            return false;
        string potentialCultureName = (string)values[parameterName];
        return CultureFormatChecker.FormattedAsCulture(potentialCultureName);
    }
}

This class extracts the potential “culture” value and uses the format checker to return true if it matches the pattern. By returning true, the GlobalisedRoute is used, otherwise the default route specified in global.asax is used. As was stated in the application overview, the culture may not be supported or even valid, it just needs to have the correct format. The sample code has a second, commented Match method that can replace the above to allow matching of supported cultures only.

CultureManager and CultureFormatChecker

The code for these can be seen in the download, they do not do anything complicated, so a description of the main functionality of these classes will be simpler than the code itself:

CultureManager

  • Has a private “SupportedCultures” dictionary mapping the two letter ISO code onto an equivalent CultureInfo object.
  • A private property returning the CultureInfo of the default culture; use when a culture formatted but invalid culture is requested in the URL.
  • A public SetCulture(string code) method that attempts to find the culture from the dictionary. It ignores the case and parses down to the short, two letter form. If the culture is supported, it sets the UI and main thread to the culture; if not, it sets these to the default culture. The culture should only normally be set to the default if the URL contains a valid but unsupported culture (such as “fr-fr”/ “fr”) or an invalid but correctly formatted culture string (such as “xx-yy” / “xx”).

CultureFormatChecker

This is the simplest class to describe, its single method bool FormattedAsCulture(string code) returns true if it matches a regex pattern that matches “xx” or “xx-xx” where x is a letter character. For those of you who like such things, the actual regex is: ^([a-zA-Z]{2})(-[a-zA-Z]{2})?$.

Results (so far!)

First, for confirmation that, without the URL overriding the behaviour, the application still uses the browser default language (see part 1 if you do not know how to do this!), here is the page with the browser default set to English:

EnglishDefault.jpg

The browser default is set Arabic:

ArabicDefault2.jpg

So far so good! From this point forward, the browser default is kept to Arabic (the images are less wide to accommodate the page and the URL!). Here the a culture is specified in the URL, using the short form:

ArabicShortEnglish.jpg

And the long form:

ArabicLongEnglish.jpg

Now we test that the application defaults to English if an unsupported culture is specified in the URL:

ArabicUnsupported.jpg

Finally, a basic test that the application still processes the route by manually specifying the default values for controller and action:

ArabicEnglishAndDefaultsSupplied.jpg

Adding UI support to switch languages

As briefly discussed in part 1, it is not desirable to force the user to use their browser’s default language. It is quite possible, for example, that they are abroad and using a locked-down machine in an Internet café; less technical users might not know how to switch it back to something they can read (trust me, I used to work in tech support :)). This final section outlines adding a hyperlink that returns the current page with the “other” language. As we are going to display the opposite language (a link for Arabic in the English page and a link for English in the Arabic page), we need to add the content to our resx files. These will be common values, so they are added to Common.resx and Common.ar.resx. For the English resource file:

OtherLanguageKey ar
OtherLanguageName عربي
ReverseTextDirection rtl

ReverseTextDirection is there to support marking the dir attribute of the link to the same direction as the text it contains, in Arabic’s case, right-to-left. This helps keep the browser select behaviour consistent.

The next step is to add the hyperlink moving to the current page, but with the addition of the opposite culture’s language key. To do this, a static class was added to the library, which provides an extension method on HtmlHelper; this is called GlobalisedRouteLink:

public static class GlobalisationHtmlHelperExtensions
{

    //Snip……

    public static MvcHtmlString GlobalisedRouteLink(this HtmlHelper htmlHelper, 
           string linkText, string targetCultureName, RouteData routeData)
    {
        RouteValueDictionary globalisedRouteData = new RouteValueDictionary();
        globalisedRouteData.Add(GlobalisedRoute.CultureKey, targetCultureName);
        AddOtherValues(routeData, globalisedRouteData);
        return htmlHelper.RouteLink(linkText, globalisedRouteData);
    }
}

The helper makes a new route dictionary and adds the culture first; the AddOtherValues method (snipped out) iterates over the route passed into the method, adding the remaining values (skipping the culture if already present). It then uses the normal RouteLink method to generate the full, globalised link.

Plumbing in

For the ASPX view engine, import directives were added:

<%@ Import  Namespace=" InternationalizedMvcApplication.Resources" %>
<%@ Import  Namespace="MvcGlobalisationSupport" %>

The first statement removes the need to qualify the namespace when adding the resource from Common.resx, the second adds the namespace containing the GlobalisedRouteLink extension method. Then the code to provide the link is added like this to the top of the body from Part 1 of the article:

<div dir="<%= Common.ReverseTextDirection %>">
    <%= Html.GlobalisedRouteLink(Common.OtherLanguageName, 
                   Common.OtherLanguageKey, ViewContext.RouteData)%>
</div>

Similarly, for Razor, a using markup is added:

@using MvcGlobalisationSupport
@using InternationalizedMvcApplicationRazor.Resources;

Then this div is added to the top of the body:

<div dir="@Common.ReverseTextDirection">
        @Html.GlobalisedRouteLink(Common.OtherLanguageName, 
                  Common.OtherLanguageKey, ViewContext.RouteData)
</div>

Razor users should note that the above markup is the only thing different between the Razor and the ASPX view engines in this article!

Testing the UI changes

First, the browser default language is set to English, at the project run:

ArabicLink.jpg

The HTML source for the link is generated as follows:

<div dir="rtl">
    <a href="http://www.codeproject.com/ar">عربي</a>
</div>

Notice that we have supported the language direction and the helper method has generated the reference. Clicking the link results in:

EnglishLink.jpg

Here is the English link’s HTML:

<div dir="ltr">
    <a href="http://www.codeproject.com/en">English</a>
</div>

Note that the language is explicitly specified. The eagle-eyed amongst you will notice that the controller and action of the current page are not specified, the MVC 3 framework is intelligent enough to know that the defaults are in use, so it does not provide them explicitly. To test if the URL is generated correctly when values other than default are used, enter the URL providing a value for an unused (but available!) ID, such as http://localhost/ar/Home/Index/1. When rendered, the required HTML is rendered to the browser for the link:

<div dir="ltr">
    <a href="http://www.codeproject.com/en/Home/Index/1">English</a>
</div>

Great success!

Points of interest

  • This is one strategy, other architectures will fit! Hopefully mine is clean (if complicated to explain…).
  • I have tested this strategy with the optional ID parameter and other controllers and actions in a mock-up app for my employer. To keep the code simple, I have not done this in the download, but you should be able to add new actions and controllers as in a normal application. The only time you need to worry about the bilingual status is for actual display differences (which would be necessary anyway) or if you want to provide other ways of switching language.
  • This mechanism can be extended for multilingual apps. The biggest problem will probably be replacing the language link with a combo box and working out how this is populated. Not a great deal of work should be needed (famous last words!). Can use regional variations of languages, but it would be sensible to do the extra work needed to validating those supported, and returning the default for variations that are not, replacing my naive pattern matching mechanism.
  • It should be possible to add a section to the web.config that states which languages are supported by providing a list of two or even four-letter codes. To do this, the coder would need to replace the hardcoded addition of cultures to the dictionary in the CultureManager and change the code that sets the default language. Again, the code to populate the language switching mechanism is likely to be the biggest headache.
  • If there is enough call, I will add a third part making the application multi-lingual.

Conclusion

Although we have gone into some technical detail and much work is required, globalising an MVC 3 application can be achieved with a very light touch from the perspective of the web application:

  • Add the RESX files and use them in the views as outlined in part 1
  • Add the globalisation support assembly described here
  • Make the changes to global.asax
  • Add a control to allow the user to manually override the default browser setting

The real and heavy work is done in the globalisation support assembly. The whole experience of globalising an MVC application is far less smooth and intuitive than a standard ASP.NET app. I hope as the MVC framework matures and becomes more mainstream (as it seems to be doing now), better Internationalization support is added.

Notice also that the language is discriminated against by the URL proper, not parameters, as sometimes is the case with a straight ASP application, and you can easily add language-specific metadata. The two together can be used, along with a sitemap to generate better language-specific search engine rankings.

As always, if you have any comments or feedback, please feel free to ask.

History

If you edit this article, please keep a running update of any changes or improvements you've made here.

  • 7 June 2011: Initial article.

License

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

About the Author

Keith Barrow
Software Developer (Senior)
United Kingdom United Kingdom
Member
I Graduated in Natural Sciences (Chemistry & Physics)from Durham University, where I did not cover myself with glory, but did fill, and often cover, myself with beer.
 
I Qualified as a secondary school teacher, taught for a while as a supply teacher. I hated teaching. I tried working as a techhie but kept getting promoted to IT Trainer, courtesy of my teaching experience, which I disliked for similar reasons. I spent some time working out what else I could do and reducing beer intake.
 
I Realised that I should work as a programmer, having enjoyed it a hobby since I was a nipper in the halcyon days of the Sinclair Spectrum (48k, Rubber Keyboard). Spent two weeks working out why I didn't think of this originally, instead of starting my dull-as-ditchwater Chemistry degree 8 years earlier. Had a beer to celebrate.
 
I Graduated in 2001 with an MSc from Newcastle Uni in Comp Sci. Did cover myself with glory, and drank some beer.
 
.Netting ever since, and loving it. Though I have largely given up beer due to not being able to hack the pace like I used to.
 
I was born, brought up, and have lived most of my life near Newcastle. In a fit of temporary insanity I moved to Amman, in my wife's homeland of Jordan, but made it back safely to the UK without any extra holes being made in my person by bullets. To be fair, Jordan is pretty safe, if you discount the roads.
 
Visit Jordan if you can by the way, the landscape is beautiful and varied, the food excellent and the people the friendliest on earth, after Geordies naturally Smile | :) .

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5membercode71113 May '13 - 12:55 
Great article Keith! I'm trying to map a globalised route for something like Community/{cityName}/forum/{forumEntry}
 
how would you go about that using your solution? many thanks!!!
 
keep oooon codin'
GeneralRe: My vote of 5membercode71113 May '13 - 13:19 
OK looks like I managed to get it to work! The highlighted code is in bold - is that the best way to map a custom globalised route? in this case for Community/{cityName}/forum/{forumEntry} ?
 
When is the part 3 of your article coming out? Smile | :)
 
        public static void RegisterRoutes(RouteCollection routes)
        {
            const string defautlRouteUrl = "{controller}/{action}/{id}";
 
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            const string forumRouteUrl = "Community/{cityName}/forum/{forumEntry}";
            RouteValueDictionary forumRouteValueDictionary = new RouteValueDictionary(new { controller = "Home", action = "LoadForum", id = UrlParameter.Optional });
            Route forumRoute = new Route(forumRouteUrl, forumRouteValueDictionary, new MvcRouteHandler());
            routes.Add("ForumGlobalisedRoute", new GlobalisedRoute(forumRoute.Url, forumRoute.Defaults));
            routes.Add("DefaultForum", new Route(forumRouteUrl, forumRouteValueDictionary, new MvcRouteHandler()));
 
            RouteValueDictionary defaultRouteValueDictionary = new RouteValueDictionary( new { controller = "Home", action = "Index", id = UrlParameter.Optional });
            Route defaultRoute = new Route(defautlRouteUrl, defaultRouteValueDictionary, new MvcRouteHandler());
            routes.Add("DefaultGlobalised", new GlobalisedRoute(defaultRoute.Url, defaultRoute.Defaults));
            routes.Add("Default", new Route(defautlRouteUrl, defaultRouteValueDictionary, new MvcRouteHandler()));
        }

QuestionMulti-lingual Part with Areamembercheetss30 Dec '12 - 21:59 
Nice and elaborated article, can you please add a multi lingual version with some Area management.
 
Great Work! Keep it up!
cheetss

GeneralMy vote of 1memberalt3rn30 Nov '12 - 3:07 
how to handle query string? incomplete! just as incomplete as microsoft technology
GeneralRe: My vote of 1mentorKeith Barrow2 Dec '12 - 1:04 
Query strings will work, I just don't want to use them for language-switching. I was pretty explict in the article about how I needed my URLs to look, and query strings didn't figure as they are pretty ugly from a user's perspective. My code isn't meant to be a whole framework, but a specific solution to a specific requirement.
Secondly, query string language switching is pretty facile to code (The switch can be done in one ugly line of code, of four readable ones in global.asax ) and has been covered elsewhere.

SuggestionRe: My vote of 1memberAlluvialDeposit13 Dec '12 - 1:05 
Please make your comment as constructive as possible. "How to handle query string"? What kind of question is that? Querystrings will work perfect with this solution and what is incomplete with Microsofts technology?
 
--------------------
When Chuck Norris' dreams come true, your worst nightmares begin.
GeneralMy vote of 5memberForoudi13 Oct '12 - 22:57 
awesome tutorial. I just made my web site globalized. thank you!
QuestionSwitch between pages.memberAlejandro Villagran11 Oct '12 - 8:03 
The use of the link to make the change between languages works perfect, but I have a problem, when I move to other page in my app the translation is lost and the new page shows in the default language.
 
For example.
 
I'm in one page, the url shows like this.
http://localhost:58713/Claim/NewVehicleNotice
 
I change the language, the url shows like this
http://localhost:58713/en/Claim/NewVehicleNotice
(I'm changing from spanish to english)
 
And when I try to go to other page in my app I expect that the new page shows in english like this.
http://localhost:58713/en/Claim/NewLocationNotice
 
but it shows in spanish, like this
http://localhost:58713/Claim/NewLocationNotice
 
Can you help me about this problem???
 
Thanks in advance.
 
Alejandro.
AnswerRe: Switch between pages.mentorKeith Barrow2 Dec '12 - 0:49 
Hi, I haven't checked this article in a while. I hope you figured out a solution!
Just in case:
The GlobalisedRouteLink is meant to be used in place of the standard MVC RouteLink, as it preserves the culture. If you are using something else to generate the link, you should be able to create an extension method similar to the GlobalisedRouteLink which calls whatever it is you are using currently.

GeneralRe: Switch between pages.memberAlejandro Villagran24 Jan '13 - 4:02 
Thanks for your help Keith.
 
Actually a found a solution to my problem, I put the links as resources in the corresponding resource files, so the culture is preserve until I change the language in the application.
 
Hope this helps someone else.
 
And thanks again for this article, it was really helpfull.
SuggestionI've added routes.MapGlobalRoute() support.memberMember 324818929 Sep '12 - 16:03 
Thank you for this excellent framework. It is really needed because of the globalization of the routes and bi-lingual sites.
 
One thing that bugged me with this implementation is the lack of the standard MapRoute() extension methods that we all know and use (and abuse) in ASP.NET MVC projects.
 
So, behold, I have modified the the code and enabled a full set of extensions called MapGlobalRoute().
 
The problem I have is, how do i get this code to you? I can't find it on Github to do a pull request.
 
I noticed the license allows me to redistribute the code. So unless there is an objection, I'll move the code to a github repo and provide the original credits back to this article and author. Then I'll submit my revisions for anyone wanting the updates. It will be posted at:
 
https://github.com/eduncan911/MvcGlobalization
 
Thanks again!
Eric A. Duncan - http://eduncan911.com

GeneralRe: I've added routes.MapGlobalRoute() support.adminChris Maunder10 Oct '12 - 1:29 
Hi Eric,
 
An option is for you to create an alternative article by clicking the "Add your own alternative[^]" button to the left )and slightly down) of the article title.
 
This allows you to post an article that's connected to the original article and provides your own, alternative download files as well as a place to explain the changes you've made.
cheers,
Chris Maunder
 
The Code Project | Co-founder
Microsoft C++ MVP

GeneralRe: I've added routes.MapGlobalRoute() support.mentorKeith Barrow10 Oct '12 - 2:51 
Hi Eric, I needed to check the legal side of things with Chris Maunder(the Hamster-in-Chief, who replied before me) as I know these can be tricky.
 
I'm more than happy for you to release with your own version on github, but think Chris's suggestion for a linked article is a better idea.For one thing, this article is linked from MSDN. Whatever happens, the code needs to remain under CPOL[^] licence (this is a requirement of CPOL, a bit like GNU licensed code).
 
The only thing I'd request (other than the back-link here which you've already mentioned) is that you prepend all cs files currently in the MvcGlobalisation Support with the following:
 
// Copyright (C)  2012 Keith Barrow
// Permission is granted to copy, distribute and/or modify this document
// under the terms of the The Code Project Open License (CPOL) 1.023
// or any later version published by The Code Project
// A copy of the license is availavble at http://www.codeproject.com/info/cpol10.aspx
// The oringal article can be found at http://www.codeproject.com/Articles/207602/Creating-a-Bilingual-ASP-NET-MVC3-Application-Part
 
I'll be adding this to the current projects (I missed this when I created the project) when I get time. The other projects contain boilerplate code,so I don't care about the copyright notice there.

QuestionTranslating RoutesmemberCanadianLumberjack27 Sep '12 - 11:47 
Hi Keith,
 
First off, I want to say that what you did and how you explained it was phenomenal. Kudos.
 
I'm looking to tweak what you did a bit and add in translated routes and I came across a posting where someone did just that but I'm trying to figure the best way to marry the two ideas:
 
http://blog.maartenballiauw.be/post/2010/01/26/Translating-routes-%28ASPNET-MVC-and-Webforms%29.aspx[^]
 
Any guidance would be great as my MVC routing skills are fairly fresh.
Thanks in advance!
QuestionDefault locale avoid culture in url.memberParhs15 Aug '12 - 2:33 
This works fine but when you switch language back you get the default locale..
I have set default locale 'el' and locales en , el.
When i switch to 'en' it is fine /en/Home but then when i click the link it gets /el/Home not /Home..
AnswerRe: Default locale avoid culture in url.mentorKeith Barrow16 Aug '12 - 2:26 
This is deliberate: if you click the link you are explicitly stating which language you want. By not specifying you are transparently opting in to your browser default, assuming it is supported.

GeneralRe: Default locale avoid culture in url.memberMember 85519103 Sep '12 - 7:22 
this way you get duplicate content no? /el/Home and /Home are the same pages, though you can access through 2 different urls..
GeneralRe: Default locale avoid culture in url.mentorKeith Barrow3 Sep '12 - 9:52 
Yes, for my last place of work this wasn't a problem. It would be pretty easy to re-direct the "plain" URL /Home to /en/Home if the "language part" wasn't present.

GeneralRe: Default locale avoid culture in url.memberAlejandro Villagran16 Oct '12 - 11:54 
How do you do the redirect???, I'm really lost how to do that...
QuestionHow to deal with AreasmemberMember 821673113 Apr '12 - 4:56 
Hi, great Article. I am unable to adapt it in order to have this running if our project have some areas...
 
I do not know if you can enlighten us with more info¡¡
 
Thanks¡
AnswerRe: How to deal with AreasmentorKeith Barrow16 Jul '12 - 13:40 
Hi, sorry for the tardy reply, I've been in the process of moving from the Middle East back to the UK!
 
I haven't really considered areas, what problems are you facing? Does the area-name work if you put it before the language?

GeneralRe: How to deal with Areasmemberstan160728 Jan '13 - 22:32 
Hi Kaith...thanks for the article...can you please provide some more info on how to add routes with Areas?
 
Thanks,
Kevin
QuestionThird partmemberel_conde3310 Apr '12 - 18:33 
Please !!!!
 
Where is a third part making the application multi-lingual.
 
Please !!!!
AnswerRe: Third partmemberMember 89144581 May '12 - 21:45 
I have the same problem!!! The link does change the URL but does not change the browser's current language! Therefore the URL shows localhost/ar when the link is clicked but the page does not change to arabic!
GeneralRe: Third partmemberMember 89144581 May '12 - 22:10 
Problem solved! There is no need for a part 3! This article works just fine! Very detailed and described step by step! What i had wrong is that since i wanted an Italian language instead of Arabic, i did not change the const string ArabicCultureName = "at"; to const string ItalianCultureName = "it";
QuestionPostbackmemberAlejandro Villagran24 Jan '12 - 11:39 
Sorry about my english, I hope you can understand me.
 
About changing the language, everything worked fine for me. But I got a question, there's a way to do the change of language without a postback, 'cause I don't want to lose the information that I enter in a webform.
 
Thanks in advance.
AnswerRe: PostbackmentorKeith Barrow29 Jan '12 - 9:41 
Alejandro Villagran wrote:
About changing the language, everything worked fine for me. But I got a
question, there's a way to do the change of language without a postback, 'cause
I don't want to lose the information that I enter in a webform.

In the normal run of things, without postback you'd need to send both sets of text in the original Html, or make an AJAX call to get the other language and update the UI text with JavaScript/jQuery. Both are likely to be messy but possible. Both would also lose the advantages of having different URLs for different languages.
Another (better?) option is to put the page in a form and use the language change link as submit (or the selected language as a parameter in the form if you have more than two languages) that way the data entered would be preserved on postback and you can re-populate the values. The only difficult you'd then face is determining that this is a language change rather than a true submit. To work around this you could add a hidden control on the form which is set by the language button as a flag.
 
I haven't had to deal with your specific problem, so this answer comes with the caveat that I haven't actually tried it(I'm pretty new to multilingual sites!). In any case I hope this helps/gives you some pointers.

QuestionLink to part 1...memberYop8321 Jan '12 - 7:39 
For some reason, the links to part 1 in the article do not work for me.
 
The proper link is:
www.codeproject.com/Articles/181738/Creating-a-Bilingual-ASP-NET-MVC3-Application-Part
AnswerRe: Link to part 1...mentorKeith Barrow26 Jan '12 - 4:06 
Thanks for letting me know. I'll fix these when I get time.

AnswerRe: Link to part 1...mentorKeith Barrow29 Jan '12 - 9:25 
It looks like there has been a technical problem. This seems to be resolved as there have been no article updates since your post and the links now work. I tested when I saw your message and they were broken as you said.
 
Sorry to repost this, but readers may not see the similar message you posted in Part 1

Questionassalammemberik2057 Oct '11 - 7:45 
i hope you're fine
and have a project to do in asp.net can you help me kate?Confused | :confused:
AnswerRe: assalammentorKeith Barrow9 Oct '11 - 23:37 
It depends what the problem is, if it is related to this article then feel free to ask. For more general stuff, you'll get a faster and probably better response from the ASP[^] message boards, or Q&A[^]

GeneralMy vote of 5memberwebmarco6 Oct '11 - 2:54 
Very clear and usefull. Thanx!
GeneralRe: My vote of 5mentorKeith Barrow9 Oct '11 - 23:35 
Thanks for your feedback, it makes a big difference to know people like your stuff!

Questionhow to get session on routehandlermembernazishrizvi11 Aug '11 - 17:25 
Hi
I want to get session at GlobalisationRouteHandler class.
I don't want to take language in url so I want to save it in session.
is it possible to get sesison at this layer??
AnswerRe: how to get session on routehandlermentorKeith Barrow11 Aug '11 - 21:44 
You can use the session to do this, but if you do you should switch the site language in global.asax. Most of my code would become redundant in this case.
The reason I did not use the session is that it is bad for SEO: web crawlers need different URLs for different languages. You might want to consider this last point for your site.

GeneralExcellent.memberreallyJim14 Jul '11 - 3:59 
Dude, you rock. I just combined this with a custom database driven resource provider, and it works like a champ. Well done, well documented.
 
Keep up the good work!
GeneralRe: Excellent.mentorKeith Barrow14 Jul '11 - 9:14 
Thanks, glad it helped!

GeneralRe: Excellent. [modified]memberreallyJim25 Jul '11 - 7:44 
Have you looked at areas with this? Curious what I'd need to change to implement that. I've been working with that for a few hours, and can't quite get it to work with the ~/{culture}/AreaName/{controller}/{action}/{id} approach.
 
Specifically, I'm running into problems getting globalized routes configured right for areas.
 
Just wondered if you had encountered anything like this--thank you in advance!

modified on Monday, July 25, 2011 1:52 PM

GeneralRe: Excellent.mentorKeith Barrow25 Jul '11 - 8:07 
I haven't worked with areas, I actually don't have a huge amount of MVC experience.
You can easily modify my code to work with the long-form language codes as the culture part e.g.
 
en --> Default(US) English
en-GB --> British English
en-ZA --> South African English
ar --> Default (Saudi) Arabic
ar-ye --> Yemeni Arabic
ar-sy --> Syrian Arabic
 
So the routing stays ~/{culture}/{Controller}/{Action}/{id}
 
That said, there isn't a one-to-one mapping of the language codes [^]to geographic area. If you want geographical area, I would certainly start with your strategy.

GeneralRe: Excellent.memberlorenzo.gr16 Aug '11 - 6:29 
Hi reallyJim,
have you solved the areas issue?
 
I've read you use a custom database driven resource provider instead of .resx files.
Can you share that code?
 
Thanks
lorenzo

GeneralRe: Excellent.memberutilityboy26 Aug '11 - 3:37 
Hi Lorenzo,
 
See my response below. Maybe it'll help regarding areas.
GeneralRe: Excellent.memberutilityboy25 Aug '11 - 18:32 
Hi... thanks for the tips in this article.
 
Regarding areas, I read a couple articles about adding some supporting DataTokens to the route, but never got it to work properly constructing them on my own. Instead I had luck with this:
 
        public override void RegisterArea(AreaRegistrationContext context)
        {
            var defaultRoute = context.MapRoute(
                "Admin_default",
                "Admin/{controller}/{action}/{id}",
                new { action = "Index", id = UrlParameter.Optional }
            );
 
            var globalizedRoute = new GlobalizedRoute(defaultRoute.Url, defaultRoute.Defaults, defaultRoute.DataTokens);
 
            context.Routes.Remove(defaultRoute);
            context.Routes.Add(globalizedRoute);
            context.Routes.Add(defaultRoute);
        }
 
Calling MapRoute on the AreaRegistrationContext constructs the proper DataTokens dictionary. You'll need to add another constructor to your route as Keith mentioned in Part 2 of his article:
 
        public GlobalizedRoute(string pRawUrl, RouteValueDictionary defaults)
            : base(
                    CulturedRoute(pRawUrl),
                    defaults,
                    new RouteValueDictionary(new { culture = new CultureRouteConstraint() }),
                    new GlobalizedRouteHandler()
            )
        {
        }
 
        // add this constructor as well
        public GlobalizedRoute(string pRawUrl, RouteValueDictionary defaults, RouteValueDictionary tokens)
             :base(
                    CulturedRoute(pRawUrl),
                    defaults,
                    new RouteValueDictionary(new { culture = new CultureRouteConstraint() }),
                    tokens,
                    new GlobalizedRouteHandler()
            )
        {
        }

GeneralVery similar [modified]memberMember 782108316 Jun '11 - 6:25 
I have just finished implementing a solution very similar to this (identical URL structure and language detection), it's great to see I wasn't crazy with the way I went about it.
 
The use of the resx for direction is a great tip, this could potentially be used for other variables like font-face and size.
 
You may also be interested in using model metadata described by DataAnnotations for localization.
 
Thanks for sharing!

modified on Thursday, June 16, 2011 12:36 PM

GeneralRe: Very similar [modified]memberjgauffin7 Feb '12 - 0:25 
I've done something similar: http://blog.gauffin.org/2011/09/easy-model-and-validation-localization-in-asp-net-mvc3/[^]
 
Github project: https://github.com/jgauffin/griffin.mvccontrib[^]
 
Nuget packages are available.
GeneralLike itmemberdevnet24714 Jun '11 - 5:18 
hi,
Learning asp.net mvc and I am currently working on application that should be multilingual.
 
I have looked at your code and might be wrong but how do you differentiate between "en-us" and "en-gb"?
 
In my current work application not mvc we have all our localisation in a separate assembly, and within this assembly we have all the various resource files.
 
Is it possible with mvc to reference an external assembly?
 
If you were to store static html pages with formatting eg "bold,red bits etc.." and these pages would have to be in multiple languages which approach do you suggest?
HTML pages in resource files?
 
well done and thanks for any advice.
thanks a lot

GeneralRe: Like itmentorKeith Barrow15 Jun '11 - 3:15 
Thanks for your response!
 

devnet247 wrote:
I have looked at your code and might be wrong but how do you differentiate
between "en-us" and "en-gb"?

I don't: I treat all "Englishes" and the same (this is the required behaviour for my employer, so this is what I implmented). The mechanism here can be extended, if you internationalising you can use the long-form (e.g. en-us) culture code in the CultureManager's dictionary instead, and when checking the culture part of the route, simply ensure the long code is available as a key in the dictionary: This removes the need for the regex matching that I have.
 
devnet247 wrote:
Is it possible with mvc to reference an external assembly?

Yes: these are just compiled down to classes.
 
devnet247 wrote:
If you were to store static html pages with formatting eg "bold,red bits etc.."
and these pages would have to be in multiple languages which approach do you
suggest?

I'd avoid doing it for maintainability purposes, but I can't see any technical reason against storing the html in a resx file.

GeneralRe: Like itmemberdevnet24715 Jun '11 - 4:08 
Thanks for your time and answer.
thanks a lot

GeneralMy vote of 5memberMonjurul Habib7 Jun '11 - 9:58 
nice article.
GeneralRe: My vote of 5mentorKeith Barrow8 Jun '11 - 7:29 
Thanks, I was worried it wasn't clear!

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 30 Jan 2012
Article Copyright 2011 by Keith Barrow
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid