First hack at globalization and localization

So I've been working on a test of the .NET globalization/localization techniques for ASP.NET (v1.1) and have been having relatively mixed results.  The documentation and step-by-step examples have been a bit weak and the documentation for this area of the framework does not seem nearly as robust as other topics. This perception might be, however, just from my lack of true understanding of how this is all supposed to work and the associated terminology.

My task was to take a single html page, convert it to asp.net, and then allow it to automatically detect the user's preferred browser language “automagically” display the page (text, images, flash) in that user's langauge.  For the initial test we decided to support English, German, and Portuguese.  In each case we were supporting only the raw language rather than region specific contexts (i.e. [pt] rather than [pt-br] for General Portuguese rather than specifically Brazilian Portuguese).

The first thing I learned is that while the .NET framework has great support for globalizing / localizing winform apps, it is a bit lacking on (particuarly GUI-based) asp.net support.  While digging into this topic, I learned that there were a handful of ways in which you could accomplish this - the two that I looked at most closely were string resource files and satelitte assemblies.  Due to the relative simplicity of everything, I elected to use the string resource file method.

I figured I'd list pretty-much step-by-step what I did so that maybe the next person will have an eaisier time of this than I did:

  1. I needed to “get a handle“ on the user's preferred langauge.  It made the most sense to do this in the global.asax file.
    1. To set the file up for this task, I added “using“ statements for System.Threading, System.Globalization, and System.Resources to the global.asax.cs file
    2. I modified the Application_Start method to create a text-file-based resource manager. The first parameter indicates the name or key that is the first part of the resource files (i.e. a resources file for the English language would have to be called global1app.en.resources).  The second parameter indicates where on the disk the resources are located.  I chose this location arbitrarily.
      Application["RM"] = ResourceManager.CreateFileBasedResourceManager("global1app", Server.MapPath("./resources"), null);
    3. I modified the Session_Start method to determine the user's language preference and to store it
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(this.Request.UserLanguages[0]);
      Thread.CurrentThread.CurrentUICulture = new CultureInfo(this.Request.UserLanguages[0]);
  2. The next task was to prepare the page for globalization/localization
    1. The first step was to convert any and all text to asp.net label controls so that I could easily change their Text properties at runtime
    2. For the images, I changed them from html <image> tags to <asp:image/> tags wherever possible so that I could set the ImageUrl property at runtime.
    3. For the places where we embedded Flash objects in the page, I simply created private string variables in the class and change the html to use the value of the appropriate variable for the flash URL by using <%= VARNAME %> embedded in the “code“.  This may not be the most elegant solution as it seems to be a bit of tangled code, but it certain worked well.
    4. In the code (besides the defaults), I only had to add a using statement for System.Resources
    5. In the Page_Load method I added code similar to the following:

      if (this.Page.IsPostBack != true)
      {
           this.Label1.Text = ((ResourceManager)Application["RM"]).GetString("UI_label01"
      );
           this.Label2.Text = ((ResourceManager)Application["RM"]).GetString("UI_label02"
      );
      }

    6. I had a line for each of my labels, images, and embedded flash objects.  This part seemed a bit tedious and “klugy“ to me... I think that I could probably write a helper class that would enumerate all of the controls on a page, loop through them, and, based on the control type look to the RM for a value (or set of values) to set for the given language... I'll have to look more into that later.
  3. The next task was to actually prepare the resources for the various langauges. 
    1. The first task was to create string-resource files that contained a list of keys and values for the text to display on the web site as well as the URLs to the various language-specific images. There are a couple of ways to create string resource files.  I struggled with this for quite some time, so let me explain... 
      1. The first is to use a text editor such as notepad and create a list of name/value pairs one line at a time.  This was initially most attractive to me because I could simply take a text file and send it to my translator and it would not be very confusing to them at all.  The file would be in the following format making it very easy to work with:
        ; this is a comment line and localization for Spanish
        UI_label01 = hola
        UI_label02 = hasta luego
      2. The second way is to create a new assembly resource file within VS.NET and set the name value pairs there.  This is nearly as easy for me (the developer) to interact with, but not quite as intuiative for the translators... especially if they do not have a good XML editor handy... and assuming they know what XML is.  I eventually chose this option due to the fact that (I must have been doing something wrong) I could never get the language-specific accent characters to display on the resultant web site using the simple text file.  I'm assuming that it had something to do with the character encoding of the original text file... maybe if I had opened notepad and created a unicode file from the beginning things would have worked as they were supposed to.
      3. In any case, I created a resorce file for each language and named it in a consistent format (this is important).  Since my app was called global1app, I used the names global1app.en.resx, global1app.pt.resx and global1app.de.resx.
    2. I then used the resgen.exe tool (from the .NET framework SDK) and “compiled“ each of these files into .resources files (i.e. global1app.en.resources, etc.)
    3. Next I created a folder in the web root called “resources“ and copied my newly-created resources files to that location.
    4. Finally, I created unique images for each language (actually, someone else created them for me) and named them in the format of <image>.<culture>.gif.  So, an image that had been logo.gif now had additional copies called logo.en.gif, logo.pt.gif, and logo.de.gif.  These names are not crucial (the URLs are in the .resources file so it really could have been anything) but I chose this naming convention to keep things consistent.
  4. The last step is to test the asp.net page to see if everything works as it should.  There are a number of different ways that you can accomplish this - the following steps are the easiest that I found and assume you are using IE 6.
    1. Open internet explorer
    2. Choose Internet Options… from the Tools menu
    3. Click the button named Languages…
    4. on the dialog that opens up, click Add
    5. From the list, select Portuguese (Brazil) [pt-br] and click OK
    6. Click Add again, and select German (Germany) [de]
    7. The language displayed on the web page is based on the order in the list, so you can move the language you want to see to the top using the move up and move down buttons
    8. click ok and then ok which should put you at the browser window.
      Visit the web page above (or refresh the page if you are already there) and everything should change languages.

Finally... if everything worked properly, you should see the site in different languages based on the order of the languages you chose in IE.

Wrapup
I'm fairly pleased with how this all worked, although I'm still going to look into the notepad-style resource file as I know that this would be easier for my translation team.  I also like the fact that if I want to add support for a new language, or update the existing support I can simply copy a new *.resources file to the web server - without recompiling the app or touching the code in any way - and everything “just works“.  The final “cool“ thing is that if I was unable to procure some of the resources for a particular culture, I can simply omit them from the resources file for that language and when the framework doesn't find the requested resource string in the current culture's resource file, it will automatically look to the resource file for the default culture (configurable in the web.config file).

7 Comments

  • I have gone down the rabbit hole with localizing Asp.Net apps.



    Compared to winforms, it is a deep dark hole.



    You really need to build up a framework for doing it.



    At home, I have a set of controls that inherit most of the standard web controls and add localization functionality. I use complex control designers and they allow me to do the localization from the design view. I generate the source code with a script.



    At work, we have stuff that walks the control tree and localizes .Text properties as well as a few controls that are self localizing.



    Using file based resources removes the compilation dependancy but ... I would suggest looking at what is &quot;Resource&quot; and what is &quot;Content&quot;.

    A page title &quot;News Release&quot; can be a resource, but the actual title &quot;Today our company did abc&quot; is content. Both need to be available in each language, but they are conceptualy different.









  • &gt; In the Page_Load method I added code similar &gt; to the following:

    &gt;

    &gt; if (this.Page.IsPostBack != true)

    &gt; {

    &gt; this.Label1.Text = ((ResourceManager)Application[&quot;RM&quot;]).GetString(&quot;UI_label01&quot;);

    &gt; this.Label2.Text = ((ResourceManager)Application[&quot;RM&quot;]).GetString(&quot;UI_label02&quot;);

    &gt; }



    Of course doing it this way increases the size of your ViewState. If this is important to you, there are ways of avoiding it:

    - For simple fixed labels, just set EnableViewState to false for the label control, and remove the test for IsPostBack in the above code.

    - Setting EnableViewState to false won't work in all cases (e.g. DataGrid column headers): in such cases consider moving the localization code into OnInit, so the localized properties don't get saved in ViewState.



    Also Session_Start isn't the right place to set CurrentUICulture: you should use Application_BeginRequest.



    Finally if you want to be really bulletproof, consider handling the case where Request.UserLanguages[0] is null or an invalid culture (IE lets you delete it or set it to any user-defined string).

  • Application_BeginRequest is good but AcquireRequestState would be better. This way, you will have the handler created for HTTPRequest, in case you need it.



    For a large protal project, I didn't follow the Request.UserLanguages approach since it doesn't reflect the default language user wants from you all the time. Here in Turkey, some companies install English Windows while users prefer to browse page in Turkish if available.



    Here's the code in global.asax.vb:



    Sub Application_AcquireRequestState(ByVal sender As Object, ByVal e As EventArgs)

    Dim culturePref As String



    If Session(&quot;Culture&quot;) Is Nothing Then

    ' Request culture cookie

    If Not (Request.Cookies(&quot;CulturePref&quot;) Is Nothing) Then

    culturePref = Request.Cookies(&quot;CulturePref&quot;).Value

    End If



    ' Try to set culture. If fails, a default will be set.

    Try

    Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(culturePref)

    Catch

    Thread.CurrentThread.CurrentCulture = New CultureInfo(&quot;tr-TR&quot;)

    End Try

    'Thread.CurrentThread.CurrentCulture.NumberFormat.



    ' Set UI culture to current culture

    Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture



    ' Save culture info in session for later checks

    Session(&quot;Culture&quot;) = Thread.CurrentThread.CurrentCulture.Name



    ' Set Cookie

    Dim cookie As HttpCookie = New HttpCookie(&quot;CulturePref&quot;, Thread.CurrentThread.CurrentCulture.Name)

    cookie.Expires = DateTime.Now.AddYears(100)

    Response.Cookies.Add(cookie)

    Else

    ' Try to set culture saved in session, else set a default

    Try

    Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(CType(Session(&quot;Culture&quot;), String))

    Catch

    Thread.CurrentThread.CurrentCulture = New CultureInfo(&quot;tr-TR&quot;)

    End Try



    ' Set UI culture to current culture

    Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture

    End If

    End Sub



    It is important to note that, having something like this in the global.asax.vb would be a clever idea in a non-english speaking country/development environment/server environment, even if you don't use localization. That way, you will be sure that your SQL strings will be built (especially date related queries) the same on the server as you code it on your dev. setup and you will not have to worry about passing correct locale to .ToString.

  • I'd leave the resourcemanager alone with large websites ...

    I'd use the DB instead as store for all the words to translate ... especially when SQL caching in Yukon is out

  • Hi Guys!



    I am trying to localize a page with french, but my label controls in asp.net doesn't display accents. Can you help?



    Thanks in advance

  • Thanks for posting this. It definitely helped me understand this topic.

  • Yes there should realize the reader to RSS my feed to RSS commentary, quite simply

Comments have been disabled for this content.