Saturday, March 12, 2022

Localizing ASP.NET 6.0 MVC web apps

Localizing your ASP.NET 6.0 MVC web app involves making it work in more than one language. This gives you access to more worldwide markets. In this article, I will show you some technical approaches in achieving this objective. 

Source code : https://github.com/medhatelmasry/GlobalWeb6.git

Prerequisites

  • .NET 6.0
  • Visual Studio Code
  • You have familiarity with ASP.NET MVC
  • Install the following extension to your Visual Studio Code:

 .resx files

Files with .resx extensions play a leading role in localizing ASP.NET applications. They are essentially XML documents that rely on placement and a consistent naming convention. Here is what a typical .resx file may look like:

<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<resheader name="reader">
<value>2.0</value>
</resheader>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>

<data name="Press Release" xml:space="preserve">
<value>Communiqué de presse</value>
</data>

<data name="Welcome" xml:space="preserve">
<value>Bienvenue</value>
</data>
</root>

Only the highlighted text above is customized to target a specific language. Note the following localization as specified in the above .resx file pertaining to english to french localization:

English Key French Translation
Press Release Communiqué de presse
Welcome Bienvenue

Getting Started

We will first create a simple ASP.NET 6.0 MVC app. This is accomplished by entering the following commands from within a terminal window is a working directory:

dotnet new mvc -o GlobalWeb
cd GlobalWeb

Create the following additional directories inside your application:

Resources
       |
       |
       ----------- Controllers
       |
       |
       ----------- Views
       |                  |
       |                  |
       |                  ----------- Home
       |
       |
       ----------- Models

Add the following to Program.cs right before var app = builder.Build():

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

Also, add the following to Program.cs right after var app = builder.Build():

var supportedCultures = new[] {
    "en","en-US","fr","fr-FR","ar", "ko"
};

var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[1])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

localizationOptions.ApplyCurrentCultureToResponseHeaders = true;

app.UseRequestLocalization(localizationOptions);

The first statement above declares that we will be supporting the following languages and cultures:

en English language
en-US English language, US culture
fr French language
fr-FR French language, France culture
ar Arabic language
ko Korean language

The second statement declares en-US as the default culture and adds support for the various cultures to our app. The final statements adds localization support to our web app.

Using localization in your controller classes

Whenever you want to access a localized string in your services or controllers, you can inject an IStringLocalizer<T> and use its indexer property. Add the following instance variable to the HomeController class:

private readonly IStringLocalizer<HomeController> _localizer;

Inject an IStringLocalizer<T> into a HomeController constructor. Your HomeController constructor will look like this: 

public HomeController(ILogger<HomeController> logger, 
    IStringLocalizer<HomeController> localizer
)
{
    _logger = logger;
    _localizer = localizer;
}

Change the Index() action method in the HomeController so it looks like this:

public IActionResult Index() {
    ViewData["pressRelease"] = _localizer["Press Release"];
    ViewData["welcome"] = _localizer.GetString("Welcome").Value ?? "";
    return View();
}

Replace Views/Home/Index.cshtml with:

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">@ViewData["welcome"]</h1>
    <p>@ViewData["pressRelease"]</p>
</div>

If we are currently using the fr culture, then the localizer will look for the following resource file:

Resources/Controllers/HomeController.fr.resx

In Visual Studio Code, create the above file. With the .resx extension installed in Visual Studio Code, your editor will immediately look like this:


Add the following name / value pairs:

Press ReleaseCommuniqué de presse
WelcomeBienvenue


Let's test our application. Run your application from within a terminal window with:

dotnet watch run

A quick way to try it out is to point your browser to /Home. This picks up the default value. However, if you point your browser to /Home?culture=fr, the value is read from the resource file.

Shared Resources

With shared resources you rely on a single file to have all the localization entries in it, so this file can be used among multiple controllers or other classes.

In the root of your application, create a blank class named SharesResource.cs with following content:

namespace GlobalWeb;
public class SharedResource {}

Copy Resources/Controllers/HomeController.fr.resx to Resources/SharedResource.fr.resx.

To keep it simple, inside HomeController.cs we will add shared resource entries. Add the following instance variable to HomeController.cs:

private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

Modify the constructor so it looks like this:

public HomeController(ILogger<HomeController> logger, 
    IStringLocalizer<HomeController> localizer
    IStringLocalizer<SharedResource> sharedLocalizer
)
{
    _logger = logger;
    _localizer = localizer;
    _sharedLocalizer = sharedLocalizer;
}

Now, let us use the shared resource in the Privacy action method. Therefore, add these statements to the Privacy action method:

ViewData["pressRelease"] = _sharedLocalizer["Press Release"];
ViewData["welcome"] = _sharedLocalizer.GetString("Welcome").Value ?? "";

Replace Views/Home/Privacy.cshtml with:

@{
    ViewData["Title"] = "Privacy Policy";
}
<h3 class="display-4">@ViewData["welcome"]</h3>
<p>@ViewData["pressRelease"]</p>

When you point your browser to /Home/Privacy?culture=fr, the values are read from the shared resource file:


Using localization in your views

In Program.cs, modify “builder.Services.AddControllersWithViews()” (around line 4) so it looks like this:

builder.Services
    .AddControllersWithViews()
    .AddViewLocalization();

We simply add ViewLocalization support as highlighted above.

We will localize strings in a single .cshtml file whereby you inject an IViewLocalizer into the view. IViewLocaliser uses the name of the view file to find the associated resources, so for the fr culture in HomeController's Privacy.cshtml view, the localizer will look for:

Resources/Views/Home/Privacy.fr.resx

Create a resource file Resources/Views/Home/Privacy.fr.resx and add to it the following localization string pair:

Privacy Policy / Politique de confidentialité


Back in Views/Home/Privacy.cshtml, add the following to the top of the file: 

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer _localizer

Change the ViewData["Title"] . . . statement to:

ViewData["Title"] = _localizer["Privacy Policy"];

Add the following above the <h3>. . . . markup:

<h1>@ViewData["Title"]</h1>

The complete Views/Home/Privacy.cshtml looks like this:

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer _localizer

@{
    ViewData["Title"] = _localizer["Privacy Policy"];
}
<h1>@ViewData["Title"]</h1>
<h3 class="display-4">@ViewData["welcome"]</h3>
<p>@ViewData["pressRelease"]</p>

Once again, check out Home/Privacy and Home/Privacy?culture=fr

.   


Let's see how we can use a shared resource in views, Add the following localization string pair to Resources/SharedResource.fr.resx:

It's great to see you again.     /    C'est génial de te revoir.


To use the new shared resource string inViews/Home/Privacy.cshtml, Add the following to the top of the file:

@using Microsoft.Extensions.Localization
@inject IStringLocalizer<SharedResource> _sharedLocalizer

Thereafter, add this markup to the bottom of Views/Home/Privacy.cshtml:

<p>@_sharedLocalizer["It's great to see you again."]</p>

You will see this when you visit Home/Privacy?culture=fr in your browser:


Using localization with data annotations

You may wish to localize the string messages that exist in your data annotations. These may represent display names and error messages. In Program.cs, modify the “builder.Services.AddControllersWithViews()” statement (around line 4) so it looks like this:

builder.Services
    .AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization();

Simply add the highlighted code shown above.

Add a class named Contact to the Models folder with the following content:

public class Contact {
  [Required(ErrorMessage = "Email is required.")]
  [EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
  [Display(Name = "Your Email.")]
  public string? Email { get; set; }
}

The above Contact model class contains error messages and a display name that are good candidates for localization.

Add a resource file names Contact.fr.resx to the Resources/Models folder with the following localization string name / value pairs:

KeyValue
Email is required.Un e-mail est requis.
The Email field is not a valid email address. Le champ E-mail n'est pas une adresse e-mail valide.
Your Email.Votre e-mail.
SuccessSuccès



Let us add an action method that renders a contact form. Add the following action method to Controllers/HomeController.cs:

public IActionResult Contact() {
  return View();
}

This requires a view. Add a file Views/Home/Contact.cshtml with the following content: 

@model GlobalWeb.Models.Contact
<div class="row">
  <div class="col-md-8">
    <section>
      <form asp-controller="Home" asp-action="Contact" method="post" class="form-horizontal" novalidate>
        <h4>@(ViewData["Result"] == null ? "Enter details" : ViewData["Result"])</h4>
        <hr />
        <div class="form-group">
          <label asp-for="Email" class="col-md-2 control-label"></label>
          <div class="col-md-10">
            <input asp-for="Email" class="form-control" />
            <span asp-validation-for="Email" class="text-danger"></span>
          </div>
        </div>
        <div class="form-group">
          <div class="col-md-offset-2 col-md-10">
            <button type="submit" class="btn btn-success">Test</button>
          </div>
        </div>
      </form>
    </section>
  </div>
</div>

The above consists of a simple form with an email input field and a submit button. It submits to Contact action method in the HomeController. Therefore, add the following post action method to HomeController.cs

[HttpPost]
public IActionResult Contact(Contact model) {
  if (!ModelState.IsValid) {
    return View(model);
  }
  ViewData["Result"] = _localizer["Success!"]; 
  return View(model);
}

Point your browser to /Home/Contact?culture=fr. You should see that the display name for email is localized.


If you want an easy solution to using shared resources with data annotations, you can do the following:

Add the following localization name / value strings pair to Resources/SharedResource.fr.resx:

Your Email.    /   Votre e-mail.

Add the following to the top of Views/Home/Contact.cshtml:

@using Microsoft.Extensions.Localization
@inject IStringLocalizer<SharedResource> _sharedLocalizer

You can then use _sharedLocalizer in Views/Home/Contact.cshtml as shown below in the highlighted code:

@using Microsoft.Extensions.Localization
@inject IStringLocalizer<SharedResource> _sharedLocalizer

@model GlobalWeb.Models.Contact
<div class="row">
  <div class="col-md-8">
    <section>
      <form asp-controller="Home" asp-action="Contact" method="post" class="form-horizontal" novalidate>
        <h4>@(ViewData["Result"] == null ? "Enter details" : ViewData["Result"])</h4>
        <hr />
        <div class="form-group">
          <label class="col-md-2 control-label">
            @_sharedLocalizer[@Html.DisplayNameFor(model => model.Email)]
          </label>
          <div class="col-md-10">
            <input asp-for="Email" class="form-control" />
            <span asp-validation-for="Email" class="text-danger"></span>
          </div>
        </div>
        <div class="form-group">
          <div class="col-md-offset-2 col-md-10">
            <button type="submit" class="btn btn-success">Test</button>
          </div>
        </div>
      </form>
    </section>
  </div>
</div>

Conclusion

We have looked at a number of techniques for localizing ASP.NET 6.0 apps. This should serve as a decent starting point for you to consider making you web apps reach a wider worldwide audience.

Sunday, March 6, 2022

Build a socket.io chat web app with node.js

Socket.IO is a JavaScript library for real-time web applications. It enables real-time, bi-directional communication between web clients and servers. 

This article addresses the very basic building blocks to creating a socket.io  application in Node.js. 

Source code: https://github.com/medhatelmasry/socket-chat.git

Pre-requisites

The following pre-requisites are assumed:
  • The latest versions of Node.js & npm are installed on your computer
  • You have some basic knowledge of Node.js and Express

Getting started

In a working directory, create a folder for our web app named socket-chat then change into that directory with the following commands:

mkdir socket-chat
cd socket-chat

Initialize a Node.js application with the following command:

npm init -y

Install packages ejsexpress, nodemon and  socket.io with the following command:

npm install ejs express nodemon socket.io

We need to create our server. To accomplish this, create a file in the root of your app named index.js with the the following content:

const express = require("express");
const app = express();
const server = require("http").createServer(app);
const io = require("socket.io")(server, { cors: { origin: "*" } }); 

app.set("view engine", "ejs");

app.get("/", (req, res) => {
  res.render("index", {title: "Socket.io chat example"});
}); 

const port = process.env.PORT || 3030;
server.listen(port, () => {
  console.log(`Server started and listening on port ${port}`);
});

// emit & receive messages
io.on('connection', (socket) => {
  console.log(`Someone connected with socket id: ${socket.id}`);
});

What does the above code do?

  • we initialize express, server, and io 
  • EJS is declared as our view engine
  • the / route points to a view page named index. This will be created later.
  • the server will be started on port 3030
  • whenever someone connects, the unique socket id is displayed in the server console

Edit file package.json and add the following to the "scripts" section:

"dev": "nodemon index.js"

The above allows us to run the web app using command "npm run dev". The advantage of nodemon is that it watches for any changes in the file system and restarts the server whenever a file changes.
 
Create a folder named views and add to it a file named index.ejs with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <title><%- title %></title>
    <script src="https://cdn.socket.io/socket.io-3.0.1.min.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"/>
</head>
<body>
    <div class="container">
        <h3><%- title %></h3>
    </div>
</body>
</html>

Also in views/index.ejs, add the following JavaScript code below the closing </div> tag:

<script>
    const socket = io('http://localhost:3030');
    socket.on("connection", () => {});
</script>

The above code does a web socket hand-shake with the server. Start your server, if it is not started already, then point your browser to http://localhost:3030. The page looks like this:


More interesting is that your console window reveals information about socket IDs:


This proves that we have established a socket.io connection between the server and clients. Let us write some code that sends messages from one client to all the others. To do that, we will create a simple input field and a button in our frontend view. Add the following markup in views/index.ejs right under " <h3><%- title %></h3>":

<input type="text" id="msg" />
<button onclick="sendMessage()" class="btn btn-success btn-sm">
   Send Message
</button>
<hr />
<ul id="msgList" class="list-group"></ul>

In our backend index.js file,  add the following just under the console.log(. . .) statement:

socket.on('chat', (data) => {
   console.log(data);
   // broadcast the same message to all except the sender
   socket.broadcast.emit('chat', data);
});

The above code indicates that when a message named "chat" is received, it will broadcast that same message to all other clients except the sender. The variable data represents the actual message.

Now, back in our views/index.ejs frontend, we will actually emit (or send) a message. Type the following code right under "socket.on("connection", () => {});":

const sendMessage = () => {
   const msgInput = document.querySelector("#msg");
   socket.emit("chat", msgInput.value);
   document.querySelector("#msg").value = "";
}

The above is the sendMessage() handler for the button click. It sends the text entered by user to the message event named chat and, subsequently, clears the input field.

To actually see the message that is being passed around, we need to add some code to the frontend that will append each chat message received to the <ul> . . . </ul> unordered list with id msgList. Add this code below the "const sendMessage = () => { . . . . });" block: 

socket.on("chat", (data) => {
  const msg = data
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
  var li = document.createElement("li");
  li.classList.add("list-group-item", "list-group-item-warning");
  li.textContent = msg;
  document.querySelector("#msgList").appendChild(li);
});

What does the above code do?

  • data is sanitized such that & is replaced by &amp; and < is replaced by &lt; and > is replaced by &gt;
  • an li element is created and given bootstrap styling
  • data is assigned as content to the li element
  • finally, the li item is appended to the unordered list with id msgList

Testing our chat app

Point two (or more) browser windows to http://localhost:3030. In the first window enter a message then click on "Send message". You will notice the second window will pickup the same message:






This is a very simple socket.io app. I hope you will use this starting point to do much more useful and interesting applications.

Saturday, March 5, 2022

Deploy ReactJS app to Azure Blob Storage through GitHub Actions

This tutorial will show you how easy it is to deploy a simple ReactJS single page application to Azure Blob Storage.

Source code: https://github.com/medhatelmasry/react2az.gitPre-requisites

The following pre-requisites are assumed:

  • The latest versions of Node.js & npm are installed on your computer
  • You have an Azure subscription
  • You have git installed on your computer
  • You have a GitHub account
  • You have some basic knowledge of ReactJS

Create a simple React app

Choose a suitable working folder and then execute the following command in a terminal window to create a ReactJS app named react2az:

npx create-react-app react2az --use-npm
cd react2az 
npm start

The app will display in your default browser as shown below:

Let us make this application more interesting. 

Install the following client-side packages for bootstrap support:

npm install react-bootstrap bootstrap

Visit React-Bootstrap · React-Bootstrap Documentation for documentation on how to use Bootstrap with ReactJS.

Create /src/pages folder, then add to this new folder a file named home-page.js with content:

import React from "react";
import { useState, useEffect } from "react";
import { Card, Col, Row } from "react-bootstrap";

const HomePage = () => {
  const [toonInfo, setToonInfo] = useState({});

  useEffect(() => {
    const fetchData = async () => {
      const result = await fetch(
        `https://api4all.azurewebsites.net/api/people/`
      );
      const body = await result.json();
      setToonInfo(body);
    };
    fetchData();
  }, []);

  const renderCard = (card, index) => {
    return (
      <Col>
        <Card style={{ width: "50%" }} key={index}>
          <Card.Img variant="top" src={card.pictureUrl} />
          <Card.Body>
            <Card.Title style={{ fontSize: "12px" }}>
              {card.firstName} {card.lastName}
            </Card.Title>
            <Card.Text style={{ fontSize: "10px" }}>
              {card.occupation}
            </Card.Text>
          </Card.Body>
        </Card>
      </Col>
    );
  };

  return (
    <React.Fragment>
      <Row xs={1} sm={2} md={3} lg={5} className="g-4">
        {Array.from(toonInfo).map((item, key) => renderCard(item, key))}
      </Row>
    </React.Fragment>
  );
};

export default HomePage;

The above code accesses an API at https://api4all.azurewebsites.net/api/people/ and renders the images of some cartoon characters in a bootstrap grid of cards.

Open src/App.js in your favourite editor and made the following changes:
  • Delete the first two import lines and replace them with the following:
import HomePage from './pages/home-page';
import 'bootstrap/dist/css/bootstrap.min.css';
  • Delete the content inside return (. . . ); and replace with:
<div className="container">
  <HomePage />
</div>

This means that your src/App.js will look like this:

import HomePage from './pages/home-page';
import 'bootstrap/dist/css/bootstrap.min.css';

function App() {
  return (
    <div className="container">
      <HomePage />
    </div>
  );
}

export default App;

Run your application and you should experience the following web page:

Push ReactJS app to GitHub

Note that your application already contains a .gitignore file. This means that the node_modules folder will be excluded for being pushed to GitHub.

Create a local git repo by running the following command in the root of your application:

git init
git add .
git commit -m "1st commit"

Login into your GitHub account and create a repository. In my case I created a repository by the same name of the application react2az. After you create a new repository, you will see a page that looks like this:



Copy the instructions under "…or push an existing repository from the command line" and execute them in the root of your application. This pushes your code to GitHub.

If you refresh the previous GitHub page, you will see your code had indeed been pushed to GitHub:

Create Storage Account on Azure

Visit https://portal.azure.com/ and login into your Azure subscription. Click on "Create a resource":

In the search field, enter the word storage, hit ENTER, then click on "Storage account".

Click on the blue Create button:



Beside "Resource group", click on "Create new" and give your resource group a suitable name. 


Meantime, change the other settings so that they are similar to the following:


Click on "Review + create":


On the next screen, click on Create. After a short while, your storage account will be provisioned and your page will look like this:

Click on "Go to resource".  In the search field, enter the word static then click on "Static website". 

Click on Enabled.

Enter index.html and 404.html then click on Save at the top.


Your static website will be placed into a folder named $web. You can go ahead and click on it to see that it contains nothing:

Click on your browser back button, clear the search field, then select "Access keys".



Click on "Show keys", copy the connection string, then temporarily paste it in a a text editor.

GitHub Actions

In GitHub, click on Settings:


Click on Secrets followed by Actions:


Click on "New repository secret".

Create a key named AZURE_STORAGE then paste the connection string that you previously saved in a text editor. Click on "Add secret".


Add a folder .github/workflows to your source code. Inside that folder, create a text file named react2az.yml with the following content:

name: Deploy React App to Azure Storage

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - '*'
defaults:
  run:
    working-directory: ./
env:
  NODE_VERSION: '17'                # set this to the node version to use
  CI: false
jobs:
  build-and-deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ env.NODE_VERSION }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ env.NODE_VERSION }}
    - name: npm install & build
      run: |
        # Build and test the project, then
        # deploy to Azure Web App.
        npm install
        npm run build
    - name: 'Deploy react app to Azure blob storage'
      uses: bacongobbler/azure-blob-storage-upload@main
      with:
          source_dir: './build'
          container_name: '$web'
          connection_string: ${{ secrets.AZURE_STORAGE }} 
          sync: 'true'

Once this file gets pushed or created on GitHub, you can see that the workflow gets kicked off. Click on Actions and you will see a workflow running. This workflow will likely have a different name than "workflow". 



If all goes well, the workflow will turn green, meaning that it completed successfully:

Viewing our web application

You must be dying to view the application hosted on Azure. Return to the Azure portal then go to "static websites". The URL for accessing your application is beside "Primary endpoint".

Visit the primary endpoint URL and you will see that your ReactJS app is happily hosted on Azure:


In Azure, if you click on $web you will see your files on Azure Blob Storage.

Happy coding and deploying.