Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Localization of RequiredAttribute in ASP.NET Core 2.0

I'm struggling with localization in my new .NET Core project. I have 2 projects:

  • DataAccess project with Models and DataAnnotations (e.g. RequiredAttribute)
  • Web project with MVC views etc.

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.

like image 739
Sven Avatar asked Feb 13 '18 14:02

Sven


People also ask

How ASP.NET applications are localized?

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.

How can I get browser language in asp net core?

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.


2 Answers

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:

  1. 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);
        }
    }
    
  2. 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}");
like image 156
Anders Avatar answered Sep 23 '22 01:09

Anders


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.

like image 39
Tseng Avatar answered Sep 20 '22 01:09

Tseng