Обзор библиотеки FluentValidation. Часть 4. Сообщения об ошибках. Локализация

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Выбор локализации.

Прямо "из коробки" доступны переводы сообщений об ошибках на разных языках для встроенных валидаторов. Перевод подбирается на основе значений глобальных свойств ValidatorOptions.Global.LanguageManager.Culture (в первую очередь, если указано) и CultureInfo.CurrentUICulture (во вторую очередь).

Чтобы выбрать русскую локаль, нужно установить следующее значение для CultureInfo.CurrentUICulture (значение для ValidatorOptions.Global.LanguageManager.Culture не указано) :

// Модель клиента
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."
}

Теперь выбираем локаль на основе 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)
}

Обратите внимание, что установленное значение в ValidatorOptions.Global.LanguageManager.Culture "перебивает" установленные значения в CultureInfo.CurrentUICulture (для библиотеки FluentValidation)

Отключение локализации.

Чтобы отключить выбор локализации на основе значений свойств ValidatorOptions.Global.LanguageManager.Culture и CultureInfo.CurrentUICulture, необходимо задать в настройках значение false для глобального свойства 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."
}

Везде будет англоязычная локаль.

Кастомная локализация для сообщений об ошибках по умолчанию.

Чтобы заменить текст сообщений (частично либо полностью), необходимо реализовать интерфейс ILanguageManager. Например, для валидатора NotNull выводится сообщение по умолчанию "'{PropertyName}' must not be empty.". Чтобы заменить это сообщение во всех местах где используется NotNull валидатор, нужно написать кастомный LanguageManager:

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

Чтобы применить кастомную реализацию, нужно установить значение в настройках для глобального свойства 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.
}

А вот так выглядит уже реализованный LanguageManager для русской локали:

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,
  };
}

Локализация кастомных сообщений об ошибках через IStringLocalizer.

Тип IStringLocalizer можно найти в пакете Microsoft.Extensions.Localization

Предполагается, что вы уже знакомы как с ним работать, т. к. он не является частью библиотеки FluentValidation.

Структура проекта:

Содержимое файла CustomerValidator.en.resx:

Содержимое файла CustomerValidator.ru.resx:

Выполняем:

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)
}

Решение возможных проблем с локализацией.

В прошлой части был комментарий: https://habr.com/ru/articles/798961/#comment_26593563

Мы хотим выводить даты в формате en локали в сообщениях об ошибках, пробуем известными способами:

// Модель клиента
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)
}

Оба варианта не дали нам форматирование, которое должно быть при локали en.

Чтобы исправить эту проблему, нужно использовать глобальное свойство 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' должно быть пустым.
}

Обратите внимание, что локаль для сообщений об ошибках осталась ru-RU. Чтобы полностью переключить на нужную локаль, нужно дополнительно прописать:

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

или

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

← Предыдущая часть

Источник: https://habr.com/ru/articles/799109/


Интересные статьи

Интересные статьи

Привет, Хабр! На связи всё ещё Настя Московкина, руководитель Отдела анализа в приложении «Кошелёк». В предыдущей статье мы по косточкам разобрали процесс подготовки к UX тестированию своими силами, а...
Оглавление: Уроки компьютерного зрения. Оглавление / Хабр (habr.com)Начиная с этого урока, я буду рассказывать о компьютерном зрении на примере моего пэт-проекта. Для начала, что это будет за проект. ...
Нас в команде всего четверо. Нам нужно всего лишь раз в день слышать друг друга и иметь возможность показывать экран. Никаких специфических требований. Не нужно корпоративной авторизации, не нужно дер...
Всем привет! Сегодня хочеться рассказать про такую интересную вещь в 3D как шейдера. Спойлер - это будет небольшая статья, здесь не будет много теории, в основном будем рассматривать написание шейдеро...
«— Скажите, пожалуйста, куда мне отсюда идти? — А куда ты хочешь попасть? — ответил Кот. — Мне все равно… — сказала Алиса. — Тогда все равно, куда и идти, — заметил Кот.» (С) «Алиса в стране ...