Follow onomatopej on Twitter Meet us on Facebook Blog Portfolio Publications Contact About

Haml + Gettext = automagic translation

Haml logoI 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

# 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.

Tags:

7 Responses to “Haml + Gettext = automagic translation”

  1. 1
    Hasan Tan

    been looking for this information. Thank you very much…

  2. 2
    Igor

    Hi. Tried to adapt your solution (used i18n however).
    However found the next flaw with your hack: HAML compiles the template and includes translated texts into the compiled form as a plain text.
    This has a consequence that template only adapts to a single locale. Meaning different users (or a single user changing its locale dynamically) will see the very first compiled localization, not different.

    There are basically two solutions to this problem. First – it is possible to somehow tell Rails template system to compile separate template for each locale. However that requires monkeyhacking and not quite straightforward.

    Another way (which I think I’ll choose) is not to return plain text within your push_plain/parse_tag, but return a push_script(“= _(‘#{text}’)”) instead and force HAML to dynamically translate stuff all the time.

    Or, is there any other solutions?

  3. 3
    Igor

    If of any use, here: http://github.com/cail/haml_i18n is practically the same code but as a rails plugin and using i18n, not gettext.

  4. 4
    Adam

    Igor: Thanks for a note. Sorry I haven’t replied your previous post. Did you managed to do it for several locales? I believe the trick was to mod HAML a bit so the stored compiled templates are separate per each locale, so adding locale code prefix to compiled template method name should make it.

  5. 5
    Igor

    Adam, in my code on github you can check the solution. I took the second approach and instead forced HAML to keep translate(“phrase”) calls within the template. So there is a single template for the whole app locales, but the keys which can be translated are stored within the template not as a plain text but as a ‘translate’ method calls.
    This gives some performance degradation of course, but its fine for my project so far.

  6. 6
    Colin Dean

    Thanks for creating this. It’s a great tool.

    However, I’m having trouble figuring out where in my Sinatra app to put the FastGettext.add_text_domain and .text_domain call/set.

    I’m using bundler to manage gems and have sinatra-hat and fast_gettext in my Gemfile. I also have gettext for :development so I can use its tools.

    Inside my app file:

    include FastGettext::Translation
    require ‘gettext/haml’

    Then, to configure FastGettext, I’ve got in in a configure block inside the class definition,

    configure do
    FastGettext.add_text_domain(‘uio’, :path => ‘locale’)
    FastGettext.text_domain = ‘uio’
    end

    but I get this when I load the page:

    FastGettext::Storage::NoTextDomainConfigured at /
    Current textdomain (nil) was not added, use FastGettext.add_text_domain !

    I’ve tried adding what’s in that configure to just under where I’m requiring gettext/haml, but to no avail.

    Thoughts?

  7. 7
    Language independant translator using haml parser | Josh Software – Where Programming is an Art!

    [...] google’d and I found some good reference here. Using this reference I made necessary changes  for parsing my .haml pages and enable for [...]

Leave a Reply

*
To prove you're a person (not a spam script), type the answer to the math equation shown in the picture. Click on the picture to hear an audio file of the equation.
Click to hear an audio file of the anti-spam equation