31

I'm using ASP.NET Core, and trying to localize the application. I managed to use new asp .net core resources to localize controllers and views, and old resources to localize error messages for model validation. However, when the error message is not linked to a model field annotation (like "Required") and the data for model binding is incorrect (like a text where a number is expected), I receive the error like below, which I'm unable to localize:

"The value 'abc' is not valid for ID."

When I enter abc for ID property in View, since the model binding can not be done to the field and it shows a validation message near the field, saying "The value 'abc' is not valid for ID.". Here is the class I'm using:

public class Country : IHasID
{
    public int ID { get; set; }

    [Required(ErrorMessageResourceType = typeof(L.Val),
    ErrorMessageResourceName = "NameR")]
    [MaxLength(100, ErrorMessageResourceType = typeof(L.Val), 
    ErrorMessageResourceName = "Max")]
    public string Name { get; set; }

    /*Some other properties*/
}

The similar issues I found on the internet were either targeted to older asp .net version, or else didn't help me solve the problem.

Reza Aghaei
  • 103,774
  • 12
  • 145
  • 300
Marko
  • 1,108
  • 3
  • 17
  • 33
  • I wonder why you get the validation message on `Test` property as it do not contain any validation attributes at all – Tseng Nov 27 '16 at 12:12
  • User might insert text in the corresponding HTML field, which would generate a model error (message od which I'm trying to change). – Marko Nov 27 '16 at 14:32

2 Answers2

60

To customize framework model binding error messages, you need to set custom accessors for different error message accessors of ModelBindingMessageProvider.

Example

Here you can download a full source code of what is described in this post. The repository contains example for ASP.NET Core 2.0 (VS 2017.3) and ASP.NET Core 1.1 (VS 2015):

Also here you can see the example, live:

Default Error Messages

These are default error messages which the framework shows when model binding to a property fails:

MissingBindRequiredValueAccessor    A value for the '{0}' property was not provided.
MissingKeyOrValueAccessor           A value is required.
ValueMustNotBeNullAccessor          The value '{0}' is invalid. 
AttemptedValueIsInvalidAccessor     The value '{0}' is not valid for {1}.
UnknownValueIsInvalidAccessor       The supplied value is invalid for {0}.
ValueIsInvalidAccessor              The value '{0}' is invalid.
ValueMustBeANumberAccessor          The field {0} must be a number.

In addition to above messages, ASP.NET Core 2.0 have these messages as well:

MissingRequestBodyRequiredValueAccessor       A non-empty request body is required.
NonPropertyAttemptedValueIsInvalidAccessor    The value '{0}' is not valid.
NonPropertyUnknownValueIsInvalidAccessor      The supplied value is invalid.
NonPropertyValueMustBeANumberAccessor         The field must be a number.

Localize ASP.NET Core Model Binding Error Messages

To localize ASP.NET Core model binding error messages, follow these steps:

  1. Create Resource File - Create a resource file under Resources folder in your solution and name the file ModelBindingMessages.fa.resx. The name can be anything else but we will use it to create a localizer. In the example, I used fa (Persian) culture.

  2. Add Resource Keys - Open the resource file and add keys and values which you want to use for localizing error messages. I used keys and values like below image:

    enter image description here

    Keys which I used are like original messages, except the key for ValueMustNotBeNull which was the same as ValueIsInvalid, so I used Null value is invalid. for it.

  3. Configure Options - In ConfigureServices method, when adding Mvc, configure its options to set message accessors for ModelBindingMessageProvider:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization(options => { options.ResourcesPath = "Resources"; });
        services.AddMvc(options =>
        {
            var F = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
            var L = F.Create("ModelBindingMessages", "AspNetCoreLocalizationSample");
            options.ModelBindingMessageProvider.ValueIsInvalidAccessor =
                (x) => L["The value '{0}' is invalid.", x];
            options.ModelBindingMessageProvider.ValueMustBeANumberAccessor =
                (x) => L["The field {0} must be a number.", x];
            options.ModelBindingMessageProvider.MissingBindRequiredValueAccessor =
                (x) => L["A value for the '{0}' property was not provided.", x];
            options.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor =
                (x, y) => L["The value '{0}' is not valid for {1}.", x, y];
            options.ModelBindingMessageProvider.MissingKeyOrValueAccessor =
                () => L["A value is required."];
            options.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor =
                (x) => L["The supplied value is invalid for {0}.", x];
            options.ModelBindingMessageProvider.ValueMustNotBeNullAccessor =
                (x) => L["Null value is invalid.", x];
        })
        .AddDataAnnotationsLocalization()
        .AddViewLocalization();
        services.Configure<RequestLocalizationOptions>(options =>
        {
            var supportedCultures = new[]{new CultureInfo("en"), new CultureInfo("fa")};
            options.DefaultRequestCulture = new RequestCulture("en", "en");
            options.SupportedCultures = supportedCultures;
            options.SupportedUICultures = supportedCultures;
        });
    }
    

    Also add this code at beginning of Configure method:

    var supportedCultures = new[] { new CultureInfo("en"), new CultureInfo("fa") };
    app.UseRequestLocalization(new RequestLocalizationOptions()
    {
        DefaultRequestCulture = new RequestCulture(new CultureInfo("en")),
        SupportedCultures = supportedCultures,
        SupportedUICultures = supportedCultures
    });
    

Important Note for ASP.NET Core 2.0

In ASP.NET Core 2.0, model binding message provider properties has got read only, but a setter method for each property has been added.

For example, to set ValueIsInvalidAccessor, you should use SetValueIsInvalidAccessor() method this way:

options.ModelBindingMessageProvider.SetValueIsInvalidAccessor (
    (x) => L["The value '{0}' is invalid.", x]);
zed
  • 2,106
  • 4
  • 23
  • 40
Reza Aghaei
  • 103,774
  • 12
  • 145
  • 300
  • I've tested it using `Microsoft.AspNetCore.Mvc 1.1.0`. Let me know if yo uhave any problem applying the solution. – Reza Aghaei Jan 16 '17 at 11:23
  • 1
    This looks really cool, however I'm not able to get it to apply in Core 1.1 - the resource file is there, and the localisation for the rest of the project is working, but using the above I'm still getting the default error messages... That is "The {0} Field is required" on the `ValueMustNotBeNullAccessor ` - so it doesn't even take the English values... Any ideas? – RemarkLima Jan 21 '17 at 16:12
  • @RemarkLima Make sure you are doing all checks and if the problem is still there send me an email (r.aghaei at outlook.com). Then I'll create a test project using `VS2015`, `Microsoft.AspNetCore.Mvc 1.1.0` and `.NET 4.6.1` and will share with you. – Reza Aghaei Jan 21 '17 at 16:59
  • **1)** If you use `...ValueMustNotBeNullAccessor = (x) => L["xxx", x];` then even if there is no resource key for `xxx`, the same message should be shown. **2)** If you create a `ModelBindingMessages.xx.resx` and add a key `The {0} Field is require`, then the value of this key will be used as error message. **3)** Don't forget `var F= ...` and `var L = ...`. **4)** The localization service should be added before mvc: `services.AddLocalization...` – Reza Aghaei Jan 21 '17 at 17:00
  • If you can make the test project public, that would be nice. I tried to apply everything that you wrote (except the language code, I used a different one) and for me it didn't work. I rechecked everything, but without success. – Marko Jan 21 '17 at 18:04
  • @Marko Send me an email (r.aghaei at outlook.com). Then I'll create a test project using `VS2015`, `Microsoft.AspNetCore.Mvc 1.1.0` and `.NET 4.6.1` and will share with you. – Reza Aghaei Jan 21 '17 at 18:59
  • Could we setup a project on Github? I'm using .NET Core framework, rather than `.NET 4.6.1` - could that be the cause? – RemarkLima Jan 21 '17 at 19:11
  • @RemarkLima I checked all options again, it works properly, just updated the answer. I added a piece of code which I used in my project which is related to the answer but was forgotten to be added in answer. I'll create a github repository for it when I have time :) – Reza Aghaei Jan 21 '17 at 20:23
  • 2
    @RemarkLima github repository added. https://github.com/r-aghaei/AspNetCoreLocalizationSample – Reza Aghaei Jan 22 '17 at 16:38
  • 1
    @Marko github repository added. https://github.com/r-aghaei/AspNetCoreLocalizationSample – Reza Aghaei Jan 22 '17 at 16:38
  • Great example. Thank you! – Marko Jan 30 '17 at 07:18
  • @RezaAghaei Thanks a lot for your answer, the translation works wel! The issue is that the parameter is not set in case of "Null value is invalid." I tried to add '{0}' to it, but no success so far. What do you think? Thanks in advance! – mirind4 Nov 10 '17 at 19:32
  • 1
    @mirind4 *1)* Error message can be set using `ValueMustNotBeNullAccessor` and the default message is `"The value '{0}' is invalid."` which means it can have parameter `{0}`. *2)* To add `{0}` you should change the message in 2 location, in startup, where we are setting `SetValueMustNotBeNullAccessor` and also in your localized resource file. This is a common mistake to forget one of those locations. *3)* After correcting those locations, keep in mind that value of `{0}` which will be passed to the message is `Null` and displays nothing instead of `{0}`! So there is no point in showing it. – Reza Aghaei Nov 10 '17 at 23:46
  • @RezaAghaei Thanks a lot for the quick reply, I appreciate it! Cheers! – mirind4 Nov 11 '17 at 05:24
  • 3
    I'm sorry, but in Core 2.0 this approach does not work for validation messages generated by [Required] annotations. It seems that the error string `The XYZ field is required.` cannot be translated this way. – Jpsy Mar 21 '18 at 11:20
  • @Jpsy Read my comments [here](https://stackoverflow.com/questions/45927545/asp-net-core-model-binding-error-messages-localization-in-asp-net-core-2-0?noredirect=1&lq=1#comment80704675_45990844). – Reza Aghaei Mar 21 '18 at 11:43
  • 4
    Yes, @RezaAghaei, you are correct. I recommend that you add to your answer here, that certain annotations cannot be translated using the `ModelBindingMessageProvider` approach, but need a static ErrorMessage string in the annotations that will trigger the translation. The `[Required]` case is one of them and it might be the case that brings most people here. I took me hours to get to this point of understanding. – Jpsy Mar 21 '18 at 12:35
  • 1
    Thanks for your comment @Jpsy. The question is about *Model Binding Error Messages Localization*. It's not about *Validation Attribute Error Messages Localization*. So I believe most users who reached to this question/answer are aware of difference :) – Reza Aghaei Mar 21 '18 at 12:38
  • @Jpsy If you download the sample project, you will see it contains localization of model binding error messages and also localization of validation attribute error messages. Maybe I add a note about it in the question. Again, thank you for your comment :) – Reza Aghaei Mar 21 '18 at 12:41
  • Somehow I cannot make it work in 2.1... If I name the file ModelBindingMessages.resx, it works, but of course it is not going to help if more than one language is supported by the app... Any thoughts here? I am using Core 2.1 – Samo Nov 27 '18 at 23:56
  • In Core 2.2 I get `Property or indexer 'DefaultModelBindingMessageProvider.ValueIsInvalidAccessor' cannot be assigned to -- it is read only`. I guess, we have to implement custom `ModelBindingMessageProvider` and also `IValidationAttributeAdapterProvider` for attributes such as [EmailAddress]. – JustAMartin Jul 11 '19 at 10:23
  • @JustAMartin You probably missed **Important Note for ASP.NET Core 2.0** → `options.ModelBindingMessageProvider.SetValueIsInvalidAccessor ((x) => L["The value '{0}' is invalid."]);` – Reza Aghaei Jul 11 '19 at 10:31
  • Maybe I update the main part of the answer to ASP.NET CORE 2 rather than just keeping as a note. – Reza Aghaei Jul 11 '19 at 10:33
  • One more gotcha that breaks these default binding translations - if your web API receives JSON, Json.Net formatter will take over the ModelBindingMessageProvider and will throw out their own messages such as `Input string '111a' is not a valid number. Path 'countryId', line 3, position 23.` I'll have to look further how to solve this... – JustAMartin Jul 11 '19 at 13:46
  • Maybe it works, but I can't got known it: I don't use any error message from this list. For example, how to set accessors for these messages in **ASP.NET Core 2.2**: `The {0} field is not a valid e-mail address.` and `The {0} field is required.`? – Vitaliy Dec 10 '19 at 14:08
  • And these: `The {0} must be at least {1} and at max {2} characters long.`, `The password and confirmation password do not match.` – Vitaliy Dec 10 '19 at 14:14
  • @Vitaliy I haven't updated the example for 2.2 but you can download a working example of 2.0 – Reza Aghaei Dec 10 '19 at 14:18
  • @RezaAghaei I've already downloaded it. According to 2.0 example I've implemented `ModelBindingMessageProvider` for 2.2. There is the same list of **options.ModelBindingMessageProvider.Set...** methods in **Startup.cs** file. **[Required]** and **[Email]** attributes aren't novelty in ASP.NET Core 2.2. The most important question is **How to implement localization for standard validation attributes?** – Vitaliy Dec 10 '19 at 14:39
  • @Vitaliy Look at [`SampleModel`](https://github.com/r-aghaei/AspNetCoreLocalizationSample/blob/master/src/vs2017-aspnetcore2.0/AspNetCoreLocalizationSample/AspNetCoreLocalizationSample/Models/SampleModel.cs) class and `StringLength` attribute. I've put the persian localized error message of this validation attribute in [`Models.SampleModel.fa.resx`](https://github.com/r-aghaei/AspNetCoreLocalizationSample/blob/master/src/vs2017-aspnetcore2.0/AspNetCoreLocalizationSample/AspNetCoreLocalizationSample/Resources/Models.SampleModel.fa.resx). – Reza Aghaei Dec 10 '19 at 14:54
  • @Vitaliy see the example [at runtime](http://aspnetcorelocalizationsample.azurewebsites.net/). Enter a long string in the Name field and see the validation message. Then change the language to Persian and enter long name again and see the validation. In fact the answer shows how to localize Validation messages and also Model binding messages. – Reza Aghaei Dec 10 '19 at 14:56
  • @RezaAghaei it works good, but only for specific message inside 1 specific model. How to make this localization rule workable for all models for standard attribute message? For example, I have **[Required]** attribute in my models 100 times, which produce a message "*The {0} field is required.*", where **{0}** is the field display name. How to override this message once for all models? – Vitaliy Dec 10 '19 at 15:29
  • @Vitaliy I got your question. It's a different topic and a good question. If you couldn't find an answer for that, I think it's better to ask a new question about it. Feel free to let me know if you asked a new question. I'll take a look at that and try to share my ideas :) – Reza Aghaei Dec 10 '19 at 15:45
  • @RezaAghaei I've created a question https://stackoverflow.com/questions/59284038/how-to-localize-standard-error-messages-of-validation-attributes-in-asp-net-core – Vitaliy Dec 11 '19 at 11:24
  • @Learning I haven't tested it using that version, but I expect no major change in the solution, If I find time, I'll add the project for 3.1 to the repository. – Reza Aghaei Jan 09 '20 at 09:19
  • It does not work if I add [FromBody] before SampleModel ``` c# HttpPost] public IActionResult Index([FromBody] SampleModel model) { return View(model); }``` – Trong Vo Mar 10 '21 at 10:34
  • @TrongVo Thanks for the feedback, I need to update the answer for .NET 5 as soon as I can. – Reza Aghaei Mar 10 '21 at 10:35
  • i still using your example. .net 2.0 – Trong Vo Mar 10 '21 at 10:37
  • For [FromBody] attribute, we can disable default message in services.AddMvc().AddJsonOptions(options => options.AllowInputFormatterExceptionMessages = false;). This will add an empty string for the error message, which we can detect and then just display a generic message to user. I have not found a better way of doing this but this method will suffice for now. – Trong Vo Mar 11 '21 at 03:32
3

With reference to this post that describes in detail the side effects for using BuildServiceProvider inside ConfigureServices, and to this answer regarding resolving services inside ConfigureServices, last but not least, considering the refered improved answer by Andrew Lock, the correct approach to localize model binding error messages should be by creating a custom configuration class that implements IConfigureOptions<T> then registering it in startup as below:

public class ConfigureModelBindingLocalization : IConfigureOptions<MvcOptions>
{
    private readonly IServiceScopeFactory _serviceFactory;
    public ConfigureModelBindingLocalization(IServiceScopeFactory serviceFactory)
    {
        _serviceFactory = serviceFactory;
    }

    public void Configure(MvcOptions options)
    {
        using(var scope = _serviceFactory.CreateScope())
        {
            var provider = scope.ServiceProvider;
            var localizer = provider.GetRequiredService<IStringLocalizer>();

            options.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor((x, y) => 
                localizer["The value '{0}' is not valid for {1}.", x, y]);

            options.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor((x) => 
                localizer["A value for the '{0}' parameter or property was not provided.", x]);

            options.ModelBindingMessageProvider.SetMissingKeyOrValueAccessor(() => 
                localizer["A value is required."]);

           options.ModelBindingMessageProvider.SetMissingRequestBodyRequiredValueAccessor(() =>
               localizer["A non-empty request body is required."]);

           options.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor((x) =>
               localizer["The value '{0}' is not valid.", x]);

           options.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor(() =>
               localizer["The supplied value is invalid."]);

           options.ModelBindingMessageProvider.SetNonPropertyValueMustBeANumberAccessor(() =>
               localizer["The field must be a number."]);

           options.ModelBindingMessageProvider.SetUnknownValueIsInvalidAccessor((x) =>
               localizer["The supplied value is invalid for {0}.", x]);

           options.ModelBindingMessageProvider.SetValueIsInvalidAccessor((x) =>
               localizer["The value '{0}' is invalid.", x]);

           options.ModelBindingMessageProvider.SetValueMustBeANumberAccessor((x) =>
               localizer["The field {0} must be a number.", x]);

           options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor((x) =>
               localizer["The value '{0}' is invalid.", x]);
        }
    }
}

Finally register the new configuration class in startup:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureModelBindingLocalization>();

    // ...
}
Marc_Sei
  • 123
  • 1
  • 6
LazZiya
  • 3,746
  • 2
  • 13
  • 26