ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


.NET Localization, Part 3: Localizing Text

by Satya Komatineni
10/21/2002

Introduction

Localization is a process by which you allow people of different cultures, languages and nationalities access your Web site. Although still a difficult process, all things considered, it is gradually becoming easier. Both the Java platform and the .NET platform have some nice features to aid localization. For instance, all strings, dates, and numbers are internally locale aware and when printed or validated will honor the localization setting.

The main topic of this article is localization of text. Text localization is typically the easier of the many efforts involved in the localization process. You simply use textual keys, and the system can load these keys from resource files using utility classes. If it is that simple, then, what is the need for this writeup? Writing a line of text in multiple languages is fairly trivial. But doing that for hundreds of pages in a language-dependent way requires a process, standards, and architecture. This is similar to scaling a dog house to a multi-story building.

What follows is a discussion of process recommendation and necessary tools for helping the localization of text under .NET.

Localizing Text

It is known that for localizing text, one would use the resource managers available under .NET. These resource managers use the main assembly and language based satellite assemblies to retrieve the string resources. A main assembly is basically your main executable file, if you are writing standalone executables. When you are writing a Web application or a Web service, this main assembly will be a DLL that is accessible by IIS. A satellite assembly is a DLL that contains only resource strings. You typically have one satellite assembly DLL for each language. Because these satellite assembly DLLs are separated from the main assembly (an .exe or DLL), it is easier to drop in multi-language support as you progress in your development process.

Related Reading

.NET Framework Essentials
By Thuan L. Thai, Hoang Lam

Based on documented literature, it is not hard to build text localization using a single resource file for the entire project. When the project has multiple modules and multiple people working on it, a single resource file will present the following difficulties:

  1. Too many entries in one file.
  2. Check-in/checkout contention for multiple developers.
  3. Difficulty in finding the text string keys that you want.

The solution is to allow multiple resource files: one for each module, or even one for each page. One would think that the resource files that get automatically generated by the IDE could be used for this purpose. But these autogenerated files are hidden, and there is no easy API to retrieve resources from multiple resource files. It is not hard to unhide these hidden per-page resource files. Even if you were able to put your resources in these resource files, these resource files may change as you change your GUI. This will make it difficult to ship these resource files to translators as they change often, not necessarily because of text strings, but because of other factors. I don't think this dependency is good; it may be better to just leave them hidden.

Whichever mechanism that we are going to adapt for multiple resource files has to be simple enough for the developer and the language translator to adapt. There is a beta tool from Microsoft called "Enterprise Localization Toolkit," based on SQL server, that will supposedly simplify this process. If you are considering using this tool, then it is well and good. But if for whatever reason, you want to roll out a less encompassing solution read on.

The following points are important to consider when you are designing a localization process:

  1. Developer access to localized keys.
  2. Developer utilities to retrieve localized keys.
  3. Developer access to localized resources for updating.
  4. Transcriber access to localized resources.
  5. Making satellite assemblies.
  6. Recommended conventions.

Developer Access to Localized Keys

You have just created a new Web page and about to enter a text string, and the localization chief looks over your shoulder and says, "Ha! My friend, you can't hard code the static text like that. You need to look up an equivalent key so that we can localize that text string." Now you have to invent a new key, or worse yet, look for an existing key, if it is already available. Here are these issues, itemized:

  1. Find a key for a text string.
  2. You don't want to misspell keys.
  3. You don't want to invent new keys when they are already available. For example, you don't want 10 different keys for the same localized "Cancel" button.
  4. You want the compiler to prompt you for these keys.
  5. You want keys for your own modules isolated from other keys.
  6. You want an easy way to substitute the string value of the key in your dialog boxes, Web pages, etc.

1. Create the Following Subdirectories Under Your Project Directory

To accomodate the above needs let us start with a directory structure for our resource-related files under the a fictitious project called "MyWebProject":


 
MyWebProject
    \resources\keys
    \resources\files

The keys subdirectory will have files to identify your keys for localized content. The files subdirectory will hold the actual resource files.

2. For Each Module in Your System, Create the Following Files



(Taking "Common" as an example) 

MyWebProject
    \resources\keys
            \module1Keys.cs
            \CommonKeys.cs
            \one .cs file for each module
    \resources\files
            \CommonResources.resx
            \module1Resources.resx
            \one .resx file for each module

CommonKeys.cs is a C# file containing project-level common definitions for the whole project, whereas module1keys.cs contains keys for your specific module. On the other hand, CommonResources.resx is an XML-based resource file that acts as a dictionary for the keys that are identified in the CommonKeys.cs key file. Easy enough so far.

3. Source Code for Your modulekeys.cs File

Let us take a look at the contents of the CommonKeys.cs file to fully understand the key definitions. Notice that the keys themselves are strings; it is important to define constants for these strings so that we don't make mistakes misspelling these keys. The provided structure for the CommonKeys.cs file will allow the IDE to prompt us for the available keys. What about root? This reserved key will define a naming context for our keys so that they are less likely to be duplicated. By convention, it can also point to the name of the module. By doing this, we can deduce the resource filename for a given resource key, without explicitly specifying the resource file from which the key originates. This property could be useful when it is time to retrieve the keys.



namespace SKLocalizationSample.resources.keys  
{  
    public class CommonKeys  
    {  
    public static string root = "Common";  
    public static string FILE = root + ".FILE";  
    public static string NEW  = root + ".NEW";  
    public static string SAVE = root + ".SAVE";  
    public CommonKeys(){}  
    }  
}  

4. How Can I See My Available Modules?

As new developers come on board and are given responsibility for existing modules or new modules, they can find out about resources for their respective modules by looking up the following directory:

     
   
    \project\resources\keys  

This will tell them the available modules for which keys are defined, which will tell them either to create a new file or use an existing file. When they define keys in these key files, they are also responsible for going over to the \resources\files\ directory and updating the corresponding .resx files with proper English values for their keys.

Developer Utilities to Retrieve Localized Keys

So far, we have shown how to create the keys and their values in a scalable manner. This section will cover how to retrieve these keys and place them on dialog boxes and Web pages. .NET provides a class called ResourceManager to assist with the retrieval of these keys with a well-defined, fall back process. A fall back process is a process by which .NET will look for a resource key in a language-dependent file first and if not found, it will look in the default resource file. It will also uses a hiearchical process to search the files; thereby, the localization process is gradual.

Let me present a couple of options to access these keys starting with the native .NET way and proceeding to demonstrate a few utilities for the same purpose.

Option 1: Direct Resource Manager

The first option is the option of directly using the resource manager classes available in .NET. In this option, you need to know the resource filename in which you are interested. In other words, you need to know the key of the resource and also the module in which the key is defined. As you can see, some of the effort we have put into our CommonKeys has already paid off. We were able to say CommonKeys.SAVE to identify the key in a discoverable, non-error-prone manner, but also able to specify the module name in a uniform anonymous manner: CommonKeys.root.

You can retrieve the keys by explicitly constructing the resource manager yourself:

     
   
Using System.resources;  
Using SKLocalizationSample.resources.keys;  
   
    ResourceManager rm = new ResourceManager(your-resource-filename,your-assembly);  
    rm.getString(CommonKeys.FILE);  
 

Option 2: Utility Function

The second option uses a utility called ResourceUtility that we are going to design in the following section. Let us consider here its usage, so that we can contrast it with Option 1 and see if it is worth the effort. One thing to notice is that we no longer need to instantiate resource managers, one for each module, ourselves. This is controlled by the static utility function. As we might embed static strings on a moment's notice in our programs, this one-line approach is very very welcome. We are still mentioning the module name and the key name, nevertheless. Let us see if we can improve on this one more step.

     
   
    String value = ResourceUtility.getString(CommonKeys.SAVE, CommonKeys.root);  

 

Option 3: A Utility Function Where the Module Name is Implied

We are able to just say the key name in the utility function. This is possible because we have used a convention where the key name includes the module name as a prefix. So inside of the utility function, we will infer the module name from the key, and accordingly retrieve the keys. This function may be slightly inefficient. Usually, this should be the least of your performance considerations. If it does, you can collapse the resource files into a single resource file at deployment time, or use another, similar method to optimize this out.

     
   
    String value = ResourceUtility.getString(CommonKeys.SAVE);  

 
Sample Code For the Above Function

Would it not be nice to cover how this function works? It is quite straightforward, so the complete code for this function is presented here. The code has enough comments to make it clear:

     
   
public class ResourceUtility  
{  
  public ResourceUtility(){}  
   
  // Define a hashtable to hold resource managers one for each module  
  static Hashtable resourceManagers = new Hashtable();  
   
  // Given a key and a  modulename return its value  
  public static string getString(string key, string modname)  
  {  
    // See if the reource manager  already exists  
    ResourceManager rm = (ResourceManager)resourceManagers[modname];  
   
    if (rm != null)  
    {  
      // ResourceManager not found,  
      // create the resource manager and add it to the hashtable  
      // the following ideally be run inside of synchronous block  
      rm = new ResourceManager("SKLocalizationSample.resources.files."  
                                + modName  
                                + "Resources",
                                Assembly.GetExecutingAssembly());  
   
      //  Notice how in the above line, the  name  of the passed in module  
      // is converted into a resource filename  
   
      resourceManagers.Add(modname,rm);  

    } 
 
    // when the resource manager is available just return the value for the key  
    return rm.GetString(key);  

  }  
        
  //***********************************************  
  //Option2, implying the module from the key  
  //************************************************  
  public static string getString(string key)  
  {  
    // get the module name from the string  
    char[] sep = {'.'};  
    string[] modKeyPair = key.Split(sep);  
    string mod = modKeyPair[0];  
    return getString(key,mod);  
  }  
}  

The only tricky part is where we are figuring out the resource file name from the module name.

For example, if the module name is:

     

    Commmon
    

Then the resource filename to be passed to the resource manager is:

     
   
    MyAppProject.resources.files.CommonResources.resources  

Developer Access to Localized Resources to Update Them

You have access to your module-specific resource file in the following directory:

     
   
\myproject\resources\files\your-module.resx  

You can update this file either through its XML or through an IDE-based editor.

Transcribers' Access to Localized Resources

Temporarily, if you want to localize any of your modules' resources, simply copy the existing resource file using the IDE into the same directory. Then rename it to the new language extension, and update the keys to reflect that language.

For ex:

     
   
\resources\files\CommonResources.resx  
\resources\files\CommonResources.en-gb.resx // British version of the file  

The Visual Studio IDE will automatically generate the satellite assemblies in the bin directory.

This process may not be practical for each of the files. In that case, we will collect all of the resource files and generate these language-dependent file outside of the framework and create satellite assemblies manually.

Refer to the article on the same site titled "Creating Satellite Assemblies" for converting these external resource files into satellite assemblies.

Recommended Conventions to Use These Utilities

Let us start with a module called MyMod and a key within that module called MYKEY:

1. Create a file called

     
   
\project\resources\keys\MyMod.cs  
    public static string root = "MyMod";  
    public static string MYKEY =  root + ".MYKEY";  

Notice the conventions used for root and the key MYKEY.

2. Create a resource file as follows (pay attention to the name of the file):

     
   
    \project\resources\files\MyModResources.res  

Key: MyMod.MYKEY

Value: Any language specific value

Note: Naming the key along with the module name should allow for better management of resources.

Satya Komatineni is the CTO at Indent, Inc. and the author of Aspire, an open source web development RAD tool for J2EE/XML.


Return to ONDotnet.com

Copyright © 2009 O'Reilly Media, Inc.