Concise & transparently localized Rails url_helper methods?

posted: April 24th, 2007 · by: Sven

in: Globalization, Programming · tagged as: , , , , , ·  19 comments »

Update: It’s worth noting that the following information has been updated in the follow-up article: Concise, localized Rails URL helpers? Solved (twice).

Recently Jeremy Hubert commented on my tutorial about routes setup for Globalize. In short he criticized that Globalize doesn’t provide a solution for transparently adding the locale to Rails url_helpers where needed. Instead of being able to say:


article_url(@article)   

You 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 does raise some eyebrows in the Rails community. So let’s see if we can come up with an acceptable solution here. This post describes some intermediary results.

Jeremy posted some lines of code to demonstrate a possible solution and get some thoughts going. He showed a modified version of the NamedRouteCollection::define_url_helper method. This method is responsible for creating all those funny url_helper methods that you get when you define a named route.

Unfortunately I couldn’t get this version working though. Instead of this:


article_path(@article) # /en/articles/1

I kept getting this:


article_path(@article) # /1/articles/localeen

Hmm. Looking a little closer at this line:


#{'args << { :locale => @current_locale }' if segment_keys.include?(:locale)}

… this seems to erroneously push a hash into the args array.

For reasons that I’ll try to explain later on this can’t work with args given as a hash anyways (i.e. with e.g. article_path(:id => 1)). On the other hand, when args is an array here (i.e. in the shorter and targeted syntax like in article_path(1)) the args variable will end up with a value like [1, {:locale => 'en'}] and it seems clear that this actually translates to the wrong URL above.

How about this?

So, standing on giant Jeremies’ shoulders I’ve tried to refine this a bit and found this line to be working a bit better:


args.unshift @current_locale unless Hash === args.first

This unshifts the current locale to front of the array, but it does it only if the first argument is not a Hash, i.e., only if the targeted array syntax has been used.

Also, I disliked overwriting the entire method a bit because it’s relatively prone to breakage if the original code changes in future. Thus I’ve constructed the following weird looking wrapper that does two things:

  • Wrap the define_url_helper method and hook up a workhorse method if the route involves a :locale segment. This method inject_locale_to_url_helper will be called immediately after the url_helper method has been defined.
  • inject_locale_to_url_helper will then wrap the just defined url_helper method and prepend the current locale to the parameters whenever the user actually calls the helper method.

The code looks quite clumbsy because of the duplicate usage of alias_method_chain:

  
module ActionController
  module Routing
    class RouteSet
      class NamedRouteCollection

        # hook into the url_helper creation process and add our call after
        # the helper has been created
        def define_url_helper_with_wrap_locale(route, name, kind, options)
          define_url_helper_without_wrap_locale(route, name, kind, options)
          if route.significant_keys.include? :locale
            inject_locale_to_url_helper url_helper_name(name, kind)
          end
        end
        alias_method_chain :define_url_helper, :wrap_locale

        # wrap the url_helper and prepends the locale to the parameters list
        # given by the user when the helper is actually used
        def inject_locale_to_url_helper(selector)
          @module.send :module_eval, <<-end_eval 
            def #{selector}_with_locale(*args)                            
              args.unshift @current_locale unless Hash === args.first
              #{selector}_without_locale(*args)
            end
          end_eval
          @module.send :alias_method_chain, selector, :locale  
        end
      end
    end
  end
end

Ok. This could already make a possible addition to Globalize, I guess.

It’s relatively unobtrusive in that it only wraps around existing methods. It also only relies on the assumption that route.significant_keys will tell us if :locale is included in this route. That makes it relatively save from future changes to the Rails code.

It allows to call url_helpers using the targeted array parameter syntax and omitting the locale from the parameter list:


article_path(@article)  

… assuming that @article.id == 1 and @current_locale == 'en' this will yield …


/en/articles/1  

If this covers our needs it should work fine and everything’s all nice and dandy.

Problem!

But there’s a shortcoming with this solution: you can’t specify a different locale in any resonable concise manner. E.g. neither of these would work:


article_path('fr') 
article_path('fr', @article) 
article_path(:locale => 'fr')
article_path(:locale => 'fr', :article => @article)

To only switch the current locale (as in “view this article in English, French, German, …”) you’d need to use a monster-like call like this:


article_path(:locale => 'fr', :controller => :articles, :action => :show, :article => @article)     

No, the usual shortcut version wouldn’t work. That’s because @current_locale would be unshifted to the parameter list:


article_path('fr', :articles, :show, @article)  

This would end up with a parameter list like ['en', 'fr', :articles, :show, @article] … and thus throw a routing error.

Really a problem?

One might argue that switching the locale is a relatively seldom needed functionality and this is still a huge improvement above needing to add the locale to each and every url_helper.

Hm, yes. Still … I’d rather like so see some solution to this before trying to integrate it to Globalize.

Rails’ concept of parameter expiration in routes

Now let’s try to understand how it comes that it doesn’t work to just add :locale => 'en' to the hash syntax like in:


article_path(:locale => 'fr', :article => @article) # doesn't work :(

Why?

The reason for this lies buried in a concept that Rails calls “parameter expiration”. It seems hard enough to understand what is going on (Jamis Buck talks a bit about the concept here). But to me it’s quite unclear why exactly Rails does things this way.

Whatever the reason was for inventing this concept in the first place … it seems clearly to be the culprit for us not being able to use such a simple and intuitive call like the one above.

In Rails’ routing system the order of the single path segments that map to a controller, an action and a bunch parameters is crucial: the parameter expiration concept assumes that a change in a segment on the left side of the path (e.g. our :locale parameter) designates the necessity to update or specify all the segments that are on the right of it.

Sounds confusing? Yes. Look here:


# this is /en/articles/show/1 so we have:
# :locale => 'en', :controller => :articles, :action => :show, :id => 1

Now these will work perfectly:


article_url :id => 2
url_for :action => :edit, :id => 1
url_for :controller => :pages, :action => :show, :id => 1
article_url :locale => 'fr', :id => 1

The reason is that any of the “expired” parameters that have changed in comparsion to the current URL are given. “Expired” are parameters that have changed themselves OR are to the right of a changed parameter.

For the same reason these won’t work:


url_for :action => :edit # :id is missing
url_for :controller => :pages, :id => 1 # :action is missing
article_url :locale => 'fr' # everything besides :locale is missing

… because :id, :action and :locale respectively are to the right of a changed parameter (:action, :controller and :locale respectively). As you can see, because :locale is always on the leftmost side of our route it will always expire all of the other parameters of the route - so we need to specify all of them! But this is Rails. So this is only true most of the times … that is, except is it not true. Ahem ;-)

I guess the reasoning for this kind of confusing concept has most probably to do with the fact that it’s a rather uncommon case that parameters are left to the :controller and :action specification (like our :locale).

But on the other hand … I’m wondering if the confusion invented by this is really outweighed by the benefits.

Feedback? Suggestions?

I hope this article - in all it’s half-baked-ness - will get some further thoughts and discussion going.

What do you think?

Leave a comment

19 Comments

  1. Jeremy Hubert said August 6th, 2007 at 08:19 AM  

    Thank you for saying what I was trying to say in a much better fashion, and then some. :)

    Keep up the great work.

  2. Steve said August 8th, 2007 at 08:06 PM  

    I’m working on solving a similar problem - persisting some session-related info either as part of the route or as a parameter so that URLs can be always bookmarkable. In my case, it isn’t language, but the basic problem is still the same - how to smoothly integrate the info without goofing up all the urlhelpers. If anyone finds a good solution for this - even a simple way to just stick a parameter onto the end of the URL via an afterfilter in the Application Controller would work as a patch - please post a link to it.

  3. Sven said August 11th, 2007 at 06:04 PM  

    Thanks Jeremy! :)

    Steve,

    you might want to have a look at my follow-up article “Concise, localized Rails URL helpers? Solved (twice).”

  4. Adam Greene said December 21st, 2007 at 05:47 AM  

    I found this to be INCREDIBLY helpful. I’ve struggled with this on and off, and while I had a working solution it was brittle and broke when moving to rails 2.0.2. This is much more elegant.

    I wanted to pass along a little tweak. If a hash is passed in, the :locale will not be set. That is pretty easy to fix by doing this:

    def #{selector}withlocale(*args) if Hash === args.first args.first[:locale] ||= @locale else args.unshift @locale unless Hash === args.first end end

    Thanks again for this very elegant solution. smashing. Adam

  5. Sven said December 21st, 2007 at 09:29 AM  

    Hi Adam,

    thanks :) Have you seen the localized_url_helpers plugin? See http://www.artweb-design.de/2007/5/13/concise-localized-rails-url-helpers-solved-twice It was the result of what I’ve started here. I’m not sure if this works with Rails 2.0 though (from what I recall, it should)

  6. Allenwood said December 5th, 2010 at 08:06 AM  

    gadgets for men - At World Of Gadgets we are continually updating our range of exciting new gadgets gizmos and novelty clothing/items to accomodate all tastes.

  7. jack said January 23rd, 2011 at 11:32 AM  

    thanks for that headsup. That’s a useful tip! I’ve never ran into that, but for sure that’s something quite some people will need a solution for. cheap vps

  8. QQQ said February 7th, 2011 at 06:32 PM  

    Finally we kissed and the passion scale went sky high and I knew I was onto a good thing - sex was a certainty free porn videos. She never hesitated when I began to fondle her breasts and she willingly exposed them for me mobile porn. They were firm and I suspected a breast enhancement but said nothing - they still felt good and I was enjoying them and gradually working my way further south free porn tube. She was a step ahead of me and before I could completely undress her she moved on me atk hairy and I was suddenly having my pants pulled down and I was enjoying one of he best cock sucking hairy pussy experiences I had ever had. ABB728019394

  9. Yeng said February 24th, 2011 at 06:21 AM  

    Dear i really want this kind of site.It is very handy pass4sure mb2-631 and accommodating post. keep up the good work.we need more good statements. I really appreciate your way of presenting such an excellent suggestion. pass4sure mb6-819 I want more and i will come back here to see more updates in future pass4sure 70-432 as well.my best wishes for you always so keep it up.

  10. Torrents said March 7th, 2011 at 01:37 PM  

    That’s a useful tip! I’ve never ran into that, but for sure that’s something quite some people will need a solution for.

  11. Kiona Stagart said March 13th, 2011 at 08:01 AM  

    Hi Sven,

    Jeremey is one smart dude. This #{‘args << { :locale => @currentlocale }’ if segmentkeys.include?(:locale)} looked correct to me at first glane. But I see the error. ALthough programming is my style Regards, Kiona Stagart Kiona is a hair stylist in the morning her tool is the farouk chi 1 inch ceramic flat hairstyling iron. At night she programs.

  12. chat said March 31st, 2011 at 07:28 PM  

    Here is the url helper fix:

    The following cleaned up the issue:

    Dependencies.loadoncepaths -= Dependencies.loadoncepaths.select{|path| \ path =~ %r(^#{File.dirname(FILE)}) }

  13. naruto said April 13th, 2011 at 08:39 PM  

    If anyone finds a good solution for this - even a simple way to just stick a parameter onto the end of the URL via an afterfilter in the Application Controller would work as a patch - please post a link to it.

  14. Lisa said April 18th, 2011 at 05:27 PM  

    Cool Sache, werde mir das mal genauer anschauen. Ich bin grad auf Malta und arbeite hier auch an einem ähnlichen Rails Projekt. Fall mal jemand nach Malta kommen will, kann ich die malta reisen seite ziemlich empfehlen da es dort cool Infos rund um Malta, Gozo und andere Orte der Insel gibt.

  15. side sleeper pillow said April 22nd, 2011 at 04:26 AM  

    Cool. Shall i visit you sooner in berlin?

  16. Porn said May 3rd, 2011 at 02:43 PM  

    Thnks for providing this code. it was really helpfull. :)

    free wp templates

  17. Okey oyunu said May 12th, 2011 at 04:00 PM  

    Thanks. Tüm dünya artik okey oyunu oynuyor. Yillardir bir çok oyun programi olmasina ragmen, içlerinden en güzeli olarak nitelendirebilecegimiz tek bir site göze çarpmaktadir. Diger tüm okey oyunu programlarinin aksine ücretsiz olmasi ve 3 boyutlu olarak hizmet vermesi mükemmel bir gelismedir. Sizlerde www.okey-oyunu.com adresinden bu essiz okey oyununu indirebilirsiniz. Kullanimi çok basit ve Türkçe dil seçenegi ile kolaylikla oyuna baslayabilirsiniz. Ister kendi ülkenizden, isterseniz dünyanin tüm farkli bölgelerinden dilediginiz oyun odalarini seçerek, oyuna hemen baslayabilirsiniz. Okey oyunu oynamak için artik arkadas bile aramaniza gerek kalmadan, bilgisayarinizdan 100 binlerce üye ile online olarak okey oyununu oynamanin zevkine varabilirsiniz.

  18. porno said May 22nd, 2011 at 01:28 PM  

    I do agree with all of the ideas you have presented in your post. They’re really convincing and will definitely work. Still, the posts are too short for newbies. Could you please extend them a bit from next time? Thanks for the post.

  19. porno said May 22nd, 2011 at 01:58 PM  

    good comment. thanks you friends.

    I’ve surfed the net more than three hours today, however, I haven’t found such useful information. Thanks a lot, it is really useful to me

Sorry, comments are closed for this article.

artweb design
Sven Fuchs
Grünberger Str. 65
10245 Berlin, Germany


http://www.artweb-design.de

Fon +49 (30) 47 98 69 96
Fax +49 (30) 47 98 69 97