ansman / validate.js

A declarative validation library written javascript
https://validatejs.org
MIT License
2.63k stars 336 forks source link

Internationalisation (i18n, multi-language support) #74

Open jakubrohleder opened 8 years ago

jakubrohleder commented 8 years ago

First of all I real like the library approach that makes it useable for multiple frameworks. The one option I real miss is some kind multi-language support. Have you thought about adding these feature?

I think the simples and sufficient fix would be to add option that turns messages into codes (like 'VALIDATEJS.ERROR.REQUIRED'). So people can handle translation on their own. What do you think about such improvement?

Ps. sorry for weird title but I want the topic to be easy to find for others.

ansman commented 8 years ago

i18n is definitely something that is lacking in this library. What you describe is already possible actually.

All included validators support overwriting the default options. So you could do this:

validate.validators.presence.options = {
  message: "VALIDATEJS.ERROR.REQUIRED"
};
validate.options = {fullMessages: false};
validate({}, {name: {presence: true}});
// => {"name": ["VALIDATEJS.ERROR.REQUIRED"]}

But if you have a concrete suggestion I'd be happy to consider it!

jsenecal commented 8 years ago

Thanks for this @ansman

jakubrohleder commented 8 years ago

@ansman Thanks for the answer and temporary fix.

I'll take a closer look at the source code and hopefully propose more specific solution along with a PR.

ansman commented 8 years ago

@jakubrohleder I would recommend that you describe your solution before implementing it in case it is in another direction than the library is heading.

This could potentially be added in a separate file that could be included after validate.js which would set the default messages when included validate-i18n.js for example, maybe even as a separate library with official support.

jakubrohleder commented 8 years ago

@ansman I think that library that modifies another library is too hackish and can cause potential problems in the future. In my opinion the best solution would be something that is built in, yet optional.

I'll describe my idea here before making a PR.

airtonix commented 8 years ago

I fail to see the need for validate.js to do anything around this?

// ./i18n/en.js

export default {
  'UsernameRequired': 'The username is required'
}
// ./i18n/jp.js

export default {
  'UsernameRequired': 'ユーザ名が必要です'
}
import i18n from 'mythical-i18n-library';
import en from './i18n/en';
import jp from './i18n/jp';

let {translate} = i18n({lang: 'en', locales: {en, jp});

let constraints = {
  username: {
    presence: {
      message: translate('UsernameRequired')
    }
  }
};

validate({}, constraints);
ansman commented 8 years ago

@airtonix Technically validate.js already supports this but some things could be nicer.

The whole idea of prefixing the message with the attribute name needs to be rethought if translations should feel like a first class citizen. For example I've been playing around with the idea that instead of always prefixing that the message will have to contain a marker (such as %{attribute} for lowercase and %{Attribute} for upper case.). But this will still require the attribute to be translated before inserting.

It's not high priority but it's definitely something that needs to be looked at before 1.0

airtonix commented 8 years ago

@ansman I still feel like translation is really not the scope of validate.js.

It is sufficiently designed to allow a project integrator to use other libraries dedicated to i18n.

ansman commented 8 years ago

@airtonix Exactly, validate.js shouldn't support i18n in itself but it should make it easy to implement.

mikegolod commented 7 years ago

@ansman @airtonix how one should do it? is there a proper hook points? I've looked at documentation and found validator.prettify, but seems like it also prettifies values. What libs do u recommend to use for i18n? Btw, idea with %{attribute} looks promising :).

vdh commented 7 years ago

@ansman That %{attribute} / %{Attribute} would be extremely helpful for getting started on translations. Sending %{attribute} can't be blank" to a translator is a lot more approachable than trying to send English fragments like can't be blank to them.

joelmora commented 6 years ago

Any updates with this? I did the workaround proposed for @ansman in https://github.com/ansman/validate.js/issues/74#issuecomment-145609665, but for validators that involves variables as "is too short (minimum is 8 characters)" is too ugly to translate a general message

marcelcremer commented 5 years ago

I solved it in my solution like Constraint:

{
  title: {
    presence: { allowEmpty: false, message: 'VALIDATION.REQUIRED' },
    length: {
      minimum: 5,
      message: { message: 'VALIDATION.MINLENGTH', value: 5 }
    }
  }

English translation:

"MINLENGTH": "Must provide at least {{value}} characters"

and calling the i18n framework like

let result = ...; // validate object, get field where the message should be printed
translate(result.message, { value: result.value});

Very simple task tbh. You could even do something like

Constraint:

{
  title: {
    presence: { allowEmpty: false, message: 'VALIDATION.REQUIRED' },
    length: {
      minimum: 5,
      maximum: 30,
      message: { message: 'VALIDATION.LENGTH_BETWEEN', value: [5, 30] }
    }
  }

English translation:

"LENGTH_BETWEEN": "You must provide betweeen {{from}} and {{to}} characters"

Calling

let result = ...; // validate object, get field where the message should be printed
translate(result.message, { from: result.value[0], to: result.value[1]});

...and so on.

I also think there's nothing more about i18n that validate.js needs to support, as it's very environment specific.

dalssoft commented 4 years ago

I think a new error format like validate.options = {format: "code"} where the message returned would be codes (ex: { message: 'VALIDATION.LENGTH_BETWEEN', value: [5, 30] }) instead of English messages could help.

azatTemirbek commented 4 years ago
import Validator from "validate.js";
import I18n from "react-native-i18n";
/**
 * need to add pronis if async is required
 * need to add format and parse method for date and date time
 */
export default class ValidatorJS {
  constructor(translateFn = I18n.t.bind(I18n)) {
    this.translate = translateFn;
    this.Validator = Validator;
    this.Validator.formatters.custom = this.overloadFormatFunc;
    this.Validator.convertErrorMessages = this.convertErrorMessages;
    this.overloadErrorMessages();
  }
  /** adapters */
  bindRequiredToPresence = (constraints) =>
    Object.entries(constraints).reduce(
      (acc, [key, { required, ...ObjVal }]) => {
        acc[key] = { presence: required, ...ObjVal };
        return acc;
      },
      {}
    );
  /** binds default messages */
  overloadErrorMessages = () => {
    this.Validator.validators.date.options = {
      message: "DATE_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.datetime.options = {
      message: "DATETIME_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.email.options = {
      message: "EMAIL_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.equality.options = {
      message: "EQUALITY_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.exclusion.options = {
      message: "EXCLUSION_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.format.options = {
      message: "FORMAT_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.inclusion.options = {
      message: "INCLUSION_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.length.options = {
      message: "LENGTH_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.numericality.options = {
      message: "NUMERICALITY_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.presence.options = {
      message: "PRESSENCE_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.url.options = {
      message: "URL_VALIDATOR_ERROR_MSG",
    };
    this.Validator.validators.type.messages = {
      array: "ARRAY_VALIDATOR_ERROR_MSG",
      boolean: "BOOLEAN_VALIDATOR_ERROR_MSG",
      date: "DATE_VALIDATOR_ERROR_MSG",
      integer: "INTEGER_VALIDATOR_ERROR_MSG",
      number: "NUMBER_VALIDATOR_ERROR_MSG",
      object: "OBJECT_VALIDATOR_ERROR_MSG",
      string: "STRING_VALIDATOR_ERROR_MSG",
    };
  };

  convertErrorMessages = (errors, options) => {
    options = options || {};
    var ret = [];
    errors.forEach((errorInfo) => {
      var error = this.Validator.result(
        errorInfo.error,
        errorInfo.value,
        errorInfo.attribute,
        errorInfo.options,
        errorInfo.attributes,
        errorInfo.globalOptions
      );

      if (!this.Validator.isString(error)) {
        ret.push(errorInfo);
        return;
      }

      if (error[0] === "^") {
        error = error.slice(1);
      } else if (options.fullMessages !== false) {
        error = error;
      }
      error = error.replace(/\\\^/g, "^");
      error = this.Validator.format(error, {
        value: this.Validator.stringifyValue(errorInfo.value, options),
      });
      ret.push(this.Validator.extend({}, errorInfo, { error: error }));
    });
    return ret;
  };
  /** uses translate with formatfunc */
  overloadFormatFunc = (errors) =>
    errors.reduce((acc, { validator, attribute, value, error }) => {
      acc[attribute] = this.translate(error, {
        validator,
        attribute,
        value,
        prettified: this.Validator.capitalize(
          this.Validator.prettify(attribute)
        ),
      });
      return acc;
    }, {});
  /** overrides */
  validate = (data, constraints, options = { format: "custom" }) => {
    // console.log("validatingData:", data);
    // console.log("validatingconstraints:", constraints);
    return this.Validator(
      data,
      this.bindRequiredToPresence(constraints),
      options
    );
    // console.log("validatedRes:", res);
    // return res;
  };
}
// usage
export default class ValidatorAdapter {
    constructor(formCtx,validator=ValidatorJS,translate){
        this.validator = new validator(translate);
        this.formCtx = formCtx;
    }
    validate = () => this.validator.validate({...this.formCtx.state.values},{...this.formCtx.elements});
}
FORM extends React.Component{
constructor(props) {
    super(props);
    this.state = {values: {}, options: {}, errors: {}, isVisible: false};
    this.validator = new ValidatorAdapter(this,props.validator, props.translate);
  }.......
_handleSubmitAndValidate = (key="_handleSubmit")=>(e)=>{
    let {values,errors} = this.state;
    let valid = this.validator.validate();
    if(!valid){
      /** this means no error at all */
      this.setState({errors:{}})
      this[key](e,values,errors);
    }else{
      errors = {...errors, ...valid}
      this.setState({errors})
    }
  }
.....
}