Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Tagged as

Multi-Lingual UIs in WPF

, 7 Feb 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
Multi-Lingual UIs in WPF

Increasingly, we’re being asked to do UIs in a multi-lingual manner. Usually, it’s not until your 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

        /// <span class="code-SummaryComment"><summary>
</span>        /// Gets the culture name of this dictionary.
        /// <span class="code-SummaryComment"></summary>
</span>        public string CultureName
        {
            get;
            private set;
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Gets the english name of the culture of this dictionary.
        /// <span class="code-SummaryComment"></summary>
</span>        public string EnglishName
        {
            get;
            private set;
        }

        #endregion Properties

        #region Public Methods

        /// <span class="code-SummaryComment"><summary>
</span>        /// Called when this dictionary needs loading from disk.
        /// <span class="code-SummaryComment"></summary>
</span>        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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

SteveAdey
Software Developer (Senior) NoProblem Software Ltd
United Kingdom United Kingdom
No Biography provided
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalKyleBM2-Sep-13 0:51 
GeneralRe: My vote of 5 PinmemberSteveAdey2-Sep-13 3:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 7 Feb 2012
Article Copyright 2012 by SteveAdey
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid