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

Multi-Lingual UIs in WPF

0.00/5 (No votes)
7 Feb 2012 1  
Multi-lingual UIs in WPF

Increasingly, we’re being asked to do UIs in a multi-lingual manner. Usually, it’s not until you are 75% of the way through the project, that someone pops their head up and says ‘oh, we can have the UI in a different language can’t we?’. If they’re still breathing after you’ve finished strangling them, you then explain that it’s not that easy to just retro-fit multi-lingual support into an application.

Now, in WinForms, this would’ve been a royal PITA, but with WPF it’s been made a whole lot easier given its dynamic layout nature. No more having to redo dialogs and forms in different languages. However, it is still best to start off with a strategy for supporting multi-lingual support and then if you don’t use it, no harm done. The work to support it is quite trivial, so you can’t really play the ‘We don’t have time to implement that’ card.

Especially as I’m going to present some code in order for you to put into your WPF application. The code I’m going to present has been made concise, so in the real world, you may want to adapt it a bit for your circumstances (hey, I can’t do everything for you).

To keep things easy, particularly if you’re going to get your translations done externally, I’m putting the strings we’ll need into XML files:

The English one looks like:

<?xml version="1.0" encoding="utf-8" ?>
<Dictionary EnglishName="English" 
CultureName="English" Culture="en-US">
  <Value Id="File" Text="_File" />
  <Value Id="From ViewModel" Text="From ViewModel" />
  <Value Id="Exit" Text="_Exit" />
</Dictionary>

And the Japanese one looks like:

<?xml version="1.0" encoding="utf-8" ?>
<Dictionary EnglishName="Japanese" 
CultureName="?????" Culture="ja-JP">
  <Value Id="File" Text="????" />
  <Value Id="From ViewModel" Text="????????" />
  <Value Id="Exit" Text="??" />
</Dictionary>;

So what we have is the English form of the string in the Id attribute and the translated text in the Text attribute.

Next, we’ll have a Language class that can actually read and store this information:

public class Language
{
    #region Fields

    private Dictionary<string, string> lookupValues =
        new Dictionary<string, string>();

    #endregion Fields

    #region Properties

    /// <summary>
    /// Gets the culture name of this dictionary.
    /// </summary>
    public string CultureName
    {
        get;
        private set;
    }

    /// <summary>
    /// Gets the english name of the culture of this dictionary.
    /// </summary>
    public string EnglishName
    {
        get;
        private set;
    }

    #endregion Properties

    #region Public Methods

    /// <summary>
    /// Called when this dictionary needs loading from disk.
    /// </summary>
    public void Load(string filename)
    {
        if (!File.Exists(filename))
        {
            throw new ArgumentException("filename",
        string.Format(CultureInfo.InstalledUICulture,
        "File {0} doesn't exist", filename));
        }

        var xmlDocument = XDocument.Load(filename);
        var dictionaryRoot = xmlDocument.Element("Dictionary");

        if (dictionaryRoot == null)
        {
            throw new XmlException("Invalid root element. Must be Dictionary");
        }

        EnglishName = dictionaryRoot.Attribute("EnglishName").Value.ToString();
        CultureName = dictionaryRoot.Attribute("Culture").Value.ToString();

        foreach (var element in dictionaryRoot.Elements("Value"))
        {
            lookupValues.Add(element.Attribute("Id").Value.ToString(),
        element.Attribute("Text").Value.ToString());
        }
    }

    public string Translate(string id)
    {
        var translatedString = id;

        if (lookupValues.ContainsKey(id))
        {
            translatedString = lookupValues[id];
        }

        return translatedString;
    }

    #endregion Public Methods
}

Basically, this reads the given XML file and stores the ids and text in a Dictionary ready for lookup.

Of course, we can have more than one language and this only handles one. So we house this in a LanguageService which has knowledge of all the installed languages.

public class LanguageService
{
    private Language currentLanguage;
    private Dictionary<string, Language> languages = new Dictionary<string, Language>();

    public event EventHandler LanguageChanged;

    public IEnumerable<Language> InstalledLanguages
    {
        get
        {
            return languages.Values;
        }
    }

    public void Initialize()
    {
        foreach (var file in Directory.GetFiles("Languages", "*.xml"))
        {
            var cultureId = Path.GetFileNameWithoutExtension(file);

            var language = new Language();
            language.Load(file);

            languages.Add(cultureId, language);
        }

        currentLanguage = languages[CultureInfo.CurrentUICulture.Name];
    }

    public string Translate(string id)
    {
        return currentLanguage.Translate(id);
    }

    public Language CurrentLanguage
    {
        get
        {
            return currentLanguage;
        }

        set
        {
            if (currentLanguage != null)
            {
                currentLanguage = value;

                if (LanguageChanged != null)
                {
                    LanguageChanged(this, null);
                }
            }
        }
    }
}

Ok, this is pretty straight forward. We simply go through our Language directory and load up each Language that is found. This list is stored in a Dictionary with the culture name as the key. If the Current Language changes, we raise an event to say so.

Next we have a markup extension. These are special WPF classes that derive from MarkupExtension and always end in Extension for the class name. This part can be omitted in the XAML. So the following TranslateExtension will just look like Translate in XAML.

public class TranslateExtension : MarkupExtension, IValueConverter
{
    private static LanguageService languageService;

    private string originalText;

    static TranslateExtension()
    {
        if (languageService == null)
        {
            languageService = ServiceLocator.Current.GetInstance<LanguageService>();
        }
    }

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

    public object Convert(object value, Type targetType,
    object parameter, System.Globalization.CultureInfo culture)
    {
        string text = (string)parameter;

        text = languageService.Translate(text);

        return text;
    }

    public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var binding = new Binding("Text")
        {
            Source = new TranslateViewModel(languageService, originalText)
        };

        return binding.ProvideValue(serviceProvider);
    }
}

Here, the class makes use of the LanguageService which is obtained through the use of the ServiceLocator. Here, we have a static constructor which will get called once (and only once) when the first instantiation of the TranslateExtension class takes place. The constructor for each instance takes the text to translate in.

Using this MarkupExtension in XAML looks like the following:

<Menu DockPanel.Dock="Top">
    <MenuItem Header="{local:Translate File}">
         <MenuItem Header="{Binding FromViewModelLabel}"/>
         <MenuItem Header="{local:Translate Exit}" 
         Command="{Binding ExitCommand}"/>
    </MenuItem>
</Menu>

Here, we show the direct translation engine being used by XAML, as in {local:Translate File}. This will instantiate a TranslateExtension object and pass ‘File’ into its constructor. The ProvideValue on that object will then be called and bind it up to a TranslateViewModel and its Property Text. Here, we basically used the adapter pattern to turn our markup extension into a binding to a TranslateViewModel and use its Text property to serve up our string.

The {Binding FromViewModelLabel} has just been provided to show an alternative mechanism to server the string up via the ViewModel rather than Markup Extension.

You can download the full source code here.

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