Click here to Skip to main content
Click here to Skip to main content

WPF Runtime Localization

, 14 Feb 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
A WPF localization solution with runtime updating and design-time support in Visual Studio and Expression Blend

Update: This article has been rewritten after a helpful comment by Kingsley Moore below made the solution much, much simpler and easier to add to an application.

Design-time support in Expression Blend

Contents

Introduction

This article is yet another attempt to address the problem of localizing WPF applications in a simple and maintainable fashion.
In this case, I also wanted to be able to:

  • Switch cultures at runtime - automatically updating all localized elements
  • Use the existing Resource file structure (*.resx files), which can be maintained in Visual Studio
  • Keep design-time support for localized elements working in Expression Blend (and hopefully other XAML design applications)

The example application has a few localized strings to show it working, but since there's only so many ways to demonstrate localized strings, I've kept it brief.

The original implementation presented in this article required a wrapper class around the set of properties, which was a hassle to either manually or auto-generate. This update removes the need for this wrapper, making this much easier to use.

Background

There have been a number of other articles on this subject, including Localizing WPF Applications using Locbaml which covered different methods of localizing XAML files, each with their own advantages and disadvantages. The first approach in that article (Targeted Localization Without Using LocBaml.exe) got me thinking, but I had to diverge from that approach in order to trigger runtime auto-updating (for all elements, even on existing Windows).

Another earlier project is WPF Multi-Lingual at Runtime, which does provide a solution for runtime auto-updating, but to my mind introduces complexity in the management of the language resource files. By maintaining support for existing *.resx files in this article, we can still use existing applications to easily translate applications to new cultures.

I thoroughly recommend reading and understanding both these articles, there is a lot of information there, as well as useful tricks that have helped me a lot in other projects.

Using the Code

Automatic Updating with ObjectDataProvider

In order to get automatic updating when we change the current Culture, I am exploiting the characteristics of the ObjectDataProvider. The little gem of information from the MSDN page is:

"This class is also useful when you want to replace your current binding source object with another object and have all the associated bindings updated."

So all we need to do is replace (or refresh) the ObjectDataProvider object instance, and any bindings on the ODP properties will be updated automagically.

Here is where this solution has been improved. While it is possible to directly bind to the properties of the auto-generated RESX designer class (Resources.Designer.cs), we also need to get an instance of this class for the ODP. All ResXFileCodeGenerators (both default and custom) mark the Resources constructor as internal, meaning it can only be accessed from within the same assembly. (This also means the constructor cannot be accessed from XAML - i.e. using an ODP ObjectType.) To solve this, we can use the MethodName property on the ODP. (An alternative might be to extend an existing custom ResXFileCodeGenerator to mark the constructor public, but there is no need for that.)...

<ObjectDataProvider x:Key="Resources"
    ObjectType="{x:Type cultures:CultureResources}" 
    MethodName="GetResourceInstance"/>

... with the method:

public Properties.Resources GetResourceInstance()
{
  return new Properties.Resources();
}

Use of MethodName means that the ODP will become the object returned by the method, allowing us to bind an instance of the Resources class. We can create this instance because the call to the internal constructor above is coming from within the same assembly, not directly from XAML. Works like a charm.

One constraint for this to work is that the Resources class must be public, as we cannot return an instance of an internal class using a public method (this gives a lovely compiler error). This means we could use either the Extended Strongly Typed Resource Generator[^] which works in both Visual Studio 2005 & 2008, or the PublicResXFileCodeGenerator tool that ships with Visual Studio 2008. I like the Extended Code Generator as it generates super helpful string formatting methods as a bonus.

public static void ChangeCulture(CultureInfo culture)
{
  //remain on the current culture if the desired culture cannot be found
  // - otherwise it would revert to the default resources set,
  //   which may or may not be desired.
  if (pSupportedCultures.Contains(culture))
  {
    Properties.Resources.Culture = culture;
    ResourceProvider.Refresh();
  }
  else
    Debug.WriteLine("Culture [{0}] not available", culture);
}

Updating the current culture is quite simple, I have added a method to the CultureResources class that updates the current Resources Culture and triggers an update on the ObjectDataProvider, causing it to call the GetResourceInstance method, updating the ODP ObjectInstance, which refreshes any bindings on the ODP - which are updated to the new resource values.

Design-time Support

At design-time, Properties.Resources.Culture is initially set to the Neutral Language set in the project, or the Culture of the current thread if a Neutral Language has not been set. Either way, any bindings will default to the strings in the default Resources file (Resources.resx).

Adding Localized Strings

All strings that you want to localize need to be defined in all Resource files for localization to work, so it is generally easier to add more cultures after you have set everything up with the default Resources.resx file. Otherwise you would need to add each new string to all existing RESX files.

Add strings to resx files

We can then add a binding to the desired UI element:

<Label x:Name="labelCultureName"
    Content="{Binding Path=LabelCultureName, Source={StaticResource Resources}}"/>

And if we have added this resource string to the default Resources RESX file, after recompiling the project this default string value should now show up in the designer, and also of course when you run the application.

If you see the case that a string added for a Resources file other than the default seems to always show the default value, check that the resource string name in each RESX file is correct. If you have binding errors, then the Path set in the Binding does not match any strings in any of the RESX files, and it cannot even fall back to the default RESX value as in the previous case.

Adding More Cultures

Copy default Resource file to add new culture

A simple way to add another culture to your project is to Copy & Paste the default Resources.resx file in Visual Studio to create a new file. Select a suitable culture code from the list on the MSDN CultureInfo reference page. Add the culture code in the extension as in Resources.Fr-fr.resx, which Visual Studio will use to create the localized DLL when the application is built. Now that you have a new RESX file, you can change the resource values for the new culture and you're done.

Enumerating Available Cultures

Example using ComboBox to select from available cultures

With a number of cultures added to this project, the code used to enumerate those we have implemented can be demonstrated. I have made this dynamic to avoid any need to rebuild the application when a new culture is added. For an existing installed copy, you just need to create a folder with the new culture name and put the new correctly named resources DLL inside. Restart the application and it will be available (or if you are importing a culture from within the application, this method could be modified to search the install directory again).

Debug.WriteLine("Get Installed cultures:");
CultureInfo tCulture = new CultureInfo("");
foreach (string dir in Directory.GetDirectories(Application.StartupPath))
{
    try
    {
        //see if this directory corresponds to a valid culture name
        DirectoryInfo dirinfo = new DirectoryInfo(dir);
        tCulture = CultureInfo.GetCultureInfo(dirinfo.Name);

        //determine if a resources DLL exists in this directory that
        //matches the executable name
        if (dirinfo.GetFiles(Path.GetFileNameWithoutExtension
            (Application.ExecutablePath) + ".resources.dll").Length > 0)
        {
            pSupportedCultures.Add(tCulture);
            Debug.WriteLine(string.Format(" Found Culture: {0} [{1}]",
                tCulture.DisplayName, tCulture.Name));
        }
    }
     //ignore any ArgumentExceptions generated for non-culture
     //directories in the bin folder
    catch { }
}

The above is a relatively quick way of checking the applications bin directory for any folders that match Culture names. The CultureInfo.GetCultureInfo method will fail as desired if the string argument does not match any of the defined CultureInfo types.

Design-Time Solution for UserControls

A problem arose for me in the veiled form of the UserControl. If the properties that you want to localize are accessible external to the user control (added to the code-behind file as dependency properties), then there is no problem, you can localize them as described above. However, if the properties you want to localize are not externally accessible, such as a Label Content property, then the solution is a little trickier.

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="WPFLocalize.UserControl2"
    x:Name="UserControl">
    <Grid x:Name="LayoutRoot">
        <Label x:Name="labelUserControl2"
            Content="{Binding Path=LabelCultureName,
                    Source={StaticResource Resources}}"/>
    </Grid>
</UserControl>

When you add a binding to a label inside a UserControl as above, it will be rendered correctly at runtime, and also at design-time (e.g. in Blend) when it is loaded up by itself. Unfortunately it will fail to render correctly when you load up a window that contains the UserControl. (This seems to only be a problem in Expression Blend, the Visual Studio 2008 Designer will render correctly in this situation.)

I understand that the problem when loading a UserControl as a child of a Window is that the designer creates an instance of the control and then adds it to the Window. The Resources that would be available at runtime are not present because the instance is not created from within the window, and so the above binding fails and the control cannot be rendered. After many failures trying to work around this situation, I eventually came up with the following:

public UserControl2()
{
#if DEBUG
    //only perform the following fix if we are in the designer
    // - the default ctor is not executed when editing the usercontrol,
    //   but is executed when usercontrol has been added to a window/page
    // NB. The Visual Studio designer might return null for Application.Current
    //     http://msdn.microsoft.com/en-us/library/bb546934.aspx
    if (DesignerProperties.GetIsInDesignMode(this) && Application.Current != null)
    {
        Uri resourceLocater =
            new System.Uri("/WPFLocalize;component/ResourceDictionary1.xaml",
                UriKind.Relative);
        ResourceDictionary dictionary =
            (ResourceDictionary)Application.LoadComponent(resourceLocater);
        //add the resourcedictionary containing our Resources ODP to
        //App.Current (which is the Designer / Blend)
        if (!Application.Current.Resources.MergedDictionaries.Contains(dictionary))
            Application.Current.Resources.MergedDictionaries.Add(dictionary);
    }
#endif
    this.InitializeComponent();
}

Using the DesignerProperties.GetIsInDesignMode() means this code is only executed at design-time, and all it's doing is adding the ResourceDictionary that contains our Resources ObjectDataProvider to the designer itself, so that they will be available when the UserControl is initialized. This will actually be a second instance of the ODP, which would be bad at runtime (as only the first one included in App.xaml would be updated), but fine at design-time as we will not be updating the culture. Problem solved.

Limitations

In this example I am using WPF Bindings, which require Dependency Properties to bind to. There are other cases where you might want to access these properties, but adding a binding is not appropriate or easy to accomplish. An example of this is when you want to access a localized value direct from code. In order to keep auto-updating working in this case, you can hook an eventhandler on the ObjectDataProvider DataChanged event, which is triggered after we update the ODP. So, when values are re-fetched in the eventhandler the updated resource values are available. Alternatively you can just make sure you re-fetch the localized value after you know the ODP has been updated, there is little difference.

ResourceCultureProvider.DataChanged +=
    new EventHandler(ResourceCultureProvider_DataChanged);

void ResourceCultureProvider_DataChanged(object sender, EventArgs e)
{
    Debug.WriteLine
        ("ObjectDataProvider.DataChanged event. New culture [{0}]",
        CurrentResourceCulture.LabelCultureName));
}

History

  • January 2008 - Initial release
  • May 2008 - Rewritten, removing need for wrapper class
  • February 2010 - Updated links

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Andrew Wood
Software Developer siliconcoach ltd
New Zealand New Zealand
No Biography provided

Comments and Discussions

 
Questionhelpful PinmemberCIDev30-Aug-11 3:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141022.1 | Last Updated 14 Feb 2010
Article Copyright 2008 by Andrew Wood
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid