Wednesday, July 18, 2012

Multilingual Website - Template



In this article, I would like to introduce you a website template which can be used as a base structure of a multilingual website. You can download the template, and use for free for your projects.
I tried to keep the source code of the project as simple as possible for the readers. Nevertheless, I explain some of the methods and classes I used in the template to clarify their usage for those who wish to use it in their projects.
First of all, we need to define which languages are supported by our website. This can be done with the LanguageEnum:

public enum LanguageEnum
{
    /// <summary>
    /// Represents the Dutch language.
    /// </summary>
    NL = 1,
 
    /// <summary>
    /// Represents the Engliah language.
    /// </summary>
    EN,
 
    /// <summary>
    /// Represents the Russian language.
    /// </summary>
    RU,
 
    /// <summary>
    /// Represents the Ukrainian language. 
    /// </summary>
    UK,
 
    /// <summary>
    /// Represents the Persian language.
    /// </summary>
    PR
}

To simplify the pages code behind, I added a PageBase class which is derived from the System.Web.UI.Page and all other pages are derived directly from this class.
The heart of the code is based on the Language property defined in the top level master page. This property need to be set every time a page of the website is loading. So, we set it in the Page_Load event handler of the master page.

protected void Page_Load(object sender, EventArgs e)
{
    this.Language = this.GetLanguage(Request.QueryString["LangId"]);
 
    if (this.IsPostBack)
        return;
}

There is no need to say that every pages of the web site should be defined under this top level master page. You can change this master page considering the principles or define nested master pages if necessary.
There are some LinkButtons defined as the language selector. When a language selector is clicked, redirecting to the same page in the selected language would be the most favorite type of switching for the readers. For, in the Click event handler of each language selector, the SwithLanguage method is called to redirect the site to the same page in the target language:

private void SwitchLanguage(string language)
{
    Uri uri = new Uri(Request.Url.ToString());
    string path = uri.AbsoluteUri;
 
    path = Regex.Replace(path, "/(" + CachedData.Instance.RegexLanguageSelector + ")(/*)""/" + language + "/", RegexOptions.IgnoreCase);
    path = Regex.Replace(path, "(\\?|&)LangId=(" + CachedData.Instance.RegexLanguageSelector + ")"string.Empty, RegexOptions.IgnoreCase);
 
    if (!Regex.IsMatch(path, "/(" + CachedData.Instance.RegexLanguageSelector + ")(/*)", RegexOptions.IgnoreCase))
    {
        path = path.Insert(uri.GetLeftPart(UriPartial.Authority).Length, "/" + language + "/");
        path = Regex.Replace(path, "/(" + CachedData.Instance.RegexLanguageSelector + ")//""/$1/", RegexOptions.IgnoreCase);
    }
 
    Response.Redirect(path);
}

To fast loading of the pages, the content of the page elements have been kept in a singleton CachedData class. Each time the web application is started or HttpRunTime.Cache is cleared, the CachedData will be reloaded (I describe it later).
In the CachedData class, there is a list of PreDefinedStringItem which allows you to keep the content in memory. The list is filled by calling the LoadPreDefinedStrings:

private void LoadPreDefinedStrings()
{
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Home", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Home", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Главная", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Головна", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "خانه", 
        LanguageEnum.PR));
 
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Contact", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Contact", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Контакты", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Контакти", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "تماس", 
        LanguageEnum.PR));
 
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 DAGEN PER WEEK 24 UUR BEREIKBAAR", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 DAYS PER WEEK 24 HOUR PER DAY", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 ДНЕЙ В НЕДЕЛЮ, 24 ЧАСА В ДЕНЬ", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 ДНІВ НА ТИЖДЕНЬ, 24 ГОДИНИ НА ДЕНЬ", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 روز در هفته، 24 ساعت در روز", 
        LanguageEnum.PR));
 
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Plaats hier hoofdinhoud.", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Place main content here.", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Поместите основному содержанию здесь.", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Помістіть основному змісту тут.", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "مطالب اصلی را اینجا قرار دهید.", 
        LanguageEnum.PR));
 
    this.resetPreDefinedStrings = false;
}

In the template, I filled the list of PreDefinedStringItem directly from the code, but you can read the content from your database and fill the list. It is up to you.
In the Global.asax.cs, we add a HttpRunTime.Cache task to force the application to reload the CachedData items every 600 seconds (10 minutes). Depend on the web server you use and its parameters, this time may be varied.

private void AddTask(string name, int seconds)
{
    onCacheRemove = new CacheItemRemovedCallback(this.CacheItemRemoved);
    HttpRuntime.Cache.Insert(
        name, 
        seconds, 
        null,
        DateTime.Now.AddSeconds(seconds), 
        Cache.NoSlidingExpiration,
        CacheItemPriority.NotRemovable, 
        onCacheRemove);
}
 
private void CacheItemRemovedTask(object arguments)
{
    CachedData.Instance.ReloadCachedItems();
}
 
private void CacheItemRemoved(string name, object sender, CacheItemRemovedReason reason)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(this.CacheItemRemovedTask));
 
    // do stuff here if it matches our taskname, like WebRequest
    // re-add our task so it recurs
    this.AddTask(name, Convert.ToInt32(sender));
}

The CacheItemRemovedTask calls the CachedData.ReloadCachedItems which forces the CachedData class to reload the pre-defined strings the first time the content property is accessed then after.
The application is checking for the Request.Path and changes and rewrite it to a format, so the user can open a page by adding the language code after the root URL path. For example, http://localhost/en/default.aspx will open the home page in English language. This process is done in the Application_BeginRequest event handler:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    string path = Request.Path;
    string defaultLanguage = "NL";
 
    this.SetRootURL();
 
    if (!Regex.IsMatch(
        path, "^/(" + CachedData.Instance.RegexLanguageSelector + ")", 
        RegexOptions.IgnoreCase) &&
        Regex.IsMatch(
        path, @"^(/(.+).aspx)", 
        RegexOptions.IgnoreCase))
    {
        path = "/" + defaultLanguage + path; 
        Response.Redirect(path);
    }
    else
    {
        if (Regex.IsMatch(
            path, "^/(" + CachedData.Instance.RegexLanguageSelector + ")/(.+).aspx", 
            RegexOptions.IgnoreCase))
        {
            path = Regex.Replace(path, 
                "^/(" + CachedData.Instance.RegexLanguageSelector + ")/(.+).aspx""/$2.aspx?LangId=$1", 
                RegexOptions.IgnoreCase);
            Context.RewritePath(path);
        }
        else if (Regex.IsMatch(
            path, "^/(" + CachedData.Instance.RegexLanguageSelector + ")(/*)\\z", 
            RegexOptions.IgnoreCase))
        {
            path = Regex.Replace(path, "^/(" + 
                CachedData.Instance.RegexLanguageSelector + ")(/*)\\z""/default.aspx?LangId=$1", 
                RegexOptions.IgnoreCase);
            Context.RewritePath(path);
        }
    }
}

I put the styles of the pages elements in Common.css file. There may be some additional styles which are exclusively available for a specific language. These styles can be added to the files which their names are <LanguageCode>.css. For example, if you have a right to left language available in your site, simple add PR.css to your site. This file will be added to the master page aspx content by the LanguageClass property:

public string LanguageClass
{
    get { return "<link href=\"/css/" + Enum.GetName(this.Language.GetType(), this.Language) + ".css\" rel=\"stylesheet\" />"; }
}

for every element of the pages content, you need to add a property to the master page code behind and use the CachedData.Instance.GetPreDefinedStringValue method to get the content of the element from the CachedData. The value should be loaded to the list of PredefinedStrings of the CachedData using the LoadPreDefinedStrings method which I explained it before.
As an example, if you want to have the text of the home navigation button in different languages, Add the HomeNavigation property to the master page code behind:

public string HomeNavigation
{
    get 
    { 
        return CachedData.Instance.GetPreDefinedStringValue("HomeNavigation"this.Language); 
    }
}

and to load it in the master page, simply call it by <%=HomeNavigation%> in the master page aspx file. This way, you can load the contents which are placed in the master page in different languages.

<a href="Default.aspx"><%=HomeNavigation%></a>

The same way, you can load the contents which are defined in other pages of the site. For example, to load the main content of the home page, add HomeContent property in the PageBase (or other pages) code behind:

public string HomeContent
{
    get 
    { 
        return CachedData.Instance.GetPreDefinedStringValue(
            "HomeContent", 
            ((SiteMasterPage)this.Master).Language); 
    }
}

and call it from the default page aspx file by <%=HomeContent%>.

<span class="mainContent"><%=HomeContent%></span>

When the application is loaded, it may be dropped by the web server after a period of idle time. To make the application available all the time, in the Application_End event handler, I force the application to open one of its pages, so the web server is forced to reload the application. Of course, depend on the web server and other parameters, this may not be worked all the time:

protected void Application_End(object sender, EventArgs e)
{
    this.LoadRootUrl();
}

I hope this would be helpful. Any comments would be highly appreciates.
Enjoy programming ;)