Internationalization in .NET and WPF

Building a localized application and ensuring that the correct language is used in its GUI (a process also called internationalization or i18n) has always been quite simple in .NET. Resx resource files natively support multiple localized versions (which are built into "satellite" dll assemblies) and automaticaly determine the correct resources to read from (eventually falling back to the base language).

This is done by creating a simple resource file containing all strings of your application (for instance, Strings.resx) and then define a localized version of the same strings in separate resource files (Strings.it.resx for the italian strings, Strings.de-CH.resx for the swiss-german dialect version... etc.).

String resources and localized versions.

Visual Studio will create a static Strings class (defined in Strings.Designer.cs) that you can use to retrieve each single string (this also allows you to retrieve the localized strings in a strongly typed fashion, ensuring that you won't have null strings and exceptions thrown around randomly in your code).

Localizing Windows Forms interfaces

When building an interface with Windows Forms in Visual Studio, the whole procedure is pretty much handled by the designer. On the properties of each of your forms, switch the "Localizable" property to true. This will instruct VS to generate a resource file for your form and load all strings from there, instead of embedding them inside the code that builds the interface.

Localizing a Form.

The strings written and displayed in the designer will now be stored in the resource file that matches the selected language in the property pane. For instance, if you're editing your "MyForm" in MyForm.cs, you'll end up with files like MyForm.resx, MyForm.it-IT.resx, and so on. The correct strings will then be loaded by .NET via the same procedure as above, looking up the CurrentUICulture of your UI thread and falling back to the default language if necessary.

Localizing with Windows Presentation Foundation

Unfortunately, internationalizing WPF applications is a bit more tiresome and requires more manual plumbing. There are two possible scenarios I encountered, that can be solved in two different ways. Both are still based on retrieving localized strings directly from the embedded .resx resources.

Localizing XAML attributes

This scenario is the more common one: suppose you have a control that is presenting a static string in a way that can be formalized as a XAML attribute. For instance:

<TextBlock>This text must be localized.</TextBlock>
<TextBlock Text="This text must be localized." />

In the example, the first TextBlock can be rewritten as the second one. Now, we need to provide an extension that will localize the strings for us. WPF exposes the MarkupExtension class that can be used very easily in this scenario:

[MarkupExtensionReturnType(typeof(string), typeof(string))]
public class TranslateExtension : MarkupExtension {

	string _key;

	public TranslateExtension(string key) {
		_key = key;
	}

	const string NotFoundError = "#StringNotFound#";

	public override object ProvideValue(IServiceProvider serviceProvider) {
		if(string.IsNullOrEmpty(_key))
			return NotFoundError;

		return Strings.ResourceManager.GetString(_key) ?? NotFoundError;
	}

}

And that's how you use the extension in your XAML:

<Window x:Class="MyWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:root="clr-namespace:MyNamespace">
    <Grid>
        <TextBlock Text="{root:Translate StringKey}" />
	</Grid>
</Window>

The code above will instantiate the TranslateExtension for your textbox and request the value at runtime. The extension class will then retrieve the correct localized string in your resource file (using the resource's ResourceManager.GetString() method and the resource key set in the XAML file).

Notice that if GetString() returns null we return a default error string instead. This is useful to spot reference errors (WPF will merrily display a null value as an empty string, which might be easily overlooked when testing the application).

Errors in Visual Studio 2010

Unfortunately there seems to be a bug in the VS 2010 beta: in the screenshot above, the XAML editor complains about TranslateExtension not having a constructor with one parameter, but the code compiles and works perfectly.

Localizing XAML source

But perhaps you might want to localize bigger chunks of your XAML source code. In my case, I was trying to put some localized text on the screen as a complexely formatted FlowDocument: of course this can't be localized using the simple method above.

The solution I used, which isn't very pretty but works nonetheless, consists of putting the localized XAML source code in your resources and the load it using the XamlReader class at runtime, injecting the result into your GUI. A sample in code:

private void ShowLocalizedDocument() {
	var document = XamlReader.Parse(Strings.MyDocument) as FlowDocument;
	if (document == null)
		return;

    //Display the document on a document viewer
	_flowDocumentViewer.Document = document;
}

Where the resource string corresponding to "MyDocument" can potentially be any XAML FlowDocument. Thanks to the power of WPF, you can parse essentially everything and put it wherever you please:

public MyWindow(){
    MyButton.Content = XamlReader.Parse(Strings.MyButtonContents);
    MyGrid.Children.Add(XamlReader.Parse(Strings.MyGridTitle));
    //...
}

On the downside, if you build your UI at runtime like this you'll have to run your application in order to preview what the interface will look like and you are pretty much dumping the WPF designer (I usually never use it anyway and prefer working on XAML directly, eventually using KaXaml if needed). On the other hand again, you can store your XAML GUI code almost everywhere: resource files are an easy pick, but you could potentially retrieve your UI from a database, a web service, a file, etc...

Note: the XAML code that is parsed by XamlReader.Parse() must include the correct XML namespace references. A simple button for instance must be formatted like this:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">Click me!</Button>

Again, building up your entire interface at runtime might also get you very sluggish performance. I didn't test it with more than a couple of elements and had no problems. Let me know if you try...  :)