Выбор локализации.
Прямо "из коробки" доступны переводы сообщений об ошибках на разных языках для встроенных валидаторов. Перевод подбирается на основе значений глобальных свойств 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");
← Предыдущая часть