Instant translation

Instant translation is an SPA friendly translation approach because the website takes effect immediately and the resource only load when it is requested. In this tutorial, you will discover:

  • Required NuGet libraries.
  • How instant translation works?
  • Instant translation with eager loading resource.
  • Instant translation with lazy loading resource.
  • Implement language selection strategies.
  • Use the instant translation.
You can download the example code used in this topic on GitHub.

Required NuGet libraries

Other than the library Microsoft.Extensions.Localization, you will also need Microsoft.AspNetCore.Localization. For eager loading resource, you will need System.Resources.ResourceManager. For lazy load resource, you will need Microsoft.Extensions.Http.


How instant translation works?

In this section, you will be able to answer the questions:

  • How many parts involved?
  • Website initial loading phase.

How many parts involved in instant translation approach?

To implement the instant translation approach, you will need:

  1. Culture provider.
  2. String localizer.
  3. Resource cache (lazy loading resource only).

Culture provider has 3 responsibilities: loading the culture resources, set the first load language and notify language change to all subscribed components.

String localizer is responsible for getting the translated text from the resource/resource cache.

Resource cache is responsible for storing the resource once it is loaded. You will only need this part for lazy loading resource strategy.

Website initial loading phase

In the website initial loading phase, you have 2 strategies to select: eager loading resource and lazy loading resource. When building the project, .NET will convert your .resx files to .dll files and load it to the Assembly. For eager loading resource, those DLL files need to be loaded before you can access it; By default, Blazor WebAssembly only load 1 set of language. Because of that, you will need to load all other resources also for the instant translation to work. For lazy loading resource, there will be no .resx file. Instead, you create the resource files under other formats like .yml, .json and use HttpClient to load only the requested resources. Once the resource has been loaded, it will be cached in the resource cache. When the user switch back to the previous resource, you will not need to load the resources again.


Instant translation with eager loading resource

To implement instant translation with eager loading resource, you need to do the following steps (the list will begin in step 5, all previous steps are described at Enable multiple languages in your website section of the previous tutorial Internalization and localization):

  1. Create the resource files.
  2. Create the culture provider.
  3. Register and config.
  4. Implement a language selection strategy.

Create the resource files

This step is similar to the equivalent step in the Defer Translation approach. We will isolate the resource file from the component for this example.

  • Create a resource folder.
  • Reconstruct the component tree in the resource folder. For example, if your component in the folder Pages, then you need to create a folder Pages. The following image illustrates the folder tree:

create-resource-eager-loading.png

Create the culture provider

  • Create a new class and inject the IJSUnmarshalledRuntime. For example:
public class BlazorSchoolCultureProvider
{
    private readonly IJSUnmarshalledRuntime _invoker;

    public BlazorSchoolCultureProvider(IJSUnmarshalledRuntime invoker)
    {
        _invoker = invoker;
    }
}
  • Add LoadCulturesAsync method to load all the resources.
public class BlazorSchoolCultureProvider
{
    ...
    private const string _getSatelliteAssemblies = "Blazor._internal.getSatelliteAssemblies";
    private const string _readSatelliteAssemblies = "Blazor._internal.readSatelliteAssemblies";
    ...
    public async ValueTask LoadCulturesAsync(params string[] cultureNames)
    {
        var cultures = cultureNames.Select(n => CultureInfo.GetCultureInfo(n));
        var culturesToLoad = cultures.Select(c => c.Name).ToList();
        await _invoker.InvokeUnmarshalled<string[], object?, object?, Task<object>>(_getSatelliteAssemblies, culturesToLoad.ToArray(), null, null);
        object[]? assemblies = _invoker.InvokeUnmarshalled<object?, object?, object?, object[]>(_readSatelliteAssemblies, null, null, null);

        for (int i = 0; i < assemblies.Length; i++)
        {
            using var stream = new MemoryStream((byte[])assemblies[i]);
            AssemblyLoadContext.Default.LoadFromStream(stream);
        }
    }
}
  • Add SetStartupLanguageAsync method to set the startup language. This method will be implemented based on your language selection strategy.
public class BlazorSchoolCultureProvider
{
    ...
    public async Task SetStartupLanguageAsync(string fallbackLanguage)
    {
        throw new NotImplementedException();
    }
}
  • Create methods to subscribe/unsubscribe language change and notify language change. For example:
public class BlazorSchoolCultureProvider
{
    ...
    private readonly List<ComponentBase> _subscribedComponents = new();
    ...
    public void SubscribeLanguageChange(ComponentBase component) => _subscribedComponents.Add(component);

    public void UnsubscribeLanguageChange(ComponentBase component) => _subscribedComponents.Remove(component);

    public void NotifyLanguageChange()
    {
        foreach (var component in _subscribedComponents)
        {
            if (component is not null)
            {
                var stateHasChangedMethod = component.GetType()?.GetMethod("StateHasChanged", 
                    BindingFlags.Instance | BindingFlags.NonPublic);
                stateHasChangedMethod?.Invoke(component, null);
            }
        }
    }
}

Register and config

You need to register IJSUnmarshalledRuntime, BlazorSchoolCultureProvider, set the localization path, load the language resource files and set the startup language in the Program.cs. For example:

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(sp => (IJSUnmarshalledRuntime)sp.GetRequiredService<IJSRuntime>());
builder.Services.AddScoped<BlazorSchoolCultureProvider>();
builder.Services.AddLocalization(options => options.ResourcesPath = "BlazorSchoolResources");

var wasmHost = builder.Build();
var culturesProvider = wasmHost.Services.GetService<BlazorSchoolCultureProvider>();

if (culturesProvider is not null)
{
    await culturesProvider.LoadCulturesAsync("fr", "en");
    await culturesProvider.SetStartupLanguageAsync("fr");
}

await wasmHost.RunAsync();

Instant translation with lazy loading resource

To implement instant translation with eager loading resource, you need to do the following steps (the list will begin in step 5, all previous steps are described at Enable multiple languages in your website section of the previous tutorial Internalization and localization):

  1. Create the resource files.
  2. Create the resource cache.
  3. Create the culture provider.
  4. Create the string localizer.
  5. Register and config.
  6. Implement a language selection strategy.

Create the resource files

You can either create a .yml file or a .json as a resource file. All resource files must be place in /wwwroot. In this example, we will use .json and we will put the resource files at /wwwroot/lang. Keep in mind that you might need some NuGet packages if you want to read those resource files. In case of using .json as the resource file, you will need to install Newtonsoft.Json NuGet library.

You will need to reconstruct the folder component tree in the resource folder. For example:

create-resource-lazy-loading.png

A resource file example content:

{
    "String1": "Blazor School"
}

Create the resource cache

You can cache the resource in local storage, cookie or memory storage. In this example, we are going to use the memory storage.

Create a new class to cache the resource. For example:

public class BlazorSchoolResourceMemoryStorage
{
    public Dictionary<KeyValuePair<string, string>, string> JsonComponentResources { get; set; } = new();
}

Create the culture provider

  • Create a new culture provider class. For example:
public class BlazorSchoolCultureProvider
{
    private readonly List<ComponentBase> _subscribedComponents = new();
    private readonly HttpClient _httpClient;
    private readonly IOptions<LocalizationOptions> _localizationOptions;
    private readonly BlazorSchoolResourceMemoryStorage _blazorSchoolResourceMemoryStorage;

    public BlazorSchoolCultureProvider(IHttpClientFactory httpClientFactory, 
        IOptions<LocalizationOptions> localizationOptions, BlazorSchoolResourceMemoryStorage blazorSchoolResourceMemoryStorage)
    {
        _httpClient = httpClientFactory.CreateClient("InternalHttpClient");
        _localizationOptions = localizationOptions;
        _blazorSchoolResourceMemoryStorage = blazorSchoolResourceMemoryStorage;
    }
}
  • Add a method to load the culture. This method will load the resource file for a component, if the resource file is not found, it will load the fallback resource file. Once the file has been loaded, it stored the resource to the cache. For example:
private async Task<string> LoadCultureAsync(ComponentBase component)
{
    if (string.IsNullOrEmpty(_localizationOptions.Value.ResourcesPath))
    {
        throw new Exception("ResourcePath not set.");
    }

    string componentName = component.GetType().FullName!;

    if (_blazorSchoolResourceMemoryStorage.JsonComponentResources.TryGetValue(new(componentName, 
        CultureInfo.DefaultThreadCurrentCulture!.Name), out string? resultFromMemory))
    {
        return resultFromMemory;
    }

    var message = await _httpClient.GetAsync(ComposeComponentPath(componentName, 
        CultureInfo.DefaultThreadCurrentCulture!.Name));
    string result;

    if (message.IsSuccessStatusCode is false)
    {
        var retryMessage = await _httpClient.GetAsync(ComposeComponentPath(componentName));

        if (retryMessage.IsSuccessStatusCode is false)
        {
            throw new Exception($"Cannot find the fallback resource for {componentName}.");
        }
        else
        {
            result = await message.Content.ReadAsStringAsync();
        }
    }
    else
    {
        result = await message.Content.ReadAsStringAsync();
    }

    _blazorSchoolResourceMemoryStorage.JsonComponentResources[new(componentName, 
        CultureInfo.DefaultThreadCurrentCulture!.Name)] = result;

    return result;
}

private string ComposeComponentPath(string componentTypeName, string language = "")
{
    var nameParts = componentTypeName.Split('.').ToList();
    nameParts.Insert(1, _localizationOptions.Value.ResourcesPath);
    nameParts.RemoveAt(0);
    string componentName = nameParts.Last();
    nameParts[^1] = string.IsNullOrEmpty(language) ? $"{componentName}.json" : $"{componentName}.{language}.json";
    string resourceLocaltion = string.Join("/", nameParts);

    return resourceLocaltion;
}
  • Add SetStartupLanguageAsync method to set the startup language. This method will be implemented based on your language selection strategy.
public class BlazorSchoolCultureProvider
{
    ...
    public async Task SetStartupLanguageAsync(string fallbackLanguage)
    {
        throw new NotImplementedException();
    }
}
  • Create methods to subscribe/unsubscribe language change and notify language change. For example:
public async Task SubscribeLanguageChangeAsync(ComponentBase component)
{
    _subscribedComponents.Add(component);
    await LoadCultureAsync(component);
}

public void UnsubscribeLanguageChange(ComponentBase component) => _subscribedComponents.Remove(component);

public async Task NotifyLanguageChangeAsync()
{
    foreach (var component in _subscribedComponents)
    {
        if (component is not null)
        {
            await LoadCultureAsync(component);
            var stateHasChangedMethod = component.GetType()?.GetMethod("StateHasChanged", 
                BindingFlags.Instance | BindingFlags.NonPublic);
            stateHasChangedMethod?.Invoke(component, null);
        }
    }
}

Create the string localizer

  • Create a string localizer that implements the interface IStringLocalizer<TComponent>. For example:
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent>
     where TComponent : ComponentBase
{
    private readonly BlazorSchoolResourceMemoryStorage _blazorSchoolResourceMemoryStorage;
    public BlazorSchoolStringLocalizer(BlazorSchoolResourceMemoryStorage blazorSchoolResourceMemoryStorage)
    {
        _blazorSchoolResourceMemoryStorage = blazorSchoolResourceMemoryStorage;
    }

    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => 
        throw new NotImplementedException("We do not need to implement this method. This method is not support asynchronous anyway.");
}
  • Add method and index to localize string. For example:
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent> 
    where TComponent : ComponentBase
{
    ...
    public LocalizedString this[string name] => FindLocalziedString(name);
    public LocalizedString this[string name, params object[] arguments] => FindLocalziedString(name, arguments);
    ...
    private LocalizedString FindLocalziedString(string name, object[]? arguments = null)
    {
        LocalizedString result = new(name, "", true, "External resource");
        _blazorSchoolResourceMemoryStorage.JsonComponentResources.TryGetValue(new(typeof(TComponent).FullName!, 
            CultureInfo.DefaultThreadCurrentCulture!.Name), out string? jsonResource);

        if (string.IsNullOrEmpty(jsonResource))
        {
            return result;
        }

        var jObject = JObject.Parse(jsonResource);
        bool success = jObject.TryGetValue(name, out var jToken);

        if (success)
        {
            string value = jToken!.Value<string>()!;

            if (arguments is not null)
            {
                value = string.Format(value, arguments);
            }

            result = new(name, value, false, "External resource");
        }

        return result;
    }
}

Register and config

You need to register BlazorSchoolStringLocalizer<>, BlazorSchoolCultureProvider, BlazorSchoolResourceMemoryStorage, add internal HttpClient, set the localization path, load the language resource files and set the startup language in the Program.cs. For example:

builder.Services.AddHttpClient("InternalHttpClient", 
    httpClient => httpClient.BaseAddress = new(builder.HostEnvironment.BaseAddress));
builder.Services.AddScoped(typeof(IStringLocalizer<>), typeof(BlazorSchoolStringLocalizer<>));
builder.Services.AddScoped<BlazorSchoolCultureProvider>();
builder.Services.AddScoped<BlazorSchoolResourceMemoryStorage>();
builder.Services.AddLocalization(options => options.ResourcesPath = "lang");

var wasmHost = builder.Build();
var culturesProvider = wasmHost.Services.GetService<BlazorSchoolCultureProvider>();

if (culturesProvider is not null)
{
    culturesProvider.SetStartupLanguage("fr");
}

await wasmHost.RunAsync();

Implement language selection strategies

In this section, you will learn how to implement:

  • Local storage strategy.
  • Cookie strategy.
  • URL strategy.
  • Multiple strategies with priority.

Local storage strategy

In this strategy, you will store the user preferred language in the local storage. In the initial loading phase, you will check if the local storage has value, then use the language in the local storage. Otherwise, use the fallback language.

  1. Add JavaScript code to access the local storage at index.html. For example:
<script>
    window.blazorCulture = {
        get: () => window.localStorage["BlazorSchoolInstantTranslation"],
        set: (value) => window.localStorage["BlazorSchoolInstantTranslation"] = value
    };
</script>
  1. Update your SetStartupLanguageAsync method of your culture provider class. For example:
public async Task SetStartupLanguageAsync(string fallbackLanguage)
{
    string languageFromLocalStorage = await _jsRuntime.InvokeAsync<string>("blazorCulture.get");

    if (string.IsNullOrEmpty(languageFromLocalStorage))
    {
        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(fallbackLanguage);
        CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(fallbackLanguage);
    }
    else
    {
        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(languageFromLocalStorage);
        CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(languageFromLocalStorage);
    }
}
The variable _jsRuntime is injected from the constructor with the type IJSRuntime.
  1. Build a language selector component. Whenever the user select a different language, you will store the language to the local storage and notify language change. For example:
@inject IJSRuntime JSRuntime
@inject BlazorSchoolCultureProvider BlazorSchoolCultureProvider 

<select @onchange="OnChangeLanguage">
    <option value="">Select</option>
    <option value="en">English</option>
    <option value="fr">France</option>
</select>

@code {
    private void OnChangeLanguage(ChangeEventArgs e)
    {
        if (string.IsNullOrEmpty(e.Value?.ToString()) is false)
        {
            string selectedLanguage = e.Value.ToString()!;
            CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(selectedLanguage);
            CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(selectedLanguage);
            InvokeAsync(() => JSRuntime.InvokeVoidAsync("blazorCulture.set", selectedLanguage));
            InvokeAsync(BlazorSchoolCultureProvider.NotifyLanguageChangeAsync);
        }
    }
}

Cookie strategy

In this strategy, you will store the user preferred language in the cookie. In the initial loading phase, you will check if the cookie has value, then use the language in the cookie. Otherwise, use the fallback language.

  1. Add JavaScript code to access cookie at index.html. For example:
<script>
    window.BlazorSchoolUtil = {
        updateCookies: (key, value) => document.cookie = `${key}=${value}`,
        getCookieValue: (key) => document.cookie.match('(^|;)\\s*' + 
            key + '\\s*=\\s*([^;]+)')?.pop() || '';
    }
</script>
  1. Update your SetStartupLanguageAsync method of your culture provider class. For example:
public async Task SetStartupLanguageAsync(string fallbackLanguage)
{
    string cookie = await _jsRuntime.InvokeAsync<string>("BlazorSchoolUtil.getCookieValue", 
        CookieRequestCultureProvider.DefaultCookieName);
    var result = CookieRequestCultureProvider.ParseCookieValue(cookie);

    if (result is null)
    {
        var defaultCulture = CultureInfo.GetCultureInfo(fallbackLanguage);
        CultureInfo.DefaultThreadCurrentCulture = defaultCulture;
        CultureInfo.DefaultThreadCurrentUICulture = defaultCulture;
    }
    else
    {
        string storedLanguage = result.Cultures.First().Value;
        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(storedLanguage);
        CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(storedLanguage);
    }
}
The variable _jsRuntime is injected from the constructor with the type IJSRuntime.
  1. Build a language selector component. Whenever the user select a different language, you will store the language to the cookie and notify language change. For example:
@inject IJSRuntime JSRuntime
@inject BlazorSchoolCultureProvider BlazorSchoolCultureProvider 

<select @onchange="OnChangeLanguage">
    <option value="">Select</option>
    <option value="en">English</option>
    <option value="fr">France</option>
</select>

@code {
    private void OnChangeLanguage(ChangeEventArgs e)
    {
        if (string.IsNullOrEmpty(e.Value?.ToString()) is false)
        {
            string selectedLanguage = e.Value.ToString()!;
            CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(selectedLanguage);
            CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(selectedLanguage);
            InvokeAsync(() => JSRuntime.InvokeVoidAsync("BlazorSchoolUtil.updateCookies", 
                CookieRequestCultureProvider.DefaultCookieName, 
                CookieRequestCultureProvider.MakeCookieValue(new(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture))));
            BlazorSchoolCultureProvider.NotifyLanguageChange();
        }
    }
}

URL strategy

In this strategy, you will determine the user preferred language by the URL. In the initial loading phase, you will check if the URL has language parameter, then use the language in the URL. Otherwise, use the fallback language.

  1. In your culture provider, inject the NavigationManager.
  2. Create a method to switch the language. For example:
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
    string languageFromUrl = GetLanguageFromUrl();
    CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(languageFromUrl);
    CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(languageFromUrl);
    await NotifyLanguageChangeAsync();
}

public string GetLanguageFromUrl()
{
    var uri = new Uri(_navigationManager.Uri);
    var urlParameters = HttpUtility.ParseQueryString(uri.Query);

    return string.IsNullOrEmpty(urlParameters["language"]) ? _fallbackLanguage : urlParameters["language"]!;
}
  1. Subscribe/unsubscribe to the LocationChanged event. For example:
public class BlazorSchoolCultureProvider : IDisposable
{
    public BlazorSchoolCultureProvider(..., NavigationManager navigationManager)
    {
        _navigationManager = navigationManager;
        _navigationManager.LocationChanged += OnLocationChanged;
    }

    public void Dispose() => _navigationManager.LocationChanged -= OnLocationChanged;
}

For this strategy, there is not language selector component. You change the language by update the language parameter in the URL.

Multiple strategies with priority

You can combine multiple strategies together, you will need to set the priority of each strategy in the code. In the end of the day, the method SetStartupLanguageAsync will select the highest priority strategy and use that language.


Use the instant translation

Once you have set up all the steps, you can use translation in a component as follows:

@inject BlazorSchoolStringLocalizer<ChangeLanguageDemonstrate> Localizer
@inject BlazorSchoolCultureProvider BlazorSchoolCultureProvider
@implements IDisposable

<PageTitle>@Localizer["String1"]</PageTitle>
<h3>ChangeLanguageDemonstrate</h3>
@Localizer["String1"]
<LanguageSelector />

@code {
    protected override async Task OnInitializedAsync() => await BlazorSchoolCultureProvider.SubscribeLanguageChangeAsync(this);
    public void Dispose() => BlazorSchoolCultureProvider.UnsubscribeLanguageChange(this);
}
BLAZOR SCHOOL
Designed and built with care by our dedicated team, with contributions from a supportive community. We strive to provide the best learning experience for our users.
Docs licensed CC-BY-SA-4.0
Copyright © 2021-2025 Blazor School
An unhandled error has occurred. Reload 🗙