|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionSomething 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 CallThe 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 We need two classes to store the response data: /// <summary>
/// JSON Response Class.
/// </summary>
class TranslationResponse
{
[JsonProperty("responseData")]
public Translation Data { get; set; }
[JsonProperty("responseDetails")]
public string Details { get; set; }
[JsonProperty("responseStatus")]
public int Status { get; set; }
}
/// <summary>
/// JSON Translation Response.
/// </summary>
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 var serializer = new JsonSerializer();
var translationResponse = (TranslationResponse)serializer.Deserialize(
new StringReader(streamReader.ReadToEnd()), typeof(TranslationResponse));
The Applying Extension MethodsThe code we just wrote can easily be written in an extension method on the string class. Our static public static string Translate(this string str, Languages.Language source,
Languages.Language target)
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 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 Moving to Windows Presentation FoundationWhen 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 <TextBlock Text="{vi:Translate Hello World}" />
The [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 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 As I mentioned before, the As with any converter class, this class implements the 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 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 // 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 <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 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 TranslationsIn 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();
}
}
ConclusionThis 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.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||