Хитрые роуты с локалями в Ruby on Rails

  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

Рассмотрим вышеуказанный кусок кода из файла config/routes.rb и построчно изучим что же он делает.

  1. К ресурсу постов добавляется параметр локали в адресе: http://example.com/en/posts

    За это отвечает часть:

      scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do
        resources :posts
      end
    

    Стоит отметить, что доступные локали берутся из настроек приложения.

  2. Корневой путь с указанием локали перенаправляет на локализованную версию постов (это у нас страница по умолчанию) http://example.com/ruhttp://example.com/ru/posts

    Данное поведение описывается следующими строками:

      scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do
        root to: redirect("/%{locale}/posts", status: 302)
      end
    

    Здесь используется конструкция %{locale}, которая позволяет применить динамический сегмент locale из scope внутри редиректа. Также очень интересно, как можно передать статус при перенаправлении - об этом практически нигде не написано, а нужно просто указать дополнительный параметр status: 302. Того же эффекта можно достичь следующей вариацией:

      scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do
        root to: redirect(status: 302) {|params, request| "/#{params[:locale]}/posts"}
      end
    

    или мы можем опустить неиспользуемые переменные:

      scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do
        root to: redirect(status: 302) {|params, _| "/#{params[:locale]}/posts"}
      end
    

    или даже можем писать вот так, за счет использования динамического сегмента:

      scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do
        root to: redirect(status: 302) {|_, _| "/%{locale}/posts"}
      end
    

    Стоит отметить, что перенаправление без использования блока нагляднее отображается в списке bin/rake routes. Сравните:

    # 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/}
    
  3. Корневой путь перенаправляет на локаль по умолчанию http://example.com/http://example.com/en/
    За это отвечает следующая линия кода:

      root to: redirect("/#{I18n.default_locale}", status: 302), as: :redirected_root
    

    Стоит отметить, что здесь обязательно использование параметра as: :redirected_root, поскольку у нас уже объявлен один root_path и второго в Rails 4 быть не может (в отличие от Rails 3)

  4. Все пути без локали перенаправляются на аналогичный путь с локалью по умолчанию, без зацикливаний: http://example.com/postshttp://example.com/en/posts
    Достигается это за счет самой длинной строчки:

      get "/*path", to: redirect("/#{I18n.default_locale}/%{path}", status: 302), constraints: {path: /(?!(#{I18n.available_locales.join("|")})\/).*/}, format: false
    

    /*path — это так называемый глоббинг. При указании такого параметра в него уходит вся оставшаяся часть строки, включая слэши и GET параметры, а в купе с параметром format: false — она однозначно обеспечивает сохранение всех символов в адресе, включая сигнатуру .json или .html в конце.

    Параметр constraint проверяет, что строка не начинается с параметра локали и только в этом случае происходит перенаправление на локализованную версию. Такой подход позволяет избежать циклических перенаправлений для несуществующих адресов. Также стоит отметить, что в последнем примере используется только GET, ибо POST запросы без указания локали не могут случиться, если только это не намеренный вызов, либо ошибка в коде.


Рассмотрим еще один, альтернативный вариант роутов

  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

Здесь нет редиректа на страницу постов и есть отдельная главная страница.


Хелперы для переключения языков

Кстати, странно, но в Rails 4 поведение при формирование ссылок - меняется, и если бы мы использовали переключалку языка, которая отлично работала в Rails 3, то для всех страниц она бы работала нормально, а вот на главной мы бы получили ссылки /en/main/index и /ru/main/index вместо привычных /en и /ru.
Вот что было в 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

И вот как приходится реализовывать тот же самый хелпер-метод в 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

Мне потребовалось некоторое время, чтобы понять как все это работает. Надеюсь данная статья поможет чем-нибудь и вам.