19

So far I have been able to translate everything in an ASP.Net Core 2.1 Web Application.

It has proven a little challenge, since the scaffolded Account Pages needed a little setup.

But what I cannot find is a way to translate the Password validation messages. Also, translating the model binding messages were a little challenge (thanks stackoverflow).

Any ideas?

I include the relevant parts of my Startup.cs file:

public void ConfigureServices(IServiceCollection services)
{
     ...

     services.AddMvc(options =>
     {
         var type = typeof(SharedResources);
         var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
         var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
         var L = factory.Create("SharedResources", assemblyName.Name);

         options.ModelBindingMessageProvider.SetValueIsInvalidAccessor(x => L["The value '{0}' is invalid.", x]);
         options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(x => L["The value '{0}' is invalid.", x]);
         options.ModelBindingMessageProvider.SetValueMustBeANumberAccessor(x => L["The field {0} must be a number.", x]);
         options.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor(x => L["A value for the '{0}' property was not provided.", x]);
         options.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor((x, y) => L["The value '{0}' is not valid for {1}.", x, y]);
         options.ModelBindingMessageProvider.SetMissingKeyOrValueAccessor(() => L["A value is required."]);
         options.ModelBindingMessageProvider.SetUnknownValueIsInvalidAccessor(x => L["The supplied value is invalid for {0}.", x]);
         options.ModelBindingMessageProvider.SetMissingRequestBodyRequiredValueAccessor(() => L["A non-empty request body is required."]);
         options.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor(x => L["The value '{0}' is not valid.", x]);
         options.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor(() => L["The supplied value is invalid."]);
         options.ModelBindingMessageProvider.SetNonPropertyValueMustBeANumberAccessor(() => L["NonPropertyValueMustBeNumber"]);
     })
     .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
     .AddDataAnnotationsLocalization(options =>
        {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
            {
                // This is for Account scaffolded pages data annotations
                return factory.Create(typeof(SharedResources));
            };
        });

     ...

}

I cannot put something like this in the InputModel of Register.cshtml.cs because the ErrorMessage gets ignored (and I won't do it anyways, since I don't want to hardcode the password policy description):

        [Required(ErrorMessage = "The {0} field is required.")]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password, ErrorMessage = "Password must be nice")]
        [Display(Name = "Password")]
        public string Password { get; set; }

Screenshot of translated registration page

Kirk Larkin
  • 84,915
  • 16
  • 214
  • 203
Samo
  • 2,093
  • 1
  • 17
  • 18

3 Answers3

28

This can be done by localizing identity error messages, there are 22 message that has to be localized.

First, create a shared resource file "its keys defined with public access modifier" and type all error messages with localized versions as in the below image:

enter image description here

then create a new class that implements IdentityErrorDescriber and override all default messages with reference to the shared resource file; in this sample the shared resource file name is LocalizedIdentityErrorMessages:

public class LocalizedIdentityErrorDescriber : IdentityErrorDescriber
    {
        public override IdentityError DuplicateEmail(string email)
        {
            return new IdentityError
            {
                Code = nameof(DuplicateEmail),
                Description = string.Format(LocalizedIdentityErrorMessages.DuplicateEmail, email)
            };
        }

        public override IdentityError DuplicateUserName(string userName)
        {
            return new IdentityError
            {
                Code = nameof(DuplicateUserName),
                Description = string.Format(LocalizedIdentityErrorMessages.DuplicateUserName, userName)
            };
        }

        public override IdentityError InvalidEmail(string email)
        {
            return new IdentityError
            {
                Code = nameof(InvalidEmail),
                Description = string.Format(LocalizedIdentityErrorMessages.InvalidEmail, email)
            };
        }

        public override IdentityError DuplicateRoleName(string role)
        {
            return new IdentityError
            {
                Code = nameof(DuplicateRoleName),
                Description = string.Format(LocalizedIdentityErrorMessages.DuplicateRoleName, role)
            };
        }

        public override IdentityError InvalidRoleName(string role)
        {
            return new IdentityError
            {
                Code = nameof(InvalidRoleName),
                Description = string.Format(LocalizedIdentityErrorMessages.InvalidRoleName, role)
            };
        }

        public override IdentityError InvalidToken()
        {
            return new IdentityError
            {
                Code = nameof(InvalidToken),
                Description = LocalizedIdentityErrorMessages.InvalidToken
            };
        }

        public override IdentityError InvalidUserName(string userName)
        {
            return new IdentityError
            {
                Code = nameof(InvalidUserName),
                Description = string.Format(LocalizedIdentityErrorMessages.InvalidUserName, userName)
            };
        }

        public override IdentityError LoginAlreadyAssociated()
        {
            return new IdentityError
            {
                Code = nameof(LoginAlreadyAssociated),
                Description = LocalizedIdentityErrorMessages.LoginAlreadyAssociated
            };
        }

        public override IdentityError PasswordMismatch()
        {
            return new IdentityError
            {
                Code = nameof(PasswordMismatch),
                Description = LocalizedIdentityErrorMessages.PasswordMismatch
            };
        }

        public override IdentityError PasswordRequiresDigit()
        {
            return new IdentityError
            {
                Code = nameof(PasswordRequiresDigit),
                Description = LocalizedIdentityErrorMessages.PasswordRequiresDigit
            };
        }

        public override IdentityError PasswordRequiresLower()
        {
            return new IdentityError
            {
                Code = nameof(PasswordRequiresLower),
                Description = LocalizedIdentityErrorMessages.PasswordRequiresLower
            };
        }

        public override IdentityError PasswordRequiresNonAlphanumeric()
        {
            return new IdentityError
            {
                Code = nameof(PasswordRequiresNonAlphanumeric),
                Description = LocalizedIdentityErrorMessages.PasswordRequiresNonAlphanumeric
            };
        }

        public override IdentityError PasswordRequiresUniqueChars(int uniqueChars)
        {
            return new IdentityError
            {
                Code = nameof(PasswordRequiresUniqueChars),
                Description = string.Format(LocalizedIdentityErrorMessages.PasswordRequiresUniqueChars, uniqueChars)
            };
        }

        public override IdentityError PasswordRequiresUpper()
        {
            return new IdentityError
            {
                Code = nameof(PasswordRequiresUpper),
                Description = LocalizedIdentityErrorMessages.PasswordRequiresUpper
            };
        }

        public override IdentityError PasswordTooShort(int length)
        {
            return new IdentityError
            {
                Code = nameof(PasswordTooShort),
                Description = string.Format(LocalizedIdentityErrorMessages.PasswordTooShort, length)
            };
        }

        public override IdentityError UserAlreadyHasPassword()
        {
            return new IdentityError
            {
                Code = nameof(UserAlreadyHasPassword),
                Description = LocalizedIdentityErrorMessages.UserAlreadyHasPassword
            };
        }

        public override IdentityError UserAlreadyInRole(string role)
        {
            return new IdentityError
            {
                Code = nameof(UserAlreadyInRole),
                Description = string.Format(LocalizedIdentityErrorMessages.UserAlreadyInRole, role)
            };
        }

        public override IdentityError UserNotInRole(string role)
        {
            return new IdentityError
            {
                Code = nameof(UserNotInRole),
                Description = string.Format(LocalizedIdentityErrorMessages.UserNotInRole, role)
            };
        }

        public override IdentityError UserLockoutNotEnabled()
        {
            return new IdentityError
            {
                Code = nameof(UserLockoutNotEnabled),
                Description = LocalizedIdentityErrorMessages.UserLockoutNotEnabled
            };
        }

        public override IdentityError RecoveryCodeRedemptionFailed()
        {
            return new IdentityError
            {
                Code = nameof(RecoveryCodeRedemptionFailed),
                Description = LocalizedIdentityErrorMessages.RecoveryCodeRedemptionFailed
            };
        }

        public override IdentityError ConcurrencyFailure()
        {
            return new IdentityError
            {
                Code = nameof(ConcurrencyFailure),
                Description = LocalizedIdentityErrorMessages.ConcurrencyFailure
            };
        }

        public override IdentityError DefaultError()
        {
            return new IdentityError
            {
                Code = nameof(DefaultError),
                Description = LocalizedIdentityErrorMessages.DefaultIdentityError
            };
        }
    }

finally, add the localized error describer to identity setup under ConfigureServices method in startup class:

services.AddIdentity<AppUser, AppRole>()
        // localize identity error messages
        .AddErrorDescriber<LocalizedIdentityErrorDescriber>()
        .AddEntityFrameworkStores()
        .AddDefaultTokenProviders();

resource: http://www.ziyad.info/en/articles/20-Localizing_Identity_Error_Messages

Additionally you may need to read the step-by-step localization articles: http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application

UPDATE - Dec. 2020

Recently I've developed a new nuget package (XLocalizer), it simplifies the localization setup of Asp.Net Core web apps, it supports auto online translation and auto resource creating. Additionaly, all identity errors, model binding errors and validation errors can be customized easily in a json file.

References:

LazZiya
  • 5,286
  • 2
  • 24
  • 37
  • 4
    Great answer. I obtained the original resource messages file from Identity's github: https://github.com/aspnet/Identity/blob/master/src/Core/Resources.resx – Samo Dec 03 '18 at 06:45
  • 1
    Thank you Laz Ziya. It's worth noting that he made a Nuget package to solve this and teaches us how to use it at https://medium.com/swlh/step-by-step-tutorial-to-build-multi-cultural-asp-net-core-web-app-3fac9a960c43. He also made working examples at https://github.com/LazZiya/ExpressLocalizationSampleCore3. – DavidC Jul 15 '20 at 21:29
  • How do you create a resource file that looks like that? Mine only has 3 columns (Name, Value and Comment) – Nemavhidi Mar 10 '22 at 22:49
  • 1
    @Nemavhidi do you mean the one in the picure above? I was using [ResXManager](https://marketplace.visualstudio.com/items?itemName=TomEnglert.ResXManager) – LazZiya Mar 11 '22 at 10:48
  • @LazZiya, Thank you, I will look into it. – Nemavhidi Mar 11 '22 at 13:09
  • 1
    XLocalizer did it for me. + i'm not using `services.AddIdentity()` so this was the good way for me ! :) – z4nzi May 18 '22 at 15:45
  • There's an open issue to provide common translations as in .NET Framework https://github.com/dotnet/aspnetcore/issues/5724 – Benjamin Freitag Jun 08 '23 at 06:29
9

These error messages are generated using IdentityErrorDescriber. Here's a sample of what the class itself looks like:

public class IdentityErrorDescriber
{
    ...

    public virtual IdentityError PasswordTooShort(int length)
    {
        return new IdentityError
        {
            Code = nameof(PasswordTooShort),
            Description = Resources.FormatPasswordTooShort(length)
        };
    }


    ...
}

To customise a specific message, create your own IdentityErrorDescriber implementation. Here's an example:

public class MyIdentityErrorDescriber : IdentityErrorDescriber
{    
    public override IdentityError PasswordTooShort(int length)
    {
        return new IdentityError
        {
            Code = nameof(PasswordTooShort),
            Description = "Your description goes here."
        };
    }
}

To use this new implementation, add it to the DI container in Startup.ConfigureServices:

services.AddScoped<IdentityErrorDescriber, MyIdentityErrorDescriber>();
Kirk Larkin
  • 84,915
  • 16
  • 214
  • 203
0

You can use ViewModel and than you can just use DataAnnotations here is how Controller, ViewModel and View looks in that case (translation is Croatian).

Controller

    [HttpGet]
    public IActionResult Login()
    {
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (ModelState.IsValid)
        {
            var result = await signInManager.PasswordSignInAsync(
                model.Email, model.Password, model.RememberMe, false);

            if (result.Succeeded)
            {
                return RedirectToAction("index", "home");
            }

            ModelState.AddModelError(string.Empty, "Neuspješni pokušaj");
        }

        return View(model);
    }

ViewModel

Under ErrorMessage put your error message

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace MyApplication.ViewModels
{
    public class LoginViewModel
    {
        [Required(ErrorMessage = "Email je obavezan")]
        [EmailAddress]
        public string Email { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Lozinka")]
        [Required(ErrorMessage = "Lozinka je obavezna")]
        public string Password { get; set; }

        [Display(Name = "Zapamti me")]
        public bool RememberMe { get; set; }
    }
}

Login View

@model LoginViewModel

@{
    ViewBag.Title = "Prijava";
}

<h2>Prijava</h2>

<div class="row">
    <div class="col-md-12">
        <form method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <div class="checkbox">
                    <label asp-for="RememberMe">
                        <input asp-for="RememberMe" />
                        @Html.DisplayNameFor(m => m.RememberMe)
                    </label>
                </div>
            </div>
            <button type="submit" class="btn btn-primary">Prijava</button>
        </form>
    </div>
</div>
Hrvoje
  • 13,566
  • 7
  • 90
  • 104