neggenbe
neggenbe

Reputation: 1885

Blazor IStringLocalizer injection to services

I am using IStringLocalizer approach to localize my Blazor app as discussed here.

Injecting the IStringLocalizer on razor pages works great. I also need this to localize some services - whether scoped or even singleton services.

Using constructor injection to inject my IStringLocalizer service into the service works. However, when users change the language via UI, the service (whether singleton or scoped) keeps the initial IStringLocalizer - i.e. the one with the original language used when starting the app, not the updated language selected by the user.

What is the suggested approach to retrieve the updated IStringLocalizer from code?

EDIT To prevent more details, here is some piece of code. First, I add a Resources folder and create there a default LocaleResources.resx (with public modifiers) and a LocaleResources.fr.resx file, which contain the key-value pairs for each language.

Supported cultures are defined in the appsettings.json file as

"Cultures": {
        "en-US": "English",
        "fr": "Français (Suisse)",
        ...
    }

In startup, I register the Resources folder and the supported cultures :

public void ConfigureServices(IServiceCollection services {
    ...
    services.AddLocalization(options => options.ResourcesPath = "Resources");
    ...
    services.AddSingleton<MySingletonService>();
    services.AddScoped<MyScopedService>();
}

// --- helper method to retrieve the Cultures from appsettings.json
protected RequestLocalizationOptions GetLocalizationOptions() {
    var cultures = Configuration.GetSection("Cultures")
        .GetChildren().ToDictionary(x => x.Key, x => x.Value);

    var supportedCultures = cultures.Keys.ToArray();

    var localizationOptions = new RequestLocalizationOptions()
        .AddSupportedCultures(supportedCultures)
        .AddSupportedUICultures(supportedCultures);

    return localizationOptions;
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
    ...
    app.UseRequestLocalization(GetLocalizationOptions());
    ...
    app.UseEndpoints(endpoints => {
        endpoints.MapControllers();
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}

I created an empty LocaleResources.razor control at the root of the project (this is a trick used to inject a single resource file to all components).

I included a routing controller to change language :

[Route("[controller]/[action]")]
public class CultureController : Controller {
    public IActionResult SetCulture(string culture, string redirectUri) {
        if (culture != null) {
            HttpContext.Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(
                new RequestCulture(culture)));
        }
        return LocalRedirect(redirectUri);
    }
}

And the language UI switcher looks like this (I use SyncFusion control here, but it could be any lookup actually, that shouldn't really matter)

@inject NavigationManager NavigationManager
@inject IConfiguration Configuration
 
<SfComboBox TValue="string" TItem="Tuple<string, string>" Placeholder="Select language" DataSource="@Cultures"
            @bind-Value="selectedCulture" CssClass="lan-switch" Width="80%">
  <ComboBoxFieldSettings Text="Item2" Value="Item1"></ComboBoxFieldSettings>
</SfComboBox>

<style>
  .lan-switch {
    margin-left: 5%;
  }
</style>

@code {
  string _activeCulture = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
  private string selectedCulture {
    get => _activeCulture;
    set {
      _activeCulture = value;
      SelectionChanged(value);
    }
  }

  List<Tuple<string, string>> Cultures;

  protected override void OnInitialized() {
    var cultures = Configuration.GetSection("Cultures")
      .GetChildren().ToDictionary(x => x.Key, x => x.Value);
    Cultures = cultures.Select(p => Tuple.Create<string, string>(p.Key, p.Value)).ToList();
  }

  protected override void OnAfterRender(bool firstRender) {
    if (firstRender && selectedCulture != AgendaSettings.SelectedLanguage) {
      selectedCulture = AgendaSettings.SelectedLanguage;
    }
  }

  private void SelectionChanged(string culture) {
    if (string.IsNullOrWhiteSpace(culture)) {
      return;
    }
    AgendaSettings.SelectedLanguage = culture;
    var uri = new Uri(NavigationManager.Uri)
    .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
    var query = $"?culture={Uri.EscapeDataString(culture)}&" +
        $"redirectUri={Uri.EscapeDataString(uri)}";

    NavigationManager.NavigateTo("/Culture/SetCulture" + query, forceLoad: true);
  }
}

Finally, to the injection. I inject the IStringLocalizer to pages as follows and it works perfectly fine on razor controls:

@inject IStringLocalizer<LocaleResources> _loc

<h2>@_loc["hello world"]</h2>

Above, when I change language, the page displays the value in the corresponding resource file.

Now, to services: the MySingletonService and MyScopedService are registered at startup. They both have a constructor like

protected IStringLocalizer<LocaleResources> _loc;
public MySingletonService(IStringLocalizer<LocaleResources> loc) {
    _loc = loc;
}

public void someMethod() {
    Console.WriteLine(_loc["hello world"])
}

I run someMethod on a timer. Strangely, when I break on the above line, the result seems to oscillate : once it returns the default language's value, once the localized one...!

Upvotes: 1

Views: 1652

Answers (1)

neggenbe
neggenbe

Reputation: 1885

The answer to my question was: your code is correct!

The reason, I found out, is that I use a Scoped service that is started on the default App's start page:

protected async override Task OnAfterRenderAsync(bool firstRender) {
if (firstRender) {
  MyScopedService.StartTimer();
}
await base.OnAfterRenderAsync(firstRender);

}

When users change language, the whole page is refreshed and a new instance of the scoped service is created and timer started. As my service did not implement IDisposable, the timer was not actually stopped.

So 2 solutions here:

  1. use singleton services
  2. make servcie disposable and ensure tasks are cancelled when service is disposed of.

Upvotes: 0

Related Questions