Localizing Custom Mapping Data With Bing Translator

The Bing Maps for Enterprise platform offers extensive localization capabilities for map navigation, map labels, directions, and geocoding results in the Bing Maps Web map control, Windows Store map control, and REST Services. Typically, you will also have your own custom data that you will be displaying within your mapping application. In this post, we will show how you can use another of the Bing Developer Services – the Bing Translator Control and Microsoft Translator API – to localize your own custom content within your Bing Maps applications. We will use the Microsoft Translator API to demonstrate how we can localize custom content in a web application using our Web Map map control, and we will also use the Bing Translator Control to show how we can localize custom content in native Windows Store apps.

Prerequisites for building our applications include:

Signing Up for Microsoft Translator on Windows Azure Marketplace

As outlined in the Bing Developer Center, the Translator Service offers easy access to robust, cloud-based, automatic translation between more than 40 languages. But before we can get started with building our application, we must subscribe to the Translator Service through the Windows Azure Marketplace, Microsoft’s one-stop shop for premium data and applications.

A detailed walkthrough of the process of signing up for the Translator service and obtaining credentials is provided here. To briefly summarize the process, you must:

  1. Sign in and register for the Windows Azure Marketplace
  2. Subscribe to the Microsoft Translator API
  3. Register an application on the Windows Azure Marketplace
  4. Obtain your authentication credentials (client ID and client secret)

Creating Our Sample Custom Data Source

In both of our sample applications, we will be pulling in data from a very simple custom data source that is hosted in Bing Spatial Data Services. The data source will contain only five fields: a unique EntityId field; Latitude and Longitude for location; and Name and Description fields, which will be translated in our apps. You can download the csv file which can be uploaded via the Data Source Management API to your own custom data source. Alternately, the code samples are already configured to access the data from an existing publicly accessible data source containing this data.

Creating a Web Application with AJAX v7 and Translator API

The first sample application we will build will be a web application that accesses the sample data via the Query API, and displays the locations as pushpins on the AJAX v7 map control. When the pushpins are clicked, the Name and Description content for the location are translated via the Translator API.

We will now create a simple project using the ASP.NET Empty Web Application template in Visual Studio 2012.

The first thing we will add to our application is a Token.cs class which helps us obtain an access token for the Translator API. The access token is obtained by making an HTTP POST request to a token service, passing the client ID and client secret that we previously obtained when signing up for the Translator service. We will populate this class with the sample code provided in the Translator SDK, here.

The Token.cs class also requires us to add the following references to our project:

  • System.Runtime.Serialization
  • System.ServiceModel

To our application, we will add a C# web form called translate.aspx. To the code-behind, we will add the following code, which will use our client ID and client secret to obtain an access token, which is added as a property that will be accessible in our JavaScript code. Populate the placeholders with your own Translator credentials:


 
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls;
namespace BingMapsTranslator
{
   public partial class translate : System.Web.UI.Page 
   {
        protected void Page_Load(object sender, EventArgs e)
        {
            AdmAuthentication admAuth = new AdmAuthentication("client  id", client 
secret");
            AdmAccessToken token =  admAuth.GetAccessToken();
            Response.Write(string.Format(@" 
            <script  type=""text/javascript""> 
                window.accessToken =  ""{0}""; 
            </script>", token.access_token));
        }
   }
} 

Now we will begin adding code to our translate.aspx web form. Aside from our JavaScript code, the page layout will be very basic. We have a map element which occupies the entire page, and we link to the Bing Maps AJAX v7 map control. Note how we are using a mkt parameter to localize the map navigation and labels in French. Also note how we will call the GetMap function when the page loads:


 
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="translate.aspx.cs"
Inherits="BingMapsTranslator.translate" %> 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html> 
<head> 
    <title>Translate Custom SDS  Data Demo</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 
    <style type="text/css"> 
        html 
       {
           overflow:hidden;
           font-family: Verdana;
           font-size: 12px;
           width:100%;
           height:100%;
       }
   
       body 
      {
          width:100%; 
          height:100%; 
          margin:0 0 0 0;
       }
   </style> 
   <script type="text/javascript" 
src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&mkt=fr-
FR"></script> 
</head> 
<body onload="GetMap();"> 
   <div id='myMap' style="position:absolute; width:100%; height:100%;"></div> 
</body> 
</html>  

To our web form, we will add some JavaScript to declare some global variables, and to define our GetMap function which will instantiate our map. Note that we also add an event handler to allow us to query our data source when the map view changes. We also extend the Pushpin class with title and description properties, which will be used to hold data to be displayed later. You will need to substitute your own Bing Maps key in the placeholder for the bmKey value:


 
// Configure global variables: var bmKey = "insert Bing Maps Key here"; var map = null; var MM = Microsoft.Maps; var datasourceLayer = null; var infoBoxLayer = null;
// Instantiate the map 
  GetMap() {
   map = new MM.Map(document.getElementById('myMap'), {
       credentials: bmKey,
       mapTypeId: MM.MapTypeId.road,
       center: new MM.Location(43.4702518, -80.7727581),
       zoom: 6
   });
   // Add Event to search for  POI when map moved: 
   Microsoft.Maps.Events.addThrottledHandler(map, 'viewchangeend', QueryDatasource, 
2000);
   // Extend Pushpin class with  title and description properties: 
   MM.Pushpin.prototype.title = null;
   MM.Pushpin.prototype.description = null;
}

In our QueryDatasource event handler function, which is called when the map view changes, we obtain the credentials from our map session, specifying QueryDatasourceRequest as our callback. QueryDatasourceRequest receives the credentials, and uses them in a request to the Spatial Data Services Query API. The current bounds of the map are obtained, and used to query our sample data using a bounding box spatial filter, and specifying json as the output format. The request is issued with the CallRestService function, which adds the request URL as the source for a JavaScript resource. We specify callbackQueryDatasource as our callback function in our request:


 
// retrieve map credentials and specify callback function:  function QueryDatasource() { map.getCredentials(QueryDatasourceRequest); }
// retrieve custom data in the current  map view: 
function QueryDatasourceRequest(credentials) {
   // Get Map bounds for spatial  query by bounding box: 
   var bounds = map.getBounds();
   var south = bounds.getSouth();
   var north = bounds.getNorth();
   var east = bounds.getEast();
   var west = bounds.getWest();
   // SDS data source details: 
   var sdsAccessId = "8687519045ca4eeabafb179cfe28e545";
   var sdsDataSourceName = "SamplePropertyData";
   var sdsEntityTypeName = "SampleProperties";
   // format request to Query  API, using bounding box spatial filter, and specifying 
callback 
   var request = "http://spatial.virtualearth.net/REST/v1/data/" +
   sdsAccessId + "/" + sdsDataSourceName + "/" + sdsEntityTypeName + "?" +
   "key=" + credentials +
   "&$format=json" +
   "&jsonp=callbackQueryDatasource" +
   "&spatialFilter=bbox(" + south + "," + west + "," + north + "," + east + 
")&$select=*&$top=250";
   // Call rest service 
   CallRestService(request);
} 
// function for making requests to REST  APIs: 
function CallRestService(request) {
   var script = document.createElement("script");
   script.setAttribute("type", "text/javascript");
   script.setAttribute("src", request);
   document.body.appendChild(script);
}  

In our callbackQueryDatasource function, we check to make sure we have received results, and then loop through each of the individual entities, adding a pushpin for each, using the coordinates to place the pushpin, and adding the Name and Description to our pushpin title and description properties.

We also add a pushpin click event handler, translateInfoBox, to each pushpin:


 
// display the custom data as pushpins on the map:  function callbackQueryDatasource(result) { // Ensure we have results:  if (result && result.d && result.d.results) { try {  // configure our entity collections:   try { datasourceLayer.clear(); infoBoxLayer.clear(); } catch (err) { datasourceLayer = new MM.EntityCollection; infoBoxLayer = new MM.EntityCollection; } // for each result, create a pushpin and add it to the map:   for (i = 0; i < result.d.results.length; i++) {  var pushpinOptions = {}; var pushpin = new MM.Pushpin(new
MM.Location(result.d.results[i].Latitude, result.d.results[i].Longitude), pushpinOptions);
    // Add title and description to pin: 
          pushpin.title =  result.d.results[i].Name;
          pushpin.description =  result.d.results[i].Description;
     // Add event handler to translate content when pushpin clicked 
         pushpinClick =  MM.Events.addHandler(pushpin, 'click', 
translateInfoBox);
        // Add pin to entity collection 
        datasourceLayer.push(pushpin);
      }
      // Add entity collections to map: 
      map.entities.push(datasourceLayer);
      map.entities.push(infoBoxLayer);
  }
  catch (err) {
  }
 }
 else {
    alert('An error  occurred.');
 }
}  

Our translateInfoBox function is called when a pushpin is clicked, and we construct a request to the Translator API AJAX interface, using the TranslateArray method, which allows us to translate an array of individual text blocks. The parameters that we pass in the request include:

  • appId – our access token, retrieved from window.accessToken, which we previously populated after using the token service with our credentials
  • from – the language we are translating from; we hardcode this as ‘en’, as our data source contains content in English
  • to – the language we are translating to; in this instance, we hardcode this as ‘fr’ to translate into French; note that we can use the GetLanguagesForTranslate method of the Translator API to dynamically retrieve all possible languages if desired
  • texts – an array of the text blocks we wish to translate, including the pushpin title and description properties
  • options – we can specify a number of optional options, including State; we use the State option to correlate the response to our request with the appropriate pushpin
  • oncomplete – we specify our callback function, showInfoBox

Note that the supported cultures for the Bing Maps APIs and map controls differ from the languages supported by the Translator APIs. In a production application, some consideration should be given to the overall application localization you wish to support, and associating the localization and translation parameters as appropriate.

We use our previously shown CallRestService function to issue the request:


 
function translateInfoBox(e) { if (e.targetType = "pushpin") { // Specify languages to translate to and from //  Note that you can use the GetLanguagesForTranslate method to  //  dynamically retrieve supported languages var to = "en"; var from = "fr";
   // obtain the pushpin index  in the entity collection, and send it in the 
   //  state parameter, enabling us to show the  infobox next to the correct 
pushpin: 
   var options = "{"State":"" + datasourceLayer.indexOf(e.target) + ""}";
   // create an array of text  blocks to translate: 
   var texts = "["" + e.target.title + "","" + e.target.description + ""]";
   // format a request to the  AJAX API, using the TranslateArray method: 
   var translateRequest = 
"http://api.microsofttranslator.com/V2/Ajax.svc/TranslateArray" +
                        "?appId=Bearer " + encodeURIComponent(window.accessToken)  +
                        "&from=" + encodeURIComponent(to) +
                        "&to=" + encodeURIComponent(from) +
                        "&texts=" + encodeURIComponent(texts) +
                        "&options=" + encodeURIComponent(options) +
                        "&oncomplete=showInfoBox";
   // Issue the request 
   CallRestService(translateRequest);
  }
}  

In our showInfoBox callback function, we receive the response from the TranslateArray request. We use the State attribute in the response to correlate the response to the correct pushpin. We then instantiate an Infobox, and use the translated string elements in the response array to display the translated name and description for the location:


 
// callback from translator request, to display infobox with translated text:  function showInfoBox(response) {  // retrieve the pushpin's index in the datasource layer from the //  State attribute in the response: var pushpin = datasourceLayer.get(response[0].State); //Add the infobox, displaying the translated Name as the title, and the //  translated Description as the description:  infoBoxLayer.clear(); infobox = new MM.Infobox(pushpin.getLocation(), { title: response[0].TranslatedText, description: response[1].TranslatedText }); infoBoxLayer.push(infobox);
}  

When we hit F5 in Visual Studio to run our application, we now see not only French-labeled navigation in our map control, but French translations for our infobox content as well:

LocalizingCustomMappingDataImg1

Creating a Windows Store Application with the Native Bing Maps Control and the Translator Control

The second sample application we will build will be a native Windows Store application that will access the same sample data via the Query API, and displays the locations as pushpins on the native Bing Maps for Windows Store Apps control. When the pushpins are clicked, the Name and Description content for the location are translated via the Translator Control.

Before we start, we must download and install the Translator Control for Visual Studio.

In Visual Studio 2012, we will first create a new project using the Visual C# Windows Store Blank App (XAML) template, and will name our project TranslateControl. We will also choose our platform target in the project Build properties as desired.

We now add the following references to our project:

  • Bing Maps for C#, C+ + or Visual Basic
  • Microsoft Visual C+ + Runtime Package
  • Bing Translator Control

Our UI will be very basic, and will leverage the infobox capabilities previously presented in the Infoboxes for Native Windows Store Apps post. We will copy the same XAML code from the previous post, and to it we will add an XML namespace declaration for Bing.Translator, a TranslatorControl with our credentials included, and will set our Culture property to fr-FR, to display our navigation controls and some map labels in French. Our final XAML code in MainPage.xaml will be as shown below. Substitute your own credentials for the Translator Control client id and client secret, as well as for the Bing Maps key:


 
<Page x:Class="TranslatorControl.MainPage"  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  xmlns:local="using:TranslatorControl" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:bt="using:Bing.Translator" xmlns:m="using:Bing.Maps" mc:Ignorable="d"> 
    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> 
         <bt:TranslatorControl ClientId="client id" ClientSecret="client secret" 
x:Name="myTranslator"/> 
        <m:Map x:Name="myMap" Credentials="insert Bing Maps Key" Culture="fr-FR"> 
            <m:Map.Children> 
<!-- See  http://www.bing.com/blogs/site_blogs/b/maps/archive/2013/06/17/infoboxes-for-native-
windows-store-apps.aspx  for infobox post--> 
<!-- Data Layer--> 
<m:MapLayer Name="DataLayer"/> 
                <!--Common Infobox--> 
                <m:MapLayer> 
                   <Grid x:Name="Infobox" Visibility="Collapsed" Margin="0,-115,-
15,0"> 
                       <Border Width="300" Height="110" Background="Black" 
Opacity="0.8" BorderBrush="White" BorderThickness="2" CornerRadius="5"/> 
                        <StackPanel  Height="100" Margin="5"> 
                            <Grid Height="40"> 
                                <TextBlock Text="{Binding  Title}" FontSize="20" 
Width="250" TextWrapping="Wrap" HorizontalAlignment="Left" /> 
                                <Button  Content="X" Tapped="CloseInfobox_Tapped" 
HorizontalAlignment="Right" VerticalAlignment="Top"/> 
                            </Grid> 
                            <ScrollViewer HorizontalScrollBarVisibility="Auto"  
VerticalScrollBarVisibility="Auto" MaxHeight="60"> 
                                <TextBlock Text="{Binding  Description}" FontSize="16" 
Width="290" TextWrapping="Wrap" Height="Auto"/> 
                            </ScrollViewer> 
                        </StackPanel> 
                    </Grid> 
                </m:MapLayer> 
            </m:Map.Children> 
        </m:Map> 
    </Grid> 
</Page> 

In our MainPage.xaml.cs code-behind, we add the following using statements:


 
using Bing.Maps; using Bing.Translator; using System.Net; using Windows.Data.Json;
using System.Threading.Tasks; 

We now update our constructor to set the map view, and to call the LoadSDSData method to retrieve our data:


 
public MainPage() { this.InitializeComponent(); // Set map view: myMap.SetView(new Location(41.4702518,-80.7727581), 6, MapAnimationDuration.None); // Load data from Spatial Data Services: LoadSDSData();
}  

In our LoadSDSData method, we mark the method with the async modifier, to allow us to request data from the Spatial Data Service asynchronously with the await operator. The method retrieves the current map bounds, and constructs a request URI for the Query API, using our sample data source, and using a bounding box spatial filter. We also specify the response format as json. The request is issued in the GetResponse method.

For simplicity, a StreamReader object is used to read the response stream, with the response parsed into a JsonObject. We use the tools provided in the System.Json namespace to retrieve the coordinates, name and description for each result. We then add a pushpin for each result with the AddPushpin method, as taken from the previously referenced infobox post:


 
private async void LoadSDSData() { // get map bounds:  LocationRect lr = myMap.Bounds; string north = lr.North.ToString(); string east = lr.East.ToString(); string south = lr.South.ToString(); string west = lr.West.ToString();
   // SDS data source details: 
   string sdsAccessId = "8687519045ca4eeabafb179cfe28e545";
   string sdsDataSourceName = "SamplePropertyData";
   string sdsEntityTypeName = "SampleProperties";
   // retrieve credentials for  use in SDS request: 
   string credentials = await myMap.GetSessionIdAsync();
   // build request to SDS Query  API, filtering by bounding box: 
   string uriString = 
string.Format("http://spatial.virtualearth.net/REST/v1/data/{0}/{1}/{2}?" +
   "key={3}&$format=json&spatialFilter=bbox({4},{5},{6},{7})&$select=*&$top=250",
   sdsAccessId, sdsDataSourceName, sdsEntityTypeName, credentials, south,  west, 
north, east);
    Uri queryRequest = new Uri(uriString);
    try 
     {

   // Obtain response as  JsonObject: 
   JsonObject j = await  GetResponse(queryRequest);
   // Retrieve Array of results: 
   JsonObject d = (JsonObject)j.GetNamedObject("d");
   JsonArray ja = (JsonArray)d.GetNamedArray("results");
   
   // for each result, create a  pushpin using the AddPushpin method 
   int arraySize = ja.Count;
   for (int i = 0; i <  arraySize; i++)
   {
        AddPushpin(new Location(ja[i].GetObject().GetNamedNumber("Latitude"), 
ja[i].GetObject().GetNamedNumber("Longitude")),
        ja[i].GetObject().GetNamedString("Name"), 
ja[i].GetObject().GetNamedString("Description"), DataLayer);
        }
   }
   catch (Exception e)
   {
       // TO-DO: implement error  handling: 
       return;
   }           
} 
private async Task<JsonObject> GetResponse(Uri uri)
{
   // issue Query API request  with HttpClient: 
   System.Net.Http.HttpClient client = new  System.Net.Http.HttpClient();
   var response = await client.GetAsync(uri);
   // read response with  StreamReader, and parse string into JsonObject: 
using (var stream = await response.Content.ReadAsStreamAsync())
   {
        StreamReader streamRead = new StreamReader(stream);
        string responseString = streamRead.ReadToEnd();
        return JsonObject.Parse(responseString)  as JsonObject;
   }
}  

We re-use the pushpin and infobox code from the previous infobox post, with one modification: we use the Translator Control to asynchronously translate our entity name and description, using the TranslateAsync method of the Translator Library. To this method, we pass the following parameters:

  • From language – our source data is in English, so we pass in ‘en’
  • To language – we will translate our data into French with a value of ‘fr’
  • Category – domain of the translation; we use the default category of ‘general’
  • Text to translate – our entity name or description

 


 
// configure to and from languages for translation:  string fromLang = "en"; string toLang = "fr";
// translate title and description  asynchronously: 
TranslationResult transTitle = await 
myTranslator.TranslatorApi.TranslateAsync(fromLang, toLang, "general", m.Title);
TranslationResult transDesc = await myTranslator.TranslatorApi.TranslateAsync(fromLang, 
toLang, "general", m.Description);
m.Title = transTitle.TextTranslated;
m.Description = transDesc.TextTranslated;

Note that we could also retrieve a list of available languages for translation through the GetLanguagesAsync method of the Translator Library, if desired. The full pushpin and infobox code, including our translation updates, is shown below:


 
#region pushpin public void AddPushpin(Location latlong, string title, string description, MapLayer layer) {
    Pushpin p = new Pushpin()
     {
         Tag = new Metadata()
         {
             Title = title,
             Description = description
         }
     };
    MapLayer.SetPosition(p, latlong);
    p.Tapped += PinTapped;
    layer.Children.Add(p);
}
public class Metadata 
{
   public string Title { get; set; }
   public string Description { get; set; }
   }
private async void PinTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
   {
   Pushpin p = sender as Pushpin;
   Metadata m = (Metadata)p.Tag;
   // configure to and from  languages for translation: 
   string fromLang = "en";
   string toLang = "fr";
   // translate title and description  asynchronously: 
   TranslationResult transTitle = await 
myTranslator.TranslatorApi.TranslateAsync(fromLang, toLang, "general", m.Title);
   TranslationResult transDesc = await 
myTranslator.TranslatorApi.TranslateAsync(fromLang, toLang, "general", m.Description);
    m.Title = transTitle.TextTranslated;
    m.Description = transDesc.TextTranslated;
    //Ensure there is content to  be displayed before modifying the infobox control 
    if (!String.IsNullOrEmpty(m.Title)  || !String.IsNullOrEmpty(m.Description))
    {
        Infobox.DataContext = m;
        Infobox.Visibility = Visibility.Visible;
        MapLayer.SetPosition(Infobox, MapLayer.GetPosition(p));
   }
   else 
   {
      Infobox.Visibility = Visibility.Collapsed;
   }
}
private void CloseInfobox_Tapped(object sender,
Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
   Infobox.Visibility = Visibility.Collapsed;
}
#endregion  

As with our previous application, when we hit F5 in Visual Studio, we now see not only French navigation in our map control, but French translations for our infobox content as well:

LocalizingCustomMappingDataImg2

We have shown how we can use the power of Bing as a developer platform to not only map our custom location data on the web and in Windows Store apps, but to also localize our custom mapping data and other application content for global audiences, using a world-class machine translation system built on over a decade of natural language research from Microsoft Research.

The samples in this application use the Translator Service to translate text at application runtime. When developing your application, you will want to consider whether your consumption of the service can be optimized by translating text as part of your content editorial workflow instead. Considerations to take into account include:

  • The frequency with which your custom content changes
  • The number of languages you wish to support with translations
  • The degree to which your custom data source infrastructure supports storing content for multiple languages
  • The frequency with which translations will be requested by end users

The complete code for both projects, as well as the sample SDS data source can be found here.

– Geoff Innis, Bing Maps Technical Specialist