Skip to content
Gregório Chalinski Kusowski edited this page Apr 2, 2014 · 13 revisions

Idea

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 field price and an available_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 like delocalize_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!

Your thoughts

@noniq (22 April 2013)

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

@daniel-rikowski (30 June 2013)

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 and attributes 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.

@paolodona (27 Jan 2014)

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.

@gregoriokusowski (02 Apr 2014)

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