Click here to Skip to main content
Click here to Skip to main content

Real-Time Multilingual WPF Demo

, 29 Jul 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
Translate a WPF User Interface using the Google AJAX Language API in real-time
realtime_multilingual_wpf_demo

Introduction

Something to keep in mind in the very early stages of your application development cycle is whether or not you want to offer your end-users multilingual support. We found two articles on The Code Project that tackle this problem using various approaches (see Localizing WPF Applications using Locbaml[^] and WPF Multi-Lingual at Runtime[^] for more info). With the development of one of our latest products, Vidyano, our goal is to offer developers a set of tools that allow them to create full-blown WPF applications much faster. Both approaches didn't really met our needs because they are way too complex for what we had in mind. In light of this, we started from a completely different approach which we would like to share with the community, in the hope that more applications will offer support for multilingual user interfaces.

Reinventing the wheel?

With today's modern translation software we already have all the tools we need to translate our applications without the need to hire an independent translation agency. In March this year Google introduced a new online service called the Google AJAX Language API[^]. This service allows us to translate blocks of text from within a webpage or an external application. This made us think about the whole concept of offering translation services from within our applications. Wouldn't it be nice if we could fallback on this service to offer the end-user a quick and dirty translation of the application they are currently using? Of course, at the moment this translation will almost never be exactly what you want it to be, but it could allow you to get a basic translation of your application that you can fine-tune in a later stage. We could even hand this basic translation to an independent translation agency and have them clean up the bits.

There are a couple of samples on the API pages that show us how to call the service from a non-Javascript language. All we have to do to get us started is implement this functionality in any .NET language.

Making the Call

The URL we need to call to get a response from the Language API is as follows: http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=Hello%World&langpair=en%7Cfr.

The "Hello%20World" in the URL above is our text we need to translate, together with the language pair at the end we query the service to translate this block of text from English to French. At the time of writing, there are no less than 24 languages available.

Let's make this call from within C#:

// Create a WebRequest, passing in the text to translate along with
// the source and target language code
var req = (HttpWebRequest)WebRequest.Create(string.Format(
"http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q={0}&langpair={1}%7C{2}",str,
 source.Code,
    target.Code));
// req.Referer = Get your Google API Key at 
// http://code.google.com/apis/ajaxsearch/key.html (Note: You must supply a valid
// referer header !)

...

WebResponse response = req.GetResponse();
var streamReader = new StreamReader(response.GetResponseStream());

This streamReader will hold the response for our request. In the Google AJAX API's case, this response is returned to us in the JSON format. Luckily for us, there is already a JSON project available for .NET, called JSON.NET[^]. We can use this library to deserialize the response to a .NET object.

We need two classes to store the response data:

/// <span class="code-SummaryComment"><summary></span>
/// JSON Response Class.
/// <span class="code-SummaryComment"></summary></span>
class TranslationResponse
{
    [JsonProperty("responseData")]
    public Translation Data { get; set; }

    [JsonProperty("responseDetails")]
    public string Details { get; set; }

    [JsonProperty("responseStatus")]
    public int Status { get; set; }
}

/// <span class="code-SummaryComment"><summary></span>
/// JSON Translation Response.
/// <span class="code-SummaryComment"></summary></span>

class Translation
{
    [JsonProperty("translatedText")]
    public string TranslatedText { get; set; }
}

Now that we have a class definition that can hold our response data, we can use the JSON.NET deserializer to get an instance from TranslationResponse.

var serializer = new JsonSerializer();
var translationResponse = (TranslationResponse)serializer.Deserialize(
    new StringReader(streamReader.ReadToEnd()), typeof(TranslationResponse));

The translationResponse.TranslatedText property will return the translated text for our request.

Applying Extension Methods

The code we just wrote can easily be written in an extension method on the string class. Our static GoogleTranslateExtensions class implements this extension method.

public static string Translate(this string str, Languages.Language source,
    Languages.Language target)

Languages.Language is a nested class that contains the language description, the ISO code and a flag that defines whether this is a LeftToRight or RightToLeft language.

public class Language
{
    internal Language(string desc, string code) :
        this(desc, code, false) { }

    internal Language(string desc, string code, bool rightToLeft)
    {
        Description = desc;
        Code = code;
        RightToLeft = rightToLeft;
    }

    public string Description { get; private set; }
    public string Code { get; private set; }
    public bool RightToLeft { get; private set; }
}

The static Languages class is there for convenience reasons. It returns a static instance of the Language class for each available language.

public static class Languages
{
    static Languages()
    {
        English = new Language("English", "en"); languages.Add(English);
        ...
    }

    public static Language English { get; private set; }
    ...
}

This way we can very easily translate strings in our application by for instance typing the following code:

public static void Main()
{
    // You may have to set your proxy here first
    // GoogleTranslateExtensions.Proxy = new WebProxy("xxx.xxx.xxx.xxx", 8080);

    // Will write "Bonjour monde" to the console window.
    Console.WriteLine("Hello World".Translate(Languages.English, Languages.French));
}

Note the Proxy on the static GoogleTranslateExtensions class. You may have to set your proxy here.

Moving to Windows Presentation Foundation

When moving to WPF, we need a way to translate all of those hardcoded strings we define in our XAML page. There are several possibilities to achieve this, we could for instance use a method binding and pass the string as an argument. However, we decided to implement this functionality by offering the developer a markup extension that can easily be wrapped around the string that needs translating. This is the TranslateExtension class defined in the Vidyano.Presentation project. This markup extension takes a string as parameter in its constructor.

<TextBlock Text="{vi:Translate Hello World}" />

The TranslateExtension class derives from the abstract MarkupExtension class. This markup extension will return a BindingExpression whenever its ProvideValue method is called by the .NET runtime, so it is defined as follow:

[MarkupExtensionReturnType(typeof(BindingExpression))]
public class TranslateExtension : MarkupExtension

Before we dive any further into this class, I would like to show you another one first, the LanguageSelector class. This class is a custom ContentControl control class and defines the scope for our translation. Every object in its content can define the {vi:Translate ... } markup extension. The reason for this wrapper control is that the markup extension needs to know from which language, to which language it needs to translate. This is done by adding two attached properties on the LanguageSelector class: SourceLanguageProperty and TargetLanguageProperty. By defining these attached properties with the FrameworkPropertyMetadataOptions.Inherits option, we effectively allow all child objects in the tree to query the current source and target language.

So the following XAML code is what you would write to translate some text in a TextBlock.

<vi:LanguageSelector xml:lang="en-US">
    <TextBlock Text="{vi:Translate Hello World}" />
</vi:LanguageSelector>

The xml:lang attribute defines the source language for this scope. Let's go back to our markup extension.

As I mentioned before, the TranslateExtension class returns a BindingExpression from its ProvideValue method. You might wonder why we simply don't return the translated text. This is because we would like the text to be updated whenever we change the target language on our LanguageSelector parent control. In order to accomplish this we need to set a converter on the binding we return. This is the TranslateConverter class. This converter class however is a bit more complicated than the average converter.

As with any converter class, this class implements the IValueConverter interface. Where this converter class differs from your average converter is the fact that it derives from the FrameworkElement class. This allows us to define two dependency properties on the converter, source and target language, which we will bind to the attached properties on the LanguageSelector class. Now, whenever the Convert method is called on our converter, the converter will use its source and target language dependency properties to call the Translate extension method which we created at the beginning of this article.

Languages.Language sourceLang = Languages.FromString(SourceLanguage);
Languages.Language targetLang = Languages.FromString(TargetLanguage);

if (sourceLang != null && targetLang != null && sourceLang != targetLang)
{
    // Asynchronously invoke the Translate method
    Action translate = () => translation = text.Translate(sourceLang, targetLang);
    translate.BeginInvoke(Translated, null);

    // Return "Loading..." as long as the translation is in progress
    return LanguageSelector.GetLoadingString(targetObject);
}

return text;

There's a catch here though, the Convert method is only called once as long as the text doesn't change. So we need to find a way to trigger an invalidation of this converter whenever the source or target language have changed. This is accomplished by hooking in the changed handlers on our dependency properties: SourceLanguageChanged and TargetLanguageChanged.

In these handler methods we make sure the following code is called:

var converter = obj as TranslateConverter;
if (converter != null)
{
    // Invalidate the binding on our target object
    BindingExpressionBase expression = BindingOperations.GetBindingExpressionBase(
        converter.targetObject, converter.targetProperty);
    if (expression != null)
        expression.UpdateTarget();
}

The targetObject and targetProperty are passed to us in the constructor. Let's jump to the creation of our converter.

// Get the TargetObject and TargetProperty via the IProvideValueTarget service
var provideValueService = (IProvideValueTarget)serviceProvider.GetService(
    typeof(IProvideValueTarget));
if (provideValueService == null)
    return null;

var targetObject = provideValueService.TargetObject as DependencyObject;
var targetProperty = provideValueService.TargetProperty as DependencyProperty;

if (targetObject != null && targetProperty != null)
{
    // There might already be a Binding
    if (Binding == null)
        Binding = new Binding();

    // Create the Converter, passing the targetObject and targetProperty
    var converter = new TranslateConverter(targetObject, targetProperty);

    Binding.Converter = converter;
    // Text may be string.Empty if a the markup extension is created with a Binding
    Binding.ConverterParameter = Text;

    // Bind the converter's SourceLanguageProperty and TargetLanguageProperty
    // to the attached properties
    var sourceLanguageBinding = new Binding
    {
        Path = new PropertyPath("(0)", LanguageSelector.SourceLanguageProperty),
        Source = targetObject
    };

    var targetLanguageBinding = new Binding
    {
        Path = new PropertyPath("(0)", LanguageSelector.TargetLanguageProperty),
        Source = targetObject
    };

    converter.SetBinding(TranslateConverter.SourceLanguageProperty,
        sourceLanguageBinding);
    converter.SetBinding(TranslateConverter.TargetLanguageProperty,
        targetLanguageBinding);

    // Return the new/updated binding
    return Binding.ProvideValue(serviceProvider);
}

return null;

The first thing to note here is the Binding. You may also use the markup extension with a binding instead of a hardcoded string. This allows you to go beyond simple UI translation and also offer translations for your data.

<vi:LanguageSelector xml:lang="en-US">
    <TextBlock Text="{vi:Translate Binding={Binding Description}}" />
</vi:LanguageSelector>

In the above sample, the textblock will bind to a Description property on the current DataContext's object. The texblock will refresh its Text whenever either the target or source language changes or whenever the Description property changes.

That's all we need to get a real-time multilingual application. There's just one thing I would like to add.

Caching and Fine-Tuning Translations

In order to go a bit easy on the Language API web requests, we added a small cache to the project. Text that needs to be translated will first be checked against a small SQL Compact Database file which will be copied to your working directory, if it didn't exist already, whenever the program starts translating. Besides its caching abilities, this local storage also adds another powerful feature to your application.

As we all know, sometimes translations can be a little "off". By opening the WPF window and selecting your target language, all translations are written into this cache. This means that if you change the rows inside the local cache, you can fine-tune your translations. The next time you open your application or change the target language, you will get your more accurate values from the cache instead of the Google AJAX Language API.

lock (Cache)
{
    // Look for the text block in the Source table.
    langSource = Cache.Source.FirstOrDefault(s => s.LangCode == source.Code &&
        s.Value == str);
    if (langSource != null)
    {
        // Get the translation for the text block and the target language from
        // the Translations table.
        Translations trans = langSource.Translations.FirstOrDefault(
            t => t.LangCode == target.Code);
        if (trans != null)
            return trans.Value;
    }
    else
    {
        // Insert the text block in the Source table.
        langSource = new Source { LangCode = source.Code, Value = str };
        Cache.Source.InsertOnSubmit(langSource);

        Cache.SubmitChanges();
    }
}

...

lock (Cache)
{
    // Some other thread might already added this information, so check first.
    Translations trans = langSource.Translations.FirstOrDefault(
        t => t.LangCode == target.Code);
    if (trans == null)
    {
        // Add the new translation for the text block.
        langSource.Translations.Add(new Translations {
            LangCode = target.Code, Value = translationResponse.Data.TranslatedText });
        Cache.SubmitChanges();
    }
}

Conclusion

This is what we wanted to share with you for now. Of course this is just a very basic implementation of multilingual support, but we hope you can see the power it brings. The next CTP version of Vidyano will go much further than this.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

2sky
2sky
Belgium Belgium
2sky focuses on pushing the development for the Microsoft technology stack using its in-house developed product, Vidyano.
Group type: Organisation

3 members

Follow on   Twitter   Google+

Comments and Discussions

 
GeneralNice Pinmembervahid113-Feb-12 20:11 
GeneralGood Stuff PinmemberMember 314862722-Feb-11 23:21 
GeneralVery cool!!! Pinmember|\/|ax1-Jul-10 2:13 
GeneralGreat Job! PinmemberMember 350625626-Dec-09 13:51 
GeneralAll your base are belong to us PinmvpJosh Smith30-Jul-08 2:36 
GeneralRe: All your base are belong to us PinmemberDavid Sleeckx30-Jul-08 2:40 
GeneralRe: All your base are belong to us PinmvpJosh Smith30-Jul-08 2:46 
GeneralRe: All your base are belong to us PinmemberDavid Sleeckx30-Jul-08 2:58 

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
Web02 | 2.8.141220.1 | Last Updated 29 Jul 2008
Article Copyright 2008 by 2sky
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid