Introduction
Translation capabilities are commonplace on the web. There are browser add-ons and plenty of translation APIs available. With a global web and marketplace, it’s important to have the ability for users to receive information in their preferred language.
The popular choice is using Google Translate to translate webpage components. However, there are many other interesting APIs available that utilize machine learning and other powerful programming techniques. One of these other translation APIs that I want to explore today is Microsoft Text Translation.
In addition to normal translation capabilities, this API offers a text-to-speech translation endpoint that returns an audio file of the submitted text. That means that the translated text is read back to the user by a Native speaker of the supported language. It’s not a typical feature that I have seen on the web and can be set up quite easily.
To showcase how to set-up the translation capability I have put together an example app that we are going to add the API calls and translation to. We will be combining the powerful features of ReactJS and RapidAPI to achieve the desired result.
New to RapidAPI?
If this is your first time using RapidAPI, you will need to sign up for an account. In addition, you will need to subscribe to the API. Fortunately, I will cover subscribing to the API and using the dashboard in the Prerequisites section and further down in the example app section below.
How to use the Microsoft Text Translation API with JavaScript
How To Add Native Speaking Audio Translations For 80 Different Languages To Your App
View the final project code on Github
I created a starter project hosted on Github that has the initial repository that we will download when we start the example. This repository was created with Create-React-App (CRA). I added CSS styling, removed some of the original components, and set up basic commenting functionality. The main page is a snippet from a different RapidAPI article on Javascript translation. The starter project allows a user to add comments below the post content.
Unfortunately, the comments are only held in the state, so they will not persist if the page is refreshed.
We will add translation capabilities to each comment, so every individual comment can be translated.
Prerequisites
There are a few skills and tools that will help you complete this project:
- Familiarity with the command-line and Git (using basic commands)
- Installation of Node >= 8.10
- Understanding of how ReactJS works (props, components, functions, hooks). I will explain some aspects as we go through the tutorial.
- An internet connection.
- A reputable code editor (I will be using Visual Studio Code)
- A free RapidAPI account
- A Subscription to the Microsoft Text Translation API on RapidAPI
Above is the basic subscription box if you followed the link above. The basic plan allows 2500 requests a month. Notice that the link to ‘Manage and View Usage’. This is done in the RapidAPI dashboard.
IMPORTANT: This is NOT Microsoft Text Translation 3.0. Make sure the API name does not have 3.0 at the end of the title.
Let’s start!
1. Clone the Starting Repository From Github
Navigate to good directory in your terminal and execute git clone https://github.com/jdretz/rapidapi-microsoft-translation-example-starter.git
Next, change directories into the folder with cd rapidapi-microsoft-translation-example-starter/
. Install the dependencies by running npm install
in the root of the project. The installation may take a few minutes because the project uses CRA.
Finally, start the app by running npm start
. The app starts on http://localhost:3000
. You should see our example blog article once you navigate there.
Feel free to test out the commenting to get a feel for adding and deleting comments. If a name is not provided to the form then the ID is used.
Project Structure
React apps start at index.js
where the application is attached to the DOM. Furthermore, we import the component <App />
into this file and rendering it to the page. The App
component lives in App.js
and is where all of our other components and data are rendered from.
Our homepage (the article) and main title are in App.js
. In addition, we use the popular libraries Bootstrap and React-Bootstrap to style the app.
All of the components have a corresponding CSS file. App.css
and index.css
contain styles that are applied anywhere in the app. In contrast, all the other component files (i.e CommentSection.js
AddComment.js
) have modular CSS styles. Modular CSS files end in module.css
and are imported at the top of the page (usually as classes
) in their partner js
file.
We will add the translation capability to our commenting components in the components
directory and therefore spending most of our time in those files.
2. Explore the Microsoft Text Translation API
Take a look at the endpoints in the API dashboard.
I highlighted the three endpoints that we need on the left side of the image. RapidAPI can generate code snippets for us when we select different languages/libraries.
Let’s use Javascript and the fetch API to make our API calls.
Finally, in the bottom right we can preview what the response will look like. Oh no! It’s XML! No problem, we will build a function for parsing the response and extracting the data we need. Two endpoints will return XML and the other, Speak, will return binary data (application/octet-string) which we will need to read and save to a local URL.
The Speak endpoint returns the submitted text spoken by someone in the target language. For example, submitting the text ‘Hello’ with Spanish as the target language returns audio with the voice of someone with a Spanish accent speaking English. In other words, the Speak endpoint does not translate the text for us. We need to do that first.
The individual comment’s text is sent to the Translate endpoint first. Then, the translated text is submitted to the Speak endpoint.
The final endpoint, Get Speak Supported Languages, is self-explanatory. However, the endpoint only returns a list of languages that are supported by the Speak endpoint. The returned languages are in their ISO 639-1 Code equivalent. These codes will be displayed as translation options for the languages.
3. Add API Calls
All three API calls are written in the same file. The functions are attached to the object exported from the file. The file has a helper code block at the top that determines how to parse the XML. Browsers have support for parsing XML, but the code block determines what options are available at runtime.
Create the file api.js
in the src
directory. Add the helper function code block the top of a new file.
let parseXml; // create xml parsing functions // the next if/else block is from the following thread // https://stackoverflow.com/questions/649614/xml-parsing-of-a-variable-string-in-javascript if (typeof window.DOMParser != "undefined") { parseXml = function(content) { return ( new window.DOMParser() ).parseFromString(content, "text/xml"); }; } else if (typeof window.ActiveXObject != "undefined" && new window.ActiveXObject("Microsoft.XMLDOM")) { parseXml = function(content) { let xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); xmlDoc.async = "false"; xmlDoc.loadXML(content); return xmlDoc; }; } else { throw new Error("No XML parser found"); }
Essentially, the above code block gives us a function, parseXML
, that accepts XML text from an XML response and returns a document that we can parse.
Next, let’s add the object to export that has the three API calls below the parse code block. I’ve added comments to explain what is happens in each function.
... export default { Translate: { // Speak endpoints. Returns the url of the local audio file textToSpeech: (encoded, target) => { return fetch(`https://microsoft-azure-translation-v1.p.rapidapi.com/Speak?text=${encoded}&language=${target}`, { "method": "GET", "headers": rapidAPIHeaders }) .then(response => response.blob()) .then(blob => { return URL.createObjectURL(blob); }) }, // Get Speak Language Options endpoint. Returns a list of strings getSpeakLanguages: () => { return fetch("https://microsoft-azure-translation-v1.p.rapidapi.com/GetLanguagesForSpeak", { "method": "GET", "headers": rapidAPIHeaders }) .then(response => response.text()) // convert response to text .then(text => { // creates XML document let document = parseXml(text) // creates array from HTMLCollection object let stringElementArray = Array.from(document.getElementsByTagName('string')) // extracts innerHTML value from string tag object let languages = stringElementArray.map(string => string.innerHTML) return languages }) }, // Translate endpoint. Returns the translated text as a string translateToText: (encoded, target) => { return fetch(`https://microsoft-azure-translation-v1.p.rapidapi.com/translate?from=en&to=${target}&text=${encoded}`, { "method": "GET", "headers": rapidAPIHeaders }) .then(response => response.text()) .then(text => { let document = parseXml(text) let stringElementArray = document.getElementsByTagName('string') return stringElementArray[0].innerHTML }) .catch(err => { console.log(err); }); } } }
The functions are basically pulled straight from the RapidAPI dashboard (pictured above). To use the functions import the object, import api from 'path/to/api/file'
, then access the functions on the Translate
object. For example, api.Translate.translateToText(encoded, target)
.
The functions are almost ready. There is a variable used in the functions that are not defined: rapidAPIHeaders
. This object is repeated in all the functions because the RapidAPI headers are sent in each request, so let’s pull it out and add it as a variable to the top of the file.
The final api.js
will be:
let rapidAPIHeaders = { "x-rapidapi-host": "microsoft-azure-translation-v1.p.rapidapi.com", "x-rapidapi-key": process.env.REACT_APP_RAPIDAPI_KEY } let parseXml; // create xml parsing functions // the next if/else block is from the following thread // https://stackoverflow.com/questions/649614/xml-parsing-of-a-variable-string-in-javascript if (typeof window.DOMParser != "undefined") { parseXml = function(content) { return ( new window.DOMParser() ).parseFromString(content, "text/xml"); }; } else if (typeof window.ActiveXObject != "undefined" && new window.ActiveXObject("Microsoft.XMLDOM")) { parseXml = function(content) { let xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); xmlDoc.async = "false"; xmlDoc.loadXML(content); return xmlDoc; }; } else { throw new Error("No XML parser found"); } export default { Translate: { textToSpeech: (encoded, target) => { return fetch(`https://microsoft-azure-translation-v1.p.rapidapi.com/Speak?text=${encoded}&language=${target}`, { "method": "GET", "headers": rapidAPIHeaders }) .then(response => response.blob()) .then(blob => { return URL.createObjectURL(blob); }) }, getSpeakLanguages: () => { return fetch("https://microsoft-azure-translation-v1.p.rapidapi.com/GetLanguagesForSpeak", { "method": "GET", "headers": rapidAPIHeaders }) .then(response => response.text()) .then(text => { // creates XML document let document = parseXml(text) // creates array from HTMLCollection object let stringElementArray = Array.from(document.getElementsByTagName('string')) // extracts innerHTML value from string tag object let languages = stringElementArray.map(string => string.innerHTML) return languages }) }, translateToText: (encoded, target) => { return fetch(`https://microsoft-azure-translation-v1.p.rapidapi.com/translate?from=en&to=${target}&text=${encoded}`, { "method": "GET", "headers": rapidAPIHeaders }) .then(response => response.text()) .then(text => { let document = parseXml(text) let stringElementArray = document.getElementsByTagName('string') return stringElementArray[0].innerHTML }) .catch(err => { console.log(err); }); } } }
Environment Variables
We need to keep our RapidAPI key safe. In order to do that we can pull the value from process.env
, which is an object available at runtime. This keeps us from publishing our API to Github where other people could use or abuse it.
Add the API key to process.env
by creating the file .env
in the project root. It should be on the same level as your package.json
file. The contents of the file are REACT_APP_RAPIDAPI_KEY=yourrapidapikey
.
Two additional points to add:
- Environmental variables in CRA need to start with ‘REACT_APP_’
- This file needs to be in our
.gitignore
file so it’s not added if we were to push the code to Github.
In .gitignore
, add .env
to the bottom.
4. Add Language Options
First, let’s add the language options. The data returned in this endpoint doesn’t change while using the app, will only need to be fetched once, and is used by every comment.
Therefore, it makes sense that the data is fetched in a component higher up the ‘tree’ and passed down the individual Comment components as props.
In App.js
, where it says ‘get speak language options here’ add the code below.
useEffect(() => { api.Translate.getSpeakLanguages() .then(response => setLanguageOptions(response)) .catch(e => console.log(e)) }, [])
This is a React hook that is called once when the page is loads. It calls our function and sets the value to the setLanguageOptions variable. However, none of the functions used above exist in our file yet.
Import { useEffect, useState }
at the top of the file, as well as our API object.
import React, { useState, useEffect } from 'react'; import { Container, Row, Col } from 'react-bootstrap' import CommentSection from './components/CommentSection/CommentSection' import api from './api' import './App.css'; import 'bootstrap/dist/css/bootstrap.min.css';
Next, create the languageOptions
variable above our call to useEffect
.
let [languageOptions, setLanguageOptions] = useState([])
Finally, pass the languageOptions
variable as a prop to the <CommentSection />
component.
The final code for App.js
will be:
import React, { useState, useEffect } from 'react' // MODIFIED import hooks import { Container, Row, Col } from 'react-bootstrap' import CommentSection from './components/CommentSection/CommentSection' import api from './api' // NEW import functions import './App.css'; import 'bootstrap/dist/css/bootstrap.min.css'; function App() { let [languageOptions, setLanguageOptions] = useState([]) // NEW create state variable // get speak language options here useEffect(() => { // NEW function api.Translate.getSpeakLanguages() .then(response => setLanguageOptions(response)) .catch(e => console.log(e)) }, []) return ( <div className="App"> <header className="App-header display-2 bg-light mb-4"> Translation Blog </header> <main> <h1 className="m-4 p-3 text-center bg-light"><a className="heading-link" href="https://rapidapi.com/blog/google-translate-api-tutorial/">How To Build Support for Language Translating In Web Forms (Google Translate API Tutorial) [JavaScript]</a></h1> <article> <Container fluid> <Row className="justify-content-center"> <Col md="8"> <p>We are back with yet another <a href="https://rapidapi.com/blog/category/tutorial/">tutorial</a> on Google Translate API. This time we are going to address the language personalization feature on the web with this API.</p> <p>As a non-native English speaker, if you come across a web form in English that you want to fill out and submit, it can be difficult to interpret the meaning of each form field. Using the Google Translate API, we can build language personalization features for web forms so that you can choose the language while filling out the form.</p> <p>If this sounds interesting, then follow along this blog post to build a demo web form with language translation capability, powered by RapidAPI. But first, a very brief introduction to Google Translate API.</p> --- Read More --- <CommentSection languages={languageOptions} /> // NEW: pass in language options </Col> </Row> </Container> </article> </main> </div> ); } export default App;
The language options are passed to the <CommentSection />
component, but their final home is in the <Comment />
component. Therefore, in CommentSection.js
pass the props another level down into the <Comment />
component.
Line 29 in CommentSection.js
should be:
return <Comment languages={props.languages} key={comment.owner_id} delete={deleteComment} comment={comment} />
languages={props.languages}
.Comment.js
add the select dropdown input and the state variable that holds the selected language option.- install react-select for the project (if you cloned the repo it already was)
- import
useState
at the top of the file again - add the variable, target, to the component’s state.
Comment.js
becomes;
import React, { useState } from 'react' import classes from './Comment.module.css' import Select from 'react-select' const Comment =(props) => { let [target, setTarget] = useState({value: 'en'}) // add translate function here return ( <div className={classes.Comment}> <button className={classes.DeleteButton + " btn btn-sm btn-danger float-right"} onClick={() => props.delete(props.comment.owner_id)}>X</button> <p className="font-weight-bold">{props.comment.name ? props.comment.name : props.comment.owner_id}</p> <p>{props.comment.comment}</p> {/* add translation functionality here */} <div className={classes.TranslateContainer}> {props.languages && <div> <label htmlFor="languages"><small>Select Language</small></label> <Select className={classes.Select} value={target} name="languages" onChange={(selectedOption) => setTarget(selectedOption)} options={props.languages.map(lang => { return { value: lang, label: lang } })} /> </div>} </div> </div> ) } export default Comment
After leaving a comment, there is now a dropdown option to select the language.
5. Add Translate Function
For the translation function to work we need it to;
- Encode the comment text so the special characters can be sent in the request URL
- translate the text into the target language
- Encode the translated language text
- Retrieve the speech recording for the translated phrase
- Set the URL to a variable in state
With HTML5 we can use the <audio>
tag to create a decent experience for users to play, playback, download, or increase the volume of .wav or .mp3 files.
The <source>
tag expects the local URL returned by textToSpech
, and later stored, in a state variable.
The final code for the file uses conditional logic to render components if the data is available. For example, {output && <Component />}
means if the output variable is truthy render the component. If this logic is not set, an error will be thrown that the value we are trying to render in <Component />
is null or undefined.
The new code blocks have comments above or below to describe what is happening. The final code for Comment.js
is;
import React, { useState } from 'react' // import the useState function import classes from './Comment.module.css' import Select from 'react-select' import api from '../../api' // import the api object const Comment =(props) => { // // Add the needed variables to state // let [target, setTarget] = useState({value: 'en'}) let [output, setOutput] = useState('') let [error, setError] = useState('') const translate = () => { // Clears URL setOutput('') // encodes original text comment const encodedEnglish = encodeURI(props.comment.comment) // translates english to target language api.Translate.translateToText(encodedEnglish, target.value) .then(response => { // encodes target language const encodedTarget = encodeURI(response) // Retrieves audio data in target languages api.Translate.textToSpeech(encodedTarget, target.value) .then(response => setOutput(response)) // sets URL }) .catch(e => setError('Something went wrong...')) } return ( <div className={classes.Comment}> <button className={classes.DeleteButton + " btn btn-sm btn-danger float-right"} onClick={() => props.delete(props.comment.owner_id)}>X</button> <p className="font-weight-bold">{props.comment.name ? props.comment.name : props.comment.owner_id}</p> <p>{props.comment.comment}</p> {/* */} {/* If there is an output url show the audio player */} {/* */} {output && <div> <div style={{textAlign: "center"}}> <audio controls> <source src={output} type="audio/wav" /> </audio> </div> </div> } <div className={classes.TranslateContainer}> {/* */} {/* Add an error tag if API call fails */} {/* */} <p style={{color: 'red'}}>{error}</p> {/* */} {/* Button that calls the tranlate function */} {/* */} <button onClick={() => translate()} className={classes.TranslateButton + ' btn'}>Translate</button> {props.languages && <div> <label htmlFor="languages"><small>Select Language</small></label> <Select className={classes.Select} value={target} name="languages" onChange={(selectedOption) => setTarget(selectedOption)} options={props.languages.map(lang => { return { value: lang, label: lang } })} /> </div>} </div> </div> ) } export default Comment
To test it out, leave a comment, select a language, click Translate, make sure your sound is on, and listen to the translation! The request for the translation might take a second or two. However, I have found it to be quite fast.
We successfully added translation capabilities to our page! Although it was added to comments, the underlying process could be replicated for larger text components and varying use cases.
Conclusion
Nice job! You learned how to add translation capabilities that can set your app apart from competitors or improve your current users’ experience.
While testing and creating the app I used ~300 calls to the Microsoft Text Translate API. Consequently, I still have over 2,000 calls left for this month to make the functionality even better.
There are some obvious you can improve the functions or components. Here are some ideas;
- Add translation support for comments that are not in English (maybe we want to translate Spanish into English)
- Display the actual language name and not just the language code for the options.
- Show the translated text in the target language below the English text in the comment.
So many possibilities! I may have glazed over some aspects of the code that confused you. If that happened, ask a question in the comments and I would be happy to answer it. Thanks for reading!
Leave a Reply