How to Build a Multi-Lingual Chatbot with IBM Translation API and Ably

From times immemorial, language has been a barrier to good communication between people that speak different languages. Imagine a French man who speaks only French trying to share his ideas with a man who only understands only English. Frustrating right?  One way to solve this issue has been the use of human translators. But we cannot always do this when it comes to chat. It might be impossible to have someone to translate every message sent to us. However, thanks to the IBM translation API, text can be translated easily from one language to another.

In this tutorial, we will build a chat app that allows people to communicate with each other in different languages. We will be using the IBM translation API that translates text in one language to another and Ably’s Realtime API to add the realtime data sharing functionality.

Prerequisites

To follow along in this tutorial,  you will need the following:

  • A basic knowledge of JavaScript.
  • A basic understanding of NodeJS and/or ExpressJS.
  • Node >= version 6 and npm >= version 5.2  (or yarn if you prefer yarn) running on your machine. You can download them here.

1. What will we build

Let us fully understand what we will be building. The app will consist of: 

  1. A dropdown: This is what the users can use to select the preferred language that they want to communicate in. We will use the IBM translation API to get a list of languages that can be converted to and from English, as this will be our primary language. We will use these languages to populate the dropdown menu.
  2. The chat section: This is where messages in the chat channel will appear. If a message is sent by the current user, this message is displayed as it is to the user. If the message is from another user, in another language, this message is first translated in the user’s choice of language before it is displayed
  3. The input section: This is where users can add messages to the chat conversation.

    Note that this is only a demo app and if a user tries sending a message with the wrong language selected, the app might behave unexpectedly.

2. Technologies we will use

The two major technologies we will use are:

2.1 The IBM translator API

IBM Watson™ Language Translator 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 their website. 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 realtime messaging infrastructure 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 messages 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 pub/sub.
To set up Ably, create a free account on their website. 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.

To get started, we’ll create a folder for our app.


mkdir multi-lingual-app
cd multi-lingual-app

Open up this newly created folder in your favourite code editor. Here is an overview of the file structure of the 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
If you have not already done so, you can download NodeJS here. Next, we will initialise the Node project using the following command.
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.

  • Express – a lightweight framework for NodeJS.
  • Browserify – Browserify lets you require(‘modules’) in the browser by bundling up all of your dependencies.
  • Watchify – Watchify automatically bundles up your dependencies as you make changes to your javascript files.
  • Nodemon -Nodemon keeps the  server running and automatically refreshes the page as we make changes to our code.
  • Babel –  Babel lets us use ES6 features and syntax in our code.
  • IBM-watson – this will be used for the language translation.
  • Ably – Ably provides realtime messaging infrastructure as a service via its distributed Data Stream Network. We will add Ably as a CDN in our html file.

 Let us install the dependencies using the following commands to get started with our app  and get our app running.

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 [email protected]^5.1.0
In your

package.json

file,  add the following  start script to the

"scripts"

.

"start": "nodemon server.js"

We will add a watch script that watches for changes in our index.js  file and automatically recompiles the ES6 code to one the browser will understand. We will also add a build script which will automatically be run when the app is deployed.

"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

Next, we add the following presets to the file.

{
    "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

In the file, set up the server by adding the following.

//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

We can now start the server.

The server should be running on port 3000.

To watch for changes in the index.js file that will be compiled, open up another terminal in the same project and run the following.

npm run watch

Good work so far! Let us move to the fun part.

As mentioned earlier, the frontend will consist of three main parts:

  • A dropdown menu that has a list a languages the user can select a preferred language from.
  • The message section that contains the messages sent to the message channel.
  • An input field and button that the user can use to send a message to the message channel.

Here is an image how the final view of the frontend will be:

Let us create the frontend view.  First, create the file using the following command.

touch index.html

Next, add the following code to the index.html file you created. Take note of the Ably CDN added.


<html>
    <head>
        
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous">script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js">script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js">script>
        
        <script src="https://cdn.ably.io/lib/ably.min-1.js">script>
        <link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
        <link rel="stylesheet" type="text/css" href="index.css" > 
    head>
    <body>
        
        <div class="chat-container">
            <div class="chat">
                <div class="language-select-container">

                    
                    <div class="form-group">
                        <label for="languageSelector">Please select a languagelabel>
                        <select class="form-control" id="languageSelector">
                        select>
                    div>
                div>

                
                <ul class="row message-container" id="channel-status" >ul>

                
                <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>

Notice that some of the elements already have classnames and ids. This will make it easier for us to add styles and javascript functions to this elements.

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.

To use the language translation API, we will create three endpoints. One for getting a list of all languages, the second for getting the models for English, which is our default language, and the third for translating the language. Before we do any of this, we need to configure our translator.

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 languageTranslator = new LanguageTranslatorV3({
  version: '{version}',
  authenticator: new IamAuthenticator({
    apikey: '{apikey}',
  }),
  url: '{url}',
});

Next, we will create the endpoints. The  translate endpoint will translate the text sent to it into another language depending on the language translation model sent as part of the request body. Create this endpoint by adding the following code.

//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);
      });
  })

To use these endpoints, we need to call them from the frontend and make use of the data they return.

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;

Next, we add a method that retrieves all languages that can be translated to and from English. We are doing this to ensure that the languages we are processing can be translated into the other. We will use English as the common ground for the languages.

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 => 
                `">${language.name}`
            )
            $("#languageSelector").html(languagesMap)
        })
    }
}

Now you should get a list of languages in the dropdown menu on the view. 

Next, we add a method that uses the translate endpoint to translate each text. This method accepts the message to be sent or received, the language the message should be translated into and the message type, if it is sent or received. Notice that it behaves differently if a message is sent or received.

If a message is sent or being published to a channel, it is first converted from the user’s language into English if the original language is not English. If a message is received, it is converted from English into the user’s selected language. That is why we needed to set a default language which in this case is English. 

//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)) 
    }

For now, we just log the translated message to the console. We will modify this method to suit our app pretty soon.

Now we have methods that we can use to consume all the endpoints we created.

7. Subscribing and Publishing on Ably

To subscribe and publish on Ably, we first need to set up our credentials. Before we do that, we need to initialise a user with a name, id and avatar. The id will serve as the clientID when publishing messages to a particular channel, while the name and avatar will be used for displaying the messages. The name and avatars, will be retrieved randomly from a list of names and avatars respectively.

At the top of our function in the index.js file, add the following code. 

// 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 = [
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_8.png?1536042504672',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_3.png?1536042507202',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_6.png?1536042508902',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_10.png?1536042509036',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_7.png?1536042509659',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_9.png?1536042513205',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_2.png?1536042514285',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_1.png?1536042516362',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_4.png?1536042516573',
    'https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_5.png?1536042517889'
]

//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 = {};

Notice that we also added an object called otherUser, this object will hold the data of other users that send messages to the channel.

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
  });

Remember to replace the key with the API key from your dashboard. 

Next, we will subscribe to a channel to get both the chat message as well as user details of other clients who are connected.

//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;
    }
});
  

Notice that we are assigning a name and avatar to the otherUser object we created earlier. That is all for subscribing to a channel. We can receive any message published to the test channel now. But what if we want to publish a message to the channel? Let’s see that next.

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 = `
  • ${messageType === "send" ? "sent-message": ""}"> ${currentUser.avatar} alt="" />
    ${currentUser.name}

    ${text}

    ${time}
  • `
    // const messageItem = `
  • ${text} ${time} $('#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; }
  • Refresh the browser for the changes we have made so far to take effect. If you send a message, you can see the message is the message container section. But you cannot receive messages yet because the translated message just gets logged to the console. Let us modify the translateText method finally.

    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)) 
        }

    Congratulations, you just built an app that people can use to communicate in different languages. Open up the app in two browsers tabs, select different languages for each app, send messages and see how the app works!

    Conclusion

    In this tutorial, we built an app that can be used to communicate in different languages. Here is a link to the live demo. Open the link into tabs and use the input field to send messages. For the full code, see the repository.

    For further reading, you can visit the following links:

    read original article here