scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do resources :posts root to: redirect("/%{locale}/posts", status: 302) end root to: redirect("/#{I18n.default_locale}", status: 302), as: :redirected_root get "/*path", to: redirect("/#{I18n.default_locale}/%{path}", status: 302), constraints: {path: /(?!(#{I18n.available_locales.join("|")})/).*/}, format: false
Let's take a look at the above code sample from config/routes.rb
and will study what it does line by line.
-
Locale parameter will be added to posts resource url:
http://example.com/en/posts
The following part of code is responsible for that:
scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do resources :posts end
Note, that available locales are taken from application config.
-
Root path with locale will redirect to localized version of posts (which is a default page)
http://example.com/ru
→http://example.com/ru/posts
This is how we achieve it:
scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do root to: redirect("/%{locale}/posts", status: 302) end
We use
%{locale}
here, which allows to use dynamic segment from the scope inside of redirect. Also it is very interesting how you can pass the status code for redirects. There are almost no mention about it in manuals. You simply need to specify additional parameterstatus: 302
. The same result could be achieved in few different ways:scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do root to: redirect(status: 302) {|params, request| "/#{params[:locale]}/posts"} end
or we can skip unused variables:
scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do root to: redirect(status: 302) {|params, _| "/#{params[:locale]}/posts"} end
or we can even use dynamic segment inside a block:
scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do root to: redirect(status: 302) {|_, _| "/%{locale}/posts"} end
I should note, that redirects without block as a parameter look prettier in the output of
bin/rake routes
. Just compare:# root to: redirect(status: 302) {|_, _| "/%{locale}/posts"} root GET /:locale(.:format) redirect(302) {:locale=>/ru|en/}
# root to: redirect("/%{locale}/posts", status: 302) root GET /:locale(.:format) redirect(302, /%{locale}/posts) {:locale=>/ru|en/}
-
Root path will redirect to default locale
http://example.com/
→http://example.com/en/
The following line is responsible for that:root to: redirect("/#{I18n.default_locale}", status: 302), as: :redirected_root
Note, that parameter
as: :redirected_root
is required, because we already have oneroot_path
and we can't have the second one in Rails 4 (in comparison to Rails 3). -
All paths with no locale specified will be redirected to the same paths with default locale:
http://example.com/posts
→http://example.com/en/posts
.
This is achieved thanks to this longest line:get "/*path", to: redirect("/#{I18n.default_locale}/%{path}", status: 302), constraints: {path: /(?!(#{I18n.available_locales.join("|")})/).*/}, format: false
Parameter
/*path
is so-called globbing. This parameter includes all the characters in the remaining part of url, including slashes, GET parameters andformat: false
allows us to capture even.json
or.html
signatures.Parameter
constraint
checks if path does not begin with locale and only in that case redirects to localized version. This way we will avoid infinite redirects for non-existing urls. I should note, that only GET requests are taken into account, because there shouldn't be any POST requests unless someone did some hacking or we have an error in our source code.
Let's take a look at another sample of routes
scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do resources :posts root to: "main#index" end root to: redirect("/#{I18n.default_locale}", status: 302), as: :redirected_root get "/*path", to: redirect("/#{I18n.default_locale}/%{path}", status: 302), constraints: {path: /(?!(#{I18n.available_locales.join("|")})/).*/}, format: false
We don't have a redirect to the posts page and we have a separate main page here.
Language switching helper
That may seem strange, but the way how links are formed was changed in Rails 4 in comparison to Rails 3. Here is an example of language switching helper for Rails 3:
# app/helpers/application_helper.rb module ApplicationHelper def lang_switcher content_tag(:ul, class: 'lang-switcher clearfix') do I18n.available_locales.each do |loc| concat content_tag(:li, (link_to loc, params.merge(locale: loc)), class: (I18n.locale == loc ? "active" : "")) end end end end
Using the above helper in Rails 3 will give us the usual /en
and /ru
links for the main page. But in Rails 4 the same code will give us /en/main/index
and /ru/main/index
instead. And this is how the very same helper should be implemented in Rails 4:
# app/helpers/application_helper.rb module ApplicationHelper def lang_switcher content_tag(:ul, class: 'lang-switcher clearfix') do I18n.available_locales.each do |loc| locale_param = request.path == root_path ? root_path(locale: loc) : params.merge(locale: loc) concat content_tag(:li, (link_to loc, locale_param), class: (I18n.locale == loc ? "active" : "")) end end end end
It took some time for me to understand how things work here, so I hope you've found something useful in this blog post too.