Building Multilingual WPF Applications

We've heard of the virtues of following the best practice of designing software to be as flexible as possible, namely ease of understanding and agility where maintenance is concerned. One dimension of this is localisation across multiple languages and cultures. That's one avenue I was wanting to explore in a WPF application that I've been working on.

From a usability point of view, I wanted the following to be true:

With that covered, the following are the aspects of the design I came up with to achieve these goals.

XML-based Language Definitions

Every single piece of text that forms the user interface is stored in localised form in an XML file for each language, with all XML files compiled as an embedded resource. The element name surrounding each piece of text determines what the text represents, i.e. it is used as a lookup to retrieve the localised text. Here's an example of the English definition, LangEN.xml:

<?xml version="1.0" encoding="utf-8" ?>
<LangSettings>
	<IsRtl>0</IsRtl>
	<MinFontSize>11</MinFontSize>
	<HeadingFontSize>16</HeadingFontSize>

	<UIText>
		<!-- Menu bar -->
		<SettingsLabel>Settings</SettingsLabel>

		<!-- Settings -->
		<AppBehaviourSectionLabel>Application Behaviour</AppBehaviourSectionLabel>
		<LanguageSectionLabel>Language</LanguageSectionLabel>

		<!-- Common Operations -->
		<ApplyLabel>Apply</ApplyLabel>
		<ApplyCurrentLabel>Apply Current</ApplyCurrentLabel>
		<ApplyAllLabel>Apply All</ApplyAllLabel>
		<UndoCurrentLabel>Undo Current</UndoCurrentLabel>
		<UndoAllLabel>Undo All</UndoAllLabel>
		<CancelLabel>Cancel</CancelLabel>

	</UIText>
</LangSettings>

In the English example above, also notice the IsRtl, MinFontSize and HeadingFontSize elements. The font sizes are used to determine what size to render the fonts for better legibility, especially when displaying text in Japanese, Korean and Arabic, for example. The IsRtl element indicates whether the language reads right-to-left (as is the case for Arabic and Hebrew).

Notice that the language name is not given in this XML. This is because the localised language names are defined in a separate XML file, LanguageNames.xml:

<?xml version="1.0" encoding="utf-8" ?>
<LangNames>
	<LangEN>English</LangEN>

	<LangAR>العربية</LangAR>
	<LangDE>Deutsch</LangDE>
	<LangEL>Ελληνικά</LangEL>
	<LangES>Español</LangES>
	<LangFR>Français</LangFR>
	<LangIT>Italiano</LangIT>
	<LangJP>日本語</LangJP>
	<LangKO>한국어</LangKO>
	<LangRU>Русский</LangRU>
	<LangSV>Svenska</LangSV>
</LangNames>

Also note that the element names correspond to the name of each language definition file. The decision to name the elements in the form 'LangXX', where XX denotes the two-letter ISO language name, is intentional.

UPDATE: I received a comment on the old blog advising that this XML structure isn't good practice. The main problem is that the schema itself would be subject to change as additional languages are included; furthermore, the language code requires custom extraction logic (using regular expressions) than leveraging XML parsing functionality provided by the framework.

Here's the recommended alternative:

<?xml version="1.0" encoding="utf-8" ?>
<LangNames>
    <LangName code="en">English</LangName>

	<LangName code="ar">العربية</LangName>
	<LangName code="de">Deutsch</LangName>
	<LangName code="el">Ελληνικά</LangName>
	<LangName code="es">Español</LangName>
	<LangName code="fr">Français</LangName>
	<LangName code="it">Italiano</LangName>
	<LangName code="jp">日本語</LangName>
	<LangName code="ko">한국어</LangName>
	<LangName code="ru">Русский</LangName>
	<LangName code="sv">Svenska</LangName>
</LangNames>

Internal State

Inside the application there are a number of entities forming the state relating to interface language. Firstly, the language information is stored in a UILanguageDefinition class, which contains the contents of the LangXX XML file that has been loaded. The XML elements inside are stored in a Dictionary, for efficient lookup of the actual text. Since only one language would be displayed in the application (except for the language selection list), only one instance of this would be loaded at once to help keep the memory footprint small.

The second major entity is the application-wide state, forming part of the 'M' in MVP. This contains the authoritative instance of the UILanguageDefinition being used by the entire application. It is this instance that gets bound to the text elements in the entire UI.

The application-wide state also contains a third major entity - the settings state. It is this state that stores the current interface language being used, as well as a reference to the list of languages to display in the Languages tab of the Settings panel. With the exception of this list, all settings are persisted to disk when the application is closed, and reloaded when the application is next opened. However, if the application is being loaded for the first time (and no settings file exists), these have to be reset to defaults.

For languages, it would be easiest to make English the default, but this isn't user-friendly. Instead, the current system language is queried by retrieving:

CultureInfo.CurrentCulture.TwoLetterISOLanguageName;

and looking up the corresponding language. If the language isn't supported by the application, English is then used as the default. This way, as long as your native language is supported, the UI will be displayed in your native language when you first run the application.

The UI

So far, this implementation has been agnostic of whether WPF is being used (as opposed to WinForms, etc.), but there are clear advantages to using WPF based on how easy it is to change the entire interface language through the use of data binding.

Essentially, data binding allows the entire interface to change as the result of changing one reference - that of the underlying UILanguageDefinition being used. This is reference is stored on the main application window, MainWindow, as a DependencyProperty. To support data binding, a DependencyProperty should be used instead of an ordinary property. In this example, the property is named "currentLanguage".

Whenever text is being set in XAML, e.g: "Apply All", the binding expression given is:

<TextBlock Text="{Binding Path=currentLanguage, Converter={StaticResource uiText},
    ConverterParameter=ApplyAllLabel,
    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type uiCt:MainWindow}}}" />

Breaking this expression down, 'currentLanguage' is the DependencyProperty of the same name in MainWindow. To successfully retrieve it, the RelativeSource needs to be specified - in this case it uses the FindAncestor mode to access the MainWindow. These attributes are constant throughout the entire application.

Remember that 'currentLanguage' is a UILanguageDefinition, not a string as expected by the Text property, therefore a value converter is needed to do this transformation. This converter uses a ConverterParameter to specify the lookup key to retrieve the corresponding localised piece of text (which is stored in a Dictionary). The beauty of this is that for every new piece of text to localise in the interface, the corresponding element needs to be added to the language XML files, making sure that the ConverterParameter and element name matches. No additional properties need to be defined in between - whether in MainWindow or UILanguageDefinition.

The converter itself is relatively straightforward. All it needs to do is implement IValueConverter (System.Windows.Data), specify the ValueConversion attribute at the class level:

[ValueConversion(typeof(UILanguageDefn), typeof(string))]

and implement the Convert function as follows:

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
	string key = parameter as string;
	UILanguageDefn defn = value as UILanguageDefn;

	if (defn == null || key == null || !defn.text.ContainsKey(key))
		return "";

	return defn.text[key];
}

Tidying up, the converter is declared as a shared resource with key "uiText", and the appropriate XML namespaces are defined at the root node of the applicable XAML documents.

Putting it all together, here are the results:

Notice how the UI elements dynamically resize to fit the content - this is achieved by following the best practice of not specifying explicit widths and heights of elements. Instead, appropriate spacing is achieved through the Margin and Padding properties. The layout system used in WPF allows the desired size of elements in the visual hierarchy to determine the final size, including where grid columns and rows have their width/height property set to 'Auto'.

So where to from here? At the moment we have localised text being displayed in the UI, and the font sizes defined in the language XML files are being honoured by similar binding expressions in the TextBlock styles (achieved via the FindAncestor mode to retrieve 'currentLanguage.minFontSize', etc. from the MainWindow - hence no need to use a value converter). The only thing that remains is to reverse the entire interface for Right-to-Left languages, so that the logical progression of the interface reflects the reading order.