DEV Community

daribock
daribock

Posted on

How can you use i18next with Yup to get clean, translated validation errors?

Form validation with Yup, react-hook-form, and i18next often starts simple—and quickly turns messy.

Every rule needs a translated message, leading to repetitive t('validation.required') calls all over your schemas.

There’s a better way.

The key is Yup’s global setLocale configuration. It lets you define all validation messages once and automatically apply them everywhere — fully translated and consistent.

The Problem (What usually goes wrong)

const schema = yup.object({
  email: yup
    .string()
    .required(t('validation.required'))
    .email(t('validation.email')),
  password: yup
    .string()
    .required(t('validation.required'))
    .min(8, t('validation.minLength', { min: 8 })),
});
Enter fullscreen mode Exit fullscreen mode

Issues:

  • ❌ Repetitive translation calls
  • ❌ Hard to maintain
  • ❌ Easy to forget translations
  • ❌ Inconsistent wording across forms

In the project I am working on we had the same Problem. Even worse we did a mix of a lot of different things to display the error message which resulted in a big unmaintainable mess.

Here is how you can solve it!

The Solution: Global Yup Locale + i18next

1️⃣ Configure Yup once (with translations)

import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import * as yup from 'yup';

export const useYupLocale = () => {
  const { t, i18n } = useTranslation();

  useEffect(() => {
    yup.setLocale({
      mixed: {
        required: t('validation.required'),
        notType: t('validation.invalid'),
      },
      string: {
        email: t('validation.email'),
        min: t('validation.minLength', { min: '${min}' }),
        max: t('validation.maxLength', { max: '${max}' }),
        url: t('validation.url'),
      },
      number: {
        min: t('validation.numberMin', { min: '${min}' }),
        max: t('validation.numberMax', { max: '${max}' }),
        integer: t('validation.integer'),
        positive: t('validation.positive'),
      },
    });
  }, [i18n.language]);
};
Enter fullscreen mode Exit fullscreen mode

✅ Yup automatically replaces ${min} / ${max}
✅ Messages update instantly when the language changes

2️⃣ Translation files

// en.json
{
  "validation": {
    "required": "This field is required",
    "email": "Please enter a valid email address",
    "minLength": "Must be at least ${min} characters",
    "maxLength": "Must be no more than ${max} characters",
    "integer": "Must be a whole number"
  }
}
Enter fullscreen mode Exit fullscreen mode
// de.json
{
  "validation": {
    "required": "Dieses Feld ist erforderlich",
    "email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
    "minLength": "Mindestens ${min} Zeichen",
    "maxLength": "Höchstens ${max} Zeichen",
    "integer": "Muss eine ganze Zahl sein"
  }
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ Initialize once at app level

export const App = () => {
  useYupLocale();

  return <YourApp />;
};
Enter fullscreen mode Exit fullscreen mode

The Result: Clean, readable schemas

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8),
  age: yup.number().required().min(18).integer(),
});
Enter fullscreen mode Exit fullscreen mode

No translations.

No duplication.

Still fully localized.

Custom messages? Still possible

confirmPassword: yup
  .string()
  .required(t('validation.passwordMatch'))
Enter fullscreen mode Exit fullscreen mode

Benefits

  • DRY – define messages once
  • Consistent validation across all forms
  • Easy maintenance (change text in one place)
  • Automatic language switching
  • Much cleaner schemas
  • Better developer experience

Takeaway

If you use Yup + i18next, configuring Yup’s global locale is the cleanest way to get translated, consistent, and maintainable validation errors.

You focus on validation logic.

Yup and i18next handle the messages.

Once you try this pattern, you won’t go back.

Top comments (0)