ASP.NET Core provides the new type of IStringLocalizer<T>
to deal with string resources
and localization. There has been written a lot about this already and a simple search
will provide you with all background information you need - so I’ll skip this here, only
talking about the basic bits where they are required to what is done and why.
Usually, you would use a IStringLocalizer<T>
where T
is a type (say MyService
)
and then also provide a .resx
file of the same name, including the culture name
for the translations (e.g. MyService.de-DE.resx
). Note that you specifically, don’t
need to provide a resource file for the default language of your application.
Let’s look at the following example to make this more clear:
public class MyService : IMyService
{
private readonly IStringLocalizer<MyService> m_strings;
public MyService(IStringLocalizer<MyService> strings)
{
m_strings = strings;
}
public string GetResult()
{
return m_strings["The results"];
}
}
In the above you can see a couple of things:
IStringLocalizer<MyService>
is usually provided by DI.For the following, there is one important thing to keep in mind:
You cannot create instances of IStringLocalizer<T>
directly;
you are expected to have them injected.
That doesn’t sound too bad, but consider the following example:
internal static class Helpers
{
// Helper method called by multiple implementations.
internal static string DoTheFoo(IStringLocalizer strings)
{
return strings["The foo was done."];
}
}
So you have an helper method (static to make the case obvious that DI is possible) that is used by multiple other services or code.
Where would you put the resources for this? You could put them
into IStringLocalizer<SharedResources>
, but simply defining
an empty class called SharedResources
and SharedResources.<culture>.resx
that goes with it.
However, that doesn’t scale too well in bigger projects:
IStringLocalizer<T>
into other classesIStringLocalizer<T>
you need to inject to
satisfy all code that a service may call downstream.Especially the last point breaks encapsulation and is plain cumbersome to maintain.
For example, consider this:
public class MyService : IMyService
{
public MyService(
IStringLocalizer<MyService> strings, // for the service itself
IStringLocalizer<IOHelpers> ioStrings, // for the IOHelpers tools
IStringLocalizer<SharedResources> sharedStrings // generic stuff
)
{
// elided for brevity
}
}
A possible solution to this is to only inject the on IStringLocalizer<T>
into a service, the one that is directly “bound” to it. But then, have
that localizer name dependencies that should also be available.
For exempale:
[InheritStringLocalization(typeof(SharedResources))]
[InheritStringLocalization(typeof(IOHelpers))]
public class MyServiceResources
{
}
// --
public class MyService : IMyService
{
public MyService(IStringLocalizer<MyServiceResources> strings)
{
// elided for brevity
}
}
OK, you still have to note all the dependencies here, but at least not on the constructor directly. Additionally, if you define a proper resource hierarchy for your project you can make things a little more obvious.
For example, in the project where I employed this technique basically all code was structured like this:
Each level could have resources so that by default every controller
resource would only have InheritStringLocalization
-attributes for
the services it uses, and every service resource would only have them
for the utility classes it uses. In other words, each level only has
explicit dependencies to the immediate next level. Something which
is not possible if you use the plain DI-approach that the framework
provides.
The InheritStringLocalization
-attribute basically says that when
attempting to resolve a resource from the type it is applied on, the
type specified by the attribute should also be consider, if the
resource is not found in the type itself. It does that down the
chain. So if the type specified by the attribute does not contain
the resource, but has an InheritStringLocalization
-attribute itself,
this type is searched, and so on.
[InheritStringLocalization(typeof(CheckAfterResourceType))]
public class ResourceType { }
[InheritStringLocalization(typeof(CheckAfterResourceType2))]
public class CheckAfterResourceType { }
public class CheckAfterResourceType2 { }
If multiple attributes are applied to a resource type, the first that
contains the resource is used. The search order can be influenced by
an optional Priority
-parameter for the attribute:
[InheritStringLocalization(typeof(SharedResources), Priority=2)]
[InheritStringLocalization(typeof(IOHelpers), Priority=1)]
public class MyServiceResources
{
}
Lower priority values are considered first.
Finally note, that using this approach one can create a resource type that serves merely as a bundle, but does not define any resources itself:
// No SystemResources.*.resx exists
[InheritStringLocalization(typeof(IOSystemResources))]
[InheritStringLocalization(typeof(UISystemResources))]
public class SystemResources { }
// IOSystemResources.*.resx exists
public class IOSystemResources { }
// UISystemResources.*.resx exists
public class UISystemResources { }
This is the attribute that is applied to resource types to indicate that they should “inherit” resources from the specified “InheritFrom” type.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class InheritStringLocalizationAttribute : Attribute
{
public InheritStringLocalizationAttribute(Type inheritFrom)
{
InheritFrom = inheritFrom ?? throw new ArgumentNullException(nameof(inheritFrom));
}
public Type InheritFrom { get; }
public int Priority { get; set; }
}
This is the implementation behind an IStringLocalizer<T>
that uses a T
that has at least
InheritStringLocalizationAttribute
applied.
public class MultiStringLocalizer : IStringLocalizer
{
private readonly List<IStringLocalizer> m_localizers;
private readonly ILogger<MultiStringLocalizer> m_logger;
private readonly CultureInfo m_cultureInfo;
public MultiStringLocalizer(List<IStringLocalizer> localizers, ILogger<MultiStringLocalizer> logger)
{
if (localizers == null)
throw new ArgumentNullException(nameof(localizers));
if (localizers.Count == 0)
throw new ArgumentException("Empty not supported", nameof(localizers));
m_localizers = localizers;
m_logger = logger ?? NullLogger<MultiStringLocalizer>.Instance;
}
private MultiStringLocalizer(List<IStringLocalizer> localizers, ILogger<MultiStringLocalizer> logger, CultureInfo cultureInfo)
: this(localizers.Select(l => l.WithCulture(cultureInfo)).ToList(), logger)
{
m_cultureInfo = cultureInfo;
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
var result = new Dictionary<string, LocalizedString>();
foreach (var localizer in m_localizers)
{
foreach (var entry in localizer.GetAllStrings(includeParentCultures))
{
if (!result.ContainsKey(entry.Name))
{
result.Add(entry.Name, entry);
}
}
}
return result.Values;
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
if (culture == null)
{
return new MultiStringLocalizer(m_localizers.ToList(), m_logger);
}
return new MultiStringLocalizer(m_localizers, m_logger, culture);
}
private void OnLogAttempt(IStringLocalizer localizer, LocalizedString result, CultureInfo cultureInfo)
{
var keyCulture = cultureInfo ?? CultureInfo.CurrentUICulture;
if (!result.ResourceNotFound)
{
m_logger.LogDebug($"{localizer.GetType()} found '{result.Name}' in '{result.SearchedLocation}' with culture '{keyCulture}'");
}
else
{
m_logger.LogDebug($"{localizer.GetType()} searched for '{result.Name}' in '{result.SearchedLocation}' with culture '{keyCulture}'");
}
}
public LocalizedString this[string name]
{
get
{
LocalizedString s = null;
foreach (var localizer in m_localizers)
{
s = localizer[name];
if (!s.ResourceNotFound)
{
OnLogAttempt(localizer, s, m_cultureInfo);
return s;
}
OnLogAttempt(localizer, s, m_cultureInfo);
}
Debug.Assert(s != null);
return s;
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
LocalizedString s = null;
foreach (var localizer in m_localizers)
{
s = localizer[name, arguments];
if (!s.ResourceNotFound)
{
OnLogAttempt(localizer, s, m_cultureInfo);
return s;
}
OnLogAttempt(localizer, s, m_cultureInfo);
}
Debug.Assert(s != null);
return s;
}
}
}
The implementation is rather straight forward. Simply iterate all passed localizers until one returns the resource in question.
This class is responsible for actually creating our MultiStringLocalizer
types.
It needs to be registered with DI, which can be done with code like this:
public static class LocalizerExtensions
{
public static IServiceCollection AddInheritStringLocalizerFactory(this IServiceCollection services, Action<LocalizationOptions> action = null)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddOptions();
services.AddSingleton<IStringLocalizerFactory, InheritStringLocalizerFactory>();
services.AddTransient(typeof (IStringLocalizer<>), typeof (StringLocalizer<>));
if (action != null)
{
services.Configure(action);
}
return services;
}
}
The actual factory implementation looks like this:
public class InheritStringLocalizerFactory : IStringLocalizerFactory
{
private readonly ConcurrentDictionary<Type, IStringLocalizer> m_cache = new ConcurrentDictionary<Type, IStringLocalizer>();
private readonly ResourceManagerStringLocalizerFactory m_factory;
private readonly ILoggerFactory m_loggerFactory;
public InheritStringLocalizerFactory(IOptions<LocalizationOptions> localizationOptions, ILoggerFactory loggerFactory)
{
m_factory = new ResourceManagerStringLocalizerFactory(localizationOptions, loggerFactory);
m_loggerFactory = loggerFactory;
}
public IStringLocalizer Create(Type resourceSource)
{
if (resourceSource == null)
throw new ArgumentNullException(nameof(resourceSource));
return CreateStringLocalizer(resourceSource);
}
private IStringLocalizer CreateStringLocalizer(Type type)
{
return m_cache.GetOrAdd(type, CreateStringLocalizerDirect);
}
private IStringLocalizer CreateStringLocalizerDirect(Type t)
{
var attributes = t.GetCustomAttributes<InheritStringLocalizationAttribute>();
if (attributes.Any())
{
var localizers = new List<IStringLocalizer>();
var localizer = m_factory.Create(t);
localizers.Add(localizer);
foreach (var attribute in attributes.OrderBy(a => a.Priority))
{
localizer = CreateStringLocalizer(attribute.InheritFrom);
localizers.Add(localizer);
}
return new MultiStringLocalizer(localizers, m_loggerFactory.CreateLogger<MultiStringLocalizer>());
}
return m_factory.Create(t);
}
public IStringLocalizer Create(string baseName, string location) => m_factory.Create(baseName, location);
}