Instant translation

🟥 Not applicable to Blazor Server

Compared to deferred translation, instant translation offers several advantages. Firstly, it enables your website to be translated into different languages without requiring a page reload. Additionally, instant translation is more flexible than deferred translation when it comes to working with resource files. While deferred translation is limited to using only RESX resources, instant translation can work with a variety of formats, including JSON and YML. Furthermore, instant translation provides more flexibility when it comes to resource loading. With instant translation, you can choose to either eagerly load all resources when the page loads or lazily load resources on an as-needed basis. In this tutorial, we will guide you through:

  • Loading your resources
  • Eagerly loading all resources
  • Lazily loading resources
  • Implementing the cookies storage strategy
  • Implementing the local storage strategy
  • Implementing the URL strategy
  • Using the instant translation
You can download the example code used in this topic on GitHub.

Loading your resources

The Blazor built-in culture provider does not support loading multiple cultures by default. Therefore, you must create your own culture provider. There are 2 approaches to consider when implementing a culture provider:

  1. Eagerly loading all resources
  2. Lazily loading resources

Eagerly loading all resources

The eagerly loading approach is tied to the RESX resource type and is not recommended due to the obsolescence of IJSUnmarshalledRuntime, which will likely be removed in .NET 8. We highly recommend implementing the lazily loading resource approach instead.

Creating and using resource files

By default, the resources files must be located with the component (centralized resource), as shown in the following image:

centralized-resource.png

However, centralizing resources can pose challenges when it comes to maintenance. To address this, you can distribute resource files into a separate folder from your component and reconstruct the component folder tree inside a folder, as shown in the following image:

distributed-resource.png

If you have centralized your resource files, you can add the following code to your Program.cs file:

builder.Services.AddLocalization();

On the other hand, if you have distributed your resource files, you can add the following code to your Program.cs file:

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

In this case, BlazorSchoolResources is the root resource folder where your resource files are located.

  1. Install the Microsoft.Extensions.Localization, System.Resources.ResourceManager, and Microsoft.AspNetCore.Localization from the NuGet library.
  2. To modify the csproj.cs file, right-click on your project and select Edit Project File. Then, add the following code to the file:

edit-csproj-file.png

<PropertyGroup>
    ...
    <BlazorWebAssemblyLoadAllGlobalizationData>
        true
    </BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
  1. In your component, inject IStringLocalizer<T>, where T represents your component type and uses IStringLocalizer<T> to display text in multiple languages. For example, in ChangeLanguageDemonstrate.razor:
@inject IStringLocalizer<ChangeLanguageDemonstrate> Localizer

@Localizer["String1"]

In the example above, String1 is your resource key.

You can also pass parameters to the localizer. To do this, add a placeholder in the translated text, such as "Hello {0}, {1}". Then, use @Localizer["Hello {0}, {1}", "Blazor School", "Blazor School Books"] to replace the placeholder with parameters.

  1. Add the code for a centralized resource or distributed resource to your Program.cs file.
  2. Create a culture provider class. For example:
public class BlazorSchoolEagerCultureProvider
{
    private readonly IJSUnmarshalledRuntime _invoker;
    private const string _getSatelliteAssemblies = "Blazor._internal.getSatelliteAssemblies";
    private const string _readSatelliteAssemblies = "Blazor._internal.readSatelliteAssemblies";
    private readonly List<ComponentBase> _subscribedComponents = new();

    public BlazorSchoolEagerCultureProvider(IJSUnmarshalledRuntime invoker)
    {
        _invoker = invoker;
    }

    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);
        }
    }
    
    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);
            }
        }
    }
}
  1. To load resource files and set the startup language for your website, you can do so in the Program.cs file.
builder.Services.AddScoped(sp => (IJSUnmarshalledRuntime)sp.GetRequiredService<IJSRuntime>());
builder.Services.AddScoped<BlazorSchoolEagerCultureProvider>();

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

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

await wasmHost.RunAsync();

Lazily loading resources

Lazily loading resources is the recommended approach for loading resources in Blazor. This is because it offers greater flexibility in terms of resource loading and can support a variety of resource file types.

Creating and using resource

When creating and using resources in Blazor, you have the option of storing translated content in either files or a database. If you choose to use files, the resource files should be stored in the wwwroot folder. For example, the image below shows JSON resource files being used.

json-resource-files-example.png

To register the folder of resource files, you can use the following code in the Program.cs:

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

Here are the steps to lazily load resource files into Blazor:

  1. Install Microsoft.Extensions.Localization package.
  2. If you're storing translated content in files, you should register the resource files location.
  3. Create a cache for resources to improve performance. You can store loaded resources in a cache, such as the following:
public class BlazorSchoolResourceMemoryStorage
{
    public Dictionary<KeyValuePair<string, string>, string> JsonComponentResources { get; set; } = new();
}
  1. Register the cache in the Program.cs file using the following code:
builder.Services.AddScoped<BlazorSchoolResourceMemoryStorage>();
  1. Install additional NuGet packages to read the resource files. In this tutorial, we will use JSON, so you need to install 2 additional packages: Microsoft.Extensions.Http and Newtonsoft.Json.
  2. Create a custom culture provider using the following code:
public class BlazorSchoolLazyCultureProvider
{
    private readonly HttpClient _httpClient;
    private readonly IOptions<LocalizationOptions> _localizationOptions;
    private readonly BlazorSchoolResourceMemoryStorage _blazorSchoolResourceMemoryStorage;

    public BlazorSchoolLazyCultureProvider(IHttpClientFactory httpClientFactory, IOptions<LocalizationOptions> localizationOptions, BlazorSchoolResourceMemoryStorage blazorSchoolResourceMemoryStorage)
    {
        ...
    }

    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;
    }
    
    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);
            }
        }
    }
}
  1. Register the culture provider in the Program.cs file using the following code:
builder.Services.AddScoped<BlazorSchoolLazyCultureProvider>();

Create the string localizer

When lazily loading a resource, it is important to create a string localizer that can find the translated content in the resource cache based on the provided keys and parameters.

  1. Create a string localizer using the following code:
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent> where TComponent : ComponentBase
{
    private readonly BlazorSchoolResourceMemoryStorage _blazorSchoolResourceMemoryStorage;

    public LocalizedString this[string name] => FindLocalziedString(name);
    public LocalizedString this[string name, params object[] arguments] => FindLocalziedString(name, arguments);

    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.");

    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;
    }
}
  1. Register the string localizer in the Program.cs file using the following code:
builder.Services.AddScoped(typeof(IStringLocalizer<>), typeof(BlazorSchoolStringLocalizer<>));

Implementing the cookies storage strategy

Blazor can use the cookie language selection strategy to determine a user's preferred language on a website. If a cookie with the preferred language exists, the website can display content in that language by default. However, if the cookie does not exist, Blazor can use other language selection strategies such as browser language or language in the URL. Alternatively, the website can display content in a fallback language.

cookie-storage-strategy-example.png

It's worth noting that while cookies can be used for language selection, MDN recommends using alternative browser storage methods like localStorage, sessionStorage, or IndexedDB due to their better performance and security.

To implement the cookie storage strategy, follow these steps:

  1. Import the JavaScript needed to access the cookies. Refer to the Cookie Storage tutorial for guidance.
  2. Add a method in your culture provider that sets the startup culture based on the cookie value.
public class BlazorSchoolCultureProvider
{
    ...
    public async Task SetStartupLanguageAsync(string fallbackLanguage)
    {
        var jsRuntime = (IJSRuntime)_invoker;
        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);
        }
    }
}
  1. Load the resources and set the startup language in your Program.cs.
// await builder.Build().RunAsync();
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();
  1. Build the language selector component. This component allows the user to choose their preferred language and stores it in a cookie, which is used to display content in their preferred language on subsequent visits to the website.
@inject IJSRuntime JSRuntime
@inject BlazorSchoolEagerCultureProvider 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();
        }
    }
}

Implementing the local storage strategy

The local storage strategy is a language selection method used by developers to determine a user's preferred language when creating a multilingual website. Local storage is a web storage API that allows web applications to store data locally within a user's browser.

With the local storage strategy, developers can store the user's preferred language as a value in the local storage object. When the user visits the website again, the website can read the stored value from the local storage object and display the website content in the preferred language.

local-storage-strategy-example.png

To implement the local storage strategy, follow these steps:

  1. Create an local storage accessor class. Refer to the InteropServices.JavaScript tutorial for guidance.
  2. Add a method in your culture provider that sets the startup culture based on the local storage value.
public class BlazorSchoolLazyCultureProvider
{
    ...
    public void SetStartupLanguage(string fallbackLanguage)
    {
        string languageFromLocalStorage = BlazorSchoolLocalStorageAccessor.GetItem("BlazorSchoolInstantTranslation");

        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);
        }
    }
}
  1. Load the resources and set the startup language in your Program.cs.
// await builder.Build().RunAsync();
var wasmHost = builder.Build();
var culturesProvider = wasmHost.Services.GetService<BlazorSchoolCultureProvider>();
culturesProvider?.SetStartupLanguage("fr");

await wasmHost.RunAsync();
  1. Build the language selector component. This component allows the user to choose their preferred language and stores it in a local storage, which is used to display content in their preferred language on subsequent visits to the website.
@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);
            BlazorSchoolLocalStorageAccessor.SetItem("BlazorSchoolInstantTranslation", selectedLanguage);
            InvokeAsync(BlazorSchoolCultureProvider.NotifyLanguageChangeAsync);
        }
    }
}

Implementing the URL strategy

The URL strategy allows users to easily switch to their preferred language or share a link with the preferred language

url-strategy-example.png

To implement the URL strategy, follow these steps:

  1. Add a method in your culture provider that sets the startup culture based on the URL value.
public class BlazorSchoolCultureProvider
{
    ...
    public void SetStartupLanguage(string fallbackLanguage)
    {
        _fallbackLanguage = fallbackLanguage;
        string languageFromUrl = GetLanguageFromUrl();
        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(languageFromUrl);
        CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(languageFromUrl);
    }

    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 to the LocationChanged event and unsubscribe from the event afterward.
public class BlazorSchoolCultureProvider : IDisposable
{
    ...
    private readonly NavigationManager _navigationManager;

    public BlazorSchoolLazyCultureProvider(..., NavigationManager navigationManager)
    {
        ...
        _navigationManager.LocationChanged += OnLocationChanged;
    }

    private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        string languageFromUrl = GetLanguageFromUrl();
        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(languageFromUrl);
        CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(languageFromUrl);
        await NotifyLanguageChangeAsync();
    }

    public void Dispose() => _navigationManager.LocationChanged -= OnLocationChanged;
}
  1. Load the resources and set the startup language in your Program.cs.
// await builder.Build().RunAsync();
var wasmHost = builder.Build();
var culturesProvider = wasmHost.Services.GetService<BlazorSchoolLazyCultureProvider>();
culturesProvider?.SetStartupLanguage("fr");

await wasmHost.RunAsync();

Using the instant translation

After creating all the necessary classes and preparing the translated resources, you can begin using instant translation. When creating a new component, it's important to subscribe and unsubscribe to the language change notification:

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

<h3>ChangeLanguageDemonstrate</h3>
@Localizer["Hello Blazor School {0} {1}", "optional param1", "optional param2"]

@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-2024 Blazor School
An unhandled error has occurred. Reload 🗙