visit
2.1 The IBM translator API
translates text from one language to another. The service offers multiple IBM provided translation models that you can customise based on your unique terminology and language.To make use of this API, you will need to create a free account on . Follow the process for setting up the form until you are able to login to your dashboard. Using the search field on the top navigation menu, search for “language-translator” and create a new language translator resource. If you successfully do this, you will be taken to a page where you can access you translator API key. We will use this later in the tutorial.2.2 The Ably Realtime API
Ably provides as a service via its distributed Data Stream Network. This reduces the operational burden of engineering teams, allowing them to build and scale faster and more efficiently. For this tutorial, we will use the Publish/Subscribe messaging pattern provided by Ably. All the messages shared via Ably are organised into logical units called channels - one for each set of data being shared in an app.Channels are the medium through which are distributed. Once users subscribe to a channel, they can receive all messages published on that channel by other users. This scalable and resilient messaging pattern is commonly called .
To set up Ably, create a free account . When you are done with the process, you will get an API key you will use as we go further in the tutorial.
Next, let us set up our app.
mkdir multi-lingual-app
cd multi-lingual-app
/
|-- node_modules //this will be generated automaticaaly as we install dependencies
|-- /public
|-- bundle.js //contains compiled js code for our index.js file
|-- index.css // styles for the file
|-- .babelrc // configuration code for our babel presets
|-- index.html // code for the view
|-- index.js // javascript for the frontend
|-- server.js // javascript code for the server
npm init
Follow the prompt to set up the project. You will notice the
package.json
file has been added to the project after completing the steps. 3.1 Installing Dependencies
We will install the following dependencies to get our app working.npm install nodemon -g
npm install express browserify watchify --save
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-runtime babelify
npm install @babel/polyfill @babel/runtime --save
npm install ibm-watson@^5.1.0
In your
package.json
file, add the following start script to the "scripts"
."start": "nodemon server.js"
"build": "browserify index.js -o public/bundle.js",
"watch": "watchify index.js -o public/bundle.js -v"
Next, we setup the browserify plugin by adding the following code to the
package.json
file."browserify": {
"transform": [
[
"babelify",
{
"presets": [
"@babel/preset-env"
]
}
]
]
}
Your
package.json
file should contain the following after following the steps above."scripts": {
"start": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "browserify index.js -o public/bundle.js",
"watch": "watchify index.js -o public/bundle.js -v"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"@babel/preset-env"
]
}
]
]
}
3.2 Setting up Babel
Next, we will set up babel for compiling our ES6 code to JavaScript that the browser understands.
First, we create the file.
touch .babelrc
{
"presets": [
[ "@babel/preset-env", {
"useBuiltIns": false
}]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
]
]
}
3.3 Setting up the server
If you have not already added a server.js file, you can do that using the following command.//create the server.js files
touch server.js
//setup the express server
var express = require('express');
require('dotenv').config();
var app = express();
app.use(express.json())
var PORT = process.env.PORT || 3000;
app.listen(PORT, function() {
console.log('Server is running on PORT:',PORT);
});
//setup routes for the index.html file
app.get('/', function(req, res) {
res.sendFile( __dirname + "/" + "index.html" );
});
//Setup route for static files
app.use(express.static(__dirname + "/" + 'public'));
Next, we will create the
index.js
file. touch index.js
We will also add a
bundle.js
file in a public directory where the code in the index.js
file will be compiled into.//create the public folder and create the bundle.js file
mkdir public
cd public
touch bundle.js
npm start
npm run watch
touch index.html
<html>
<head>
<!-- scripts for the app -->
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<!-- Ably script -->
<script src="//cdn.ably.io/lib/ably.min-1.js"></script>
<link href="//fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="index.css" >
</head>
<body>
<!--The chat app container-->
<div class="chat-container">
<div class="chat">
<div class="language-select-container">
<!--Dropdown menu for the language selector-->
<div class="form-group">
<label for="languageSelector">Please select a language</label>
<select class="form-control" id="languageSelector">
</select>
</div>
</div>
<!--Message container - the chat messages will appear here-->
<ul class="row message-container" id="channel-status" ></ul>
<!--Input container for the input field ans send button-->
<div class="input-container">
<div class="row text-input-container">
<input type="text" class="text-input" id="input-field"/>
<input id="publish" class="input-button" type="submit" value="Send">
</div>
</div>
</div>
</div>
</body>
<script src="bundle.js"></script>
</html>
Now if you open up your localhost on your browser
localhost:3000,
you should see this screen.This is the basic app display without any styles. Looking ugly right? Let us add some styles.
Create the
index.css
file in the public directory like in the file structure above and add the following.
* {
box-sizing: border-box;
font-family: 'Roboto', sans-serif;
}
body {
background: #f2f2f2;
}
.chat-container {
background-color: #f2f2f2;
color: #404040;
width: 505px;
margin: 40px auto;
border: 1px solid #e1e1e8;
border-radius: 5px;
}
.language-select-container {
background-color: #ffffff;
padding: 20px 40px 20px;
border-radius: 5px 5px 0 0;
}
.language-select-container select {
height: 30px;
margin-left: 20px;
font-size: 14px;
min-width: 150px;
}
.input-container {
background-color: #ffffff;
padding: 20px;
border-radius: 0 0 5px 5px;
}
.text-input-container {
background-color: #f2f2f2;
border-radius: 20px;
width: 100%;
}
.text-input {
background-color: #f2f2f2;
width: 80%;
height: 32px;
border: 0;
border-radius: 20px;
outline: none;
padding: 0 20px;
font-size: 14px;
}
.input-button {
width: 19%;
border-radius: 20px;
height: 32px;
outline: none;
cursor: pointer;
}
.message-container {
height: 300px;
overflow: scroll;
list-style-type: none;
}
.message-time {
float: right;
margin-right: 40px;
}
.message {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.message picture {
width: 15%
}
.message-info {
width: 65%;
}
.message-image {
width: 50px;
height: 50px;
border-radius: 50%;
}
.message-name {
margin-top: 0;
margin-bottom: 5px;
}
.message-text {
margin-top: 8px;
}
If you refresh the page, you will see a styled and beautiful user interface.
Let us move on to creating the API endpoints for our app.
Let us do this in the
server.js
file.First, we create an instance of the Language translator by adding the following code. To create this instance, we need to authenticate the app using the
IamAuthenticator
. Remember to replace apikey
and version
with your own API key and version respectively.const LanguageTranslatorV3 = require('ibm-watson/language-translator/v3');
const { IamAuthenticator } = require('ibm-watson/auth');
//create an instance of the language translator.
const translator = new LanguageTranslatorV3({
version: '{version}',
authenticator: new IamAuthenticator({
apikey: '{apikey}',
}),
url: '{url}',
});
//This endpoint translates the text send to it
app.post('/api/translate', function(req, res, next) {
translator.translate(req.body)
.then(data => res.json(data.result))
.catch(error => next(error));
});
The
get-languages
endpoint gets all the languages that can be processed by the IBM language translator. //This endpoint gets all the langauges that can be processed by the translator
app.get('/api/get-languages', function(req, res, next) {
translator.listIdentifiableLanguages()
.then(identifiedLanguages => {
res.json(identifiedLanguages.result);
})
.catch(err => {
console.log('error:', err);
});
})
The
get-model-list
endpoint gets a list of all translation model available. Translation models specifies the language that the text is being translated from and the language it is being translated into. For instance, the en-fr
model is a model for translating English text into French.//This endpoint gets all the model list.
app.get('/api/get-model-list', function(req, res, next) {
translator.listModels()
.then(translationModels => {
res.json(translationModels.result)
})
.catch(err => {
console.log('error:', err);
});
})
We will make use of the endpoints the
index.js
file that serves the frontend. So let us create some methods that will call these endpoints and return the data. First, we will add two methods. The first method retrieves a list of all languages using the
get-languages
endpoint we created in the last section. The second method retrieves a list of all language models using the get-model-list
we created in the last section. We are using async for both methods because we want the methods to return the data when the data has been fetched.Add the following code to the
index.js
file.import '@babel/polyfill'
function index() {
//This method retrieves a list of all languages
async function getLanguages() {
let response = await fetch("/api/get-languages", {
method: 'GET'
});
return await response.json();
}
//This method retrieves a list of all language models
async function getModels() {
let response = await fetch("/api/get-model-list", {
method: 'GET'
})
return await response.json();
}
}
index();
export default index;
This
getTranslatableLanguages
method will also sort the languages gotten and will also populate the dropdown in our frontend view. Add the following code to your index.js
file.function index() {
getTranslatableLangauges()
function getTranslatableLangauges() {
//get languages and all language models
const allLanguages = getLanguages();
const models = getModels();
//resolve the promises
Promise.all([allLanguages, models]).then( values => {
const allLanguages = values[0].languages;
const models = values[1].models;
//get translation models that have English as their source
const englishModels = models.filter(model => model.source === "en");
//get all languages that can be translated from English
let translatableEnglishLanguages = englishModels.map(model => {
return allLanguages.find(language => model.target === language.language)
})
//sort languages
translatableEnglishLanguages.sort((a,b) => {
var nameA = a.name.toUpperCase(); // ignore upper and lowercase
var nameB = b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
})
const languagesMap = translatableEnglishLanguages.map( language =>
`<option value="${language.language}">${language.name}</option>`
)
$("#languageSelector").html(languagesMap)
})
}
}
//this method translates the text using the `translate` enpoint created.
function translateText(message, language, messageType = "receive") {
//check if the message is a sent message or received message
const text = messageType === "send" ? message: message.data;
const translateParams = {
text: text,
modelId: messageType === "send" ? `${language}-en` : `en-${language}`,
};
var nmtValue = '2019-09-28';
fetch('/api/translate', {
method: 'POST',
body: JSON.stringify(translateParams),
headers: new Headers({
'X-WDC-PL-OPT-OUT': $('input:radio[name=serRadio]:radio:checked').val(),
'X-Watson-Technology-Preview': nmtValue,
"Content-Type": "application/json"
}),
})
.then(response => response.json())
.then(data => {
console.log(data)
})
.catch(error => console.error(error))
}
// A method to randomly get an item from an array
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
//a list of avatars that will randomly be assigned to each app user
const avatarsInAssets = [
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_8.png?72',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_3.png?02',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_6.png?02',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_10.png?36',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_7.png?59',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_9.png?05',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_2.png?85',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_1.png?62',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_4.png?73',
'//cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_5.png?89'
]
//a list of names that will randomly be assigned to each app user
const namesInAssets = [
'Sarah Tancredi',
'Michael Scoffied',
'Waheed Musa',
'Ada Lovelace',
'Charles Gabriel',
'Mr White',
'Lovely Spring',
'William Shakespare',
'Prince Williams',
'Queen Rose'
]
//create a user object by randomly assigning an id, an avatar and a name.
let user = {
id: "id-" + Math.random().toString(36).substr(2, 16),
avatar: avatarsInAssets[getRandomArbitrary(0, 9)],
name: namesInAssets[getRandomArbitrary(0, 9)]
};
//this object will hold the data of other users that send messages to the channel
let otherUser = {};
7.1
To enable us see messages sent to the channel by other clients, we need to subscribe to a message channel. First, we create an instance of Ably Realtime.const ably = new Ably.Realtime({
key: YOUR_ABLY_API_KEY,
clientId:`${user.id}`,
echoMessages: false
});
//specify the channel the user should belong to. In this case, it is the `test` channel
const channel = ably.channels.get('test');
//Subscribe the user to the messages of the channel. So the use rwill receive each message sent to the test channel.
channel.subscribe("text", function(message) {
const selectedLanguage = $("#languageSelector").find(":selected").val();
translateText(message, selectedLanguage)
});
//This gets the data of other users as they publish to the channel.
channel.subscribe("user", (data) => {
if (data.clientId != user.id) {
let otherAvatar = data.data.avatar;
let otherName = data.data.name;
otherUser.name = otherName;
otherUser.avatar = otherAvatar;
}
});
7.2
To contribute to a channel, we need to publish to the channel. We will define the behaviour of the app when the message is typed and the send button is clicked. Then we add a method that displays the message to the users.
//Get the send button, input field and language dropdown menu elements respectively.
const sendButton = document.getElementById("publish");
const inputField = document.getElementById("input-field");
const languageSelector = document.getElementById("languageSelector")
//Add an event listener to check when the send button is clicked
sendButton.addEventListener('click', function() {
const input = inputField.value;
const selectedLanguage = languageSelector.options[languageSelector.selectedIndex].value;
inputField.value = "";
let date = new Date();
let timestamp = date.getTime()
//display the message as it is using the show method
show(input, timestamp, user, "send")
//translate the text as a sent message
translateText(input, selectedLanguage, "send")
});
//This method displays the message.
function show(text, timestamp, currentUser, messageType="receive") {
const time = getTime(timestamp);
const messageItem = `<li class="message ${messageType === "send" ? "sent-message": ""}">
<picture>
<img class="message-image" src=${currentUser.avatar} alt="" />
</picture>
<div class="message-info">
<h5 class="message-name">${currentUser.name}</h5>
<p class="message-text">${text}</p>
</div>
<span class="message-time"> ${time}</span>
</li>`
// const messageItem = `<li class="message">${text}<span class="message-time"> ${time}</span></li`;
$('#channel-status').append(messageItem)
}
//This method is used to convert a timestamp to 24hour time format, this is the format we will display the time of the message in.
function getTime(unix_timestamp) {
var date = new Date(unix_timestamp);
var hours = date.getHours();
var minutes = "0" + date.getMinutes();
var seconds = "0" + date.getSeconds();
// Will display time in 10:30:23 format
var formattedTime = hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
return formattedTime;
}
Replace the
console.log
message with the modified code.function translateText(message, language, messageType = "receive") {
...
fetch('/api/translate', {
method: 'POST',
body: JSON.stringify(translateParams),
headers: new Headers({
'X-WDC-PL-OPT-OUT': $('input:radio[name=serRadio]:radio:checked').val(),
'X-Watson-Technology-Preview': nmtValue,
"Content-Type": "application/json"
}),
})
.then(response => response.json())
.then(data => {
// when messages are translated, they get published to the channel
const translatedText = data['translations'][0]['translation'];
if ( messageType === "send") {
channel.publish('text', translatedText);
channel.publish("user", {
"name": user.name,
"avatar": user.avatar
});
} else {
show(translatedText, message.timestamp, otherUser);
}
})
.catch(error => console.error(error))
}