Record Gettext at Test Runtime

Recently our team got frustrated with all the phrases gettext would not find.

  • words in if blocks
  • word produced by helpers
  • words used in arrays that get translated at runtime

Here is a simple solution: collect all phrases that were used during testing. Since the testsuite often has  100% C0 coverage and any phrase that is not found will signals missing tests.

Install

#spec/spec_helper.rb or test/test_helper.rb
if ENV['LOG_GETTEXT']
  def _(word)
    File.open(ENV['LOG_GETTEXT'], File::WRONLY|File::APPEND|File::CREAT) do |f|
      f.puts word
    end
    gettext(word)
  end
end


#lib/taskts/gettext_test_log.rb
task :gettext_test_log do
  tmpfile = 'locale/tmp_gettext_test_log.txt'
  outfile = "app/testlog_phrases.rb"
  
#  system "rake test LOG_GETTEXT=#{tmpfile}"
  system "rake spec LOG_GETTEXT=#{tmpfile}"
  process_log(tmpfile,outfile)
end

def process_log(tmpfile,outfile)
  found = {}
  File.readlines(tmpfile).each do |line|
    line.strip!
    next if line.empty?
    next if line =~ /%s of /
    next if %w[nil Sun Mon Tue Wed Thu Fri Sat Sunday Monday Tuesday Wednesday Thursday  Friday  Saturday  Jan Feb Mar  Apr Jun  Jul Aug Sep Oct Nov Dec January February March April May June July August September October November December].include? line
    found[line]=true
  end
  
  File.open(outfile,'w') do |f|
    found.each do |k,v|
      f.puts "_('#{k}')"
    end
  end
end

Usage
The generated testlog_phrases.rb should be placed in a folder that is search by your updatepo task(it is not meant to be executed).

Custom Form Builders Are Evil

Every Formbuilder i see does the same, building some rows/tables/div structure around the fields it is be called on.

f.text_field => tr td label input

In theory this sounds great, but as soon as one tries to make some real life use of it it breaks down:

  • How do i handle I18N ?
  • How can i change the label text from the default
  • How do i put the checkbox in front of the label
  • How can i output only a field/2 fields in one row

Form builders that always output label+input are unsuited for everyday development!

So what else can we do ?

#app/helpers/application_helper.rb
class ActionView::Helpers::FormBuilder
  def build_translated_label(content)
    if content.class == Array
      label(content[0],content[1])#label + text
    elsif content.class == String
      label('',content)#text
    else content.class == Symbol
      #uses gettext translation, feel free to insert your own...
      label(content,_("#{object.class}|#{content.to_s.capitalize.gsub('_',' ')}"))#label + translation        
    end
  end
  
  def row(label,content)
    @template.content_tag('div',build_translated_label(label) + content.to_s,:class=>'row')
  end
end

Usage

f.row(_('Movie|Website')+'http://', f.text_field(:website))
f.row(:tags, f.text_field(:tag_list)+_('comma divided'))

If someone wants the R-specs, drop me a mail 🙂

Small Spec Helpers

So small but so powerful, i just wanted to share my little time savers, and hope you share your most essential helpers in the comments.

(I use @item as an alias for the current object in all my tests, to make generic tests less painful.)

Small example:

it "renders feedback" do
  expects_find
  get :feedback , :id => @item.to_param
  response.should render_template('edit')
end

Code:

#spec/spec_helper.rb
def mock_create(item,success,para=nil)
  if para
    item.class.expects(:new).with(para).returns item
  else
    item.class.expects(:new).returns item
  end
  item.expects(:save).returns success
end

def mock_update(item,success)
  expects_find(item)
  item.expects(:save).returns success
end

def expects_find(item=nil)
  item ||= @item
  item.class.expects(:find).with(item.to_param).returns(item)
end

The Universal email_token

Activation_code, password_reminder_token and more to come.

Strange code:

before_create :make_activation_token

def make_activation_code
  self.activation_code = Digest::SHA1.hexdigest( ...)
end

def forgot_password
  ...
  password_reset_token=Digest::SHA1.hexdigest(...)
end
#and so on...

Let the mailman handle the postbox keys
Stay with a simple email token. It is updated every time we send any activation/reset/verification mail so no user can perform two action with the same token or say ‘find’ an old token and then request a password reset.

#user.rb
def update_email_token
  update_attribute(:email_token,Digest::SHA1.hexdigest(..)
end

#user_mailer.rb
def setup_email(user)
  user.update_email_token
  ...
end

Generic + Smart – link_to_s link_to_edit link_to_destroy

Building on my work in Separate Rights Management from Controllers i killed all linking-view-logic.

Example

before:
(movie.online? or movie.owner_id==current_user.id) ?
  link_to(movie,movie) : ''
(movie.owner_id==current_user.id) ?
  link_to('edit',movie) : ''

after:
link_to_s(movie)
link_to_edit(movie)

Code

#app/helpers/link_helper.rb
module LinkHelper
  def is_record?(something)
    something.kind_of?(ActiveRecord::Base)
  end

  def can_write?(obj)
    user = current_user || User.new
    user.can_write?(obj)
  end

  def can_read?(obj)
    user = current_user || User.new
    user.can_read?(obj)
  end

  #can we build a link ?
  #true if it is an id / path
  #false if it is an record and i cannot reda/write it
  def is_linkable?(object,rw)
    raise ":r or :w" unless [:r,:w].include? rw
    return false unless object
    return true unless is_record?(object)
    return rw == :r ? can_read?(object) : can_write?(object)
  end

  def link_to_s(object,options={})
    return "" unless is_linkable?(object,:r)
    link_to(object.to_s,object,options)
  end

  def link_to_edit(path_or_object,options={})
    return "" unless is_linkable?(path_or_object,:w)
    if is_record?(path_or_object)
      path_or_object = edit_polymorphic_path(path_or_object)
    end
    link_to("edit",path_or_object,options)
  end

  def link_to_destroy(path_or_object,options={})
    return "" unless is_linkable?(path_or_object,:w)
    link_to( 'X', path_or_object, options.merge(:class=>'destroy'))
  end
end

Please drop a comment if you can think of enhancements, if i can find more uses/methods i will build a new link-helper-plugin 🙂