View source code on GitHub
In first part of WPF Localization & Translation I described the requirements to localization library. In this article I will show WPF localization at runtime using ResX files.
Lets start with simple case and try to localize a text on a Button.
1 |
<Button Content="Save"/> |
We want changing language to German causes “Save” changing to “Speichern” on the fly without restarting application. The second our desire is to make life easy for developers and designers who will write localizable XAML. I think next XAML is quite simple for this purpose:
1 |
<Button Content="{Res Lib_Save}"/> |
History
The idea and solution was initially compiled from several sources such as:
WPF Runtime localization
WPF Localization Using RESX Files
Localization of a WPF application using a custom MarkupExtension
and other similar articles.
Since the beginning some chalenging tasks were solved and now our library successfully functioning in quite large server and client .NET solutions (80 projects, 2000+ XAML files).
Layers
In spite of simple XAML
1 |
<Button Content="{Res Save}"/> |
the translation system consists of some layers that responsible for their own part of localization:
1. Storage with all localized strings in all languages. For example *.resx files. But it can be any other form of storage: DB or XML-file
2. Manager that chooses appropriate string from Storage depending on current language and returns it
3. XAML Markup Extension provides convenient using for developers and designers
4. Layer between XAML and Manager.
Storage. WPF localization at runtime using resx files.
We made a *.resx file as storage for our resources for several reasons.
- It’s natively supported by .NET Framework. You can easy get resource from *.resx file via .NET classes.
- Visual Studio has it’s own resource editor.
- There are many third party tools for resource manipulating. Such as ResX Resource Manager. Or you can write your own. It’s not so difficult.
- It’s easy to divide resources by language putting them into different *.resx files: Lib.resx, Lib.de.resx, Lib.ru.resx. Below is the screenshot of one of the resource editor implementation
ResManager
It’s nonWPF class that can retrieve resources from storage by key. It’s methods such as GetResourceString(“Lib_Save”), GetResourceObject(“Lib_SomeObjectKey”) return localized resources.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <summary> Resource manager </summary> public interface IResManager { /// <summary> /// Get string from resources by key depending on current culture. /// </summary> /// <param name="resourceKey">Resource key.</param> /// <returns>String from resources.</returns> string GetResourceString(string resourceKey); /// <summary> /// Get string from resources by key depending on given culture. /// </summary> /// <param name="resourceKey">Resource key.</param> /// <param name="cultureInfo"></param> /// <returns>String from resources.</returns> string GetResourceString(string resourceKey, CultureInfo cultureInfo); /// <summary> /// Get object from resources by key depending on current culture. /// </summary> /// <param name="resourceKey">Resource key.</param> /// <returns>Object from resources.</returns> object GetResourceObject(string resourceKey); /// <summary> /// Get object from resources by key depending on given culture. /// </summary> /// <param name="resourceKey">Resource key.</param> /// <param name="cultureInfo"></param> /// <returns>Object from resources.</returns> object GetResourceObject(string resourceKey, CultureInfo cultureInfo); /// <summary> /// Returns true if resource manager contains resource for <paramref name="resourceKey"/>. /// </summary> /// <param name="resourceKey">Resource key.</param> bool Match(string resourceKey); } |
Here we have to decide about important thing – resources organization in files.
Having localization library in large solution with more than 20 developers we very quickly fall into swelling our resx file to something that impossible to manage. There are advices to create one resx file per xaml file. It’s not good for some solutions I worked on, with 2000+ XAML files. You get 2000+ additional resx files for each language you supported. For 2 languages it will be 4000+ files, for 3 – 6000+ etc.
How to organize your own resources depends on your project. Below I describe the kind of storage that successfully works with such big project I described above.
Resources storage organization
Almost all application consists of two parts: common library and application itself. Common library is a set of projects with common classes. “Common” means classes and other stuff that can be used in many applications. For example Reflection helpers, collection helpers, Linq extensions, common dialogs, WPF controls, converters and much more each company have in it’s arsenal. This common library can be used by any application. It’s the base for any application. So all resouces that belong to common library and invariant for all applications we can put to independent file Lib.*.resx. Such strings as “Save”, “Cancel”, “Close”, “Do you really want to delete this row?”, “ID”, “Name” and so on.
Application resources we can divide into groups too. If your app get entities from DB it’s probably you have a deal with some form of codegeneration. Entity Framework as example. And you can generate resource files for entities. With T4 Text Templates or Codesmith Generator. Here we get another resx file: Entity.*.resx
Other application resources we put into third MyApp.*.resx
Keys
How should we ask the ResManager to get text for Save button that was put in Lib.*.resx file? It would be convenient to say
1 |
ResManager.Instance.GetResourceString("Save") |
Our ResManager, depending on current UI Culture, would search through the resource files and find appropriate resource in Lib.*.resx. But here we stumble upon two points:
1. Performance. It’s not good searching in all files
2. Ambiguity. You can’t be sure that one or another of developers put resource string with “Save” key into another resx file. Of course you can rely upon developer’s awareness and attention. But I’ve found more than 30 “Save” strings in “MyApp”.resx file in our application. Developers put them there for Save buttons in views they’ve created.
To solve these two problems we make our keys consist of several parts:
Prefix_Name_Suffix
Suffix can be complex and I will describe it in the next articels when will show complex cases. For most cases we are interesting in Prefix and Name. Prefix defines file(for file based storage) where resource is stored. Name is the name of resource. For Save button in Lib.*.resx file it would be stored with Key = “Lib_Save”.
For entities in Entity.*.resx file we have resources with keys Entity_Order_IssueDate, Entity_Order_Status, Entity_Customer_ShortName, Entity_Customer_Phone. For ID and Name it’s better to have special keys Entity_Base_Id, Entity_Base_Name as they are used in almost all entities.
But there are huge amount of resources for the MyApp application. How can we manage this heap? We can make our keys smarter:
Prefix_ViewName_Name_Suffix
Lets imagine button on OrderView.xaml with text “Close order”. A key for it will be:
1 |
<Button Content="{Res MyApp_OrderView_CloseOrder}"/> |
Default resource file
95% of text on the view is taken from MyApp.*.resx file. It’s not good for developer or designer to write “MyApp_…” on each TextBlock or Button. We could define and register one of resx files as default with no prefix. So 95% of resources would start without prefix but with View name.
XAML Markup Extension
How can we provide such short form as {Res …} for Button.Content or TextBlock.Text properties? Only through MarkupExtension:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[MarkupExtensionReturnType(typeof(object))] public class ResExtension : MarkupExtension, IServiceProvider { public override object ProvideValue(IServiceProvider serviceProvider) { // some work to get localized Value ... localizedValue = ResManager.Instance.GetResourceObject(...); ... return localizedValue; } ... } |
Layer between XAML and Manager
It works fine but has one flaw. ProvideValue() method returns localized object which wouldn’t change when current UI culture is changed. We need to make ProvideValue() more complex to return BindingExpression from it. This will allow us not only to react on culture changing but to react on key changing in case of dynamic keys(I’ll explain dynamic keys later in the next article).
I wouldn’t perplex reader with details how we can accomplish this task. You can look at ResExtension.ProvideValue() and ResConverter class in the source code
Make it shorter
Now we need to write namespace in each case:
1 |
<Button Content="{myLibNamespase:Res Lib_Save}"/> |
which may be tedious. To workaround this we can use the next trick. Create XmlnsDefinitions.cs file and write code:
1 2 3 |
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Common.Wpf.Res")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2007/xaml/presentation", "Common.Wpf.Res")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2008/xaml/presentation", "Common.Wpf.Res")] |
It’s not honest trick but successfully works with no side effects. Now we can write short form:
1 |
<Button Content="{Res Lib_Save}"/> |
Summary
In this article I described main parts of localization library and storage details that need just to put tranlated text to Button.Content. In the next article we consider other more complex localization cases: enums, dynamic keys, formatted strings and so on.
Hello sir. I am grateful for this solution you’ve posted. Do you know of a way to provide a resx file at runtime in order to add a new language at runtime? I’d like to drop a file in the folder and have it accessible as a new language. Thanks for any thoughts.
I didn’t look at the sample project close enough. Looks like you have a FileResManager that might be the answer.
From my research, doesn’t look possible.
Hi Leland,
I think this should work out of the box. You copy your files to folder and change current language:
CultureManager.Instance.UICulture = cultureInfo;
Thank you. I will try this and report back.
Me again 🙂 I’m trying to use a converter to make the text all UPPER CASE.
The object supplied to the converter is a ResExtension object, so I was able to check for that and manually call ResManager.Instance().GetResource(obj.Key);
This works at runtime, but at design time I get a NullReferenceException.
Have you successfully implemented a way to make the text all Upper Case?
Although this works at runtime, when changing languages, this approach doesn’t work. Hoping you have solved this another way.
Hey, I tried to use it but all the place where I use {Res } it will show “#stringName” at run time. It’s not getting the string… Can someone help me? thanks.