Build Multilanguage in .NET 8

This course aims to construct a website that supports multiple languages using NET 8 and includes the creation of localized resources and language-switching features.

The purpose of this article is to discuss the process of building a multi-language site using NET 8.

Globalization

The objective of globalization in software development is to ensure that applications are accessible, usable, and relevant to a global audience, regardless of their location or language.

Localization

The creation of localized resources for every supported language and region is a crucial aspect of globalization in software development. Based on the user’s preferences or the system’s settings, these resources can dynamically change the software’s behavior and presentation.

Globalization vs. localization

The goal of globalization is to make software accessible to global audiences, while localization involves adapting it to meet specific requirements of specific countries and regions.

Setting up the project in steps

A demonstration of localization in ASP.NET It will be displayed. Should start by creating a new ASP.NET application. Installing some NuGet packages which are shown below.

Microsoft.AspNetCore.Localization
ResXResourceReader.NetStandard
Newtonsoft.Json

Blow shared some images related to the code.

Step 1.

Home controller

Step 2.

Resources

Step 3. Program.cs

Program

using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options;
using ProjectLanguage.Models;
using System.Globalization;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<LanguageConversationService>();
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddSession();
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddMvc().AddViewLocalization().AddDataAnnotationsLocalization(options =>
{
    options.DataAnnotationLocalizerProvider = (type, factory) =>
    {
        var assemblyName = new AssemblyName(typeof(Resource).GetTypeInfo().Assembly.FullName!);
        return factory.Create("Resource", assemblyName.Name!);
    };
});
builder.Services.Configure<RequestLocalizationOptions>(
    options =>
    {
        var supportedCultures = new List<CultureInfo> { new CultureInfo("ar-AE"), new CultureInfo("fr-FR"), new CultureInfo("en-US") };
        options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
        options.SupportedCultures = supportedCultures;
        options.SupportedUICultures = supportedCultures;
        options.RequestCultureProviders.Insert(0, new QueryStringRequestCultureProvider());
    });
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRequestLocalization(app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value);
app.UseRouting();
app.UseSession();
app.UseAuthorization();
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();

Step 4. Res.cshtml

Res

@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>Add Res</h1>
<div class="mb-5">
    <a href="/Home/Sync" class="btn btn-outline-primary d-flex float-end">Sync</a>
</div>
<form asp-action="Res">
    <div class="row">
        <div class="col-3">
            <div class="form-group">
                <label for="Key">Key</label>
                <input type="text" class="form-control" name="Key" id="Key" placeholder="Enter Key">
            </div>
        </div>
        <div class="col-3">
            <div class="form-group">
                <label for="EN">EN</label>
                <input type="text" class="form-control" name="EN" id="EN" placeholder="Enter EN">
            </div>
        </div>
        <div class="col-3">
            <div class="form-group">
                <label for="AE">AE</label>
                <input type="text" class="form-control" name="AE" id="AE" placeholder="Enter AE">
            </div>
        </div>
        <div class="col-3">
            <div class="form-group">
                <label for="FR">FR</label>
                <input type="text" class="form-control" name="FR" id="FR" placeholder="Enter FR">
            </div>
        </div>
        <div class="col-3 mt-1">
            <button type="submit" class="btn btn-primary">Add</button>
        </div>
    </div>
</form>
<table class="table">
    <thead>
        <tr>
            <th scope="col">key</th>
            <th scope="col">En</th>
            <th scope="col">AE</th>
            <th scope="col">FR</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in ViewBag.lans)
        {
            <tr>
                <th>@item.Key</th>
                <td>@item.EN</td>
                <td>@item.AE</td>
                <td>@item.FR</td>
            </tr>
        }
    </tbody>
</table>

Step 5. Index.cshtml

Index

@{
    ViewData["Title"] = "Home Page";
}
<select class="default-select language-switch form-control wide ms-0" id="Language" asp-for="@ViewData["Language"]" onchange="LanguageSwitch()">
    <option value="en-US">EN</option>
    <option value="fr-FR">FR</option>
    <option value="ar-AE">AE</option>
</select>
<div class="text-center">
    <h1 class="display-4">@_localization.Getkey("Welcome")</h1>
    <p class="">@_localization.Getkey("WelcomeMessage")</p>
</div>
@section scripts {
    <script>
        function LanguageSwitch() {
            $.ajax({
                url: '/Home/ChangeLanguage?culture=' + $("#Language").val(),
                type: 'GET',
                success: function (response) {
                    if ($("#Language").val() == "ar-AE") {
                        $('body').css('direction', 'rtl');
                    } else {
                        $('body').css('direction', 'ltr');
                    }
                    location.reload();
                },
                error: function (error) {
                    alert(error);
                }
            });
        }
    </script>
}

Step 6. _Layout.cshtml

Layout

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - New Project For Language</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/ProjectLanguage.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">New Project For Language</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Res">Res</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>
    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2024 - ProjectLanguage - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Step 7. _ViewImports.cshtml

View imports

@using ProjectLanguage
@using ProjectLanguage.Models
@inject LanguageConversationService _localization
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Step 8. HomeController.cs

Home controller

using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using ProjectLanguage.Models;
using System.Collections;
using System.Diagnostics;

namespace ProjectLanguage.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public HomeController(ILogger<HomeController> logger, IHttpContextAccessor httpContextAccessor)
        {
            _logger = logger;
            _httpContextAccessor = httpContextAccessor;
        }

        public IActionResult Index()
        {
            var languageVl = HttpContext.Request.Cookies[".AspNetCore.Culture"] ?? "";
            ViewData["Language"] = string.IsNullOrEmpty(languageVl) ? "" : languageVl.Split("=")[2];
            return View();
        }

        public IActionResult Res()
        {
            string lanstr = _httpContextAccessor.HttpContext!.Session.GetString("LanCon") ?? "";
            if (string.IsNullOrEmpty(lanstr))
            {
                var en = ResourceFile.GetResourceFile(@"Resources\Resource.en-US.resx");
                var ae = ResourceFile.GetResourceFile(@"Resources\Resource.ar-AE.resx");
                var fr = ResourceFile.GetResourceFile(@"Resources\Resource.fr-FR.resx");
                List<Lan> list = new List<Lan>();
                foreach (DictionaryEntry item in en)
                {
                    Lan lan = new Lan()
                    {
                        Key = item.Key!.ToString() ?? "",
                        EN = item.Value!.ToString() ?? "",
                        AE = ae[item.Key?.ToString()!]?.ToString()!,
                        FR = fr[item.Key?.ToString()!]?.ToString()!,
                    };
                    list.Add(lan);
                }
                _httpContextAccessor.HttpContext!.Session.SetString("LanCon", JsonConvert.SerializeObject(list));
            }
            ViewBag.lans = JsonConvert.DeserializeObject<List<Lan>>(lanstr) ?? new List<Lan>();
            return View();
        }

        [HttpPost]
        public IActionResult Res(string Key, string EN, string AE, string FR)
        {
            string lanstr = _httpContextAccessor.HttpContext!.Session.GetString("LanCon") ?? "";
            List<Lan> lans = JsonConvert.DeserializeObject<List<Lan>>(lanstr) ?? [];
            lans.Add(new Lan() { Key = Key, EN = EN, AE = AE, FR = FR });
            _httpContextAccessor.HttpContext!.Session.SetString("LanCon", JsonConvert.SerializeObject(lans));
            ViewBag.lans = JsonConvert.DeserializeObject<List<Lan>>(lanstr) ?? [];
            return RedirectToAction("Res");
        }

        public IActionResult Sync()
        {
            string lanstr = _httpContextAccessor.HttpContext!.Session.GetString("LanCon") ?? "";
            List<Lan> l1s = JsonConvert.DeserializeObject<List<Lan>>(lanstr) ?? [];
            Hashtable hashtableEn = [];
            Hashtable hashtableAr = [];
            Hashtable hashtableFr = [];
            foreach (var item in l1s)
            {
                hashtableEn.Add(item.Key ?? "", item.EN);
                hashtableAr.Add(item.Key ?? "", item.AE);
                hashtableFr.Add(item.Key ?? "", item.FR);
            }
            ResourceFile.SyncResourceFile(hashtableEn, @"Resources\Resource.en-US.resx");
            ResourceFile.SyncResourceFile(hashtableAr, @"Resources\Resource.ar-AE.resx");
            ResourceFile.SyncResourceFile(hashtableFr, @"Resources\Resource.fr-FR.resx");
            return RedirectToAction("Index");
        }

        [HttpGet]
        public async Task<IActionResult> ChangeLanguage(string culture)
        {
            Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)), new CookieOptions()
            {
                Expires = DateTimeOffset.UtcNow.AddDays(200)
            });
            return Json(new { });
        }
    }
}

Step 9. LanguageConversationService.cs

Language conversation

using Microsoft.Extensions.Localization;
using System.Collections;
using System.Reflection;
using System.Resources.NetStandard;
namespace ProjectLanguage.Models
{
    public class Resource { }
    public class LanguageConversationService
    {
        private readonly IStringLocalizer _localizer;
        public LanguageConversationService(IStringLocalizerFactory factory)
        {
            var assemblyName = new AssemblyName(typeof(Resource).GetTypeInfo().Assembly.FullName!);
            _localizer = factory.Create("Resource", assemblyName.Name!);
        }
        public LocalizedString Getkey(string key)
        {
            return _localizer[key];
        }
    }
    public class ResourceFile
    {
        public static Hashtable GetResourceFile(string path)
        {
            Hashtable resourceEntries = new Hashtable();
            ResXResourceReader reader = new ResXResourceReader(path);
            if (reader != null)
            {
                IDictionaryEnumerator id = reader.GetEnumerator();
                foreach (DictionaryEntry d in reader)
                {
                    if (d.Value == null)
                        resourceEntries.Add(d.Key!.ToString() ?? "", "");
                    else
                        resourceEntries.Add(d.Key!.ToString() ?? "", d.Value.ToString());
                }
                reader.Close();
            }
            return resourceEntries;
        }
        public static void SyncResourceFile(Hashtable data, string path)
        {
            Hashtable resourceEntries = new Hashtable();
            ResXResourceReader reader = new ResXResourceReader(path);
            if (reader != null)
            {
                IDictionaryEnumerator id = reader.GetEnumerator();
                foreach (DictionaryEntry d in reader)
                {
                    if (d.Value == null)
                        resourceEntries.Add(d.Key!.ToString() ?? "", "");
                    else
                        resourceEntries.Add(d.Key!.ToString() ?? "", d.Value.ToString());
                }
                reader.Close();
            }
            foreach (string key in data.Keys)
            {
                if (!resourceEntries.ContainsKey(key))
                {
                    string value = data[key]!.ToString() ?? "";
                    if (value == null)
                        value = "";
                    resourceEntries.Add(key, value);
                }
                else
                {
                    resourceEntries.Remove(key);
                    string value = data[key]!.ToString() ?? "";
                    if (value == null)
                        value = "";
                    resourceEntries.Add(key, value);
                }
            }
            ResXResourceWriter resourceWriter = new ResXResourceWriter(path);
            foreach (string key in resourceEntries.Keys)
            {
                resourceWriter.AddResource(key, resourceEntries[key]);
            }
            resourceWriter.Generate();
            resourceWriter.Close();
        }
    }
    public class Lan
    {
        public string Key { get; set; }
        public string EN { get; set; }
        public string AE { get; set; }
        public string FR { get; set; }
    }
}

Output

Add res

Welcome