Real Time Language Translation Chat Using SignalR And Azure Cognitive Services And TypedHttpClient - .Net Core 2.1

Introduction

In this article, I will discuss how to do real-time translation chat using the language of your choice in two-way communication (sending and receiving) using SignalR and Cognitive Services Translate API in .NET Core 2.1. I have also used one of the new features of.Net Core 2.1 HttpClientFactory – TypedHttpClient to make API calls. This is a simple chat application where the user can login with their name and choice of language that they want to use to send and receive the message. When the user sends a message, it translates the text in real time and sends it to other users with the choice of their selected languages and vice versa. 
 
 
Demo
 
Architecture
 
 
 
The real-time chat application is developed in .NET Core Web App using Razor Pages with Bootstrap stylesheets and .NET Core SignalR Libraries. When the user logs in with the name and choice of language they selected, the system establishes the connection with SignalRHub and puts the user into the selected language group. It also notifies all the other users that a new user has joined. When the user sends the message, SignalRHub makes the Web API call with Cognitive Services API to get the translated text and sends the translated message to other members in their selected languages in real time. When the user exits the chat, it removes them from the group and also notifies all the other users. 
 
How it Works? 
  • Create a New Razor Web App with Login and Chat Pages and Adding SignalR Libraries
  • Create a SignalR Hub and Register with Web App
  • Create and Consume Cognitive Services in Azure (Free Tier) 
Prerequisites 
  • Visual Studio 2017 v 15.7.3 or above
  • .Net Core SDK 2.1
Creating a New Razor Web App & Adding SignalR Libraries
  • Launch the Visual Studio and use the File > New Project menu option and choose ASP.NET Core Web Application. Name the project  RealTimeTranslationChat
  • Select the Web Application and make sure Asp.Net Core 2.1 is selected.
  • By default, the Microsoft.AspNetCore.SignalR package contains its server libraries as part of its ASP.NET Core Web Application template. However, the JavaScript client library for SignalR must be installed using npm. Use the following commands from Node Package Manager Console to install it and copy the signalr.js file from node_modules\@aspnet\signalr\dist\browser to wwwroot\lib\signalr\signalr.js. (Create a SignalR Folder under Lib Directory)
  1. npm init -y   
  2. npm install @aspnet/signalr  
Create the SignalRHub and Register with Web App

Create a New folder called Models and add a User model class.
  1. public class User  
  2.     {  
  3.         public string Name { getset; }  
  4.         public string LanguagePreference { getset; }  
  5.         public string ConnectionId { getset; }  
  6.     }  
This model class holds the Name and the Language preference selected by user and the SignalR Connection ID.

Create a New Folder called Hubs and add a new class called ChatHub.cs

  1. public class ChatHub : Hub  
  2.     {  
  3.         private CognitiveServiceClient _client;  
  4.         public ChatHub(CognitiveServiceClient client)  
  5.         {  
  6.             _client = client;  
  7.         }  
  8.    
  9.         static List<user> ConnectedUsers = new List<user>();  
  10.    
  11.         public User CurrentUser  
  12.         {  
  13.             get  
  14.             {  
  15.                 return ConnectedUsers.FirstOrDefault(i => i.ConnectionId == Context.ConnectionId);  
  16.             }  
  17.         }  
  18.    
  19.         public string LanguageFormatted  
  20.         {  
  21.             get  
  22.             {  
  23.                 string result = "";  
  24.                 var items = ConnectedUsers.Select(i => i.LanguagePreference).Distinct();  
  25.                 foreach (var language in items)  
  26.                 {  
  27.                     result += $"to={language}&";  
  28.                 }  
  29.    
  30.                 if (result.Length > 1)  
  31.                     result = result.Substring(0, result.Length - 1);  
  32.                 return result;  
  33.             }  
  34.         }  
  35.    
  36.         public async Task SendMessage(string user, string message)  
  37.         {  
  38.             var results = _client.Translate(message, LanguageFormatted);  
  39.             var translationResult = results.Result.FirstOrDefault();  
  40.             if (translationResult != null)  
  41.             {  
  42.                 foreach (var translation in translationResult.translations)  
  43.                 {  
  44.                     await Clients.GroupExcept(translation.to, Context.ConnectionId).SendAsync("ReceiveMessage", user, translation.text); 
  45.                 }  
  46.             }  
  47.         }  
  48.    
  49.         public async Task Connect(string name, string language)  
  50.         {  
  51.             var id = Context.ConnectionId;  
  52.             if (ConnectedUsers.Count(x => x.ConnectionId == id) == 0)  
  53.             {  
  54.                 ConnectedUsers.Add(new User() { Name = name, ConnectionId = id, LanguagePreference = language });  
  55.    
  56.                 await Groups.AddToGroupAsync(id, language);  
  57.                 await Clients.Caller.SendAsync("onConnected", ConnectedUsers, name, id);  
  58.                 await Clients.AllExcept(id).SendAsync("onNewUserConnected", name);  
  59.             }  
  60.    
  61.         }  
  62.    
  63.         public async Task Disconnect()  
  64.         {  
  65.             var item = ConnectedUsers.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);  
  66.             if (item != null)  
  67.             {  
  68.                 ConnectedUsers.Remove(item);  
  69.                 await Clients.AllExcept(item.ConnectionId).SendAsync("onDisconnected", item.Name);  
  70.             }  
  71.         }  
  72.    
  73.     }  
This is the main signalr hub that will communicate with all the clients and also make the API calls to Translation Library to translate the text. The constructor based dependency injection loads the TypedHttpClient object that are configured in Startup.cs to make the http API calls.

Connect Method will be called whenever the new user is connected and it adds the users into the static list (Preferably to store in concurrent dictionary to avoid multi thread locking issues) and also adds to the signalr group based on the launguage the user selected. It also sends the message back to caller with the list of users so that it will be populating user panel on the UI side. It also sends the notification to all the other users that a new user is joined the chat.
  1. await Groups.AddToGroupAsync(id, language);  
  2. await Clients.Caller.SendAsync("onConnected", ConnectedUsers, name, id);  
  3. await Clients.AllExcept(id).SendAsync("onNewUserConnected", name);  
Disconnect Method gets called when the user exits the chats. It removes the user from the list and also notifies all the other users.
  1. ConnectedUsers.Remove(item);  
  2. await Clients.AllExcept(item.ConnectionId).SendAsync("onDisconnected", item.Name); 
SendMessage Method will be called whenever user enters the message and it makes the API calls to translation service to get the translated text in each language and sends the associated translated text to each group. 

Create and Consume Cognitive Services in Azure (Free Tier) 
  • Login to Azure Portal and Search for cognitive service and select Cognitive Services.
  • Click Add Button and Search for Translator Text and Select it and click the Create Button.
 
  • You can select the Free Tier for Development purpose. It allows upto two million characters to translate per month in the Free Tier.
 
  • After the Cognitive Service is created, you can obtain the Subscription Keys from the Quick Start Menu.
 
  • By default, two keys will be provided. You can copy the key1 value and put it in usersecrets.json for the application to access it.
 
Now that we have completed setting up the Translator Service in Azure, we will switch back to code to consume the API Service. We will be using TypedHttpClient to consume the web service. Typed clients are custom classes with HttpClient injected in the constructor. This will be wired up within the DI system by adding generic AddHttpClient method in Startup.cs with the custom type. Another advantage of having typed client is that we can encapsulate the all the HTTP calls inside specific business methods like SendMessage, GetSupportedLanguages.
  1. public class CognitiveServiceClient  
  2.     {  
  3.         private HttpClient _client;  
  4.         private readonly string _apiKey;  
  5.         private readonly string _apiVersion;  
  6.         private ILogger<cognitiveserviceclient> _logger;  
  7.         public CognitiveServiceClient(HttpClient client, ILogger<cognitiveserviceclient> logger, IConfiguration config)  
  8.         {  
  9.             _client = client;  
  10.             _client.BaseAddress = new Uri(config["AppSettings:APIBaseURL"]);  
  11.             _apiVersion = config["AppSettings:APIVersion"];  
  12.             _apiKey = config["AppSettings:SubscriptionKey"];  
  13.             _logger = logger;  
  14.         }  
  15.    
  16.         public async Task<List Language> GetSupportedLanguages()  
  17.         {  
  18.             var languages = new List<Language>();  
  19.             try  
  20.             {  
  21.                 var languagesUrl = new Uri($"/languages?api-version={_apiVersion}", UriKind.Relative);  
  22.                 var res = await _client.GetAsync(languagesUrl);  
  23.                 res.EnsureSuccessStatusCode();  
  24.                 var jsonResults = await res.Content.ReadAsStringAsync();  
  25.    
  26.                 dynamic entity = JObject.Parse(jsonResults);  
  27.                 foreach (JProperty property in entity.translation.Properties())  
  28.                 {  
  29.                     dynamic langDetail = JObject.Parse(property.Value.ToString());  
  30.                     var language = new Language();  
  31.                     language.Code = property.Name;  
  32.                     language.Name = langDetail.name;  
  33.                     languages.Add(language);  
  34.                 }  
  35.                 return languages;  
  36.             }  
  37.             catch (HttpRequestException ex)  
  38.             {  
  39.                 _logger.LogError($"An error occurred connecting to CognitiveService API {ex.ToString()}");  
  40.                 throw;  
  41.             }  
  42.         }  
  43.    
  44.         public async Task<List TranslationResult> Translate(string message, string languages)  
  45.         {  
  46.             try  
  47.             {  
  48.                 System.Object[] body = new System.Object[] { new { Text = message } };  
  49.                 var requestBody = JsonConvert.SerializeObject(body);  
  50.    
  51.                 var translateUrl = new Uri($"/translate?api-version={_apiVersion}&{languages}", UriKind.Relative);  
  52.                    
  53.                 string jsonResults = null;  
  54.                 using (var request = new HttpRequestMessage())  
  55.                 {  
  56.                     request.Method = HttpMethod.Post;  
  57.                     request.RequestUri = translateUrl;  
  58.                     request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");  
  59.                     request.Headers.Add("Ocp-Apim-Subscription-Key", _apiKey);  
  60.    
  61.                     var res = await _client.SendAsync(request);                     
  62.                     jsonResults = await res.Content.ReadAsStringAsync();  
  63.                 }  
  64.                 return JsonConvert.DeserializeObject<list TranslationResult>(jsonResults);  
  65.                        
  66.             }  
  67.             catch (HttpRequestException ex)  
  68.             {  
  69.                 _logger.LogError($"An error occurred connecting to CognitiveService API {ex.ToString()}");  
  70.                 throw;  
  71.             }  
  72.         }  
  73.     }  

GetSupportedLanguages method will make the API calls to get the list of supported languages.

Translate method takes the message and list of languages to translate and returns the json output translation text array with all the requested languages.

Additional Points

In Startup.cs, make sure to add the below code in ConfigureServices method. 
  1. services.AddSignalR();  
  2.  services.AddHttpClient<cognitiveserviceclient>();  

GetSupportedLanguages method will make the API calls to get the list of supported languages.

Translate method takes the message and list of languages to translate and return the json output translation text array all the requested languages.

Additional Points

In Startup.cs, make sure to add the below code in ConfigureServices method. 
  1. app.UseSignalR(routes =>  
  2.             {  
  3.                 routes.MapHub<chathub>("/chathub");  
  4.             })  
In the IndexModel.cs , we will have the implementation for GET method to return the list of supported languages.
  1. private CognitiveServiceClient _client;  
  2.         public IndexModel(CognitiveServiceClient Client)  
  3.         {  
  4.             _client = Client;  
  5.         }  
  6.    
  7.         [BindProperty]  
  8.         public User user { getset; }  
  9.    
  10.         public SelectList LanguageSL { getset; }  
  11.    
  12.         public async Task OnGetAsync()  
  13.         {  
  14.             await GetLanguages();  
  15.         }  
  16.    
  17.         private async Task GetLanguages()  
  18.         {  
  19.             LanguageSL = new SelectList(await _client.GetSupportedLanguages(),nameof(Language.Code),nameof(Language.Name));  
  20.         }  
Conclusion

Finally, when we run the application in multiple windows, you can do the realtime translation chat in the selected languages. I have also verified the application by hosting in Azure with the Free Application Plan Tier. In the future, it will be possible to bring this kind of feature to popular messaging apps like whatsapp when the group chat is having people who speak different regional languages and enabling automatic translation settings would help a lot. I have posted the entire source code in my github library.