WEBINAR: On-demand webcast
How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >
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.
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.
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.
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.
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.