JSON localization in Blazor can be production-ready when JSON is kept behind IStringLocalizer<T>, loaded as immutable application data, cached per culture, and combined with explicit fallback and safe culture selection. The implementation below targets .NET 10 while preserving the working .NET 7 tutorial and demo as a clearly marked historical section.
The short answer: use one JSON file per resource and culture, register a custom IStringLocalizerFactory, let ASP.NET Core own CurrentUICulture, and reload after writing a validated culture cookie. Do not write translation keys from live requests.
Table of Contents
Why build on IStringLocalizer?
The production boundary should be the standard IStringLocalizer<T> abstraction, even when translations live in JSON. Components stay idiomatic, unit tests can substitute a localizer, and the storage format remains an infrastructure decision instead of leaking through the UI.
Use a custom JSON provider only when JSON is a real workflow requirement—for example, translators already deliver JSON or another service exports it. If that constraint does not exist, the built-in RESX provider has less code and less operational risk.
- Keep resource files read-only at runtime.
- Cache parsed dictionaries by resource and culture.
- Apply deterministic culture fallback.
- Return the key when a translation is missing and observe the miss.
- Keep culture selection within an allowlist.
Define the JSON resource contract
Store one flat object per resource and culture. Stable dotted keys are easier to compare in CI than nested objects, and they avoid binding the resource format to a component hierarchy.
{
"Employee.Title": "Employees",
"Employee.FirstName": "First name",
"Employee.Save": "Save"
}
{
"Employee.Title": "الموظفون",
"Employee.FirstName": "الاسم الأول",
"Employee.Save": "حفظ"
}
Name these files AppTexts.en-US.json and AppTexts.ar-SA.json under Resources/Localization. Treat the default-culture file as the source-of-truth key set; missing keys in other cultures should fail a build, not appear for the first time in production.
Configure JSON localization options
Keep file location, default culture, and cache lifetime in validated options. This prevents path and fallback policy from being scattered across the provider.
public sealed class JsonLocalizationOptions
{
public string ResourcesPath { get; init; } = "Resources/Localization";
public string DefaultCulture { get; init; } = "en-US";
public TimeSpan CacheDuration { get; init; } = TimeSpan.FromMinutes(30);
}
public sealed record JsonResource(
IReadOnlyDictionary<string, string> Values,
string ResourceName,
CultureInfo Culture);
Load and cache resources with culture fallback
The store below loads each exact file once per cache window and merges cultures from most specific to least specific. For sv-SE, the lookup order is sv-SE → sv → en-US. TryAdd preserves the first, most-specific value.
public sealed class JsonResourceStore(
IMemoryCache cache,
IWebHostEnvironment environment,
IOptions<JsonLocalizationOptions> options)
{
private readonly JsonLocalizationOptions _options = options.Value;
public IReadOnlyDictionary<string, string> Get(
string resourceName,
CultureInfo requestedCulture)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var culture in GetFallbackChain(requestedCulture))
{
foreach (var pair in LoadExact(resourceName, culture))
{
result.TryAdd(pair.Key, pair.Value);
}
}
return result;
}
private IReadOnlyDictionary<string, string> LoadExact(
string resourceName,
CultureInfo culture)
{
var cacheKey = $"json-localizer:{resourceName}:{culture.Name}";
return cache.GetOrCreate(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = _options.CacheDuration;
var fileName = $"{resourceName}.{culture.Name}.json";
var path = Path.Combine(
environment.ContentRootPath,
_options.ResourcesPath,
fileName);
if (!File.Exists(path))
{
return new Dictionary<string, string>();
}
using var stream = File.OpenRead(path);
return JsonSerializer.Deserialize<Dictionary<string, string>>(stream)
?? new Dictionary<string, string>();
})!;
}
private IEnumerable<CultureInfo> GetFallbackChain(CultureInfo requested)
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var current = requested;
current != CultureInfo.InvariantCulture;
current = current.Parent)
{
if (seen.Add(current.Name))
{
yield return current;
}
}
var fallback = CultureInfo.GetCultureInfo(_options.DefaultCulture);
if (seen.Add(fallback.Name))
{
yield return fallback;
}
}
}
In production, deploy resources with the application or from an immutable configuration source. Do not let a web request modify JSON files: concurrent instances, read-only containers, and rolling deployments make runtime writes unreliable.
Implement IStringLocalizer and its factory
The localizer reads CultureInfo.CurrentUICulture for each request or Blazor circuit and reports a missing resource when it must return the key. Formatting uses the active UI culture.
public sealed class JsonStringLocalizer(
string resourceName,
JsonResourceStore store) : IStringLocalizer
{
public LocalizedString this[string name]
{
get
{
var values = store.Get(resourceName, CultureInfo.CurrentUICulture);
var found = values.TryGetValue(name, out var value);
return new LocalizedString(
name,
found ? value! : name,
resourceNotFound: !found);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var value = this[name];
return new LocalizedString(
name,
string.Format(CultureInfo.CurrentUICulture, value.Value, arguments),
value.ResourceNotFound);
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) =>
store.Get(resourceName, CultureInfo.CurrentUICulture)
.Select(pair => new LocalizedString(pair.Key, pair.Value));
}
public sealed class JsonStringLocalizerFactory(
JsonResourceStore store) : IStringLocalizerFactory
{
public IStringLocalizer Create(Type resourceSource) =>
new JsonStringLocalizer(resourceSource.Name, store);
public IStringLocalizer Create(string baseName, string location) =>
new JsonStringLocalizer(baseName.Split('.').Last(), store);
}
// Marker type used by IStringLocalizer<AppTexts>.
public sealed class AppTexts
{
}
Register localization and supported cultures
Register the custom factory once, then configure the same culture allowlist for request localization and the language picker.
builder.Services.AddLocalization();
builder.Services.AddMemoryCache();
builder.Services.Configure<JsonLocalizationOptions>(options =>
{
options.ResourcesPath = "Resources/Localization";
options.DefaultCulture = "en-US";
options.CacheDuration = TimeSpan.FromMinutes(30);
});
builder.Services.AddSingleton<JsonResourceStore>();
builder.Services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
var supportedCultures = new[] { "en-US", "sv-SE", "ar-SA" };
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.SetDefaultCulture("en-US")
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
});
var app = builder.Build();
app.UseRequestLocalization();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
Place UseRequestLocalization before endpoint mapping. For server-side interactive components, changing culture should normally reload the page so the next request and circuit start with the selected culture.
Add a safe culture switcher
Never copy an arbitrary query-string value into the culture cookie or redirect target. Validate the culture against the configured allowlist and use Results.LocalRedirect to prevent open redirects.
var allowedCultures = new HashSet<string>(
supportedCultures,
StringComparer.OrdinalIgnoreCase);
app.MapGet("/culture/set", (
string culture,
string? returnUrl,
HttpContext context) =>
{
if (!allowedCultures.Contains(culture))
{
return Results.BadRequest("Unsupported culture.");
}
context.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture)),
new CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
IsEssential = true,
SameSite = SameSiteMode.Lax,
Secure = true
});
return Results.LocalRedirect(
string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl);
});
@inject NavigationManager Navigation
<select value="@CultureInfo.CurrentUICulture.Name"
@onchange="ChangeCulture">
<option value="en-US">English</option>
<option value="sv-SE">Svenska</option>
<option value="ar-SA">العربية</option>
</select>
@code {
private void ChangeCulture(ChangeEventArgs args)
{
var culture = Uri.EscapeDataString(args.Value?.ToString() ?? "en-US");
var returnUrl = Uri.EscapeDataString(
Navigation.ToBaseRelativePath(Navigation.Uri));
Navigation.NavigateTo(
$"/culture/set?culture={culture}&returnUrl=/{returnUrl}",
forceLoad: true);
}
}
The redirect is deliberate: it creates a new request with the cookie applied. Pure client-side culture mutation can leave server-rendered markup, validation messages, and the active circuit on different cultures.
Use localized strings in Blazor components
Set both language and direction on the document. RTL is a layout concern, not only a translation concern.
@using System.Globalization
<html lang="@CultureInfo.CurrentUICulture.Name"
dir="@(CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? "rtl" : "ltr")">
<head>
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
@page "/employees"
@inject IStringLocalizer<AppTexts> Texts
<PageTitle>@Texts["Employee.Title"]</PageTitle>
<h1>@Texts["Employee.Title"]</h1>
<label for="firstName">@Texts["Employee.FirstName"]</label>
<InputText id="firstName" @bind-Value="Model.FirstName" />
<button type="submit">@Texts["Employee.Save"]</button>
This API also keeps reusable UI independent of JSON. The same component can later move back to RESX—or use a test localizer—without changing its markup. The approach fits components such as the reusable Blazor DataGrid and the Blazor confirm dialog.
Production checks and trade-offs
Localization failures are usually data-quality or deployment failures, so enforce the resource contract before release and make missing-key telemetry actionable.
public sealed class TranslationParityTests
{
[Fact]
public void Every_culture_has_the_default_keys()
{
var root = Path.Combine("Resources", "Localization");
var baseline = ReadKeys(Path.Combine(root, "AppTexts.en-US.json"));
foreach (var file in Directory.EnumerateFiles(root, "AppTexts.*.json"))
{
var actual = ReadKeys(file);
Assert.True(
baseline.SetEquals(actual),
$"{Path.GetFileName(file)} has missing or extra keys.");
}
}
private static HashSet<string> ReadKeys(string path) =>
JsonSerializer.Deserialize<Dictionary<string, string>>(
File.ReadAllText(path))!.Keys.ToHashSet(StringComparer.Ordinal);
}
- Validate every JSON file during CI and fail on duplicate, missing, or empty values.
- Log missing keys with resource name and culture, but rate-limit repeated events.
- Decide whether cache invalidation happens only on deployment or through a controlled change token.
- Test pluralization, date/number formatting, validation messages, and RTL layouts separately.
- Do not inject HTML from translations; localize text and keep markup in components.
- For multiple application instances, publish identical immutable resource files to every node.
The examples above target the .NET 10 Blazor hosting model and were reviewed against current Microsoft documentation. This environment did not contain a .NET SDK, so they received static review rather than an executed build. The original .NET 7 demo below was executed successfully when published, and its video preserves that result.
Historical 2023 .NET 7 implementation
Historical note: The following section preserves the original .NET 7 tutorial, all 19 code samples, eight screenshots, the working video, and the original GitHub repository. It demonstrates the successful 2023 implementation, but its runtime file writes, custom cascading service pattern, Newtonsoft.Json dependency, and local-storage culture state should not be copied into a new production application without redesign.
Localization is a process of translating the labels of elements from UI to another language. In this article, we will create a custom json-based localization service for the Blazor server application that can be used in different components, e.g. in the Reusable DataGrid Blazor created in the previous post.
Take a look at the final JSON-based localization service for the Blazor server application project:
Creating a Localization Solution and adding a class library project
1. Open Visual Studio 2022 and click the Create a new project button.
2. Select the Class library project and click the Next button.
3. Enter CS.Services.Localizer in the Project name textbox and Localization in the Solution name and click the Next button.
4. Select .NET 7.0 as the version of the Framework to use and click the Create button.

5. Install the following NuGet packages :
Microsoft.Extensions.HostingNewtonsoft.JsonBlazored.LocalStorage
Creating a custom Localization service
1. Create a new project folder called Objects, and add a class file called Translation that contains the translation in different languages for a given key.
namespace CS.Services.Localizer.Objects { public class Translation { public string English { get; set; } public string Russian { get; set; } public string Arabic { get; set; } } }
2. Create a new project folder named Interfaces, and add an interface file named ILocalizerStorageService
namespace CS.Services.Localizer.Interfaces { public interface ILocalizerStorageService { Task<IDictionary<string, string>> GetLocalizedDictionary(string language); string GetLocalizedString(IDictionary<string, string> localizedDictionary, string key); } }
The LocalizerStorageService is responsible for creating the JSON file if it does not exist and returns the localized dictionary and the translated string according to the selected language.
3. Add a class file named LocalizerStorageService , that implements the ILocalizerStorageService interface.
namespace CS.Services.Localizer { public class LocalizerStorageService : ILocalizerStorageService { private readonly IHostEnvironment hostEnvironment; private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); public LocalizerStorageService(IHostEnvironment hostEnvironment) { this.hostEnvironment = hostEnvironment; } public Task<IDictionary<string, string>> GetLocalizedDictionary(string language) { var storage = GetOrCreateStorage(); return Task.FromResult((IDictionary<string, string>)storage. SelectMany(v => v). ToDictionary( kvp => kvp.Key, kvp => JObject.Parse(JsonConvert.SerializeObject(kvp.Value))[language].ToString())); } public string GetLocalizedString(IDictionary<string, string> localizedDictionary, string key) { if (key == null) { return string.Empty; } else if (localizedDictionary.ContainsKey(key)) { if (localizedDictionary.TryGetValue(key, out string value) && !string.IsNullOrEmpty(value)) { return value; } else { return "{" + key + "}"; } } else { InsertTranslation(key).ConfigureAwait(false); return "{" + key + "}"; } } private async Task InsertTranslation(string key) { if (hostEnvironment.IsDevelopment()) { try { await semaphore.WaitAsync(); var list = GetOrCreateStorage().ToList(); var test = list.SelectMany(v => v); if (!list.SelectMany(v => v).Any(kvp => string.Compare(kvp.Key, key, true) == 0)) { list.Add(new Dictionary<string, Translation> { { key, new Translation() } }); var path = Directory.GetCurrentDirectory(); var fileName = $"{path}/translations.json"; using StreamWriter writer = File.CreateText(fileName); var json = JsonConvert.SerializeObject(list, Formatting.Indented); writer.Write(json); } } finally { semaphore.Release(); } } } private IEnumerable<IDictionary<string, Translation>> GetOrCreateStorage() { var result = new List<IDictionary<string, Translation>>(); var path = Directory.GetCurrentDirectory(); var fileName = $"{path}/translations.json"; if (!File.Exists(fileName)) { if (hostEnvironment.IsDevelopment()) { using var writer = File.CreateText(fileName); writer.Write(JsonConvert.SerializeObject(new List<IDictionary<string, Translation>>(), Formatting.Indented)); } else { throw new NotSupportedException("You can not create json file in Development Environment"); } } using (var reader = File.OpenText(fileName)) { var json = reader.ReadToEnd(); result = JsonConvert.DeserializeObject<List<IDictionary<string, Translation>>>(json); } return result ?? new List<IDictionary<string, Translation>>(); } } }
Line 14: GetLocalizedDictionary method returns the localized dictionary IDictionary<string, string> based on the language parameter,
Line 16: GetOrCreateStorage either returns the JSON file or creates it if it does not exist
Line 26: GetLocalizedString returns the localized string or inserts the new key if it does not exist.
Line 45: InsertTranslation writes the new key to the JSON file if it does not exist.
4. Add a class file called Preference to the Objects folder.
namespace CS.Services.Localizer.Objects { public class Preference { public string Language { get; set; } } }
5. Add an interface file named ILocalizerServiceto Interfaces folder
namespace CS.Services.Localizer.Interfaces { public interface ILocalizerService { Task<Preference> GetOrSetPreferences(); string GetLocalizedString(string key); Task<string> ChangeLanguage(string language); Preference Preference { get; } } }
The LocalizerService is responsible for loading or saving Preferenceto browser localStorage and returns the localized string based on the data stored in localStorage.
6. Add a class file named LocalizerService , that implements the ILocalizerService interface.
namespace CS.Services.Localizer { public class LocalizerService : ILocalizerService { private readonly ILocalizerStorageService storageService; private readonly ILocalStorageService localStorageService; private Preference preference; private IDictionary<string, string> translations; public LocalizerService(ILocalizerStorageService storageService, ILocalStorageService localStorageService) { this.storageService = storageService; this.localStorageService = localStorageService; } public Preference Preference => preference; public string GetLocalizedString(string key) { return storageService.GetLocalizedString(translations, key); } public async Task<Preference> GetOrSetPreferences() { var pref = await localStorageService.GetItemAsync<Preference>("preference"); preference = pref ?? new Preference() { Language = "English" }; translations = await storageService.GetLocalizedDictionary(preference.Language); await SavePreference(); return preference; } public async Task<string> ChangeLanguage(string language) { preference.Language = language; translations = await storageService.GetLocalizedDictionary(preference.Language); await SavePreference(); return preference.Language; } // Helper methods private async Task SavePreference() { await localStorageService.SetItemAsync("preference", preference); } } }
Line 23: GetOrSetPreferences method gets the preferred language stored in LocalStorage or sets it to English as the default value and set the localized dictionary translations local variable.
Line 18: GetLocalizedString returns a localized string.
Line 36: ChangeLanguage is responsible to change the language.
The project should look like below:

Creating a Razor class library
1. Right-click the solution and select the Add, New Project option from the menu.
2. Select the Razor Class Library project template and click the Next button.
3. Name the project CS.Localizer.Razor and click the Next button.
4. Select .NET 7.0 as the version of the Framework and click the Create button.
5. Delete the files created by default.
6. Right-click the CS.Localizer.Razorproject and select the Add, Project Reference option from
the menu and check the CS.Services.Localizercheckbox and click the OK button.

Creating the Localizer components
In this section we will create three components CSStateProviderComponent , CSLocalizerComponent, CSSwitcherComponent.
CSStateProviderComponent
Many components rely on LocalStorage of the browser and during prerendering it’s not possible to interact with LocalStorage.
CSStateProviderComponent renders its child content only after loading is complete so other components can easily work with the stored data .
To make the data accessible to all components of the application, we need to wrap it around the Router component.
1. Right-click the CS.Localizer.Razor project and add a new folder named StateProvider.
2. Right-click the StateProvider folder and add a class that derives from ComponentBase named CSStateProviderComponent
namespace CS.Localizer.Razor.StateProvider { public class CSStateProviderComponent: ComponentBase { [Inject] ILocalizerService LocalizerService { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } public bool HasLoaded { get; set; } public string CurrentLanguage => LocalizerService.Preference.Language; public async Task ChangeLanguage(string language) { await LocalizerService.ChangeLanguage(language); StateHasChanged(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await LocalizerService.GetOrSetPreferences(); HasLoaded = true; StateHasChanged(); } } } }
OnAfterRenderAsync is executed once the application is loaded and rendered in the browser. At this point we can check if this is the firstRender, and if so, call the GetOrSetPreferences method and then explicitly tell Blazor to render again by calling StateHasChanged.
3. Right-click the StateProvider folder and add a Razor component file named CSStateProvider
@inherits CSStateProviderComponent @if (HasLoaded) { <CascadingValue Value="@this"> @ChildContent </CascadingValue> } else { <p>Loading...</p> }
Also read https://dotnetcoder.com/creating-a-blazor-confirm-dialog-component/
CSLocalizerComponent
CSLocalizerComponentis responsible for retrieving the localized string based on the key parameter.
1. Right-click theCS.Localizer.Razor project and add a new folder named Localizer.
2. Right-click the Localizer folder and add a class that derives from ComponentBase named CSLocalizerComponent.
namespace CS.Localizer.Razor.Localizer { public class CSLocalizerComponent:ComponentBase { [Inject] protected ILocalizerService localizerService { get; set; } [CascadingParameter] CSStateProvider cSStateProvider { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public string Key { get; set; } } }
3. Right-click the Localizer folder and add a Razor component file named CSLocalizer.
@inherits CSLocalizerComponent @(LocalizerService.GetLocalizedString(Key))
CSSwitcherComponent
CSSwitcherComponent is responsible for switching the language and setting the dir attribute when the Arabic language is selected.
1. Right-click theCS.Localizer.Razorproject and add a new folder named Switcher.
2. Right-click the Switcher folder and add a class that derives from ComponentBasenamedCSSwitcherComponent.
namespace CS.Localizer.Razor.Switcher { public class CSSwitcherComponent:ComponentBase { [CascadingParameter] CSStateProviderComponent CSStatePtovider { get; set; } [Inject] public IJSRuntime JSRuntime { get; set; } protected string CurrentLanguage { get; set; } protected Dictionary<string, string> languages = new Dictionary<string, string> { {"English", "English" }, {"Russian", "Русский" }, {"Arabic", "العربية" } }; protected override async Task OnInitializedAsync() { CurrentLanguage = CSStatePtovider.CurrentLanguage; await ChangeDirection(CurrentLanguage); } public async Task OnLanguageChange(ChangeEventArgs e) { await CSStatePtovider.ChangeLanguage(e.Value.ToString()); await ChangeDirection(e.Value.ToString()); } private async Task ChangeDirection(string lang) { if (lang == "Arabic") { await JSRuntime.InvokeVoidAsync("document.body.setAttribute", "dir", "rtl"); } else { await JSRuntime.InvokeVoidAsync("document.body.setAttribute", "dir", "ltr"); } } } }
3. Right-click the Localizer folder and add a Razor component file named CSSwitcher.
@inherits CSSwitcherComponent <Select Value="@CurrentLanguage" @onchange="OnLanguageChange"> @foreach (var kvp in languages) { <option value="@kvp.Key" selected="@(kvp.Key == CurrentLanguage)"> @kvp.Value </option> } </Select>
Creating Blazor Server App Empty
1. Right-click the Localization solution and select the Add, New Project option from the menu.
2. Select the Blazor Server App Empty project template and click the Next button.
3. Name the project CS.Localizer.WebApp and click the Next button.
4. Select .NET 7.0 as the version of the Framework and click the Create button.
6. Right-click the CS.Localizer.WebAppproject and select the Add, Project Reference option from
the menu and check the CS.Localizer.Razorcheckbox and click the OK button.
7. Right-click the wwwroot/css folder and select Add, Client-Side Library from
the menu, type bootstrap in the Library search textbox and click the Install button.
8. Update _Host.cshtml file
<link href="~/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
Using the custom Localizer in our code
1. Register your custom Localizer services in the Program class in CS.Localizer.WebApp by adding the following services to the DI Container
builder.Services.AddBlazoredLocalStorage(); builder.Services.AddScoped<ILocalizerStorageService, LocalizerStorageService>(); builder.Services.AddScoped<ILocalizerService, LocalizerService>();
2. Add the following using statements to the _Import.razor file.
@using CS.Localizer.Razor.StateProvider @using CS.Localizer.Razor.Localizer @using CS.Localizer.Razor.Switcher
3. WrapCSStateProvidercomponentt around theRouter component to make the state accessible to all components in our application.
<CSStateProvider> <Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> <NotFound> <PageTitle>Not found</PageTitle> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CSStateProvider>
4.Update the Index.razor component by adding the following code to it.
@page "/" <PageTitle>Index</PageTitle> <h4><CSLocalizer Key="employee.add-employee"></CSLocalizer></h4> <label><CSLocalizer Key="employee.first-name"></CSLocalizer></label> <input type="email"> <label><CSLocalizer Key="employee.last-name"></CSLocalizer></label> <input type="email"> <label><CSLocalizer Key="employee.job"></CSLocalizer></label> <input type="email"> <label><CSLocalizer Key="employee.salary"></CSLocalizer></label> <input type="email"> <label><CSLocalizer Key="employee.hire-date"></CSLocalizer></label> <input type="email"> <button type="submit" style="width:150px;"> <CSLocalizer Key="employee.save"></CSLocalizer> </button> <button type="submit" style="width:150px;"> <CSLocalizer Key="employee.cancel"></CSLocalizer> </button> <h4><CSLocalizer Key="employee.employees"></CSLocalizer></h4> <table> <thead> <tr> <th><CSLocalizer Key="employee.first-name"></CSLocalizer></th> <th><CSLocalizer Key="employee.last-name"></CSLocalizer></th> <th><CSLocalizer Key="employee.job"></CSLocalizer></th> <th><CSLocalizer Key="employee.salary"></CSLocalizer></th> <th>#</th> <th>#</th> </tr> </thead> <tbody> @foreach(var employee in employees) { <tr> <td>@employee.FirstName</td> <td>@employee.LastName</td> <td>@employee.Job</td> <td>@employee.Salary</td> <td><button style="width:150px;"><CSLocalizer Key="employee.update"></CSLocalizer></button></td> <td><button style="width:150px;"><CSLocalizer Key="employee.delete"></CSLocalizer></button></td> </tr> } </tbody> </table> @code{ List<Employee> employees = new List<Employee> { new Employee{FirstName= "John", LastName= "Bond", Job = "ANALYST", Salary= "2000"}, new Employee{FirstName= "Kimberly", LastName= "McLean", Job = "CLERK", Salary= "2500"}, new Employee{FirstName= "Kevin", LastName= "Pullman", Job = "MANAGER", Salary= "3000"}, new Employee{FirstName= "Amy", LastName= "Skinner", Job = "MANAGER", Salary= "3500"}, new Employee{FirstName= "Katherine", LastName= "Peters", Job = "PRESIDENT", Salary= "5100"} }; class Employee { public string FirstName{ get; set; } public string LastName{ get; set; } public string Job { get; set; } public string Salary{ get; set; } } }
5. Update the MainLayout.razor file by adding the CSSwitcher component
<main> <nav dir="ltr"> <a> <CSLocalizer Key="internationalization"></CSLocalizer> </a> <CSSwitcher></CSSwitcher> </nav> <article> @Body </article> </main>
5. Run the application and observe the output.

Note that the translations.json file was created when you ran the application.

Whenever you use CSLocalizer in your code and run the application in the development environment, a new object is added to the translations.json file.
6. Add your translations manually.
[ { "internationalization": { "English": "Internationalization", "Russian": "Интернационализация", "Arabic": "التدويل" } }, { "employee.first-name": { "English": "First name", "Russian": "Имя", "Arabic": "الاسم الأول" } }, { "employee.last-name": { "English": "Last name", "Russian": "Фамилия", "Arabic": "الكنية" } }, { "employee.job": { "English": "Job", "Russian": "Профессия", "Arabic": "المهنة" } }, { "employee.salary": { "English": "Salary", "Russian": "Заработная плата", "Arabic": "المرتب" } }, { "employee.hire-date": { "English": "Hire date", "Russian": "Дата приема на работу", "Arabic": "تاريخ التعيين" } }, { "employee.save": { "English": "Save", "Russian": "Сохранять", "Arabic": "حفظ" } }, { "employee.cancel": { "English": "Cancel", "Russian": "Отмена", "Arabic": "إلغاء" } }, { "employee.update": { "English": "Update", "Russian": "Редактировать", "Arabic": "تحرير" } }, { "employee.delete": { "English": "Delete", "Russian": "Удалить", "Arabic": "حذف" } }, { "employee.employees": { "English": "Employees", "Russian": "Сотрудники", "Arabic": "الموظفين" } }, { "employee.add-employee": { "English": "Add Employee", "Russian": "Добавить сотрудника", "Arabic": "الرجاء إضافة موظف" } } ]
7. Run the application again and observe the output.

8. Switch the language to Russian and then to Arabic.

Note that the dir attribute has been changed from ltr to rtl when the selected language is Arabic.

The code for the demo can be found Here
References
- Blazor globalization and localization (.NET 10)
- Globalization and localization in ASP.NET Core
- IStringLocalizer API
- System.Text.Json overview
- Caching in .NET
- Historical internationalization sample on GitHub
Found this useful? Support more practical developer content.