Localization of custom validations - ruby-on-rails

I'm working on a project in which I'd like to move all strings used in the app to a single file, so they can be easily changed and updated. However I'm having trouble with the custom validation. I have validations in my app as follows:
validate :thing_is_correct
def thing_is_correct
unless thing.is_correct
errors[:base] << "Thing must be correct"
end
end
I'm not sure how to move "Thing must be correct" into my en.yml file and out of the model. Any help would be greatly appreciated.

The Rails Way to do that would be to use the mechanism described in the Guides.
errors is an instance of ActiveModel::Errors. New messages can be added by calling ActiveModel::Errors#add. As you can see in the docs, you can not only pass a message but also a symbol representing the error:
def thing_is_correct
unless thing.is_correct?
errors.add(:thing, :thing_incorrect)
end
end
Active Model will automatically try fetching the message from the namespaces described in the Guides (see the link above). The actual message is generated using ActiveModel::Errors#generate_message.
To sum up:
Use errors.add(:think, :thing_incorrect)
Add thing_incorrect under one of the YAML keys listed in the Guides.

You can access the I18n inside the model.
validate :thing_is_correct
def thing_is_correct
unless thing.is_correct
errors[:base] << I18n.t('mymodel.mymessage')
end
end
Inside config/locales/en.yml
en:
mymodel:
mymessage: "Thing must be correct"
Inside another locale: (config/locales/es.yml)
es:
mymodel:
mymessage: "Esto debe ser correcto"
If you set I18n.locale = :en, the message inside en.yml will be displayed. If you set it to :es, the one inside es.yml will be used instead.

Related

Rails 4: remove attribute name from error message in custom validator

In my Rails 4 app, I implemented a custom validator named LinkValidator on my Post model:
class LinkValidator < ActiveModel::Validator
def validate(record)
if record.format == "Link"
if extract_link(record.copy).blank?
record.errors[:copy] << 'Please make sure the copy of this post includes a link.'
end
end
end
end
Everything works fine, except that currently, the message displayed is:
1 error prohibited this post from being saved:
Copy Please make sure the copy of this post includes a link.
How can remove the word "copy" from the above message?
I tried record.errors << '...' instead of record.errors[:copy] << '...' in the validator, but then the validation no longer works.
Any idea?
Unfortunately currently the format of full_messages for errors is controlled with a single I18n key errors.format, hence any change to it will have a global consequences.
Common option is to attach error to the base rather than to the attribute, as full messages for base errors do not include attribute human name. Personally I don't like this solution for number of reason, main being that if the validation error is caused by a field A, it should be attach to field A. It just makes sense. Period.
There is no good fix for this problem though. Dirty solution is to use monkey patching. Place this code in a new file in your config/initializers folder:
module ActiveModel
class Errors
def full_message(attribute, message)
return message if attribute == :base
attr_name = attribute.to_s.tr('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
klass = #base.class
I18n.t(:"#{klass.i18n_scope}.error_format.#{klass.model_name.i18n_key}.#{attribute}", {
:default => [:"errors.format", "%{attribute} %{message}"],
:attribute => attr_name,
:message => message
})
end
end
end
This preserves the behaviour of full_messages (as per rails 4.0), however it allows you to override the format of full_message for particular model attribute. So you can just add this bit somewhere in your translations:
activerecord:
error_format:
post:
copy: "%{message}"
I honestly dislike the fact that there is no clean way to do this, that probably deserves a new gem.

Remove prepended attribute name from full error messages for errors[:base]

We're creating a Rails form that uses the dynamic_form gem to display error messages. For one of the models the form interacts with, we created a custom validator:
class Contact < ActiveRecord::Base
belongs_to :agency
validate :new_signups_have_contact_information
def new_signups_have_contact_information
if agency && agency.is_new_signup?
unless (email && email != '') || (phone && phone != '')
errors[:base] << "Please include a phone number or email."
end
end
end
end
So far, so good. When we display these errors our view, though:
<%= form_for #contact do |contact_form| %>
<%= contact_form.error_messages %>
<%# snip %>
<% end %>
We get this message in the errors that are generated when the validation fails:
Contacts base Please include a phone number or email.
How can we remove the prepended "Contacts base" from the generated error string?
We've done some research: We know that this is happening because, by default, Rails' error message system automatically prepends attribute names to their error strings. In addition, in most cases, we can modify the English localization file to remove the prepended strings.
The official Rails guide to do so, however, does not list how to change the localization for error messages given to the errors[:base] array or generated using custom validations. It lists how to override the generated strings for all of the built-in validations, but no other errors.
So, one approach we'd be willing to use: How do we configure config/locales/en.yml to remove the prepended "Contacts base" string?
Another approach we researched that we would rather not use is opening up the ActiveRecord::Errors class and writing our own implementation of the errors#full_messages function. (This blog post describes the technique.) This approach changes the behavior of the ActiveRecord::Errors class for the entire project, and we would rather use an approach that has a much more local impact. If we can't use the lcoalization file to achieve what we want, is there a more straightforward way to do so than opening up the ActiveRecord:Errors class?
EDIT
The contact.errors hash is:
$ contact.errors.to_yaml
--- !ruby/object:ActiveModel::Errors
base: !ruby/ActiveRecord:Contact
attributes:
id:
# snip
messages: !omap
- :base:
- Please include a phone number or emai.
Looks like your validator is making :base appear as an attribute, override it with this locale.
# config/locales/en.yml
en:
activerecord:
attributes:
contacts:
base: ""
Keep in mind various attributes of the error message can also be configured
# config/locals/en.yml
en:
activerecord:
attributes:
contacts:
base: ""
errors:
messages:
blank: "missing"

Rails 3 translations within models in production

I'm trying to translate an app into Japanese and everything was going smoothly until I put it into production.
As cache_classes is now true any translation within a model reverts to the default locale.
I know I'm probably supposed to target the translations directly in the yml file but I'm not sure how I would do that for the following simplified code:
class TimeseriesForecast < ActiveRecord::Base
##field_names = {
:location_name => I18n.t('forecast_timeseries.location_name'),
:local_date_time => I18n.t('forecast_timeseries.local_date_time'),
:zulu_date_time => I18n.t('forecast_timeseries.zulu_date_time'),
:temp_mean => I18n.t('forecast_timeseries.temp_mean')
}
end
Many thanks
Your I18n.t() call is evaluated at compile time since you are defining class variables, not instance variables. You need to put your call to I18n.t where they will be evaluated at runtime.
But if you want to translate ActiveRecord field names, use human_attribute_name and provide your translations via YML. You do not need to manually provide translations, Rails handles it all for you automatically.
The respective documentation is at http://guides.rubyonrails.org/i18n.html Chapter 5.1.
Don't use I18n.t or translate method in your models. You can do this instead:
In your model
Use something like this to add internationalized errors to the name attribute of your model (Check documentation: ActiveModel/Errors/method-i-add):
self.errors.add(:name, :your_error_key)
# The error key could be something like :wrong_name
NOTE: Sometimes you won't even need to add errors with errors.add method. For example if you add validations in your model with somethind like this:
validates :name, presence: true
Rails will add an error with the key :blank (the key depens on the validation type). In other words rails internally will issue self.errors.add(:name, :blank)
In your locale
Then in your locale.jp.yml can use any of this (just one):
activerecord.errors.models.[model_name].attributes.[attribute_name]
activerecord.errors.models.[model_name]
activerecord.errors.messages
errors.attributes.[attribute_name]
errors.messages
In your case replace [model_name] with timeseries_forecast and [attribute_name] with your_error_key
For example:
en:
errors:
messages:
your_error_key: "Your error message in english"
Don't think you're improving performance by caching the names in the class. Make it a method instead.
class TimeseriesForecast < ActiveRecord::Base
def self.field_names
{ :location_name => I18n.t('forecast_timeseries.location_name'),
:local_date_time => I18n.t('forecast_timeseries.local_date_time'),
:zulu_date_time => I18n.t('forecast_timeseries.zulu_date_time'),
:temp_mean => I18n.t('forecast_timeseries.temp_mean') }
end
end
# usage
TimeseriesForecast.field_names
Better yet, return just the actual fields and do the translation in the view, if you're gonna be strict MVC about it (some Rails methods - like collection_select - make it harder to do that though, hence the suggestion above).

Display link in Rails form error message

On our sign-up form, we validates_uniqueness_of :email
When the a user is attempting to use our sign up form and they specify an existing email address, I'd like them to see an error message like this
This email address is already in use. If you're having trouble logging in, you can reset your password
Obviously, I'd like to use the named route for the link, but my User model does not have access to it. How can I accomplish this?
Side note: We will be offering translations for our app soon and all of these error messages will end up in YAML files. Can I somehow inject my new_password_url in a message in my YAML locale files? (e.g., config/locales/en.yml)
I know this is an old question, but for future users who want to insert a link into an error message, here are some guidelines that worked for me.
First, the I18n error messages are assumed html safe already, so you can go ahead and write a suitable error message. In this example, I'm changing an "email is taken" message.
# config/locales/en.yml
activerecord:
errors:
models:
user:
attributes:
email:
taken: 'has already been taken. If this is your email address, try logging in instead.'
Notice the interpolated variable %link.
Now all you need to is pass in a value for that variable in your validator, like so:
# app/models/user.rb
validates :email, :uniqueness => {:link => Rails.application.routes.url_helpers.login_path}
(By default, any options you pass in here will automatically be sent over to the I18n translator as variables, including some special pre-populated variables like %value, %model, etc.)
That's it! You now have a link in your error message.
This may not streamline well with the translations, but here's a suggestion:
In your user_controller#create action, wrap everything you already have with an if statement. Here's a rough example:
class UserController < ApplicationController
...
def create
if User.find(params[:email])
flash[:alert] = "This email address is in use. You can ".concat(generate_reset_password_link(params[:email])
render :action => 'new'
else
<your current code>
end
end
After this, you'll have to write a helper method generate_reset_password_link, but I think this mostly respects the MVC layout. The controller is meant to interface with the view and model. It is violating the DRY principle a little, since you're essentially bypassing validates_uniqueness_of :email, but you get some custom behavior. DRY doesn't seem to be 100% achievable to me if you want to make more complex apps, but perhaps you can refine this and prove me wrong ;)
You may have to massage this a little so that the render :action => 'new' will repopulate itself with the previously entered data (in case the user just mistyped his own email address and it actually isn't in the system).
If you decide to use this approach, I would throw a comment in both the controller and the model indicating that the email uniqueness is essentially checked in 2 places. In the event someone else has to look at this code, it'll help them to understand and maintain it.
You can place a tag of your own like ~[new_password_url] in your error messages. Then at the point of rendering your error messages gsub ur tag with the actual. if you want to do it generically you can get the path out using regexp and then eval it to get the url then gsub it back in. make you use the raw method if you are putting html into your text.
If you're using 2.3.x, replace your call to error_messages with your own helper, written in UsersHelper. It should accept the FormBuilder or an ActiveRecord object and adjust the error message as you see fit. You could make as many customizations as you like, or it could be as simple as a gsub:
def user_error_messages(f)
find_error = "This email address is already in use."
replacement = "This email address is already in use. #{link_to(...)} to reset your password"
f.error_messages.sub(find_error, replacement).html_safe
end
If you're using Rails3, make a helper method to simply process #user.errors.full_messages before they're emitted to the view.
Stumbled across this today:
http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html
If you need to access this auto-generated method from other places (such as a model), then you can do that by including ActionController::UrlFor in your class:
Step 1
Getting awareness of named routes to the model is the hard part; this gets me most of the way.
Now I can do something along the lines of
class User < ActiveRecord::Base
include Rails.application.routes.url_helpers
def reset_password_uri
new_user_password_path(self)
end
end
# User.find(1).reset_password_uri => "/users/password/new"
Step 2
So we have access to the named route, but now we need to inject it into the YAML message.
Here's what I just learned about YAML variables:
# en.yml
en:
welcome: "Hello, %{username}!"
# es.yml
es:
welcome: "¡Hola, %{username}!"
I can inject the username by including a hash with the t method
<div id="welcome">
<%= t :welcome, :username => #user.username %>
</div>
Step 3
Now we just need a way to add interpolation to the error message described in the original question. This is where I am currently stuck :(
After hours trying to figure this out for Rails 4 with devise, I realised you can just add the link directly into the validation:
# app/models/user.rb
validates :username, presence: true, uniqueness: {:message => "username has already been taken - <a href='/users'>search users</a>" }
where the link in this case is my users index. Hopefully this will help someone!

Using Rails validation helpers :message but want it without listing the column name in message

Using Rails validation helpers-
validates_xxx_of :the_column, :message => "my message"
will generate a validation message :
the_column my message
Is there a way to turn-off the inclusion of the column name?
(substitute xxx with any validation helper method)
I know this question is old. But just for reference in case someone else stumbles over it as I just did.
At least in Rails 3.0.x (not sure about earlier versions) you can use the leading ^ as indicated by RadBrad without the need for any gems/plugins.
Rails 4 answer
Took a look at the code here:
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb#L374
You can therefore set in your en.yml file:
en:
errors:
format: '%{message}'
This means you need to also set the full error message everywhere else but I guess this is preferable. Note, and I found this confusing, the errors format is not in the ActiveRecord namespace where I generally put the rest of my error messages.
There is a customer error message gem that should do what you want
https://github.com/jeremydurham/custom-err-msg
It allows you to override the normal message construction, and define the complete message yourself like this:
:message=> "^ Your email address seems rather messed up, please try again"
Notice the ^ character, that tells rails NOT to prepend anything, just use the message exactly as defined, (except it removes the ^)
If you don't put a leading ^, then you get the normal rails generated error message.
This is the best explanation I could find.
http://adamhooper.com/eng/articles/5
Essentially, in an initializer, change the full_messages method in ActiveRecord.Errors to return full sentences (not column_name, message concatenations) if you give a :message attribute in the validation.
Update - If you try Adam's code, you have to use the en.yml property file, if not it won't work as expected. You can either do this or get around it by modifying the full_messages method further. This works for me. I added the following to an initializer (/imitializers/active_record_errors.rb)
if RAILS_GEM_VERSION =~ /^2\.3/
ActiveRecord::Errors.class_eval do
# Remove complicated logic
def full_messages
returning full_messages = [] do
#errors.each_key do |attr|
#errors[attr].each do |message|
next unless message
if attr == "base"
full_messages << message
elsif message =~ /^\^/
full_messages << $' #' Grabs the text after the '^'
else
attr_name = #base.class.human_attribute_name(attr)
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
end
end
end
end
end
end
end
Adam also makes good arguments for modifying Rails to support this for internationalization efforts.
The way that I did this was override ALL the messages, and not use the Rails form helpers for displaying error messages.
This seems like a lot of work, but it actually has some nice benefits. You get full control of the message, and then you can implement a custom form builder that can put the error messages inline, which is nicer for the user.
You use it like this:
validates_uniqueness_of :foobar, :message => "The foobar isn't unique."
Then don't use full_messages when printing the error message.
the link to rubyforge doesn't work, here is the custom error message plugin on github:
http://github.com/gumayunov/custom-err-msg
Why not simply use the #object.errors hash ?!
Change the messages as you with in the validate part:
validates_uniqueness_of :foobar, :message => "The foobar isn't unique."
And then,
#foo.errors.to_a
You get a nice array where the second entry is the error message itself.
In rails 2, you can do on your model:
validate :test_validation
private
def test_validation
if error_condition
errors.add_to_base("Message")
end
end
In rails 3 or greater:
validate :test_validation
private
def test_validation
if error_condition
errors[:base] << "Message"
end
end
I too had the same issue with RoR 3.0.3. I couldn't find a way to display the validation messages without the name of the attributes. The code that Swards posted earlier didn't work for me, but it was a good start.
I placed the following code in an RB file in the config/initializers folders:
ActiveModel::Errors.class_eval do
# Remove complicated logic
def full_messages
full_messages = []
each do |attribute, messages|
messages = Array.wrap(messages)
next if messages.empty?
if attribute == :base
messages.each {|m| full_messages << m }
else
attr_name = attribute.to_s.gsub('.', '_').humanize
attr_name = #base.class.human_attribute_name(
attribute,
:default => attr_name
)
options = { :default => "%{message}", :attribute => attr_name }
messages.each do |m|
full_messages << I18n.t(:"errors.format", options.merge(:message => m))
end
end
end
full_messages
end
end
This removes the name of the attributes from all messages, which is exactly what I wanted to do.
I've found this answer more useable:
validate uniqueness amongst multiple subclasses with Single Table Inheritance
Rails 3.2 future-proof solution
For those who stumbled here (and potentially scrolled to bottom) looking for how to do this in later versions of Rails, here's some good news for you: it will be pretty simple to do this in Rails 4 after this pull request is merged. It may need some additional polishing for some scenarios, but progress has been made here.
Until then, you can monkey-patch Rails in your project with the pull request :)
class ActiveModel::Errors
def full_message(attribute, message)
return message if attribute == :base
attr_name = attribute.to_s.tr('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
I18n.t(:"errors.formats.attributes.#{attribute}", {
:default => [:"errors.format","%{attribute} %{message}"],
:attribute => attr_name,
:message => message
})
end
end
And add following to your locale file:
en:
errors:
formats:
attributes:
name: '%{message}'
Here's a fairly straightforward implementation that should do the trick. Notably, it only affects a single model (unlike most locale-based tricks I've seen) and isn't too heavy-handed.... it just modifies a single object:
class Widget < ActiveRecord::Base
validates_numericality_of :quantity, greater_than: 0, message: "numericality"
def errors
super.tap do |e|
e.extend(FixQuantityErrorMessage)
end
end
module FixQuantityErrorMessage
def full_message(attribute, message)
if attribute.to_s == 'quantity' && message == "numericality"
"You need at least one!"
else
super
end
end
end
end

Resources