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 string
s we’ll need into XML files:
The English one looks like:
="1.0" ="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:
="1.0" ="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
public string CultureName
{
get;
private set;
}
public string EnglishName
{
get;
private set;
}
#endregion Properties
#region Public Methods
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.
<like href="http://tap-source.com/?p=232" layout="box_count" show_faces="false" width="45">