Skip to content

Better localizability for dm-validations #20

Open
solnic opened this Issue May 17, 2011 · 19 comments

7 participants

@solnic
DataMapper member
solnic commented May 17, 2011

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.

  • a default implementation when using dm-validations stand-alone,
  • dm-rails can tie it into the i18n part of Rails.

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
DataMapper member
solnic commented May 17, 2011

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
DataMapper member
solnic commented May 17, 2011

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
DataMapper member
solnic commented May 17, 2011

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
DataMapper member
solnic commented May 17, 2011

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
DataMapper member
solnic commented May 17, 2011

This feature would help me out

by Xavier Shay

@solnic
DataMapper member
solnic commented May 17, 2011

I pushed a second version implementing a cleaner way of transforming errors (see gix/dm-validations@7145db7#diff-0).

Some unresolved things:

  • I kept a single class for errors (called Violation), because I see no value in abstracting that further (you would need around 27 different classes to cover all combinations), it certainly does not really help dealing with additional validator-specific values.
  • I’d like to have some input on whether the class property to set a transformer is the best way to go (I’d prefer clean DI but that’s not really possible with DM).
  • Method and block validators aren’t changed yet so that they return violation objects, too, instead of messages.
  • How should custom messages be handled with multiple validation contexts? The specs use a message hash with the context symbol as key, but the whole hash is passed in as "error message" which seems weird to me because I have to remember the context to retrieve the appropriate message.

    by gix

@solnic
DataMapper member
solnic commented May 17, 2011

[bulk edit]

by Dan Kubb (dkubb)

@nhoffmann

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
DataMapper member
solnic commented May 14, 2012

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

@nhoffmann

That's great news. Thanks solnic

@shingara

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

@solnic
DataMapper member
solnic commented May 23, 2012

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

@emmanuel
DataMapper member

@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
DataMapper member

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
DataMapper member
snusnu commented Jun 5, 2012

@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
DataMapper member
emmanuel commented Jun 5, 2012

@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
DataMapper member
snusnu commented Jun 6, 2012

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

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
zaldip commented Nov 14, 2014

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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.