Home | Site map   
  Home Products Downloads Support Contacts
  ASPRunner.NET:  Overview | Try now | Buy now | Tutorial |  Screenshots | Live demo | FAQ | Articles | Forum
 
  Back to list of Articles

Basics of creating server controls. Part II. Events.
Author: Ozornin Artiom aspnetman@aspnetmania.com

1. Introduction.

This article speaks about different ways of developing multilingual support; emphasis is given on method using resource files and satellite assemblies.

2. Possible solutions and terms definition.
  2.1. Localization and databases.

Well, what choices do we have? Since any modern dynamic web application is just unthinkable without connecting to a database, the most evident solution probably - to store multilingual presentation of one and the same information in a database. Indeed, where else can we store arrays of information, substantial in volume and needed to be changed from time to time, such as articles, chats info, adverts, different news and analytical reviews. It's logical all the more, as for entity its localization versions in several languages we can consider as different kinds of this entity's dynamics. Let's call data described above application's target dynamic content (it's clear that it is not the only type of changeable information, but if we estimate the speed of changing info displayed to a user, this info type is subject to the most frequent changes). How will the structure of storing entities in the database change in this case? Let's consider the following example. On the figure 2.1 the table is given implementing Document entity which intent is to store static html and text documents that need localization.

There are 2 ways: first - change primary table key and make it composite - comprising 2 fields: DocumentId and LanguageId:

Worth mentioning, that not only the structure of storing data has been changed, types of the fields storing localized info has been changed also - Title and Body fields store Unicode characters. So, with such data structure, we can store each version of document localization in the separate table row. But what should we do, if data structure of the document demands changes, for example, we will need to add Author, Creation Date, Expire Date attributes to the Document entity. In this case structure described above turns into the storage of large number of excessive data, which generates number of difficulties with their synchronization and changing, apart from everything else. Therefore, if there is no absolute guarantee, that given structure of the entity will not be changed, or there is no need in certain de-normalization of data, it is better to take another way of dynamic content localization - to extract localization into a separate entity.

The scheme is quite clear, and probably, the only point that needs explanation - Id field in tblDocumentLocalizations table. This autoincrement field is a unique table index. The Id field is optional, I use it for table data editing convenience. Apart from target dynamic content any application contains various additional info that needs localization - controls, containing text info, some static parts of text and similar text info segments, which we can name additional dynamic content. "Static parts of text" and "additional dynamic content"? Contradiction, at the first glance, but recall, we are talking about developing multilingual support and in the problem definition itself there is a deny of any text info statics - cause any static text that has several presentations in different languages can not be considered as static. While creating web applications of average and high complexity the total number of elements implementing additional dynamic content can be quite big, and sub-tasks of development, support and management of localization of the content of such type with the help of database needs considerable efforts and can result in substantial additional load on database server. Development of localized display of additional dynamic content directly in application code is also quite routine and non flexible solution. So what should we do? Quite simple - .NET Framework developers have provided tools to resolve this problem.

  2.2. Satellite assemblies, resource files and System.Resources.

Satellite assemblies are used in the .NET Framework to store compiled localized application resources (i.e. there is no any program code in these assemblies): strings, pictures, video and similar binary information; as for resources, there is special System.Resources namespace to work with them, and of all its members the following classes are the most interesting for developers:
1. ResourceManager - is used to access current culture resources from satellite assembly.
2. ResourceSet - is used to access all resources of one specific culture, this class instance scans all resources of specific culture and stores results in HashTable.

Let's sort out how is it possible to use satellite assemblies in your applications and access data from them (I'll tell you upfront, that will use Visual Studio.NET for this purpose, those who like "handmade" products, can read al.exe and resgen.exe utilities descriptions in MSDN and Creating Satellite Assemblies topic there as well). So, let's create new project LocalizingDemo, rename WebForm1.aspx into Default.aspx (and corresponding class into Default), add Label control to this form, name it headerLabel, and then let's see what is displayed on Solution Explorer panel (click Show All Files and Refresh buttons on its toolbar).

As you can see, nice:) Visual Studio have already created default resource file in resx format. ResX - is a resource file format that allows to store defined entities, such as strings and objects as XML-tags, and therefore it is XML file of particular structure. To make sure, let's open it and choose XML design mode.
As you see ResX file consists of two parts - xsd scheme, defining data structure in the file, and the data itself, consisted of set of resource file headers () and data set (), consisted of pairs name/value, structured according to the following xsd-scheme:

         
	<data name = "Name">
         
            <value> Value </value>

         </data>

Resource file Default.aspx.resx is a neutral resource file that stores resources that depend on localization. Let's add a resource file with a given culture and "English-USA" region to our project (File -> Add New Item -> Assembly Resource File) and call it Default.en-US.resx (property Build Action should be set to Embedded Resource), then let's compile application. We will notice that the structure of our application's bin directory has been changed.

En-US catalog appeared, containing LocalizingDemo.resources.dll file- it is that satellite assembly, which was compiled together with major assembly of the application, and which will include all resource files, belonging to this culture/region. En-US combination which was used to name resource file in Default class and which was used by VisualStudio to form bin directory sub catalog - this is nothing else but culture name according RFC 1766 standard: - , where - is two-letter combination as per ISO 639-1 Standard (international standard for representation of names of language), consisted of lowercase letters and - two-letter combination as per ISO 3166 (international standard for representation of names of countries/regions ) consisted of uppercase letters. (I would like to stress once again that Studio substantially simplifies creating, deployment (in appropriate directory) and compiling satellite assemblies. If you work with command line you would need to do several quite wearisome operations in order to reach described result. So, resource file we added corresponds to English-USA culture -region. Into this resource file let's add text value to display headerLabel element.

Let's add the following code to Page_Load event:

private void Page_Load(object sender, System.EventArgs e)
{
   //    Set culture "en-US" for current user request thread 
   Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");

   //    Set culture "en-US" that is used by ResourceManager  to search 
   // localized resources
   Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("en-US");

   //    Set encoding of response to user request
   this.Response.ContentEncoding = Encoding.GetEncoding(Thread.CurrentThread.CurrentCulture.TextInfo.ANSICodePage);         

   //    Create instance of ResourceManager class for current class and current assembly 
   ResourceManager _resourceManager = new ResourceManager(this.GetType().BaseType.FullName,
                                                            this.GetType().BaseType.Assembly);

//Get localized value from localized resources assembly   this.headerLabel.Text = _resourceManager.GetString("headerLabel");
}

Compile, run, and get the following result approximately:

Event handler's code was commented quite carefully, we will not speak about it any longer, but the only thing I would like to draw your attention to is that overriden version of ResourceManager.GetString method is used, which accepts CultureInfo instance as second parameter, i.e. we could just write this.headerLabel.Text = _resourceManager.GetString("headerLabel", �ultureInfo.CreateSpecificCulture("en-US")) and not override Thread.CurrentThread.CurrentCulture with Thread.CurrentThread.CurrentUICulture, but since usually homogeneous localization of all page elements/user control is needed, implementation of this version of method's reloading is not optimal (at least, imho :) ). However, nobody needs localization for one language/culture, that is why let's add resource file Default.ru-RU.resx to our project, to store Russian localization resources in it , and add render parameter headerLabel.

Then let's add two HyperLink controls to the page (I'll give code of the whole page, as it's quite short).
Let's adjust Page_Load event code to the following:

private void Page_Load(object sender, System.EventArgs e)
{

   if (this.Request.QueryString["Culture"] == null)
   {
      return;
   }
   
   string _sCulture = this.Request.QueryString["Culture"];
   
   //    Set culture for current user request thread 
   Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(_sCulture);
   
   //    Set culture that is used by ResourceManager  to search 
   // localized resources 
   Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(_sCulture);

   //    Set encoding of response to user request 
   this.Response.ContentEncoding = Encoding.GetEncoding(Thread.CurrentThread.CurrentCulture.TextInfo.ANSICodePage);         

   //    Create instance of ResourceManager class for current class and current assembly 
   ResourceManager _resourceManager = new ResourceManager(this.GetType().BaseType.FullName,
                                                            this.GetType().BaseType.Assembly);

   //    Get localized value from localized resources assembly   
   this.headerLabel.Text = _resourceManager.GetString("headerLabel");
}

Now, depending this or that link has been clicked, we get string value from this or that localized assembly to render headerLabel element:

So, we got quite operable application demonstrating localization mechanism in .NET, and with this we could have finished this section, but I want to focus on one more aspect of ResourceManager class. Let's add to our project resource file Default.en.resx and the same pair name/value that we have added to Default.en-US.resx for headerLabel control, and let's delete this pair from Default.en-US.resx (or delete this file at all). While re-compiling and starting this application we will find out its behavior was not changed. Let's go further on, and add the same pair name/value to Default.aspx.resx file and delete it from Default.en.resx file (or delete the file itself). Once again compile and start - application is still working without any changes. Explanation is in ResourceManager class speciality - when it can not find necessary value in resource assembly for specific culture/region (in our case en-US), it tries to find these data in resource assembly for culture without specifying region (en), and then in neutral resources assembly. This mechanism is quite logical, and allows to distribute rationally all info that is localized in such a way that, for example, for cultures/regions en-US (USA) and en-GB (Great Britain) common info is stored in assembly for bin /en/AssemblyName.resources.dll culture, and different info in assemblies bin/en-US/AssemblyName.resources.dll and bin/en-GB/AssemblyName.resources.dll correspondingly, resources that do not depend on localization can be stored in neutral assembly.

3. Development of (one-page) multilingual application.

I'll tell you straight off, that I am going to speak about localization at a language level, not at a region level, cause I never had a chance to resolve that kind of tasks, but the principles of creating an application that is localized at the region level are identical and solution described here can be easily extended up to the necessary level of localization. To start with, I would like to show diagram of data storing structure for one-page portal

Structure and purpose of tables tblPages, tblModules, tblContainers and tblPageConfigurations are similar to those that I described in my article "Creating a simple one-page portal", tblModuleDefinitions and tblDocuments - is and influence of IBuySpy Portal (I finally managed to do it :) ). TblDocuments table and its descendant tblDocumentLocalizations are the only of these tables, not related to the structure info of the portal itself and are shown on the diagram just as the representatives of information part of site, in real life here can be announcement tables, forum tables etc. As we can see from the diagram, each entity that has info to be rendered, has additional localization table, and all localization tables have relationships (:) !) many-to-one with tblLanguages table. The description of main fields of tblLanguages table is given on the figure 2.3., the only thing I want to add is that Alias field serves for storing language name as per ISO 639-1 standard. So, the first thing we want is to get information on all languages supported by the application upon its start. Therefore, we write the following in Global.asax.cs:

protected void Application_Start(Object sender, EventArgs e)
{
   this.Application.Add("AvailableLanguages", this.localizationsData.GetLanguages());
}

LocalizationsData.GetLanguages() - it is a business component method that returns result of the following protected procedure as a HashTable:

CREATE PROCEDURE dbo.spSelectLanguages
AS
BEGIN

   SELECT Alias         AS LanguageAlias,
          EnglishName   AS LanguageName
      FROM tblLanguages
         
END

If the application has relatively high speed of dynamics, probably it is reasonable to run this operation not only while starting the application but from time to tile while running application. Then, after each user request we need to define, information in what language he wants to get:

protected void Application_BeginRequest(Object sender, EventArgs e)
{
   string _sUserLanguage;
   bool   _bLanguageCookieExist = false;
   
   //    Check if language cookie exists 
   if (this.Request.Cookies["Language"] == null)
   {
      Hashtable _languageHashtable = (Hashtable)this.Application["AvailableLanguages"];
      
      //    Check if default language of client's browser is 
      // among the languages supported by your site 
      if (_languageHashtable[this.Request.UserLanguages[0].Substring(0,2)] != null)
      {
         _sUserLanguage = this.Request.UserLanguages[0].Substring(0,2);                     
      }
      else
      {
         //    If default language is not supported, we choose English locale 
         _sUserLanguage = "en";
      }
   }
   else
   {
      _sUserLanguage = this.Request.Cookies["Language"].Value;
      _bLanguageCookieExist = true;
   }
   
   //    Set current culture for current user request thread 
   Thread.CurrentThread
         .CurrentCulture   = CultureInfo.CreateSpecificCulture(_sUserLanguage);

   //    Set culture that is used by ResourceManager  to search 
   // localized resources
   Thread.CurrentThread
         .CurrentUICulture = CultureInfo.CreateSpecificCulture(_sUserLanguage);

   //    Set encoding of response to user request 
   this.Response.ContentEncoding = 
      Encoding.GetEncoding(Thread.CurrentThread.CurrentCulture.TextInfo.ANSICodePage);

   
   if (!_bLanguageCookieExist)
   {	
      //    Set value for cookie storing language selected by the user 
      this.Response.Cookies["Language"].Value = 
         Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;
      
      this.Response.Cookies["Language"].Expires = DateTime.Now.AddYears(5);
   }
}

Everything is quite clear here, the only thing I would like to draw your attention to is that you better don't use such expressions as Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(this.Request.UserLanguages[0]); - cause some unconcerned Opera can send something like "en;q=0,1", instead of this.Request.UserLanguages[0], and this for sure will rise an exception, because parameter sent to CultureInfo.CreateSpecificCulture should be RFC 1766 string. With the code written above in Global.asax.cs, and any control that sets cookie for language select, we will get system for centralized management and support of localization of Multilingual application. So, at any moment of application run-time we can get target dynamic content from the database server in accordance with user's current language setting and display it correctly. As per additional dynamic content stored in satellite assemblies, I think you'll agree, that it is quite tiresome to write similar code for each control at each page or each user control that is why I use slightly changed class, which implements localization of the whole page or user control:

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Globalization;
using System.Threading;
using System.Resources;
using System.Collections.Specialized;


namespace Volortho.Configuration
{
	/// 
	///   Static methods encapsulation for localization of page 
	/// controls / user control
	/// 
	public class WebUILocalizer
	{
      #region Private static methods 
      
      /// 
      /// Loading key/value collection from resource file of element that is localized 
      /// 
      ///  
      ///   Localization target object, usually derives 
      /// from System.Web.UI.Page or System.Web.UI.UserControl
      /// 
      ///  
      ///   Set of some additional parameters, stored in 
      /// resource file 
      /// 
      /// 
      /// Key/value collection from resource file of element that is localized 
      ///
      private static NameValueCollection LoadLabels(Control mainControl, string[] sExceptions)
      {
         string _sBaseName = mainControl.GetType().BaseType.FullName;
         string _sTwoLetterISOLanguageName = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
         
         NameValueCollection _labelsCollection = 
            (NameValueCollection)HttpContext.Current.Cache[_sBaseName + "_" + _sTwoLetterISOLanguageName];

         if (_labelsCollection == null)  
         {
         
         ResourceManager _resourceManager = 
            new ResourceManager(_sBaseName,typeof(WebUILocalizer).Assembly); 
         
         _labelsCollection = new NameValueCollection(); 
         
         FillResourcesCollection(mainControl,  _resourceManager, _labelsCollection); 
         
         if(sExceptions != null && sExceptions.Length >  0)
         {
            foreach(string ex in sExceptions)
            {
               _labelsCollection.Add(ex, _resourceManager.GetString(ex));
            }
         }
            
         HttpContext.Current.Cache.Insert(_sBaseName + "_" + _sTwoLetterISOLanguageName,
            _labelsCollection, null, DateTime.MaxValue, TimeSpan.FromMinutes(2));
         }

         return _labelsCollection;
      }
     
      /// 
      /// Load key/value collection from resource file of element that is localized 
      /// 
      ///  
      ///   Localization target object, usually derives
      /// from System.Web.UI.Page or System.Web.UI.UserControl
      /// 
      ///  
      /// Current localization resource manager  
      /// 
      /// 
      /// Key/value collection from resource file of element that is localized 

      /// 
      private static void FillResourcesCollection
		(Control control, ResourceManager resourceManager, NameValueCollection labelsCollection)
      {
         foreach(Control _childControls in control.Controls)
         {
            FillResourcesCollection(_childControls, resourceManager, labelsCollection);
         }

         if (control is WebControl)
         {
            string _sKey = control.ID;
            string _sVal = resourceManager.GetString(_sKey);
            
            if(_sVal != null)
            {
               if(_sVal != "array")
               {
                  labelsCollection.Add(_sKey, _sVal);
               }
               else
               {
                  labelsCollection.Add(_sKey, _sVal);
                  int _iArrayItemCounter = 1;
                  
                  while(resourceManager.GetString(_sKey + "_" + _iArrayItemCounter.ToString()) != null)
                  {
                     labelsCollection.Add(_sKey + "_" + _iArrayItemCounter.ToString(), 
                                          resourceManager.GetString(_sKey + "_" + _iArrayItemCounter.ToString()));
                     _iArrayItemCounter++;
                  }
               }
            }
         }
      }

      /// 
      ///   Set localized property values taken from  labelsCollection for 
      /// appropriate controls 
      /// 
      ///  
      ///   Target object of localization
      /// 
      /// Key/value collection from resource file of element that is localized 
      /// 
      private static void SetLabels(Control control, NameValueCollection labelsCollection)
      {
         foreach(Control _childControl in control.Controls)
         {
            SetLabels(_childControl, labelsCollection);
         }
         
         if(control is WebControl)
         {
            string _sKey = control.ID;
            string _sVal = labelsCollection[_sKey];
            
            if(_sVal != null)
               switch(control.GetType().Name)
               {
                  case "Label":
                     ((Label)control).Text = _sVal;
                     break;

                  case "CheckBox":
                     ((CheckBox)control).Text = _sVal;
                     break;

                  case "TextBox":
                     ((TextBox)control).Text = _sVal;
                     break;

                  case "LinkButton":
                     ((LinkButton)control).Text = _sVal;
                     break;

                  case "Button":
                     ((Button)control).Text = _sVal;
                     break;

                  case "HyperLink":
                     ((HyperLink)control).Text = _sVal;
                     break;

                  case "DropDownList":

                  case "RadioButtonList":

                  case "CheckBoxList":

                  case "ListBox":
                     ListControl _listControl = (ListControl)control;
                     int _iItemCounter = 0;

                     foreach(ListItem _listItem in _listControl.Items)
                     {
                        _listItem.Text = labelsCollection[_sKey + "_" + (_iItemCounter + 1).ToString()];
                        _iItemCounter++;
                     }
                     break;

                  case "RequiredFieldValidator":
                  case "RegularExpressionValidator":
                  case "CusomValidator":
                  case "CompareValidator":
                     ((BaseValidator)control).ErrorMessage = _sVal;
                     break;
               }
         }
      }

      #endregion Private static methods 
      
      
      #region Public static methods 

      /// 
      ///   Localizes control as per the current culture 
      /// 
      ///  
      ///   Localization target object, usually derives
      /// from System.Web.UI.Page or System.Web.UI.UserControl
      /// 
      ///  
      ///   Set of some additional parameters, stored in 
      /// resource file
      public static void Localize(Control mainControl, string[] sExceptions)
      {
         if (Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName == "en")
         {
            return;
         }

         WebUILocalizer.SetLabels(mainControl, 
                                  WebUILocalizer.LoadLabels(mainControl, sExceptions));
      }

      /// 
      ///   Localizes control as per the current culture
      /// 
      ///  
      ///   Localization target object, usually derives
      /// from System.Web.UI.Page or System.Web.UI.UserControl
      /// 
      public static void Localize(Control mainControl)
      {
         if (Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName == "en")
         {
            return;
         }
         
         WebUILocalizer.SetLabels(mainControl, 
                                  WebUILocalizer.LoadLabels(mainControl, null));
      }

      #endregion Public static methods       
   }
}

To use this code it is enough to add it into the class of the object that is localized:

private void Page_Load(object sender, System.EventArgs e)
{
   if (!this.IsPostBack)      
   {
      WebUILocalizer.Localize(this);            
   }
}

Back to top

 
 

Home | Products | Downloads | Support | Contacts

  © 1999 - 2005 XLineSoft. All rights reserved. All comments send to webmaster@xlinesoft.com