Localized urls
In this article, I’m going to describe a simple way to localize your application’s urls using globalize.
Localize your urls
What do I mean by “localize your urls” exactly? For example, you browse to :
http://ocium.com/artists/millivanilli
and are shown the “Milli Vanilli” artist info page in the base locale (which is ‘english’ for arguments sake). You click a link to switch locale within the page and you’re transported to:
http://es.ocium.com/artistas/millivanilli
As you can see, the whole url has been translated into the language of the currently active locale.
Locale defined as a path parameter
In this example, I’m using subdomains to indicate the locale, but you can easily use path/query parameters to define the locale.
e.g.
http://ocium.com/en/artists/millivanilli
=>
http://ocium.com/es/artistas/millivanilli
The plugin
I’ve wrapped up the code to do that within the LocalizedRoutes plugin:
Installation
- Assuming you have the latest version of globalize installed (at least for-1.2),
- Install the plugin via :
script/plugin install svn://rubyforge.org/var/svn/sidirodromos/plugins/localized_routes/trunk
Usage
You can now translate the urls in your routes via:
1 2 3 4 5 6 7 8 9 |
ActionController::Routing::Routes.draw do |map| #Will translate 'list' to the default namespace map.list 'list'.t, :controller => 'blog', :action => 'list' #Will translate 'list' to the 'urls' namespace map.list 'list'>> :urls, :controller => 'blog', :action => 'list' end |
Note: The use of the ‘urls’ namespace when translating routes. This is recommended as it’s then easy to separate url translations from other translations.
The localized_routes plugin looks at the incoming request and calculates which locale is to be set as active from one of three possible methods in the following order:
- “Query parameter” named “locale” e.g
http://saimonmoore.net/listar?locale=es - “Path parameter” extracted by matching request path against Globalize::LocalizedRoutes::LOCALE_PATH_MATCHING_RE e.g.
1 2 3
Globalize::LocalizedRoutes::LOCALE_PATH_MATCHING_RE => /^\/([\w-]+)|\/([\w-]+)\/.*$/
Note: If you want to define the locale in the path differently, then just override the regexp.http://saimonmoore.net/es/listar or http://saimonmoore.net/es - “Subdomain” e.g.
http://es.saimonmoore.net/listar
If you’re using the path parameter to define your locale then it’s a good idea to specify it in the route like so:
1 2 3 4 |
ActionController::Routing::Routes.draw do |map| map.list ':locale/list'>> :urls, :controller => 'blog', :action => 'list' end |
Then you’ll have access to the “locale” parameter in the controller and it’s useful for url helper methods.
Some examples:
You want to define the “locale via a path parameter”.
1 2 3 4 |
ActionController::Routing::Routes.draw do |map| map.list ':locale/list'>> :urls, :controller => 'blog', :action => 'list' end |
You translate the urls for each language you support:
1 2 3 4 5 6 7 8 |
Locale.set_base_language('en-GB') Locale.set('es-ES') Locale.set_translation_with_namespace(':locale/list', :urls, ':locale/listar') Locale.set('fr-FR') Locale.set_translation_with_namespace(':locale/list', :urls, ':locale/enumerer') |
You can now access:
http://saimonmoore.net/en/list
http://saimonmoore.net/es/listar
http://saimonmoore.net/fr/enumerer
To access:
http://saimonmoore.net/list
you’d need to additionally define:
1 2 |
map.list 'list', :controller => 'blog', :action => 'list' |
If, on the other hand, you’d like to define the “locale via subdomains”, then:
1 2 3 4 |
ActionController::Routing::Routes.draw do |map| map.list 'list'>> :urls, :controller => 'blog', :action => 'list' end |
Again you translate the urls for each language you support:
1 2 3 4 5 6 7 8 |
Locale.set_base_language('en-GB') Locale.set('es-ES') Locale.set_translation_with_namespace('/list', :urls, '/listar') Locale.set('fr-FR') Locale.set_translation_with_namespace('/list', :urls, '/enumerer') |
You can now access:
http://saimonmoore.net/list
http://es.saimonmoore.net/listar
http://fr.saimonmoore.net/enumerer
Switch Locale Links
Here are examples of how you’d create a list of links to switch the current locale using all three methods for defining the locale:
Assuming routes defined as:1 2 3 4 5 6 |
ActionController::Routing::Routes.draw do |map| map.list_with_locale ':locale/list' >> :urls, :controller => 'application', :action => 'list' map.list 'list' >> :urls, :controller => 'application', :action => 'list' map.connect ':controller/:action/:id' end |
Note: For this example I’ve already translated the urls for each of the supported languages.
Then in your partial (or extract to a helper):
-
path parameter
Note:To make it even simpler you could use one of these techniques to only specify “list_with_locale_url”1 2 3 4 5 6 7 8
<% SupportedLocales.supported_language_codes.each do |code| -%> <% Locale.switch_locale(code) do -%> <% ActionController::Routing::Routes.reload -%> <%= link_to Locale.language.native_name, list_with_locale_url('locale' => code) %> <% end -%> <% end -%>
Browsing to http://saimonmoore.net/list generates:1 2 3 4 5 6
<a href="http://saimonmoore.net/en/list">English</a> <a href="http://saimonmoore.net/el/katalago">Ελληνικά</a> <a href="http://saimonmoore.net/es/listar">Español</a> <a href="http://saimonmoore.net/ca/listar">Català</a> -
query parameter
Browsing to http://saimonmoore.net/list generates:1 2 3 4 5 6 7 8
<% SupportedLocales.supported_language_codes.each do |code| -%> <% Locale.switch_locale(code) do -%> <% ActionController::Routing::Routes.reload -%> <%= link_to Locale.language.native_name, list_url('locale' => code) %> <% end -%> <% end -%>1 2 3 4 5 6
<a href="http://saimonmoore.net/list?locale=en">English</a> <a href="http://saimonmoore.net/katalogo?locale=el">Ελληνικά</a> <a href="http://saimonmoore.net/listar?locale=es">Español</a> <a href="http://saimonmoore.net/listar?locale=ca">Català</a> -
subdomain
Browsing to http://saimonmoore.net/list generates:1 2 3 4 5 6 7 8
<% SupportedLocales.supported_language_codes.each do |code| -%> <% Locale.switch_locale(code) do -%> <% ActionController::Routing::Routes.reload -%> <%= link_to Locale.language.native_name, subdomain_for(:list_url) %> <% end -%> <% end -%>1 2 3 4 5 6
<a href="http://saimonmoore.net/list">English</a> <a href="http://el.saimonmoore.net/katalogo">Ελληνικά</a> <a href="http://es.saimonmoore.net/listar">Español</a> <a href="http://ca.saimonmoore.net/listar">Català</a>
Test application
If you’d like to experiment with this, I’ve bundled up a test application which has everything you need to play around with this. Just set up a db & run rake globalize:setup. In the db directory you can find an sql file, with all the translations used in this example.
Note: In the last example I used a subdomain_for() helper to correctly generate the right host for each locale. This is very simple and you can see the code in the example app.
The code
I’ve babbled a lot about how you can use this plugin but let’s take a look at the actual code. It’s really quite simple.
In essence, I realized that for each request that goes through rails, the dispatcher takes care of running it through the defined routes in order to recognize the appropriate controller and action for the request and then passes it along to the controller for processing.
What I needed then is to ensure that the locale is set before routing recognition occurs
So I simple overrode Dispatcher.dispatch() to add in a call to a set_locale() method before the call to ActionController::Routing::Routes.recognize. This means Globalize::Locale is now set when the routing code does it’s recognition which means you can then translate strings within the route definitions.
i.e.
1 2 3 4 5 6 7 8 9 10 11 |
def dispatch_with_locale(cgi = nil, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
controller = nil
if cgi ||= new_cgi(output)
request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)
=> set_locale(request) #Set the locale before route recognition
prepare_application
controller = ActionController::Routing::Routes.recognize(request)
controller.process(request, response).out(output)
end
....
|
Note: I hadn’t seen Dispatcher.to_prepare before and Josh Sierles brought it to my attention however, this only executes the callback on the first request in production mode so it’s not really usefull in this case. Another problem is that set_locale needs access to the request. I’m thinking of sending in a patch to rails to make it configurable and will then convert the code to using this.
Take a look at the code to see what set_locale does (though it’s nothing fancy).
Hope that you find this handy. I certainly have.
Saimon
P.S. I’ve incorporated this functionality directly into my mephisto_i18n plugin which makes it handy if you need to extend mephisto with custom routes.
Update: I’ve refactored the code slightly to use a patched Dispatcher which adds support for request_callbacks (similar to preparation_callbacks but which are executed on every request and which have access to the request object).
Problem?: The code now reloads the routes for the current locale on every request. This is so that you can reload routes within Locale.switch_locale blocks and generate the localized url correctly. I realize this may have a performance hit but initiall tests show this to minimal.
1 comments
Saltar a formulari de comentaris | comentaris rss [?]