Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passing translated strings to redux-form field level validation #306

Closed
rntorm opened this issue Sep 28, 2017 · 17 comments
Closed

Passing translated strings to redux-form field level validation #306

rntorm opened this issue Sep 28, 2017 · 17 comments

Comments

@rntorm
Copy link

rntorm commented Sep 28, 2017

I am not able to come up with a way to translate field level validations in redux-form.
In the example below, how can I translate a string outside of the component scope?

const minValue0 = minValue(0)('translatedString')
const TheForm => props => {
 ..        return(<Field
          id="quantity"
          type="number" 
          name="quantity"
          validate={[minValue0]}
          component={InputField} />)

}

Defining minValue0 inside the component does not work because the validation will break.

@jamuhl
Copy link
Member

jamuhl commented Sep 28, 2017

You can require i18n and use it's t function outside of components (no translate hoc), sample:

import Validator from 'validatorjs';
import i18n from './i18n';
import { unflatten } from 'flat';

// Check if value is includes in usedValuesString.
// Comparison is performed after passing values through 'normalizer'.
function checkUniqueness(value, usedValuesString, normalizer) {
  const normalizedUsedValues = usedValuesString.split(',').map(normalizer);
  const normalizedValue = normalizer(value);
  return !normalizedUsedValues.includes(normalizedValue);
}

export function validate(rules) {

  return (values) => {
    const commonMessages = i18n.t('validation:validationErrors', { returnObjects: true });
    const customMessages = i18n.t('validation', { returnObjects: true });
    const validator = new Validator(values || {}, rules, { ...commonMessages, ...customMessages });
    validator.passes();
    const errors = {};
    Object.keys(validator.errors.all()).forEach(field => {
      errors[field] = validator.errors.first(field);
    });
    return unflatten(errors);
  };
}

@jamuhl
Copy link
Member

jamuhl commented Sep 28, 2017

So for you code that would mean if i get it right:

import i18n from '../i18n'; // or where every that file is

const minValue0 = minValue(0)(i18n.t('translatedString'))
const TheForm => props => {
 ..        return(<Field
          id="quantity"
          type="number" 
          name="quantity"
          validate={[minValue0]}
          component={InputField} />)

}

@rntorm
Copy link
Author

rntorm commented Sep 29, 2017

Hey, tried it out but could not get it to work.

My i18n file:

import i18n from 'i18next';
import LocizeBackend from 'i18next-locize-backend';
import LocizeEditor from 'locize-editor';
import LanguageDetector from 'i18next-browser-languagedetector';


i18n
  .use(LocizeBackend)
  .use(LocizeEditor)
  .use(LanguageDetector)
  .init({
    fallbackLng: 'en',
    appendNamespaceToCIMode: true,
    saveMissing: true,

    // have a common namespace used around the full app
    ns: ['translations'],
    defaultNS: 'translations',

    debug: true,
    keySeparator: '### not used ###', // we use content as keys

    backend: {
      projectId: '386b678b-7052-48ba-ac20-7bb93b01bd52', // <-- replace with your projectId
      apiKey: '86346aab-bb3d-42e9-9e8b-71e99f877ace',
      referenceLng: 'en'
    },

    interpolation: {
      escapeValue: false, // not needed for react!!
      formatSeparator: ',',
      format: function(value, format, lng) {
        if (format === 'uppercase') return value.toUpperCase();
        return value;
      }
    },

    react: {
      wait: true
    }
  });


export default i18n;

I get like 78 of these errors, seems like some kind of loop:

Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.

I also have these import in the same file. Maybe it breaks something?

import React from 'react'
import { translate } from 'react-i18next'
import { Field, reduxForm } from 'redux-form'
import {minValue } from './validators'
import i18n from '../../i18n'; // or where every that file is

const minValue0 = minValue(0)(i18n.t('translatedString'))
const OrderForm => props => {
 ..        return(<Field
          id="quantity"
          type="number" 
          name="quantity"
          validate={[minValue0]}
          component={InputField} />)

}

const OrderFormComponent = reduxForm({
  form: 'order'})(OrderForm)
export default translate('translations')(OrderFormComponent)

@jamuhl
Copy link
Member

jamuhl commented Sep 29, 2017

not sure but guessing the warnings come from missing key? Do those show up before the setState warnings? If so that shouldn't be an issue on next reload as the key would been added (saveMissing).

Looking at the full code i would move the minValue0 to your render function - so you're sure i18next has loaded the translations - else calling t upfront will result in not existing value - or even set to wrong language (if changing that programmatically):

import React from 'react'
import { translate } from 'react-i18next'
import { Field, reduxForm } from 'redux-form'
import {minValue } from './validators'
import i18n from '../../i18n'; // or where every that file is

const OrderForm => props => {
 ..        return(<Field
          id="quantity"
          type="number" 
          name="quantity"
          validate={[minValue(0)(i18n.t('translatedString'))]}
          component={InputField} />)

}

const OrderFormComponent = reduxForm({
  form: 'order'})(OrderForm)
export default translate('translations')(OrderFormComponent)

Beside that you leaked your API key - i highly recommend to generate a new one in your project settings and remove the old one - else people could use that to add content on your behalf.

@rntorm
Copy link
Author

rntorm commented Sep 29, 2017

Oops. Generated a new one.
The warnings will be generated every time if the i18n.t('string') is use and on top of that the translations will not be rendered. If I remove i18n.t('string'), everything starts working again.

Moving minValue0 to render will not work, thats the whole problem. Redux-form is not designed that way for some reason. That is why I need to translate the key outside the component.

On another note, how should the translate work in production if the API key cannot be exposed. My react project is client side?

@jamuhl
Copy link
Member

jamuhl commented Sep 29, 2017

Strange technically there is no difference between:

validate={[minValue(0)(i18n.t('translatedString'))]}

and

validate={[minValue0]}

but you might use validate={[minValue(0)(props.t('translatedString'))]} as t is on props too...

what is minValue returning?!? https://redux-form.com/7.0.4/docs/api/field.md/#-code-validate-value-allvalues-props-name-gt-error-code-optional-

The API key is not needed in production - it is only needed for writing missings. The projectId is enough to load translations.

@rntorm
Copy link
Author

rntorm commented Oct 1, 2017

There is a difference, if inside the render method, the validation function is regenerated every time and that causes failure redux-form/redux-form#2453 (erikras comment). So as I understand there is no option from the i18n side also, since it might not be loaded yet?

@jamuhl
Copy link
Member

jamuhl commented Oct 2, 2017

did you try the suggestion from @szerintedmi redux-form/redux-form#2453 (comment)

@rntorm
Copy link
Author

rntorm commented Oct 2, 2017

@jamuhl. That solution seems to work, even though it forces the use of component with a state. If you would like to use stateless component, the gist in this comment seems to do the trick.

@jamuhl
Copy link
Member

jamuhl commented Oct 2, 2017

yes the alternative would be a component memoizing once created...

personally i would go the memoizing way - but would extend it so there is a listen to langaugeChanged event on i18next and on change the mem would be resetted.

@rntorm
Copy link
Author

rntorm commented Oct 2, 2017

Further test shows that having two memoized validators also creates an issue of not validating the fields. Like -> [minValue0, required].

@jamuhl
Copy link
Member

jamuhl commented Oct 2, 2017

How annoying...what we did (before there was validate introduced on field:

Using https://redux-form.com/7.0.4/docs/api/reduxform.md/#-code-validate-values-object-props-object-gt-errors-object-code-optional-

using the hoc

  form({
    form: 'loginForm',
    validate: validate({
      username: 'required|max:50',
      password: 'required|min:8'
    }),
    autoToggleReadonly: false
  })(MyComponent);

where validate is:

import Validator from 'validatorjs';
import i18n from './i18n';
import { unflatten } from 'flat';

// Check if value is includes in usedValuesString.
// Comparison is performed after passing values through 'normalizer'.
function checkUniqueness(value, usedValuesString, normalizer) {
  const normalizedUsedValues = usedValuesString.split(',').map(normalizer);
  const normalizedValue = normalizer(value);
  return !normalizedUsedValues.includes(normalizedValue);
}

export function validate(rules) {

  return (values) => {
    const commonMessages = i18n.t('validation:validationErrors', { returnObjects: true });
    const customMessages = i18n.t('validation', { returnObjects: true });
    const validator = new Validator(values || {}, rules, { ...commonMessages, ...customMessages });
    validator.passes();
    const errors = {};
    Object.keys(validator.errors.all()).forEach(field => {
      errors[field] = validator.errors.first(field);
    });
    return unflatten(errors);
  };
}

But i would open an issue over at redux-form - i mean having two such memoized validators should work in my opinion...

@melounek
Copy link

melounek commented Oct 2, 2017

I headed the same question and I was also dealing with async server validation where I receive constants, and I solved that by returning constants also from static validators:

export const required = value =>  !value ? 'REQUIRED' : false

And translate them in rendering component

{touched && error && <span>{t(error)}</span>}

@rntorm
Copy link
Author

rntorm commented Oct 2, 2017

@melounek This seems to be another good workaround. Only problem I see with this is when my error would use a variable.
For example:

export const minValue = message => min => value => value && value < min ? `${message} ${min}` : undefined

@jamuhl
Copy link
Member

jamuhl commented Oct 20, 2017

@rntorm did you find a solution to this? if so could this be closed?

@rntorm
Copy link
Author

rntorm commented Oct 23, 2017

@jamuhl Yeah, I used a workaround that creates the translations in the class constructor. It works that way, even though its not the perfect solution. I prefer stateless components to class.

@jamuhl jamuhl closed this as completed Oct 23, 2017
@ideasOS
Copy link

ideasOS commented Sep 1, 2018

I wanted to document a related problem that I have managed to solve. The issue is already closed, but I believe this is the best place for others to find this hint.
In my project, I am using redux, React, redux-form FieldArrays, react-select, react-i18next.

I wanted to create a form which was able to create an array as one of the form fields. I did not manage to use a function from the props for the 'component' of the FieldArray inside the render function of the form component, but had to create one outside of the render function. That helped me to avoid an infinite loop and is working fine.

Of course it was necessary to import i18n's t function as @jamuhl recommends #306 (comment) .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants