I'm struggling with localization in my new .NET Core project. I have 2 projects:
My wish is to localize all validation attributes globally in one single place to have the similar behavior like MVC 5. Is this possible?
I do not want to have separate language files for Models/Views etc.
Microsofts documentation is not very clear on using SharedResources.resx file with localized DataAnnotation messages.
In MVC 5 I didn't take care of it. I only needed to set the locale to my language and everything was fine.
I tried setting the ErrorMessageResourceName and ErrorMessageResourceType to my shared resource file name "Strings.resx" and "Strings.de.resx" in the DataAccess project:
[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
I also tried the setting name to be RequiredAttribute_ValidationError - but it's not working.
I already added .AddDataAnnotationsLocalization()
in Startup.cs - but it seems to do nothing.
I've read several articles but I couldn't find the cause why it's not working.
EDIT: What I have so far:
1.) LocService class
public class LocService
{
private readonly IStringLocalizer _localizer;
public LocService(IStringLocalizerFactory factory)
{
_localizer = factory.Create(typeof(Strings));
}
public LocalizedString GetLocalizedHtmlString(string key)
{
return _localizer[key];
}
}
2.) Added Folder "Resources" with Strings.cs (empty class with dummy constructor)
3.) Added Strings.de-DE.resx file with one item "RequiredAttribute_ValidationError"
4.) Modified my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<MessageService>();
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<LocService>();
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc()
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver())
.AddDataAnnotationsLocalization(
options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Strings));
});
services.Configure<RequestLocalizationOptions>(
opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("de-DE"),
};
opts.DefaultRequestCulture = new RequestCulture("de-DE");
// Formatting numbers, dates, etc.
opts.SupportedCultures = supportedCultures;
// UI strings that we have localized.
opts.SupportedUICultures = supportedCultures;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
I've followed the instructions here but it doesn't work: https://damienbod.com/2017/11/01/shared-localization-in-asp-net-core-mvc/
Please keep in mind that my Models are kept in a separate project.
App localization involves the following: Make the app's content localizable. Provide localized resources for the languages and cultures you support. Implement a strategy to select the language/culture for each request.
Declaration of supported languages is defined in Configure() of your Startup class (see example). If you still need all accepted languages as a simple string[] like the older Request. UserLanguages property, then use the HeaderDictionaryTypeExtensions. GetTypedHeaders() extension defined in the Microsoft.
As @Sven points out in his comment to Tseng's answer it still requires that you specify an explicit ErrorMessage
, which gets quite tedious.
The problem arises from the logic ValidationAttributeAdapter<TAttribute>.GetErrorMessage()
uses to decide whether to use the provided IStringLocalizer
or not.
I use the following solution to get around that issue:
Create a custom IValidationAttributeAdapterProvider
implementation that uses the default ValidationAttributeAdapterProvider
like this:
public class LocalizedValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
private readonly ValidationAttributeAdapterProvider _originalProvider = new ValidationAttributeAdapterProvider();
public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
attribute.ErrorMessage = attribute.GetType().Name.Replace("Attribute", string.Empty);
if (attribute is DataTypeAttribute dataTypeAttribute)
attribute.ErrorMessage += "_" + dataTypeAttribute.DataType;
return _originalProvider.GetAttributeAdapter(attribute, stringLocalizer);
}
}
Register the adapter in Startup.ConfigureServices()
Before calling AddMvc()
:
services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider, LocalizedValidationAttributeAdapterProvider>();
I prefer to use "stricter" resource names based on the actual attributes, so the code above will look for resource names like "Required" and "DataType_Password", but this can of course be customized in many ways.
If you prefer resources names based on the default messages of the Attributes you could instead write something like:
attribute.ErrorMessage = attribute.FormatErrorMessage("{0}");
I tried setting the ErrorMessageResourceName and ErrorMessageResourceType to my shared resource file name "Strings.resx" and "Strings.de.resx" in the DataAccess project:
[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
I also tried the setting name to be RequiredAttribute_ValidationError - but it's not working.
You were on the right track, but you don't necessarily need to set ErrorMessageResourceName
/ ErrorMessageResourceType
properties.
Was we can see in the source code of ValidationAttributeAdapter<TAttribute>
, the conditions to use the _stringLocalizer
verison is when ErrorMessage
is not null
and ErrorMessageResourceName
/ErrorMessageResourceType
are null
.
In other words, when you don't set any properties or only ErrorMessage
. So a plain [Required]
should just work (see source where is passed to the base classes constructor).
Now, when we look at the DataAnnotations resource file we see that the name is set to "RequiredAttribute_ValidationError" and the value to "The {0} field is required." which is the default English translation.
Now if you use "RequiredAttribute_ValidationError" with the German translation in your "Strings.de-DE.resx" (or just Strings.resx as fallback), it should work with the corrected namespace from the comments.
So using the above configuration and the strings from the GitHub repository you should be able to make the localization work without extra attributes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With