ボクココ

少人数でのサービス開発運営に関するテックブログ

Rails アプリの国際化の実装と考察

ども、@kimihom です。2018年もどうぞよろしくお願いします。

さて、今回は Rails アプリを世界へ向けて展開したい人向けの記事を書いていこう。

基本的な参考資料

まずは Rails の I18Nドキュメント を読むことから始めよう。ここに基本的なことは全て書かれている。

Rails国際化 (I18n) API | Rails ガイド

I18N で立ちはだかるのは大きく2つの事項がある。言語と時差だ。以下にそれぞれ記す。

多言語化対応

まずは多言語化から見ていこう。基本的に Rails では config/locales の中に en.yml ja.yml などを定義していって、 View の中に t('home.title') みたいな感じで指定すれば勝手にそれぞれのロケールの言語で翻訳してくれる。てな訳で開発中の多言語化の際には上記 yml ファイルの中に記載していく習慣を持っておけば基本的には問題ないだろう。

ただし、JavaScript 側でアラートメッセージなどを出したいって場合には Rails 側で管理できないので工夫が必要だ。自前でやる上で一番簡単な方法は、View のなかに display: none な Key-Value のセットを保存しておくやり方がある。ただこの方法だと量が多くなると管理が相当めんどいので、量が多い場合は i18n-js などを検討しよう。

<div style="display: none;">
  <span id="error1"><%= t('error.msg1') %></span>
  <span id="error2"><%= t('error.msg2') %></span>
  <span id="error3"><%= t('error.msg3') %></span>
  ...
</div>

<script>
console.log($("#error1").text());
</script>

そして重要なのが、ユーザーごとに言語を切り替える設定の部分だ。デフォルトのロケールは config/application.rb で書く。以下の例では 何も指定がなければ、日本語になる。

    config.i18n.available_locales = ["en", "ja"]
    config.i18n.default_locale = :ja

ではどうやってユーザーごとに最適な言語を表示させるのか。それは先ほどの Rails 国際化のリンクに方法が書いてあるので、検討してみてほしい。地理やブラウザ言語などで分ける方法もあるけど、私としては URL で完全に分ける方法が最も良いと考えている。なぜなら 基本的に Google や SNS でシェアした URL に言語設定が入っていれば、それがアクセスする人にとっての最適な言語に間違いないからだ。

てな訳で、ルーティングの設定のなかに locale が入るようにセットする。

Rails.application.routes.draw do
  get '/:locale' => "home#index", as: :root
  get '/' => redirect("/ja")

  scope ":locale", locale: /ja|en/ do
    resources :contacts
    # ...
  end
end

URL でロケールを明示する方法の場合、Root URL だけ ja or en のどっちかがわからないのでリダイレクトさせる必要がある点に注意しよう。https://www.my-worldapp.com/en/contacts/1 のような URL 構成になる。

さて、この URL に params[:locale] が入るようになるので、あとはコントローラ側で表示の出し分けをすれば OK だ。

class ApplicationController < ActionController::Base
  before_action :set_locale

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

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

default_url_options を指定することで、link_to の時も勝手にロケールが入るみたい。ついでに ActionMailer 側も以下のように設定しておくと、メールに URL 含める場合でもロケールが考慮される。

class ApplicationMailer < ActionMailer::Base
  default from: 'support@my-worldapp.com'
  layout 'mailer'

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

タイムゾーン

さてタイムゾーンの話に移っていこう。時差がある中でどうやってユーザーに最適な日時を表示させるかという問題である。

当然のことながら データベースでの日時は UTC で保存しよう。config/application.rbtime_zone の設定がなければ勝手に UTC になる。

Rails 側で勝手に作られる created_atupdated_at は基本的にサーバー側で UTC で保存されるようになるから問題になることはないだろう。あとはどうやってユーザーに時差付きのタイムゾーンを表示させるか、となる。

んで JavaScript には時差をとってくる方法があるので、JS 側でみんなやっちゃえばいいやんという考え方が1つある。しかし、その方法だと日時のパースのためにユーザーの表示を待たせてしまう必要があるし、そもそも日時を処理する必要があるため非効率的だ。何かしらの SPA フレームワークを使ってない場合はあまり良い方法ではない。

てことで今回は、ユーザーごとにタイムゾーン情報をデータベースに保存する方法を案内する。

1. User に time_zone を保存

stringtime_zone を用意しよう。そんでモデル側で以下のようなバリデーションを用意する。

validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }

これで、例えば Tokyo のようなタイムゾーンをユーザーごとに保存する土台が整う。

2. User ごとのタイムゾーン表示

そんで app/controllers/application_controller.rb で以下のような記述をすると、ログインしたユーザーごとに DateTime を生成してくれる!さすが Rails といったところだ。

class ApplicationController < ActionController::Base
  around_action :user_time_zone, if: :current_user

  private
  def user_time_zone(&block)
    Time.use_zone(current_user.time_zone, &block)
  end
end

3. User のタイムゾーンの選択

最後に、ユーザーがどうやって自分の最適なタイムゾーンを選択するかという話に移ろう。これが厄介だ。 一番簡単なのは、ユーザー登録の時点でタイムゾーンを選択してもらう方法である。 Rails の View では <%= f.time_zone_select :time_zone %> ってのが用意されているので、これを使えばセレクトボックスを簡単に用意できる。

f:id:cevid_cpp:20180106141647p:plain

タイムゾーンを更新する場合は上記方法でも良いかもしれないけど、登録の際にこのセレクトボックスを出させるのは気が引ける方は多いと思う。だって、JavaScript でタイムゾーンを取ってこれるからね。てことで、例えば会員登録の際にタイムゾーンは JavaScript で取ってきたタイムゾーンで登録させるっていうクールな方法を紹介する。

var min = -(new Date().getTimezoneOffset());
var hrs = min / 60;
((hrs > 0) ? "+" : "-") + ((Math.abs(hrs) < 10) ? "0" : "") + parseInt(Math.abs(hrs)) +  ":" + ((min % 60 > 0) ? "" : "0") + min % 60;
// => "+09:00"

なかなかトリッキーな方法だけど、上記のコードを書くことで実現した。なんと時差で "4:30" みたいな 30分ずれるみたいなこともあるらしく、ちょっと複雑なコードになってしまった。

残念ながら、JavaScript では上記のように時差(+9:00) のような文字しかとってくることができない。データベースで保存しているのは、Tokyo といった文字列である。てことで、先ほどの <%= f.time_zone_select :time_zone %> を活用してなんとか動かすように JavaScript 側で工夫しよう。最終的に以下のようなコードで実現した。

    var min = -(new Date().getTimezoneOffset());
    var hrs = min / 60;
    var timezone = ((hrs > 0) ? "+" : "-") + ((Math.abs(hrs) < 10) ? "0" : "") + parseInt(Math.abs(hrs)) +  ":" + ((min % 60 > 0) ? "" : "0") + min % 60;
    $("#time_zone option").each(function(i, elm) {
      if ($(elm).text().includes(timezone)) {
        $("#time_zone").val($(elm).val());
        return false;
      }
    });

time_zone_select に時差を表示してくれている部分があるので、最初にその時差とマッチするセレクトボックスがあったらそれを値として登録するようにした。この方法のデメリットとして、+9:00 の場合は Osaka がデフォルトになる という点だ。まぁ時間さえ一致すればあまり問題にはならないかなということで、この方法で今やってみている。

終わりに

今回は 世界的ウェブサービスを生むために必要な、I18N について記載した。

こんな記事を書いているということは、私自身そういう想いで作っている何かがあるということである。是非みなさんも世界へ向けたサービス開発にチャレンジしてみてほしい。それに必要なのは、根拠のない自信と実際にやるという行動力だ!