Многоязычность

Валидационные сообщения в Vest строятся вокруг принципа декларативных правил, где каждое правило может возвращать текст ошибки. Поддержка многоязычности в такой модели не сводится к простой подмене строк — она требует архитектурного разделения логики валидации и слоя представления сообщений.

Основная проблема интернационализации валидации заключается в том, что сообщения об ошибках часто оказываются «зашиты» в бизнес-логику правил. В Vest это решается через функцию test, где сообщение может формироваться динамически, а также через внешние словари локализации.

Базовый подход строится на разделении:

  • ключей ошибок (стабильные идентификаторы)
  • словарей переводов (i18n-слой)
  • контекста параметров ошибки (динамические данные)

Такой подход позволяет избегать дублирования логики и упрощает добавление новых языков без изменения самих правил.

Использование ключей вместо строк

Ключи ошибок являются центральным элементом интернационализации. Вместо возврата готового текста правило возвращает идентификатор:

import { test, enforce } from "vest";

test("username", "required", () => {
  enforce(data.username).isNotEmpty();
});

Здесь "required" — это не текст, а код ошибки. На уровне интерфейса этот код преобразуется в строку на нужном языке.

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

Слой локализации

Слой локализации представляет собой словарь, сопоставляющий ключи и строки:

const messages = {
  en: {
    required: "Field is required",
    min_length: "Minimum length is not met"
  },
  ru: {
    required: "Поле обязательно для заполнения",
    min_length: "Не достигнута минимальная длина"
  }
};

Выбор языка выполняется на уровне отображения результатов валидации:

function translate(error, lang = "en") {
  return messages[lang][error.key] || error.key;
}

Такой механизм позволяет централизованно управлять всеми текстами без вмешательства в правила.

Динамические параметры сообщений

Часто ошибки требуют контекстных данных: минимальной длины, допустимого диапазона или значения ограничения. В этом случае используется параметризация.

test("password", "min_length", () => {
  enforce(data.password).longerThan(8);
});

Ошибка может содержать метаданные:

{
  key: "min_length",
  meta: {
    min: 8
  }
}

Локализационный слой использует эти данные:

const messages = {
  ru: {
    min_length: ({ min }) => `Минимальная длина — ${min} символов`
  }
};

Функциональный формат позволяет строить гибкие сообщения без усложнения логики правил.

Контекст языка на уровне формы

В приложениях часто требуется поддержка нескольких языков одновременно, например в многошаговых формах или административных панелях. В таком случае язык передаётся как часть контекста обработки результатов.

function formatErrors(errors, lang) {
  return errors.map(err => ({
    field: err.field,
    message: messages[lang][err.key](err.meta)
  }));
}

Такой подход делает систему независимой от глобальных настроек и позволяет отображать разные языки для разных пользователей одновременно.

Переиспользование словарей между проектами

При масштабировании системы валидации словари переводов выделяются в отдельные модули. Это позволяет использовать одинаковые ключи ошибок в разных приложениях:

export const validationDictionary = {
  required: "required",
  min_length: "min_length",
  email: "email_invalid"
};

Правила начинают ссылаться только на ключи:

test("email", validationDictionary.email, () => {
  enforce(data.email).matches(/.+@.+\..+/);
});

Это устраняет зависимость от конкретного языка и упрощает поддержку библиотек.

Композиция языковых наборов

При наличии большого количества модулей валидации словари разбиваются на домены:

  • общие ошибки (required, invalid)
  • ошибки формы (email, password)
  • бизнес-правила (age restriction, permissions)

Объединение выполняется на уровне конфигурации приложения:

const messages = {
  ru: {
    ...commonRu,
    ...formRu,
    ...businessRu
  }
};

Такая композиция уменьшает связность и облегчает расширение системы.

Поддержка форматов множественного числа

В некоторых языках требуется различная форма слов в зависимости от числа. Это решается через функции форматирования:

const pluralize = (count) =>
  count === 1 ? "символ" : "символов";

Использование в сообщениях:

min_length: ({ min }) => `Минимум ${min} ${pluralize(min)}`

Для сложных языков подключаются специализированные библиотеки ICU-форматирования.

Ленивое вычисление переводов

Перевод сообщений часто откладывается до момента отображения, а не выполняется в момент валидации. Это позволяет:

  • хранить ошибки в нейтральном виде
  • менять язык без повторной валидации
  • кэшировать результаты проверки

Структура ошибки остаётся неизменной:

{
  key: "required",
  field: "username"
}

Локализация применяется только на этапе рендера.

Интеграция с внешними i18n-системами

В крупных приложениях система сообщений интегрируется с библиотеками интернационализации, такими как ICU MessageFormat-подобные решения. В этом случае ключ ошибки становится частью общего пространства переводов приложения.

intl.formatMessage({ id: error.key }, error.meta);

Это позволяет унифицировать валидацию и интерфейсные сообщения в одной системе локализации.

Разделение логики и текстов как основа масштабирования

При росте количества форм и правил становится критичным отсутствие текстов внутри валидаторов. Любое включение строк напрямую в правила приводит к:

  • дублированию переводов
  • невозможности централизованного обновления текста
  • сложностям тестирования

Поэтому в архитектуре Vest многоязычность строится вокруг неизменяемых ключей и внешних словарей, а не вокруг строковых сообщений внутри тестов.