Localizing DataAnnotations

Data annotations are property attributes like Display, Required, StringLength, ...etc. Localizing data annotations made easy with ASP.NET Core 2.1 using shared resource files.

First we need to create a model class to be used in our sample project.

Modify “Trips” file and create a simple trip model and form for user input:

Trips.cshtml.cs
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyTrips.Pages
{
    public class TripsModel : PageModel
    {
        public class Trip
        {
            [Required]
            public string Destination { get; set; }
            public DateTime TravelDate { get; set; }
            public decimal TicketPrice { get; set; }
        }

        [BindProperty]
        public Trip MyTrip { get; set; }

        public void OnGet()
        {

        }
    }
}
Trips.cshtml :
@page

@model MyTrips.Pages.TripsModel
@{
    ViewData["Title"] = Localizer.Text("Trips");
}

<h2>@ViewData["Title"]</h2>

<form method="post">
    <p>@Localizer.Text("Please fill below your travel info:")</p>
    <div asp-validation-summary="All" class="alert-danger"></div>
    <div class="form-group">
        <label asp-for="MyTrip.Destination"></label>
        <input asp-for="MyTrip.Destination" class="form-control" />
        <span asp-validation-for="MyTrip.Destination" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="MyTrip.TravelDate"></label>
        <input asp-for="MyTrip.TravelDate" class="form-control" />
        <span asp-validation-for="MyTrip.TravelDate" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="MyTrip.TicketPrice"></label>
        <input asp-for="MyTrip.TicketPrice" class="form-control" />
        <span asp-validation-for="MyTrip.TicketPrice" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">@Localizer.Text("Submit")</button>
</form>

Run the application and switch to any culture, you will notice the highlighted areas needs to be localized:

trips form no localization

All data annotations contains two parameters for error messages, ErrorMessageResourceName and ErrorMessageResourceType, localization can be done by providing resource name and resource type for each property, but this requires a lot of work and at the end the model will look more complicated and ugly.

Don't apply this step, this is only to show the old way of localizing data annotations!
Assuming that we have created a shared resource file with public access modifiers, then our localized model will look like below:

Trips.cshtml.cs
public class Trip
    {
        [Required(
            ErrorMessageResourceName = nameof(MyDataAnnotations.Required),
            ErrorMessageResourceType = typeof(MyDataAnnotations))]
        [StringLength(maximumLength: 100, MinimumLength = 3,
            ErrorMessageResourceName = nameof(MyDataAnnotations.StringLength),
            ErrorMessageResourceType = typeof(MyDataAnnotations))]
        [Display(
            Name = nameof(MyDataAnnotations.Destination), 
            ResourceType = typeof(MyDataAnnotations))]
        public string Destination { get; set; }

        [Required(
            ErrorMessageResourceName = nameof(MyDataAnnotations.Required),
            ErrorMessageResourceType = typeof(MyDataAnnotations))]
        [Display(
            Name = nameof(MyDataAnnotations.TravelDate),
            ResourceType = typeof(MyDataAnnotations))]
        public DateTime? TravelDate { get; set; }

        [Required(
            ErrorMessageResourceName = nameof(MyDataAnnotations.Required),
            ErrorMessageResourceType = typeof(MyDataAnnotations))]
        [Range(10.00, 10000.00,
            ErrorMessageResourceName = nameof(MyDataAnnotations.Range),
            ErrorMessageResourceType = typeof(MyDataAnnotations))]
        [Display(
            Name = nameof(MyDataAnnotations.TicketPrice),
            ResourceType = typeof(MyDataAnnotations))]
        public decimal? TicketPrice { get; set; }
    }

Now, lets do it the right way, as described in ASP.NET Core documentations, Data annotation localization can be configured in the startup file, then we only provide our serror messages inside the attribute tags. First we need to configure data annotation localization to use a shared resource file:

startup.cs
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
    .AddViewLocalization(o=>o.ResourcesPath = "Resources")
    .AddDataAnnotationsLocalization(o=> {
        var type = typeof(ViewResource);
        var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
        var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
        var localizer = factory.Create("ViewResource", assemblyName.Name);
        o.DataAnnotationLocalizerProvider = (t, f) => localizer;
    })
    .AddRazorPagesOptions(o => {
        o.Conventions.Add(new CultureTemplateRouteModelConvention());
    });    

Well, in the documentations it looks a bit simpler, but this way the resource files can be defined in another project like a class library project that can be shared across multiple projects.

Next, modify Trips model by decorating all its properties with relevant attributes:

Trips model

public class Trip
{
    [Required(ErrorMessage = "Please enter value for {0}")]
    [StringLength(
        maximumLength: 100, MinimumLength = 3,
        ErrorMessage = "'{0}' must be at least {1} and maximum {2} characters")]
    [Display(Name = "Destination")]
    public string Destination { get; set; }

    [Required(ErrorMessage = "Please enter value for {0}")]
    [Display(Name = "Travel date")]
    public DateTime? TravelDate { get; set; }

    [Required(ErrorMessage = "Please enter value for {0}")]
    [Range(
        10.00, 10000.00, 
        ErrorMessage = "'{0}' must be between [{1} - {2}]")]
    [Display(Name = "Ticket price")]
    public decimal? TicketPrice { get; set; }
}

The model looks less complicated, and easier to read. Notice that we have defined TravelDate and TicketPrice as nullable properties by adding ( ? ) to the defintion in order to trigger [Required] attribute validation. see Override Required Validation Attribute Error Message for more details about this issue.

Next we add the localized error messages to ViewResource.xxx.resx files, then we run the app o see the results.

arabic localized form labels

turkish localized form labels

english localized form labels

Next : Localizing ModelBinding Error Messages

Demo Project: My trips application

Source code on github: MyTrips