Multilanguage in Plain English

The solution presented in this paper is a simple one, yet I've spent a few days looking for it, and then building and improving it. I'm sure this solution it is not new and that it can be improved, so please contribute your comments to it.

The Problem

I was working to migrate a part of an existing C++ application to .NET. The application must be available in at least five languages and the translation is not made in house. A pecularity of this application is that it makes heavy use of Crystal Reports. These reports are written in house, but there is also the remote possibility of having them from outside sources.

Solution 1

The obvious choice is to use the localization support in .NET. Add a few resx files and voilà, the application is ready.

Howeverm this didn't feel right. This approach implied that the people doing the translations must have access to all those files that are part of the project. The worst case scenario would be the need of a translation in between builds. Why is that? Well, the satellite assemblies contain not only the resources in the RESX files added but also all the resources in your project.

So, rebuilding the satellites from a working version and deploying them to be used in an official build might break the build because resources might be missing/changed. And, that's not fun at all.

Solution 2

Searching for a solution, I found the Localized Hello World example and it looked like the answer to all my prayers.

The main idea in the example was to have a separate project for the resources to keep what was needed. After a closer examination, however, I have found out that all the major problems of the previous solution were still there. The only difference was that now I had three projects instead of one.

Nevertheless. the example is very good for learning the magic behind the resources and how they are built and deployed. The tools involved in this transformation are resgen.exe and al.exe.

Figure: 1 resource - 1 assembly

Note: The "Project's Exe" is actually the project's output, so it can very well be a DLL, but it is referred to this way for the sake of simplicity.

This process is elegantly implemented in the example by using Custom Build rules as described in the LocalizedHelloWorld example.

resx/txt -> resources
resgen.exe "$(InputPath)"

resources -> satellite
al.exe /culture:en
   /out:"$(ProjectDir)..\bin\$(OutDir)\en\HelloWorld.resources.dll"
   /embed:"$(InputPath)","HelloWorld.Resources.$(InputFileName)"
   /template:"$(ProjectDir)..\obj\$(IntDir)\HelloWorld.exe"

The above code is taken from the LocalizedHelloWorld example.

This is the simplest scenario. But, you can have more than one resource file packed in a single satellite assembly.

Figure: Many resources - 1 assembly

This scenario can no longer be handled using the project structure used by the MSDN example. Instead, you would need to use a batch file. Assuming your project's resources are located in D:\localized\resources and that the executable is in D:\localized\bin, the script would look like this:

resx/txt -> resources
<remains unchanged>

resources -> satellite 
al.exe /culture:en
       /out:"d:\Localized\bin\en\HelloWorld.resources.dll"
       /embed:"d:\Localized\resources\Content.en.resources",
              "HelloWorld.Resources.Content.en.resources"
       /embed:"d:\Localized\resources\Messages.en.resources",
              "HelloWorld.Resources.Messages.en.resources"
       /embed:"d:\Localized\resources\Forms.en.resources",
              "HelloWorld.Resources.Forms.en.resources"
       /template:"d:\Localized\bin\HelloWorld.exe"

This is not difficult to write, but it's a bit more complicated and error prone to maintain.

And, this is where the core of my problems reside. My resources need to be available always because I need all of them to build the satellites, even if only one of them is changed.

In either scenario, the resources for the neutral culture must remain in the "real" project so that they are embedded in the executable. This is required because of the way the resources are actually used:

// Get a resource manager
System.Resources.ResourceManager resources
   = new System.Resources.ResourceManager(
      "HelloWorld.Resources.Content",
      System.Reflection.Assembly.GetExecutingAssembly());

Why is this such a big problem? One might argue that if you add strings in a resource file, you also have to use them somewhere in your project, thus the executable needs to be rebuilt anyway. Well, this isn't exactly true.

One example where this functionality is needed is when using Crystal Reports at runtime. You can have them translated using the resources without having to -->hardcode--> the name of the string in the resource file (but this is a different topic). If the application is designed to accept new reports at runtime, it is only natural that it should be made to accept the resources for them in the same way.

Another situation is shown in the example project: The strings to be localized are acquired from an external source; in this case, the user.

At this point, I want to introduce the example project as it is referred to in the paragraphs to follow.

Multilanguage in Plain English

Multilanguage

Multilanguage, the sample project, consists of the test C# Windows Application and two C++ empty projects that were modified to hold the resources.

The solution's output consists of:

  • test.exe and its satellite assemblies containing the form's localization
  • the assemblies for the Content resource set
  • the assemblies for the Messages resource set

The application a single form holds one label, two text files, and three buttons. With the buttons, you control the thread's culture and UI culture. You can examine the Content resource set with the help of the text fields. The resource set contains only one string, but you can add as many as you need for testing.

When the language is changed, a message box indicating the new language will pop up. This message box has the text and caption loaded from the Messages resource set.

[example_1.gif]

Figure: Multilanguage Example

Note: The project is built with Visual Studio 2005, but you can very well do it in Visual Studio 2003.

First Final Solution

Solution 2 was quite close to what I needed so I've decided to try to make it work as I needed. My objectives were 2:

  • Keep the resources separated from one another.
  • Eliminate the need for the neutral culture resource to being kept in the main executable.

[resources_3.gif]

Figure: 1 resource set - 1 assembly

The solution proved to be extremely simple once I made it work: Make an assembly for each resource set. That assembly would contain the neutral culture resources for the given resource set. So, instead of making a ResourceManager that uses the executing assembly, I would have one using a different assembly, a DLL loaded manually.

The way the other resources are built is left almost unchanged.

resx/txt -> resources
resgen.exe "$(InputPath)"
resources -> satellite
al.exe /culture:en
       /out:"$(ProjectDir)..\bin\$(OutDir)\en\Content.dll"
       /embed:"$(InputPath)","Content.Resources.$(InputFileName)"
       /template:"$(ProjectDir)..\bin\Test.exe"

The output name is boldedm yet it is not highlighted enough. When I first tinkered with this idea, I have simply copy/pasted the command used for the other resources, leaving the output name as Reports.resources.dll.

With the assembly named that way, all worked great except for the fact that the resources for other languages were never found.

After a day of futile attempts, it took a more rigorous reading of how the Resource Manager works and of the resource naming convention that showed me the error: The naming of the "main" assembly was wrong. Do note that the "main" assemblies for the resource sets do not contain resources in their name. The reason for this is the resources naming convention. Here is a simplified version of it:

   - get the assembly name ( Content.dll)
   - cut the extension ( Content)
   - add "resources" ( Content.resources)
   - add the extension ( Content.resources.dll)
   - look for the culture's folder ( ro\Content.resources.dll)

So, if the main assembly name is Content.resources.dll, the satellites are expected to be in the Content.resources.resources.dll format.

For the sample project, once you set up all your resources and their custom build commands, you will end up with the following:

Project.bin
   fr
      Reports.resources.dll
      Messages.resources.dll
      Test.resources.dll
   ro
      Reports.resources.dll
      Messages.resources.dll
      Test.resources.dll
   Reports.dll
   Messages.dll
   Test.exe

How do you use them? This is actually the core of the solution. For each resource set, you need to use a Resource Manager.

// Get a resource manager for the Reports resources
System.Reflection.Assembly contentAssembly
   = System.Reflection.Assembly.LoadFile(
      <full path to Content.dll>)
System.Resources.ResourceManager contentResources
    = new System.Resources.ResourceManager(
       "Content.Resources.Content" , contentAssembly);

// Get a resource manager for the Messages resources
System.Reflection.Assembly messagesAssembly
   = System.Reflection.Assembly.LoadFile(
      <full path to Messages.dll>)
System.Resources.ResourceManager messagesResources
   = new System.Resources.ResourceManager(
      "Messages.Resources."Messages" , messagesAssembly );

And that's that. From here, you use the Resource Manager normally. To summarize, here are the pros and cons of this solution.

Pros

  • Externalise ony the resources that require it. A set of resources can be kept within the project.
  • The resources are not interdependent. You can modify and deploy them independently.
  • Use custom build rules. There's no need for an external batch script.

Cons

  • None so far


About the Author

Spurlos (*)

Alive and kicking :)

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Cisco and Intel have harnessed flash memory technology and truly innovative system software to blast through the boundaries of today's I/O-bound server/storage architectures. See how they are bringing real-time responsiveness to data-intensive applications—for unmatched business advantage. Sponsored by Cisco and Intel® Partnering in Innovation

  • Live Event Date: August 14, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Data protection has long been considered "overhead" by many organizations in the past, many chalking it up to an insurance policy or an extended warranty you may never use. The realities of today makes data protection a must-have, as we live in a data-driven society -- the digital assets we create, share, and collaborate with others on must be managed and protected for many purposes. Check out this upcoming eSeminar and join Seagate Cloud …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds