Overview of the FluentValidation library. Part 4. Error messages. Localization

Localization selection.

Translations of error messages in different languages ​​for built-in validators are available right out of the box. Translation is selected based on global property values ValidatorOptions.Global.LanguageManager.Culture (first if specified) and CultureInfo.CurrentUICulture (in second place).

To select Russian locale, you need to set the following value for CultureInfo.CurrentUICulture (value for ValidatorOptions.Global.LanguageManager.Culture not indicated) :

// Модель клиента
public class Customer
{
  // Фамилия
  public string? Surname { get; set; }
}

// Валидатор для модели клиента
public class CustomerValidator : AbstractValidator<Customer>
{
  public CustomerValidator()
  {
    RuleFor(customer => customer.Surname)
      .NotNull();
  }
}

static void Main(string[] args)
{
  // ru-RU - культура-субкультура, формат RFC 4646
  CultureInfo.CurrentUICulture = new CultureInfo("ru-RU");

  // Валидируем, как обычно
  var customer = new Customer { Surname = null };
  var validator = new CustomerValidator();
  var result = validator.Validate(customer);

  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет > "'Surname' должно быть заполнено."

  // Меняем на англоязычную культуру
  // en-US - культура-субкультура, формат RFC 4646
  CultureInfo.CurrentUICulture = new CultureInfo("en-US");
  
  // Валидируем, как обычно
  result = validator.Validate(customer);
  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет > "'Surname' must not be empty."
}

Now we select the locale based on ValidatorOptions.Global.LanguageManager.Culture:

// ... тот же код что выше

static void Main(string[] args)
{
  // ru-RU - культура-субкультура, формат RFC 4646
  ValidatorOptions.Global.LanguageManager.Culture = new CultureInfo("ru-RU");
  // fr - двузначный код ISO 639 в нижнем регистре
  CultureInfo.CurrentUICulture = new CultureInfo("fr");

  // Валидируем, как обычно
  var customer = new Customer { Surname = null };
  var validator = new CustomerValidator();
  var result = validator.Validate(customer);

  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет > "'Surname' должно быть заполнено." (использовалась культура ru-RU)

  // en-US - культура-субкультура, формат RFC 4646
  CultureInfo.CurrentUICulture = new CultureInfo("en-US");

  // Валидируем, как обычно
  result = validator.Validate(customer);
  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет > "'Surname' должно быть заполнено." (использовалась культура ru-RU)
}

Please note that the set value in ValidatorOptions.Global.LanguageManager.Culture “interrupts” the set values ​​in CultureInfo.CurrentUICulture (for library FluentValidation)

Disable localization.

To disable localization selection based on property values ValidatorOptions.Global.LanguageManager.Culture And CultureInfo.CurrentUICultureyou need to set the value in the settings false for global property ValidatorOptions.Global.LanguageManager.Enabled:

// ... тот же код что выше

static void Main(string[] args)
{
  // Отключаем выбор локализации на основе
  // значения ValidatorOptions.Global.LanguageManager.Culture
  // и CultureInfo.CurrentUICulture
  ValidatorOptions.Global.LanguageManager.Enabled = false;
  // ru-RU - культура-субкультура
  CultureInfo.CurrentUICulture = new CultureInfo("ru-RU");

  // Валидируем, как обычно
  var customer = new Customer { Surname = null };
  var validator = new CustomerValidator();
  var result = validator.Validate(customer);

  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет > "'Surname' must not be empty."

  // Меняем на англоязычную культуру
  // en-US - культура-субкультура
  CultureInfo.CurrentUICulture = new CultureInfo("en-US");

  // Валидируем, как обычно
  result = validator.Validate(customer);
  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет > "'Surname' must not be empty."
}

Everywhere there will be an English-language locale.

Custom localization for default error messages.

To replace the message text (partially or completely), you need to implement the interface ILanguageManager. For example, for a validator NotNull default message is displayed "'{PropertyName}' must not be empty.". To replace this message in all places where it is used NotNull validator, you need to write a custom one LanguageManager:

public class CustomRussianLanguageManager : FluentValidation.Resources.LanguageManager
{
  public CustomRussianLanguageManager()
  {
    // ru-RU - локаль для которой переопределяем перевод
    // NotNullValidator - название валидатора свойства для которого переопределяем перевод (PropertyValidator)
    // Последний параметр - текст выводимого сообщения об ошибке
    AddTranslation("ru-RU", "NotNullValidator", "Свойство '{PropertyName}' очень обязательно к заполнению.");
  }
}

To apply a custom implementation, you need to set the value in the settings for the global property ValidatorOptions.Global.LanguageManager:

// Модель клиента
public class Customer
{
  public string? Surname { get; set; }
  public string? Forename { get; set; }
}

// Валидатор для модели клиента
public class CustomerValidator : AbstractValidator<Customer>
{
  public CustomerValidator()
  {
    RuleFor(customer => customer.Surname)
      .NotNull();

    RuleFor(customer => customer.Forename)
      .Null();
  }
}

static void Main(string[] args)
{
  // Устанавливаем кастомный LanguageManager
  ValidatorOptions.Global.LanguageManager = new CustomRussianLanguageManager();
  CultureInfo.CurrentUICulture = new CultureInfo("ru-RU");

  // Валидируем, как обычно
  var customer = new Customer { Surname = null, Forename = "значение" };
  var validator = new CustomerValidator();
  var result = validator.Validate(customer);

  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет >
  // Свойство 'Surname' очень обязательно к заполнению.
  // 'Forename' должно быть пустым.

  // Меняем локаль на en-US
  CultureInfo.CurrentUICulture = new CultureInfo("en-US");

  // Валидируем как обычно
  result = validator.Validate(customer);
  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет >
  // 'Surname' must not be empty.
  // 'Forename' must be empty.
}

And this is what it looks like already implemented LanguageManager for Russian locale:

internal class RussianLanguage
{
  public const string Culture = "ru";

  public static string GetTranslation(string key) => key switch
  {
      "EmailValidator" => "'{PropertyName}' неверный email адрес.",
      "GreaterThanOrEqualValidator" => "'{PropertyName}' должно быть больше или равно '{ComparisonValue}'.",
      "GreaterThanValidator" => "'{PropertyName}' должно быть больше '{ComparisonValue}'.",
      "LengthValidator" => "'{PropertyName}' должно быть длиной от {MinLength} до {MaxLength} символов. Количество введенных символов: {TotalLength}.",
      "MinimumLengthValidator" => "'{PropertyName}' должно быть длиной не менее {MinLength} символов. Количество введенных символов: {TotalLength}.",
      "MaximumLengthValidator" => "'{PropertyName}' должно быть длиной не более {MaxLength} символов. Количество введенных символов: {TotalLength}.",
      "LessThanOrEqualValidator" => "'{PropertyName}' должно быть меньше или равно '{ComparisonValue}'.",
      "LessThanValidator" => "'{PropertyName}' должно быть меньше '{ComparisonValue}'.",
      "NotEmptyValidator" => "'{PropertyName}' должно быть заполнено.",
      "NotEqualValidator" => "'{PropertyName}' не должно быть равно '{ComparisonValue}'.",
      "NotNullValidator" => "'{PropertyName}' должно быть заполнено.",
      "PredicateValidator" => "Не выполнено указанное условие для '{PropertyName}'.",
      "AsyncPredicateValidator" => "Не выполнено указанное условие для '{PropertyName}'.",
      "RegularExpressionValidator" => "'{PropertyName}' имеет неверный формат.",
      "EqualValidator" => "'{PropertyName}' должно быть равно '{ComparisonValue}'.",
      "ExactLengthValidator" => "'{PropertyName}' должно быть длиной {MaxLength} символа(ов). Количество введенных символов: {TotalLength}.",
      "InclusiveBetweenValidator" => "'{PropertyName}' должно быть в диапазоне от {From} до {To}. Введенное значение: {PropertyValue}.",
      "ExclusiveBetweenValidator" => "'{PropertyName}' должно быть в диапазоне от {From} до {To} (не включая эти значения). Введенное значение: {PropertyValue}.",
      "CreditCardValidator" => "'{PropertyName}' неверный номер карты.",
      "ScalePrecisionValidator" => "'{PropertyName}' должно содержать не более {ExpectedPrecision} цифр всего, в том числе {ExpectedScale} десятичных знака(ов). Введенное значение содержит {Digits} цифр(ы) в целой части и {ActualScale} десятичных знака(ов).",
      "EmptyValidator" => "'{PropertyName}' должно быть пустым.",
      "NullValidator" => "'{PropertyName}' должно быть пустым.",
      "EnumValidator" => "'{PropertyName}' содержит недопустимое значение '{PropertyValue}'.",
      // Additional fallback messages used by clientside validation integration.
      "Length_Simple" => "'{PropertyName}' должно быть длиной от {MinLength} до {MaxLength} символов.",
      "MinimumLength_Simple" => "'{PropertyName}' должно быть длиной не менее {MinLength} символов.",
      "MaximumLength_Simple" => "'{PropertyName}' должно быть длиной не более {MaxLength} символов.",
      "ExactLength_Simple" => "'{PropertyName}' должно быть длиной {MaxLength} символа(ов).",
      "InclusiveBetween_Simple" => "'{PropertyName}' должно быть в диапазоне от {From} до {To}.",

      _ => null,
  };
}

Localization of custom error messages using IStringLocalizer.

Type IStringLocalizer can be found in the package Microsoft.Extensions.Localization

It is assumed that you are already familiar with how to work with it, since it is not part of the library FluentValidation.

Project structure:

File contents CustomerValidator.en.resx:

File contents CustomerValidator.ru.resx:

We do:

using FluentValidation;
using FluentValidationTests.Models;
using FluentValidationTests.Validators;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Globalization;

// Модель клиента
public class Customer
{
  // Фамилия
  public string? Surname { get; set; }
}

// Валидатор для модели клиента
public class CustomerValidator : AbstractValidator<Customer>
{
  public CustomerValidator(IStringLocalizer<CustomerValidator> localizer)
  {
    // Используем IStringLocalizer<CustomerValidator> внутри метода WithMessage
    // SurnameNotNull - это ключ, по которому можем найти нужное значение
    // в файле ресурсов
    RuleFor(customer => customer.Surname)
      .NotNull()
        .WithMessage(localizer["SurnameNotNull"]);
  }
}

static void Main(string[] args)
{
  // Устанавливаем культуру "en", на её основе подбирается нужный
  // файл ресурсов из папки Resources
  CultureInfo.CurrentUICulture = new CultureInfo("en");

  // Регистрируем IStringLocalizer (сделано через DI)
  var services = new ServiceCollection();

  services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
  services.AddLocalization(opts => opts.ResourcesPath = "Resources");

  var provider = services.BuildServiceProvider();

  // Получаем из контейнера DI IStringLocalizer для CustomerValidator
  var localizer = provider.GetRequiredService<IStringLocalizer<CustomerValidator>>();

  // Валидируем, как обычно
  var customer = new Customer { Surname = null };
  // Передаём IStringLocalizer в валидатор
  var validator = new CustomerValidator(localizer);
  var result = validator.Validate(customer);

  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выведет > "Английский язык, название свойства 'Surname'"
  // из файла (/Resources/Validators/CustomerValidator.en.resx)
}

Solving possible localization problems.

There was a comment in the last part: https://habr.com/ru/articles/798961/#comment_26593563

We want to display dates in the format en locale in error messages, try using known methods:

// Модель клиента
public class Customer
{
  public string? Surname { get; set; }
}

// Валидатора для модели клиента
public class CustomerValidator : AbstractValidator<Customer>
{
  public CustomerValidator()
  {
    // Возвращаем текущую дату и время по UTC в сообщении об ошибке
    RuleFor(customer => customer.Surname)
      .NotNull()
        .WithMessage($"{DateTime.UtcNow}");
  }
}

static void Main(string[] args)
{
  // Пробуем первым способом указать локаль
  CultureInfo.CurrentUICulture = new CultureInfo("en");

  // Валидируем как обычно
  var customer = new Customer { Surname = null };
  var validator = new CustomerValidator();
  var result = validator.Validate(customer);

  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выводит > "7.03.2024 5:31:55" (локаль ru-RU)

  // Пробуем вторым способом указать локаль
  ValidatorOptions.Global.LanguageManager.Culture = new CultureInfo("en");
  
  result = validator.Validate(customer);
  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выводит > "7.03.2024 5:31:55" (локаль ru-RU)
}

Both options did not give us the formatting that the locale should have en.

To fix this problem, you need to use the global property CultureInfo.CurrentCulture:

public class Customer
{
  public string? Surname { get; set; }
  public string? Forename { get; set; }
}

public class CustomerValidator : AbstractValidator<Customer>
{
  public CustomerValidator()
  {
    RuleFor(customer => customer.Surname)
      .NotNull()
        .WithMessage($"{DateTime.UtcNow}");

    RuleFor(customer => customer.Forename)
      .Null();
  }
}

static void Main(string[] args)
{
  CultureInfo.CurrentCulture = new CultureInfo("en");

  var customer = new Customer { Surname = null, Forename = "значение" };
  var validator = new CustomerValidator();
  var result = validator.Validate(customer);

  Console.WriteLine(result.ToString(Environment.NewLine));
  // Выводит >
  // 3/7/2024 5:40:26 AM
  // 'Forename' должно быть пустым.
}

Please note that the locale for error messages remains ru-RU. To completely switch to the desired locale, you need to additionally write:

CultureInfo.CurrentUICulture = new CultureInfo("en");

or

ValidatorOptions.Global.LanguageManager.Culture = new CultureInfo("en");

← Previous part

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *