Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building Multilingual WPF Applications

0.00/5 (No votes)
2 Jul 2014 2  
An outline of how to build multilingual support into your WPF applications, where the language may be dynamically changed at runtime.

A selection of languages in the one application at runtime

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>
        <!-- Menu bar -->
        <Entry key="TransformLabel">Transform</Entry>
        <Entry key="LanguageLabel">Language</Entry>

        <!-- Common Operations -->
        <Entry key="ApplyLabel">Apply</Entry>
        <Entry key="UndoLabel">Undo</Entry>
        <Entry key="CancelLabel">Cancel</Entry>

        <!-- Section Headings -->
        <Entry key="InputLabel">Input</Entry>
        <Entry key="OutputLabel">Output</Entry>
        <Entry key="InfoLabel">Info</Entry>
        <Entry key="TransformPropertiesLabel">Transform</Entry>

        <!-- Item Labels -->
        <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">&Epsilon;&lambda;&lambda;&eta;&nu;&iota;&kappa;ά</Language>
    <Language code="es">Espa&ntilde;ol</Language>
    <Language code="fr">Fran&ccedil;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:

/// <summary>
/// Gets the localised text value for the given key.
/// </summary>
/// <param name="key">The key of the localised text to retrieve.</param>
/// <returns>The localised text if found, otherwise an empty string.</returns>
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();
        
        //Settings are loaded here, where CurrentLanguageCode is decided.

        UpdateLanguageData();
    }
    
    public string CurrentLanguageCode
    {
        get 
        {
            // Retrieves the current language code from
            // the Settings model (abstracted away)
        }
    }
    
    /// <summary>
    /// Registers the languages by their corresponding ISO code.
    /// </summary>
    private void RegisterLanguages()
    {
        // Defined in Constants class
        string[] supportedLanguageCodes =
        {
            "en", "ar", "de", "el", 
            "es", "fr", "ko", "hi", 
            "it", "he", "jp", "ru", "sv"  
        };
        
        foreach(string languageCode in supportedLanguageCodes)
            UILanguageDefn.RegisterSupportedLanguage(languageCode);
    }

    /// <summary>
    /// Loads the list of available languages from the embedded XML resource.
    /// </summary>
    private void LoadLanguageList()
    {
        // Defined in Constants class
        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);
    }

    /// <summary>
    /// Updates the UI language data from that
    /// defined in the corresponding language file.
    /// </summary>
    /// <returns>
    public bool UpdateLanguageData()
    {
        string languageCode = CurrentLanguageCode;
        if (String.IsNullOrEmpty(languageCode)) return false;

        //This follows a convention for language definition files
        //to be named 'LangXX.xml' (or 'LangXX-XX.xml')
        //where XX is the ISO language code.
        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()
{
    // Initialise the default language code.
    // In most cases this will be overwritten by the
    // restored value from the saved settings, or that of the current culture.
    _uiLanguageCode = Constants.DefaultLanguageCode;     //"en"

    string languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;

    // If the system language is supported, this will
    // ensure that the application first loads
    // with the UI displayed in that language.
    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:

/// <summary>
/// Gets or sets the language definition used by the entire interface.
/// </summary>
/// <value>The language definition.</value>
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:

/// <summary>
/// Refreshes the UI text to display in the current language.
/// </summary>
public void RefreshUILanguage()
{
    _model.UpdateLanguageData();
    CommonViewModel.Current.LanguageDefn = _model.CurrentLanguage;

    //Notify any other internal logic to prompt a refresh (as necessary)
    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:

Side-by-side comparison of languages with varying font size

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:

Partial localisation of right-to-left languages

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}">
        <!-- Contents -->
    </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:

More complete localisation of right-to-left languages

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here