Localization In Blazor App Using Microsoft.JSInterop

Introduction

In this article, we will see how to achieve localization in the Blazor app using Microsoft.JSInterop API.

We have already seen the basic features of a Blazor application in my previous articles on C# Corner. Please refer to the below articles to get some idea of the Blazor framework.

Blazor is an experimental .NET web framework using C#/Razor and HTML that runs in the browser with WebAssembly. Blazor provides the benefits of a client-side web UI framework. NET.

In this article, we will see the localization with Microsoft.JSInterop API.

Please open Visual Studio 2017 (I am using a free community edition) and create a Blazor app. Choose an ASP.NET Core Web Application project template.

Localization in Blazor App using Microsoft.JSInterop

We can choose Blazor (ASP.NET Core hosted) template.

ASP.NET Core hosted

Our solution will be ready in a moment. Please note that there are three projects created in our solution - “Client”, “Server”, and “Shared”.

The client project contains all the client-side libraries and Razor Views, while the server project contains the Web API Controller and other business logic. The shared project contains commonly shared files, like models and interfaces.

Client-side libraries

By default, Blazor creates many files in these three projects. We can remove all the unwanted files like “Counter .cshtml”, “FetchData.cshtml”, “SurveyPrompt.cshtml” from the Client project, and “SampleDataController.cs” file from the Server project.

In this app, we do not require a Shared project as well. We can remove that from our solution.

In the Server project, please create a “Resources” folder to keep your resource files inside this folder. These resource files will be used for localization later.

We can create our first locale resource for the “en” (English) locale.

 Locale resource

We can add “Name” and “Value” inside this resource file. I will add 5 key-value pairs in this resource file.

Resource file

We can create one more resource file for “hi” (Hindi) locale now.

I must add 5 key-value pairs for this resource file also.

 5 key-value pairs

We can create a new folder “Extensions” and create a new class “CsrfTokenCookieMiddleware” inside that folder.

Please copy the below code and paste it to the new class.

CsrfTokenCookieMiddleware.cs

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace BlazorLocalization.Server.Extensions
{
    public class CsrfTokenCookieMiddleware
    {
        private readonly IAntiforgery _antiforgery;
        private readonly RequestDelegate _next;

        public CsrfTokenCookieMiddleware(IAntiforgery antiforgery, RequestDelegate next)
        {
            _antiforgery = antiforgery;
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            if (context.Request.Cookies["CSRF-TOKEN"] == null)
            {
                var token = _antiforgery.GetAndStoreTokens(context);
                context.Response.Cookies.Append("CSRF-TOKEN", token.RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
            }
            await _next(context);
        }
    }
}

We can modify Startup.cs file with below changes.

Startup.cs

using BlazorLocalization.Server.Extensions;
using Microsoft.AspNetCore.Blazor.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.DependencyInjection;
using System.Globalization;
using System.Linq;
using System.Net.Mime;

namespace BlazorLocalization.Server
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddResponseCompression(options =>
            {
                options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
                {
                    MediaTypeNames.Application.Octet,
                    WasmMediaTypeNames.Application.Wasm,
                });
            });

            services.AddLocalization(options => options.ResourcesPath = "Resources");
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseResponseCompression();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            var supportedCultures = new[]
            {
                new CultureInfo("en"),
                new CultureInfo("hi")
            };

            app.UseRequestLocalization(new RequestLocalizationOptions
            {
                DefaultRequestCulture = new RequestCulture("en"),
                // Formatting numbers, dates, etc.
                SupportedCultures = supportedCultures,
                // UI strings that we have localized.
                SupportedUICultures = supportedCultures
            });

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "default", template: "{controller}/{action}/{id?}");
            });

            app.UseMiddleware<CsrfTokenCookieMiddleware>();
            app.UseBlazor<Client.Program>();
        }
    }
}

We have added “CultureInfo” for our two locale files. We also invoked “CsrfTokenCookieMiddleware” class in Startup file.

We can create a Web API controller now. This API will be used for localization.

Please copy below code and paste in the Controller file.

I18nController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using System.Collections.Generic;
using System.Linq;

namespace BlazorLocalization.Server
{
    // This class is used for naming the Resource files.   
    // You must specify exact name of Resource here.   
    public class BlazorResource { }
}

namespace BlazorLocalization.Server.Controllers
{
    [Route("api/[controller]/")]
    public class I18nController : Controller
    {
        private IStringLocalizer<BlazorResource> stringLocalizer;

        public I18nController(IStringLocalizer<BlazorResource> stringLocalizer)
        {
            this.stringLocalizer = stringLocalizer;
        }

        [HttpGet]
        public ActionResult GetClientTranslations()
        {
            var res = new Dictionary<string, string>();
            return Ok(stringLocalizer.GetAllStrings().ToDictionary(s => s.Name, s => s.Value));
        }
    }
}

We can add a class for our Resource files now. This class is used for naming the Resource files. Please give the same name to the class as our Resource file.

We must create this class inside the Web API Controller file. This is an empty class. We have completed the coding part in the Server project. Now, we can add files to the Client project.

Add a new “Services” folder. We will add some interfaces and classes inside this folder. Please add “JsInterop” class.

JsInterop.cs

using Microsoft.JSInterop;
using System.Threading.Tasks;

namespace BlazorLocalization.Client.Services
{
    public static class JsInterop
    {
        public static async Task<string[]> Languages()
        {
            return await JSRuntime.Current.InvokeAsync<string[]>("navigatorLanguages");
        }

        public static async Task<string> GetCookie()
        {
            return await JSRuntime.Current.InvokeAsync<string>("getDocumentCookie");
        }
    }
}

We will call two JavaScript methods “navigatorLanguages” and “getDocumentCookie” from this class. The JavaScript file will be added later.

Please add the below 4 interfaces and 4 classes inside the Services folder.

IBrowserCookieService.cs

using System;
using System.Threading.Tasks;

namespace BlazorLocalization.Client.Services
{
    public interface IBrowserCookieService
    {
        Task<string> Get(Func<string, bool> filterCookie);
    }
}

BrowserCookieService.cs

using System;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorLocalization.Client.Services
{
    public class BrowserCookieService : IBrowserCookieService
    {
        public async Task<string> Get(Func<string, bool> filterCookie)
        {
            return (await JsInterop
                .GetCookie())
                .Split(';')
                .Select(v => v.TrimStart().Split('='))
                .Where(s => filterCookie(s[0]))
                .Select(s => s[1])
                .FirstOrDefault();
        }
    }
}

IHttpApiClientRequestBuilder.cs

using System;
using System.Threading.Tasks;

namespace BlazorLocalization.Client.Services
{
    public interface IHttpApiClientRequestBuilder
    {
        Task GetAsync();
        HttpApiClientRequestBuilder OnOK<T>(Action<T> todo);
        void SetHeader(string v, string lg);
    }
}

HttpApiClientRequestBuilder.cs

using Microsoft.JSInterop;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace BlazorLocalization.Client.Services
{
    public class HttpApiClientRequestBuilder : IHttpApiClientRequestBuilder
    {
        private readonly string _uri;
        private HttpClient _httpClient;
        private Func<HttpResponseMessage, Task> _onOK;
        private IBrowserCookieService _browserCookieService;

        public HttpApiClientRequestBuilder(HttpClient httpClient, string uri, IBrowserCookieService browserCookieService)
        {
            _uri = uri;
            _httpClient = httpClient;
            _browserCookieService = browserCookieService;
        }

        public async Task GetAsync()
        {
            await ExecuteHttpQueryAsync(async () => await _httpClient.SendAsync(await PrepareMessageAsync(new HttpRequestMessage(HttpMethod.Get, _uri))));
        }

        public HttpApiClientRequestBuilder OnOK<T>(Action<T> todo)
        {
            _onOK = async (HttpResponseMessage r) =>
            {
                var response = Json.Deserialize<T>(await r.Content.ReadAsStringAsync());
                todo(response);
            };
            return this;
        }

        public void SetHeader(string key, string value)
        {
            _httpClient.DefaultRequestHeaders.Add(key, value);
        }

        private async Task HandleHttpResponseAsync(HttpResponseMessage response)
        {
            switch (response.StatusCode)
            {
                case System.Net.HttpStatusCode.OK:
                    if (_onOK != null)
                        await _onOK(response);
                    break;
                case System.Net.HttpStatusCode.BadRequest:
                    break;
                case System.Net.HttpStatusCode.InternalServerError:
                    break;
            }
        }

        private async Task<HttpRequestMessage> PrepareMessageAsync(HttpRequestMessage httpRequestMessage)
        {
            string csrfCookieValue = await _browserCookieService.Get(c => c.Equals("CSRF-TOKEN"));
            if (csrfCookieValue != null)
                httpRequestMessage.Headers.Add("X-CSRF-TOKEN", csrfCookieValue);
            return httpRequestMessage;
        }

        private async Task ExecuteHttpQueryAsync(Func<Task<HttpResponseMessage>> httpCall)
        {
            try
            {
                var response = await httpCall();
                await HandleHttpResponseAsync(response);
            }
            catch
            {
                throw;
            }
            finally
            {
            }
        }
    }
}

IHttpApiClientRequestBuilderFactory.cs

namespace BlazorLocalization.Client.Services
{
    public interface IHttpApiClientRequestBuilderFactory
    {
        IHttpApiClientRequestBuilder Create(string url);
    }
}

HttpApiClientRequestBuilderFactory.cs

using System.Net.Http;

namespace BlazorLocalization.Client.Services
{
    public class HttpApiClientRequestBuilderFactory : IHttpApiClientRequestBuilderFactory
    {
        private readonly HttpClient _httpClient;
        private readonly IBrowserCookieService _browserCookieService;

        public HttpApiClientRequestBuilderFactory(HttpClient httpClient, IBrowserCookieService browserCookieService)
        {
            _httpClient = httpClient;
            this._browserCookieService = browserCookieService;
        }

        public IHttpApiClientRequestBuilder Create(string url)
        {
            return new HttpApiClientRequestBuilder(_httpClient, url, _browserCookieService);
        }
    }
}

II18nService.cs

using System.Threading.Tasks;

namespace BlazorLocalization.Client.Services
{
    public interface II18nService
    {
        Task<string> Get(string name);

        void Init(string lg);
    }
}

I18nService.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorLocalization.Client.Services
{
    public class I18nService : II18nService
    {
        private readonly IHttpApiClientRequestBuilderFactory _httpApiClientRequestBuilderFactory;
        private Lazy<Task<Dictionary<string, string>>> _translations;

        public I18nService(IHttpApiClientRequestBuilderFactory httpApiClientRequestBuilderFactory)
        {
            _httpApiClientRequestBuilderFactory = httpApiClientRequestBuilderFactory;
            _translations = new Lazy<Task<Dictionary<string, string>>>(() => FetchTranslations(null));
        }

        private async Task<Dictionary<string, string>> FetchTranslations(string lg)
        {
            var client = _httpApiClientRequestBuilderFactory.Create("/api/i18n");
            if (lg != null)
                client.SetHeader("accept-language", lg);

            Dictionary<string, string> res = null;
            await client.OnOK<Dictionary<string, string>>(r => res = r).GetAsync();
            return res;
        }

        public async Task<string> Get(string key)
        {
            return !(await _translations.Value).TryGetValue(key, out string value) ? key : value;
        }

        public void Init(string lg)
        {
            _translations = new Lazy<Task<Dictionary<string, string>>>(() => FetchTranslations(lg));
        }
    }
}

We can add “Locale.cshtml” inside the “Shared” folder. This Razor View will be used for displaying the values from Resource file using I18nService. This Razor View acts as a user-defined control/child view.

Locale.cshtml

@using BlazorLocalization.Client.Services

@inject II18nService i18n;

@displayValue

@functions{
    [Parameter]
    string key { get; set; }

    public string displayValue { get; set; }
    protected override async Task OnInitAsync()
    {
        await i18n.Get(key).ContinueWith(t =>
        {
            displayValue = t.Result;
            this.StateHasChanged();
        });
    }
}

We can modify the “NavMenu.cshtml” file with the below code.

NavMenu.cshtml

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href=""><Locale key="Title" /></a>
    <button class="navbar-toggler" onclick=@ToggleNavMenu>
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match=NavLinkMatch.All>
                <span class="oi oi-home" aria-hidden="true"></span> <Locale key="HomeMenu" />
            </NavLink>
        </li>
    </ul>
</div>

@functions {
    bool collapseNavMenu = true;

    void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

We have used our Local Child View inside this View.

We can add “localization.js” file inside the “wwwroot” folder.

localization.js

getDocumentCookie = function () {
    return Promise.resolve(document.cookie);
};

navigatorLanguages = function () {
    return Promise.resolve(navigator.languages);
};

Here, we have added two JavaScript methods inside this file.

Modify the index.html under the wwwroot folder.

index.html (wwwroot folder)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width">
    <title>Blazor Localization</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>Loading...</app>

    <script src="/localization.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

I have added the script path of localization.js file in this HTML file.

We can now modify index.html under Pages folder.

index.html (Pages folder)

@page "/"

<h1><Locale key="Welcome" /></h1>
<hr />
<p><Locale key="About" /> </p>
<hr />
<p>Localization in Blazor By : <b><Locale key="Developer" /></b></p>

Please remove the Starup.cs file from Client project. We do not require it. We can modify “Program.cs” file with below code.

Program.cs

using BlazorLocalization.Client.Services;
using Microsoft.AspNetCore.Blazor.Browser.Rendering;
using Microsoft.AspNetCore.Blazor.Browser.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using System.Globalization;
using System.Linq;

namespace BlazorLocalization.Client
{
    public class Program
    {
        public static BrowserServiceProvider serviceProvider;

        public static void Main(string[] args)
        {
            serviceProvider = new BrowserServiceProvider(configure =>
            {
                configure.Add(new ServiceDescriptor(
                    typeof(IHttpApiClientRequestBuilderFactory),
                    typeof(HttpApiClientRequestBuilderFactory),
                    ServiceLifetime.Scoped));
                configure.Add(new ServiceDescriptor(
                      typeof(IBrowserCookieService),
                      typeof(BrowserCookieService),
                      ServiceLifetime.Singleton));
                configure.Add(new ServiceDescriptor(
                    typeof(II18nService),
                    typeof(I18nService),
                    ServiceLifetime.Singleton));
            });

            JSRuntime.Current.InvokeAsync<string[]>("navigatorLanguages")
               .ContinueWith(t => CultureInfo.DefaultThreadCurrentCulture = t.Result.Select(c => CultureInfo.GetCultureInfo(c)).FirstOrDefault())
               .ContinueWith(t => new BrowserRenderer(serviceProvider).AddComponent<App>("app"));
        }
    }
}

We have injected all the dependencies for our services inside this file.

We have completed all the coding part. We can run the application now.

Our application will display all the text values in English locale ( the default language of my browser is English).

 English locale

Now, we can change the browser language settings to Hindi.

Please refresh the screen. It will automatically change the display text to Hindi. It takes the value according to the Hindi resource file.

Hindi resource file

We can add one more resource file. This time I will add Malayalam resource. I will again add 5 key-value pair values for Malayalam locale.

Malayalam locale

We can add these locale settings to Startup.cs file also.

Locale settings

We can again run the application and change the browser language to Malayalam. It will display all the text values in the Malayalam language.

Malayalam language

In this article, we saw localization in Blazor with the help of Microsoft.JSInterop API.

For this article, I was inspired by many good articles. Mainly I got some reference for localization service from this GitHub repo. Really thankful to the author Mr. Rémi Bourgarel.

We can see more Blazor features in upcoming articles.


Similar Articles