Globalization And Localization With Razor Web Pages

Globalization is the process of preparing your site so that it is accessible to as wide an audience as possible. This is largely achieved by presenting content in the native language of the visitor. This article explores how you can approach this task within the ASP.NET Web Pages framework.

ASP.NET already provides a localization framework, which is based on Cultures and Resources. Web Pages have a Culture property and a UICulture property. The first of these controls how culture-sensitive formatting is managed for dates, currencies, numbers and so on. The second property works with .NET Resource files. More of these in a second. Culture and UICulture values are defined using a standard set of values described on MSDN. These values are typically composed of two lower-case letters defining the language, and two upper case letters defining the locale. For example, English language is defined as "en". Britain is defined as "GB". British English is therefore defined as "en-GB", while American English is defined as "en-US". These culture codes are important, as they are used by the ASP.NET Globalization framework to locate the correct resource file.

If you want to present different version of the same content to users based on their language, you need a way of organising multiple versions of the same content. One way in which you can do this in the .NET world is through Resource files, which are XML files ending in .resx. Resource files contain name/values, which represent strings to be used in web pages. WebMatrix doesn't (currently) offer a way to generate resource files, which means you have two options: download and use the free version of Visual Web Developer (or use Visual Studio if you have it), or create text files and use the resgen.exe tool that comes with the Windows SDK to convert them to valid .resx files. In the following example, I used Visual Studio. You can also use a database as a resource store rather than resource files. You will see both approaches used in this example.

The sample site used to illustrate the concepts discussed here is a simple one:

It displays some content which is determined by the language chosen by the user from the dropdown list at the top. The English version is shown above, and this is the French version:

Once a new empty site has been created, it needs to be opened in Visual Web Developer or Visual Studio, which is achieved by clicking the Launch button in the ribbon bar. Right click on the project name in solution Explorer and choose Add New Item, and then from the dialog that results, choose Resource file.

I named mine TestResource.resx, but you can call it anything you like. When you click the Add button, you will be prompted to save the file to a folder called App_GlobalResources. Agree to this option, as App_GlobalResources is a special ASP.NET folder which is expected to house resource files. You'll see how this convention works shortly. Resource files themselves are little more than convoluted name/value pairs. The Resource File Editor within VS/VWD offers an easy way for you to input your name/value pairings. Here's a screenshot showing the sample site resource file:

In the Name column, you see what are effectively labels. These will actually become methods that return strings when the resource file is compiled on first run. The strings that these methods return are the corresponding Value entries. When the resource file is compiled, it becomes a class named after the file name - in this case TestResource.

At the moment, you have one resource file which represents the default culture for the site. You need additional resource files for other languages. These need to be named carefully, as the framework expects to find valid culture codes as part of the file name. A French resource file can be created by copying the English one, and renaming the result TestResource.fr-FR.resx. Notice the culture code fr-FR appearing between the file name and its extension. Any errors in the culture code will lead to compiler objections to the effect that "The namespace 'Resources' already contains a definition for 'TestResource'", where TestResource is the name of the resource file. A valid German version of the resource file would be named TestResource.de-DE.resx. Leave the Name column entries as they are and replace the Value entries with French and German versions respectively. So now you should have three different resource files:

There are other files in the image, but they can wait for a bit. Next, you need a database. It's a simple one-table database. The table has four columns - and Id column (int IDENTITY), a column called section, which defines the location of the content, the content itself - stored in an ntext field, and a column called Culture. This column will hold the culture code for each row of data. The content itself is entered three times for the Default page - once for each of the languages represented by the Resource files.

Back to the other files in the earlier image... Here's the _Layout.cshtml page:

@using Resources;
<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div>
            <form method="post">
                @TestResource.SelectLanguage
                <select name="lang">
                    <option></option>
                    <option value="en-GB" @(Culture == "en-GB" ? "selected=\"selected\"" : "")>English</option>
                    <option value="fr-FR" @(Culture == "fr-FR" ? "selected=\"selected\"" : "")>French</option>
                    <option value="de-DE" @(Culture == "de-DE" ? "selected=\"selected\"" : "")>German</option>
                </select>
                <input type="submit" value="@TestResource.Submit" />
            </form>
        </div>
        @RenderBody()
    </body>
</html>

There is a using statement at the top of the file that makes the resources class available via its namespace (which is automatically generated on compilation). The generated namespace is always Resources. The layout page contains a drop down which acts as a language selector. This will be available on any other page that makes use of the layout file. You can see the first reference to one of the TestResource class' methods - SelectLanguage. This returns the appropriate string based on the culture of the page. And that is driven by the choice made using the select list. Notice also that the Page's Culture property is referenced simply by the property name.

The culture is set within the _PageStart.cshtml file:

@{
    Layout = "~/_Layout.cshtml";
    if(!Request["lang"].IsEmpty()){
        Culture = UICulture = Request["lang"];
    }
}

In fact, both the Culture property and the UICulture property are set here.

The final page is the Default page itself:

@using Resources;
@{
    var db = Database.Open("Localization");
    var sql = "SELECT Content FROM Content WHERE Section = 'Default' AND Culture = @0";
    var content = db.QueryValue(sql, Culture);
}
<h1>@TestResource.Welcome</h1>
<p><img src="images/@TestResource.FlagImage" /></p>
<p>@TestResource.Intro</p>
<p>@content</p>
<p>Currency: @(10000.ToString("c"))</p>
<p>Date: @DateTime.Now</p>

This page also makes the Resources namespace available via a using statement. You can see a number of examples of the TestResource class methods being referenced. One of those examples returns a file name, which is used as part of the src attribute to an image. The others return strings to be rendered verbatim. The database query uses the value of the page's Culture property to obtain the correct version of text for the current culture. Finally, two examples showing the Culture property at work - dictating the way that currency and dates are formatted for display.

This article has explored the basics behind localizing a Razor Web Pages site. You have seen two approaches - Resource files and database. Resource files compile to dlls when the application first runs. For this reason, they are best used for content that changes infrequently. Since they are easiest to work with when you have specialist tools, they are not suitable for storing user-provided values. Databases are better for user-supplied content and that which may change on a regular basis.

A sample site containing all the code is available as a GitHub repo.

 

Date Posted: Monday, October 10, 2011 9:35 PM
Last Updated: Monday, December 17, 2012 9:53 PM
Posted by: Mikesdotnetting
Total Views to date: 59580

10 Comments

Monday, November 21, 2011 12:46 PM - Ankit

its good

Monday, September 24, 2012 5:57 PM - Carole

Looks like what I need, but including the project file would've been nice.

Tuesday, September 25, 2012 12:01 PM - Mike

@Carole,

The sample containing the code for the article is provided in a link which can be found at the end of the article - just above the Twitter and ratings features. It's been there all the time...

Wednesday, November 14, 2012 6:01 PM - Kalle

We are setting up a multilingual website for our newly created Malta Centre and your tutorial has been very helpful. We encountered two problemes, however: 1. Setting culture in the way you suggest works for the current page and its componenents (e.g. Layout), however it doesn't persist across pages. I worked around this using a session variable, which works but is not perhaps the most elegant method; 2. - and more serious - since the culture is set by posting back to the same page, it interferes with additional forms on the same page, which is very annoying. Have you any suggestions?
Thanks in advance, Kalle

Wednesday, November 14, 2012 7:34 PM - Mike

@Kalle,

I think I would use a cookie to store the language preference. That can be retained across visits so you don't have to ask the visitor again in the future. The language selector should not interfere with other forms being posted. I would imagine you have a design flaw somewhere. If you post details of your actual issue to the forums at www.asp.net, someone should be able to help you resolve the issue.

Sunday, February 10, 2013 2:30 PM - oleg_harp

Mike, thanks for your work!
What's about SEO? Your pages have the same name but different content. Would it be better to give them different names, for example fr/Default, de/Default and so on?

Monday, April 1, 2013 6:12 AM - waqar

excellent article

Thursday, May 2, 2013 6:43 AM - Amit Rohilla

It is okay, it is good for static data, but i want to know how to translate dynamic data unto any language using culture??

Thursday, May 16, 2013 8:01 AM - al-ma

you can use the following method with cookies to make the language selection persistent.

@{

Layout = "~/_SiteLayout.cshtml";
if (!Request["lang"].IsEmpty())
{
//this runs when you post the language change so I save the language to a cookie named lang
Response.Cookies["lang"].Value = Request["lang"];
Culture = UICulture = Request["lang"];
}
else if (Response.Cookies.Get("lang").Value != null)
{
//this will only run if there was no post request with lang and if there is a cookie lang set
Culture = UICulture = Response.Cookies.Get("lang").Value;
}

}

Btw Mike cheers for the great effort with these turorials! I got your book as well and pretty much taught myself the basics in a weeks time with very little previous experience

Saturday, May 31, 2014 12:43 PM - Nandhagopal M

Nice articles
Add your comment

If you have any comments to make about this article, please use this form to do so. Make sure that your comment relates specifically to the article above. More general comments can be posted through the form on the Contact page.

Please note, all comments are moderated, and some may not be published. The kind of things that will ensure your comment is deleted without ever seeing the light of day are as follows:

  • Not relevant to the article
  • Gratuitous links to your own site or product
  • Anything abusive or libellous
  • Spam
  • Anything in a language I don't understand including gibberish.

I do not pass email addresses on to spammers, so a valid one will assist me in responding to you personally if required.

Recent Comments

Gautam 11/20/2014 8:01 AM
In response to I'm Writing A Book On WebMatrix
Hello Mike, I read your book, loved it! However, I have a few request/suggestions: 1) an example...

Bret Dev 11/19/2014 8:39 PM
In response to The Difference Between @Helpers and @Functions In WebMatrix
Excellent post! One concern - where can you place global @Functions code within an MVC project to Is...

Rob Farquharson 11/19/2014 4:28 PM
In response to iTextSharp - Links and Bookmarks
How can I place text at an absolute position on the page? Also, how can I rotate text?...

Andy 11/17/2014 8:08 PM
In response to MVC 5 with EF 6 in Visual Basic - Sorting, Filtering and Paging
Hello I'm testing your sorting instructions above. This is great and I was able to get it to work...

Gautam 11/17/2014 5:51 PM
In response to WebMatrix - Database Helpers for IN Clauses
Hi Mike, I am very new to programming: In the above example if I want to use a delete button the...

donramon 11/17/2014 3:22 PM
In response to Entity Framework 6 Recipe - Alphabetical Paging In ASP.NET MVC
Congratulations on your new website look and the excellent articles. Thank you!...

Gautam 11/17/2014 11:26 AM
In response to Looking At The WebMatrix WebGrid
Hi Mike, I add the jquery script at the end of my html file.. when ajax attribute is added to the be...

Chet Ripley 11/15/2014 6:57 PM
In response to Adding A New Field
It appears the command is case sensitive. I had the same issue as Cameron. When I changed the to it...

Alvin 11/14/2014 12:49 PM
In response to Razor Web Pages E-Commerce - Adding A Shopping Cart To The Bakery Template Site
Great article Mike! When do you plan to extend the bakery shopping cart beyond this point?...

Gautam 11/14/2014 10:16 AM
In response to Web Pages - Efficient Paging Without The WebGrid
to get the count can we use only the below sql, why to join category and author table var sql =...