Haml + Gettext = automagic translation

13 Apr 2009

HAML

I was rather sceptic to Haml once I have first time read about it. But after recently playing a while with it I can frankly express that it is simply outstanding template engine for Ruby. What I miss about Haml is some seamless integration with some i18n framework (gem).

So I decided to create Haml “mod” that uses GetText (FastGettext alternatively) to automagically translate static texts from Haml templates during precompilation stage. So something that you don’t see and you don’t need to worry about.

Normally you would use _(text) calls to translate string in your templates as below:

%h1= _('Items')
- items.each do |item|
  .item
    %h1= item.title
    %a{ href='/edit' }= _('Edit')
    %a{ href='/delete' }= _('Delete')

You can see this is pretty ugly and unreadable. If you gonna have multi-language site you want to translate all of the static texts there anyway, without doing anything extra. So you would prefer to type:

%h1= Items
- items.each do |item|
  .item
    %h1= item.title
    %a{ href='/edit' } Edit
    %a{ href='/delete' } Delete

And have “Items”, “Edit”, “Delete” just translated, right? This is what my mod does. It translates all of those at precompilation stage which also is much better for your application performance, since GetText routines are called only once at precompilation stage. Here it goes.

haml_gettext.rb

# !/bin/bash
# Haml gettext module providing gettext translation for all Haml plain text calls
#
# http://pastie.org/445295

class Haml::Engine
  # Inject _ gettext into plain text and tag plain text calls
  def push_plain(text)
    super(_(text))
  end
  def parse_tag(line)
    tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
      nuke_inner_whitespace, action, value = super(line)
    value = _(value) unless action || value.empty?
    [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
        nuke_inner_whitespace, action, value]
  end
end

All you need is to choose GetText or FastGettext and load my mod later on.

require 'haml'
require 'fast_gettext'
include FastGettext::Translation
require 'haml_gettext'

You probably want to know the way to your gettext pot/po files out of the tempaltes. I have prepared here Rake tasks for you as well (based on work I found somewhere over Ruby blogs) to parse Haml templates extended to include static texts in the translation.

Rakefile

desc "Update pot/po files."
task :updatepo do
  require 'gettext/tools'
  require 'haml_parser'
  GetText.update_pofiles("rbigg", Dir.glob("{lib,views}/**/*.{rb,haml}") << "rbigg.rb", "rbigg 1.0.0", :po_root => 'locale')
end

haml_parser.rb

# Haml gettext parser module
#
# http://pastie.org/445297
require 'gettext/tools/rgettext'
require 'gettext/parser/ruby'
require 'haml'

class Haml::Engine
  # Overriden function that parses Haml tags
  # Injects gettext call for plain text action.
  def parse_tag(line)
    tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
      nuke_inner_whitespace, action, value = super(line)
    @precompiled << "_(\"#{value}\")\n" unless action || value.empty?
    [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
        nuke_inner_whitespace, action, value]
  end
  # Overriden function that producted Haml plain text
  # Injects gettext call for plain text action.
  def push_plain(text)
    @precompiled << "_(\"#{text}\")\n"
  end
end

# Haml gettext parser
module HamlParser
  module_function

  def target?(file)
    File.extname(file) == ".haml"
  end

  def parse(file, ary = [])
    haml = Haml::Engine.new(IO.readlines(file).join)
    code = haml.precompiled.split(/$/)
    GetText::RubyParser.parse_lines(file, code, ary)
  end
end

GetText::RGetText.add_parser(HamlParser)

So this is it. Have fun with nice and clean templates that are translated on the fly

See & get more…

You can find more recent and complete code at my Sinatra Hat GitHub hosted Ruby gem project.

Posted in Programming, Ruby