WPF Localization

recently at work I was asked to look into Localization techniques when working with WPF/XAML. There are some excellent sources around that cover the various different techniques such as

this excellent article which outlines the following techniques

Using Locbaml

Locbaml is a localization tool that Microsoft have as a free download, available from http://msdn.microsoft.com/en-us/library/ms771568.aspx. This tool may be used to translate compiled XAML (binary XAML, BAML) resources into a CSV and may be used to translate a CSV file into a new Dll.

Here are the steps involved with doing this

1. Create normal WPF App, and use a ResourceDictionary for strings

2. Use MergedDictionary in App.resources

3. Add [assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] in AssemblyInfo.cs

4. Add new <UICulture>en-US</UICulture> in all ProjectGroup tags for current project

5. Open command prompt at project level, run msbuild /t:updateuid <PROJECT NAME>.csproj

6. Open command prompt at project level, run msbuild /t:checkuid <PROJECT NAME>.csproj

7. Build it from VS

8. Obtain 1st cultures CSV file, LocBaml.exe /parse en-US/<PROJECT NAME>.resources.dll /out:<PROJECT NAME>.csv

9. Edit CSV save as new csv file name, such as <PROJECT NAME><nn-XX CULTURE STRING>.csv

10. Need to do translation for new culture. Do something like

11. LocBaml.exe /generate en-US/HelloWorld.resources.dll /trans :HelloWorldfr-CA.csv /out:c: /cul:fr-CA

12. Create new <nn-XX CULTURE STRING> directory in Bin folder

13. Copy <PROJECT NAME>.resources.dll from step 10, which was placed on C: to the folder created in step 11

14. Create a constructor for App class like follows:

public App()

{

string culture = “fr-CA”; //Where this is the culture you want to use

System.Threading.Thread.CurrentThread.CurrentUICulture =

new System.Globalization.CultureInfo(culture);

System.Threading.Thread.CurrentThread.CurrentCulture =

new System.Globalization.CultureInfo(culture);

}

Advantages Of This Method

  • You can do it at the end of the project
  • You don’t have to interfere with your main Assembly to install new cultures

Disadvantages Of This Method

· It is laborious and error prone

· Not well suited for teams of developers as a new CSV file is created each time you use the LocBaml tool, so you need to know what was different between newly exported CSV file and the translation CSV file

Using RESX Files

An alternative approach is to not use Locbaml at all and simply use RESX (resource files).

Here are the steps involved with doing this

  1. Create normal WPF App
  2. There will already be a resx file within the Properties folder, so simply add a string value to that for the text you want to localize. Make sure to select public from Access Modifier drop down
  3. Within any Window that you want to use localized text, add a clr-namespace such as
  4. xmlns:properties=”clr-namespace:HelloWorldUsingResXFiles.Properties”
  5. Hook up a items text to a resource file text, like
  6. <Button Margin=”10″ Name=”button1″
  7. Content=”{x:Static properties:Resources.MainButtonText}”></Button>
  8. Copy and paste the existing resx file within the Properties folder to a new file with the following name
  9. Resources.<nn-XX CULTURE STRING>.resx
  10. Modify the App.cs file to include whatever culture should be used, for example

public App()

{

HelloWorldUsingResXFiles.Properties.Resources.Culture = new CultureInfo(“fr-CA”);

}

Advantages Of This Method

  • Very easy to implement

Disadvantages Of This Method

· All RESX files must be stored in Main Assembly

· As all cultural resource files are in Main Assembly, may make doing automated build harder

Another Way : Use Dynamically Loaded Assemblies With ResourceDictionaries

I just couldn’t help but think there was a better way, so I had a think about this, and it seems just like the way skins are applied in WPF. You wire certain control properties to DynamicResources where a new XAML file (the skin) is loaded which effects the control that is referencing the resource. So I had a think about it and thought why not do the same for Localization strings. So that’s what I did. Here is the idea

Keep a Main Assembly which expects to get localization resources from a ResourceDictionary, and then at runtime load in an extra Assembly which contains just the ResourceDictionaries for the matching CurrentCulture where the application is running.

The loaded Assemblies ResourceDictionaries will be used to replace the default one within the main Assembly.

1. Create normal WPF App, and use a ResourceDictionary for strings

2. Use MergedDictionary in App.resources

3. Make sure any localizable controls using a DynamicResource like

<Label Content=”{DynamicResource label1}”/>

4. Create a new culture assembly with a sensible name, such as Culture_fr-CA

5. For assembly created in step 4, create mirror image ResourceDictionary that matches the original Assemblies ResourceDictionary. but with translated strings

6. Compile the culture assembly to some folder under main Assemblies bindebug folder. This demo assumes this folder is called “CultureFiles”

7. When main app runs, get current culture and Load the matching Assembly from disk

8. From the currently loaded Culture Assembly, extract the ResourceDictionary

9. Using the extracted ResourceDictionary, replace the current Application.MergedResources dictionary with the newly extracted culture ResourceDictionary

10. All controls that refer to the Dynamic resources should be ok now with new cultural strings

In my main App I simply have the following ResourceDictionary

   1:  <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   2:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   3:      xmlns:system="clr-namespace:System;assembly=mscorlib">
   4:      <system:String x:Key="label1">label1</system:String>
   5:      <system:String x:Key="label2">label2</system:String>
   6:      <system:String x:Key="label3">label3</system:String>
   7:      <system:String x:Key="label4">label4</system:String>
   8:      <system:String x:Key="label5">label5</system:String>
   9:  </ResourceDictionary>

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Then in the App file I have the following

   1:  <Application x:Class="LooseXAML.App"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      StartupUri="Window1.xaml">
   5:  
   6:      <Application.Resources>
   7:          <ResourceDictionary>
   8:              <ResourceDictionary.MergedDictionaries>
   9:                  <ResourceDictionary Source="Dictionary_en-US.xaml" />
  10:              </ResourceDictionary.MergedDictionaries>
  11:          </ResourceDictionary>
  12:      </Application.Resources>
  13:  
  14:  </Application>

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Then somewhere I may reference these resources like

   1:  <Label Content="{DynamicResource label1}"/>

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

So that is all pretty standard stuff. Next I created a new Assembly with a single ResourceDictionary in it. I am using the culture string as part of the Assembly name and or the actual ResourceDictionary

image

The actual ResourceDictionary MUST contain the name items as the main Assembly

   1:  <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   2:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   3:      xmlns:system="clr-namespace:System;assembly=mscorlib">
   4:      <system:String x:Key="label1">french label1</system:String>
   5:      <system:String x:Key="label2">french label2</system:String>
   6:      <system:String x:Key="label3">french label3</system:String>
   7:      <system:String x:Key="label4">french label4</system:String>
   8:      <system:String x:Key="label5">french label5</system:String>
   9:  </ResourceDictionary>

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

From here I compile this assembly and put it somewhere visible from the main Assembly, such as bindebugCultureFiles

image

From there is just a question of loading the seperate culture Assemblies and extracting the contained ResourceDictionary and replacing the current Main Assemblies ResourceDictionary. Thus forcing all resourced referencing controls to update with the new resource data.

There is a small demo application that demonstrated this approach here which you may test by clicking on the the fr-CA listbox item

Before

image

After

image

But you MUST ensure that your current culture is set to French-CANADA via control panel for it to work. This means change region Options AND languages. If you don’t do this the CurrentCulture will not be correct for the demo code to work.

image

Advantages Of This Method

  • Very easy to implement

Disadvantages Of This Method

· Lots of different Assemblies to manage

· Culture Assemblies ResourceDictionary entries must match Main Assembly otherwise a lookup may fail at runtime

About these ads

35 thoughts on “WPF Localization

  1. Hey Sacha

    Great outline of available techniques! I’m expecting nothing less from you than the final fourth approach that provides *the* ultimate solution. Preferably by the end of the week ;)

    Joking aside: Being a citizen of a country (Switzerland) in which four languages are spoken and English not being one of them, localization is an inevitable subject. The WinForms approach with satellite assemblies was ok, and I must say I’m rather disappointed that WPF complicated things rather than improving them. Let’s hope the next VS will simplify the process…

  2. sacha says:

    Philipp

    Localization in WPF at the moment is crap, lets hope it improves. LocBaml is aweful approach.

  3. Sven says:

    Hi Sacha, it’s great to see the options for WPF laid out. At my job we’re developing a winforms app (actually it’s a winforms/wpf hybrid now) and we’ve developed our own little system for localization. It’s simple but it does the job for us.

    We replace all English captions/titles/messages in the UI with their translated counterparts at run-time. The translation is easily done with the help of a singleton “Translations” object. This approach has the following advantages:
    * you’re not tied to Windows’ current culture
    * you’re not dependent on the Visual Studio’s crappy localization infrastructure. It sucks bigtime when trying to keep all cultures synchronized or when releasing new versions.
    * you get to choose the file format for the “dictionary” files. We went with simple (unicode !) text files that contain 2 columns (english key & translated text). This way your human translators could even use notepad for their work. However, we wrote a small winforms app that allows to do their work and it provides some help in keeping dictionaries synchronized.
    * human readable (English) texts in your source code and in your winforms(/wpf ?) designer . I haven’t gotten around to investigating a mechanism for the WPF screens yet. It’ll probably turn out to be a bit more involved because of the Xaml/code-behind duality.
    * you translate every piece of text only once since it all ends up in a dictionary. Moreover, we wrote a small add-in for VS that allows our programmers to check whether a string already exists in the master dictionary. If it doesn’t the developer can add it from within VS then and there.

    There are some disadvantages too:
    * no hot-swapping between cultures while the program is running. As soon as a piece of UI has been translated the original English key texts have been replaced and you can’t re-translate into another language. Changing the culture means having to restart the program if you want actually want to see the effects of the change.
    * you need to write some code that does the replacing at run-time. However, you only need to do it once.

    As I wrote, I still need to investigate how far we can take this approach with WPF. Ideally, we would only have to call a method in the Loaded event that translates all controls but given WPF’s nature that’s probably not going to happen. The UI changes around a lot because of DataTemplates and Styles et cetera.

    If you’ve any ideas on the matter, I’m all ears BTW :-)

    Sven

  4. sacha says:

    Sven

    I had also thought about this approach also, but my team leader didnt like the idea of loose files he wants tight VS integration. But I like this appraoch also.

    What I would say, is that if it works for you, then just use what you have.

    Thanks for the very detailed message though, great stuff

  5. Hi Sacha,

    I have been following you excellent series on WPF over at codeproject and have been so encouraged that I downloaded the Blend 2.5 preview release to continue my own development.

    Like you I am in the UK, but was horrified to find out how much more we are paying for these microsoft tools here in the UK.

    58% more to buy expression studio here, after removing VAT and converting at the current exchange rate.

    See this artice for a full breakdown of the costs.

  6. sacha says:

    Hey Ambercat glad you like my WPF stuff.

    As I am a Microsoft MVP I get all of their products for free, so I dont have to care about the costs.

    Sorry to tell you that, but its of course good news for me

  7. Bjarke Mortensen says:

    Hi Sacha,
    A small change to your example application removes the dependency on the current locale:
    private void LoadAssemblyAndGetResourceCultureFile(FileInfo assemblyNameForCulture)
    {
    Assembly ass = Assembly.LoadFile(assemblyNameForCulture.FullName);
    string assName = assemblyNameForCulture.Name.Substring(0,
    assemblyNameForCulture.Name.LastIndexOf(“.”));

    string localeName = assName.Substring(assName.IndexOf(“_”) + 1);

    Uri cultureDictionaryUri =
    GetResourceFromDll(assName,
    string.Format(“Dictionary_{0}.xaml”, localeName));

    // Tell the Application to load the culture resources.
    App app = Application.Current as App;
    app.ApplyCultureDictionary(cultureDictionaryUri);
    }

    But how to handle dynamically created controls?
    I’ve tried something like this:
    Button dynbtn = new Button();
    dynPanel.Children.Add(dynbtn);
    dynbtn.Content = FindResource(“label3″);

    which loads the correct string, but the content is not updated when the language is changed.

    How can I do the equivalent of {DynamicResource label3} in code?

    Hope you can help me.

    Kind regards,
    /Bjarke

  8. sacha says:

    As for the dynamically created control, Yeah that is a limitation of this approach with DynamicResources.

    Im going to ask about on that one, and see if I can get a better idea of a good answer.

    Thanks for the other tip

  9. sacha says:

    Yeah I am mates with Josh and asked him, and he just sent me that URL. So cool glad you got an answer

  10. What about using a third party software once you get all your “uid” set correctly into your xaml file.

    So far I’m looking at some commercial solution to face this issue we already have in place a team of translator and we are tring to move from having the developer to update the text to having the translator to create their own localized satellite assemby.

    Anyone as some experience about this we are currenlty looking at “Lingobit” and “Sisuliser 2008″.

    Cheers,
    Eric.

  11. sacha says:

    InTagger, that looks very cool.

    I will look further if we have to use Localization on any new projects.

    Cool work

  12. klauswiesel says:

    Sacha

    I managed to get your solution for localization to run in my VB based project. Everything is fine except for the column-headers of the ctp-datagrid from MS
    They are initialized corrctly from the local resource, but won’t get changed after the assembly load

    Maybe it is an issue like the button that was loaded at runtime? Unfortunately, I do not see a way to call .SetResourceReference on the column

    Any assists?
    Regards
    Klaus

  13. klauswiesel says:

    Sacha

    another issue: are there any performance problems when using many controls whose content / text property is bound using dynamicresource??

    Regards
    Klaus

  14. sacha says:

    I have not tried this with the CTP of the datagrid, so could not say. The only thing that may be happening is that they column headers are overwriting these values in the metadata, you could try and use Reflector and have a look, look within the static constructors. This is normally where the metadata is altered.

  15. sacha says:

    another issue: are there any performance problems when using many controls whose content / text property is bound using dynamicresource??

    Answer : Dynamic resources do consume more memory, as they take more tracking, but I am not aware of any limits as such.

  16. Ajax Smith says:

    Can you tell how we can know how the Properties.Resources.Culture is set to what at runtime and how to change.

  17. Ajax Smith says:

    Thanks Sacha,for your follow up reply.

    Well I guess I made some progress, the thing is that I have different DLLS (WPF Controls) for different modules of my WPF Application,so far I am just localizing for English and Arabic and we have at least 2 resource assemblies to say from which the strings are picked up from the folder and UI is using binding for display.This follows the article in some ways.

    http://www.codeproject.com/KB/WPF/WPFLocalize.aspx

    The things is that I had code like

    CultureResources.Init();
    CultureInfo selected_culture2 = new CultureInfo(“ar-KW”); CultureResources.ChangeCulture(selected_culture2); wpfWindow.FlowDirection = FlowDirection.RightToLeft;

    based on my setting in the configuration file for now.Well in one DLL the UI would default to arabic resources in english mode

    I added Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

    Since both were not coming correctly….

    The thing is not may examples that show how to handle localization if you have one app + multiple UI assemblies and how to achieve it….

  18. Swapna Gundlapally says:

    Its a very good article but i am facing an issue.

    The culture set at application level is being overriden by the new threads created in the application with the default culture(en-US).

    Can anyone help me how to set the culture at application level for the new threads created in the application.

  19. sacha says:

    Swapna

    Just do this in the App

    public App()

    {

    string culture = “fr-CA”; //Where this is the culture you want to use

    System.Threading.Thread.CurrentThread.CurrentUICulture =

    new System.Globalization.CultureInfo(culture);

    System.Threading.Thread.CurrentThread.CurrentCulture =

    new System.Globalization.CultureInfo(culture);

    }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s