Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Localisation made easy for WPF

0.00/5 (No votes)
23 Jan 2013 1  
Localize your WPF application in a snap, and allow others to translate it easily.

Introduction

I was looking for a way to localize my application in a way that would be simple to handle, both for the developer and for the translator, and since I couldn't find any, I found this very handy solution that I would like to share with you. 

My goals were: 

  • Application localisation. 
  • Dynamic application language change (at run time). 
  • Simple to add/modify a language for the developer or for the user (no compilation or such).
  • Design time support in Visual Studio / Blend in any language. 

And those goals are met... 

..but... an important remark: this library, so far, does not work with ClickOnce applications. Look at the "To ClickOnce Users : the folder issue" part of this article for details. I am open to comments to improve this.

In this article I explain how the library works, and how to use it. I also present a tool that allows to handle easily the localised strings for the developer or the user. 

I released a demo in both C# and VB, so no WPF lover is left behind.

Background

My solution uses Resource Dictionaries, which are, as expected, dictionaries: they associate a Key to an Object (for localisation purposes: a String). In WPF, those resource dictionaries can be used within several scopes: Application, Window, Control, and even at FrameWorkElement level, so almost anything can have its own resources.

A feature of the WPF ResourceDictionary is that it can contain other ResourceDictionarys, which allows to split the dictionary into multiple smaller ones. Those inner dictionaries are called MergedDictionarys.

The same key can be used in several merged RDs. In this case, the value returned for this key is the latest added.

In case the ResourceDictionary is changed, a notification is issued to all XAML controls using this key as a DynamicResource (controls using StaticResource remains unaffected).

So the idea is simple:

  1. Store all application strings for each language within a ResourceDictionary.
  2. Keep a fallback language RD in the application merged dictionary. 
  3. Only use dynamic reference to a resource string within the application.
  4. On language change requested, we add or remove a RD, ensuring that: The fallback RD is always loaded. - The latest RD is the language requested.  

So on language change all the strings will be updated by the standard WPF resource management. (Rq: Step 2 is done to avoid any bad surprise in case you forget to add a key in a foreign RD. In this case, the fallback language string will be shown.) 

Using the library

First step: Create the Resource Dictionaries

Create a resource dictionary (a standard WPF item) for each language, using the same keys.
Rq :  It is wise to use a suffix to prevent resource names from colliding: I use 'Str' as suffix.
For example, for English, with three strings having HelloStr, DemoModeStr, and RealModeStr as keys, the RD looks like : 

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <s:String  x:Key="HelloStr"      > Hello                  </s:String> 
        <s:String    x:Key="DemoModeStr" > Currently in demo mode </s:String> 
        <s:String    x:Key="RealModeStr" > Currently in real mode </s:String> 
</ResourceDictionnary>   

Same dictionary for French (only localised text changes, use copy paste to create other versions):

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:s="clr-namespace:System;assembly=mscorlib" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <s:String     x:Key="HelloStr"    > Bonjour                    </s:String> 
        <s:String     x:Key="DemoModeStr" > Actuellement en mode démo  </s:String> 
        <s:String     x:Key="RealModeStr" > Actuellement en mode réel  </s:String>
</ResourceDictionnary>

Now you must save those RDs in separate files with a naming convention, by using "_" and the two letter ISO of the language as a suffix. I used "InterfaceStrings" as a prefix (you can choose yours). So for instance, for English, French, and Spanish we will have the files: InterfaceStrings_en.xaml, InterfaceStrings_fr.xaml, InterfaceStrings_es.xaml

In Visual Studio : 

  1. Put all those RDs in the same folder (I used 'Languages'). 
  2. Set those RD properties 'Build Action' as 'Content', as it should be packed with your application. 
  3. Set 'Copy to Output Directory' to 'Copy always'.

Second step: Include one of this language dictionaries as a resource for your application 

Choose your fallback language: this will be the language you want to see in design time, and the language of your application when it launches (if you don't change language at startup).

Add the fallback dictionary inside your resources in the first place within the App/Window/Control you want to localize. For application resources, this will look like:

<Application (...) >
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!--This is the fallBack language resource 
                   dictionnary. it should always be in First position. --!>
                 <!--set EZLocalise.FallBackLanguage to the language used here-->
<ResourceDictionary Source="/Languages/InterfaceStrings_en.xaml"/>
                 <!-- ... Some Other Resource Dictionary for my Application ... -->
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>  

If you have other resources (like themes, styles, templates...) put them in another ResourceDictionary inside the MergedDictionaries after the fallback RD. Notice that he fallback dictionary will always be kept inside the application RD, so you can rest assured that all that worked at design time will at least display in your fallback language at run time. It also allows you to add some keys only in the fallback language during development, then add the other languages strings later-on.

Rq 1: In the EZLocalise constructor you set FallBackLanguage. You should always set its value to be the real fallback language, i.e., the language of the first ResourceDictionary of your application (in the above example, "en").

Rq2: To use another language at design time, change this first dictionary within your App resources, and update your EZLocalize constructor's FallBackLanguage.

Third step: Setup the library

Add a reference to EZLocalize.dll to your project. You now have access to the EZLocalizeNS namespace. Create a new EZLocalize to handle your resources. The arguments are:

  1. The resource to localize
  2. The fallback language 
  3. The folder where the application is launched; use null to auto-detect for non-ClickOnce applications. 
  4. The folder within the application folder where to find language files
  5. The base name for the localized files

So for application resources, the code is:

(C#):

MyEZLocalize = new EZLocalize(App.Current.Resources, "en", null, "Languages\\", "InterfaceStrings");

(VB):

MyEZLocalize = New EZLocalize(My.Application.Resources, "en", Nothing, "Languages/", "InterfaceStrings")

So now your project should look like:

  • Rq1: Attempting to localize a resource more than once will raise an exception.
  • Rq2: You can retrieve an existing EZLocalize for a given resource by using the static/shared GetInstanceForResource(ResourceDictionnary).
  • Rq3: CurrentLanguage, FallBackLanguage, CurrentLggPath are properties that provides the expected information. 
  • Rq4: The library can help you find available language files with getFileBaseNames, which gives you all the file base names within a folder ("InterfaceStrings") and LanguagesForFileBaseName will give you all languages for a base name (LanguagesForFileBaseName("InterfaceStrings") = { "en", "fr", "es"} ).
  • Rq5: use naming conventions if you localize many objects, and use Reflection.
    Expl : if you localize MyUserControl, use "MyUserControl" as file base name.
    If LanguagesForFileBaseName("MyUserControl") is empty then your control is not localized.  

Fourth step: Use the localised resources

Now each time you are using a string in your application :

  • you must always declare it in the FallBack RD
  • you can declare it very localised RD.
  • you must  no longer 'hard-code' your strings : see below how to retrieve a string. 

Rq : As mentioned above, a non-existing key in a 'foreign' (non-fallback) language will only result in the application displaying a fallback language string, so you can delay the translation while coding, and test at run time in a foreign language even if not every string is translated in this language.    

To use your strings, use standard WPF: 

In XAML, use a DynamicResource Markup extension:

<TextBlock Text="{DynamicResource HelloStr}" />

Rq: If you use StaticResource, only the fallback language will get used and XAML strings will not get updated if you dynamically change the language, unless you change the language during application startup (before Windows/controls gets loaded).   

In code,use standard WPF also : FindResource/TryFindResource, or SetResourceReference for dynamic use on controls. 

(C#):

string MyHelloString = (string)TryFindResource("HelloStr");
MyTextBlock.SetResourceReference(TextBlock.TextProperty, "HelloStr"); 

(VB): 

Dim HelloString As String = My.Application.TryFindResource("HelloStr")
MyTextBlock.SetResourceReference(TextBlock.TextProperty, "DynamicStr")

!! I used the application resource as an example, but you must use the resource dictionary that you localized to get your localized strings. For instance, in the code behind of a user control that you localised, use this.TryFindResource("MyStringName").   

!! If you are using both strings defined at application level and strings defined at a lower level, when seeking for a resource the search will start from the lower then walk up the visual tree until it finds a match. So a control won't find the Application strings unless the control is within this visual tree.   

If you want to change the text of an element which is not a Framework element (such as the header of a DataGridView column for instance) you cannot use SetResourceRefrence, you have to update it yourself: in this case handle the LanguageChanged event. 

(C#): 

EZLocalise.LanguageChanged += HandleLggChanged;  // to add handler
EZLocalise.LanguageChanged -= HandleLggChanged;  // to remove handler

private void HandleLggChanged (string newLgg) { /* do some stuff */ }

(VB): 

AddHandler EZLocalise.LanguageChanged, AddressOf HandleLanguageChanged  
RemoveHandler EZLocalise.LanguageChanged, AddressOf HandleLanguageChanged   

Private Sub HandleLanguageChanged(ByVal newLgge As String)
  ' do some stuff
End Sub

Changing the language at startup/ at run time

On application startup, the default language will be the fallback language you defined in the application resources.
 If you want to use the current OS language as the language on startup, you have to:

  • handle the startup event (in App.xaml/Application.xaml, define Startup="Application_Startup")
  • in the handler, change language using current culture:

(C#): 

string CurrentOSLgg = System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
EZLocalise.ChangeLanguage(CurrentOSLgg);  

(VB): 

Dim CurrentOSLgg=CultureInfo.CurrentCulture.TwoLetterISOLanguageName
EZLocalise.ChangeLanguage(CurrentOSLgg)

As mentioned earlier, if you update language before startup, StaticResource will use that language. So if you do not wish to change language dynamically, use StaticResource to slightly reduce overhead.

If you want to change language at any time, just call ChangeLanguage with the new language string ("en", "fr", ...). And all your strings will update. 

(C#): 

string res = MyEZLocalise.ChangeLanguage("fr");

(VB):

Dim res = MyEZLocalise.ChangeLanguage("fr")

If you want to check if a language was loaded successfully, just test if the returned string is the language requested (res == "fr"). If not, res is an error message in English (: "file not found" or "xaml file corrupted at line xxx").

To ClickOnce users: Folder issue  

If you are either: 

  1. Distributing your application by copying the Debug/ or Release/ folder
  2. Using a classical deployment project (generating setup.exe and .msi files)
  3. Using a third party installer

Then there is no concern for you. But, as mentioned earlier, the EZLocalize class won't work with ClickOnce applications, that you get if you use 'Publish' to provide your applications. The reason for that is that the ClickOnce applications are SandBoxed when they are launched, so there is no way to retrieve the launch folder, hence no way to get the full path for the XAML files containing the localised resource dictionaries.You can set the folder where to seek for languages in the library constructor, if you know where to find it . 

Details : I found no reliable way to have it work on both XP and Seven, even within MS documentation or StackOverflow posts... and if you look here: MSDN documentation, you will see at the end of the "ClickOnce and Windows Installer Comparison Table", that the "Application installation location"' is the "ClickOnce application cache". 

Using EZLocalize with larger projects

EZLocalize is very simple and small: Unless someone asks, I won't detail the code (provided in VB and C#). 

If you want to use EZLocalize within a large project, or if you support many languages, it will soon take too much time to add each key in all supported languages. I developed a small application, StringResourceEditor, that allows to add/remove/modify/export in CSV/import in CSV all the key value pairs, which is more convenient and avoids many errors. I added this app in the 'Tools' menu of Visual Studio 2010, so I can change any string at any time during development. I also gave this tool to my customers so that they can edit the interface strings, which in fact is useful even for small projects since most people don't know how to edit an XAML file. So I suggest you:

  • add StringResourceEditor.exe in your Languages folder. 
  • set its 'Build Action' to 'Content'. 
  • set 'Copy to Output Directory' to 'Copy Always'.

Then, to add it as a tool in VS2010 :

  • Go into 'Tools' menu, 'External Tools' sub menu,
  • Choose a title ('string edit'),
  • Command: $(ProjectDir)Languages\StringResourceEditor.exe,
  • Argument: admin,
  • Initial Directory: $(ProjectDir).

When called with "admin" as parameter, StringResourceEditor allows to add/remove a key (developer). Otherwise those choices are hidden (customer).

Screenshot for the small demo strings:

The demo

A small demo is provided with this article, in its executable format, as a C# project and also as a VB project. Feel free to play with the controls, you can even add a language file into "Languages" folder, (expl: InterfaceStrings_de.xaml) and this language will be available on the next application startup. Screenshots:

Rq1: To create a new localisation, go into the 'Languages' folder, copy the fallback version, and rename it (expl for Italian: copy InterfaceStrings_en.xaml to InterfaceStrings_it.xaml). Once you have the new file, you can use StringResourceEditor.  

Rq2: If you edit the strings directly with a text editor,  change the content, !not the keys!, And use an UTF16 compliant editor, otherwise you won't be able to save non-English text.  

Last words

Rq1: My only concern here was text but a similar scheme might be used for any type of resource (color, image, ...). expl : the flag image for each country. 

Rq2: You might want to use localised strings at Window/Control/DLL/... level for more modularity. I found it more relevant at application level, since the same words are used and again in all parts of an application.
You might even do both (application wide strings + component specific strings).

I hope this library can help you localize your application, tell me if it did!

History

  • January 8,   2103: First published.
  • January 23, 2013 :Updated StringResourceEditor (a non admin could delete a key)  + minor clarifications.  

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here