Chapter 19

3-9. Devise 日本語化

Masuyama
Masuyama
2022.10.15に更新

Devise 日本語化

現時点では devise に関するエラーメッセージはデフォルトの英語のままとなっています。
例えば、空欄を許可しないカラムについては "can't be blank" と表示されます。
(前回、User.nickname のテストを書いた時にはそれを利用していました)

image

しかし TechLog は日本語の Web アプリですので
devise 関連で表示されるエラーメッセージは日本語化することで、
ユーザーがより使いやすいアプリケーションにしていきます。

テスト修正

今回もテスト駆動開発の流れに則り、先にあるべき姿(仕様)をテストで定義しましょう。

日本語化対応を行った結果、各種エラーメッセージは日本語が表示されることを期待したテストに修正します。
devise 関連のエラーメッセージのチェックはモデルの Spec・System Spec の両方で行なっていたため、それぞれ修正しましょう。

spec/models/user_spec.rb

require 'rails_helper'

describe User do
  let(:nickname) { 'テスト太郎' }
  let(:email) { 'test@example.com' }
  let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) } # 変数に格納

  describe '.first' do
    before do
      create(:user, nickname: nickname, email: email)
    end

    subject { described_class.first }

    it '事前に作成した通りのUserを返す' do
      expect(subject.nickname).to eq('テスト太郎')
      expect(subject.email).to eq('test@example.com')
    end
  end

  describe 'validation' do
    let(:password) { '12345678' }

    describe 'nickname属性' do
      describe '文字数制限の検証' do
        context 'nicknameが20文字以下の場合' do
          let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字

          it 'User オブジェクトは有効である' do
            expect(user.valid?).to be(true)
          end
        end

        context 'nicknameが20文字を超える場合' do
          let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字

          it 'User オブジェクトは無効である' do
            user.valid?

            expect(user.valid?).to be(false)
            expect(user.errors[:nickname]).to include('は20文字以下に設定して下さい。')
          end
        end
      end

      describe '存在性の検証' do
        context 'nicknameが空欄の場合' do
          let(:nickname) { '' }

          it 'User オブジェクトは無効である' do
            expect(user.valid?).to be(false)
            expect(user.errors[:nickname]).to include("が入力されていません。")
          end
        end
      end
    end
  end
end

spec/system/users_spec.rb

require 'rails_helper'

describe 'User', type: :system do
  before { driven_by :selenium_chrome_headless }

  # ユーザー情報入力用の変数
  let(:email) { 'test@example.com' }
  let(:nickname) { 'テスト太郎' }
  let(:password) { 'password' }
  let(:password_confirmation) { password }

  describe 'ユーザー登録機能の検証' do
    before { visit '/users/sign_up' }

    # ユーザー登録を行う一連の操作を subject にまとめる
    subject do
      fill_in 'user_nickname', with: nickname
      fill_in 'user_email', with: email
      fill_in 'user_password', with: password
      fill_in 'user_password_confirmation', with: password_confirmation
      click_button 'ユーザー登録'
    end

    context '正常系' do
      it 'ユーザーを作成できる' do
        expect { subject }.to change(User, :count).by(1) # Userが1つ増える
        expect(page).to have_content('ユーザー登録に成功しました。')
        expect(current_path).to eq('/') # ユーザー登録後はトップページにリダイレクト
      end
    end

    context '異常系' do
      context 'エラー理由が1件の場合' do
        let(:nickname) { '' }
        it 'ユーザー作成に失敗した旨のエラーメッセージを表示する' do
          subject
          expect(page).to have_content('エラーが発生したためユーザーは保存されませんでした。')
        end
      end

      context 'エラー理由が2件以上の場合' do
        let(:nickname) { '' }
        let(:email) { '' }
        it '問題件数とともに、ユーザー作成に失敗した旨のエラーメッセージを表示する' do
          subject
          expect(page).to have_content('エラーが発生したためユーザーは保存されませんでした。')
        end
      end

      context 'nicknameが空の場合' do
        let(:nickname) { '' }
        it 'ユーザーを作成せず、エラーメッセージを表示する' do
          expect { subject }.not_to change(User, :count) # Userが増えない
          expect(page).to have_content('ニックネーム が入力されていません。') # エラーメッセージのチェック
        end
      end

      context 'nicknameが20文字を超える場合' do
        let(:nickname) { 'あ' * 21 }
        it 'ユーザーを作成せず、エラーメッセージを表示する' do
          expect { subject }.not_to change(User, :count)
          expect(page).to have_content('ニックネーム は20文字以下に設定して下さい。')
        end
      end

      context 'emailが空の場合' do
        let(:email) { '' }
        it 'ユーザーを作成せず、エラーメッセージを表示する' do
          expect { subject }.not_to change(User, :count)
          expect(page).to have_content('メールアドレス が入力されていません。')
        end
      end

      context 'passwordが空の場合' do
        let(:password) { '' }
        it 'ユーザーを作成せず、エラーメッセージを表示する' do
          expect { subject }.not_to change(User, :count)
          expect(page).to have_content('パスワード が入力されていません。')
        end
      end

      context 'passwordが6文字未満の場合' do
        let(:password) { 'a' * 5 }
        it 'ユーザーを作成せず、エラーメッセージを表示する' do
          expect { subject }.not_to change(User, :count)
          expect(page).to have_content('パスワード は6文字以上に設定して下さい。')
        end
      end

      context 'passwordが128文字を超える場合' do
        let(:password) { 'a' * 129 }
        it 'ユーザーを作成せず、エラーメッセージを表示する' do
          expect { subject }.not_to change(User, :count)
          expect(page).to have_content('パスワード は128文字以下に設定して下さい。')
        end
      end

      context 'passwordとpassword_confirmationが一致しない場合' do
        let(:password_confirmation) { "#{password}hoge" } # passwordに"hoge"を足した文字列にする
        it 'ユーザーを作成せず、エラーメッセージを表示する' do
          expect { subject }.not_to change(User, :count)
          expect(page).to have_content('確認用パスワード が一致していません。')
        end
      end
    end
  end

  describe 'ログイン機能の検証' do
    before do
      create(:user, nickname: nickname, email: email, password: password, password_confirmation: password) # ユーザー作成

      visit '/users/sign_in'
      fill_in 'user_email', with: email
      fill_in 'user_password', with: 'password'
      click_button 'ログイン'
    end

    context '正常系' do
      it 'ログインに成功し、トップページにリダイレクトする' do
        expect(current_path).to eq('/')
      end

      it 'ログイン成功時のフラッシュメッセージを表示する' do
        expect(page).to have_content('ログインしました。')
      end
    end

    context '異常系' do
      let(:password) { 'NGpassword' }
      it 'ログインに失敗し、ページ遷移しない' do
        expect(current_path).to eq('/users/sign_in')
      end

      it 'ログイン失敗時のフラッシュメッセージを表示する' do
        expect(page).to have_content('メールアドレスまたはパスワードが違います。')
      end
    end
  end

  describe 'ログアウト機能の検証' do
    before do
      user = create(:user, nickname: nickname, email: email, password: password, password_confirmation: password) # ユーザー作成
      sign_in user # 作成したユーザーでログイン
      visit '/'
      click_button 'ログアウト'
    end

    it 'トップページにリダイレクトする' do
      expect(current_path).to eq('/')
    end

    it 'ログアウト時のフラッシュメッセージを表示する' do
      expect(page).to have_content('ログアウトしました。')
    end
  end
end

また、前のカリキュラムでナビゲーションバーを追加したことでログアウトの操作を出来るようになったため、
ログアウト時のメッセージを検証するテストも追加してあります。

現時点ではもちろんテストが失敗することを確認してください。

$ bin/rspec spec/system/users_spec.rb
...
Finished in 32.33 seconds (files took 0.45752 seconds to load)
16 examples, 12 failures

次は、これらのテストを通すために各種設定をしていきましょう。

アプリ全体の日本語化設定

Rails の言語は config/application.rb で設定します。
デフォルトでは英語ですが、次のように 1 行を追加することで日本語として設定されます。

...
module TechLog
  class Application < Rails::Application
    ...
    config.i18n.default_locale = :ja # 追記
  end
end

これにより devise も日本語のエラーメッセージを表示しようとするのですが、
devise の gem 自体には日本語のエラーメッセージが含まれていません。

翻訳ファイルを用意

次に、英語と日本語の対訳を .yml 形式のファイルで置いてあげます。

config/localesというディレクトリの直下にdevise.views.ja.ymlというファイルを作成し、
中身を次のように編集します。

config/locales/devise.views.ja.yml ※新規作成

ja:
  activerecord:
    errors:
      models:
        user:
          attributes:
            email:
              taken: "は既に使用されています。"
              blank: "が入力されていません。"
              invalid: "は有効でありません。"
            nickname:
              blank: "が入力されていません。"
              too_long: "は%{count}文字以下に設定して下さい。"
            password:
              blank: "が入力されていません。"
              too_short: "は%{count}文字以上に設定して下さい。"
              too_long: "は%{count}文字以下に設定して下さい。"
              invalid: "は有効でありません。"
            password_confirmation:
              confirmation: "が一致していません。"
    attributes:
      user:
        nickname: "ニックネーム"
        email: "メールアドレス"
        password: "パスワード"
        password_confirmation: "確認用パスワード"
    models:
      user: "ユーザー"
  errors:
    messages:
      not_saved: "エラーが発生したため%{resource}は保存されませんでした。"

  devise:
    failure:
      invalid: "%{authentication_keys}またはパスワードが違います。"
    registrations:
      user:
        signed_up: ユーザー登録に成功しました。
    sessions:
      new:
        sign_in: ログイン
      signed_in: ログインしました。
      user:
        signed_out: ログアウトしました。

このように .yml 形式で、エラー内容を階層に分けて日本語訳を記載するというのは、devise のお作法に則ったやり方となります。
現在はユーザー登録・ログイン・ログアウト機能しか実装していませんが、パスワード再設定など devise 特有の機能を利用するときには本ファイルを編集することも覚えておきましょう。

テストを実行

さて、ブラウザで動作確認する前にテストを実行してみましょう。

実はこの時点でコケるテストが一つあります。

$ bin/rspec
...
Failures:

  1) Home ナビゲーションバーの検証 ログインしている場合 ログアウトリンクが機能する
     Failure/Error: expect(page).not_to have_content('ログアウト')
       expected not to find text "ログアウト" in "TechLog\nユーザー登録\nログイン\nログアウトしました。\nHome#top\nFind me in app/views/home/top.html.erb"
...
Failed examples:

rspec ./spec/system/home_spec.rb:52 # Home ナビゲーションバーの検証 ログインしている場合 ログアウトリンクが機能する

ナビゲーションバーを追加した時のテストがコケてしまいました。

テストのエラーメッセージをチェックしてみます。

     Failure/Error: expect(page).not_to have_content('ログアウト')
       expected not to find text "ログアウト" in "TechLog\nユーザー登録\nログイン\nログアウトしました。\nHome#top\nFind me in app/views/home/top.html.erb"

expected not to find text "ログアウト"... という一文に注目してください。
「ログアウトというテキストが含まれないことを期待していたが、実際には含まれていた」というエラーです。

ここでテストの中身を確認してみます。

spec/system/home_spec.rb

...
      it 'ログアウトリンクが機能する' do
        click_button 'ログアウト'

        # ログインしていない状態のリンク表示パターンになることを確認
        expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
        expect(page).to have_link('ログイン', href: '/users/sign_in')
        expect(page).not_to have_content('ログアウト')****
      end
...

上記のうち expect(page).not_to have_content('ログアウト') という箇所になります。

これは、ログアウトのリンク有無の識別として「ログアウトという文字があるかどうか」を基準にしていました。
しかし今回、ログアウト成功時のフラッシュメッセージを「ログアウトしました。」というテキストに設定したため、この中の「ログアウト」をログアウトリンクとして認識されています。

原因はテストの書き方にあることが分かりましたので、問題のあったテスト 1 行を修正しましょう。

幸い、ログアウトリンクは type が submit でしたので、Capybara の have_button マッチャを使用します。
これを用い、次のように変更します。

spec/system/home_spec.rb

...
      it 'ログアウトリンクが機能する' do
        click_button 'ログアウト'

        # ログインしていない状態のリンク表示パターンになることを確認
        expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
        expect(page).to have_link('ログイン', href: '/users/sign_in')
        expect(page).not_to have_button('ログアウト') # 修正
      end
...

修正後、先ほどコケていたテストも含めすべてのテストに通ることを確認しましょう。

$ bin/rspec
...
Finished in 9.45 seconds (files took 0.41715 seconds to load)
29 examples, 0 failures

最後に、実際に画面上ではどのように変わっているかをブラウザで確認していきます。
テストで通っていれば機能要件は満たせているものの、見た目として違和感がないかをチェックすることも重要です。

動作確認

bin/devコマンドで開発用サーバを起動し、確認します。
(config/application.rb に変更を行った場合は開発用サーバの再起動が必要になるため、前回から起動したままだった場合は再起動してください)

ユーザー登録、ログイン、ログアウトそれぞれで出るメッセージがすべて日本語になっていることを確認しましょう。

ユーザー登録

http://localhost:3000/users/sign_up にアクセスしましょう。

最初はすべての欄を空にしたまま、ユーザー登録に失敗します。

image

意図したメッセージが表示されています。

次は、ちゃんとした情報を埋めてユーザー登録に成功しましょう。

image

成功時のメッセージがバッチリ表示されていますね。

ログイン

http://localhost:3000/users/sign_in にアクセスしましょう。

最初は適当な認証情報でログインに失敗します。

ログイン失敗時

image

認証に失敗した理由とともに、エラーメッセージが出ていますね。

次は作成したユーザーの認証情報でログインします。

ログイン成功時

image

「ログインしました。」と表示されていますね。

ログアウト

最後に、ログアウト時のメッセージが翻訳できていることを確認しましょう。
ナビゲーションバーからログアウトリンクを選択します。

image

「ログアウトしました。」と表示されていれば OK です。

変更をコミット

ここまでの変更をコミットしておきます。

$ git add .
$ git commit -m "devise関連のメッセージを日本語化"
$ git push

これで devise ユーザー認証周りの変更は一旦おしまいです。
ボリュームが多く、devise のルールを覚えるのは大変だったかと思います。
お疲れ様でした!

宿題

devise-i18n

devise-i18n というまた別な gem を使うと、翻訳ファイルを自分で用意しなくてもコマンドで生成したりもできます。
(ただし、不要な翻訳の取捨選択が必要なことや、ブラックボックス化を避けるため今回は使いませんでした)

gem の使い方を調べる前提とはなりますが、興味がある方は参考にしてみてください。

have_button

今回、Capybara でログアウトリンク(ボタン)を探すために have_button というマッチャを使いました。

have_button マッチャがどういう要素を見つけてくるのかは、前にも紹介した人気の Qiita 記事で復習しておきましょう。