One of the cool features of Visual Studio .NET 2005/2008 IDE is a custom tool called ResXFileCodeGenerator that is automatically associated with resources (*.resx files) every time they are added into a project. Whenever your project is rebuilt, a resource file is saved or a custom tool is run manually. The tool generates a managed class that exposes every resource you have in the *.resx file as a strongly typed static property. In order to support more than one language, our application needs to support localization. So, we need to store our strings in resource files so that .NEt will automatically create a satellite assembly for a specific culture. There are two approaches to manage resource strings in our application:
- Storing resources in each individual assembly (not a good approach).
- Creating a centralized resource strings assembly.
The project that I am working on has almost 40 assemblies. Among 40 assemblies, 30 assemblies have resource strings. We are going to support three different languages. Since .NET will create a satellite assembly for each culture, we will have 60 satellite assemblies. We felt this is quite large and unmanageable. And, we preferred to create a centralized resource strings assembly. In this article, I am going to explain both approaches.
Storing the resources in each individual assembly
Step 1: Create a console project “ResourceMultipleProjects”.
Step 2: Add the resource file Strings.resx. This is the default resource file for the default culture.
Step 3: Add the resource files Strings.de-DE.resx and Strings.fr-FR.resx for the cultures German and French, respectively. A culture specific file name should be in the following format:
Take the example "Strings.de-DE.resx". Here:
- “Strings” is our main resource filename.
- “de-DE” is the culture specific string for German in Germany.
Step 4: Add the following resource string in all three resource files (double click the resource file to open the following view):
Step 5: Right click each resource file and open the Properties window and set these properties:
- Build Action: Embedded Resource
- Custom Tool: RexFileCodeGenerator
The Custom Tool is responsible for generating the code.
Build the project. In the output bin directory, you can see the following folders:
Inside each folder, you can see the “ResourceMultipleProjects.resources.dll” assembly. This is the satellite assembly. To access the resource string “ApplicationName”, you have to use:
Strings.ApplicationName. When you build the application, .NET will automatically generate the class “
Strings”. In order to see the auto-generated code, you can open the file “Strings.Designer.cs”.
Shown below is the auto generated code for Strings.Designer.cs:
Since the “
Strings” class is
internal, you can’t add the resource strings of other assemblies. Because you can’t access an
internal member from outside the assembly, for each assembly, you need to add a separate resource strings file. The following screenshot shows that:
You have two projects in the above solution. Each project contains separate resource strings. For each culture specific resource strings file, .NET will create a separate satellite assembly. Run the sample application in order to understand more. When you run the sample “ResourceMultipleProjects.exe”, you will get the following output:
Note: To differentiate the cultures, I prefixed each resource string with its culture string, like FR and DE.
Though it is easy to add individual resources in each assembly, it has the following issues:
- If the number of assemblies increases, then the number of satellite assemblies will increase drastically. (A medium size project will have 15 to 25 assemblies.)
- Even for one or two resource strings, you need to create the entire resource strings files for each culture in the assembly.
- The resource strings will be scattered across all the assemblies.
- In future, if you decide to support one more language, you need to add a separate resource file for each assembly. In this case, the installer creation will be a nightmare for anyone.
- The .NET runtime has to load all the satellite assemblies. So it will affect performance.
In order to overcome the above issues, we need to use a centralized resource location for each culture.
Creating a centralized resource strings assembly
In this approach, we are not going to create separate resource strings in each assembly. Instead, we are going to put all our resource strings in one common assembly. Create a project solution as per the following diagram, or open the sample solution ResourceInSingleProject.
Here, the “StringManager” assembly contains all the resource strings for each culture. The individual project doesn’t have a separate resources string file. You should refer to the String Manger assembly (project reference) from all the other projects. Now, you need to do the following steps in the resource strings file.
Step 1. Right click the individual resource file and open the Properties window:
Set the BuilAction property to Embedded Resource, as you do normally. Then, you need to set the Custom Tool property. If you set the CustomTool property to ResXFileCodeGenerator, then it will generate the class
Strings in the file “Strings.Designer.cs”. The access specifier of the
Strings class will be
internal. So, you can’t access the resources from outside the assembly. In order to create a
Strings class, you need to set the CustomTool property to “ResXFileCodeGeneratorEx”. To get this option, you need to install ResXFileCodeGeneratorEx. This is a free tool, and you can download it from the following link: http://www.codeproject.com/KB/dotnet/ResXFileCodeGeneratorEx.aspx. With this sample application, I have put the source code and the installer for this tool. This tool was developed by “Dmytro Kryvko”.
Add some sample resource strings in the Strings.resx, Strings.de-DE.resx, and Strings.fr-FR.resx files and open the file “Strings.Designer.cs”. You can see that the class
Strings has the
public access modifier.
Since the “
Strings” class is
public, we can access the resources from any other assembly. When you run the sample "ResourceInSingleProject.exe", you will get the following output:
The advantages of ResXFileCodeGeneratorEx and ResXFileCodeGenerator are:
- ResXFileCodeGeneratorEx will create a public resource class file so that you can access the resource strings from other assemblies.
- ResXFileCodeGeneratorEx is thread safe.
In my experience, using a centralized resource manager is the best option. It is very easy to maintain, and you can easily add new culture specific assemblies. In this approach, we will have only one satellite assembly for each culture.