Pimp your Globalize: Extensions, Plugins and Patches - Get on Rails with Globalize! Part 6 of 8
posted: May 26th, 2007 · by: Sven
This article is part of the series “Get on Rails with Globalize!” and like the last installment it’s a catch-all list: this time we’re going to get some really cool extensions, plugins and patches caught. Stuff that makes Globalize an even mightier tool by adding useful functionality or connecting it to other important tools:
- Multilingual URLs
- Get Globalize working with :include and no base language
- Translate your application while browsing it
- Localized, concise Rails URL helpers
- Get Liquid templates to play nice with Globalize
- Globalize time_ago_in_words method of Rails
- Multiple arguments to fetch
I’ll edit and complete this list as needed. If you know of a Globalize extension, a library, plugin or patch that you found useful and that would fit into this collection, please drop me a note!
Multilingual URLs
Having your URLs translated? Does that make sense?
Sure! Sometimes it really does. For example, look at how much attention the usual SEO suspects pay at their URLs and the keywords that they contain. Obiviously when you translate your content you’ll want your URL contain different keywords, too.
To the best of my knowledge the first complete plugin that accomplishes this is the LocalizedRoutes plugin by Saimon Moore.
Saimon’s code hooks into the dispatch process so that it is able to determine and set the current locale even before the routes are defined. Thus, you can use the locale to translate parts of your routes like so:
ActionController::Routing::Routes.draw do |map|
# translate 'list'
map.list 'list'.t, :controller => 'blog', :action => 'list'
end
If that sounds interesting to you head over to Saimons blog article where he describes in detail how the plugin can be used with different URL formats (e.g. using the locale as a subdomain, part of the path or query parameter): Localized URLs.
(There’s also been a earlier effort by Assente which is not that complete but deserves to be mentioned here, too. So there you go: you can read about Assente’s shot at this feature here: Reverse URL translation with Globalize.)
Get Globalize working with :include and no base language
If you need Globalize to work with the ActiveRecord::Base#find features like :include and :select, for example because you need working has_many :through or some plugin that relies on these features, than you might want to check out the following patch of Lourens Naudé.
Basically Lourens “refactored the Globalize::DbTranslate module to an AR::Associations pattern, override Model.find to always preload translations whilst removing all of the custom finder SQL.”
The main advantage of this is that Globalize will work with the mentioned ActiveRecord functionality again. I.e. :include and :select will work and so will all those nice plugins that depend on these options.
Check out this article on Lourens’ blog: Globalize::DbTranslate with :include and no base language patch.
Translate your application while browsing it
Different from other I18n tools Globalize stores your translations where the rest of your data is: in the database. Thus, there’s a wide variety of tools that you can use to edit your translations - including Rails migrations, database web frontends, GUI applications and so on.
One tool that is targeted particulary to Globalize translations though is the Globalize Translator plugin by Joshua Sierles:
Once installed it hooks into your Rails views and presents a shiny DOM popup interface that let’s you translate the strings on the current view: a convenient and integrated approach to this problem.
Installation is done easily - just follow the instructions in the README file. If you’re looking for an on-site translation GUI, you should give it a go: http://svn.diluvia.net/rails_plugins/globalize_translator/
Localized, concise Rails URL helpers
Globalize itself doesn’t provide a solution for transparently adding the locale to Rails url_helpers where needed. What does that mean?
It means that if you define one of those RESTy Rails resources like this (and you really should use this stuff this way):
map.resources :articles, :path_prefix => ":locale"
… then you’re in a big trouble because you can’t use Rails’ url_helper methods in a reasonable DRY way any more! For example instead of being able to say:
article_url(@article)
… you now have to specify the locale for each and every call to an url_helper, like so:
article_url(@current_locale, @article)
Needless to say that this caused some eyebrows to raise in the Rails community.
Yet, there are even two fire-and-forget solutions to this problem. You can find them described here: Concise, localized Rails URL helpers? Solved (twice).
Get Liquid templates to play nice with Globalize
There are two ways to get Liquid talk to Globalize. There’s an canonical way that uses the official Liquid api. That’s the less sexy one. And there’s a reckless monkeypatch that even invents a new Liquid syntax element. That’s less clutterish.
Let’s start with the safer-use, conservative way. Put the following anywhere in your application:
module GlobalizeFilter
def t(input)
input.t
end
end
Liquid::Template.register_filter(GlobalizeFilter)
For example, you could just make a tiny plugin from this so you don’t need to clutter your environment.rb or something like this. For my personal reference I’ve filed this away as a plugin here: Plugin Globalize Liquid filter.
Now, you can use the following syntax to translate strings from within Liquid templates:
{{ 'welcome' | t }}
But that looks a bit noisy, doesn’t it? So probably what you really want is recklessly hack Liquid so that it gives you a new syntax like this:
{| 'welcome' |}
… and make it act just the same way as the standard way above. So if you’re ready for hazardous moves like this, check out this (pretty small and relative unobtrusive) patch for Localized Liquid syntax.
Oh! And because it internally uses Globalize’s compability method _() instead of .t it will work with just about any other Localization solution, too.
Globalize time_ago_in_words method of Rails
Globalize won’t touch the distance_of_time_in_words and time_ago_in_words family of methods by default. It probably should, but it doesn’t. So if you really need this kind of stuff, your screwed.
Just kidding. Of course you’re not.
You’re a developer and so you will just come up with your own solution. So did Rafael Lima who put together a nice piece of code so you can copy and paste it from his blog.
Multiple arguments to fetch
On your never-ending quest for cleaner and more concise code and templates it might be tempting to do something like this:
'How many %s of %s will the %s have?' / ['gigabytes', 'diskspace', 'iPhone']
“Danger, Will Robinson. Danger!”
Don’t do this. This is a no-go. When you look at the implementation of method / you see that it’s just a proxy to String#translate with the optional quantity argument. Each time you call this the String (that the method is called on) will be inserted into the translations table if it’s not already present.
Thus, in the example above you’d end up with 3 entries in your database, namely:
How many %s of %s will the %s have? How many gigabytes of %s will the %s have? How many gigabytes of diskspace will the %s have?
In a real application where you’d call this with arbitrary data you’d get masses of entries that you’d need to translate. And that’s not really what you wanted, is it?
But there’s help. Instead you might want to have a look at the monkeypatch that you can find on the Globalize wiki.
This allows you to do the following:
'How many %s does a pack of %s have? %d' / ['cigarettes', 'Marlboro', 20]
Be aware though that this hasn’t made its way into the Globalize core for a reason: it breaks Globalize’s pluralization mechanism that otherwise works pretty nicely.
joost said June 11th, 2007 at 09:37 AM ¶
Hi Sven,
Super articles about Globalize.. I can’t find the ‘monkeypatch’ though.. (in ‘Multiple arguments to fetch’). Is there a reason why Globalize doesn’t simply use something like:
“Hi, %s. How are you this %s?”.t(‘Sven’, ‘Monday’)
Sven said June 26th, 2007 at 02:56 PM ¶
Hey joost,
just go to this page on the Globalize wiki and scroll down to the section “Multiple arguments to fetch”. The code shown in this section is the “monkeypatch” that I was referring to. Sorry for not being more clear on this - I’ll change that in the article.
I believe that the reason why Globalize doesn’t support the syntax that you suggest (which is what the above patch actually does enable) has to do with the fact that this wouldn’t make sense for all languages. There are languages with pretty weird pluralization rules.
Also, this feature would clash with the auto-pluralization feature that Globalize comes with. If you inject a number into a string like “%d monkey” Globalize would try to choose different translations depending on the number (0, 1, n being the most usual cases, but there are even more e.g. in Polish or Arabian) you inject. Thus, you’d need to provide (e.g.) 3 translations in this case. If Globalize would allow to inject more than one number into that string (e.g. in “%d monkey dancing under %d trees”) you’d need to provide translations for all of those possible combinations.
I’m not saying that it makes most sense as it is implemented right now. I’m just trying to explain some of the consequences :-)
joost said August 16th, 2007 at 11:25 AM ¶
Hi Sven,
I successfully use Globalize based on all of your tips and tricks. The way I configured it is using domains. So ‘en.mysite.com’ is English, etc. I was only wondering if there is a way to use page caching in this situation..
I think I need to configure Rails and Apache in some way to cache pages based on the language set. Eg. English pages in public/en/..
And then Apache needs to get those depending on the domain that is set.
Any suggestions?
Sven said August 16th, 2007 at 12:43 PM ¶
Hi joost,
I believe you could prepend the locale to the URL through Apache RewriteRules. I don’t have a working one at hand right now, but I guess Google will be your friend here. Once you have an URL like yourdomain.tld/en/controller/action (…) this should work nicely with page caching. That’s the easy part.
I guess things get a bit more difficult when it comes to urlhelpers: how do you get your urlhelpers to output full URLs including the right subdomain? Either my memory is letting me down here or I haven’t seen a proper solution for this yet.
Please tell me (feel free to send me an email) about your solution(s) for this. This could make a useful addition to this article.
PS: I’ve just seen that there’s a config setting like this:
config.action_controller.page_cache_directory = RAILS_ROOT + “/public/cache/”
Probably it will work to simply change this directory on the fly (before the controller get’s called, like in Dispatcher.dispatch())?
Tex said August 29th, 2007 at 11:32 AM ¶
Hi Sven,
Congratulans for your very useful article !!!
When I use :include into finders globalize create wrong sql statements.
Ok, this is a known issue and on your article you write that there is a workaround to :include/:select problems: Globalize::DbTranslate with :include and no base language patch (http://blog.methodmissing.com/2006/12/22/globalize-dbtranslate-with-include-and-no-base-language-patch/).
The problem is that the patch above cannot be applied to globalize for rails 1.2 because seems that the patch given by Louren is too old to be applied (globalize sources is very different respect to the patch).
How can I find a patch for this problem for globalize for rails 1.2 ?
Thank you very much in advance…