Sunday, 9 July 2023

Localizing Blazor WASM applications with a language picker

This article presents code how to localize a Blazor WASM app with a language picker. This is part of globalizing an app. The sample app is in this sample app in GitHub: https://github.com/toreaurstadboss/HelloBlazorLocalization

First off, we need to add some Nuget package references, such as adding a capability of using local storage in a convenient way in the Blazor WASM app. The project file of the sample app has this setup :

Project file - HelloBlazorLocalization.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
	<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="Shared\Resources\**" />
    <Content Remove="Shared\Resources\**" />
    <EmbeddedResource Remove="Shared\Resources\**" />
    <None Remove="Shared\Resources\**" />
  </ItemGroup>

  <ItemGroup>
	  <PackageReference Include="Blazored.LocalStorage" Version="4.3.0" />
	  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
	  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
	  <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.1.1" />
	  <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
	  <PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.3" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="wwwroot\flag-icons\" />
  </ItemGroup>

  <ItemGroup>
    <None Include="HelloBlazorLocalization.sln" />
  </ItemGroup>

</Project>



Note the use of the property setting : BlazorWebAssemblyLoadAllGlobalizationData This is required to add localization to your Blazor WASM app ! Also note that we use Blazored.LocalStorage to write and access local storage. Let's look at the Program.cs file next how we set up the app.

Program.cs

using Blazored.LocalStorage;
using HelloBlazorLocalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    string[] supportedCultures = new[] { "no", "en" };
    options
        .AddSupportedCultures(supportedCultures)
        .AddSupportedUICultures(supportedCultures)
        .SetDefaultCulture("no");
});

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

builder.Services.AddBlazoredLocalStorage();

await builder.Services.BuildServiceProvider().SetDefaultCultureAsync();

await builder.Build().RunAsync();


An extension method is added to ServiceProvider to load up selected culture from local storage. It also inspects the query string set, if any, since language picker component presented later on will reload the Blazor WASM app after selecting language.


WebAssemblyHostExtensions.cs

using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.WebUtilities;
using System.Globalization;

namespace HelloBlazorLocalization
{

    public static class WebAssemblyHostExtensions
    {
        public async static Task SetDefaultCultureAsync(this ServiceProvider serviceProvider)
        {
            var navigationManager = serviceProvider.GetService<NavigationManager>(); 
            var uri = navigationManager!.ToAbsoluteUri(navigationManager.Uri);
            var queryStrings = QueryHelpers.ParseQuery(uri.Query);
            var localStorage = serviceProvider.GetRequiredService<ILocalStorageService>();

            if (queryStrings.TryGetValue("culture", out var selectedCulture))
            {
                await localStorage.SetItemAsStringAsync("culture", selectedCulture);
            }

            var cultureString = await localStorage.GetItemAsync<string>("culture");
            CultureInfo cultureInfo;

            if (!string.IsNullOrWhiteSpace(cultureString))
            {
                cultureInfo = new CultureInfo(cultureString);
            }
            else
            {
                cultureInfo = new CultureInfo("en-US");
            }

            CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
            CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
        }
    }

}


Now, let's look at the Index.razor file where we repeat some of the code in the extension method shown above.


Index.razor

@page "/"
@using System.Globalization;

@inject NavigationManager NavigationManager
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage
@inject IStringLocalizer<SharedResources> Localizer

<PageTitle>@Localizer["Home"]</PageTitle>

<h1>@Localizer["Home"]</h1>

@Localizer["HomeDescription"]

<SurveyPrompt Title="How is Blazor working for you?" />


@code {

    protected override async Task OnParametersSetAsync()
    {
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
        var queryStrings = QueryHelpers.ParseQuery(uri.Query);
        if (queryStrings.TryGetValue("culture", out var selectedCulture))
        {
            await LocalStorage.SetItemAsStringAsync("culture", selectedCulture);

        }
        else
        {
            selectedCulture = await LocalStorage.GetItemAsStringAsync("culture");
        }
        if (!string.IsNullOrWhiteSpace(selectedCulture))
        {
            var cultureInfo = new CultureInfo(selectedCulture);
            CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
            CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;            
        }

    }

}

To localize strings, we first do an inject of the IStringLocalizer as shown in the razor file. We also set the resource key when we fetch the localized text (Value). This is set up in the SharedResource files. This is done in the sample app in three files.
  • An empty class called SharedResources at the root level
  • Two resources files (.resx) called SharedResources.en.resx and SharedResources.no.resx
You can have multiple resource file in Blazor WASM. Note that we in Program.cs set up the ResourcesPath to the sub folder Resources, where we put the .resx files. See the sample app for details (clone the Github repo). Next up, let's look at the LanguagePicker.razor file that will show a language picker. The sample app got flag icons for all flags of countries so check out the folder flag-icons under wwwroot folder in the sample app.


LanguagePicker.razor

@using Microsoft.AspNetCore.Localization
@using Microsoft.Extensions.Options
@using System.Globalization
@inject IOptions<RequestLocalizationOptions> LocalizationOptions
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage

<div class="mt-3 mb-3 mx-5">

    @foreach (var culture in LocalizationOptions.Value.SupportedCultures)
    { 
        
        <a style="cursor:pointer" onclick="location.href = '/?culture=@culture.ToString()';" class="text-decoration-none">
            <img style="width:20px" src="flag-icons/@(culture.Name).png" alt="@culture.Name" />
            <span class="badge rounded-pill mx-1 border border-primary 
            @((culture.ToString() == CultureInfo.CurrentCulture.ToString() || culture.ToString() == _selectedCulture) ?
                "btn btn-success" : "btn btn-info text-dark")">@culture</span>
        </a>  <br />
    }
</div>

@code {
    private string? _selectedCulture;
    protected override async Task OnParametersSetAsync()
    {
        _selectedCulture = await LocalStorage.GetItemAsStringAsync("culture");
    }
}


Note that Blazor WASM app should refresh entirely after choosing another language. Also note that you should set up multiple languages in your browser to get the expected results. You should have the supported languages set up in Blazor WASM, however it might still work to get the localization done if the language settings are not set up to include the specified languages. But if you do not see the expected results, check the language settings in your browser. And as can be seen, we use local storage to persist our selected language. The selected language is displayed with the green button to indicate selected. When the Blazor WASM reloads, the selected language is fetched from local storage. This can be seen in Application => Local Storage in F12 Developer Tools in Chrome for example, when running the app. Blazor WASM supports a reduced set of localization functionality, compared to Blazor server side apps.
A limited set of ASP.NET Core's localization features are supported:

✔️Supported: IStringLocalizer and IStringLocalizer are supported in Blazor apps.

❌Not supported: IHtmlLocalizer, IViewLocalizer, and Data Annotations localization are ASP.NET Core MVC features and not supported in Blazor apps.

No comments:

Post a Comment