Code first Json localization on Blazor Web App

Xavier Solau
YounitedTech
Published in
9 min readApr 18, 2024
Photo by Alexander Schimmeck on Unsplash

Have you ever used localization with dotnet and Blazor? If so, you’ve probably noticed that it can be quite verbose in some way because you have to use textual keys to access localized string values in the code that you have to repeat in the Json files and that you have to maintain manually. In this article, I will demonstrate how to simplify all this writing almost only C# interfaces.

Note that we will see a practical use case of the localization tooling in a Blazor Web App but it can be used in any kind of dotnet projects.

Create a Blazor Web App

First, we are going to set up a project with the new Blazor Web App template that was released with .Net 8.0 and we are going to enable Json localization.

The Blazor Web App project provides both support for server-side and client-side web assembly rendering.

Note that you can learn more about Blazor Hybrid Web Application on this page.

To create a new Blazor hybrid web application, we can run the dotnet command:

dotnet new blazor --name MyWebApp -o MyWebApp -int WebAssembly

This is resulting in the creation of 2 projects:

  • One for web hosting and server-side rendering:
    MyWebApp\MyWebApp.csproj
  • And another one for the web assembly client:
    MyWebApp.Client\MyWebApp.Client.csproj

Enable Json localization

Photo by Scott Graham on Unsplash

Now that our web application is created, we can upgrade our projects with Json localization support. To do so we will update both projects adding some dependencies and some services provided by this library.

In this example we are going to host the Json localization files as HTTP static assets in the application.

Set up server-side project

First, update the server project to add the Nuget package that enables the Json localization features:

dotnet add MyWebApp/MyWebApp.csproj package SoloX.BlazorJsonLocalization.ServerSide
dotnet add MyWebApp/MyWebApp.csproj package SoloX.BlazorJsonLocalization.Attributes

Note that the SoloX.BlazorJsonLocalization.Attributes package is only required for the code first tooling.

Update the MyWebApp/Program.cs file to register the localization services like this:

using SoloX.BlazorJsonLocalization.ServerSide;

// [...]

// Add services to enable Json localization.
builder.Services
.AddServerSideJsonLocalization(builder =>
{
builder
.UseHttpHostedJson(options =>
{
// Set up the HTTP main static assets contributor assemblies.
// Here we need both the Host assembly and the Client because the
// host can render components from both the host side or the client
// side.
options.ApplicationAssemblies =
[
typeof(MyWebApp.Client._Imports).Assembly,
typeof(MyWebApp.Components.App).Assembly
];
});
});

Set up client-side project

Then update the client project to add the Nuget package to enable Json localization features dedicated to WebAssembly:

dotnet add MyWebApp.Client/MyWebApp.Client.csproj package SoloX.BlazorJsonLocalization.WebAssembly
dotnet add MyWebApp.Client/MyWebApp.Client.csproj package SoloX.BlazorJsonLocalization.Attributes

Update the MyWebApp.Client/Program.cs file to register the localization services like this:

using SoloX.BlazorJsonLocalization.WebAssembly;

// [...]

// Since our localization files are HTTP assets, we need to inject the Host
// HttpClient.
builder.Services
.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});

// Add services to enable Json localization.
builder.Services
.AddWebAssemblyJsonLocalization(builder =>
{
builder
.UseHttpHostedJson(options =>
{
// Setup the HTTP main static assets contributor assemblies.
// Here we only need the Client assembly because the Client won't
// use any localization from the server side.
options.ApplicationAssembly =
typeof(MyWebApp.Client._Imports).Assembly;
});
});

Install the localization generator tool

Photo by Sigmund on Unsplash

Code First Localization feature requires to use a dotnet tool to generate all localization code and the default Json files. We are going to register the tool within the current solution.

We need to create a dotnet tool manifest file within the solution:

dotnet new tool-manifest

Once the manifest is created, we can register the tool:

dotnet tool install SoloX.BlazorJsonLocalization.Tools.Command

The LocalizationGen tool is now ready to be used and you can use it with the dotnet command:

dotnet localizationgen

Create our localized messages

Photo by Bekky Bekks on Unsplash

Now we are going to create our first application localized messages.

In this example we will create 2 sets of messages: one dedicated to the server project and one another for the client project.

Note that you may define multiple localization sets. For example, you may define one set per component or per page, depending on your preferences.

To define your messages you will need to write an interface with one property or one method to point to one localized message. The idea is to inject this interface in your component to get access to your localized message values.

The LocalizationGen tool will help you to write the class implementing your interface.

Create a localizer on the server-side

On the server project we can create an interface to describe your messages and the way you want to use it. Since this localization tooling is based on the .Net localization abstraction our interface is extending Microsoft.Extensions.Localization.IStringLocalizer<T>.

Let’s create the file:
MyWebApp/Localizer/IServerGlobalStringLocalizer.cs

And write the interface:

using Microsoft.Extensions.Localization;
using SoloX.BlazorJsonLocalization.Attributes;

namespace MyWebApp.Localizer
{
/// <summary>
/// The component class.
/// In this example it is an empty class but instead you can use directly
/// a razor component.
/// </summary>
public class ServerGlobal
{
}

/// <summary>
/// Here, we use the Localizer attribute to tell the LocalizationGen tool
/// that we need the Json file stored in the wwwroot folder and that we
/// want the fr-FR and en-UK files in addition of the default Json file.
/// </summary>
[Localizer("wwwroot", ["fr-FR", "en-UK"])]
public interface IServerGlobalStringLocalizer :
IStringLocalizer<ServerGlobal>
{
/// <summary>
/// Let's define some properties to access the localized values.
/// The optional Translate attribute allows to define a default
/// translation for the localized entry.
/// </summary>

[Translate("Home")]
string HomePageTitle { get; }

[Translate("Hello, world!")]
string HelloTitle { get; }

[Translate("Welcome to your new app.")]
string HelloBody { get; }
}
}

Create a localizer on the client-side

Now we can create the interface on the client project with the file:
MyWebApp.Client/Localizer/IClientGlobalStringLocalizer.cs

And write the interface:

using Microsoft.Extensions.Localization;
using SoloX.BlazorJsonLocalization.Attributes;

namespace MyWebApp.Client.Localizer
{
/// <summary>
/// The component class.
/// In this example it is an empty class but instead you can use directly
/// a razor component.
/// </summary>
public class ClientGlobal
{
}

/// <summary>
/// Here, we use the Localizer attribute to tell the LocalizationGen tool
/// that we need the Json file stored in the wwwroot folder and that we
/// want the fr-FR and en-UK files in addition of the default Json file.
/// </summary>
[Localizer("wwwroot", ["fr-FR", "en-UK"])]
public interface IClientGlobalStringLocalizer :
IStringLocalizer<ClientGlobal>
{
/// <summary>
/// Let's define properties to access the localized values.
/// The optional Translate attribute allows to define a default
/// translation for the localized entry.
/// </summary>

[Translate("Counter")]
string CounterPageTitle { get; }

[Translate("Counter")]
string CounterTitle { get; }

[Translate("Click me")]
string ClickMe { get; }

/// <summary>
/// Here is an example where we define a localized entry with a method
/// instead of a property. This is to provide a way to use arguments in
/// your localized message.
/// </summary>
/// <param name="currentCount">
/// The argument we want to use in the message.
/// </param>
/// <returns>The localized value with the argument(s) serialized within
/// the message.
/// </returns>
[Translate("Current count: {0}")]
string CurrentCount(int currentCount);
}
}

Generate the localization implementation

Photo by Chad Kirchoff on Unsplash

Using the LocalizationGen tool

Once your interfaces are created, we can run the LocalizationGen tool to generate the interface implementations and the associated Json resources.

dotnet localizationgen MyWebApp/MyWebApp.csproj

The tool will generate the CS files:
MyWebApp\Localizer\ServerGlobalStringLocalizer.g.cs
MyWebApp\Localizer\ServerGlobalStringLocalizerExtensions.g.cs

And the Json files:
MyWebApp\wwwroot\Localizer\ServerGlobal.json
MyWebApp\wwwroot\Localizer\ServerGlobal.fr-FR.json
MyWebApp\wwwroot\Localizer\ServerGlobal.en-UK.json

Then we can run the command on the client side:

dotnet localizationgen MyWebApp.Client/MyWebApp.Client.csproj

Resulting in the CS files:
MyWebApp.Client\Localizer\ClientGlobalStringLocalizer.g.cs
MyWebApp.Client\Localizer\ClientGlobalStringLocalizerExtensions.g.cs

And the Json files:
MyWebApp.Client\wwwroot\Localizer\ClientGlobal.json
MyWebApp.Client\wwwroot\Localizer\ClientGlobal.fr-FR.json
MyWebApp.Client\wwwroot\Localizer\ClientGlobal.en-UK.json

Update translation in the Json files

As you can see, the Json files have been generated with the default value or with the value defined with the attribute Translate.

ServerGlobal.json will look like:

{
"HomePageTitle": "Home",
"HelloTitle": "Hello, world!",
"HelloBody": "Welcome to your new app."
}

And ClientGlobal.json will look like:

{
"CounterPageTitle": "Counter",
"CounterTitle": "Counter",
"CurrentCount": "Current count: {0}",
"ClickMe": "Click me"
}

You can update those files with the proper translations and don’t worry, if you run the generation tool again your translation won’t be overwritten. Only new message key will be added.

In this example we can change the ServerGlobal.fr-FR.json and the ClientGlobal.fr-FR.json with the proper French message translations.

The ServerGlobal.fr-FR.json will look like this:

{
"HomePageTitle": "Accueil",
"HelloTitle": "Bonjour à tous !",
"HelloBody": "Bienvenu dans votre nouvelle application."
}

And ClientGlobal.fr-FR.json like this:

{
"CounterPageTitle": "Compteur",
"CounterTitle": "Compteur",
"CurrentCount": "Compte courant : {0}",
"ClickMe": "Cliquer ici"
}

Register and use the localizer

Photo by NASA on Unsplash

Now that the localizer interface implementations are generated, we can update the services registration to be able to inject the right implementation in your components.

Register the implementations

In the server project we can update the MyWebApp/Program.cs file:

using MyWebApp.Client.Localizer;
using MyWebApp.Localizer;

// [...]

builder.Services
// Register the server global localization.
.AddTransient<IServerGlobalStringLocalizer, ServerGlobalStringLocalizer>()
// Since client pages can also be rendered on server side we need to
// register the client global localization.
.AddTransient<IClientGlobalStringLocalizer, ClientGlobalStringLocalizer>();

And in the client project we can update the file:
MyWebApp.Client/Program.cs

using MyWebApp.Client.Localizer;

// [...]

builder.Services
// Register the client global localization.
.AddTransient<IClientGlobalStringLocalizer, ClientGlobalStringLocalizer>();

We also need to make sure that the localization resources are really loaded since the Json files are hosted on the server side as static asserts. This is especially true for the client components since the Json file is actually loaded through the HTTP connection.

In order to make sure this is loaded before it is used, we can update MyWebApp.Client/Program.cs like this:

using SoloX.BlazorJsonLocalization;

// [...]

// Load localization resources asynchronously
// replacing :
//
//await builder.Build().RunAsync();
//
// with
//
var webAssemblyHost = builder.Build();

// Get the client localizer.
var localizer = webAssemblyHost.Services
.GetRequiredService<IClientGlobalStringLocalizer>();

// Load resources.
await localizer.LoadAsync();

// And run the application.
await webAssemblyHost.RunAsync();

Note that we could have use the embedded configuration of the Json resources so that we wouldn’t need to care about asynchronous load of the resources.

Add default using statements

Update the default imports files to include the Localizer namespace:

In MyWebApp/Components/_imports.razor file:

@using MyWebApp.Localizer
@using MyWebApp.Client.Localizer

And in MyWebApp.Client/_imports.razor file:

@using MyWebApp.Client.Localizer

All is ready to be used

Then we can update the Home page in the file
MyWebApp/Components/Pages/Home.razor

@page "/"

@* Inject the server global localizer *@

@inject IServerGlobalStringLocalizer Localizer

@*
Then we can use the localizer properties to get the localized messages.
*@

<PageTitle>@Localizer.HomePageTitle</PageTitle>

<h1>@Localizer.HelloTitle</h1>

@Localizer.HelloBody

And we can use it in the client-side WebAssembly components for example, in MyWebApp.Client/Pages/Counter.razor:

@page "/counter"
@rendermode InteractiveWebAssembly

@* Inject the client global localizer *@

@inject IClientGlobalStringLocalizer Localizer

@*
Then we can use the localizer properties or methods to get the localized
messages.
*@

<PageTitle>@Localizer.CounterPageTitle</PageTitle>

<h1>@Localizer.CounterTitle</h1>

<p role="status">@Localizer.CurrentCount(currentCount)</p>

<button class="btn btn-primary" @onclick="IncrementCount">
@Localizer.ClickMe
</button>

@code {
private int currentCount = 0;

private void IncrementCount()
{
currentCount++;
}
}

Last word

Photo by Markus Spiske on Unsplash

Localization is a critical aspect of web development, ensuring that your applications are accessible and user-friendly across different languages and regions. By adopting code-first JSON localization, you’ll take a significant step towards creating more efficient and maintainable multilingual applications.

Remember, the techniques discussed in this article are not limited to Blazor web apps; they can be applied to various .NET projects. Whether you’re a seasoned developer or just starting, mastering localization techniques can greatly enhance the quality and reach of your applications.

If you have any questions or feedback, don’t hesitate to reach out. And for those eager to dive deeper, you can find the example code sources in this repository, ready for exploration and experimentation.

Thank you for reading, and happy coding!

--

--