Localization with language selector in Razor Pages

  • Wednesday, 18 January 2023
  • Darko
  • Tutorial
  • Comments
blog-title

Localization with language selector in Razor Pages

This tutorial will show how to localize ASP.NET Core Razor Pages applications using the built-in middleware and how to implement a Bootstrap language selector with flags.

A multilingual website allows the site to reach a wider audience. ASP.NET Core provides services and middleware for localizing into different languages and cultures. Docs

Inject Localizer to View and Page Model

To make a razor page localizable we inject an IStringLocalizer to our page model and to page view. The documentation only shows how to implement it in a MVC controller, but it is esentially the same for the razor page.

Index.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Localization;

namespace Tutorial.Pages
{
    public class IndexModel : PageModel
    {
        private readonly IStringLocalizer<IndexModel> _localizer;

        public IndexModel(IStringLocalizer<IndexModel> localizer)
        {
            _localizer = localizer;
        }

        public void OnGet()
        {
            ViewData["Title"] = _localizer["Title"];
        }
    }
}

Index.cshtml

@page
@model IndexModel
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer

<div class="text-center">
    <h1 class="display-4">@Localizer["Welcome to tutorial"]</h1>
</div>

Create Resources

Once we have injected the Localizer to our page we need to create a localized resource for the Title and Welcome to tutorial strings.

We will add German and French resource file for the Index page in this tutorial:

Picture1

Picture2

Picture3

The default English language does not need its own resource file, the strings will be both a name and a corresponding value.

More info about resource file naming and location here

Configure Middleware

Next we need to configure and register the Localization middleware in our application Program file.

We add a location for the resources folder:

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

Add view localization to razor pages:

builder.Services.AddRazorPages().AddViewLocalization();

We need to explicitly configure which cultures we are going to support, create a RequestLocalizationOptions object and add it to the request localization middleware.

var supportedCultures = new[] { "en-GB", "de-DE", "fr-FR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

builder.Services.AddRequestLocalization(options => {
    options.DefaultRequestCulture = localizationOptions.DefaultRequestCulture;
    options.SupportedCultures = localizationOptions.SupportedCultures;
    options.SupportedUICultures = localizationOptions.SupportedUICultures;
});

We also need to tell our application to use the request localization middleware:

app.UseRequestLocalization(localizationOptions);

The final Program code from the default template looks like this:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

// Add services to the container.
builder.Services.AddRazorPages().AddViewLocalization();

var supportedCultures = new[] { "en-GB", "de-DE", "fr-FR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

builder.Services.AddRequestLocalization(options => {
    options.DefaultRequestCulture = localizationOptions.DefaultRequestCulture;
    options.SupportedCultures = localizationOptions.SupportedCultures;
    options.SupportedUICultures = localizationOptions.SupportedUICultures;
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseRequestLocalization(localizationOptions);

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

We can test the localization working by passing the query string value to our Index page:

https://localhost:7278/?culture=fr-FR

Language selector with Flags

Lets first install a client side library flag-icon-css from the cdnjs provider, what's great about this library is it follows the ISO 3166 standard as can be seen here, the RegionInfo class also follows the ISO 3166 standard so the flags match the cultures defined in Program file of our application

Picture 4

Once the library is installed reference it in the _Layout page <head> tag

<link rel="stylesheet" href="~/lib/flag-icon-css/css/flag-icons.css" />

Next we will implement a Language selector as a partial view in our _Layout page.

We create a partial page _SelectLanguagePartial.cshtml in the Shared folder

First we inject the Localizer and LocalizationOptions objects

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> localizationOptions

Then we get the current culture and the current region from the HttpContext. The culture region is needed to extract the TwoLetterISORegionName to be used in a CSS class. We also populate the Bootstrap dropdown with supported cultures we defined in the Program file, which we get from LocalizationOptions object as an annonymous type

@{
    var currentCulture = Context.Features
		.Get<IRequestCultureFeature>().RequestCulture.UICulture;
    var currentRegion = new RegionInfo(currentCulture.LCID);
    
    var cultureItems = localizationOptions.Value.SupportedUICultures
        .Select(c => new {	Value = c.Name, 
				            Text = c.Parent.NativeName,
				            TwoLetterISORegionName = new RegionInfo(c.LCID)
					            .TwoLetterISORegionName })
        .ToList();
    var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" :
        $"~{Context.Request.Path.Value}";
}

Next we render the Bootstrap dropdown, the @currentCulture.Parent.NativeName will display only the Language name without the associated Country. The dropdown has a list of buttons from the SupportedCultures object which when submitted invokes the SetLanguage page handler in the Index.cshtml.cs page. The buttons also have route parameters for the culture and returnUrl that are processed in the page handler.

<div>
	<form id="selectLanguage" method="post" class="form-horizontal">
	     <div class="dropdown">
             <a class="btn btn-white border-secondary dropdown-toggle" 
		                id="dropdownMenuLink" data-bs-toggle="dropdown" 
		                aria-haspopup="true" aria-expanded="false">
                    <span class="flag-icon flag-icon-background 
	                    flag-icon-@currentRegion.TwoLetterISORegionName.ToLower()">
                    </span>
                    @currentCulture.Parent.NativeName
             </a>
             <ul class="dropdown-menu">
                    @foreach (var item in cultureItems)
                    {<li>
						<button asp-page="Index" asp-page-handler="SetLanguage"
								asp-route-returnUrl="@returnUrl" 
								asp-route-culture="@item.Value" 
								type="submit" class="dropdown-item">
							<span class="flag-icon flag-icon-background 
								flag-icon-@item.TwoLetterISORegionName.ToLower()">
							</span> @item.Text
                        </button>
                    </li>}
            </ul>
        </div>
	</form>
</div>

SetLanguage handler in Index.cshtml.cs handles the culture and returnUrl route parameters and sets a cookie to store the current selected culture.

 public IActionResult OnPostSetLanguage(string culture, string returnUrl)
 {
     Response.Cookies.Append(
         CookieRequestCultureProvider.DefaultCookieName, 
         CookieRequestCultureProvider.MakeCookieValue(
             new RequestCulture(culture)),
             new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
            
     return LocalRedirect(returnUrl);
 }

The final code for _SelectLanguagePartial.cshtml should look like this:

@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options
@using System.Globalization

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> localizationOptions

@{
    #nullable disable
    var currentCulture = Context.Features
        .Get<IRequestCultureFeature>().RequestCulture.UICulture;
    var currentRegion = new RegionInfo(currentCulture.LCID);
    var cultureItems = localizationOptions.Value.SupportedUICultures
        .Select(c => new {	Value = c.Name, 
				            Text = c.Parent.NativeName, 
				            TwoLetterISORegionName = new RegionInfo(c.LCID)
					            .TwoLetterISORegionName })
        .ToList();
    var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : 
        $"~{Context.Request.Path.Value}";
}

<div>
    <form id="selectLanguage" method="post" class="form-horizontal">
        <div class="dropdown">
            <a class="btn btn-white border-secondary dropdown-toggle" 
	                id="dropdownMenuLink" data-bs-toggle="dropdown" 
	                aria-haspopup="true" aria-expanded="false">
                <span class="flag-icon flag-icon-background 
                    flag-icon-@currentRegion.TwoLetterISORegionName.ToLower()">
                </span>
                @currentCulture.Parent.NativeName
            </a>
            <ul class="dropdown-menu">
                @foreach (var item in cultureItems)
                {<li>
					<button asp-page="Index" asp-page-handler="SetLanguage"
							asp-route-returnUrl="@returnUrl" 
							asp-route-culture="@item.Value" 
							type="submit" class="dropdown-item">
						<span class="flag-icon flag-icon-background 
							flag-icon-@item.TwoLetterISORegionName.ToLower()">
						</span> @item.Text
                    </button>
                </li>}
            </ul>
        </div>
    </form>
</div>

All that is left is to render the partial page in our _Layout.cshtml in the Menu

<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">	            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
		</li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
        </li>
    </ul>
    <partial name="_SelectLanguagePartial" />
</div>

The final result should look like this: Picture 5

And it is easy to add additional languages by just adding to the supported cultures in the Program.cs file.