Bridge Globalize and TZInfo

The Globalize plugin integrates Internationalization (i18n) and Localization (L10n) by default with very little effort.

However, its trivial to bridge the Globalize::Country model with the TZInfo gem and tzinfo_timezone plugin and have DRY timezones indexed by country, with daylight savings time covered.

TZInfo gem

An OS independent (no zoneinfo files required) library that provides daylight savings aware conversions.

Install with:

gem install tzinfo

tzinfo_timezone plugin

A replacement for Rail's bundled TimeZone class, released by Jamis Buck.Note the depracated

TimeZone#unadjust
and
TimeZone#adjust
needs to be replaced with
TimeZone#localto_utc
and
TimeZone#utcto_local
respectively.

Install with:

ruby script/plugin install tzinfo_timezone

The hacks

In a perfect world this could be wrapped in a plugin, but implementation is fairly application specific, thus the following is just to illustrate the concept.Adapt accordingly.

Make Globalize::Country Timezone aware

Wrab this in a library required by your environment.rb

   
 Globalize::Country.class_eval do

    #Add associations specific to your application 
    has_many :accounts, :class_name => 'Account'
    has_many :addresses, :class_name => 'Address'  

    def timezones 
      TZInfo::Country.get( self.code ).zone_names 
    end  

  end

Which should yield the following from the console:

   
  >> Globalize::Country.find_by_code('PT').timezones
  => ["Europe/Lisbon", "Atlantic/Azores", "Atlantic/Madeira"]

Adding a Timezone to your Account ( User etc. )

Application specific models:

   
  class Account < Activerecord::Base
    belongs_to :country, :class_name => 'Globalize::Country'
    has_one :setting, :dependent => :destroy

    delegate :tz, :to => :setting   
  end

  class Setting < ActiveRecord::Base
     belongs_to :account
     composed_of :tz, :class_name => 'TZInfo::Timezone', 
                                   :mapping => %w(timezone time_zone) 
  end

The user interface

Controller action:

  class Admin::SettingController < Admin::BaseController

     def edit
       #@account instance variable stubbed - set in inherited before_filter
       @setting = @account.setting   
       %w(account setting).collect{|o| instance_variable_get( "@#{o}".to_sym ).send( :attributes=, params[o.to_sym]) }
       if request.post? && [@setting, @account].reject(&:save).empty? 
          #stubbed for clarity
       end
     end

  end

Helper methods:

  def country_to_currency_map
    map ||= {}
    Globalize::Country.find(:all).each{|c| map.store(c.id, c.currency_code) }
    map.to_json 
  end

  def country_to_timezone_map
    map ||= {} 
    Globalize::Country.find(:all).each{|c| map.store(c.id, c.timezones) }
    map.to_json 
  end

The view:

    #begin form_for block ( customer form builder, ignore hash options )
     <% fields_for 'account', @account do |a| %>
          <%= a.select( 'country_id', Globalize::Country.find(:all).collect{|c| ["#{c.code} - #{c.english_name}", c.id]}, { :label => 'Country'.t, :required => true, :description => "( currency code #{@account.country.currency_code} )" } ) %>    
     <% end %>

     <%= s.select( 'timezone', @account.country.timezones.collect{ |t| [t,t] }, { :label => 'Time Zone', :required => true } ) %>

    <%= javascript_tag("new Country( #{country_to_currency_map()}, 
                                     #{country_to_timezone_map()} );") %>

   #other fields here and end form_for block

Javascript for good measure:

    var Country = Class.create();
    Country.prototype = {
      initialize: function( locale_data, tz_data ){
        this.locale_data = $H(locale_data); 
        this.tz_data = $H(tz_data);
    $('account_country_id').onchange = this.update.bindAsEventListener(this);
      },

      update: function(){
      this.update_currency();
      this.update_timezones();  
      },

      update_currency: function(){
        $('account_country_id_description').update( '( currency code ' + this.currency_for_country() + ' )' );
        new Effect.Highlight( 'account_country_id_description', { duration: 0.8 })
      },

      currency_for_country: function(){
        var country_hash = this.locale_data.detect(function(i,index){ if( i.key == $F('account_country_id') ){ return true } }) 
        return country_hash.value 
      },

      update_timezones: function(){
      var template = '';   
         $('setting_timezone').replace( template.gsub('#{options}', this.build_time_zones_for_country() ) );
         new Effect.Highlight( 'setting_timezone', {duration: 0.8} )
      },

      build_time_zones_for_country: function(){
        var country_hash = this.tz_data.detect(function(i,index){ if( i.key == $F('account_country_id') ){ return true } });    
        var options = '';
        country_hash.value.each(function(i,index){ options += '' })
       return options
      } 
    }

The result:

On page load

Change country callback


About this entry