datamapper / dm-validations

Library for performing validations on DM models and pure Ruby object
http://datamapper.org/
MIT License
50 stars 43 forks source link

Better localizability for dm-validations #20

Open solnic opened 13 years ago

solnic commented 13 years ago

I want to revise the issue of localization again because it has been one of the pain points for me in using datamapper; and the last (and only?) ticket I could find on this issue is several years old and closed.

dm-validations allows changing the error messages, but that's of little use without the ability to translate model and field names. It also doesn't integrate nicely e.g. into Rails.

There are different ways to achieve this, but I think it would be good if dm-validations would be language-agnostic. For example instead of generating an error message immediately when adding an error, add information about the violation: the resource, field name, failed validation and validation-specific context values (user object, :password, :too_short, :minimum => 6).

With such an approach it would be easy to put different ways of translation on top of it, e.g.

Custom messages can be added as additional context information (my fork of dm-validations does that), though with the suggested localizability these would maybe become a bit superfluous.


Created by gix - 2011-01-29 20:03:11 UTC

Original Lighthouse ticket: http://datamapper.lighthouseapp.com/projects/20609/tickets/1477

solnic commented 13 years ago

Yes! I’m suffering because of that too. Let’s work on this man, I’ll start by looking at your fork. It’s already too late to push this into 1.1 release, but we can add that to 1.1.1!

by Piotr Solnica (solnic)

solnic commented 13 years ago

I’ve quickly looked at your forks, all looks great. I will try to use it in my app as a test drive. If it works fine then why not merging your work in now? Are there any public/semipublic API changes?

by Piotr Solnica (solnic)

solnic commented 13 years ago

I don’t know too much about localization, so I’ll let people who know more speak up about specifics. One idea I had, playing off gix’s original, is to instead of returning error messages like we do now, we setup objects for each type of error that have enough information in their type, attributes to reconstruct the error message, but when stringified default to the current error messages we have now.

This would allow us to keep a lot of the same API we have now, but we’d have rich objects for each error message that could be built on top of.

I’d probably want some sort of base class for the behavior, one subclass for each distinct error message, and one for custom validators. We can switch on the object type for different behavior for each object, rather than inspecting a Symbol attribute (which is just simulated polymorphism, a code smell).

by Dan Kubb (dkubb)

solnic commented 13 years ago

That’s essentially what my idea and branch do, except there is a single class instead of multiple ones, and context is provided via a hash. It would be easy to change that if it’s preferred that way. Actually thinking about it I prefer that idea myself instead of having a symbol for each error.

Regarding backwards compatibility, this mostly affects the ValidationErrors collection (methods like #on, #each or #[] returning error objects instead of strings). Giving error objects #to_str and #to_s methods returning the formatted error message works as long as no string methods are called on it. But since there is no real overlap in methods between the error class and strings method_missing could take care of it and delegate it to the message returned by #to_s (and give a deprecation warning).

by gix

solnic commented 13 years ago

This feature would help me out

by Xavier Shay

solnic commented 13 years ago

I pushed a second version implementing a cleaner way of transforming errors (see https://github.com/gix/dm-validations/commit/7145db7c8d8c556a2c787a5282b4750c82818fa0#diff-0).

Some unresolved things:

solnic commented 13 years ago

[bulk edit]

by Dan Kubb (dkubb)

nhoffmann commented 12 years ago

So it is really true that error messages can not be translated in a uniform way? I am wondering if there are any best practices or proposed workarounds for 1.2?

Maybe I am missing something and this is implemented in the 1.3 branch already?

solnic commented 12 years ago

Yes it'll be possible to add translations in 1.3 (current master)

nhoffmann commented 12 years ago

That's great news. Thanks solnic

shingara commented 12 years ago

@solnic How we can do that ? There are some example ?

solnic commented 12 years ago

I believe @emmanuel should know. He's done most of the work.

emmanuel commented 12 years ago

@shingara — unfortunately there are no real docs. Take a look at: https://github.com/datamapper/dm-validations/blob/master/lib/data_mapper/validation/message_transformer.rb#L90-106 for an example of a simple DataMapper::Validation::MessageTransformer subclass which looks up violation error messages from I18n with keys like errors.absent. If you want something like ActiveRecord's style of keys (see here for more info: https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb#L297-352), you would need to define a new MessageTransformer subclass with a custom #transform method, like:

class BetterI18n < DataMapper::Validation::MessageTransformer
  def transform(violation)
    raise ArgumentError, "+violation+ must be specified" if violation.nil?

    violation_type = violation.type
    resource       = violation.resource
    model_name     = resource.model.model_name
    attribute_name = violation.attribute_name
    i18n_scope     = self.i18n_scope(resource, model_name, attribute_name)

    options = {
      :model     => transform_model_name(model_name),
      :attribute => transform_attribute_name(model_name, attribute_name),
      :value     => resource.validation_property_value(attribute_name)
    }.merge(violation.info)

    ::I18n.translate("#{i18n_scope}.#{violation_type}", options)
  end

  def i18n_scope(resource, model_name, attribute_name)
    'datamapper.errors'
  end

  def transform_model_name(model_name)
    ::I18n.translate("models.#{model_name}")
  end

  def transform_attribute_name(model_name, attribute_name)
    ::I18n.translate("models.#{model_name}.attributes.#{attribute_name}")
  end
end # class BetterI18n

And then set your transformer as the default like so:

DataMapper::Validation::Violation.default_transformer = BetterI18n.new
emmanuel commented 12 years ago

To clarify the intent of this code: if you follow the link and check out the #generate_message method from ActiveModel::Validations, you'll see a potentially long list of defaults that is built up for every violation message lookup.

This interface (MessageTransformer#transform) allows you to specify exactly the strategy you want for violation message lookups, and it can do that, and only that (as opposed to supporting many different strategies by always looking everywhere).

snusnu commented 12 years ago

@emmanuel Thx for the explanation, this works perfectly for me! In case anyone else needs it, here's the errors.en.yml file I use. I haven't yet tested all interpolations but I went through dm-validation's rules and took the keys from there.

en:
  datamapper:
    errors:
      absent: "%{attribute} must be absent"
      inclusion: "%{attribute} must be one of %{set}"
      invalid: "%{attribute} has an invalid format"
      confirmation: "%{attribute} does not match the confirmation"
      accepted: "%{attribute} is not accepted"
      nil: "%{attribute} must not be nil"
      blank: "%{attribute} must not be blank"
      length_between: "%{attribute} must be between %{min} and %{max} characters long"
      too_long: "%{attribute} must be at most %{maximum} characters long"
      too_short: "%{attribute} must be at least %{minimum} characters long"
      wrong_length: "%{attribute} must be %{expected} characters long"
      taken: "%{attribute} is already taken"
      not_a_number: "%{attribute} must be a number"
      not_an_integer: "%{attribute} must be an integer"
      greater_than: "%{attribute} must be greater than %{minimum}"
      greater_than_or_equal_to: "%{attribute} must be greater than or equal to %{minimum}"
      equal_to: "%{attribute} must be equal to %{expected}"
      not_equal_to: "%{attribute} must not be equal to %{not_expected}"
      less_than: "%{attribute} must be less than %{maximum}"
      less_than_or_equal_to: "%{attribute} must be less than or equal to %{maximum}"
      value_between: "%{attribute} must be between %{minimum} and %{maximum}"
      primitive: "%{attribute} must be of type %{primitive}"
emmanuel commented 12 years ago

@snusnu — this is great!

Someday I'll figure out how to set up dm-validations to work out-of-the-box with i18n (including putting your yml in 18n's load path) while still keeping the i18n dependency optional.

snusnu commented 12 years ago

fwiw, here are my german translations for the above:

  datamapper:
    errors:
      absent: "%{attribute} darf nicht vorhanden sein"
      inclusion: "%{attribute} muss einer der folgenden Werte sein: %{set}"
      invalid: "%{attribute} hat ein ungültiges Format"
      confirmation: "%{attribute} stimmt mit der Bestätigung nicht überein"
      accepted: "%{attribute} wurde nicht akzeptiert"
      nil: "%{attribute} muss vorhanden sein"
      blank: "%{attribute} muss ausgefüllt sein"
      length_between: "%{attribute} muss zwischen %{min} und %{max} Zeichen lang sein"
      too_long: "%{attribute} darf maximal %{maximum} Zeichen lang sein"
      too_short: "%{attribute} muss mindestens %{minimum} Zeichen enthalten"
      wrong_length: "%{attribute} muss genau %{expected} Zeichen enthalten"
      taken: "%{attribute} wird bereits verwendet"
      not_a_number: "%{attribute} muss eine Zahl sein"
      not_an_integer: "%{attribute} muss eine Ganzzahl sein"
      greater_than: "%{attribute} muss größer als %{minimum} sein"
      greater_than_or_equal_to: "%{attribute} muss größer oder gleich %{minimum} sein"
      equal_to: "%{attribute} muss den Wert %{expected} haben"
      not_equal_to: "%{attribute} darf nicht den Wert %{not_expected} haben"
      less_than: "%{attribute} muss einen Wert kleiner als %{maximum} haben"
      less_than_or_equal_to: "%{attribute} muss einen Wert kleiner oder gleich %{maximum} haben"
      value_between: "%{attribute} muss einen Wert zwischen %{minimum} und %{maximum} haben"
      primitive: "%{attribute} muss vom Typ %{primitive} sein"
mmontossi commented 12 years ago

Hi, I tried the custom MessageTransformer and the DefaultI18n that comes with the new version of the DataMapper but for some reason "model_name" it's now available and I get "undefined method `model_name' for".

Changing model_name to storage_name solves the problem, I'm really new to DataMapper maybe I'm doing something wrong?

zaldip commented 10 years ago

Hi, I am using ruby 2.0.0, sinatra 1.4.4 and Datamapper 1.2.0. I can´t get datamapper working with DefaultI18n.new. I tried to add this line: DataMapper::Validation::Violation.default_transformer = DataMapper::Validation::MessageTransformer::DefaultI18n.new Inside the configure block of my sinatra app.

Any help would be great :)