0
votes

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...!

1
I think the problem lies on the language change logic, not the IStringLocalizer. But it's hard do tell, you need to provide more info on what exactly the code is doing. - Leandro Requena
You haven't provided enough information (at least to me) to understand how IStringLocalizer is based on the user's selected language. Is the selected language a constructor parameter to the instance of IStringLocalizer? If so, it means your service is probably outliving the scope that would contain the change in the language. But you really need to provide a more concrete (and still short) example here. - Kirk Woll
ok, let me adjust the question to make it clear enough! - neggenbe

1 Answers

0
votes

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.