简体   繁体   中英

Dynamically changing (i18n) UI language after Yup validation errors show in Formik form, using hooks -> unexpected behaviour

I am showing a login screen to the user. This login screen (or the whole app, to be fair) can be shown in different languages.

The language switching works fine for all UI components except validation errors.

Currently, if a user leaves the email / password field blank, or enters an invalid email address, gets an error message and then switches languages, the error message won't change to the new language.

After a lot of guesswork and experimentation I have arrived at a point where the language changes after clicking either the 'de' or 'en' buttons twice, followed by typing into the input.

I can't for the life of me work out the right combination of useState and useEffect hooks to make the validation messages translate immediately upon hitting either of the language buttons.

Following code:

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Formik, Field, Form, ErrorMessage } from 'formik'
import * as Yup from 'yup'
import { useTranslation } from 'react-i18next'
import { accountService, alertService } from '@/_services'

function Login({ history, location }) {
  //'common' is defined in i18n.js under init's namespace ('ns:') option
  //if that's not set, use useTranslation('common'), or whatever the name
  //of the json files you use in the language-specific folders is
  const { t, i18n } = useTranslation()

  const [emailRequired, setEmailRequired] = useState(t('login.email_error_required'))
  const [emailInvalid, setEmailInvalid] = useState(t('login.email_error_invalid'))
  const [passwordRequired, setPasswordRequired] = useState(t('login.password_error_required'))

  const [validationSchema, setvalidationSchema] = useState(
    Yup.object().shape({
      email: Yup.string().email(emailInvalid).required(emailRequired),
      password: Yup.string().required(passwordRequired),
    }),
  )

  useEffect(() => {})

  const initialValues = {
    email: '',
    password: '',
  }

  const onSubmit = ({ email, password }, { setSubmitting }) => {
    alertService.clear()
    accountService
      .login(email, password)
      .then(() => {
        const { from } = location.state || { from: { pathname: '/' } }
        history.push(from)
      })
      .catch((error) => {
        setSubmitting(false)
        alertService.error(error)
      })
  }

  return (
    <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
      {({ errors, touched, isSubmitting }) => (
        <Form>
          <h3 className="card-header">{t('login.page_title')}</h3>
          <div className="card-body">
            <div className="form-group">
              <label>{t('login.email_input_label')}</label>
              <Field name="email" type="text" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />
              <ErrorMessage name="email" component="div" className="invalid-feedback" />
            </div>
            <div className="form-group">
              <label>{t('login.password_input_label')}</label>
              <Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
              <ErrorMessage name="password" component="div" className="invalid-feedback" />
            </div>
            <div className="form-row">
              <div className="form-group col">
                <button type="submit" disabled={isSubmitting} className="btn btn-primary">
                  {isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                  {t('login.login_btn_label')}
                </button>
                <Link to="register" className="btn btn-link">
                  {t('login.register_btn_label')}
                </Link>
                <button
                  type="button"
                  onClick={() => {
                    i18n.changeLanguage('de')
                    setEmailRequired(t('login.email_error_required'))
                    setEmailInvalid(t('login.email_error_invalid'))
                    setPasswordRequired(t('login.password_error_required'))
                    setvalidationSchema(
                      Yup.object().shape({
                        email: Yup.string().email(emailInvalid).required(emailRequired),
                        password: Yup.string().required(passwordRequired),
                      }),
                    )
                  }}
                >
                  de
                </button>
                <button
                  type="button"
                  onClick={() => {
                    i18n.changeLanguage('en')
                    setEmailRequired(t('login.email_error_required'))
                    setEmailInvalid(t('login.email_error_invalid'))
                    setPasswordRequired(t('login.password_error_required'))
                    setvalidationSchema(
                      Yup.object().shape({
                        email: Yup.string().email(emailInvalid).required(emailRequired),
                        password: Yup.string().required(passwordRequired),
                      }),
                    )
                  }}
                >
                  en
                </button>
              </div>
              <div className="form-group col text-right">
                <Link to="forgot-password" className="btn btn-link pr-0">
                  {t('login.forgot_password_label')}
                </Link>
              </div>
            </div>
          </div>
        </Form>
      )}
    </Formik>
  )
}

export { Login }

Things I have tried and their results:

1.)

I once didn't use useState at all, so the validationSchema was composed this way:

  const emailRequired = t('login.email_error_required');
  const emailInvalid = t('login.email_error_invalid');
  const passwordRequired = t('login.password_error_required');

  const validationSchema = Yup.object().shape({
    email: Yup.string().email(emailInvalid).required(emailRequired),
    password: Yup.string().required(passwordRequired),
  })

This lead to only one click of the 'de' or 'en' buttons being required before typing into one of the inputs would change the error message to the new translation.

2.)

I made the language change buttons into submit-type buttons:

<button
  type="button"
  onClick={() => {
    i18n.changeLanguage('en')
  }}
>

but that just felt wrong. I can't imagine that would be the correct solution. It worked, however, as it simply re-ran the 'OnSubmit' function...

3.)

Various combinations of adding 'validationSchema' and the error variables into useState and useEffect hooks, in the hopes that any of them worked, but it simply didn't.

What I would like to understand is why the Formik object appears to 'insulate' its inner components from updates brought about by useEffect. Why is React not seeing the change in language for the validation errors, but for everything else it does?

I haven't had any responses, but figured it out myself in the meantime.

Turns out the translation keys need to go into the schema:

  const validationSchema = Yup.object().shape({
    email: Yup.string().email('login.email_error_invalid').required('login.email_error_required'),
    password: Yup.string().required('login.password_error_required'),
  })

And then the error messages need to be custom components. So they need to change from this:

<ErrorMessage name="password" component="div" className="invalid-feedback" />

To this:

{errors.password && <div className="invalid-feedback">{t(errors.password)}</div>}

So, for email, the whole thing looks like:

<div className="form-group">
  <label>{t('login.password_input_label')}</label>
  <Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
  {errors.password && <div className="invalid-feedback">{t(errors.password)}</div>}
</div>

I have no idea why this made a difference, but it's hopefully going to help someone else in the future.

Thanks op, it helped me figure out how to make translations work.

The approach it worked for me is:

const LoginSchema = Yup.object().shape({
username: Yup.string().required('loginForm.required'),
password: Yup.string()
    .min(6, 'loginForm.passwordShort')
    .required('loginForm.required'),
});

But now extract does not work so you have to manually add these keys inside your.po files.

{errors.username && touched.username ? (
                        <IonItem
                            lines="none"
                            className={`${classes.Error} ion-margin-top ion-justify-content-center`}
                        >
                            {t({
                                id: errors.username,
                            })}
                        </IonItem>
                    ) : null}

This approach works for Yup + Lingui.js

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM