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:
You can download the example code used in this topic on GitHub.
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
.
In this section, you will be able to answer the questions:
To implement the instant translation approach, you will need:
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.
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.
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):
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.
IJSUnmarshalledRuntime
. For example:public class BlazorSchoolCultureProvider { private readonly IJSUnmarshalledRuntime _invoker; public BlazorSchoolCultureProvider(IJSUnmarshalledRuntime invoker) { _invoker = invoker; } }
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); } } }
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(); } }
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); } } } }
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();
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):
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:
A resource file example content:
{ "String1": "Blazor School" }
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(); }
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; } }
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; }
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(); } }
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); } } }
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."); }
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; } }
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();
In this section, you will learn how to implement:
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.
index.html
. For example:<script> window.blazorCulture = { get: () => window.localStorage["BlazorSchoolInstantTranslation"], set: (value) => window.localStorage["BlazorSchoolInstantTranslation"] = value }; </script>
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 typeIJSRuntime
.
@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); } } }
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.
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>
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 typeIJSRuntime
.
@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(); } } }
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.
NavigationManager
.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"]!; }
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.
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.
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); }