zargony.com

#![desc = "Random thoughts of a software engineer"]

Links in gettext translated strings

If you use gettext in your web application, you'll sooner or later have to face the problem of handling links that should be embedded into a translated text. What can easily be done in a normal, non-localized template with ERb, can become a nightmare if used extensively in localized templates. However you can prevent yourself (and your translators) a lot of nightmares by using a small helper method.

As an example: if your web application offers personal user accounts, you probably display a string like "please log in or register" for guests. Of course, you probably want "log in" and "register" to be links that point to the corresponding pages. Without localization, the ERb template would look like:

please <%= link_to('log in', login_url) %> or <%= link_to('register', register_url) %>

With localization, you need to wrap the whole sentence in a call to gettext:

<%= _('please log in or register') %>

If you would embed a link into the string passed to gettext, you'll be faced with at least two problems. First, gettext looks up the string in its translation table (.mo file). In order to know a string, it has to be extracted by rgettext, which only works for static strings (since a string that is build with one or more variables cannot be determined without actually running the code. You could work around this problem with extra calls to N_, but that'd be pretty ugly with links). Second, you'd have HTML tags in your translation string, which not only looks ugly and will probably confuse your translators, but also is prone to errors, e.g. if an URL changes.
An easy and safe way to put links in a translated string is to use substitution, like this:

<%= _('please %s or %s') % [link_to(_('log in'), login_url), link_to(_('register'), register_url)] %>

This works fine and is probably sufficient if it's used in only a few places. However it quickly becomes a mess if you need to place it more often or need to embed many links in a text. Besides that the above line already looks complicated enough and becomes more and more unreadable if you add more links, there's another problem: The initial sentence that should be translated, is now split into seperate strings and put together at runtime. That's something that should generally be prevented, because it might not work with every translation. In other languages, the result might look strange, because the translator wasn't able to see the whole sentence and the translation of "log in" might not be appropriate when put together in this sentence.
With a small helper method, all this gets easier. Just use a custom substitution method, that is appropriate for links. I usually call it linkify in my applications:

class String
  def linkify (*urls)
    self.gsub(/\{(\d+):([^}]+)\}/) { link_to(_($2), urls[$1.to_i]) }
  end
end

This small little helper method can be called on every string and substitutes text enclosed in curly braces with links that point to the URLs given as arguments to the method. Example:

_('Please {0:log in} or {1:register}').linkify(login_url, register_url)

Now the ERb templates look (more or less) clean and the translator gets meaningful texts again.

Unfortunately this helper method creates the link by putting together the markup directly. It'd be nicer to use rails' link_to helper which respects routing and can take additional arguments, but unfortunately that helper method isn't available from within the class String.

In his gettext pluralize plugin, Iain does something similar, but uses a block to construct the link. That way, you can use the link_to helper and all of its features at the cost of writing a bit more code in your templates.