Virtual Developer Workshop: Containerized Development with Docker
In this article I will demonstrate how to build an external resource file for your application's text and build additional files for multi-language support. After reading this article you will know how to build a resource file, compile the resource file using the resgen.exe utility and embed the resource file in a satellite assembly using the al.exe (assembly linker) utility. Finally, we will wrap up with a practical approach that developers can employ to simplify testing resource assemblies before throwing them over the wall to testing teams or customers.
Externalizing Text Content
It is easy to use the Visual Studio .NET IDE and the Properties window to add text content to your application's controls. Other content, like that displayed in a MessageBox, Console, or Web page, is often embedded amongst the lines of code or as constants. Externalizing this text content only requires a bit of additional work and prepares your application for globalization.
Creating a Resource Text File
RES_DATABASE_CONNECTION_ERROR = Unable to connect to database
Each of the name and value pairs must be on a single line in the text file.
Compiling a Resource File
After each of the content strings we are ready to compile the text file into a resources file using the resgen.exe (resource generator) utility. If you click on the Start|All Programs|Microsoft Visual Studio .NET 2003|Visual Studio .NET Tools|Visual Studio .NET 2003 Command Prompt then the necessary environment variables, including paths to the .NET SDK utilities, will be properly configured.
To compile the text file into a resources file we need to run the resource generator passing the name of the input text file and optionally the resources output file. For example, if our target assembly is myapp.exe then my input file should by myapp.txt and the output file should be myapp.resources, and the resource generator command line would be
resgen myapp.txt myapp.resources
After compiling the resource text file into a resources file we can add the resources file to the application. This resource will be embedded into the target assembly and will represent the default resource and consequently default language of the application.
Using External Resource Files
For our demo I created a text file containing some English text, the default for my laptop.
; This is my default resource file RES_WHAT=What time is it? RES_WHERE=Where is the train station?
I named the resource source file the same as my demo application's name, and compiled the default resource file with the following command line:
resgen InternationalizationDemo.txt InternationalizationDemo.resources
To use the external resource file we need to create an instance of the System.Resources.ResourceManager manager class, initializing the ResourceManager object with the resource type name and the Assembly containing the resource. A statement similar to the following works well.
Private Shared resourceManager As resourceManager = _ New resourceManager(GetType(Form1).Namespace + _ ".InternationalizationDemo", GetType(Form1).Assembly)
The member field is shared because only one instance is needed for all instances of the containing entity. The field is in the Form class, so we can get the namespace and the Form's containing assembly from the type object of the Form. GetType(Form1).Namespace + ".InternationalizationDemo" will build the resource type name correctly regardless of language the culture name, and GetType(Form1).Assembly will return the Form's assembly. For other applications substitute the Form1 parameter with the name of the class containing your ResourceManager field, and substitute the ".InternationalizationDemo" part with the file name part of your resource file.
After creating an instance of the ResourceManager we can request content anywhere we need to use the content. For example, if I want to display the What question then I could write resourceManager.GetString("RES_WHAT"), which would return the text "What time is it?" if we were running the application with the default culture settings of en-US.
Defining a Satellite Assembly
To define a satellite assembly, add a folder to your project containing the culture name for languages you'd like to support. For example, to support German we would add a folder named de-DE—case counts here. Next, add a file with the same prefix as your default file, a .txt suffix, and de-DE separated by a period in between. Placing this file in the proper folder would yield a Project Explorer that looks like the one shown in figure 1.
Figure 1: The demo application showing the default and German resource source and compiled resource files.
The figure shows the German language folder with the source text file and compiled resource file. We would add an addition folder for each language version we'd like our application to support. In addition, to the resources file we need to link our additional culture files into a DLL assembly—the default resource file is embedded into the application assembly—and we use the assembly linker to do this part.
We completed the first step by creating the culture specific sub-folder. The next step is to create the language version of the text file. If you just want to stub out the language text file append some additional text to a temporary file. Knowing enough German for our demo file I created the following German resource text file.
; This is the German version of the resource file RES_WHAT=Wie viel Uhr ist es? RES_WHERE=Wo ist der Bahnhof?
The next step is to compile the resource file using the resource generator. The command is almost identical to the previous example; all we need to do is substitute the file names to include the culture name in the file name.
resgen InternationalizationDemo.de-DE.txt InternationalizationDemo.de-DE.resources
Finally the following command shows how to use the assembly linker to embed the German resource into a satellite assembly that will be loaded when the culture name matches the German culture name, de-DE.
al /out:InternationalizationDemo.resources.dll /v:22.214.171.124 /c:de-DE /embed:InternationalizationDemo.de-DE.resources, InternationalizationDemo.InternationalizationDemo.de-DE.resources, Private
The last step is to test our satellite assemblies for each culture version we'd like to support.
Testing a Language Assembly
I'm a firm believer that developers should perform unit testing and get as many bugs out the code before it goes to testing. To that end we need to test our satellite assemblies. There are two good ways to test multi-language clients: one is to change the current culture information for the assembly and the second is to install software like VMWare or Virtual PC and set up a partition on your development machine with the language versions you'd like to test. For our purposes we'll change the culture information to see if the same code, without revision, properly loads the correct culture text when we change the current culture to de-DE.
An easy way to change the culture information is to import System.Threading and set the culture name to the names we'd like to test. In our simple Windows Forms application we can accomplish this by assigning a new CultureInfo object to Form Thread's CurrentUICulture property, as demonstrated:
Thread.CurrentThread.CurrentUICulture = _ New CultureInfo("de-DE")
Re-run the sample application. Now when the statement requesting the RES_WHAT resource string is requested the application should automatically load the satellite assembly and the correct language version of the string. To verify this you can open the Debug|Windows|Modules window and see that the resource satellite is automatically resolved when the ResourceManager.GetString method is called.
Figure 2: The correct satellite resource assembly is loaded, as shown, when the culture information has changed and resources are requested.
Using resource files is a good way to separate content from the behavior of an application. It is possible to implement the application and let the language lawyers figure out the content. A greater benefit is that by externalizing resources and creating satellite assemblies the same body of code will load resources needed for each of the languages you'd like to support. Additionally, the satellite assemblies can be added and shipped over time as you elect to support a greater variety of languages.In this article you learned that text values containing name and value pairs can be compiled and embedded into satellite assemblies using the resgen.exe and al.exe utilities. After the default resource and satellite assemblies are created, the correct content will be dynamically loaded by the ResourceManager class based on the culture information. When running in the current culture the default resources will be used, and without modifying your code, the correct satellite assembly—if you have created one for the specific culture—will be loaded when the application runs in the context of an alternate culture.
About the Author
Paul Kimmel is a software architect, writer, and columnist for codeguru.com. Look for his recent book Visual Basic .NET Power Coding from Addison-Wesley. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at firstname.lastname@example.org .
# # #