Instant translation is an approach to translate a website that doesn't require a refresh to see the translated content. Not only that, but you can break free from RESX file, this technique can read from multiple data source like database, YML, JSON files. You can decide the priority of each language selection strategies. In this tutorial, we will walk you through this amazing technique step by step with the following content:
You can download the example code used in this topic on GitHub.
There are 2 approaches when making a multilingual website, instant translation is one of the approaches. When a user selects a language, the translation will process immediately, which means the user doesn't need to refresh the web page to see the new language. Furthermore, you can choose where to store the language, whether it is in the database, whether it is in the YML/JSON file. You can also have the control over the priority of language selection strategy.+
Deferred Translation | Instant Translation | |
User experience | Bad | Good |
Language selection strategy | Not customizable: only supports cookies, browser settings, and URL. Cannot change the priority of the strategies. | Customizable: support cookies, browser settings, URL, and custom strategies. Can change the priority of the strategies. |
Resource type | .resx files only. |
Support any file extensions and database. |
Implement effort | Low | High |
In instant translation approach, there are 3 main building blocks:
String localizer: Fetch the translated text by the provided key.
Request culture provider: Determine the fallback language when no language is specified (the most common case is the user visits your website for the first time). Decide which language selection strategy wins, either cookies or URL.
Language notifier: When the language changes, the language notifier will notify all the components to update themselves.
This is a continuation of the previous tutorial, begins at step 7.
public class BlazorSchoolLanguageNotifier { 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)); } } } }
Program.cs
as follows:builder.Services.AddScoped<BlazorSchoolLanguageNotifier>();
By implementing the IStringLocalizer
interface, you can fetch the translated content from many data sources (database, YML/JSON/RESX files). In this example, we will fetch resource from the RESX files. The interface IStringLocalizer
has 2 important indexes, that are: LocalizedString this[string name]
and LocalizedString this[string name, params object[] arguments]
. The former index allows you to find the translated content by its name, the latter has the same functionality, but you can have some arguments act as placeholders in the translated content. For example in English, we say "31st" (no space) in English but in Chinese, it is "31 日" (has a space between the number and text); The number 31 is the parameter to the translated content.
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent> { public LocalizedString this[string name] => FindLocalziedString(name); public LocalizedString this[string name, params object[] arguments] => FindLocalziedString(name, arguments); public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { throw new NotImplementedException(); } }
IOptions<LocalizationOptions>
so we can access the resource location. For example:public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent> { ... private readonly IOptions<LocalizationOptions> _localizationOptions; public BlazorSchoolStringLocalizer(IOptions<LocalizationOptions> localizationOptions) { _localizationOptions = localizationOptions; } }
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent> { ... private LocalizedString FindLocalziedString(string key, object[]? arguments = default) { var resourceManager = CreateResourceManager(); LocalizedString result; try { string value = resourceManager.GetString(key); if (arguments is not null) { value = string.Format(value, arguments); } result = new(key, value, false, GetResourceLocaltion()); } catch { result = new(key, "", true, GetResourceLocaltion()); } return result; } private ResourceManager CreateResourceManager() { string resourceLocaltion = GetResourceLocaltion(); var resourceManager = new ResourceManager(resourceLocaltion, Assembly.GetExecutingAssembly()); return resourceManager; } private string GetResourceLocaltion() { var componentType = typeof(TComponent); var nameParts = componentType.FullName.Split('.').ToList(); nameParts.Insert(1, _localizationOptions.Value.ResourcesPath); string resourceLocaltion = string.Join(".", nameParts); return resourceLocaltion; } }
The method FindLocalziedString
will be called by the indexes of the string localizer class.
Program.cs
. For example:builder.Services.AddScoped(typeof(IStringLocalizer<>), typeof(BlazorSchoolStringLocalizer<>));
There are 2 suitable language selection strategies for the instant translation approach: cookies and URL.
In this strategy, you will store the user preferred language in the cookie. You will check if the cookie has value, then use the language in the cookie. Otherwise, use the fallback language.
export function addCookies(key, value) { document.cookie = `${key}=${value}`; }
public class BlazorSchoolRequestCultureProvider : RequestCultureProvider { public string DefaultCulture { get; set; } public BlazorSchoolRequestCultureProvider(string defaultCulture) { DefaultCulture = defaultCulture; } public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext) { string inputCulture = httpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] ?? ""; var result = CookieRequestCultureProvider.ParseCookieValue(inputCulture); if (result is null) { CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(DefaultCulture); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(DefaultCulture); result = new(DefaultCulture); } else { CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(result.Cultures.First().Value); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(result.UICultures.First().Value); } return Task.FromResult<ProviderCultureResult?>(result); } }
Program.cs
. For example:builder.Services.Configure<RequestLocalizationOptions>(options => { options.AddSupportedCultures(new[] { "en", "fr" }); options.AddSupportedUICultures(new[] { "en", "fr" }); options.RequestCultureProviders = new List<IRequestCultureProvider>() { new BlazorSchoolRequestCultureProvider("en") }; });
In this code sample, we select English as the default language.
@inject NavigationManager NavigationManager @inject IJSRuntime JSRuntime @inject BlazorSchoolLanguageNotifier BlazorSchoolLanguageNotifier @implements IAsyncDisposable <select @onchange="OnChangeLanguage"> <option value="">Select</option> <option value="en">English</option> <option value="fr">France</option> </select> @code { private Lazy<IJSObjectReference> LanguageSelectorModule = new(); protected override async Task OnAfterRenderAsync(bool firstRender) { LanguageSelectorModule = new(await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/BlazorSchoolLanguageSelector.js")); } private void OnChangeLanguage(ChangeEventArgs e) { CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo((string)e.Value); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo((string)e.Value); LanguageSelectorModule.Value.InvokeVoidAsync("addCookies", CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture))); BlazorSchoolLanguageNotifier.NotifyLanguageChange(); } public async ValueTask DisposeAsync() { if (LanguageSelectorModule.IsValueCreated) { await LanguageSelectorModule.Value.DisposeAsync(); } } }
In this strategy, you will determine the user preferred language by the URL. When the user send the first request, you will check if the URL contains the language parameter, then use the language in the URL. Otherwise, use the fallback language.
Because this is a different strategy so we will start at step 13:
public class BlazorSchoolRequestCultureProvider : RequestCultureProvider { private readonly string _defaultLanguage; public BlazorSchoolRequestCultureProvider(string defaultLanguage) { _defaultLanguage = defaultLanguage; } }
public class BlazorSchoolRequestCultureProvider : RequestCultureProvider { ... private string? GetLanguageFromUrl(string url) { var uri = new Uri(url); var urlParameters = HttpUtility.ParseQueryString(uri.Query); return urlParameters["language"]; } }
DetermineProviderCultureResult
method to change the culture. For example:public class BlazorSchoolRequestCultureProvider : RequestCultureProvider { ... public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext) { if (httpContext.Request.Headers["Sec-Fetch-Dest"] == "document") { string url = httpContext.Request.GetDisplayUrl(); _selectedLanguage = GetLanguageFromUrl(url); if (string.IsNullOrEmpty(_selectedLanguage)) { _selectedLanguage = _defaultLanguage; } CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(_selectedLanguage); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(_selectedLanguage); var result = new ProviderCultureResult(_selectedLanguage); return Task.FromResult<ProviderCultureResult?>(result); } else { return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult(_selectedLanguage)); } } }
The browser sends many requests of different types like images, JavaScript, stylesheet so the filter httpContext.Request.Headers["Sec-Fetch-Dest"] == "document"
is needed to only change the language for the HTML document.
Program.cs
. For example:builder.Services.Configure<RequestLocalizationOptions>(options => { options.AddSupportedCultures(new[] { "en", "fr" }); options.AddSupportedUICultures(new[] { "en", "fr" }); options.RequestCultureProviders = new List<IRequestCultureProvider>() { new BlazorSchoolRequestCultureProvider("en") }; });
@inject NavigationManager NavigationManager @inject IJSRuntime JSRuntime @inject BlazorSchoolLanguageNotifier BlazorSchoolLanguageNotifier <select @onchange="OnChangeLanguage"> <option value="">Select</option> <option value="en">English</option> <option value="fr">France</option> </select> @code { private void OnChangeLanguage(ChangeEventArgs e) { var uri = new Uri(NavigationManager.Uri); var culture = CultureInfo.GetCultureInfo(e.Value as string); var cultureEscaped = Uri.EscapeDataString(culture.Name); var urlParameters = HttpUtility.ParseQueryString(uri.Query); urlParameters["language"] = cultureEscaped; string urlWithoutQuery = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped); NavigationManager.NavigateTo($"{urlWithoutQuery}?{urlParameters.ToString()}"); CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureEscaped); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(cultureEscaped); BlazorSchoolLanguageNotifier.NotifyLanguageChange(); } }
When create a new Razor component, subcribe the language change from the language notifier. For example:
@inject IStringLocalizer<ChangeLanguageDemonstrate> Localizer @inject BlazorSchoolLanguageNotifier BlazorSchoolLanguageNotifier @implements IDisposable <h3>ChangeLanguageDemonstrate</h3> @Localizer["String1"] <LanguageSelector /> @code { protected override void OnInitialized() => BlazorSchoolLanguageNotifier.SubscribeLanguageChange(this); public void Dispose() => BlazorSchoolLanguageNotifier.UnsubscribeLanguageChange(this); }