Language is a core component of the human condition. According to the Washington Post, at least 50% of the world’s population is bilingual. That’s a fantastic statistic, that means every second user to our application could be bilingual. Sadly, most app implementations do not support multiple languages and could be missing serving an audience.

In this post, we’ll be covering the quick steps necessary to localize an ASP.NET Core application to target multiple cultures.

Set Up RequestLocalizationMiddleware

Like most new cross-cutting functionality in ASP.NET Core, localization is a feature implemented using middleware. And like most middleware implementations, there are two essential steps we need to take to enable this feature: configuring the service, and then adding it to our ASP.NET Core pipeline.

Let’s first look at what we need to register the configuration for request localization.

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // 1.
    services
        .AddRazorPages()
        .AddViewLocalization();

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

    // 3.
services.Configure<RequestLocalizationOptions>(options =>
{
    options.SetDefaultCulture("en-Us");
    var cultures = new[] {"en-US", "de-DE", "ja-JP"};
    options.AddSupportedCultures(cultures);
    options.AddSupportedUICultures(cultures);
    options.FallBackToParentUICultures = true;
});
}

The first step is to add the services required by the middleware. In our case, we’ll be localizing Razor Pages. To do that, we need to make a call to AddViewLocalization. This call registers a few services we’ll be using later in our view. Here is the internal implementation of the method.

services.TryAdd(ServiceDescriptor.Singleton<IHtmlLocalizerFactory, HtmlLocalizerFactory>());
services.TryAdd(ServiceDescriptor.Transient(typeof(IHtmlLocalizer<>), typeof(HtmlLocalizer<>)));
services.TryAdd(ServiceDescriptor.Transient<IViewLocalizer, ViewLocalizer>());

The second step tells the localization middleware where to start looking for resx files. In our case, we’ll be creating a folder named Resources under our project structure.

The localization services work by using the namespace of a class to determine the resource file location. We’ll get more into this later when we start localizing values in our views. For now, here is a screenshot showing the layout of the project.

localization project layout

The third and final step in ConfigureServices is configuring our options and the supported cultures. In our case, we’ll be support German, English, and Japanese. An important note about the difference between CurrentCulture and CurrentCultureUI. CurrentCulture is utilized to localize system types like dates and currency, while the user interface uses ‘CurrentCultureUI`, which in our case is the Razor view engine.

In the Configure method, we need to register our middleware somewhere in our pipeline before our call to UseEndpoints. The registration of this middleware is dependant on the applications other middleware, which means developers will need to adjust according to their apps.

If we need to access the request’s culture in our middleware implementations, we’ll need to register this middleware before our custom middleware.

For clarity, I’ve included the full Configure method call below.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    // add request localization middleware
    app.UseRequestLocalization();
    
    app.UseRouting();
    app.UseAuthorization();

    app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); });
}

Resources Search Strategy

The IStringLocalizer<T> interface is a fascinating interface. It uses the namespace of type T to determine the name of a resource file.

If our application has a type WebApplication.Pages.IndexModel, then a request for IStringLocalizer<WebApplication.Pages.IndexModel> will look for a resource instance under the folder prefix we set up in ConfigureServices concatenated with our namespace. In this case, it would look for a resource with the namespace WebApplication.Resources.Pages.IndexModel.

Here is the project structure we saw in the previous section.

localization project layout

The naming approach is merely conventional, and the type of T could be anything. In our sample, we also have a resource for IStringLocalizer<Program>, which will hold all of our global string values.

If we were to use IViewLocalizer, the naming and placement of our resource files would become more critical.

The default implementation of IViewLocalizer finds the resource file based on the view’s file name. Microsoft

In my opinion, the IViewLocalizer implementation is a friendly approach to enforcing conventions but is also limiting as it excludes access to global shared resource files. Using IStringLocalizer and IHtmlLocalizer provide more flexibility.

Using The Localizers

The localizers are services and, like other services, can be accessed using ASP.NET Core’s dependency injection container.

@page
@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@model IndexModel

@inject IStringLocalizer<IndexModel> Localizer
@inject IHtmlLocalizer<IndexModel> HtmlLocalizer

@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">@Localizer["Welcome"]</h1>
    <p>
        @HtmlLocalizer["Learn"]
    </p>
</div>

In this example, we are utilizing two interfaces, IStringLocalizer and IHtmlLocalizer, with each accessing the resource files associated with IndexModel.resx.

A recommendation from reading the documentation is that keys also be the fallback value. Take the following example.

<div>This is awesome!</div>

The Microsoft documentation recommends the following usage of IStringLocalizer.

<div>@Localizer["This is awesome!"]</div>

The localizer implementation will return the key in a LocalizedString type. As developers, we can still develop an app in our native tongue while sewing the seeds for localization.

Switching Cultures

Localization doesn’t work unless our application understands how to switch cultures. The RequestLocalizationMiddleware has three default mechanisms for changing between cultures:

  • QueryStringRequestCultureProvider - looks for a culture= query string value.
  • CookieRequestCultureProvider - looks for a .AspNetCore.Culture with a culture value.
  • AcceptHeadersRequestCultureProvider - looks for a Accept-Language header in incoming HTTP requests.

These mechanisms are configurable using the RequestLocalizationOptions registered in ConfigureServices. We won’t be going into detail on how to configure the providers, but knowing its possible is worth mentioning.

In the next section, you’ll see how we can utilize the QueryStringRequestCultureProvider to switch between cultures.

The Localization Demo

Putting all these pieces together, we should have a functioning, localized web application. Here’s what it looks like:

project demo

For folks who want to play around with this code sample, it is available on a GitHub repository.

Our culture picker is a simple HTML form that submit when ever the value changes.

@model WebApplication.ViewComponents.CulturePickerModel

<div>
    <form method="GET">
        <div class="form-group">
            <label class="sr-only" for="culture-picker">
                Culture
            </label>
            <select
                id="culture-picker"
                class="form-control"
                name="culture"
                onchange="this.parentElement.parentElement.submit();">
                @foreach (var culture in Model.SupportedCultures)
                {
                    <option
                        value="@culture.Name"
                        selected="@(Model.CurrentUICulture.Name == culture.Name)">
                        <span>@Model.ToFlagEmoji(culture.Name)</span> @culture.DisplayName - @culture.NativeName
                    </option>
                }
            </select>
        </div>
    </form>
</div>

We also use a neat Unicode trick to display the culture’s country flag using the later part of the culture name. For example, the culture of ja-JP would yield a country code of JP.

public string ToFlagEmoji(string country)
{
    country = country
        .Split('-')
        .LastOrDefault();

    if (country == null)
        return "⁉️️";
    
    return string.Concat(
        country
        .ToUpper()
        .Select(x => char.ConvertFromUtf32(x + 0x1F1A5))
    );
}

Conclusion

Localization may seem scary, but the ASP.NET team has taken significant care to make it a manageable problem. Tools like JetBrains Rider with its localization manager make editing values quick and efficient. The internet has made it possible to localize our apps with the help of language experts to correct local idioms. When it comes to localization in ASP.NET, once we understand how the pieces fit together, its just a matter of rinse and repeat.

I want to point you to some great resources that helped me learn more about localization.