I want to change the display name of some properties (of ViewModel) directly without using [DisplayName(\"prop name\")]
. This should happen either direc
@Tseng, sorry I should have said that more clearly, I meant that we should use either naming convention, or SharedResources. but not both, I have many cases where I have a lot of shared resources, and many ViewModel-specific strings (so a mixture). That is not achievable with .Net Core localization solution.
If your only worries is that you can or can't determine if one or multiple resource files are chosen, that can easily be configured. I had to dig a bit in the source code, bit its seems possible.
As we can see here the localizer
is determined by the factory defined in the configuration
if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null)
{
localizer = _localizationOptions.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory);
}
whereas _localizationOptions
is MvcDataAnnotationsLocalizationOptions
.
The default implementation of MvcDataAnnotationsLocalizationOptions
is here:
/// <inheritdoc />
public void Configure(MvcDataAnnotationsLocalizationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) =>
stringLocalizerFactory.Create(modelType);
}
So it uses per model resources by default.
You can change that to a SharedResource
file for all data annotations if you like, with the following in your Startup.ConfigureServices
(untested, but should work):
services.AddMvc()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResource));
});
This will effectively ignore the passed type and always return a shared string localizer.
Of course, you can add any logic there and decide on type-per-type case which localizer you are going to use.
If that's not enough, you can implement your own custom IDisplayMetadataProvider
which handles it the way you want. But using the DisplayAttribute
should be enough actually. DisplayAttribute
has additional parameters which allow you to define the resource type.
[Display(Name = "StringToLocalize", ResourceType = typeof(SharedResource))]
With the ResourceType
you can choose the class (and hence the resource file name) used to look up for the localization.
IStringLocalizer
with fallback to per-viewmodel resourceThe more elegant solution involves using the above MvcDataAnnotationsLocalizationOptions
options file to return your own IStringLocalizer
which looks into one resource file and falls back to the other one.
public class DataAnnotationStringLocalizer : IStringLocalizer
{
private readonly IStringLocalizer primaryLocalizer;
private readonly IStringLocalizer fallbackLocalizer;
public DataAnnotationStringLocalizer(IStringLocalizer primaryLocalizer, IStringLocalizer fallbackLocalizer)
{
this.primaryLocalizer = primaryLocalizer ?? throw new ArgumentNullException(nameof(primaryLocalizer));
this.fallbackLocalizer = fallbackLocalizer ?? throw new ArgumentNullException(nameof(fallbackLocalizer));
}
public LocalizedString this[string name]
{
get
{
LocalizedString localizedString = primaryLocalizer[name];
if (localizedString.ResourceNotFound)
{
localizedString = fallbackLocalizer[name];
}
return localizedString;
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
LocalizedString localizedString = primaryLocalizer[name, arguments];
if (localizedString.ResourceNotFound)
{
localizedString = fallbackLocalizer[name, arguments];
}
return localizedString;
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
=> primaryLocalizer.GetAllStrings(includeParentCultures).Concat(fallbackLocalizer.GetAllStrings(includeParentCultures));
public IStringLocalizer WithCulture(CultureInfo culture)
=> new DataAnnotationStringLocalizer(primaryLocalizer.WithCulture(culture), fallbackLocalizer.WithCulture(culture));
}
And with the following options
services.AddMvc()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
return new DataAnnotationStringLocalizer(
factory?.Create(typeof(SharedResource)),
factory?.Create(type)
);
};
});
Now, the string is first resolved from the shared resource and if the string wasn't found there, it will resolve it from the view model type (type parameter passed to the factory method).
If you don't like the logic and you want that it first looks into the view-model resource files, you just change the order to
services.AddMvc()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
return new DataAnnotationStringLocalizer(
factory?.Create(type),
factory?.Create(typeof(SharedResource))
);
}
});
Now the view model is the primary resolver and shared resource the secondary