.NET Internationalization Utilities

John Robbins

Code download available at:Bugslayer0404.exe(244 KB)


.NET Resource Handling
Building Real-world Internationalization DLLs
Windows Forms Internationalization
Internationalization Helper Programs

As you saw in last month's column, .NET internationalization support is excellent and allows you to move your application to a world audience quite easily. Before you jump into this month's discussion, you may want to go back and read the March column.

This month, I'll discuss numerous utilities to make real-world .NET internationalization easier to achieve in a team development environment. The first is a set of batch files that can ease the pain of building your internationalized resources. The second utility is used to translate just the string resources so you don't have to distribute your entire source code tree. The final utility, Pseudoizer, is a great testing tool that allows you to do internationalized resource testing even if you are a monolingual American. Before I jump into the tools, I need to discuss .NET resource handling because it sometimes confuses developers the first time they see it.

.NET Resource Handling

Back in the days of Win32®, you were on your own to devise a resource-handling scheme that automatically loaded internationalized resources. With the .NET Framework, Microsoft built a very nice scheme, called hub-and-spoke, directly into the Framework itself. The hub represents the parent assembly that contains the code, non-localizable resources, and the neutral or default language localizable resources. There is only one hub. The spokes (there's one for each language) are satellite assemblies that have no code and contain only the localized resources for a language.

Describing the exact steps the Framework takes to load the appropriate satellite assembly could take an entire column on its own. Fortunately, there is a good explanation of what happens when you request a satellite DLL in the "Resource Fallback Process" section in the MSDN® Library. You should also read the section on "Versioning Support for Satellite Assemblies". I found it extremely helpful to draw out the steps on paper as I understood them for the Resource Fallback Process section to ensure I could see how the Framework accomplished the loading. The beauty of the resource fallback process scheme is that the Framework will go to heroic efforts to find each resource you request. This means that if you forget to translate a particular string, the user will see at least the default language of the hub assembly.

The documentation does not make clear the physical disk layout for loading satellite assemblies from directories below the parent assembly. Figure 1 shows the directory structure for the InternationalizationDemo program. The main assembly directory contains InternationalizationDemo.exe with code and default resources for the application. Under the main directory is a directory for each supported locale.

Figure 1 Directory Layout for Resource Loading

Figure 1** Directory Layout for Resource Loading **

The one interesting directory is ES, which contains the Spanish language resources that are not culture specific. If the locale is Spanish as spoken in Mexico (es-MX), the ResourceManager will look in the .\es-MX directory for the resources from the satellite DLL. If .NET does not find a requested resource, it looks in the satellite DLL in the .\es directory, which contains the Spanish language, culture-independent resources. Falling back to the base language in this manner allows you to reuse common language resources. As you would expect, if the resource is not found in the satellite DLL in .\es, the system takes a look in the main assembly, InternationalizationDemo.exe, for the resource as that is considered to be the default resource.

Before I get too far into the discussion of resources, I want to talk about the different types of files you can put your raw resources into and how to convert them into resource files. I'll also add a few words of wisdom. You can use two file formats for your resources. The first is the text file in which all the strings in the file are in the form of <value>=<string> with one per line. The format I prefer is the XML-based ResX file.

You've probably always seen ResX files as part of Windows® Forms applications, but there's nothing stopping you from using ResX files for any of your resource needs. What makes ResX files so much better than text files is that they let you store any type of resource—strings, graphics, or any custom type you want. Since they are XML files, you can easily edit them with the built-in XML editor in Visual Studio® .NET.

Using either format, it's a two-step process to turn the source files into an assembly. RESGEN.EXE, the resource generator, turns the source into a .RESOURCES file. The second step is to embed the .RESOURCES file into an assembly with AL.EXE, the Microsoft® intermediate language (MSIL) assembler. The good news is that the work of embedding your resources is automatically handled by Visual Studio .NET for your main assembly. I'll talk about building those specific cultural resources in a moment.

If you've read the documentation, you might be wondering why I haven't talked about using .RESOURCES files directly by your applications. While it's possible, you're strongly encouraged not to do so. The main problem is that the .RESOURCES file is just a binary file, so there is no version information associated with the file, and you can't strongly sign the file. .RESOURCES files will cause file locking issues with ASP.NET applications and can make XCOPY deployment more difficult.

Adding a ResX file to your projects is as simple as right-clicking on the project, selecting Add New Item from the context menu, and selecting Assembly Resource File as the item type. The name of the file is important, as that will form part of the name you'll need to pass to the ResourceManager class to load resources. Once you add the ResX file, you'll see that its Build Action is set to Embedded Resource. Any resources you put in the new ResX file will be included in the main assembly in your project. One problem with ResX files added this way into Visual Studio .NET is that the internal dependency checking doesn't always notice that the edited ResX file has changed for a normal build. If you're wondering what happened to the resources you just added, you can do a rebuild to see your changes.

Once you've got the resources embedded into your assembly, it's a simple matter of allocating an instance of the ResourceManager class to do the loading. Figure 2 shows part of the InternationalizationDemo program—the part that loads internationalized greetings based on the UI culture. If you need to load types other than strings, you can use the ResourceManager.GetObject method. Note that in Figure 2 the StartUp.ApplicationUICultureInfo is an internal object, a property that retrieves the CultureInfo class that the whole application is supposed to use. This is a trick to allow an application to display and use a different culture than the default. Keep in mind that ResourceManager instances are not inexpensive to create, so you'll probably want to create one instance that you can share or batch load the resources you'll need.

Figure 2 ResourceManager in Action

private void MainForm_Load(object sender, System.EventArgs e) { // Start up the timer. timerTicker.Tick += new EventHandler ( TimerTickEvent ) ; // Force the first display data in the date and time. TimerTickEvent ( null , null ) ; // Fill out all the data on the form using the UI culture // selected at startup. labelUILanguage.Text = StartUp.ApplicationUICultureInfo.NativeName ; labelCultureLanguage.Text = StartUp.ApplicationCultureInfo.NativeName ; // Get the string resources out of the internationalized greetings // string table. ResourceManager rm = new ResourceManager ( "InternationalizationDemo.Greetings" , Assembly.GetExecutingAssembly ( ) ) ; // Technically, if you've set the Thread.CurrentThread.CurrentCulture, // you don't need to pass the IFormatProvider derived class, but I // do it here to make FxCop happy. Notice that since these values // are being displayed in the listbox, I'm using the *UI* culture. String str = rm.GetString ( "FormalMan" , StartUp.ApplicationUICultureInfo ) ; listboxGreetings.Items.Add ( str ) ; str = rm.GetString ( "FormalWoman" , StartUp.ApplicationUICultureInfo ) ; listboxGreetings.Items.Add ( str ) ; str = rm.GetString ( "InformalMan" , StartUp.ApplicationUICultureInfo ) ; listboxGreetings.Items.Add ( str ) ; str = rm.GetString ( "InformalWoman" , StartUp.ApplicationUICultureInfo ) ; listboxGreetings.Items.Add ( str ) ; }

Building Real-world Internationalization DLLs

While I've gone through the gyrations necessary to get ResX files added to the projects and the strings loaded, now I'll describe the steps necessary to build separate satellite assemblies that will contain the internationalized resources in a clean and reproducible manner. While you could create a separate project for each internationalized satellite assembly, that would be a little tedious. I wanted a way to build these assemblies that would work for real-world build systems. In thinking about how I wanted to tackle the problem, I ran across an article by Satya Komatineni where he presented a batch file to call RESGEN and AL for ResX files. I took Satya's idea, added lots of error handling, generalized some approaches, and added new features to arrive at the BuildIntl.CMD, BuildIntlHelper.CMD, and IntlRes.CMD batch files.

Integrating BuildIntl.CMD into your build process should be simple. Put your internationalized ResX files for a particular language/region into a directory that follows the RFC 1766 naming convention. For example, my InternationalizationDemo program in the .\InternationalizedDemo directory has four directories, .\InternationalizedDemo\ES, .\InternationalizedDemo\ES-MX, .\InternationalizedDemo\EN-AU, and .\InternationalizedDemo\EN-GB, for the four internationalized sets of resources. The three batch files that make up BuildIntl.CMD go into the directory above the named language/region directories. In the case of InternationalizedDemo, they would go into the .\InternationalizedDemo directory.

Once you've created the international directories and the appropriate ResX files are set, copy the three batch files into the root directory. You'll only have to edit the file BuildIntl.CMD to put in your particular options (see Figure 3 for an example). The first items you may want to change are the output directories for debug and release builds. Search for OUTDIR and you'll see that they are set depending on the command-line option passed to BuildIntl.CMD. The second items to change are the internationalized directories to process for your project. Simply add them to the FOR statement's list, each separated by a space. The last step is to put the name of your assembly between the %%d and the %OUTDIR% on the FOR statement line. Once you've saved your changed BuildIntl.CMD, pop open a Command Prompt and ensure that the RESGEN.EXE and AL.EXE files are both in the path by executing <Visual Studio .NET Install Dir>\Common7 \Tools\VSVARS32.BAT.

Figure 3 BuildIntl.CMD

@ECHO OFF @REM ------------------------------------------------------------------ @REM Takes care of compiling all resources for the @REM Internationalization Demo program @REM @REM The only valid parameters are debug or release. @REM ------------------------------------------------------------------ @REM Any environment variables are local to this batch file. SETLOCAL @REM - You may want to change the OUTDIR settings depending on your @REM - build. @REM Check the required command-line option. SET OUTDIR= IF /i "%1"=="debug" SET OUTDIR=..\bin\debug\ IF /i "%1"=="release" SET OUTDIR=..\bin\release\ IF "%OUTDIR%"=="" GOTO InvalidArg @REM - Add your RFC1766 directories in parenthesis @REM - Change InternationalizationDemo to the name of your assembly @FOR %%d IN (en-AU es es-MX en-GB) DO CALL BuildIntlHelper.CMD %%d InternationalizationDemo %OUTDIR% GOTO End :InvalidArg @ECHO BuildIntl.CMD - John Robbins - (john@wintellect.com) @ECHO Usage: BuildIntl [debug][release] @ECHO debug - Build resources into debug directory @ECHO release - Build resources into release directory :End

As you can see from Figure 3, BuildIntl.CMD simply loops through your internationalized directories and calls the BuildIntlHelper.CMD batch file. BuildIntlHelper.CMD will change to the appropriate directory and call the real workhorse batch file, IntlRes.CMD, shown in Figure 4. The main work of IntlRes.CMD is to wrap the call to RESGEN.EXE and AL.EXE to produce the internationalized satellite assembly. The first parts of the processing involve figuring out if the output directory exists and deleting any existing .RESOURCES files that might be lying around. After RESGEN.EXE is called on all the ResX files found in the directory, it's time to build up the command line to AL.EXE.

Figure 4 IntlRes.CMD

@ECHO OFF @REM ------------------------------------------------------------------ @REM A batch file that takes care of compiling a directory of .RESX @REM files into an internationalized .NET satellite DLL. @REM @REM This batch file idea is from Satya Komatineni's article: @REM http://www.ondotnet.com/pub/a/dotnet/2002/10/14/local2.htm?page=1 @REM John Robbins (john@wintellect.com) added all the error handling, @REM file cleanup, ALLOPTS.TXT processing, and optional output code. @REM @REM RESGEN.EXE and AL.EXE are assumed to be in your path. @REM If not run VSVARS32.BAT @REM ------------------------------------------------------------------ @REM Any environment variables are local to this batch file. SETLOCAL @REM Clear out any previous error files IF EXIST ErrorOut.txt DEL /Q ErrorOut.txt > nul @REM Check the required command-line options. IF "%1"=="" GOTO InvalidArg IF "%2"=="" GOTO InvalidArg SET LANG=%1 SET NAMESPACE=%2 @REM Check the optional command-line arg. If it's present, add the @REM language onto the directory and build the directory variable. IF NOT "%3"=="" ( IF NOT EXIST %3%LANG% ( MKDIR %3%LANG% >> ErrorOut.txt IF ERRORLEVEL==2 GOTO Error ) SET OUTDIR=%3%LANG%\ ) ELSE ( SET OUTDIR=.\ ) @REM Delete any .RESOURCES files in the current directory. IF EXIST *.RESOURCES DEL /Q *.RESOURCES > nul @REM Echo out the file names we're about to do as RESGEN does not report @REM the file name with the error in it. @FOR %%f IN (*.RESX) do ECHO %%f >> ErrorOut.txt @REM Convert all the .RESX files in this directory to .RESOURCE files. @FOR %%f IN (*.RESX) do RESGEN %%f >> ErrorOut.txt IF NOT %ERRORLEVEL%==0 GOTO Error @REM Get rid of any existing RESP.TXT file. IF EXIST RESP.TXT DEL /Q RESP.TXT @REM Ensure the type is set to LIB. ECHO /t:lib > RESP.TXT @REM Poke in the resources in the directory. FOR %%f IN (*.resources) DO ECHO /embed:%%f,%NAMESPACE%.%%f >> RESP.TXT @REM If ALOPTS.TXT is there, add that to the mix. IF EXIST ALOPTS.TXT TYPE ALOPTS.TXT >> RESP.TXT @REM Ensure there's a CR/LF after the ALOPTS.TXT just in @REM case the user forgot to add it. ECHO. >> RESP.TXT @REM Specify the culture. ECHO /culture:%LANG% >> RESP.TXT @REM Add the output name. ECHO /out:%OUTDIR%%NAMESPACE%.resources.dll >> RESP.TXT @REM Delete the resources DLL I'm about to create. IF EXIST %OUTDIR%%NAMESPACE%.resources.dll DEL /Q %OUTDIR%%NAMESPACE%.resources.dll > nul @REM Create the resource DLL. AL /nologo @RESP.TXT >> ErrorOut.TXT IF NOT %ERRORLEVEL%==0 GOTO Error @REM Get rid of files no longer needed. IF EXIST ErrorOut.TXT DEL /Q ErrorOut.TXT > nul IF EXIST RESP.TXT DEL /Q RESP.TXT > nul IF EXIST *.RESOURCES DEL /Q *.RESOURCES > nul GOTO End @REM Spit out any error output file. :Error IF EXIST ErrorOut.TXT TYPE ErrorOut.txt IF EXIST ErrorOut.TXT DEL /Q ErrorOut.TXT > nul GOTO End @REM Show the usage. :InvalidArg @ECHO IntlRes.CMD - John Robbins - (john@wintellect.com) @ECHO Invalid command-line arguments @ECHO Usage: IntlRes [language] [Application Namespace] [Version] [Output @ECHO Dir] @ECHO [language] - The full language for the resources @ECHO See the list in the CultureInfo class @ECHO overview. (Required) @ECHO [Application Namespace] - The application namespace for all the @ECHO resources. (Required) @ECHO [Output Dir] - The optional output directory for the @ECHO output DLL. If specified, the output @ECHO DLL is placed in @ECHO [Output Dir]\[language] @ECHO Trailing backslash IS REQUIRED! @ECHO If you want to set additional command-line options to AL, (such as @ECHO version, signing, and so on) put the options in a file called @ECHO ALOPTS.TXT in the same directory as the resources. If the file is @ECHO present, this batch file picks them up and ensures they are @ECHO included. @REM The end of the program. :End

Since AL.EXE can take a response file listing the options, I chose that option because I wanted the user to be able to specify their own options to AL.EXE. You can do that by including a file called ALOPTS.TXT in the same directory as the ResX files. For the most part, you'll want to include the version number information as well as the strong name key file in your own ALOPTS.TXT files. See the InternationalizationDemo for examples of using ALOPTS.TXT files (available in the download at the link at the top of this article). Always remember to set the version information in satellite assemblies to the same as the main assembly so all satellite assemblies will share the main assembly's strongly named key file. After creating the full response, AL.EXE builds the satellite assembly that will have the appropriate output name of <Assembly Name>.resources.DLL. The rest of the processing in IntlRes.CMD takes care of cleaning up any temporary files used by the build.

Windows Forms Internationalization

Now that you have a way to cleanly build your international resources for both ASP.NET and Windows Forms applications, I want to spend a little time going over Windows Forms internationalization. The most important issue with Windows Forms internationalization is setting a form's Localizable property to true in the Properties window. Setting Localizable to true causes two important additions to the form. The first is to reflect any changes to the code generated in the InitializeComponent method. When Localizable is set to false, here is the code that's generated. You can see the hardcoded strings in the code:

private void InitializeComponent() { this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(292, 266); this.Name = "Form1"; this.Text = "Example Form"; }

Setting the Localizable property to true causes the code in Figure 5 to be generated. The code is quite different and uses the ResourceManager class to do all the work of loading anything about the form. As you can probably guess, the second action performed when setting Localizable to true is to put all hardcoded values into the ResX file associated with the form.

Figure 5 Localizable Set to True

private void InitializeComponent() { System.Resources.ResourceManager resources = new System.Resources.ResourceManager(typeof(Form1)); // // Form1 // this.AccessibleDescription = resources.GetString("$this.AccessibleDescription"); this.AccessibleName = resources.GetString("$this.AccessibleName"); this.AutoScaleBaseSize = ((System.Drawing.Size)(resources.GetObject ("$this.AutoScaleBaseSize"))); this.AutoScroll = ((bool)(resources.GetObject("$this.AutoScroll"))); this.AutoScrollMargin = ((System.Drawing.Size)(resources.GetObject ("$this.AutoScrollMargin"))); this.AutoScrollMinSize = ((System.Drawing.Size)(resources.GetObject ("$this.AutoScrollMinSize"))); this.BackgroundImage = ((System.Drawing.Image)(resources.GetObject ("$this.BackgroundImage"))); this.ClientSize = ((System.Drawing.Size)(resources.GetObject("$this.ClientSize"))); this.Enabled = ((bool)(resources.GetObject("$this.Enabled"))); ••• }

To work on a different language version of a particular form, you just need to set its Language property to that particular language. That will copy the default ResX file to two separate ResX files. For example, if you choose "Spanish (Mexico)" in the Language property for Form1, that will create Form1.es.ResX and Form1.es-MX.ResX. These two files will be added to the project, though you won't see them by default. You'll need to click the Show All Files button in Solution Explorer to see them under the Form1.CS entry. When you go to build your application, you'll see two new directories created under the output directory. In the case of the example using "Spanish (Mexico)" you'll see .\ES and .\ES-MX directories.

While it's nice that Visual Studio .NET does provide for internationalization in your Windows Forms, it has a few drawbacks. First, because Visual Studio .NET takes over the building of the internationalized satellite DLLs, you have no way to add your own resources to the mix. Consequently, this makes general internationalization much more difficult. Second, because Visual Studio .NET produces the ResX files, you'll have to give all your source code to the translators so they can translate the UI. While that might be fine if you have the translators on staff to do the work, most development shops outsource the translation work and won't want to expose their intellectual property.

These issues, in my opinion, make the use of Visual Studio .NET for internationalization less than ideal. Fortunately, working around these issues is relatively simple. When you undertake the process, you should always set the Localizable property to true on every one of the Windows Forms that you want to internationalize. This makes the code load the resources in the ResX file. Once you have the form working and the UI is set, you can then hand off just the form's ResX file to the translator because that file will contain all the information about the form. Your translators must have the Framework SDK installed, for they will use WinRes.EXE to open the ResX file. When you run WinRes.EXE, you'll see that it is essentially the Windows Forms editor from Visual Studio. Your translators can translate all the strings and save the file for the appropriate language/region, which will add the RFC 1766 name between the form name and the ResX extension. Once the translation is complete, they'll ship the translated ResX file back to you.

There are a couple of caveats about using WinRes.EXE. The first issue with WinRes.EXE is that the ResX files it produces are no longer editable by Visual Studio .NET, but there's nothing you can do about that. Second, WinRes.EXE does not just limit changes to strings in the form. The translator could reposition or resize controls on the form. For languages such as Chinese that require special fonts and resizing, that might be necessary. However, most development teams don't want their translators rearranging their forms. The ideal solution would be for WinRes.EXE to support an editing mode that only allowed changes to string data. It should be noted, however, that unless great care has been taken to design a UI that can withstand localization, the quality of the localized software will suffer if localizers aren't given the ability to adjust sizing and positioning. Doing so could force localizers to compromise on language or could result in a slew of sizing bugs which will then need to be found and fixed. That said, there is still some value in such a feature.

Thinking about the problem, I came up with a program called ResXInternational that runs through a ResX file and produces an output ResX file that contains just the core string and a few other international values. By running ResXInternational across the files returned from the translator, you'll have just the core translated strings and you'll avoid accidental UI changes. Since your default resource set will have all the other pieces of the form, everything will work just great. You can find ResXInternational in this month's source code distribution. Now that all the dealings with ResX files are out of the way, I want to turn to some implementation highlights of ResXInternational and some of the other programs that I wrote or enhanced for this column.

Internationalization Helper Programs

As you've already seen, there are a couple of places where I found that writing a program can make your internationalization chores easier. The first program I tackled was ResXInternational. Originally, I thought I was going to have to do quite a bit of work to read in and process the XML-based ResX format. A quick search of the MSDN Library yielded the ResXResourceReader and ResXResourceWriter classes for reading and writing ResX files. As you look over the source code to ResXInternational, you'll see just how simple it really is to manipulate resource files. In all, it took me about 30 minutes to hammer out ResXInternational.

In working on my internationalized application, which needed a huge number of internationalized string resources, I found that the Visual Studio .NET XML editor worked pretty well. Its only downside is that it does not provide a way to add icon and graphic objects to the ResX file. I was all set to start working on a small resource editor when I ran across the ResEdit sample from the Framework SDK. When I fired it up, I realized it was an excellent ResX file handler all on its own. I was using it for a while and ran into a bug in which it had trouble opening certain files. I tracked it down, fixed the bug, and created a full Visual Studio .NET solution for the project, which it did not have originally. The problem turned out to be that ResEdit did not handle entries that had a null value. As those types were empty strings, I added a check in ResHolder.cs to change the value from null to String.Empty. While I was in the code, I also updated the File Open dialog to look for ResX files first instead of .RESOURCES. ResEdit isn't perfect; it groups by type (such as string and anchor), instead of by control or form values. Nevertheless, in all the resource files I've edited, that hasn't been much of a problem.

The Pseudoizer program is something I've wanted for a very long time. When it comes to testing localization, it's generally quite difficult because unless the testers and developers are multilingual and are paying very close attention, it's easy for untranslated strings to slide through. One way to find internationalization problems is to use a technique called pseudo-localization. It involves translating the resources and other UI pieces into something readable, but drastically different from normal text. For example, you could replace every "a" with an "â". Pseudo-localization also adds extra padding characters to the ends of strings. The types of errors that pseudo-localization helps you find are: hardcoded strings that need internationalization, strings that shouldn't be translated, non-Latin characters, and errors handling longer language strings.

While you could manually run through all your string resources and convert each character by hand, the word tedious doesn't even begin to describe the agony. What I wanted was a program that would read in my resources and allow me to choose which strings to translate. This was important as some strings included in translated resources, such as the menu separator string, do not need to be pseudo-translated.

At this point, it's probably easiest if I show you a pseudo-translated UI. Figure 6 shows the InternationalizationDemo UI with all resource strings in pseudo-English form. All strings are surrounded by "[]" so that you can see where each begins and ends. As you can see in Figure 6, it's relatively simple to read the strings that have be pseudo-translated and it makes finding the non-translated strings a snap. The non-translated strings in the figure are from the .NET Framework itself, so they cannot be pseudo-translated.

Figure 6 Pseudo-translated UI

Figure 6** Pseudo-translated UI **

I poked around in .NET a bit to see if I could create my own language/region, but only the ones listed in the CultureInfo class documentation are usable. That meant I needed to pick a culture that I was going to use as the pseudo-translated culture. As you can see in Figure 6, I decided on English as spoken in the United Kingdom (en-GB) as my pseudo-translated culture. This also meant I would get date, time, and currency translations that were different from my normal US-based English. Whatever language/region you choose to override for your pseudo translation, just make sure to avoid shipping the translated resources with your application or you're going to get some angry tech support calls!

While it's now relatively easy in Windows XP to set the current language/region through the Regional and Language Options control panel applet, I set the culture manually in my debug builds. In the Pseudoizer program itself, I have debug-only code that looks at the command-line options and sets the language/region appropriately. If you have a complex multithreaded application, you may want to set an environment variable and add debug-only code that looks for it and sets the language/region in each of the threads.

To be thorough I just had to run Pseudoizer on itself. I was quite surprised to see an internationalization bug pop right out! In the About box I was using a LinkLabel control to link to my Web site. When I ran the pseudo-translated version, I noticed that the LinkArea didn't extend to the end of the control as I expected. I found that when you use a LinkLabel, the Form editor hardcodes the length of the string into the LinkArea at design time, which is a bug. I'm happy when a utility can find a bug in itself!

The last program in the source code distribution is the InternationalizationDemo I've been mentioning through this column. As the name implies, it shows off the techniques of internationalization. I translated the UI into Spanish and supported two region areas, Spanish (Mexico) and English (Australia). If you pass a valid RFC 1766 language/region on the command line, you'll see the various language-specific items change appropriately. Additionally, in InternationalizationDemo I show a technique for overriding the user locale by providing the static Startup.ApplicationCultureInfo and Startup.ApplicationUICulture, which I use to set all thread cultures and pass to all ResourceManager loads.

The tools and information provided in these two columns on internationalization should get you started and make real-world internationalization easier to achieve. As I mentioned last time, Microsoft earns 60 percent of its revenues from internationalized software so that should provide you with plenty of motivation.


Tip 61 If you're looking to see how .NET works or want to see how someone implemented a cool program, but you don't have the source, look at Lutz Roeder's Reflector. Not only does it disassemble, it's a complete decompiler that will translate MSIL into C# or Visual Basic .NET.

Tip 62 While Reflector is a standalone program, Jamie Cansdale has written a cool article in which he takes Reflector and, with some magic, converts it into a Visual Studio .NET AddIn. Not only does he make a phenomenal utility even more useful, Jamie's code is a great example of .NET Framework programming.

Send your questions and comments for John to  slayer@microsoft.com.

John Robbins is a cofounder of Wintellect, a software consulting, education, and development firm that specializes in programming for .NET and Windows. His latest book is Debugging Applications for Microsoft .NET and Microsoft Windows (Microsoft Press, 2003). You can contact John at http://www.wintellect.com.