user7245021
user7245021

Reputation: 153

Yup doesn't work properly with i18n

I have this piece of a code. I want to add error messages depending on user's locale, but yup throws errors, same if fields are filled in incorrectly

[missing "en.login.emailRequiredError" translation] [missing "en.login.passRequiredError" translation]

const schema = yup.object().shape({
  email: yup
      .string()
      .email(i18n.t('login.emailSpellError'))
      .required(i18n.t('login.emailRequiredError')),
  password: yup
      .string()
      .matches(/^((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,15})$/i, i18n.t('login.passSpellError'))
      .required(i18n.t('login.passRequiredError')),
});

i18n.t('login.passRequiredError') works fine when I put it into a render method for checking it but it does not work with the yup. Any suggestions? Thanks in advance

Upvotes: 13

Views: 24397

Answers (6)

Ricardo Weiss
Ricardo Weiss

Reputation: 51

Adding on @Ilya Pasternak answer, for the react hook form users I've created a fully typed hook that will simplify and encapsulate the logic:

export const useReactiveTranslatedForm = <T extends AnyObject>(
  getSchemaFunction: () => ObjectSchema<T>,
  defaultValues: DefaultValues<T>
) => {
  const { i18n } = useTranslation()
  const memoizedSchema = useMemo(() => getSchemaFunction(), [i18n.language])
  const form = useForm<T>({
    resolver: yupResolver(memoizedSchema),
    defaultValues
  })
  useEffect(() => {
    if (form.formState.isSubmitted) {
      form.trigger()
    }
  }, [i18n.language])
  return form
}

Upvotes: 1

Ilya Pasternak
Ilya Pasternak

Reputation: 21

I encountered this problem when using react-hook-form, I managed to solve the problem by adding useEffect to the form processing hook

export type LoginFormType = {
  captcha: string
  email: string
  password: string
}

export const getLoginFormSchema = (): ObjectSchema<LoginFormType> =>
  yup.object({
    captcha: yup.string().required(i18n.t('Enter code from image')).trim(),
    email: yup
      .string()
      .required(i18n.t('Enter your email'))
      .matches(emailRegex, i18n.t('Enter a valid email address'))
      .trim(),
    password: yup.string().required(i18n.t('Enter the password')).trim(),
  })

export const defaultValues: LoginFormType = {
  captcha: '',
  email: '',
  password: '',
}

export const useLoginForm = (defValue?: Partial<LoginFormType>): UseFormReturn<LoginFormType> => {
  const schema = useMemo(() => getLoginFormSchema(), [i18n.language])

  const form = useForm({
    defaultValues: {
      ...defaultValues,
      ...defValue,
    },
    mode: 'onSubmit',
    resolver: yupResolver(schema),
  })

  useEffect(() => {
    if (form.formState.isSubmitted) {
      form.trigger()
    }
  }, [i18n.language])

  return form
}

Upvotes: 1

Artem
Artem

Reputation: 21

I've created a few custom hooks for this approach

This one to refresh error messages inside schema when is changing app language

import { yupResolver } from '@hookform/resolvers/yup';
import { useRouter } from 'next/router';
import { useMemo } from 'react';

const useSchema = (getSchema) => {
  const { locale } = useRouter();
  const resolver = useMemo(getSchema, [locale]);

  return yupResolver(resolver);
};

export default useSchema;

And this one to set global in App component localised error messages

import { useTranslation } from 'react-i18next';
import { setLocale } from 'yup';

export const useLocalisedYupSchema = () => {
  const { t } = useTranslation('common');

  setLocale({
    mixed: {
      required: t('validation.required')
    },
    string: {
      min: ({ min }) => t('validation.min', { min }),
      max: ({ max }) => t('validation.max', { max })
    },
  });
};

Also usage of schemas inside component with React Hook Form

import { getChangePasswordSchema } from 'static/schemas/changePassword';
import useSchema from 'utils/hooks/useSchema';
import { useForm } from 'react-hook-form';

const AccountContentSecurity = () => {
  ...
  const resolver = useSchema(getChangePasswordSchema);
  const { reset, control, handleSubmit } = useForm({
    defaultValues: {
      'current_password': '',
      'new_password': '',
      'password_confirmation': '',
    },  
    resolver,
  });
  ...

and schema

import { passwordSchema } from 'static/schemas';
import { object } from 'yup';

export const getChangePasswordSchema = () => object({
  'current_password': passwordSchema,
  'new_password': passwordSchema,
  'password_confirmation': passwordSchema,
});

Upvotes: 1

mbao01
mbao01

Reputation: 191

A solution will be to make a function that returns your validation schema. Then call that function in your component with the result memoized. This way, you are guaranteed that translations for validation messages are computed on the fly.

Another advantage here is you translate at the source of the message.

// Translation file
{
  "validation.invalid-email": "Email is invalid",
  "validation.field-required": "Field is required"
}


// Validation schema
const forgotPasswordSchema = () => {
  return yup.object().shape({
    email: yup
      .string()
      .email(i18n.t('validation.invalid-email'))
      .required(i18n.t('validation.field-required')),
  });
};


// Your component
const FormComponent = () => {
  const schema = useMemo(() => forgotPasswordSchema(), [i18n.language]); // NB: `[i18n.language]` is optional and `[]` will suffice depending on how you're handling language change

  return <>...</>;
}

Upvotes: 5

Jacob Joy
Jacob Joy

Reputation: 556

Yup Validation method,

// You define the key mentioned in the translation file, in my example 'Invalid email' and 'Required'  

    let ForgotPasswordSchema = yup.object().shape({
      email: yup.string().email('Invalid email').required('Required'),
    });

In render method,

// As per your definition

isInvalid={(!!errors.email) && this.context.t(!!errors.email)}
invalidText={(errors.email) && this.context.t(errors.email)}

Translation File

export const translations = {
  "cy": {
    "Required":"Gofynnol",
    "Invalid email":"Nid yw'r cyfeiriad ebost yn ddilys",
}
 };  

Upvotes: 7

Mark Carlson
Mark Carlson

Reputation: 211

In your schema, replace:

.email(i18n.t('login.emailSpellError'))

with

.email('login.emailSpellError')

then in your render method:

{t(`form.errors.${form.errors.email}`)}

This assumes your translation file has an entry like this:

"form": { "errors": {"login": {"emailSpellError": "Your email is invalid"}}}}

The goal here is to move the t() method into your render method and have all translations happen there.

Upvotes: 21

Related Questions