Add new overloads to IStringLocalizerFactory to enable creating IStringLocalizer instances for a specific culture
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
## Background and Motivation
We used to have an API on `IStringLocalizer` that would enable creating an instance for a specific culture, but it was removed (see #7756) due to reports of confusion by implementors. In doing so, we removed any non-static way of creating a localizer for a specific culture, which is necessary in scenarios where the current culture isn't implicitly set by the execution context, e.g. sending emails from a background worker. We've received feedback that this scenario is quite common and the lack of a first-class way to get a culture-specific instance of `IStringLocalizer` without resorting to static manipulation is undesirable.
## Proposed API
Propose we add two new overloads with default implementations (to ensure the interface change isn't breaking) that allow creating `IStringLocalizer` instances for a specific culture:
```diff
public interface IStringLocalizerFactory
{
+ public IStringLocalizer Create(Type resourceSource, CultureInfo culture)
+ {
+ try
+ {
+ var originalCulture = CultureInfo.CurrentUICulture;
+ CultureInfo.CurrentUICulture = culture;
+ return Create(resourceSource);
+ }
+ finally
+ {
+ CultureInfo.CurentUICulture = originalCulture;
+ }
+ }
+ public IStringLocalizer Create(string baseName, string location, CultureInfo culture)
+ {
+ try
+ {
+ var originalCulture = CultureInfo.CurrentUICulture;
+ CultureInfo.CurrentUICulture = culture;
+ return Create(baseName, location);
+ }
+ finally
+ {
+ CultureInfo.CurentUICulture = originalCulture;
+ }
+ }
}
```
The implementation class `ResourceManagerStringLocalizerFactory` would be updated to implement these new methods to create instances of `ResourceManagerStringLocalizer` using the specified culture.
## Usage Examples
```csharp
public class EmailSender(IStringLocalizerFactory stringLocalizerFactory, CustomerDbContext db, IEmailSender emailSender)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
var since = DateTime.UtcNow - TimeSpan.FromDays(7);
var customersToVerify = await db.Customers
.Where(e => e.VerifiedOn == null && e.LastVerificationEmailSentOn >= since).Select(c => new { c.Id, c.FirstName, c.UICulture })
.ToListAsync(cancellationToken);
var templateName = "VerifyUser";
var subject = "Please verify your email address";
foreach (var customer in customersToVerify )
{
object[] parameters = [customer.FirstName];
await emailSender.SendAsync(templateName, customer.Culture, subject, parameters, cancellationToken);
await db.Customers.Where(c => c.Id == customer.Id)
.ExecuteUpdateAsync(setters => setters.SetProperty(c => c.LastVerificationEmailSentOn = DateTime.UtcNow));
}
}
}
```
## Alternative Designs
- A new interface like `ICultureAwareStringLocalizerFactory` that these methods are defined on but with no default implementation and that implementations have to add the culture-aware overloads for. Then consuming code would need to be updated to type-check for the new interface and if implemented, call the new methods on it, or the code could first attempt to resolve the new interface from DI, and if none is registered, fallback to the original interface and manually set the current culture before creating a localizer instance.
- Have the default implementations of the new overloads throw `NotImplementedException` rather than applying the [current suggested workaround](https://github.com/dotnet/aspnetcore/issues/7756#issuecomment-469353174).
## Risks
The default implementation setting static properties on `CultureInfo` could be problematic if not done correctly resulting in "leaking" culture specifics onto the current thread, but this is the approach we currently recommend to customers anyway.
Contributor guide
Assessment
This issue has not been assessed yet.