Reactive translation is one of the internationalization techniques. Reactive translation provides the best user experience because it does not require the user to reload the page. The translation process automatically applies to text and formats (date time format, number format, etc...) to all pages.
In this guide, you will be covered on how to i18n your website with 2 methodologies for detecting the user's preferred language:
You can download the example code used in this topic on GitHub.
To prepare your website for translations, you should have a basic understanding of the following subjects.
With this technique, the selected language will be stored on the cookies of the browser, you are going to use JavaScript to change the cookies.The reason is Blazor Server is an SPA framework. Therefore, the users only send one initial request to the server and you cannot modify the cookies based on that request unlike the Passive translation technique.
IStringLocalizer
You first need to customize the IStringLocalizer
because later on, you will have a customized RequestCultureProvider
. Create a new class BlazorSchoolStringLocalizer
as follows:
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent> { private readonly IOptions<LocalizationOptions> _localizationOptions; public LocalizedString this[string name] => FindLocalziedString(name); public LocalizedString this[string name, params object[] arguments] => FindLocalziedString(name, arguments); public BlazorSchoolStringLocalizer(IOptions<LocalizationOptions> localizationOptions) { _localizationOptions = localizationOptions; } public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { var resourceManager = CreateResourceManager(); var result = new List<LocalizedString>(); try { var resourceSet = resourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true); result = resourceSet.Cast<DictionaryEntry>().Select(item => new LocalizedString((string)item.Key, (string)item.Value, false, GetResourceLocaltion())) .ToList(); } catch { result.Add(new("", "", true, GetResourceLocaltion())); } return result; } private LocalizedString FindLocalziedString(string name, object[] arguments = null) { var resourceManager = CreateResourceManager(); LocalizedString result; try { string value = resourceManager.GetString("Name"); if (arguments is not null) { value = string.Format(value, arguments); } result = new(name, value, false, GetResourceLocaltion()); } catch { result = new(name, "", 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 BlazorSchoolStringLocalizer
will read the settings from LocalizationOptions
, create a ResourceManager
and provide the translation text.
RequestCultureProvider
You need to create a custom RequestCultureProvider
and use this class as a culture determination on your website, this class will take the culture in cookies and decide which culture to show on the screen. Create a new class BlazorSchoolRequestCultureProvider
as follows:
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(result); } }
This class also has a default culture and later on you can change its default in Startup.cs
.
Follow the section "Localization" in the guide Internationalization and Localization, and once you have done so, you should have a Resources folder that contains all the language files. The next step is to use the Resources folder in Startup.cs
.
public void ConfigureServices(IServiceCollection services) { ... services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddScoped(typeof(IStringLocalizer<>), typeof(BlazorSchoolStringLocalizer<>)); services.Configure<RequestLocalizationOptions>(options => { options.AddSupportedCultures(new[] { "en", "fr" }); options.AddSupportedUICultures(new[] { "en", "fr" }); options.RequestCultureProviders = new List<IRequestCultureProvider>() { new BlazorSchoolRequestCultureProvider("en") }; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRequestLocalization(); ... }
BlazorUtil.js
as follows:class BlazorUtil { addCookies(key, value) { document.cookie = `${key}=${value}`; } } window.BlazorUtil = new BlazorUtil();
Then, include it in the _Host.cshtml
.
... <body> ... <script src="~/js/BlazorUtil.js" type="text/javascript"></script> <script src="_framework/blazor.server.js"></script> </body> ...
To change the language, you need to change the CultureInfo.CurrentCulture
and CultureInfo.CurrentUICulture
to change the current language. Then call the BlazorUtil.addCookies(key, value)
to store the selected language in the cookies for later use. You can see our example at I18N.razor
. The following code is a snippet taken from our example.
<select @onchange="OnChangeLanguage"> <option value="">Select</option> <option value="en">English</option> <option value="fr">France</option> </select> @code { private void OnChangeLanguage(ChangeEventArgs e) { CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo((string)e.Value); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo((string)e.Value); JSRuntime.InvokeVoidAsync("BlazorUtil.addCookies", CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue( new(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture))); } }
With this technique, Blazor Server will determine the language displayed on the website by the requested URL. Every component has to subscribe to the NavigationManager.LocationChanged
event and call StateHasChanged()
when the LocationChanged
occurs.
LocalizationOptions
LocalizationOptions
only allow you to set the Resource folder, you will also need to add a default language to this options because later on, you will re-use the default language in App.razor
. You can create a new class to customize LocalizationOptions
as follows:public class BlazorSchoolLocalizationOptions : LocalizationOptions { public string DefaultLanguage { get; set; } }
IStringLocalizer
Just like the cookies appoach, you will need to have a customize IStringLocalizer
. Create a new class BlazorSchoolStringLocalizer
as follows:
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent> { private readonly IOptions<BlazorSchoolLocalizationOptions> _localizationOptions; public LocalizedString this[string name] => FindLocalziedString(name); public LocalizedString this[string name, params object[] arguments] => FindLocalziedString(name, arguments); public BlazorSchoolStringLocalizer(IOptions<BlazorSchoolLocalizationOptions> localizationOptions) { _localizationOptions = localizationOptions; } public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { var resourceManager = CreateResourceManager(); var result = new List<LocalizedString>(); try { var resourceSet = resourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true); result = resourceSet.Cast<DictionaryEntry>().Select(item => new LocalizedString((string)item.Key, (string)item.Value, false, GetResourceLocaltion())) .ToList(); } catch { result.Add(new("", "", true, GetResourceLocaltion())); } return result; } private LocalizedString FindLocalziedString(string name, object[] arguments = null) { var resourceManager = CreateResourceManager(); LocalizedString result; try { string value = resourceManager.GetString("Name"); if (arguments is not null) { value = string.Format(value, arguments); } result = new(name, value, false, GetResourceLocaltion()); } catch { result = new(name, "", 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; } }
RequestCultureProvider
You also need to customize the RequestCultureProvider
. This class will take the requested URL and parse the language for the initial request. Create a new class BlazorSchoolRequestCultureProvider
as follows:
public class BlazorSchoolRequestCultureProvider : RequestCultureProvider { private readonly BlazorSchoolLocalizationOptions _localizationOptions; private string _selectedLanguage { get; set; } public BlazorSchoolRequestCultureProvider(BlazorSchoolLocalizationOptions localizationOptions) { _localizationOptions = localizationOptions; } public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext) { if (httpContext.Request.Headers["Sec-Fetch-Dest"] == "document") { var query = httpContext.Request.Query; string selectedLanguage = query["language"].ToString() ?? _localizationOptions.DefaultLanguage; CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(selectedLanguage); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(selectedLanguage); var result = new ProviderCultureResult(selectedLanguage); _selectedLanguage = selectedLanguage; return Task.FromResult(result); } else { return Task.FromResult(new ProviderCultureResult(_selectedLanguage)); } } }
In our example, we use language
as the parameter name for the language in the URL, you can also change this parameter name.
NavigationManager.LocationChanged
eventThe BlazorSchoolRequestCultureProvider
only provides the language for the initial request. Blazor Server is a SPA framework and therefore, there is only one request. You need to subscribe to the NavigationManager.LocationChanged
event to detect if the user has chosen a new language. You need to subscribe to the event in the first Razor Component called by _Host.cshtml
. By default, it is App.razor
.
@using System.Globalization @using System.Web; @using ReactiveI18NUrlBlazorServer.Data @using Microsoft.Extensions.Options @inject NavigationManager NavigationManager @inject IOptions<BlazorSchoolLocalizationOptions> BlazorSchoolLocalizationOptions @implements IDisposable ... @code { protected override void OnInitialized() { NavigationManager.LocationChanged += DetermineLanguage; } private void DetermineLanguage(object sender, LocationChangedEventArgs e) { var uri = new Uri(NavigationManager.Uri); var urlParameters = HttpUtility.ParseQueryString(uri.Query); CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(urlParameters["language"] ?? BlazorSchoolLocalizationOptions.Value.DefaultLanguage); CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(urlParameters["language"] ?? BlazorSchoolLocalizationOptions.Value.DefaultLanguage); } public void Dispose() { NavigationManager.LocationChanged -= DetermineLanguage; } }
Follow the section "Localization" in the guide Internationalization and Localization, and once you have done so, you should have a Resources folder that contains all the language files. The next step is to use the Resources folder in Startup.cs
.
public void ConfigureServices(IServiceCollection services) { ... services.Configure<BlazorSchoolLocalizationOptions>(options => { options.ResourcesPath = "Resources"; options.DefaultLanguage = "en"; }); services.AddScoped(typeof(IStringLocalizer<>), typeof(BlazorSchoolStringLocalizer<>)); services.Configure<RequestLocalizationOptions>(options => { options.AddSupportedCultures(new[] { "en", "fr" }); options.AddSupportedUICultures(new[] { "en", "fr" }); options.RequestCultureProviders = new List<IRequestCultureProvider>() { new BlazorSchoolRequestCultureProvider(Configuration.Get<BlazorSchoolLocalizationOptions>()) }; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... app.UseRequestLocalization(); app.UseStaticFiles(); app.UseRouting(); ... }
The final step is to apply to a component. Subscribe to the NavigationManager.LocationChanged
event and re-render the component every time the event is fired, also remember to unsubscribe from the event when the component is being disposed.
@using Microsoft.Extensions.Localization @using System.Globalization @inject IStringLocalizer<I18N> Localizer @inject NavigationManager NavigationManager <h3>@Localizer["Name"]</h3> @code { protected override void OnInitialized() { NavigationManager.LocationChanged += Render; } private void Render(object sender, LocationChangedEventArgs e) { StateHasChanged(); } public void Dispose() { NavigationManager.LocationChanged -= Render; } }
Once you have applied to your component, you can navigate to your component and include language
to change the language. For example, http://localhost:5283/i18n?language=fr, http://localhost:5283/i18n, http://localhost:5283/i18n?language=en.