Introduction
If you are serious about letting people all around the globe use your application comfortably, you will need to localize it. In .NET, the most common way of doing this is to use application resources. Visual Studio provides a very convenient way to edit resources and automatically generates wrappers that allow easy access from your programming language.
Whenever you need a string in your code, just use property of auto generated class.
string myText = Properties.Resources.MyMessage;
This is really nice, except if you have formatting placeholders inside the string, you will need to format it in a separate step.
string format = Properties.Resources.MyFormatString;
string myFormattedText = string.Format(format, DateTime.Now, 3.14);
There are two problems with separate retrieval and formatting of the strings. First, there is no automatic validation of number and syntax correctness of format placeholders and actual parameters passed to the format function and you do not even see the format string as it is in resources. That means that you will get an exception during the execution of the program when there is anything wrong with the string or parameters. Which is silly as when we are building the application, we know this information and can check it. Moreover, the localizers are people who likely are not developers and hence have no clue how harmful it is to mess with placeholders. In another words, the translated resource files have to be a subject of validation too.
The second problem is the call to Format above is missing formatting culture. As the culture not been passed, the Thread.CurrentCulture
will be used. Of course, you can be very pedantic and pass the culture in all such cases. But actually, the reason you are getting the string from resource is you want to show it to the user, so you are going to pass the current culture all the time and once again there is no good way of checking that you pass it in all cases. Not passing the culture at all might not look like a major issue, but Visual Studio static analysis will complain about it. Of course, one can always turn off such warnings, but they are there for a reason. For more details on why you should provide culture info explicitly, please read this article.
The solution for this problem can be a tool that generates properties for strings that do not have format items in them and functions for strings with them. I found a tool that does almost what I need here.
Unfortunately, it generates functions with all parameters being objects. I would like to have a better one that can generate functions more precisely typed. This way, the compiler will have more information to check parameters.
In majority of cases, it is not possible to infer from the string the types of parameters, so developer should provide this information to the tool. It is possible to use comment field of resource editor for this purpose. Therefore, if there are no format items in the string itself, then the tool will ignore the comment, if there are such items, the tool should look in the comment field, take types and names of parameters from there, and use them when generating code for functions.
Another important feature of such tool can be pseudo localization. This is a technique when a tool generates a string based on the real strings, but longer and with different characters. For example, the string: “hello
” can be pseudo localized to: “[-~=hëllö=~-]
”. Notice that you still can read it, but no characters are of English alphabet and the string is longer than the original. This is a good way of finding hardcoded strings and missing translations. The longer strings can show you problems in your UI layout.
In the attachment of this tip, you can find such a tool as well as a sample project showing the usage of the tool.
The tool is a standalone command line application unlike the standard resource generator. This is because the tool needs to get the entire project for processing translations rather than one resource file like in the standard generator.
To get all the command line parameters, you can run the tool with /?
. However, the main required parameter is /p
<project file>. If you already have a big project, you can use /o
flag to make parameter declaration optional for the period of transition to this tool; when not all strings with format items have corresponding parameter declarations yet.
In the comment field for each resource, you can have three types of declarations:
- {function parameters in C# syntax}
- !(Comma separated list of possible values)
- - (minus sign) turning off format item validation
The first syntax is used for the resources where formatting items are present and you want to generate strongly typed function. The resource string as well as all the corresponding resources in the other language files will be validated to match number of parameters and check validity of format items.
The second one is used if you need to place in your resource file value of some enum
that can be different in different cultures. In this case, the main resource and all the translations will be checked for value in the provided list.
Finally, the third syntax is turning off any validation of this resource. The tool will generate property in this case even if there are any format items in the string itself.
Here is an example of what you can have in your resources:
In all cases, you can have any other text after the declarations. Usually, people provide additional instructions for translators about context of the string.
Here is what the tool will generate for some of the above strings:
public static string MessageSelection(int index, string value, string text) {
return string.Format(FormatCulture,
ResourceManager.GetString("MessageSelection", Culture), index, value, text);
}
public static string SelectingResource {
get { return ResourceManager.GetString("SelectingResource", Culture); }
}
public static string NotFormatted {
get { return ResourceManager.GetString("NotFormatted", Culture); }
}
In order to turn on pseudo localization, you will need to provide fully qualified name of the resource. For example, in the demo application, the parameter should be: /Pseudo ResoucesDemo.Properties.Resources
.
Using the Tool Along with Visual Studio
How to integrate the tool in your Visual Studio solution. First, let us look at ResourceWrapper.Generator.csproj.
To look at the project file in Visual Studio, you can unload it from solution; just right click on the project in the solution explorer and select Unload Project from the context menu. After the project is unloaded, you can right click it again and select Edit project menu item. This will bring the project file as a text file in Visual Studio and you can inspect or modify it. When you are done, just close the text file and right click the project in the solution explorer again and select Reload Project.
In the project file, scroll to the very end. You will see:
<PropertyGroup>
<ToolsFolder>$(SolutionDir)Tools</ToolsFolder>
</PropertyGroup>
<Target Name="AfterBuild">
<Copy Condition="Exists($(ToolsFolder))" SkipUnchangedFiles="true"
SourceFiles="$(TargetPath)" DestinationFolder="$(ToolsFolder)" />
</Target>
Where ToolsFolder
is a new project property that defines a location of executable where other projects will invoke it. Then after build target will copy the tool to the defined location if the solution folder contains it.
Now let us see what is happening in the demo application project.
Once again, open the project in the Visual Studio and scroll to the very bottom. You will see the following:
<PropertyGroup>
<ResourceWrapperGeneratorCommand>"$(SolutionDir)Tools\ResourceWrapper.Generator.exe"
/FlowDirection /p "$(ProjectPath)"</ResourceWrapperGeneratorCommand>
<PseudoBuildDefined>$(DefineConstants.Contains("Pseudo"))</PseudoBuildDefined>
<ResourceWrapperGeneratorCommand Condition="$(PseudoBuildDefined)"
>$(ResourceWrapperGeneratorCommand)
Pseudo=ResoucesDemo.Properties.Resources</ResourceWrapperGeneratorCommand>
</PropertyGroup>
<Target Name="BeforeBuild">
<Exec Command="echo $(ResourceWrapperGeneratorCommand)" />
<Exec Command="$(ResourceWrapperGeneratorCommand)" />
</Target>
In the property group, it is a new property ResourceWrapperGeneratorCommand
defined. This is the main flavor of the command to invoke the tool. The second property used for detection of conditional compilation symbol is "Pseudo
". Normally, you can define this constant without unloading project. In the project properties:
Finally, the third element in the property group is redefinition of ResourceWrapperGeneratorCommand
, which is adding to the previously defined command names of classes to pseudo localize. This redefinition will happen if the project is defining Pseudo
constant.
The before build target will print out the final command and execute it.
You can easily control if you need pseudo localization by simply adding or removing conditional compilation symbol as shown above. You can also use ThreadRU
or ResourceRU
symbols. To understand what they are doing, open App.xaml.cs file. You will find that it setting culture in the CurrentUICulture
of the current thread or sets two properties on the resource wrapper class. This will affect formatting of dates and numbers. To check the effect of each symbol, run the application and see how it looks. If you use symbol Pseudo, then the application will choose randomly left to right or right to left flow direction. This will help you verify your layout for right to left cultures.
This is because the Window’s property FlowDirection
is set to resource property with the same name. The resource property is generated by the tool when you use /FlowDirection
command parameter. This is useful for WPF applications.
Notice on the above pictures that strings that were not pseudo localized standing out in the UI making it easier to find such hard coded strings.
For examples of use of generated properties and functions, please look at MainWindow.xaml and MainWindow.xaml.cs files.
To use the tool in your own project, you can follow the same pattern and have Tools folder in your solution or you can build the tool separately and put it somewhere on the path and execute it from there. I prefer to keep it within the solution so it is easier to transfer my project between computers without the need to install the tool everywhere. If you decided to have the tool in your solution, first create folder Tools in your solution folder. Copy ResourceWrapper.Generator folder from the attached sample in your Tools. Open your solution and add the generator project from the location you just copied it to. Set project dependencies such that your project will depend on the generator project. This will ensure that Visual Studio built the tool before your project. Now you will need to modify your project to invoke the generator tool. Just copy/paste the snippet above your project and you are good to go.
If you are looking for real life example of using this tool, please check out Logic Circuit - open source educational software for designing and simulating digital logic circuits. The application translated to 15 languages and the tool is making it much easier to maintain sanity of resources.
The application web site is http://www.logiccircuit.org/. You can get source code of the application from https://logiccircuit.codeplex.com/SourceControl/latest.
History
- 29th March, 2015: First version