[
Advertise | Submit Code | About us | Contact us | Link us
]
Go!
Membership Services
Login
Register

Home
C# General

General

C# Language

Design & Architecture

Algorithms

Database

Security

Active Directory

COM Interop

Remoting
C# Windows Forms

General

Combo and List boxes

Miscellaneous Controls

Button Controls

Edit Controls
Cutting Edge

ASP.NET 2.0

Visual Studio 2005

Windows Longhorn

SQL Server 2005
C# Multimedia and GDI+

General

DirectX

GDI+

Audio
Internet & Web

General

Images and multimedia

Database

Utilities

Security

ASP.NET Controls

Design and Architecture

Webservices
.NET

General

Design & Architecture

Algorithms

Database

Security

Active Directory

COM Interop

Remoting

ADO.NET

XML.NET

Tools

Enterprise

IDE
Visual Basic .NET

VB.NET General

VB.NET Controls
General Reading

.NET Books Review

Product Showcase

Book Chapters

Business Design & Strategy
Community

Discuss

Job Board

Discussion

CodeXchange
DeveloperLand

Advertise

Submit Code

About us

Contact us

Link us
Miscellaneous

Favorite Links

Downloads

Programming Sites

Top Stories
Regular Expressions

E-Mail

Date/Time
Home > Internet & Web > General
Creating multilingual websites - Part 1
Posted by on Tuesday, February 07, 2006 (EST)

Extend the existing globalization capabilities of .NET to create flexible and powerful multilingual web sites. First, create a custom ResourceManager, and then create custom localized-capable server controls to easily deploy multilingual functionality.

This article has been viewed: 8,225 times
Technology: General.

LocalizedSample.zip (77.55 KB)
LocalizedSampleVB.zip (77.00 KB)

Contents

I'd like to thank Jean-Claude Manoli for developing his C# Code format [^], which i used in writing this tutorial.

Introduction

Developing websites to support multiple languages can be a challenging and time-consuming process. With standard HTML pages this involves creating and maintaining duplicate versions of each page for each supported language as well as having the language content embedded into the HTML, where content cant easily be edited. While the process improved slightly with the introduction of scripting technologies such as ASP and PHP, no significant development or maintenance time was saved. For those of you who have to develop multi-lingual interfaces and applications, youll be glad to know that ASP.Net makes things considerably easier.

ASP.Net and the .Net framework ship with support for multilingual applications, namely in the form of Resource Files, the CultureInfo class and the System.Globalization and System.Resources.ResourceManager namespaces. Unfortunately in its present state, localizing content in ASP.Net applications is still a tedious process. Like everything else .Net though, the object model and sheer power available makes extending what's already available and developing new functionality to support better localization easy as 1 - 2 - 3.

In this first part, we'll develop a custom resource manager which avoids the limitation of .Net Assembly Resource Files as well as extend a number of classes to easily support localization. In the second part [^] well spend more time talking about creating multilingual applications, specifically looking at database implementations and techniques.

By the end of this tutorial you should be able to create multilingual applications with a minimum of work and maintenance and be able to easily add new languages to it later on.

Top Go to Table of Contents

Before we get started

If you aren't familiar with localization in .Net, don't worry. This tutorial mostly skips what's available in .Net and talks about alternatives to make the job easier. There are a couple core principals you should know though. (Even if you don't know the basic, you can skip this section and download the simple application [^] I've created to showcase the core functionality, playing with it will probably be the best way to understand)

Top Go to Table of Contents

Basics

The way localization works in .Net is fairly straightforward. Content is stored in a pretty simple XML files called Resource Files. You create a Resource File for each supported language (more can be added later on). When the application is compiled, the resource files are embedded into assemblies - the default resource file is embedded in the main assembly (.dll file), language-specific resource files are embedded into their own assemblies called satellite assemblies. Resource files are pretty simple and look a lot like a Hashtable, they have a name and a value - the name is the same for all resource files, and the value is a language specific translation of some content. In essence, this allows you to use the System.Resources.ResourceManager class to do things like:

UserNameLabel.Text = myResourceManager.GetString("Username");
UserNameValidator.ErrorMessage = myResourceManager.GetString("RequiredUsername");

The resource manager will automatically load the right resource file based on the current thread's CurrentCulture value - more on this in the next section. Hopefully you are already seeing a lot of potential. Some of the key highlights are:

  • Content is separated into simple XML files
  • There's a separate XML file for each supported language
  • The code to load values is relatively simple and short
  • The ResourceManager class automatically retrieves the content from the right XML file based on the thread's CurrentCulture value
  • You can easily have 1 actual page, for N supported language

Top Go to Table of Contents

Cultures

It's important to have a good understanding of Cultures since our new code will make use of them - specifically the System.Globalization.CultureInfo class and the culture name value which follows the RFC 1766 naming standard. Basically, you create a new CultureInfo instance by specifying the culture name in the constructor:

CultureInfo c = new CultureInfo("en-US"); //creates a CultureInfo instance for American English
CultureInfo c = new CultureInfo("en-AU"); //creates a CultureInfo instance for Australian English
Cultureinfo c = new CultureInfo("he-IL"); //creates a CultureInfo instance for Israel Hebrew

Once you have a CultureInfo instance, you can set the current thread's UIculture to it, which will make your ResourceManager in the above code automatically fetch the content from the right XML resource file.

CultureInfo c = new CultureInfo("en-US"); //creates a CultureInfo instance for American English
System.Threading.Thread.CurrentThread.CurrentCulture = c; //Will automatically format dates and such
System.Threading.Thread.CurrentThread.CurrentUICulture = c; //Used by the ResourceManager to get the correct XML File

In part 2 [^] we'll discuss ways to figure out which culture to load, but for now, it can be as simple as passing a code in the QueryString. For example, when lang=f is present, the French Canadian culture should be used. The other key factor is where to do all of this. The simplest and most logical place is in the Global.Asax's Begin_Request.

Top Go to Table of Contents

Download dummy application

The best way to understand the basics is to play with some code. I've created an extremely basic VB.Net web application to demonstrate the basic principals. Download it and play with it. Look at the structure of the 3 resource files, the codebehind for index.aspx, and the code in global.asax. Download [^]

Top Go to Table of Contents

Why not use what's available as-is?

While it's certainly possible to develop a multilingual application with the tools provided with ASP.Net, there are a number of limitation which make the task less than streamlined. Some of the key problems are:
  • Resource files are embedded into [satellite] assemblies
  • Resource files can't return strongly-typed objects
  • Web controls aren't easily hooked with resource files
While the list might seem small, the above three issues can be quite serious - with the first being the worst. For example, since resource files are embedded into assemblies, its very difficult to ship a product which provides the client with the flexibility to change the content - a feature offered by many products. At my previous job, every time the translation department wanted to change some text, we'd need to recompile the entire application, stop 20 web servers and copy the .dll into the bin folder - a frustrating process.

Top Go to Table of Contents

Building a better Resource Manager

Our first task is to build a better Resource Manager which won't cause our Resource Files to be embedded into assemblies. This will allow Resource Files to be easily edited in a production or client environment. Our core functionality will be located in three functions:
  1. The public method GetString which is used throughout the application to access the required resource
  2. The private method GetResource which gets a Hashtable either from the cache or by calling LoadResource
  3. The private method LoadResource which parses the XML file and stores it into the cache

Top Go to Table of Contents

GetString()

public static string GetString( string key) {
 Hashtable messages = GetResource();
        if (messages[key] == null){
  messages[key] = string.Empty;
#if DEBUG
  throw new ApplicationException("Resource value not found for key: " + key);
#endif
 }
 return (string)messages[key];
}

The method accepts a single argument, the key of the resource we want to get. It then retrieves a Hashtable of content using GetResource, which is culture-aware and returns us the correct Hashtable. If the requested key doesnt exist, well throw an exception if the application is in DEBUG mode, else well simply return an empty string.

Top Go to Table of Contents

GetResource()

private static Hashtable GetResource() {
 string currentCulture = CurrentCultureName;
 string defaultCulture = LocalizationConfiguration.GetConfig().DefaultCultureName;
 string cacheKey = "Localization:" + defaultCulture + ':' + currentCulture;
  if (HttpRuntime.Cache[cacheKey] == null){
   Hashtable resource = new Hashtable();
   LoadResource(resource, defaultCulture, cacheKey);
  if (defaultCulture != currentCulture){
   try{
    LoadResource(resource, currentCulture, cacheKey);
   } catch (FileNotFoundException){}
  }
 }
 return (Hashtable)HttpRuntime.Cache[cacheKey];
}

The GetResource() method is slightly more complicated. Its goal is to retrieve a Hashtable which can be looked up by a key to retrieve a value. The method will first look to see if the Hashtable has already been loaded and cached [line: 5]. If so, it simply returns the value from the cache. Otherwise it will use LoadResource() to parse the appropriate XML file [lines: 6-13]. Something worthy of noting is that the "appropriate XML file" is actually a mix of the XML file for the current culture as well as the one for the default culture. The default culture is specified in the configuration file [line: 3], and the current culture is retrieved from the current thread's current culture [line: 2].

First the default culture is loaded [line: 8], and then the current culture is loaded [line: 11]. This means if a key is defined in both XML files (which most should be), the default value will be overridden by the culture-specific value. But if it doesnt exist in the culture-specific value, the default value will be used.

Top Go to Table of Contents

LoadResource()

private static void LoadResource(Hashtable resource, string culture, string cacheKey) {
 string file = LocalizationConfiguration.GetConfig().LanguageFilePath + '\\' + culture + "\\Resource.xml";         
 XmlDocument xml = new XmlDocument();
 xml.Load(file);
  foreach (XmlNode n in xml.SelectSingleNode("Resource")) {
    if (n.NodeType != XmlNodeType.Comment){
      resource[n.Attributes["name"].Value] = n.InnerText;
   }
  }
  HttpRuntime.Cache.Insert(cacheKey, resource, new CacheDependency(file), DateTime.MaxValue, TimeSpan.Zero);
}

LoadResource loads the XML file [line: 4] (it gets the root path from our configuration file [line: 2]) and simply parses it while loading the values into our hashtable [line: 5 - 9]. Finally, the hashtable is stored in the Cache [line: 10].

Top Go to Table of Contents

Other enhancements

Top Go to Table of Contents

Wrappers

There are a number of minor enhancements which can be done to our ResourceManager class. For example, I build bilingual webpages in English and French. Annoyingly, in English a colon is always glued to the word it follows, but in French there has to be a space. For example:

Username:  //English
Nom d'utilisateur : //French

This means the colon needs to be localized. Instead of using the GetString() method, we can simply build a wraper:

public static string Colon {
 get { return GetString("colon"); }
}

In our English resource file the colon would simply be ':', while in the French one it would have a space ' :'.

Top Go to Table of Contents

Strongly-typed resources

The reason we use Hashtable instead of a NameValueCollection is because the ResourceManager class can be expanded to return strongly-typed objects. For example, you might have localized help content which is more than just a single value. It might have a title, an example and the help text. While exploring this is beyond the scope of this article (perhaps a part 3??) the capability exists.

Top Go to Table of Contents

Localized Controls

Our next goal is to make our life easier when developing a website by expanding existing server controls (literals, labels, buttons) to be localization-aware. We begin by creating a very simple interface our new controls will implement.

Top Go to Table of Contents

ILocalized

public interface ILocalized{
 string Key {get; set; }
 bool Colon {get; set; }
}

ILocalized defines a Key property which will be passed to our ResourceManager's GetString() method. In order to show how you can expand these classes to fit your own needs, I've also included a Colon property as a boolean, which will tell our controls if they shoudl append a colon at the end of their value.

Top Go to Table of Contents

LocalizedLiteral

public class LocalizedLiteral : Literal, ILocalized {
        #region fields and properties
        private string key;
        private bool colon = false;
   
        public bool Colon {
           get { return colon; }
           set { colon = value; }
        }
   
        public string Key {
           get { return key; }
           set { key = value; }
        }
        #endregion
   
   
        protected override void Render(HtmlTextWriter writer) {
           base.Text = ResourceManager.GetString(key);
           if (colon){
              base.Text += ResourceManager.Colon;
           }
           base.Render(writer);
        }
}
The first web control that we'll look at making localization-aware if the oft-used System.Web.UI.WebControls.Literal. First we make our class inherit from the Literal control and inherit our ILocalized interface [line: 1]. Next we implement the Key and Colon properties as defined in the ILocalized interface [line 3 - 14]. Finally we override the Render method of our base Literal class and use the ResourceManager's GetString() method and Colon property to fully localize our control [line: 19 - 22]. Don't forget to call the base classes Render() method afterwards to let it work its magic [line: 23].

Top Go to Table of Contents

Rinse, wash and repeater

You can copy and past the same code over and over again and simply change the name of the class and what it inherits from, for example, let's do a localized button:
public class Localized Button  : Button, ILocalized {
   
        #region Fields and Properties
        private string key;
        private bool colon = false;
   
        public string Key {
           get { return key; }
           set { key = value; }
        }
   
        public bool Colon {
           get { return colon; }
           set { colon = value; }
        }
   
        #endregion
   
        protected override void Render(HtmlTextWriter writer) {
           base.Text = ResourceManager.GetString(key);
           if (colon){
              base.Text += ResourceManager.Colon;
           }
           base.Render(writer);
        }
}
Notice that only the two bolded words have changed.

When desired, you can expand the functionality. For example, it isn't uncommon to have a LinkButton which pops up a JavaScript confirmation box when deleting something. We can easily achieve this by creating a 2nd key property:
using System.Web.UI;
using System.Web.UI.WebControls;
   
  namespace Localization {
     public class LocalizedLinkButton : LinkButton, ILocalized {
        #region Fields and Properties
        private string key;
        private bool colon;
        private string confirmKey;
   
        public string ConfirmKey {
           get { return confirmKey; }
           set { confirmKey = value; }
        }
        public string Key {
           get { return key; }
           set { key = value; }
        }
         public bool Colon {
           get { return colon; }
           set { colon = value; }
        }
        #endregion
   
        protected override void Render(HtmlTextWriter writer) {
           if(key != null){
              Text = ResourceManager.GetString(key);
              if (colon) {
                 Text += ResourceManager.Colon;
              }
           }
           if (confirmKey != null) {
              Attributes.Add("onClick", "return confirm('" + 
                        ResourceManager.GetString(confirmKey).Replace("'", 
                        "\'") + "');");
           }
   
           base.Render(writer);
        }
   
     }
}

Top Go to Table of Contents

Using Localized Controls

You use the localized controls like any other server control. First register the control on the page:

<%@ Register TagPrefix="Localized" Namespace="Localization" Assembly="Localization" %>

Then, without having to write and code, you can simply add the control either by drag and dropping it in the designer, or in the HTML mode by typing:

<Localized:LocalizedLiteral id="passwordLabel" 
 runat="server" Key="password" Colon="True" />
<Localized:LocalizedButton id="login"
 runat="server" colon="false" Key="login" />

Top Go to Table of Contents

Download

The best thing to do now is to play a bit with some code. I've again created a sample site (similar to the previous one), but this time using our new ResourceManager class and Localized controls. You might need to change the web.config's languageFilePath property to point to the right folder.

Top Go to Table of Contents

About DeveloperLand Administrator

Click here if you want to know more about .

Other articles that may interest you

  • Write a Word Add-In – Part 0
  • Write a Word Add-In – Part I
  • Lengthy Operations on Single Thread in .NET Application
  • Learning Draughts
  • Exceptions and Performance
  • Average Rating :

    Discussion Forums
    Got a programming related question? Hopefully someone has the answer... Want to help out other developers? Visit our discussion forums.

    Sponsored by:

    New Articles

  • Exceptions and Performance
    Almost every time exceptions are mentioned in mailing lists and newsgroups, people say they're really expensive.Let's examine that claim, shall we?

  • Creating multilingual websites - Part 1
    Extend the existing globalization capabilities of .NET to create flexible and powerful multilingual web sites. First, create a custom ResourceManager, and then create custom localized-capable server controls to easily deploy multilingual functionality.

  • Parameter passing in C#
    Many people have become fairly confused about how parameters are passed in C#, particularly with regard to reference types. This page should help to clear up some of that confusion

  • Most Popular Articles

  • LDAP, IIS and WinNT Directory Services
    This article explains how to use .NET Directory Services to retrieve and search directory objects, create new directory objects and edit or delete existing directory objects. Describes Active Directory Application Mode (ADAM) and how to use the IIS, WinNT and LDAP directory (ADSI) provider.

  • An in-depth look at WMI and instrumentation, Part II
    WMI stands for Windows Management Instrumentation and, as the name indicates, is about managing your IT infrastructure this article is the second part of a two-part series.

  • An in-depth look at WMI and instrumentation, Part I
    WMI stands for Windows Management Instrumentation and, as the name indicates, is about managing your IT infrastructure this article provides an in-depth look at WMI and MOM 2005

  • New Books

  • Murach's ASP.NET 2.0 Upgrader's Guide: VB Edition
    What’s new and how to use it! That’s what this book delivers if you’re a VB developer who’s interested in upgrading from ASP.NET 1.x to ASP.NET 2.0.

  • C# in easy steps
    Learn to program with Microsoft’s premier programming language. No previous programming knowledge is assumed. With numerous easy-to-follow examples, this title explains the essentials of object-oriented programming with C#.

  • Murach's ASP.NET web programming with VB.NET
    Murach's ASP.NET web programming with VB.NET by Doug Lowe and Anne Prince is a in depth training and reference book for ASP.NET programming using VB.NET. The book builds upon Murach's previous books and covers more advanced concepts for programming ASP.NET pages.

  • Got Code?

    if you have any article , source code , or anything else you'd like to share with this community that you think others might find useful, please submit it here and we will gladly make it available on this site. submit@developerland.com.
    Partners

    All articles are copyrighted by their individual authors unless otherwise specified , everything else Copyright ©2004-2006 DeveloperLand