Sunday 27 November 2011

My WPF Localization Solution

The world awaits
I ran into a need to have my WPF application support multiple languages, to prepare it for its big day day when it finally becomes the hottest product in China or at least in Burkina-Faso.

There are plenty of localization solutions out there. I've been searching for the optimal way to make my application multilingual, and being the lazy developer I am, by "optimal" I usually mean optimal for me (i.e. the developer):
  • I want all the texts to change to the current selected culture automatically.
  • I want to retrieve additional resources, such as images, for a specific culture. 
  • I want to be able to add new multilingual resources to my application without effort.
  • I want all multilingual support and resources to be reusable in a separate assembly.
Article Level:
Not quite rocket science

Showing off
First, to show some abilities:
Click to see it in action
Some features which can be seen in this screen capture:
  • The following texts in the window are bound to localized resources and they change automatically when the culture is changed:
    • The title of the window itself.
    • The 1st text block in the window ("Hello, world!").
    • The 2nd text block in the window ("Text in English").
    • The 3rd text block in the window ("This year, 2011").
    • The text on the button.
    • The text in the message-box, which pops when the button is clicked.
    • The menu item "Language", 
  • The menu under "Language" automatically lists the languages for which there are available resources.
  • The flag on each menu item is also retrieved automatically, if it's available.
The foundations
I've split my solution into 2 projects:
Solution structure
Localization Demo - is the sample WPF application with a single XAML window.

Localization - is the external assembly (class library) which holds the entire functionality to make everything multilingual. It contains all the string resources and the useful static CultureResources class (see breakdown below).

In this example there is 1 resource file named MainApp.resx, which contains the default strings in American English (en-US), and corresponding translation- MainApp.bg-BG.resx which contains the strings in Bulgarian (bg-BG) and MainApp.he-IL.resx which contains the strings in Hebrew (he-IL).

New resources for the application should be easily added and placed in the Resources folder, split into the cultures they support. That is all. No additional actions are required.

Resources in different languages should have matching resource names, and since all the resources are in a separate assembly, they should be declared as public. Notice this in the "Access Modifier" drop down below.
The default English resource file MainApp.resx

The Bulgarian resource file MainApp.bg-BG.resx

The Hebrew resource file MainApp.he-IL.resx
I've also included a resource file which contains the flags for each of the supported languages as 16x16 px images. Note that there is a convention for the flags' names which correspond to its matching culture.
This resource's Access Modifier is also set as Public.
The flags file Flags.resx
Anatomy of the CultureResources static class

Lazy loading: Mapping the available resources

No, you jokers! I don't mean "Lazy loading" as in getting just the data you need. I mean "Lazy loading" as in be lazy and let the programme load and handle its manifested resources on its own.
If resources are added/removed, this process will auto-update them. Hurray for laziness!

So this will be done dynamically via reflection. In order not to repeat the costly process of reflection more than once, I keep a dictionary of the resources as a private field which is populated once, and kept cached statically in memory:
private static Dictionary<stringobject> localizationResources = new Dictionary<stringobject>();

Those resources are mapped and accessed via the matching public static property:
/// <summary>
/// Gets the resources manifested by this assembly.
/// </summary>
public static Dictionary<stringobject> LocalizationResources
{
    get
    {
        if (CultureResources.localizationResources.Keys.Count == 0)
        {
            string[] resourcesInThisAssembly = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceNames();
            foreach (string resource in resourcesInThisAssembly)
            {
                string res = resource.Substring(0, resource.LastIndexOf("."));
                string resKey = res.Substring(res.LastIndexOf(".") + 1);
                if (!CultureResources.localizationResources.ContainsKey(res))
                {
                    Type t = Type.GetType(res);
                    object resourceInstance = t.GetConstructor(
                            BindingFlags.NonPublic | BindingFlags.Instance,
                            null,
                            Type.EmptyTypes, null)
                                .Invoke(new object[] { });
                    CultureResources.localizationResources.Add(resKey, resourceInstance);
                }
            }
        }
        return localizationResources;
    }
}
The keys of the dictionary are the names of the resources (in this case will contain 2, named: MainApp and Flags). The values are the resources themselves.

I can also retrieve a specific resource by its name, by this rather simple method:
/// <summary>
/// Gets a resource by name, corresponding to one of the resources in the Localization assembly.
/// </summary>
/// <param name="resname">The resname.</param>
/// <returns></returns>
public static object GetResource(string resname)
{
    if (LocalizationResources.ContainsKey(resname))
        return LocalizationResources[resname];
    return null;
}

The background workers: Associating each resource with an object data provider
The data providers do the whole trick of automatic language switching. Each of the resources is associated with a data provider of type ObjectDataProvider.
When the culture of the UI is changed, each of these providers is refreshed, and the resources automatically reload, getting the strings in the newly set culture.
In an approach similar to the one used to map the resources, I'm keeping those providers in their own static dictionary:
private static Dictionary<stringObjectDataProvider> resourceDataProviders = new Dictionary<stringObjectDataProvider>();

A method PopulateDataProviders is used to construct each of the data providers.
Again, the resources are scanned through reflection, and a dynamic ObjectDataProvider is created to correspond with each of them.

Another important statement to note in this piece of code, is the addition of the provider to Application.Current.Resources. This will allow our executing assembly (the main WPF executable, not the Localization class library where this code resides) to access the resources directly, as if they were added to it.
/// <summary>
/// Instantiates the object data providers associated with the resources manifested by this assembly,
/// Pupulates the the resources and data providers dictionaries,
/// Adds all the resources to the current application resources (which is why it should be called on OnStartup of App.xaml.cs
/// </summary>
public static void PopulateDataProviders()
{
    if (CultureResources.resourceDataProviders.Keys.Count == 0)
    {
        string[] resourcesInThisAssembly = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceNames();
        foreach (string resource in resourcesInThisAssembly)
        {
            string res = resource.Substring(0, resource.LastIndexOf("."));
            string resKey = res.Substring(res.LastIndexOf(".") + 1);
            if (!CultureResources.resourceDataProviders.ContainsKey(res))
            {
                ObjectDataProvider prov = null;
                try
                {
                    if (Application.Current.Resources.Contains(resKey))
                        prov = (ObjectDataProvider)Application.Current.FindResource(resKey);
                    else
                    {
                        prov = new ObjectDataProvider() {
                          ObjectInstance = Localization.CultureResources.GetResource(resKey) 
                        };
                        Application.Current.Resources.Add(resKey, prov);
                    }
                }
                catch
                {
                    prov = null;
                }
                CultureResources.resourceDataProviders.Add(resKey, prov);
            }
        }
    }
}

The ObjectDataProviders are also accessible via a public property, which first makes sure the dictionary is initialized:
/// <summary>
/// Gets the object data providers associated with the resources manifested by this assembly.
/// </summary>
public static Dictionary<stringObjectDataProvider> ResourceDataProviders
{
    get
    {
        CultureResources.PopulateDataProviders();
        return resourceDataProviders;
    }
}

A method which verifies a specific ObjectDataProvider (for a specific resource) is instantiated is also added and will be used momentarily. The importance of this method is to make sure the resources and their data providers are accessible and instantiated, in case they had been addressed too early.
/// <summary>
/// Instantiates a data provider associated wth a static resource 
/// (once the provider is refreshed, the bound strings are reloaded with the current UI language).
/// </summary>
/// <param name="resource">The name of resource to associate with the provider.</param>
/// <returns>The instantiated data provider.</returns>
private static ObjectDataProvider InstantiateDataProvider(string resource)
{
    try
    {
        if (ResourceDataProviders.ContainsKey(resource))
        {
            if (ResourceDataProviders[resource] == null)
                ResourceDataProviders[resource] = (ObjectDataProvider)Application.Current.FindResource(resource);
        }
    }
    catch
    {
        return null;
    }
    return ResourceDataProviders[resource];
}

Where the magic happens: Changing the current UI culture
Switching among the different available cultures is the main idea here, so first of all I'm keeping track of the current UI culture with this property:
/// <summary>
/// Gets or sets the current UI culture name.
/// </summary>
/// <value>
/// The current UI culture name.
/// </value>
public static string CurrentCulture
{
    get;
    set;
}

Now changing to a new culture is as easy as calling this method:
/// <summary>
/// Changes the UI culture.
/// </summary>
/// <param name="culture">The new UI culture.</param>
public static void ChangeCulture(CultureInfo culture)
{
    CurrentCulture = culture.Name;
    System.Threading.Thread.CurrentThread.CurrentUICulture = culture;
 
    for (int i = 0; i < ResourceDataProviders.Keys.Count; i++)
    {
        ObjectDataProvider prov = InstantiateDataProvider(ResourceDataProviders.ElementAt(i).Key);
        if (prov != null)
            prov.Refresh();
    }
}

As you can see, it changes the current UI culture of the current thread, then goes through the ObjectDataProviders in the dictionary, verifies every one of them is instantiated, then the Refresh method is invoked. That's where the magic happens.

Bonus feature I: Listing the available cultures (Lazy loading II)
It may be important for the running application to know which cultures have valid translations. For example, in this demo application I construct a menu which lists them.
Again, being the lazy loader I am, I do not want to do that myself. I'll let my app figure it out on its own.

For each culture, a folder will be created at build time, which will contain a satellite assembly with the same name of our resources assembly ("Localization" in our case) and a postfix of resources.dll.
Contents of the application's output folder and subfolders
In order to determine which cultures are supported by our application, this feature checks file system for folder containing *.resource.dll file
Since I don't need it to run more than once, I cache the results in memory :
private static List<CultureInfo> availableCultures = new List<CultureInfo>();

and get populate it on the public property with the help of some LINQ:
/// <summary>
/// Gets the list of cultures valid for the resources manifested by this assembly.
/// </summary>
public static List<CultureInfo> AvailableCultures
{
    get
    {
        if (availableCultures.Count == 0)
        {
            if (localizationResources.Count > 0)
            {
                availableCultures.Add(new CultureInfo("en-US"));
            }
            string resourceFileName = Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location) 
                        + ".resources.dll";
            DirectoryInfo rootDir = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
            availableCultures.AddRange((from culture in CultureInfo.GetCultures(CultureTypes.AllCultures)
                                        join folder in rootDir.GetDirectories() on culture.IetfLanguageTag equals folder.Name
                                        where folder.GetFiles(resourceFileName).Any()
                                        select culture));
        }
        return availableCultures;
    }
}


Bonus feature II: Retrieving a flag image for a specific culture, if available
As can be seen above, I'm holding some flags in a resource file, flags.resx. Of course I could have just used them as regular image files as regular or embedded resources.
Since this project is all about dynamic resources, and management of cultures, I wanted my assembly to be able to get me a flag automatically, if the resource holds it.
Since the resource files create dynamic types, the flags should be retrieved as properties, then I cast them to bitmaps:
/// <summary>
/// Gets a 16x16 icon image of a flag by a specific culture (if stored in the currnet resources)
/// </summary>
/// <param name="cultureName">Name of the culture (e.g. "en-US").</param>
/// <returns>A 16x16 icon image of a flag by a specific culture (if stored in the currnet resources).</returns>
public static System.Drawing.Bitmap GetFlag(string cultureName)
{
    string flagName = "Flag_" + cultureName.Replace("-""_");
    PropertyInfo flagProperty = typeof(Localization.Resources.Flags).GetProperty(flagName);
    if (flagProperty == null)
        return null;
    else
        return (System.Drawing.Bitmap)flagProperty.GetValue(nullnull);
}

Bonus feature III: Retrieving a string from a resource (in the current culture)
Similar to the method for getting flags, I can also retrieve a specific string from a resource. The string will be retrieved in the current UI culture. This is how:
public static string GetString(string resourceName, string key)
{
    string str = null;
    object resource = GetResource(resourceName);
    if (resource != null)
    {
        PropertyInfo resStr = resource.GetType().GetProperty(key);
        if (resStr != null)
        {
            str = System.Convert.ToString(resStr.GetValue(nullnull));
        }
    }
    return str;
}

A secret nugget: Associating the resources with the application's executable
Just before it all comes together, in order to make the main WPF executable be aware of all the resources available in the Localization assembly, 2 steps need to be taken:
  • Localization assembly has to be added as reference to the main WPF executable, LocalizationDemo.
  • The following code needs to be placed in the WPF executable's App.XAML.cs file:
    using System.Windows;
     
    namespace LocalizationDemo
    {
        public partial class App : Application
        {
            protected override void OnStartup(StartupEventArgs e)
            {
                base.OnStartup(e);
                Localization.CultureResources.PopulateDataProviders();
            }
        }
    }
    It calls the PopulateDataProviders which initializes all the providers and attaches them as resources to the currently executing application (the exe assembly).
    Now all the resources should be accessible on the WPF application.
The really really easy part: Making it work for you
Now that everything is in its place, all we have to do is bind our textual elements to the right resources.

Binding texts declaratively in the XAML 
This is how properties of text elements can be bound to dynamic texts directly in the XAML. They will change according to the current culture automatically.
For example, I've placed a TextBlock (the first one which reads "Hello, world!"):
<!-- This TextBlock is bound directly from the XAML -->
<TextBlock Text="{Binding Path=HelloWorld, Source={StaticResource MainApp}}" Margin="10"/>

Same for the "Languages" menu item:
<Menu Name="mainMenu">
    <MenuItem Name="menuOptionsLanguage">
        <MenuItem.Header>
            <TextBlock Text="{Binding Path=LanguageMenuItem, Source={StaticResource MainApp}}"/>
        </MenuItem.Header>
    </MenuItem>
</Menu>

Same for the button text:
<Button Click="Button_Click" Width="100" Margin="10">
    <TextBlock Text="{Binding Path=ButtonText, Source={StaticResource MainApp}}"/>
</Button>

And same for the window title itself:
<Window x:Class="LocalizationDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="256" Width="393" WindowStartupLocation="CenterScreen"
        Title="{Binding Path=HelloWorld, Source={StaticResource MainApp}}">
    <!-- Content... -->
</Window>

Binding texts programmatically
The 2nd text block, which reads "Text in English" is bound to a dynamic resource as well, but its binding is performed in code. This is how:
this.codeBoundTextBlock.SetBinding(
    TextBlock.TextProperty, 
    new Binding() 
    { 
        Source = CultureResources.ResourceDataProviders["MainApp"], 
        Path = new PropertyPath("LoclizedText") 
    }
);

Binding texts with formatted strings
The 3rd text block, which reads "This year, 2011", consists of a formatted string with placeholders.  This means the binding here needs to be more dynamic, with an unknown number of arguments.
For that I've created a class named StringFormatConverter, in the Localization Assembly. It inherits from IMultiValueConverter, so I can use regular binding of text resources with multiple arguments. This is its content:
public class StringFormatConverter : IMultiValueConverter
{
    public object Convert(object[] values, System.Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Count() == 0)
            return string.Empty;
        string str = values[0].ToString();
 
        if (values.Count() == 1)
            return str;
 
        List<object> paramsList = values.ToList();
        paramsList.RemoveAt(0);
        return string.Format(str, paramsList.ToArray());
    }
 
    public object[] ConvertBack(object value, System.Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new System.NotImplementedException();
    }
 
    public static StringFormatConverter Converter = new StringFormatConverter();
}
It constructs a formatted string based on the bound values, and it has a static instance of itself, for ease of use.
And here is how it is used in the MainWindow.XAML.cs:
this.codeBoundTextBlockWithArgs.SetBinding(TextBlock.TextProperty, new MultiBinding() 
{
    Converter = StringFormatConverter.Converter,
    Bindings = 
    { 
        new Binding() 
        { 
            Source = CultureResources.ResourceDataProviders["MainApp"], 
            Path = new PropertyPath("FormattedString") 
        },
        new Binding() 
        { 
            Source = DateTime.Now.Year.ToString() 
        }
    }
});

Manually getting a string
Clicking the button pops up a message box, which shows a text in the current UI culture. It's as simple as retrieving the string, and showing it:
private void Button_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show(CultureResources.GetString("MainApp""HelloWorld"));
}  

 Download the project

4 comments:

  1. Hi, Alon! If you're interested in a reliable localization tool to help you manage software localization projects, I recommend you this software translation app: https://poeditor.com/

    ReplyDelete
  2. German is a language that is made up of lots of complicated terms, beginnings as well as contextual foundations to translate story. Somebody that just attempts to match verbatim would certainly not have the ability to justify the size and also touches of either language.

    ReplyDelete