Translator Starter Kit for Windows Phone

[This documentation is preliminary and is subject to change.]

July 06, 2012

This Windows Phone Starter Kit is a complete translator application written in C#. The program provides the user with the ability to enter a word or phrase in one language and have it translated to another language. The program can also play an audio clip of the translated text. As a convenience, the program also provides a list of commonly used phrases. The supported languages are English, French, Italian, Spanish, and German.

Note

To make a successful call to the Microsoft Translator service, you must obtain a Bing API AppID. For information about obtaining a Bing API AppID, see Getting Your AppID. Once you have a Bing API AppID, you should store it in the TextToSpeechID and TextTranslateID value fields of the Strings.resx file found in the Localization folder of the project.

Goals

Getting Started

Program Files

Calling the Web Service

Using the XNA Framework To Play Audio

Storing a History List In Isolated Storage

Animating the Text To Speech Button

Extending the Translator Application

Note

This documentation assumes that you have a basic knowledge of C# programming concepts and the Windows Phone SDK. You can download the Windows Phone SDK here. The code for this Translator Starter Kit can be downloaded here.

Goals

After reading through this topic, you will understand how the Translator program works. You will also understand a few ways in which you can customize it using the Windows Phone SDK. This starter kit demonstrates how to:

  • Call a web service.

  • Use an XNA Framework API from within a Silverlight application to play a sound.

  • Maintain a history list in isolated storage.

  • Animate an image in a ToggleButton.

Top

Getting Started

To compile and run the Translator Starter Kit:

  • Download and unzip the Translator Starter Kit.

  • Open the Translator.sln solution file in Visual Studio.

  • Build the Translator application.

  • Run the program in the emulator or on a device. You can translate any phrase in the history list. To do additional translations, see Getting Your AppID for information about obtaining a Bing API AppID. Once you have a Bing API AppID, you should store it in the TextToSpeechID and TextTranslateID value fields of the Strings.resx file found in the Localization folder of the project.

Top

Program Files

The Translator application uses the Microsoft Translator web service to translate text from one language to another. Users first set the language they want to translate from and the language the program should translate the text into. After the user enters text into a TextBox and presses the translate button, the program calls the Microsoft Translator web service to translate the text and display the results in another TextBox. A second call to the Microsoft Translator web service converts the translated text to an audio file, which the user can then listen to by pressing the speaker button.

The program also maintains a history of translated phrases. This history list is initially seeded with about 50 commonly used phrases and their translations. When a new phrase has been successfully translated, it is added to the history list. This list is stored in isolated storage.

Main Program

  • App.xaml.cs - Contains the App() method – the location where the program begins execution and where event handlers are initialized.

AppSettings

  • IsoStoreAppSettings.cs - Base class to manage saving and restoring settings in the isolated storage.

Content

  • This folder contains the default phrases and translations supplied for the history list. It also includes .wav file translations for all the phrases in each supported language.

ContextMenu

  • ContextMenu.cs - Implements a context menu.

  • ContextMenu.generic.xaml - Contains styles and templates for the context menu.

  • ContextMenuService.cs - Helps to attach a context menu onto a control.

HistoryList

  • HistoryList.cs - Maintains the history list of translated phrases.

  • Phrases.cs - The history list is a collection of phrases.

  • Translation.cs - Each phrase contains a list of translations for each language.

HistoryPage

  • HistoryPage.xaml.cs - Displays the history list of translated phrases.

LanguagePickerPage

  • LanguagePickerPage.xaml.cs - Displays a list of languages to translate to or from.

MainPage

  • MainPage.xaml.cs - Displays the main page for entering phrases and getting them translated.

Model

  • Model.cs - Contains the data model for the application. It consists of the application settings, the history list, and the available languages list.

Settings

  • AppSettings.cs - Contains information about the applications settings, in this case the languages to translate from and to.

  • LanguageInfo.cs - Contains information about a particular language.

  • SoundFilenameGenerator.cs - Generates a unique filename for a language sound file.

Speech

  • Speech.cs - Class to use the cloud to get a wave file of text translated to speech, and play it back.

Themes

  • Generic.xaml - Helps to bring in the ContextMenu.generic.xaml.

Tilt

  • TiltEffect.cs - Provides attached properties for adding a 'tilt' effect to all controls within a container. To learn more about implementing the ‘tilt’ effect, see Control Tilt Effect for Windows Phone.

Tool

  • PhoneUtils.cs - Contains some utility functions for reading state information.

Translator

  • Translator.cs - Class to use the cloud services to translate text from one language to another.

WebRequestHelper

  • WebRequestHelper.cs - Stateless Helper class to handle an asynchronous web request.

Top

Key Concepts

Calling the Web Service

Translator.cs contains the code for calling the translation web service. When a translation has been requested, the history list is checked first. If the phrase does not exist in the history list, then an async call to the web service is made. CallWebApi sets up a URI and sends it to be called in InitiateRequest.

        /// <summary>
        /// Calls async web api to get the text translation.
        /// </summary>
        /// <param name="fromString">From text</param>
        /// <param name="fromLang">From language</param>
        /// <param name="toLang">To language</param>
        /// <returns>True if a web call was made, false if the translator already had this phrase cached away.</returns>
        protected bool CallWebApi(
            string fromString,
            string fromLang,
            string toLang
            )
        {
            bool callMade = false;

            if (
                (String.Compare(fromString, _lastPhraseString, StringComparison.CurrentCultureIgnoreCase) != 0)
                ||
                (fromLang != _lastFromLanguage) || (toLang != _lastToLanguage)
                ||
                (App.Model.TestingParameterForceAllWebCalls == true)
                )
            {
                _lastPhraseString = fromString;
                _lastFromLanguage = fromLang;
                _lastToLanguage = toLang;
                callMade = true;

                //
                // If there was already a translate request out on the wire, do not bother listening to it any more.
                //
                if (_currentRequest != null)
                {
                    _currentRequest.Cancelled = true;
                }

                _currentRequest = InitiateRequest(GetUriRequest(fromString, fromLang, toLang), fromString, fromLang, toLang);
            }

            return callMade;
        }

GetUriRequest builds the URI based on the given phrase and the languages to translate from and to.

        /// <summary>
        /// Builds the URI for the Web call.
        /// </summary>
        /// <param name="phrase"></param>
        /// <param name="fromLang"></param>
        /// <param name="toLang"></param>
        /// <returns>The URI</returns>
        private string GetUriRequest(string phrase, string fromLang, string toLang)
        {
            string apiFormat = LocalizationStrings.Strings.TextTranslateURI;
            string uriRequest =
                  String.Format(
                    apiFormat,
                    LocalizationStrings.Strings.TextTranslateID,
                    Uri.EscapeDataString(phrase),
                    fromLang,
                    toLang
                    );
            return uriRequest;
        }

InitiateRequest takes the passed in URI and makes the web service call to translate the text. If the translation is successful, the phrase is added to the history list and GetTextToSpeech will initiate a second web service call to translate the text to speech.

        /// <summary>
        /// Makes actual call to cloud to get translation. When the result is retrieved,
        /// the HistoryList is updated, and the Speech object is set into motion to
        /// get the text translated to speech.
        /// </summary>
        /// <param name="uriRequest">URI</param>
        /// <param name="fromString">From text</param>
        /// <param name="fromLang">From langauge</param>
        /// <param name="toLang">To language</param>
        /// <returns>A WebRequestHelper.RequestInfo which contains information about the current request in the cloud.</returns>
        private WebRequestHelper.RequestInfo InitiateRequest(
            string uriRequest,
            string fromString,
            string fromLang,
            string toLang
            )
        {
            // The signature for SendStringRequest looks like this:
            //
            //  public static RequestInfo 
            //      SendStringRequest(
            //          string uriString,
            //          Action sent,
            //          Action<string> received,
            //          Action<string> failed );
            //
            WebRequestHelper.RequestInfo returnValue =
                    WebRequestHelper.SendStringRequest(
                        uriRequest,
                        () => // Sent()
                        {
                            // Update the UI to let the user know the request is out on the wire.
                            ToPhrase = LocalizationStrings.Strings.Sending;
                            ResultValid = false;
                            App.Speech.ResultValid = false;
                        },
                        (resultXML) => // Received(string resultXML)
                        {
                            // This code is called from the WebRequest's thread, so anything that touches the UI
                            // will need to be marshalled.
                            string translatedText = ParseResult(resultXML);

                            if (string.IsNullOrEmpty(translatedText))
                            {
                                ReportWebError();
                            }
                            else
                            {
                                // There are going to be cases where the translator service does not
                                // translate a word, and it returns the source text. In these cases,
                                // we do not want to save away the translation. That way, if the translator
                                // service is updated in the future to correctly translate that word,
                                // the correct translation will be stored in the history list. Otherwise,
                                // the incorrect translation will be cached away and the user will never
                                // get to benefit.
                                if (String.Compare(fromString, translatedText, StringComparison.CurrentCultureIgnoreCase) != 0)
                                {
                                    App.Model.AddTranslatedText(fromString, fromLang, translatedText, toLang);
                                }
                                else
                                {
                                    // And, if the phrase was not cached, reset the last phrase so that each subsequent call
                                    // will hit the web again (this value is used to optimize web calls at the CallApi method)
                                    _lastPhraseString = string.Empty;
                                }
                                // Get the result and update the UI.
                                // Since both of these properties use the NotifyPropertyChanged
                                // method, which uses BeginInvoke to ensure the call is made on the
                                // UI thread, we do not need to marshall code here.
                                ToPhrase = translatedText;
                                ResultValid = true;

                                // Send the Speech object on its way to get the text to speech.
                                App.Speech.GetTextToSpeech(
                                    fromString,
                                    translatedText,
                                    fromLang,
                                    toLang
                                    );
                            }

                        },
                        (errorMsg) => // Failed(string errorMsg)
                        {
                            ReportWebError();
                        }
                    );

            return returnValue;
        }

Top

Using the XNA Framework To Play Audio

The method SpeakResult in Speech.cs plays the wave file of the translation. The _player object of type DynamicSoundEffectInstance is used to play the audio buffer.

        /// <summary>
        /// Converts the wave into a format playable by the XNA player and plays it.
        /// </summary>
        /// <param name="soundBytes"></param>
        private void SpeakResult(byte[] soundBytes)
        {
            byte[] upscaled = ConvertTo16BitAudio(soundBytes, cHeaderSize, soundBytes.Length - cHeaderSize);

            if (upscaled.Length > 0)
            {
                if (_xnaUpdateTimer == null)
                {
                    //
                    // The XNA framework has some code that assumes a game loop is running.
                    // Playback of wave files is one of them, and without calls to FrameworkDispatcher.Update(),
                    // the wave files could get garbled, and the BufferNeeded event will not fire. Since this class
                    // needs the BufferNeeded event, and since the user wants a clean wave file playback, this
                    // snippet of code will call the FrameworkDispatcher.Update() method at a high frequency.
                    // The timer is turned on when the _player starts to play, and it is turned off when the
                    // BufferNeeded is called. This preserves battery life.
                    //
                    _xnaUpdateTimer = new DispatcherTimer();
                    _xnaUpdateTimer.Interval = new TimeSpan(0, 0, 0, 0, 25);
                    _xnaUpdateTimer.Tick += (s, e) =>
                    {
                        FrameworkDispatcher.Update();
                    };
                }

                // Initialize the player.
                if (_player == null)
                {
                    _player = new DynamicSoundEffectInstance(8000, AudioChannels.Mono);
                    _player.BufferNeeded += OnPlayerBufferNeeded;
                }
                // Need to send the first Update immediately, as the timer will not send one for a period.
                FrameworkDispatcher.Update();
                _ignoreNextBufferNeeded = true;
                _player.SubmitBuffer(upscaled);
                _player.Play();
                FrameworkDispatcher.Update();
                // Turn on the XNA timer to ensure the wave file is ungarbled.
                _xnaUpdateTimer.Start();

                IsSpeaking = true;
            }
        }

Top

Storing a History List in Isolated Storage

The HistoryList contains the previously translated phrases. It consists of a collection of phrases, with each phrase containing a list of translations for that phrase into various languages. An initial set of phrases for the HistoryList is provided by the application.

The Load method in HistoryList.cs loads the HistoryList from isolated storage. If the HistoryList file does not exist in isolated storage, then the application provided list of phrases is loaded.

        /// <summary>
        /// Loads the HistoryList from Isolated Storage. If there is no file in isolated storage,
        /// then the list if prepopulated with canned phrases.
        /// </summary>
        /// <returns>A HistoryList that is ready for use</returns>
        public static HistoryList Load()
        {
            HistoryList returnValue = null;

            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (store.FileExists(Filename))
                {
                    IsolatedStorageFileStream stream = store.OpenFile(Filename, System.IO.FileMode.Open);
                    XmlSerializer serializer = new XmlSerializer(typeof(HistoryList));
                    try
                    {
                        returnValue = (HistoryList)serializer.Deserialize(stream);
                        stream.Close();
                    }
                    catch (SerializationException se)
                    {
#if DEBUG
                        Debug.WriteLine(se.ToString());
#endif
                    }
                }
            }

            if (returnValue == null)
            {
                returnValue = GetDefaultHistoryList();
            }

            return returnValue;
        }

Save the HistoryList to isolated storage by serializing the HistoryList object.

        /// <summary>
        /// Saves the HistoryList to isolated storage.
        /// </summary>
        public void Save()
        {
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (store.FileExists(Filename))
                {
                    store.DeleteFile(Filename);
                }

                IsolatedStorageFileStream stream = store.CreateFile(Filename);
                XmlSerializer serializer = new XmlSerializer(typeof(HistoryList));
                serializer.Serialize(stream, this);
                stream.Close();
            }
        }

Top

Animating the Text To Speech Button

After a phrase has been translated and a wave file of the translation has been successfully retrieved, the Speech button is enabled. If the user presses this button, the wave file will play and the button will display an animated speaker for as long as the wave file is playing.

The Speech ToggleButton and its storyboarded animation are defined in MainPage.xaml. A DependencyProperty IsSpeaking is defined in MainPage.xaml.cs to maintain the state information.

        // Using a DependencyProperty as the backing store for IsSpeaking.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsSpeakingProperty =
            DependencyProperty.Register(
                "IsSpeaking", // Name of the property in this class
                typeof(bool),   // Property type
                typeof(MainPage), // Owner of the property (i.e. MainPage.IsSpeaking)
                new PropertyMetadata(false, IsSpeaking_PropertyChangedCallback)  // Defaults to false, and the IsSpeaking_PropertyChangedCallback is called when the prop changes
                );

The value of IsSpeaking is set to true in the SpeakResult method in Speech.cs when a wave file starts playing. IsSpeaking is set to false when the wave file has finished playing in OnPlayerBufferNeeded or when the Speech object is disposed of in Dispose.

When the value of IsSpeaking changes, the state of the button and its animation is updated.

        /// <summary>
        /// When the speaking changes, update the visuals
        /// </summary>
        /// <param name="dependancyObject"></param>
        /// <param name="e"></param>
        static void IsSpeaking_PropertyChangedCallback(DependencyObject dependancyObject, DependencyPropertyChangedEventArgs e)
        {
            MainPage mainPage = dependancyObject as MainPage;
            mainPage._btnToSpeech.IsChecked = (bool)e.NewValue;
            mainPage._btnToSpeech.IsEnabled = !(bool)e.NewValue;
        }

Top

Extending the Translator Application

Here are some suggested ideas to extend the functionality of the Translator application.

  • Add support for landscape mode.

  • Add support for additional languages.

Top

See Also

Other Resources

Windows Phone Development

The Windows Phone Developer Blog