Structured ASP.NET Localization
Jani Giannoudis
Posted on August 17, 2023
In the globally connected IT world, delivering software in different languages is a basic requirement. This article presents a structured approach to localizing ASP.NET applications using the Blazor template application.
Structuring localization provides the following benefits
- Localized text is grouped by topic
- Type safety for localization parameters
- Enumeration localization
- Clear translation templates
From Global to Local
To localize an application, it must be designed to be as culturally neutral as a global application. Global applications are extended by the localization of the respective culture. This separation is achieved by placing the localized texts in resource files of type .resx
.
The following overview shows the localization of the sample application.
Localization is represented in Visual Studio as follows:
- Localizations.csproj - Class library
- LocalizerBase.cs - Base class for all localizers
- Localizer.cs - Strater class for localization
- Resources\Localizations.resx - English (default) localization resources
- Resources\Localizations.de.resx - German localization resources
- GlobalApp.csproj - Blazor server application
- Shared\*.razor - Shared components
- Pages\*.razor - Application pages
Building Localizers
All localized texts are divided thematically into localizer classes, which are derived from the base class LocalizerBase
. The localized texts are accessed through the root class Localizer
:
public class Localizer : LocalizerBase
{
public Localizer(IStringLocalizerFactory factory) :
base(factory, nameof(Localizer))
{
App = new(factory);
Home = new(factory);
Survey = new(factory);
Counter = new(factory);
Forecast = new(factory);
}
public AppLocalizer App { get; }
public HomeLocalizer Home { get; }
public SurveyLocalizer Survey { get; }
public CounterLocalizer Counter { get; }
public ForecastLocalizer Forecast { get; }
}
For each topic, a localizer is implemented to provide access to the translated resources, like the example CounterLocalizer
:
public class CounterLocalizer : LocalizerBase
{
public CounterLocalizer(IStringLocalizerFactory factory) :
base(factory)
{
}
public string Title => Localization();
public string Click => Localization();
public string CurrentCount(int currentCount) =>
ApplyParameter(Localization(),
nameof(currentCount), currentCount);
}
The fixed translation data is determined with the Localization()
method. The translation keys Counter.Title
, Counter.Click
and Counter.CurrentCount
are generated from the class name and the property. Translations with variable parameters generate a formatted text with the parameter name as placeholder Current count: {currentCount}
.
Setup Cultures
The Culture
class manages the translation culture:
- Available languages
- Default language
- Access path to translation resources
- Getting and changing the runtime language
For ASP.NET applications, the structure is not set at the application level (CultureInfo.CurrentCulture
and CultureInfo.CurrentUICulture
), but at the web request thread level (CultureInfo.DefaultThreadCurrentCulture
and CultureInfo.DefaultThreadCurrentUICulture
).
Providing the Localizer
The Localizer
is accessed through dependency injection, which is set up when the application is launched.
public class Program
{
private static void SetupLocalization(IServiceCollection services)
{
services.AddLocalization(o =>
{ o.ResourcesPath = Cultures.ResourcesPath; });
services.AddTransient<Localizer>();
Cultures.ApplyCulture();
}
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
// localizations
SetupLocalization(builder.Services);
var app = builder.Build();
// app setup
// ...
app.Run();
}
}
The SetupLocalization
method performs the following steps
- Register the Microsoft Localization Extensions
- Register the 'Localizer' in the dependency injection
- Set the default language for the current thread
Using the Localizer
The Localizer
is inserted into the page using Inject
and replaces the hard-coded text with a readable property or method access.
@page "/counter"
@using Localization
<PageTitle>Counter</PageTitle>
<h1>@Localizer?.Counter.Title</h1>
<p role="status">@Localizer?.Counter.CurrentCount(currentCount)</p>
<button class="btn btn-primary"
@onclick="IncrementCount">@Localizer?.Counter.Click</button>
@code {
private int currentCount;
[Inject]
private Localizer? Localizer { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
The value @Localizer?.Counter.Title
returns the localized title and @Localizer?.Counter.CurrentCount(currentCount)
provides type safety (int
).
Localizing Enumerations
Another scenario is the translation of enumerations whose localization is stored according to the Enum.<EnumTypeName>.<EnumValue>
convention. The localized enumerations can be queried with Localizer.Enum<T>(T value)
.
@page "/"
@using Localization
<PageTitle>@Localizer?.Home.Title</PageTitle>
<h1>@Localizer?.Home.Header</h1>
@Localizer?.Home.Welcome
<SurveyPrompt Title="@Localizer?.Home.SurveyTitle" />
@Localizer?.Home.EnumLocalization
<InputSelect @bind-Value="CurrentColor">
@foreach (var value in Enum.GetValues(typeof(Color)))
{
<option>@Localizer?.Enum((Color)value)</option>
}
</InputSelect>
@code {
[Inject]
private Localizer? Localizer { get; set; }
private string? CurrentColor { get; set; }
protected override Task OnInitializedAsync()
{
CurrentColor = Localizer?.Enum(Color.Red);
return base.OnInitializedAsync();
}
}
Runtime Language Switching
Switching the language at runtime is done via the drop-down list in the header, which activates the language in the background via the Culture
class:
Resource Translation
The localization project contains the localized texts (Localizations.resx
) in the default language, usually English. When localizing this file into other languages, the following aspects should be considered
- If the translation is for a language or country
Language:
de
Countries:de-US
orde-GB
- All placeholders (
count
in the example above) must be included - All resources must be translated
Tools like ReSharper are helpful in finding broken translation resources.
Source Code
š The source code is available in the GitHub repository Giannoudis/StructuredLocalization.
Posted on August 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.