paint-brush
How to Build a Multi-Lingual Chatbot with IBM Translation API and Ablyby@sarah-chima
2,994 reads
2,994 reads

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

by Sarah ChimaNovember 8th, 2019
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

How to Build a Multi-Lingual Chatbot with IBM Translation API and Ably's Realtime API. We will be using the IBM translation API that translates text in one language to another. The app will consist of a dropdown menu that users can select languages they want to communicate in. The chat section: This is where messages in the chat channel will appear. 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.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Build a Multi-Lingual Chatbot with IBM Translation API and Ably
Sarah Chima HackerNoon profile picture

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.

3. Setting Up the 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.

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.

  • 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 ibm-watson@^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.

npm start

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.

4. Building the Frontend

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>
        <!-- scripts for the app -->
        <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>
        <!-- Ably 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>
        <!--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>

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.

5. Creating the API Endpoints

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 translator = 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.

6. Consuming the Endpoints

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 => 
                `<option value="${language.language}">${language.name}</option>`
            )
            $("#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.

7.1 Subscribing to a 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.

7.2 Publishing to a channel

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

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: