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