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.
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 |
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:
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:
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:
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.
(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.
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(){}
}
}
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.
|
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.
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);
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);
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
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.
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.
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.