Internationalization in WPF
Introduction
Everyone has their own opinion on how a WPF application should be internationalized.
People have tried referencing resources directly using {x:Static}
, using tools to parse the compiled XAML for strings, and everything in between.
Each approach has advantages and disadvantages, but it’s hard to figure out what’s best for you.
In my case, I couldn’t find any existing solutions which fitted my needs well, so I did what I normally end up doing and wrote my own. I’m posting it here in case it fits anyone else’s use cases.
Terminology
Before we get too stuck in, let’s first define some terminology. The output of the translation effort will be a list of strings for each language. Each string consists of a key and a value, for example:
Key | Value |
---|---|
MainView_HelloWorld | Hello, World |
In our view, we’ll ask the system to display the value corresponding to the key MainView_HelloWorld
.
If the current culture is English, it will display “Hello, World”.
Values can also contain placeholders, into which parameters are substituted, for example:
Key | Value |
---|---|
MainView_HelloUser | Hello, {0} |
In our view, we’ll ask the system to display the value corresponding to the key MainView_HelloUser
, using “Bob” as the first parameter.
This time it will display “Hello, Bob”.
Requirements
I needed my system to be able to do the following:
- Accept the key to use directly in the view, or through a binding from the ViewModel
- Substitute one or more parameters into placeholders through bindings from the ViewModel
- Display an appropriate placeholder if the resource can’t be found
- Support all translations from a single build of the application (a restart is acceptable to change the current language)
The requirement to bind the key to use from the ViewModel meant that LocBaml-esque tools were out of the window, while the requirement to substitute placeholders through bindings meant that referencing resource strings directly with {x:Static}
wasn’t an option either.
My Solution
I ended up writing a little trio of a markup extension, a number of converters, and a static Localizer
, which between them provide the functionality I was after.
I’ll show how they’re used, before giving the source and a sample project.
At the heart of everything is a static class called Localizer
, which is ultimately responsible for turning a key + some parameters into a value to display.
It has the following methods:
Localizer.Format("Hello, {0}", "Bob"); // => "Hello, Bob"
Localizer.Translate("MainView_HelloUser", "Bob"); // => "Hello, Bob"
Localizer.Format
is just a wrapper around String.Format
, specifying the correct culture.
You can also extend it handle more advanced placeholders to allow things like different plural forms, which we’ll discuss later.
It’s also useful if you want to format a string for display outside of a View (to display in a tray icon balloon message, for example): Localizer.Format(Resources.MainView_HelloUser, "Bob")
.
Localizer.Translate
takes a key, and looks it up in the correct resource file for the current culture.
If it finds a value, it runs it through Localizer.Format
.
If it doesn’t, it creates an appropriate placeholder (e.g. “!MainView_HelloUser:Bob!“) and returns that instead.
The real magic, though, happens inside the markup extension, which I called LocExtension
.
To simply display, “Hello, World”:
<TextBlock Text="{l:Loc MainView_HelloWorld}"/>
If you’re got a ViewModel with the following property:
public class ViewModel
{
public string UserName { get; private set; } = "Bob";
}
Then you can display “Hello, Bob” using:
<TextBlock Text="{l:Loc MainView_HelloUser, ParamBinding={Binding UserName}}"/>
If you add a converter to the ParamBinding
, that will be preserved.
Binding to multiple parameters is also supported. Assuming you have the resource file string:
Key | Value |
---|---|
MainView_LoggedInUser | Hello, {0}. You are logged in as {1} |
And the ViewModel:
public class ViewModel
{
public string UserName { get; private set; } = "Bob";
public string AccessLevel { get; private set; } = "Admin";
}
Then you can display the text “Hello Bob, you are logged in as Admin” using (the slightly wordy):
<TextBlock>
<TextBlock.Text>
<l:LocExtension Key="MainView_LoggedInUser">
<l:LocExtension.ParamBindings>
<MultiBinding>
<Binding Path="UserName"/>
<Binding Path="AccessLevel"/>
</MultiBinding>
</l:LocExtension.ParamBindings>
</l:LocExtension>
</TextBlock.Text>
</TextBlocK>
Again, any converters on any of the bindings will be preserved.
You can also bind the key from the ViewModel, for example:
public class ViewModel
{
public string Key { get; private set; } = "MainView_HelloWorld";
}
<TextBlock Text="{l:Loc KeyBinding={Binding Key}}"/>
(Converters are again preserved).
Of course you can use KeyBinding
with both ParamBinding
and ParamBindings
, as demonstrated above.
Handling Lists and Plurals
Handling plurals and lists in a locale-sensitive way is hard.
I strongly recommend SmartFormat.NET, which allows you to write things like You have {0:plural:1 new message|{} new messages}
(where the correct plural form is chosen based on per-locale rules).
It can also handle lists of items, and much more.
Simply replace the String.Format
with Smart.Format
in Localizer.cs
below.
The Source
I’ve created a downloadable demo project which shows off this tool’s full capabilities.
I’ve also reproduced it inline below.