Sunday, March 29, 2009

Automatically translate your Silverlight Application

Get the source now (58Kb)

UPDATE: Thanks to my colleague Emile van Ewijk who pointed out the the is keyword in C# does the same as the extension method that was used, I could refactor and optimize the code. So the source is updated. Thanks, Emile! (and shame on me for not knowing that...)


Let me start by saying that I’m no great fan of automatic translations. I rarely come across an automatically translated text that makes sense :)

As most of you know, I’m Dutch. That’s why my English sux ;). When I was at MIX09, where I met all of these great people, I also received a flyer in my goodie bag of Microsoft Live Translator. Since we, at Amercom, work for some international clients and create multilingual sites this attracted my attention.

And since everyone seems to be blogging on the new features of Silverlight 3B1 I figured, why not do it in plain old Silverlight 2 :P

Preparing your website for multilanguage in Silverlight

First thing I noticed when working with the Cultures in Silverlight (which most of you probably already know) was that the culture did NOT respond to the language settings of the browser. After some research I came across an article on MSDN library that said:

“The .NET Framework for Silverlight provides data for the invariant culture, but it retrieves information about all other cultures from the operating system. This means that the information that is available to a specific culture may differ across operating systems or even across versions of the same operating system.”

IMHO it is a mistake not to read the browser settings by default, but using the OS settings. There must be a pretty good reason for this, but I can’t think of it.

Anyway, to make sure your Silverlight 2 application responds to the settings of the browser, 2 configuration steps are necessary:

1. If you’re using an ASP.NET website/web application, set the following tag in the system.web section of the web.config:


2. Set the following parameters in the object tag of the silverlight control in the ASPX page (ASPX is needed for this functionality):

<param value="<%=System.Threading.Thread.CurrentThread.CurrentCulture.Name %>" name="Culture" />
<param value="<%=System.Threading.Thread.CurrentThread.CurrentUICulture.Name %>" name="UICulture" />


Once you’ve done that, you’ll be able to use the System.Globalization.CultureInfo.CurrentCulture and System.Globalization.CultureInfo.CurrentUICulture properties to retrieve the language set in the browser’s language settings.


What I wanted to achieve with my application is that I wanted to do nothing more then call a single method which takes a Panel control and goes through the children. It should be able to identify the type of control and should check the content for strings that can be translated. One thing that is not working in the sample application is controls that are contained in other controls than those that are derived of a Panel. So controls added to a Button.Content won’t be translated, but I doubt it will be difficult to add that functionality as well.


Adding the Live Translator


To add the Live translator to our application we only need to add a reference to the webservice they created. However, to use the webservice you also need to request an App_Id. You can create an app_id on the following website: http://search.live.com/developers/appids.aspx


On the previously mentioned site there is also more information available on the Live Translator service (API documentation, etc.). I created a constant which contains my app_id so I can easily reuse it through my entire class.

For the webservice, app_id and the source language I created the following fields/const:

private readonly LanguageServiceClient _languageServiceClient = new LanguageServiceClient();
private const string APP_ID = "7BAAE0482B3EA27295D38DA669A42E0F57B56B57";
private const string SOURCE_LANGUAGE = "en";


NOTE: the SOURCE_LANGUAGE should contain the TwoLetterISOLanguageName of the culture you want to use as the source for the translation. I chose English which means that all of the text I use in my Application should be added as English text. Translator should, in some cases be able to detect the language, but since we’re using single words in our application like “Upload” or “Download” I won’t rely on the automatic detection.


In my Page constructor I created a few basic Eventhandlers and a call to the translate method we will create in a moment:

_languageServiceClient.TranslateCompleted += _languageServiceClient_TranslateCompleted;
if(System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName != SOURCE_LANGUAGE)
{
DoTranslateElements(TranslateGrid);
}
ReTranslate.Click += ReTranslate_Click;


Create the DoTranslateElements method


We can use: if (element is TextBox) to check if the element is a TextBox or derived from the type.


Now for the DoTranslateElements method, the most imprtant thing that is done here is the following:

foreach (var element in parent.Children)
{
//contentcontrol elements (button, etc)
if (element is ContentControl)
{
ContentControl contentControl = element as ContentControl;
if (contentControl != null)
if (contentControl.Content is string)
{
_languageServiceClient.TranslateAsync(
APP_ID,
contentControl.Content.ToString(),
SOURCE_LANGUAGE,
System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName,
contentControl);
}
}

//elements which contain children
if (element is Panel)
{
//recursive call
DoTranslateElements(element as Panel);
}

if (element is TextBlock)
{
TextBlock textBlock = element as TextBlock;
if (textBlock != null)
_languageServiceClient.TranslateAsync(
APP_ID,
textBlock.Text,
SOURCE_LANGUAGE,
System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName,
textBlock);
}

if (element is TextBox)
{
TextBox textBox = element as TextBox;
if (textBox != null)
_languageServiceClient.TranslateAsync(
APP_ID,
textBox.Text,
SOURCE_LANGUAGE,
System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName,
textBox);
}
}


What is actually done here is loop through all of the elements in a Panel and:


1. If the element is a button, check if content is a string, than translate 2. If the element is a textblock or textbox, translate the text property value 3. If the element is a Panel, call the method itself again (recursive call)


What needs to be noticed is that I use the UserState property to send the object that is translated (button, textblock, etc) to the webservice. This object is returned in the TranslateCompleted event as you will see in the next section.


That is basically it. Now all of the (async) calls to the webservice are made and we only have to catch the TranslateCompleted event and set the correct content in the correct control property.


The TranslateCompleted event


In this event we need to retrieve the data which is returned from the webservice and set it in the corresponding property.

var element = e.UserState;
if (element is ContentControl)
{
ContentControl contentControl = e.UserState as ContentControl;
if (contentControl != null)
{
contentControl.Content = e.Result;
}
}

if (element is TextBlock)
{
TextBlock textBlock = e.UserState as TextBlock;
if (textBlock != null)
{
textBlock.Text = e.Result;
}
}

if (element is TextBox)
{
TextBox textBox = e.UserState as TextBox;
if (textBox != null)
{
textBox.Text = e.Result;
}
}


That's it! Now we get an automatically translated version of our application. I created a couple of buttons and a textblock - and box to test it. I also nested a few controls in a Grid and a Canvas which also worked fine. I have no more space on my personal site, so I can't get a demo online right now, but the source is available: Get the source now (58Kb) Hope this helps!

3 comments:

  1. Nice work. I had asked the Translator team to implement this in Silverlight but appears you beat them to it! :)

    -
    Scott Barnes
    Rich Platforms Product Manager
    Microsoft.

    ReplyDelete
  2. Rob,

    I have some trouble trying to download the source. Both IE and Firefox show up as blank pages ...

    Regards, Nathan

    ReplyDelete
  3. @Scott: Thanks :)

    @Nathan: Thanks for letting me know. Just updated the links. Don't understand what went wrong...

    ReplyDelete