Rails i18n で パスで言語を切り替える と 言語指定のパス無し を両立する

Rails では yaml の定義で言語の変更ができるのですが、「少し込み入ったことをしたいなぁ」と試したので、メモです。

参考

やりたかったこと

言語の切り替え機能を維持しつつ、デフォルトではパスに言語を含まないものも許容したい。
要は以下の URL すべてを満たしたい。それも少ない労力で。

  • hogehoge.jp/home
  • hogehoge.jp/ja/home
  • hogehoge.jp/en/home

とりあえず、作ってみる

application.rb

config/application.rb の記述は、Rails ガイドに親切に書かれているので、そのまま踏襲。

config/application.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require_relative "boot"

require "rails/all"

Bundler.require(*Rails.groups)

module App
class Application < Rails::Application
config.load_defaults 6.1

# 言語ファイル参照先パス
I18n.load_path += Dir[Rails.root.join("config/locales/*.{rb,yml}")]

# 使用する言語
config.i18n.available_locales = %i(ja en)

# ロケールのホワイトリスト化を強制するかどうか
config.i18n.enforce_available_locales = true

# デフォルト言語
config.i18n.default_locale = :ja
end
end

routes.rb

ルーティングは、以下のようにしておく。

routes.rb
1
2
3
4
5
6
Rails.application.routes.draw do
scope "/:locale", locale: /#{I18n.available_locales.map(&:to_s).join("|")}/ do
get "home", to: "home#index"
end
get "home", to: "home#index"
end

コントローラー

routes.rb で取り出した locale を i18n の設定として記述する設定を記述。

app/controllers/application_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ApplicationController < ActionController::Base
before_action :set_locale

def set_locale
I18n.locale = locale
end

def locale
@locale ||= params[:locale] ||= I18n.default_locale
end

def default_url_options(options = {})
{ locale: I18n.locale }
end
end

特に変数の設定はしないのでコントローラを作っておくだけ。

app/controllers/home_controller.rb
1
2
class HomeController < ApplicationController
end

view

言語の切り替えだけを確認したいので、非常に簡単な実装で。

app/views/home/index.html.erb
1
2
<%= link_to "en", "/en/home" %> <%= link_to "ja", "/ja/home" %> <%= t(".hello")
%>

locales

locales は、ja と en を両方用意。

config/locales/js.yml
1
2
3
4
ja:
home:
index:
hello: "ハロワ"
config/locales/en.yml
1
2
3
4
en:
home:
index:
hello: "Hello world"

確認

ここまでの実装で起動すると<%= t(".hello") %>の部分が変わる。

パスと、表示の内容の組み合わせは以下の通り。

  • hogehoge.jp/home => ハロワ
  • hogehoge.jp/ja/home => ハロワ
  • hogehoge.jp/en/home => Hello world

とりあえずできることは確認。

ここで問題 パスが増えたら辛くない?

例えば以下のような状態。

  • hogehoge.jp/home
  • hogehoge.jp/ja/home
  • hogehoge.jp/en/home
  • hogehoge.jp/users
  • hogehoge.jp/ja/users
  • hogehoge.jp/en/users
  • hogehoge.jp/users/:id
  • hogehoge.jp/ja/users/:id
  • hogehoge.jp/en/users/:id

など多数の時。

routes.rb
1
2
3
4
5
6
7
8
9
10
11
Rails.application.routes.draw do

scope "/:locale", locale: /#{I18n.available_locales.map(&:to_s).join("|")}/ do
get "home", to: "home#index"
resources :user
end

get "home", to: "home#index"
resources :user

end

こんな形で locale を含んだパスと、含まないパスをそれぞれ書くのか?という問題。

draw で解決した

この件、drow を使ってまとめてあげると解決。
以下のようにルーティングを記述。

config/routes/root.rb
1
2
get "home", to: "home#index"
resources :user

準備した、root.rb を routes.rb で呼び出す。(名前は変えた方がよかっただろうな。)

routes.rb
1
2
3
4
5
6
7
8
Rails.application.routes.draw do

scope "/:locale", locale: /#{I18n.available_locales.map(&:to_s).join("|")}/ do
draw :root
end
draw :root

end

この書き方をすると、root.rb でパスを記述しておくと、locale 有り無しにまとめて対応できたのでとても楽。
そして、変更箇所が一か所で済むのでとてもベスト。

そのほか小ネタ

いろいろ試していて、気が付いたこと。

パラメータつきの URL を維持したまま、言語切り替え

言語を切り替えるリンクを踏むと、そのページを表示するために使っていた GET クエリパラメータが消えてしまう。

もちろん?a=1というパラメータを持っていた場合には、次のように対応できる。

<%= link_to "index", home_path(locale: locale, a: params[:a]) %>

が、各ページ作るのは面倒だし、検索ページなどパラメーターが増えた時煩雑すぎる。

こんな形で対応できた。

1
2
<% non_locale_path = request.fullpath.gsub(/^\/.*\//, '') %> <%= link_to "en",
"/en/#{non_locale_path}" %> <%= link_to "ja", "/ja/#{non_locale_path}" %>

request.fullpath がパラメーターを含んだパス。
例えば、/ja/home?a=1というパスが有ったら、home?a=1 だけ切り出して、言語部分と連結してリンクを作る。
これだと、クエリを維持したまま、言語切り替えするリンクが作れる。


draw を使うと割と解決できてしまうところがわかったのでよかった。
同じことを複数書かなくていいのはやはりいいなと感じるところ。

ではでは。