Contents
Introduction
One of the things I've recently been working on is building multilingual support into WPF applications, in order to improve usability for a wider audience who may not understand English. There were a few goals I wanted to achieve in doing this:
- Have one version of the application cater for multiple languages. This means no separate English version, French version, Japanese version, etc. Many electronic products produced (such as TVs and digital cameras) support multiple languages from the same model. You don't have to buy a slightly different model or apply firmware patches to have a different language to what is installed by default.
- Allow easy switching of the interface language at runtime. No need to close the application and configure the culture of the Operating System, let alone having to rerun the installer.
- Choose the correct language on first use. When the application is run for the first time, it sets the interface language to the system language specified by the Operating System. This makes sense - French users would want to install, run, and use the software right away; not try to navigate an unfamiliar application to change the language before being able to use it.
- Allow the UI to be scalable to fit the translated text, minimizing the likelihood of clipping text.
Additionally, the implementation of this should not make it harder to develop with as the user interface grows. (This one I learned the hard way.)
So this article serves to outline the details of the solution I developed to achieve these goals, and is based on a few blog posts I wrote accordingly (here, here, and here). Along the way, I'll point out the relevant parts of the sample to give you an idea of how it all fits together.
Disclaimer: The text in the sample was translated using automated online services. Despite best efforts to ensure that this was as accurate as possible (by reverse-translation checking), there may be some inaccuracies or errors in the translated content. Especially when it uses a completely different writing system that I do not understand.
High-Level Overview
This implementation is designed for WPF applications following a Model-View-ViewModel (MVVM) pattern. The language data is stored in embedded XML files, which are loaded into memory on an as-needed basis - which is when the interface language changes. This forms part of the Model.
The ViewModel exposes a property containing the language data for the current language to the entire WPF application. It is the collection of XAML files forming the View that contains bindings to this language data. To select the exact value needed for a particular text element, each binding makes use of a custom value converter with a converter parameter to perform the lookup of the text key. Finally, a custom markup extension is used to abstract away the details of the binding so that only the key (i.e., converter parameter) needs to be specified.
The Sample
To illustrate how this implementation works in practice, I have created a small sample application around this functionality. Dubbed 'RePaver', this application tidies up Path markup expressions, with the added functionality of performing basic rotations, flipping, translation, and scaling on the actual geometry (i.e., without requiring layout transforms). Behind the scenes, it uses Regular Expressions to extract the path segments and perform an in-place transformation on each segment.
To give you an idea, consider the following example of a path expression you might typically get from exporting vector graphics as XAML (which is nothing compared to some of the paths I've encountered on projects at work!):
<Path Data="M 470.567,400.914 L 470.578,
390.903 L466.551,390.863 L 472.6,384.876 L472.598,400.888 Z" ... />
If you copy and paste the Data expression (from inside the quotes) into the Input box and click 'Go', you should get the following output:
M 4,16 L 4,6 L 0,6 L 6,0 L 6,16 Z
You'll also get a handy visualization on the right, with a 'before' and 'after'.
Feel free to play with the settings - you'll notice that the operations are performed in the order Rotation/Flip -> Scale To [bounding box size] -> Offset. And of course, feel free to try it out in a different language.
The Model
XML
As mentioned, 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 parent element of each piece of text contains a key attribute that is used as a lookup to retrieve the localised text. Here's an example of the contents of the English definition file, LangEN.xml:
<LangSettings>
<IsRtl>0</IsRtl>
<MinFontSize>11</MinFontSize>
<HeadingFontSize>16</HeadingFontSize>
<UIText>
-->
<Entry key="TransformLabel">Transform</Entry>
<Entry key="LanguageLabel">Language</Entry>
-->
<Entry key="ApplyLabel">Apply</Entry>
<Entry key="UndoLabel">Undo</Entry>
<Entry key="CancelLabel">Cancel</Entry>
-->
<Entry key="InputLabel">Input</Entry>
<Entry key="OutputLabel">Output</Entry>
<Entry key="InfoLabel">Info</Entry>
<Entry key="TransformPropertiesLabel">Transform</Entry>
-->
<Entry key="FlipRotateLabel">Flip / Rotate</Entry>
<Entry key="OffsetLabel">Offset</Entry>
<Entry key="ScaleToLabel">Scale To</Entry>
<Entry key="DimensionsLabel">Dimensions</Entry>
<Entry key="WidthLabel">Width</Entry>
<Entry key="HeightLabel">Height</Entry>
<Entry key="GoLabel">Go</Entry>
</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:
<LangNames>
<Language code="en">English</Language>
<Language code="ar">العربية</Language>
<Language code="de">Deutsch</Language>
<Language code="el">Ελληνικά</Language>
<Language code="es">Español</Language>
<Language code="fr">Français</Language>
<Language code="he">עברית</Language>
<Language code="hi">हिन्दी</Language>
<Language code="it">Italiano</Language>
<Language code="jp">日本語</Language>
<Language code="ko">한국어</Language>
<Language code="ru">Русский</Language>
<Language code="sv">Svenska</Language>
</LangNames>
Note that the name of each language definition file is named following a convention, 'LangXX.xml', where XX corresponds to the two-letter ISO language code that is also used for each Language
element in LanguageNames.xml. This convention may be extended or adapted to handle locales (e.g., en-NZ, en-US), or even three-letter ISO language codes.
The UILanguageDefn Class
The data in the language definition file for the active interface language is loaded into an internal class, UILanguageDefn
, for consumption by the rest of the application. The main component of this is a Dictionary<string, string>
that contains mappings from the text key to a localised text value. Other properties expose the IsRtl
, MinFontSize
, and HeadingFontSize
values.
When this class is used, the localisd language text is retrieved by calling the following method:
public string GetTextValue(string key)
{
if (_uiText.ContainsKey(key))
return _uiText[key];
return "";
}
Apart from this, the UILanguageDefn
class also contains a static mapping of language codes to localised language names as loaded from LanguageNames.xml, for example "en" to "English" and "sv" to "Svenska". This is used to populate the list of available languages in the 'Language' tab, and is filtered by another list - the list of authoritative languages supported by the application. Thus, any language not in this list won't be shown in the UI, even if there is a language definition file and entry in LanguageNames.xml. This is described further in the following section.
Loading the Data
The UILanguageDefn
class only forms one part of the Model. The second major entity in the Model is the application-wide state, MainWindowModel
. This contains the authoritative instance of the UILanguageDefn
being used by the entire application. It is this instance that gets bound to the text elements in the entire UI (via the ViewModel).
When MainWindowModel
is being constructed, it first registers the authoritative list of languages and loads the localised language names from the LanguageNames.xml resource file, before loading the current language. Here's a simplified look at how this works:
public class MainWindowModel
{
private UILanguageDefn _languageMapping;
public MainWindowModel(int maxWidth, int maxHeight)
{
RegisterLanguages();
LoadLanguageList();
UpdateLanguageData();
}
public string CurrentLanguageCode
{
get
{
}
}
private void RegisterLanguages()
{
string[] supportedLanguageCodes =
{
"en", "ar", "de", "el",
"es", "fr", "ko", "hi",
"it", "he", "jp", "ru", "sv"
};
foreach(string languageCode in supportedLanguageCodes)
UILanguageDefn.RegisterSupportedLanguage(languageCode);
}
private void LoadLanguageList()
{
string resourcePath = "RePaverModel.LanguageData.LanguageNames.xml";
System.IO.Stream file =
Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath);
XmlDocument languageNames = new XmlDocument();
languageNames.Load(file);
UILanguageDefn.LoadLanguageNames(languageNames.DocumentElement);
}
public bool UpdateLanguageData()
{
string languageCode = CurrentLanguageCode;
if (String.IsNullOrEmpty(languageCode)) return false;
string resourcePath =
String.Format(Constants.LanguageDefnPathTemplate, languageCode.ToUpper());
System.IO.Stream file =
Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath);
XmlDocument languageData = new XmlDocument();
languageData.Load(file);
_languageMapping = new UILanguageDefn();
_languageMapping.LoadLanguageData(languageData.DocumentElement);
return true;
}
}
You may notice that the above code mentions a third major entity - the settings state. It is this state that stores the current interface language being used, among other settings that may be set at runtime. Nearly 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 settings 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. This is what it looks like in code, found inside the Settings model hierarchy:
public LanguageSettings()
{
_uiLanguageCode = Constants.DefaultLanguageCode;
string languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
if (UILanguageDefn.AllSupportedLanguageCodes.Contains(languageCode))
_uiLanguageCode = languageCode;
}
Another method in this class, which may be called later (if there are saved user settings), overwrites _uiLanguageCode
with the value extracted from the saved settings file.
The ViewModel
Here's where an MVVM implementation differs from Model-View-Presenter (MVP) in WPF and Silverlight applications. In MVP, we'd have a Presenter that would pass the current language definition (or the individual localised pieces of text) to the View, which would take care of displaying / updating the text in the UI. Given that we're using WPF, this text is most easily updated via data binding; and given that the language definition would be used across the entire application (i.e., in component controls or windows), we'd need a shared class to store the 'current language' property so that it can be retrieved by any part of the UI when data binding takes place.
In MVVM, such a shared class would form part of the ViewModel layer alongside the other ViewModels (such as MainWindowViewModel
). This class, CommonViewModel
, is implemented as a Singleton so that the static instance property (Current
) can be assigned as the Source
property of a Binding
. The non-static properties are therefore referenced via the binding's Path
property. Another crucial detail is that the ViewModel implements the INotifyPropertyChanged
interface so that the UI can automatically update the bindings when the source value is changed.
Here are the CommonViewModel
properties bound to by the UI, given the data defined in the UILanguageDefn
class:
public UILanguageDefn LanguageDefn
{
get { return _languageDefn; }
set
{
if (_languageDefn != value)
{
_languageDefn = value;
OnPropertyChanged("LanguageDefn");
OnPropertyChanged("HeadingFontSize");
OnPropertyChanged("MinFontSize");
OnPropertyChanged("IsRightToLeft");
}
}
}
public double HeadingFontSize
{
get
{
if (_languageDefn != null)
return (double)_languageDefn.HeadingFontSize;
return (double)UILanguageDefn.DefaultHeadingFontSize;
}
}
public double MinFontSize
{
get
{
if (_languageDefn != null)
return (double)_languageDefn.MinFontSize;
return (double)UILanguageDefn.DefaultMinFontSize;
}
}
public bool IsRightToLeft
{
get
{
if (_languageDefn != null)
return _languageDefn.IsRightToLeft;
return false;
}
}
The MainWindowViewModel
, being the topmost in the ViewModel hierarchy, is responsible for updating the current language in the CommonViewModel
when the value changes in the MainWindowModel
:
public void RefreshUILanguage()
{
_model.UpdateLanguageData();
CommonViewModel.Current.LanguageDefn = _model.CurrentLanguage;
if (LanguageChanged != null)
LanguageChanged(this, new EventArgs());
}
The View
As I've been mentioning, the localised text is displayed in the View via data binding. However, WPF by itself doesn't know what to do with a UILanguageDefn
, let alone how to extract the proper localised text value. This is where the final part of the puzzle comes in.
The Value Converter
Remember that CommonViewModel.Current.LanguageDefn
is a UILanguageDefn
, not a string
as expected by the Text
property of a TextBlock
, 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 from the UILanguageDefn
instance. Remember, it is the UILanguageDefn
that changes when the interface is changed.
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 the element name matches. No additional properties need to be defined in between - whether in the ViewModel, UILanguageDefn
, or other parts of the Model.
The converter itself is relatively straightforward. All it needs to do is implement IValueConverter
(in 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) return "";
return defn.GetTextValue(key);
}
The Binding
Now that we've got a value converter, we can put it all together in a Binding
expression:
<TextBlock Text="{Binding Path=LanguageDefn,
Converter={StaticResource UIText}, ConverterParameter=ApplyLabel,
Source={x:Static vm:CommonViewModel.Current}}" />
For this to work, the XML namespace has to be defined for vm
(pointing to the ViewModel's namespace), and the UIText
resource needs to be defined (assuming conv
is the XML namespace for the value converter):
zlt;conv:UITextLookupConverter x:Key="UIText" />
Short and Sweet - The Custom Markup Extension
If you went on your merry way with this in its current state (like I did), you'll find that it becomes tedious repeating the same, long Binding expression in most of your XAML files. And don't even think about renaming the classes or properties as part of a refactoring!
Of course, there's a way to make this much more succinct, given that the only thing that changes between these bindings is the ConverterParameter
. The solution I've chosen is using custom markup extensions.
To do this, the custom markup extension is simply a class that derives from MarkupExtension
(in System.Windows.Markup
), and is named according to the convention [name]Extension
. At its heart, the main thing that needs to be done is override the ProvideValue
method. But what should this do?
The whole point of this custom markup extension is to be able to write something like this in XAML:
<TextBlock Text="{ext:LocalisedText Key=ApplyLabel}" />
Therefore, the custom extension is called LocalisedTextExtension
, and a public string
property called Key
is added. Because a Binding
is still being used behind-the-scenes, I've created a private Binding
field and instantiated it in the constructor as follows:
public LocalisedTextExtension()
{
_lookupBinding = UITextLookupConverter.CreateBinding("");
}
where the static CreateBinding
method is defined in the value converter as:
public static Binding CreateBinding(string key)
{
Binding languageBinding = new Binding("LanguageDefn")
{
Source = CommonViewModel.Current,
Converter = _sharedConverter,
ConverterParameter = key,
};
return languageBinding;
}
So with the Binding
defined, the Key
property simply gets and sets the ConverterParameter
. That leaves the ProvideValue
method to do its magic:
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _lookupBinding.ProvideValue(serviceProvider);
}
Remember that a Binding
is a MarkupExtension
, so it has its own ProvideValue
method that we can take advantage of.
Rinse and Repeat - Font Sizes and Flow Direction
For some languages, the character sets contain glyphs so complex that they would be hard to read at a font size that may be acceptable for our Latin-based alphabet. You'll notice that the CommonViewModel
exposes the HeadingFontSize
and MinFontSize
properties. These provide the font sizes respectively for localised headings and all remaining localised text, and would have larger values for Japanese than English, for example.
Fortunately, these can be bound to from shared styles (defined in application-wide resource dictionaries) following a similar pattern, but without the need of a value converter:
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="{Binding Path=MinFontSize,
Source={x:Static vm:CommonViewModel.Current}}" />
<!-- Remaining setters ... -->
</Style>
Here's a side-by-side to show the difference:
There are also languages that read from right-to-left, such as Arabic and Hebrew. To properly localise the UI to these languages, it would make sense to reverse the interface, otherwise it would be somewhat confusing to try to use the application if the logical order isn't consistent with the reading order:
Fortunately, WPF has a handy (attached) property to do the hard work of reversing the entire UI: FrameworkElement.FlowDirection
.
What makes this quite powerful is that I only need one binding at the root-level control contained inside the main window, since this value is inherited by every FrameworkElement
below it in the visual hierarchy. This binding only needs to look at the IsRightToLeft
property of the CommonViewModel
and convert that (via another value converter) to a FlowDirection
enumeration. A custom markup extension was created, following a similar pattern to before, to simplify the XAML to this:
<Window x:Class="RePaver.UI.MainWindow" ... >
<DockPanel FlowDirection="{ext:LocalisedFlowDirection}">
-->
</DockPanel>
</Window>
Given how powerful this is, here are some points / gotchas to consider:
- Custom panels are automatically laid out in reverse, so you do not need to create an
IsReversed
property (or similar) and adjust your ArrangeOverride
calculations accordingly.
- Bitmap images and shapes (e.g., Paths) are reversed. If you want to preserve the rendering of these, independent of flow direction (e.g., for corporate logos / branding), then you need to override the
FlowDirection
by setting it to LeftToRight
.
- If the interface has a
FlowDirection
of RightToLeft
and an element (e.g., Image) has a FlowDirection
of LeftToRight
, then the Margin
on the element will act in a RightToLeft
manner. Since a Padding
acts on the internal visual hierarchy of the element, a padding will behave in a LeftToRight
manner.
- TextBoxes containing language-invariant data should have the
FlowDirection
set to LeftToRight
. Ideally, this should be set in a style to minimise repetition and guarantee consistency.
So here's the obligatory 'after' screenshot:
Note that the paths, rotation selection control, and Input / Output text boxes are rendered in a left-to-right manner regardless of the language. This is because these elements are specific to the problem domain, where it wouldn't make sense (and would cause some confusion) if they were displayed in a right-to-left manner.
Conclusion
So there you have it - a localised WPF application that can change the UI language dynamically at runtime. Run it for the first time on a local computer in France and voila, il est affiché en Français. All from the same language edition.
One final point to note, which isn't covered in detail, is that the entire UI is laid out in a fluid manner so that the layout is automatically adjusted to fit the content. Rather than explicitly setting widths and heights on controls, grid row/column definitions, etc., these are left as 'automatic' while minimum and maximum sizes may be defined. This is one of the more general best practices (rather than being specific to localisation), but not following this practice really shows up when switching language.
PostScript
It should come as a little surprise that localisation is a hot topic in the world of software development, so naturally, I wouldn't be the only person writing about it. As it turns out, I've since discovered that a few others have done similar things:
- Sebastian Przybylski (article) has also stored the UI text in XML files as embedded resources, however the XAML binds directly to the XML resources instead of a ViewModel.
- David Sleeckx (article) used a custom markup extension to either retrieve the translated text from a local cache, or call the Google Language API to perform the translation in real time.
- 'SeriousM' has uploaded the WPF Localization Extension to CodePlex. This implementation uses custom markup extensions to extract the localised text (and other values) from resource files / satellite resource assemblies.
Clearly, there are now a few options on how to localise WPF applications - and they aren't mutually exclusive. Depending on your priorities, some parts of my implementation would suit particular areas of your application, while other parts of an alternative implementation would be suitable elsewhere. Feel free to tailor these to meet your needs.
History
- 05 August, 2009: Initial version.