-
Notifications
You must be signed in to change notification settings - Fork 84
delocalize 1.0
I've been thinking a lot about the issues that delocalize currently has and has had in the past.
Lots of the issues that we've had with delocalize over the years seem to have come from the fact that it is just too involved with Rails inner workings – ActiveRecord/ActiveModel in particular. The tight coupling to AR/AM is particularly worrying since i18n/l10n are frontend rather than backend concerns: It shouldn't be the model layer's responsibility to parse user input and since delocalize is currently implemented in the model layer, it is doing things on the wrong level.
I think the issue is similar to what we've seen with attr_accessible
/attr_protected
and the new strong_parameters gem as well as issues outlined by various people about putting business logic into ActiveModel callbacks (e.g. the famous example where creating a user always sends a confirmation email). Hence, I think the solution to the problem at hand is similar: We need to move delocalize from the model to the controller layer.
I think, delocalize could be used for parsing and converting the params
hash directly. The syntax could actually be quite similar to the spike
branch:
class Admin::ProductsController < Admin::BaseController
delocalize :product => { :price => :number, :available_at => :datetime }
end
delocalize would then be executed as a before_filter
, check if params[:product]
is set and finally check for the presence of price
and available_at
and convert them as usual.
Regarding the localization of stored data (i.e. forms), delocalize could store the data based on conventions and use them in forms automatically. For cases where conventions can't be followed, the data could be exposed via a helper method. I'm thinking along the following lines:
- A model
Product
has a numeric fieldprice
and anavailable_at
datetime (just like in the example above). - If the controller uses the
delocalize
filter mentioned above, the localized values are automatically stored in a hash that is accessible using a helper method. - This helper method could provide an API like this:
delocalize_localized_value(@product, :price)
. This would result in a lookup likedelocalize_localized_values[:products]['1'][:price]
(1 is the primary key). - If one needs to break the convention, the helper method can be used manually.
While I acknowledge that this means more setup compared to the original implementation (even more than the spike
branch), it has the benefit of being much less involved with Rails' inner workings and thus being much less brittle. Also, delocalizing could be done on a case by case basis (e.g. not using it in API controllers where programmers can be trusted/forced to use certain formats) and even customized as necessary just like any other before_filter
.
Also note that I haven't quite figured out the localization part – so consider this work in progress and feel free to share your thoughts on that part.
What are your opinions on this?
Please append your opinions to this wiki page and include your GitHub username followed by the date and your thoughts. Thanks!
I think that's exactly the right thing to do, so +1 from me. (Last time I struggled with some problem caused by delocalizing in the model layer, I tried to rethink the whole concept and came to conclusion very similar to yours … never got around to implement it, though :-| )
I also think this is the way to go. I hacked together a quick-and-dirty Rails controller concern which implements this functionality:
module Delocalizer
extend ActiveSupport::Concern
module ClassMethods
def delocalize(attributes)
before_filter -> { delocalize_params(params, attributes) }
end
end
private
def delocalize_params(params, attributes)
attributes.each do |key, value|
next unless params.key?(key)
if [:date, :time, :numeric].include?(value)
params[key] = value.to_s.classify.constantize.parse_localized(params[key])
else
delocalize_params(params[key], value)
end
end
end
end
This also works with nested attributes:
delocalize item: { amount: :numeric, effective_date: :time,
transaction_attributes: { amount: :numeric, effective_date: :time } }
I guess this still needs some work, but currently I'm using the above code in production (except for some syntactic sugar provided by Rails 4.0 and Ruby 2.0 )
One additional thought: I think it would be possible to derive the data types by looking at the models (Model.columns_hash['my_column_name'].type
) That way the delocalize
statement could be condensed and it would look a lot like strong_parameters
code:
delocalize item: [:amount, :effective_date, transaction_attributes: [:amount, :effective_date]]
But that would definitely be a lot more code and would also introduce additional work for other ORM adapters like Mongoid etc, so I'm not sure if this is worth the effort.
Clemens: Thanks for sharing this, Daniel, that looks intriguing. Questions:
- Is there a specific reason why you use the proc form for your
before_filter
?params
andattributes
would both be accessible directly in the filter, right? - Do you have any input on the localizing part (i.e. forms)?
Daniel:
I think here the proc syntax is the most convenient one. Note that delocalize_params
is called recursively with a subset of params
and attributes
, too. It would be possible to use a different method for the first level of the recursion in order to avoid the proc syntax of before_filter
, but personally I don't think the code quality would benefit from that.
I'm not sure if I correctly understood the part on form localization. If it is about setting the value of form inputs (e.g. value: number_to_currency(@product.price)
) then currently I can't imagine any helper/dsl/syntax which is more concise than manually calling the existing I18n helpers. Especially considering the abundance of possible parameters. Even with the current system I often end up providing a value
manually more often than sticking with the one provided by delocalize.
I don't particularly like the repetition of specifying each attribute that needs to be delocalized per controller.
I've tried to play around with it and came up with the following:
class ApplicationController < ActionController::Base
unlocalize /.*date/ => :date,
/date_of_birth/ => :date,
/.*time/ => :time,
/.*_amount/ => :numeric,
/.*_rate/ => :numeric
end
This will scan any parameters matching the regular expressions provided, and delocalize as required.
see code at: https://gist.github.com/paolodona/6214d5c677f640e19d41
Also, this works with nested hashes or param arrays, let me know what you think.
Rails was previously using attr_accessible
for mass-assignment parameter filtering. Now its using strong_parameters as a way to do that. What do you guys think about apply delocalization on that moment?
I mean, at the controller something like this:
def order_params
params
.require(:order)
.permit(:customer_id, :value, :emission_at)
.delocalize(:decimal => :value, :date => :emission_at)
end