Instant translation

🟥 Not applicable to Blazor WebAssembly

Instant translation offers several advantages over deferred translation. Firstly, it allows your website to be translated into different languages without requiring a page reload, providing a seamless user experience. Additionally, instant translation is more flexible when it comes to working with resource files, as it can work with a variety of formats such as JSON and YML, whereas deferred translation is limited to using only RESX resources. In this tutorial, we will use RESX resources in the example, but you can use other formats as needed. We will cover the following topics:

  • Involved classes and their roles
  • Building the language notifier class
  • Building the string localizer class
  • Implementing the local storage strategy for language preference
  • Implementing the URL strategy for language preference
  • Using instant translation in your Blazor Server website
You can download the example code used in this topic on GitHub.

Involved classes and their roles

The instant translation feature consists of two main classes:

  • Language notifier: Responsible for determining the current language and notifying all components that need to be translated. When the current language changes, this class sends notifications to all components to update themselves accordingly.
  • String localizer: Provides the translated content to the components. This class determines where the resource is located and how to fetch the translated content. It is also responsible for providing the translated content in the correct language to the components that request it.

Building the language notifier class

  1. Create a new class that allows components to be registered, unregistered, and notified of language changes. The code for this class is as follows:
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);

    private 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. Add a full property for the current culture. This property sets the current culture and notifies all components that are subscribed to the language change event. The code for this property is as follows:
public class BlazorSchoolLanguageNotifier
{
    ...
    private CultureInfo _currentCulture;

    public CultureInfo CurrentCulture
    {
        get => _currentCulture;

        set
        {
            if (string.IsNullOrWhiteSpace(value.Name))
            {
                _currentCulture = CultureInfo.CurrentCulture;
                NotifyLanguageChange();
            }
            else
            {
                var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);

                if (allCultures.Contains(value))
                {
                    _currentCulture = value;
                    NotifyLanguageChange();
                }
            }
        }
    }

    public BlazorSchoolLanguageNotifier(IOptions<RequestLocalizationOptions> options)
    {
        _currentCulture = options.Value.DefaultRequestCulture.Culture;
    }
}
  1. Register at Program.cs as a scoped service.
builder.Services.AddScoped<BlazorSchoolLanguageNotifier>();

Building the string localizer class

  1. Create a new class that implements the IStringLocalizer<T> interface to provide the translated content. For example:
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)
    {
        // Blazor Server will not use this method so you can leave it empty
    }
}
  1. Inject IOptions<LocalizationOptions> to access the resource location and the language notifier class to determine the current language. For example:
public class BlazorSchoolStringLocalizer<TComponent> : IStringLocalizer<TComponent>
{
    ...
    private readonly IOptions<LocalizationOptions> _localizationOptions;
    private readonly BlazorSchoolLanguageNotifier _blazorSchoolLanguageNotifier;

    public BlazorSchoolStringLocalizer(IOptions<LocalizationOptions> localizationOptions, BlazorSchoolLanguageNotifier blazorSchoolLanguageNotifier)
    {
        _localizationOptions = localizationOptions;
        _blazorSchoolLanguageNotifier = blazorSchoolLanguageNotifier;
    }
}
  1. Create methods to fetch the translated content. The example methods below fetch translated text from RESX files:
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, _blazorSchoolLanguageNotifier.CurrentCulture);

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

Implementing the local storage strategy for language preference

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

Blazor Server is rendered twice, once on the server-side and once on the client-side. When Blazor Server is rendered on the server-side, there is no local storage available, and the default language is used. However, when it is rendered on the client-side, it can read the local storage and display the user's preferred language. Due to this situation, you may notice a brief flash of the default language before quickly translating to the user's preferred language.

To implement the local storage strategy for instant translation, follow these steps:

  1. Create resource files (refer to the previous tutorial for more details).
  2. Register the resource files in Program.cs (refer to the previous tutorial for more details).
  3. Configure localization settings in Program.cs using the following code:
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    options.SetDefaultCulture("fr");
    options.AddSupportedCultures(new[] { "en", "fr" });
    options.AddSupportedUICultures(new[] { "en", "fr" });
});
  1. Change the CurrentCulture property of the language notifier instance in App.razor to the value stored in local storage, if available.
@inject ProtectedLocalStorage ProtectedLocalStorage
@inject BlazorSchoolLanguageNotifier BlazorSchoolLanguageNotifier

<Router AppAssembly="@typeof(App).Assembly">
    ...
</Router>

@code {
    protected override async Task OnInitializedAsync() 
    {
        try // when render at server side, there is no local storage. Wrap the code in an empty try catch is necessary
        {
            var inputCultureLocalStorageResult = await ProtectedLocalStorage.GetAsync<string>("BlazorSchoolLanguage");

            if (inputCultureLocalStorageResult.Success && !string.IsNullOrEmpty(inputCultureLocalStorageResult.Value))
            {
                var selectedCulture = CultureInfo.GetCultureInfo(inputCultureLocalStorageResult.Value);
                BlazorSchoolLanguageNotifier.CurrentCulture = selectedCulture;
            }
        }
        catch
        {
        }
    }
}
  1. Build the language selector component to allow the user to choose their preferred language and update the local storage value and the CurrentCulture property of the BlazorSchoolLanguageNotifier instance.
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@inject BlazorSchoolLanguageNotifier BlazorSchoolLanguageNotifier
@inject ProtectedLocalStorage ProtectedLocalStorage

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

@code {
    private async Task OnChangeLanguageAsync(ChangeEventArgs e)
    {
        string selectedCulture = e.Value as string;

        if (!string.IsNullOrEmpty(selectedCulture))
        {
            await ProtectedLocalStorage.SetAsync("BlazorSchoolLanguage", selectedCulture);
            BlazorSchoolLanguageNotifier.CurrentCulture = CultureInfo.GetCultureInfo(selectedCulture);
        }
    }
}

Implementing the URL strategy for language preference

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

url-example.png

To implement the URL strategy for instant translation, follow these steps:

  1. Create resource files (refer to the previous tutorial for more details).
  2. Register the resource files in Program.cs (refer to the previous tutorial for more details).
  3. Configure localization settings in Program.cs using the following code:
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    options.SetDefaultCulture("fr");
    options.AddSupportedCultures(new[] { "en", "fr" });
    options.AddSupportedUICultures(new[] { "en", "fr" });
});
  1. In the App.razor file, check if the culture is present in the URL, if it is, set the culture in the language notifier instance.
@inject NavigationManager NavigationManager
@inject BlazorSchoolLanguageNotifier BlazorSchoolLanguageNotifier
@implements IDisposable

<Router AppAssembly="@typeof(App).Assembly">
    ...
</Router>

@code {
    protected override void OnInitialized() 
    {
        NavigationManager.LocationChanged += OnLocationChanged;
        LoadCulture();
    }

    private void LoadCulture()
    {
        var cultureFromUrl = GetCultureFromUrl(NavigationManager.Uri);
        BlazorSchoolLanguageNotifier.CurrentCulture = cultureFromUrl;
    }

    private void OnLocationChanged(object? sender, LocationChangedEventArgs locationChangedEventArgs)
    {
        LoadCulture();
    }

    private CultureInfo GetCultureFromUrl(string url)
    {
        var uri = new Uri(url);
        var urlParameters = HttpUtility.ParseQueryString(uri.Query);
        var culture = CultureInfo.GetCultureInfo(urlParameters["language"] ?? "");

        return culture;
    }

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

The code subscribes to the LocationChanged event of the NavigationManager and updates the current culture of the BlazorSchoolLanguageNotifier instance based on the culture code extracted from the URL parameter language. It also calls the LoadCulture() method in the OnInitialized lifecycle method to load the culture from the URL when the component is first initialized. The Dispose method unsubscribes from the LocationChanged event to avoid memory leaks.

  1. Build the language selector component to allow the user to choose their preferred language and update the URL and the CurrentCulture property of the language notifier instance. The following code shows an example of a language selector component.
@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()}");
        BlazorSchoolLanguageNotifier.CurrentCulture = CultureInfo.GetCultureInfo(cultureEscaped);
    }
}

Using instant translation in your Blazor Server website

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 🗙