There are few things more awesome than the Microsoft Translate service available on the Azure Marketplace. Unfortunately, the vast majority of examples of how to use it are not written for Windows Phone, which is missing some key components available in other .NET solutions.
(Note: Yes, I know you this should be easier with the Azure toolkit for Windows Phone, etc, etc, etc. I didn’t find the classes I needed in there and this blog post is for people like me who got stuck like me.)
So lets get started and we can see exactly how easily we can integrate this awesome translation service into a application.
Step 1: Sign Up For Microsoft Translation in the Azure Marketplace
Let’s say we’ve never been to the Azure Marketplace. Let’s go there now and click on Sign-In.
We’ll go through a pretty typical registration process (name, rank and serial number) and then we’re signed in for an account. Now we need to sign up specifically for the Microsoft Translation service. Click on “Data”.
Do a search for “translation’” and Microsoft Translator will pop right up.
If we click on it, we’ll see some details as well as all of our pricing options.
Helpfully, at the bottom of that, there is an option for 2 million characters for the low, low price of free. This should be enough for us to get a fun little project started.
Now that we’re signed up for our free service, we need to register our specific app and get a Client ID and Client Secret to identify our app to this service. To do so, we will not click on “My Applications” or “My Data” or “Account Keys”, but down on the “Developers” link at the bottom.
Once you’re in there, Azure Marketplace will generate a client secret for you. All you need to do is give your application a client ID, some identifying name and a redirect URI. Don’t worry about the redirect URI, we won’t use it with this service.
Write down the ClientID and the Client Secret, we’ll need those for our app.
After we hit “Create”, we’ll see our application in the Registered Applications section.
And if we like we can click on “My Data” option in the “My Account” menu and try out our brand new service using a web interface.
We’re going to build our requests based on these queries, so go ahead and grab the “Service Root URL” at the top (should look like http://api.datamarket.azure.com/Data.ashx/Bing/MicrosoftTranslator/v1/ ). Play around with the query building and then we’ll work on our app.
Step 2: Get And Save Our Token
Let’s write our app already! Open Visual Studio and start a new “Windows Phone App” project.
First, so we don’t run into problems later, lets add some references. Right click on the references folder, “Add Reference…” and add System.Servicemodel.Web (which we’ll need to read JSON) and System.Xml.Linq (which we’ll need to read XML).
Add a “Services” folder and add two files, TranslationService.cs and TokenService.cs. Add a “Models” folder and add the file “AdmAccessToken.cs”.
The way this service works is to go out and get an access token for the client. This token will come back as our AdmAccessToken, which is valid for 10 minutes. We’ll use that token to authenticate our app with the service. We’re going to assume that we need a new token every time we open the app, but try to use the same token during each app session. (If you want to know more about the access tokens, head over here.)
The first 4 properties in AdmAccessToken.cs are required for proper serialization. The rest of the class is there for determining when the token has expired.
1: public class AdmAccessToken
2: {
3: // required for deserialization
4: public string access_token { get; set; }
5: public string token_type { get; set; }
6: public string expires_in { get; set; }
7: public string scope { get; set; }
8:
9: // properties and methods for determining expired tokens
10: private DateTime tokenEndTime { get; set; }
11:
12: public bool IsExpired()
13: {
14: DateTime now = DateTime.Now;
15: double secondsLeft = tokenEndTime.Subtract(now).TotalSeconds;
16: if(secondsLeft < 30)
17: return true;
18: else
19: return false;
20: }
21:
22: public void Initalize()
23: {
24: tokenEndTime = DateTime.Now.Add(new TimeSpan(0, 0, 600));
25: }
26: }
Now we’ll write our token service in TokenService.cs. We’ll send an event once we get our token, so we need to define some event args for sending the token in the event. I usually add these to the bottom of my service class file.
1: public class TokenServiceCompleteEventArgs : EventArgs
2: {
3: public TokenServiceCompleteEventArgs(bool isSuccess, AdmAccessToken token)
4: {
5: IsSuccess = isSuccess;
6: TranslationToken = token;
7: }
8:
9: public bool IsSuccess { get; private set; }
10: public AdmAccessToken TranslationToken {get; private set;}
11: }
And add the event to the service class so we can raise it when we’re done.
1: public event EventHandler<TokenServiceCompleteEventArgs> AccessTokenComplete;
2: private void RaiseAccessTokenComplete(bool isSuccess, AdmAccessToken token)
3: {
4: if(AccessTokenComplete!=null)
5: AccessTokenComplete(this, new TokenServiceCompleteEventArgs(isSuccess, token));
6: }
We’re going to save our Client ID, Client Secret, and OAuth URL as string in the top of the class:
1: private static readonly string CLIENT_ID = "My_Translation_App";
2: private static readonly string CLIENT_SECRET = "[client secret from the registration process]";
3: private static readonly string OAUTH_URI = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13";
And then add our call for the token. Our process here is 1) check to see if we have a valid token. 2) If we have a valid token, return it. 3) If we don’t have a valid token, start our Http POST request to get a token.
1: public void GetToken()
2: {
3: if (IsolatedStorageSettings.ApplicationSettings.Contains("admAccessToken"))
4: {
5: AdmAccessToken savedToken = (AdmAccessToken)IsolatedStorageSettings.ApplicationSettings["admAccessToken"];
6: if (!savedToken.IsExpired())
7: {
8: RaiseAccessTokenComplete(true, savedToken);
9: return;
10: }
11: }
12:
13: // Create our HTTP request
14: WebRequest request = WebRequest.Create(OAUTH_URI);
15: request.Method = "POST";
16: request.ContentType = "application/x-www-form-urlencoded";
17:
18: //IAsyncResult postCallback = (IAsyncResult)request.BeginGetRequestStream(new AsyncCallback(RequestStreamReady), request);
19: request.BeginGetRequestStream(new AsyncCallback(RequestStreamReady), request);
20: }
Then we write our POST data…
1: private void RequestStreamReady(IAsyncResult asyncResult)
2: {
3: try
4: {
5: // Create the data we're going to write into the POST body
6: string clientID = CLIENT_ID;
7: string clientSecret = CLIENT_SECRET;
8: string scope = "scope=" + HttpUtility.UrlEncode("http://api.microsofttranslator.com");
9: string grant_type = "grant_type=" + HttpUtility.UrlEncode("client_credentials");
10: String postBody = string.Format("{0}&client_id={1}&client_secret={2}&{3}", grant_type, HttpUtility.UrlEncode(clientID), HttpUtility.UrlEncode(clientSecret), scope);
11:
12: // Write the data to the POST body
13: HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
14: byte[] bytes = System.Text.Encoding.UTF8.GetBytes(postBody);
15: Stream postStream = request.EndGetRequestStream(asyncResult);
16: postStream.Write(bytes, 0, bytes.Length);
17: postStream.Close();
18:
19: // Get the response (including the token)
20: request.BeginGetResponse(new AsyncCallback(GetTokenResponseCallback), request);
21: }
22: catch (WebException webExc)
23: {
24: RaiseAccessTokenComplete(false, null);
25: }
26: }
And get our response. We’ll save our token to IsolatedStorageSettings and then fire our event with the token attached.
1: private void GetTokenResponseCallback(IAsyncResult asyncResult)
2: {
3: try
4: {
5: HttpWebRequest endRequest = (HttpWebRequest)asyncResult.AsyncState;
6: HttpWebResponse response = (HttpWebResponse)endRequest.EndGetResponse(asyncResult);
7: DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(AdmAccessToken));
8: AdmAccessToken token = (AdmAccessToken)serializer.ReadObject(response.GetResponseStream());
9:
10: // Set the token so that it will begin the expiration process
11: token.Initalize();
12: // Let's set this token to be accessible on the app level
13: App.SetTranslationToken(token);
14: // And save the token to our app settings
15: if (IsolatedStorageSettings.ApplicationSettings.Contains("admAccessToken"))
16: IsolatedStorageSettings.ApplicationSettings["admAccessToken"] = token;
17: else
18: IsolatedStorageSettings.ApplicationSettings.Add("admAccessToken", token);
19: IsolatedStorageSettings.ApplicationSettings.Save();
20: // Finally, we're ready to send our token out into the wild
21: RaiseAccessTokenComplete(true, token);
22: }
23: catch (WebException webExc)
24: {
25: RaiseAccessTokenComplete(false, null);
26: }
27: }
We are now sufficiently token-ed and we’re ready to translate some things.
Step 3: Translate All The Things
Our real goal with the translation service is to enter some text, pick a source language, pick a target language and away we go.
To that end, we’ll set up some properties in TranslationService.cs to help us do this.
1: public class TranslationService
2: {
3: private TokenService _tokenService;
4: private string _originalText;
5: private string _sourceLanguage;
6: private string _targetLanguage;
7:
8: public TranslationService()
9: {
10: _tokenService = new TokenService();
11: _tokenService.AccessTokenComplete += _tokenService_AccessTokenComplete;
12: }
13: }
Then when we move to start the translation, we’ll run the token service, which will check to see if we have a valid token and, if we do, return it. (If we’ don’t, it will go and get one for us. When it is done with either task, it will raise the AccessTokenComplete event, which we’ll use to kick off the translation service.
1: public void GetTranslation(string originalText, string sourceLanguage, string targetLanguage)
2: {
3: _originalText = originalText;
4: _sourceLanguage = sourceLanguage;
5: _targetLanguage = targetLanguage;
6:
7: _tokenService.GetToken();
8: }
9:
10: void _tokenService_AccessTokenComplete(object sender, TokenServiceCompleteEventArgs e)
11: {
12: if (e.IsSuccess)
13: StartTranslationWithToken(e.TranslationToken);
14: else
15: RaiseTranslationFailed("There was a problem securing an access token");
16: }
The http service work is done in the StartTranslationWithToken method. We’ll set up the service uri we need to do the translation, create a WebRequest and set the Authorization header of our request to use “Bearer “ plus our access token. When the response comes back, we should have our XML with the data in it. We can read this using the XDocument utility (make sure to add System.XML.Linq to your project resources to get access to XDocument). Finally, we’ll raise an event that says everything went according to plan and returns our translation, as well as the original input data.
1: private void StartTranslationWithToken(AdmAccessToken token)
2: {
3: string translateUri = string.Format("http://api.microsofttranslator.com/v2/Http.svc/Translate?text={0}&from={1}&to={2}",
4: HttpUtility.UrlEncode(_originalText),
5: HttpUtility.UrlEncode(_sourceLanguage),
6: HttpUtility.UrlEncode(_targetLanguage));
7:
8: WebRequest translationRequest = HttpWebRequest.Create(translateUri);
9:
10: // We need to put our access token into the Authorization header
11: // with "Bearer " preceeding it.
12: string bearerHeader = "Bearer " + token.access_token;
13: translationRequest.Headers["Authorization"] = bearerHeader;
14:
15: // Finally call our translation service
16:
17: translationRequest.BeginGetResponse(asyncResult =>
18: {
19: try
20: {
21: HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
22:
23: HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
24:
25: // Read the contents of the response into a string
26: Stream streamResponse = response.GetResponseStream();
27: StreamReader streamRead = new StreamReader(streamResponse);
28: string translationData = streamRead.ReadToEnd();
29:
30: // Read the XML return from the translator
31: // you can get a preview of this XML if you go to your Azure
32: // account, click on My Data on the left and then on "Use" on
33: // Microsoft Translator. run a trial query and then click the
34: // XML button at the top of the query tool.
35:
36: // You'll need to add "System.XML.Linq" to your project to use XDocument
37: XDocument translationXML = XDocument.Parse(translationData);
38: string translationText = translationXML.Root.FirstNode.ToString();
39:
40: RaiseTranslationComplete(_originalText, _sourceLanguage, _targetLanguage, translationText);
41: }
42: catch (WebException webExc)
43: {
44: RaiseTranslationFailed(webExc.Status.ToString());
45: }
46: }, translationRequest);
47: }
In the interest of completeness, here is the class we made for the EventArgs that we use to return the translation (I added them to the bottom of my service cs file).
1: public class TranslationCompleteEventArgs : EventArgs
2: {
3: public TranslationCompleteEventArgs(string originalText, string fromLang, string toLang, string translation)
4: {
5: OriginalText = originalText;
6: FromLanguage = fromLang;
7: ToLanguage = toLang;
8: Translation = translation;
9: }
10:
11: public string OriginalText { get; private set; }
12: public string FromLanguage { get; private set; }
13: public string ToLanguage { get; private set; }
14: public string Translation { get; private set; }
15: }
16:
17: public class TranslationFailedEventArgs :EventArgs
18: {
19: public TranslationFailedEventArgs(string error)
20: {
21: ErrorDescription = error;
22: }
23:
24: public string ErrorDescription { get; private set; }
25: }
And here are the events in my TranslationService class that we’ll fire after the translation is complete (or has failed).
1: public event EventHandler<TranslationCompleteEventArgs> TranslationComplete;
2: private void RaiseTranslationComplete(string originalText, string fromLang,
3: string toLang, string translation)
4: {
5: if (TranslationComplete != null)
6: TranslationComplete(this, new TranslationCompleteEventArgs(originalText, fromLang, toLang, translation));
7: }
8:
9: public event EventHandler<TranslationFailedEventArgs> TranslationFailed;
10: private void RaiseTranslationFailed(string error)
11: {
12: if(TranslationFailed != null)
13: TranslationFailed(this, new TranslationFailedEventArgs(error));
14: }
Now our translation is ready to rock and roll, so the only thing we have left to do is put together a simple user interface for interacting with our translator.
Step 3: Using Our Translator
Now lets build the UI that we’ll use to execute a translation and then (finally) translate something. Because this post is already running absurdly long We’re going to create UI for translating from English to Spanish rather than build up all the infrastructure for selecting multiple languages.
Open your project using Blend (you should be using Blend to create your Windows Phone UI) and go to the MainPage.xaml. Change the top text to the name of your app and the text beneath it to “translate”.
Beneath that, get rid of anything inside the ContentPanel and the hover your mouse along the left edge of the ContentPanel. you should see an orange line show up.
Click it and you’ll see that you’ve created a separation in your Grid layout. You’ll see numbers that indicate the status of the Grid.Row that you’ve made. Hover over those numbers and you’ll see an editing system pop up (note, this is for Blend 5 and up). Click in that pop-up to edit the size of the row.
I’m editing mine to be: 1*, 1*, which will look in the XAML like
1: <Grid.RowDefinitions>
2: <RowDefinition Height="1*"/>
3: <RowDefinition Height="1*"/>
4: </Grid.RowDefinitions>
The top area will be for the source text (what we want to translate), the middle area will be for selection of the translation languages (which we won’t implement in this tutorial) and the bottom will be where we’ll place the translated text.
To this end, let’s add a TextBlock indicating the function of the top and bottom areas and a TextBox for adding or copying text. I’m going to add some text into our source TextBox just to give us something to work with.
Let’s also give our TextBoxes names so we can add and extract text from them in the code-behind. By clicking on the element in the Objects and Timeline tree, we can see the properties on the right hand side.
Just change the name at the top to make it accessible by that name in the code-behind.
We’ll start the translation using an app bar button. Instead of explaining all of the application bar creation, I’ll point you to this post that goes through it on Blend.
When the user clicks on the ApplicationBar button, we’ll fire an event that starts up the translation service. To define this event, select your ApplicationBarIconButton in the Objects and Timeline panel.
‘
Then click the lightning icon in Properties panel. This will bring up a list of events that are available to that object. In the “Click” box, type the name of the event handler you’d like to use and Blend will insert the appropriate code into the code behind.
Now, to look at our code behind (MainPage.xaml.cs). Before we call the translation service, we need to initiate it. So at the top of our class, we’ll add our translation service, instantiate it and add some event handlers in our constructor.
1: TranslationService _translator;
2:
3: public MainPage()
4: {
5: InitializeComponent();
6: _translator = new TranslationService();
7: _translator.TranslationComplete += _translator_TranslationComplete;
8: _translator.TranslationFailed += _translator_TranslationFailed;
9: }
Let’s start our translation service in our ApplicationBarIconButton event handler. We’ll hard code the language information for now from English to Spanish, but if you need a list of supported language codes, check out this… um… list of supported language codes.
1: private void On_CheckClicked(object sender, System.EventArgs e)
2: {
3: _translator.GetTranslation(sourceTextBox.Text, "en", "es");
4: }
And all we need to do is handle the resulting events and we’re done. The TranslationService will return the information off the UI thread, so in order to pass the information along without throwing a thread exception, Deployment.Current.Dispatcher.BeginInvoke to the rescue!
1: void _translator_TranslationComplete(object sender, TranslationCompleteEventArgs e)
2: {
3: Deployment.Current.Dispatcher.BeginInvoke(() =>
4: {
5: targetTextBox.Text = e.Translation;
6: });
7: }
8:
9: void _translator_TranslationFailed(object sender, TranslationFailedEventArgs e)
10: {
11: Deployment.Current.Dispatcher.BeginInvoke(() =>
12: {
13: MessageBox.Show("Bummer, the translation failed. \n " + e.ErrorDescription);
14: });
15: }
And we’re done! Don’t forget, you can grab the entire project on github. So check it out.
Thanks to Laurence Moroney of netNavi.tv for his post on this topic that helped me fill in some key missing pieces.