Multilingual applications

Living in the good old S of A it is natural for us to be bilingual or even multilingual. So what do we do if we have to create software that can/must be used by people of multiple language backgrounds? Traditionally Microsoft had the concept of resource files that you can use to create multiple versions of the same application for different languages or locales. Problem with this is once the executable has been compiled it is also fixed. What happens if you want the application to be multilingual at ‘run-time’?

Well, I’ve had to do something like this for myself. I first looked at resource files but it just did not solve the whole issue on its own. So I started creating something for myself that works for my application. Part of the solution still use .Net resource files for storing xml data that gets use in the language look up procedure. The idea is to load the text of controls and windows at the time the window open since control and window text get loaded at run-time anyway. The following is an explanation of the classes I created to facilitate my solution. It is a fully working solution and already in use inside a proper application. There may be other and even better or smarter solutions ‘out there’ but this one works for me.

LanguageHandler

This is the central class that handles all the language setting/reading etc. functionality. It encapsulate the list of ‘phrases’ used in the application and provides methods to retrieve words or phrases depending on a look-up key. Simply translating words alone is not good enough so the focus is on whole ‘phrases’.

The following is a partial view of the class without methods:

public static class LanguageHandler
{

private static List<Phrase> phraseList = new List<Phrase>();

#region Properties
private static string languageID = “en-za”;
public static Language AppLanguage {

get {

switch (languageID) {
case “af”:
return Language.Afrikaans;
case “en-za”:
return Language.English;
default:
return Language.Afrikaans; }

}
set {

switch (value) {
case Language.Afrikaans:
languageID = “af”;
break;
case Language.English:
languageID = “en-za”;
break;
default:
languageID = “en-za”;
break; }

}

public static string PhraseSource { set; get; }
#endregion

}

PhraseList.xml

The definition of the phrases can probably be stored in any data form but I choose a plain simple xml file to host the raw phrase data. This make it easy to maintain and port. It is also included into the application as a file resource although theoretically it could be located anywhere as long as the application can reach it.

The structure is also very basic as it is simply a serialized version of List<Phrase>

<?xml version=”1.0″ encoding=”utf-16″?>
<ArrayOfPhrase xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
<phrase id=”someId” default=”Some default”>

<phraseEntry lang=”af”>Afrikaanse waarde</phraseEntry>
<phraseEntry lang=”en-za”>English value</phraseEntry>

</phrase>
<phrase id=”someId2″ default=”Some default 2″>

</ArrayOfPhrase>

LoadPhrases method

Loading the data is as simple as deserializing the data. I have a helper class (not shown) to simply take the xml file and deserialize it:

#region LoadPhrases
public static void LoadPhrases()
{

if ((PhraseSource != null) && (PhraseSource.Length > 0))

phraseList = SerializationUtils.DeserializeXML<List<Phrase>>(PhraseSource);

}
#endregion

GetLanguagePhrase method

The main (and only relevant) method for retrieving stuff from the class is GetLanguagePhrase. It simply takes one parameter – the id value of the phrase required.

#region GetLanguagePhrase
public static string GetLanguagePhrase(string languagePhraseID)
{

if (phraseList.Count == 0)

LoadPhrases();

Phrase phrase = (from p in phraseList where p.LanguagePhraseID == languagePhraseID select p).FirstOrDefault();
if (phrase != null)
{

PhraseEntry phraseEntry = (from pe in phrase.Phrases where pe.LangId == languageID select pe).FirstOrDefault();
if (phraseEntry != null)

return phraseEntry.Text;

else

return phrase.DefaultValue;

}
throw new Exception(“Undefined language phrase ID”);

}
#endregion

SetControlTextAndTip methods

To make it a bit easier to set the text (and tooltips if available) of controls (my implementation use WinForms) I created a few helper methods to automatically set the text on the control based on a phrase id specified.

#region SetControlText
public static void SetControlTextAndTip(string languagePhraseID, params ToolStripItem[] tsis)
{

string text = GetLanguagePhrase(languagePhraseID);
foreach (ToolStripItem tsi in tsis)
{

tsi.Text = text;
tsi.ToolTipText = text;

}

}
public static void SetControlTextAndTip(string languagePhraseID, params Control[] crtls)
{

string text = GetLanguagePhrase(languagePhraseID);
foreach (Control crtl in crtls)
{

crtl.Text = text;

}

}
public static void SetControlTextAndTip(string languagePhraseID, params ColumnHeader[] columnHeaders)
{

string text = GetLanguagePhrase(languagePhraseID);
foreach (ColumnHeader ch in columnHeaders)
{

ch.Text = text;

}

}
#endregion

The Phrase and PhraseEntry classes

These two classes are really simple. For the purpose of this article they could be a lot simpler but since I built a separate ‘editor’ they have a bunch of attributes to make editing easier. This editor plus a few method inside LanguageHandler will have to wait for another article.

[Serializable, XmlType(“phrase”)]
public class Phrase : IComparable
{

[XmlAttribute(“id”),
Browsable(true),
CategoryAttribute(“Identifier”),
DefaultValueAttribute(“”),
DescriptionAttribute(“Phrase ID”),
ReadOnly(true)]
public string LanguagePhraseID { get; set; }
[XmlElement(“phraseEntry”),
Browsable(true),
CategoryAttribute(“Value details”),
DefaultValueAttribute(“”),
DescriptionAttribute(“Phrase entries”)]
public List<PhraseEntry> Phrases { get; set; }
[XmlAttribute(“default”),
Browsable(true),
CategoryAttribute(“Value details”),
DefaultValueAttribute(“”),
Editor(typeof(MultilineStringEditor), typeof(UITypeEditor)),
DescriptionAttribute(“Default value. Use ‘\\r\\n’ to indicate crlf.”)]
public string DefaultValue { get; set; }

#region IComparable Members
public int CompareTo(object obj)
{

Phrase otherPhrase = (Phrase)obj;
return LanguagePhraseID.CompareTo(otherPhrase.LanguagePhraseID);

}
#endregion

}

[Serializable, XmlType(“entry”), TypeConverter(typeof(PhraseEntryConverter))]
public class PhraseEntry
{

public PhraseEntry() { }
public PhraseEntry(string phraseEntryDef)
{

if (phraseEntryDef.Contains(“:”))
{

LangId = phraseEntryDef.Substring(0, phraseEntryDef.IndexOf(“:”));
Text = phraseEntryDef.Substring(phraseEntryDef.IndexOf(“:”) + 1);

}

}

[XmlAttribute(“lang”),
Browsable(true),
CategoryAttribute(“Phrase entry”),
DefaultValueAttribute(“”),
DescriptionAttribute(“Language”)]
public string LangId { get; set; }
[XmlText(),
Browsable(true),
CategoryAttribute(“Phrase entry”),
DefaultValueAttribute(“”),
Editor(typeof(MultilineStringEditor), typeof(UITypeEditor)),
DescriptionAttribute(“Text”)]
public string Text { get; set; }

public override string ToString()
{

return LangId + “:” + Text;

}

}

Using it all

Using LanguageHandler is really easy. Typically in a form’s onload event I call a private local method that sets all the texts of the controls that need to be set. There is no real performance issue to worry about since all the phrase data is stored in memory. One disadvantage is that normally the form need to be closed and reopened if the ‘language’ has change. This is not really a problem since changing the language is something that should not happen often. Technically this private LoadLanguageResources method could be ran any number of times whenever you want. The only place where I usually call it multiple times is on the ‘Options’ dialog where you choose the language being used.

If you have to use the same phrase multiple times you can always store the value in a local string variable.

The following is a short example of how to use it:

static class Program
{

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{


if (Properties.Settings.Default.LanguageID == “English”)

LanguageHandler.AppLanguage = Language.English;

else

LanguageHandler.AppLanguage = Language.Afrikaans;

LanguageHandler.PhraseSource = Properties.Resources.PhraseList;
LanguageHandler.LoadPhrases();

Form mainForm = new MainForm();

Application.Run(mainForm);

}

public partial class MainForm : Form
{

….

private void LoadLanguageResources()
{

Text = LanguageHandler.GetLanguagePhrase(“AppName”);
LanguageHandler.SetControlTextAndTip(“File”, fileToolStripMenuItem);
LanguageHandler.SetControlTextAndTip(“NewPerson”, newPersoonToolStripMenuItem);

}

private void MainForm_Load(object sender, EventArgs e)
{

LoadLanguageResources();

}

}

Summary

As you can see the solution is not really complicated. I really like simple solutions that ‘just works’. This one has proven to be working for my needs so far. I have over 340 different phrases, some are very long themselves like message box prompts.

Leave a Reply

%d bloggers like this: