Rails 3 translations within models in production - ruby-on-rails

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

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.

How does one alias a method that does not yet exist? (Ruby Decorator)

This is going to sound pretty crazy, but I'm trying to build a generic decoration-based system which will allow a decorated class to do all kinds of crazy stuff with attributes. The goal is to be able to define attributes at a high level, decorate an ORM class (ActiveRecord, for example, though our primary case is actually quite a bit different), and use those decorations in various places in the app to automate some dynamic "magic' our app needs. For instance, we'll use the attributes to automatically generate forms and views, translate complex form hashes into flatter structures, etc.
To accommodate both use cases we've identified so far, I have a mixable module and a decorator (using Draper so Rails form magic still works, though I'm not married to Draper necessarily) which look more or less like this (obviously lots of details are omitted):
class DecoratorThing < Draper::Decorator
include CoreMixinStuff
delegate_all
end
module CoreMixinStuff
extend ActiveSupport::Concern
module ClassMethods
def attribute(stuff, blah)
attribute = AttributeDefinition.new(...)
add_translation_methods(attribute)
...
end
def add_translation_methods(attribute)
name = attribute.name
reader = name
writer = "#{name}="
# In the case of field wrappers, we have to alias the original reader and writer so we
# don't overwrite them completely
if attribute.translation_type == :wrapper
alias_method :"_orig_#{reader}", reader
alias_method :"_orig_#{writer}", writer
# Otherwise, we need to error if the reader or writer would collide
elsif instance_methods.include?(reader) || instance_methods.include?(writer)
raise RuntimeError.new("Cannot define an attribute which overrides existing methods (#{name.inspect})")
end
end
end
end
Then the actual decorator for a specific instance does things like this:
class FooDecorator < DecoratorThing
decorates Foo
attribute :field, multiple: true, serialize: true
attribute :field2, field: :delegation_field
attribute :field3 do |field|
field.subtype ...
field.subtype ...
end
end
The intent there would be to allow Foo#field to take an array and serialize it internally into a string before sending it off to wherever the decorated object takes it. Foo#field2 would just pass data as-is to delegation_field. Foo#field3 would take a complex hash of data and delegate it to the subtype fields.
The latter two cases are painful, but I have them working in a prototype. The first is the problem because of the alias_method stuff above - since the attribute method is run on the decorator, the method I'm trying to alias doesn't actually exist yet. It's not until FooDecorator.new(some_foo_instance) is called that those other instance methods are available.
I think my options are limited to the following, but I'm hoping there's some better choice:
Give up on decoration and just accept that this whole thing has to be a mixin instead
Give up on the mixin, requiring decoration instead, and go through the decorated object rather than aliasing the methods
Give up on field wrappers and require attributes to always have a unique name and delegate to a field (in the example above, attribute :field... would become attribute :field_wrapper, multiple: true, serialize: true, field: "field")
Give up on serializing data, making it the user's responsibility to properly define / override methods to handle the data they define
The fourth option is probably the sanest, but I've made a lot of assumptions around being able to serialize, so if somebody else knows a nice way to make this happen, that'd be super swell.
I think I have an answer that fits this question.
Basically, my solution adds a proxy column over the top of an existing column associated with an active-record model.
This proxy column overrides the original column's accessor methods. The basic proxy functionality doesn't do much beside set and get the value of original columns.
The actual code is quite long so I've created a gist for it. Check it out here.
The proxy column object can be subclassed/overridden to provide any sort of functionality required. For example, in your code AttributeDefinition would subclass ProxyColumn. The AttributeDefinition would act as a proxy between the different attributes and the values. It could wrap/unwrap the attributes value in a DecoratorThing based on whatever conditions that you deem appropriate.
One thing it doesn't do, at this stage, is provide a way of overriding how/what methods are added. However, all that could easily be changed based on your needs. I actually based that part off your add_translation_methods method anyways.
This is the basic usage to define the columns:
class MyModel
# Add the concern to get the functionality
include DataCollectionElements # concern
# create proxy column and set various options
proxy_column :my_col_1
proxy_column :my_col_2, :alias => [:col1, col2, col3]
proxy_column :my_col_3, :column => :actual_col_name
# define the proxy object using class/string/symbol
proxy_column :my_col_4, :proxy => MyProxy # or 'MyProxy' or :my_proxy
# define the proxy object using a proc/lambda
proxy_column :my_col_5, :proxy => ->(model, code, options) { MyProxy.new(model, code, options) }
proxy_column :my_col_6, :proxy => proc {|model, code, options| MyProxy.new(model, code, options) }
# define the proxy object using a block
proxy_column :my_col_7 do |model, code, options|
MyProxy.new(model, code, options)
end
end

How to perform Rails model validation checks within model but outside of filters using ledermann-rails-settings and extensions

Background
I'm using ledermann-rails-settings (https://github.com/ledermann/rails-settings) on a Rails 2/3 project to extend virtually the model with certain attributes that don't necessarily need to be placed into the DB in a wide table and it's working out swimmingly for our needs.
An additional reason I chose this Gem is because of the post How to create a form for the rails-settings plugin which ties ledermann-rails-settings more closely to the model for the purpose of clean form_for usage for administrator GUI support. It's a perfect solution for addressing form_for support although...
Something that I'm running into now though is properly validating the dynamic getters/setters before being passed to the ledermann-rails-settings module. At the moment they are saved immediately, regardless if the model validation has actually fired - I can see through script/console that validation errors are being raised.
Example
For instance I would like to validate that the attribute :foo is within the range of 0..100 for decimal usage (or even a regex). I've found that with the previous post that I can use standard Rails validators (surprise, surprise) but I want to halt on actually saving any values until those are addressed - ensure that the user of the GUI has given 61.43 as a numerical value.
The following code has been borrowed from the quoted post.
class User < ActiveRecord::Base
has_settings
validates_inclusion_of :foo, :in => 0..100
def self.settings_attr_accessor(*args)
>>SOME SORT OF UNLESS MODEL.VALID? CHECK HERE
args.each do |method_name|
eval "
def #{method_name}
self.settings.send(:#{method_name})
end
def #{method_name}=(value)
self.settings.send(:#{method_name}=, value)
end
"
end
>>END UNLESS
end
settings_attr_accessor :foo
end
Anyone have any thoughts here on pulling the state of the model at this point outside of having to put this into a before filter? The goal here is to be able to use the standard validations and avoid rolling custom validation checks for each new settings_attr_accessor that is added. Thanks!
Here is a newer version that works in the new 2x syntax. Yes it is ugly and does double eval.
This produces namespaces method names and adds them to the attr_accessible list. The names are in the form of "#{namespace}_#{attribute} and can be use in forms. I am monkeying with a ppatch to the gem to do this automatically but I an not there yet.
has_settings do |s|
eval 'def self.settings_accessors(namespace, defaults)
defaults.keys.each do |method_name|
attr_accessible "#{namespace}_#{method_name}"
eval "def #{namespace}_#{method_name}
self.settings(:#{namespace.to_s}).send(:#{method_name})
end
def #{namespace}_#{method_name}=(value)
self.settings(:#{namespace}).send(:#{method_name}=, value)
end
"
end
end'
namespace = :fileshare
defaults = {:media => false, :sit => false, :quota_size => 1}
s.key namespace, :defaults => defaults
self.settings_accessors(namespace, defaults)
end

How to use a gem method (returning strings) as custom validation in rails app

I'm new to RoR/Gems, this is a basic question.
I created a gem, MyNameGem, in order to learn the process. It contains these methods:
def returnValidationString1
puts 'Validation String'
end
def returnValidationString2
puts 'ANother Validation String'
end
I included the gem in a simple rails app, everything seems to be working as expected.
I this to my model:
validates :name => MyNameGem.returnValidationString1
What I'm trying to create is a gem that I can use inside a validation routine. So, for example, I want to do this: validates :name => (call my gem method, return a string, and use that string as the validation requirement)
puts only prints to console.
if you want to return 'MyNameGem' write return 'MyNameGem' or simply 'MyNameGem because the last line gets returned automatically.
The function of puts is to put things to the console, so that's exactly what it will do. What your validates call is doing is kind of unusual though, and doesn't seem to make any sense. Your code evaluates to:
validates :name => (puts "MyNameGem")
That really doesn't mean anything. puts usually returns nil.
If you want to write a custom validation routine that's stored in a gem, that's a different question.

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!

Resources