Локализация сообщений

Библиотека Yup позволяет централизованно управлять текстами ошибок валидации. Локализация особенно важна в крупных приложениях, где:

  • интерфейс поддерживает несколько языков;
  • сообщения должны соответствовать терминологии продукта;
  • требуется единый стиль ошибок;
  • необходимо разделять техническую и пользовательскую формулировки.

По умолчанию Yup возвращает сообщения на английском языке:

import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().email().required(),
});

Результат:

email must be a valid email

или:

email is a required field

Для полноценной локализации используется механизм setLocale.


Метод setLocale

Метод setLocale() изменяет глобальные сообщения библиотеки.

import * as yup from 'yup';

yup.setLocale({
  mixed: {
    required: 'Поле обязательно',
  },
});

Теперь все схемы будут использовать новое сообщение:

const schema = yup.object({
  name: yup.string().required(),
});

Ошибка:

Поле обязательно

Структура локализации

Объект локализации делится по типам схем:

yup.setLocale({
  mixed: {},
  string: {},
  number: {},
  date: {},
  boolean: {},
  array: {},
  object: {},
});

Каждый раздел содержит набор правил валидации.


Раздел mixed

mixed содержит общие сообщения, применимые ко всем типам данных.

required

yup.setLocale({
  mixed: {
    required: 'Это поле обязательно',
  },
});

Пример:

const schema = yup.string().required();

oneOf

Используется при ограничении допустимых значений.

yup.setLocale({
  mixed: {
    oneOf: 'Недопустимое значение',
  },
});

Пример:

const schema = yup
  .string()
  .oneOf(['admin', 'user']);

notOneOf

yup.setLocale({
  mixed: {
    notOneOf: 'Значение запрещено',
  },
});

defined

yup.setLocale({
  mixed: {
    defined: 'Поле должно быть определено',
  },
});

default

Сообщение по умолчанию для неизвестных ошибок.

yup.setLocale({
  mixed: {
    default: 'Некорректное значение',
  },
});

Локализация string

Раздел string отвечает за строковые значения.


min

yup.setLocale({
  string: {
    min: 'Минимум ${min} символов',
  },
});

Пример:

const schema = yup.string().min(5);

Ошибка:

Минимум 5 символов

max

yup.setLocale({
  string: {
    max: 'Максимум ${max} символов',
  },
});

email

yup.setLocale({
  string: {
    email: 'Некорректный email',
  },
});

url

yup.setLocale({
  string: {
    url: 'Некорректный URL',
  },
});

matches

yup.setLocale({
  string: {
    matches: 'Неверный формат',
  },
});

length

yup.setLocale({
  string: {
    length: 'Длина должна быть ${length} символов',
  },
});

lowercase и uppercase

yup.setLocale({
  string: {
    lowercase: 'Только строчные буквы',
    uppercase: 'Только заглавные буквы',
  },
});

trim

yup.setLocale({
  string: {
    trim: 'Уберите пробелы в начале и конце',
  },
});

Локализация number


min и max

yup.setLocale({
  number: {
    min: 'Минимальное значение: ${min}',
    max: 'Максимальное значение: ${max}',
  },
});

integer

yup.setLocale({
  number: {
    integer: 'Требуется целое число',
  },
});

positive и negative

yup.setLocale({
  number: {
    positive: 'Число должно быть положительным',
    negative: 'Число должно быть отрицательным',
  },
});

lessThan и moreThan

yup.setLocale({
  number: {
    lessThan: 'Значение должно быть меньше ${less}',
    moreThan: 'Значение должно быть больше ${more}',
  },
});

Локализация date


min

yup.setLocale({
  date: {
    min: 'Дата должна быть позже ${min}',
  },
});

max

yup.setLocale({
  date: {
    max: 'Дата должна быть раньше ${max}',
  },
});

Локализация array


min

yup.setLocale({
  array: {
    min: 'Минимум элементов: ${min}',
  },
});

max

yup.setLocale({
  array: {
    max: 'Максимум элементов: ${max}',
  },
});

length

yup.setLocale({
  array: {
    length: 'Количество элементов должно быть ${length}',
  },
});

Локализация boolean

yup.setLocale({
  boolean: {
    isValue: 'Неверное логическое значение',
  },
});

Использование параметров шаблонов

Yup поддерживает интерполяцию значений через ${}.

Пример:

yup.setLocale({
  string: {
    min: 'Минимум ${min} символов',
  },
});

Доступные параметры зависят от типа проверки.


Доступные переменные

Для min

${min}

Для max

${max}

Для length

${length}

Для oneOf

${values}

Пример:

yup.setLocale({
  mixed: {
    oneOf: 'Допустимые значения: ${values}',
  },
});

Функции вместо строк

Сообщения можно генерировать динамически.

yup.setLocale({
  string: {
    min: ({ min }) => `Минимум ${min} символов`,
  },
});

Возврат объектов вместо строк

Полезно при интеграции с системами i18n.

yup.setLocale({
  mixed: {
    required: () => ({
      key: 'validation.required',
    }),
  },
});

Интеграция с i18next

Популярный вариант локализации — связка Yup и i18next.

Конфигурация

import i18next from 'i18next';
import * as yup from 'yup';

yup.setLocale({
  mixed: {
    required: () => i18next.t('validation.required'),
  },

  string: {
    email: () => i18next.t('validation.email'),
  },
});

Файлы переводов

{
  "validation": {
    "required": "Поле обязательно",
    "email": "Введите корректный email"
  }
}

Локализация через react-i18next

При использовании React и react-i18next локализацию часто выносят в отдельный модуль.

// validationLocale.js

import * as yup from 'yup';
import i18n from './i18n';

export function setupYupLocale() {
  yup.setLocale({
    mixed: {
      required: () => i18n.t('required'),
    },

    string: {
      email: () => i18n.t('email'),
    },
  });
}

Динамическое переключение языка

Проблема глобальной локализации заключается в том, что setLocale() меняет сообщения сразу для всех схем.

При смене языка требуется повторная инициализация.

import i18n from './i18n';

i18n.on('languageChanged', () => {
  setupYupLocale();
});

Локализация отдельных сообщений

Глобальная локализация не обязательна.

Сообщения можно задавать локально:

const schema = yup.object({
  password: yup
    .string()
    .required('Введите пароль')
    .min(8, 'Минимум 8 символов'),
});

Комбинирование глобальных и локальных сообщений

Локальное сообщение имеет приоритет.

yup.setLocale({
  mixed: {
    required: 'Поле обязательно',
  },
});

const schema = yup.object({
  email: yup
    .string()
    .required('Введите email'),
});

Результат:

Введите email

Создание собственного словаря локализации

Крупные проекты часто выносят сообщения в отдельный файл.

// yupLocaleRu.js

export default {
  mixed: {
    required: 'Поле обязательно',
    default: 'Некорректное значение',
  },

  string: {
    email: 'Некорректный email',
    min: 'Минимум ${min} символов',
  },

  number: {
    min: 'Минимум ${min}',
  },
};

Подключение:

import * as yup from 'yup';
import ruLocale from './yupLocaleRu';

yup.setLocale(ruLocale);

Поддержка нескольких языков

Структура каталогов:

/locales
  /ru
    yup.js
  /en
    yup.js
  /de
    yup.js

Русская локаль

export default {
  mixed: {
    required: 'Поле обязательно',
  },
};

Английская локаль

export default {
  mixed: {
    required: 'Field is required',
  },
};

Автоматическая загрузка локали

async function loadLocale(lang) {
  const locale = await import(`./locales/${lang}/yup.js`);

  yup.setLocale(locale.default);
}

Локализация кастомных тестов

Пользовательские проверки через test() также поддерживают локализацию.

const schema = yup.string().test(
  'latin-only',
  'Разрешены только латинские буквы',
  value => /^[a-z]+$/i.test(value)
);

Динамические сообщения в test

const schema = yup.string().test(
  'password-strength',
  ({ value }) => `Пароль "${value}" слишком простой`,
  value => validatePassword(value)
);

Использование createError

Для сложных сценариев применяется createError.

const schema = yup.string().test({
  name: 'custom',
  test(value, context) {
    if (!value.startsWith('A')) {
      return context.createError({
        message: 'Строка должна начинаться с A',
      });
    }

    return true;
  },
});

Локализация ошибок вложенных объектов

const schema = yup.object({
  user: yup.object({
    email: yup
      .string()
      .email()
      .required(),
  }),
});

Ошибка:

Некорректный email

Локализация применяется автоматически ко всем вложенным схемам.


Локализация Formik

Formik тесно интегрируется с Yup.

const validationSchema = yup.object({
  email: yup
    .string()
    .email()
    .required(),
});

После настройки setLocale() ошибки автоматически отображаются на нужном языке.


Локализация React Hook Form

При использовании React Hook Form вместе с @hookform/resolvers/yup сообщения Yup также передаются автоматически.

const schema = yup.object({
  name: yup.string().required(),
});

Проблемы глобального состояния

setLocale() изменяет глобальную конфигурацию библиотеки.

В SSR-приложениях это может привести к конфликтам между запросами пользователей с разными языками.


Решение для SSR

Вместо глобальной локализации можно хранить сообщения рядом со схемой.

function createSchema(t) {
  return yup.object({
    email: yup
      .string()
      .required(t('required'))
      .email(t('email')),
  });
}

Локализация в Next.js

В Next.js часто используют фабрики схем.

export const buildSchema = (t) =>
  yup.object({
    login: yup.string().required(t('required')),
  });

Использование констант

Для единообразия сообщений удобно создавать набор констант.

export const validationMessages = {
  required: 'Поле обязательно',
  invalidEmail: 'Некорректный email',
};

Генерация сообщений через функции

export const messages = {
  minLength: min => `Минимум ${min} символов`,
};

Использование:

yup.string().min(8, messages.minLength(8));

Типизация локализации в TypeScript

В TypeScript можно описать тип словаря локализации.

interface ValidationLocale {
  mixed: {
    required: string;
  };

  string: {
    email: string;
  };
}

Полная русская локализация

import * as yup from 'yup';

yup.setLocale({
  mixed: {
    default: 'Некорректное значение',
    required: 'Поле обязательно',
    oneOf: 'Недопустимое значение',
    notOneOf: 'Запрещённое значение',
  },

  string: {
    length: 'Должно быть ${length} символов',
    min: 'Минимум ${min} символов',
    max: 'Максимум ${max} символов',
    email: 'Некорректный email',
    url: 'Некорректный URL',
    trim: 'Удалите лишние пробелы',
    lowercase: 'Только строчные буквы',
    uppercase: 'Только заглавные буквы',
  },

  number: {
    min: 'Минимум ${min}',
    max: 'Максимум ${max}',
    lessThan: 'Должно быть меньше ${less}',
    moreThan: 'Должно быть больше ${more}',
    positive: 'Введите положительное число',
    negative: 'Введите отрицательное число',
    integer: 'Введите целое число',
  },

  date: {
    min: 'Дата слишком ранняя',
    max: 'Дата слишком поздняя',
  },

  array: {
    min: 'Минимум ${min} элементов',
    max: 'Максимум ${max} элементов',
  },
});