【Ruby on Rails】enum の日本語化は gem なしで実現できる

# はじめに

こんにちは、 [FarStep](https://twitter.com/8SKeEdvtHfOcczY) です。
Rails でアプリケーションを作成していると enum の値を日本語化する場面があると思います。
そんなとき [enum_help](https://github.com/zmbacker/enum_help) を導入していませんか？

もちろん enum_help を用いた enum の日本語化も間違った実装ではありません。
しかし、アプリケーションにインストールする gem は少ないに越したことはありません。
なぜなら、

- gem が多いとアプリケーションの保守が大変になる（Rails との依存関係等）
- gem はメンテナンスがいつ終わるか分からない

といった理由があるからです。

そこで本記事では **gem を使わずに** enum の値を日本語化する方法を紹介します。
それでは、始めましょう 🚀


# 環境

最初に、動作確認を行う環境について説明します。
Ruby と Ruby on Rails のバージョンは下記の通りです。

| 名前 | バージョン |
| ---- | ---- |
| Ruby | 3.2.0 |
| Ruby on Rails | 7.0.4 |

本記事では、**Rails 7.0.4** と **Ruby 3.2.0** に対応した Docker 化されたサンプルアプリを使います。Nick Janetakis 氏が公開しているこちらのリポジトリを採用しました。

https://github.com/nickjj/docker-rails-example

本記事の完成版のソースコードは下記の通りです。適宜ご活用ください。

https://github.com/FarStep131/enum_ja

今回は Rails の scaffold を使って作成した CRUD アプリを題材にして enum の日本語化について解説します。作成した CRUD アプリは本の投稿機能を備えており、本には

- **下書き**
- **無料で公開**
- **有料で公開**

というステータスが存在すると想定してください。このステータスを enum で管理します。


# enum の日本語化

## 日本語化前

最初に enum の日本語化を行う前の状況を確認します。
enum はモデルで下記のように定義されています。

```ruby:app/models/book.rb
class Book < ApplicationRecord
  enum status: {
    draft: 0,
    free_pub: 1,
    paid_pub: 2,
  }
end
```

このままの状態で本のステータスを表示すると、下記のようになります。

![](https://storage.googleapis.com/zenn-user-upload/cafb5d56160d-20230203.png)

<details><summary>ステータスを表示しているコード</summary>


```erb
<%= book.status %>
```

</details>

enum で定義した名前がそのまま表示されていますね。

## デフォルトの言語を日本語に設定

それでは、enum の値の日本語化を行います。
まずは、デフォルトの言語を日本語に設定してください。
`config/application.rb` を開いて下記コードを追加しましょう。

```diff ruby:config/application.rb
class Application < Rails::Application
  # ...略
  
+ config.i18n.default_locale = :ja
    
  # ...略
end
```

日本語は `ja` となります。

## ロケールファイルの作成

続いて、ロケールファイルを作成します。
Rails には **ロケール（local）** という多言語化用の言語ファイルが存在します。

`rails new` コマンドを実行すると、デフォルトで `config/locales/en.yml` が作成されるはずです。この `en.yml` がロケールファイルです。`config/locales` 配下の YAML ファイルが、ローケルファイルとして Rails に認識されます。

今回は、日本語化を行うため `config/locales/ja.yml` を作成しましょう。

```bash
$ touch config/locales/ja.yml
```

## `ja.yml` 内の書き方

`ja.yml` が作成できたら、下記コードを記述してください。

```yml:ja.yml
ja:
  activerecord:
    models:
      book: 本
    attributes:
      book:
        title: タイトル
        body: テキスト
        status: ステータス
      book/status:
        draft: 下書き
        free_pub: 無料で公開
        paid_pub: 有料で公開
```

書き方のルールは下記の通りです。

- モデル名を対応させるときは `models` の階層を作って日本語にする文字を記入。
- カラム名を対応させるときは `attributes` の階層を作って日本語にする文字を記入。
- enum の値を対応させるときには、`モデル名/カラム名` の階層を作って日本語にする文字を記入。

今回特に重要なのが、enum の値を日本語に対応させる部分です。

```yml
book/status:
  draft: 下書き
  free_pub: 無料で公開
  paid_pub: 有料で公開
```

`draft`、`free_pub`、`paid_pub` はそれぞれ `app/models/book.rb` で定義されている名前に一致している必要があります。タイポにご注意ください。

これで enum の値を日本語に対応させることができました。

## 日本語化された enum の取得

それでは、日本語化された enum の値を取得してみましょう。

:::message
既に Rails のサーバを起動している場合は、サーバを再起動させてください。
`config/application.rb` の内容はサーバの起動時に読み込まれるためです。
:::

下記コマンドを実行して、コンソールを起動しましょう。

```bash
$ rails c
```

コンソールを起動できましたら、下記コマンドを実行してみましょう。

```bash
$ Book.human_attribute_name('status.draft')
=> "下書き"
```

`"下書き"` が返って来れば成功です 👏
上記コマンドも実行すると、他のステータスも日本語化されているのが確認できるはずです。

```bash
$ Book.human_attribute_name('status.free_pub')
=> "無料で公開"
$ Book.human_attribute_name('status.paid_pub')
=> "有料で公開"
```

ここで、`human_attribute_name` というメソッドが登場しました。
**`human_attribute_name`** とは、ActiveRecord::Base のクラスメソッドであり、内部的に I18n モジュールを利用してくれるメソッドです。I18n モジュールは、ある言語の文言を別の言語の文言に翻訳してくれるモジュールです。
`human_attribute_name` メソッドの引数にロケールファイルで定義した `attributes` を渡すと、よしなに翻訳してくれるというわけです。

`human_attribute_name` は下記コードで定義されています。
メソッドの中で I18n モジュールが使われているのがわかります。

https://github.com/rails/rails/blob/main/activemodel/lib/active_model/translation.rb#L46

`human_attribute_name` を使うことで、日本語化された値を取得することができました。

## enum の値を日本語で取得するメソッドの作成

日本語化された enum の値が取得方法がわかったところで、これをメソッドとして定義して使えるようにしましょう。

```diff ruby:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class

+ def self.human_attribute_enum_value(attr_name, value)
+   return if value.blank?
+   human_attribute_name("#{attr_name}.#{value}")
+ end

+ def human_attribute_enum(attr_name)
+   self.class.human_attribute_enum_value(attr_name, self.send("#{attr_name}"))
+ end
end
```

定義したメソッドを解説します。

### `self.human_attribute_enum_value` メソッド

`self.human_attribute_enum_value` の `self` はクラスを表します。
`self` を使うことで様々なモデルに対してこのメソッドを使うことができます。
メソッドの内容は、下記の通りです。

1. `attr_name`（カラム名）と `value`（enumの値）を引数として受け取る。
2. `value` が空白（空白とは、空であることに加えて `nil`、 `false`、 空白文字で構成されているということを含む）であるか確認し、空白だった場合には return する。
3. `human_attribute_name` メソッドを使って日本語化された enum の値を取得する
4. 日本語化された enum の値を返す

`human_attribute_name` を使って、日本語化された enum の値を返しているんですね。

### `human_attribute_enum` メソッド

メソッドの内容は、下記の通りです。

1. `attr_name`（カラム名）を引数として受け取る。
2. `self.class` に対して先ほど定義した `self.human_attribute_enum_value` を呼び出す。
3. カラム名である `attr_name` と、`self.send` を用いて取得した **カラムに格納されている値** を `self.human_attribute_enum_value` に渡す。
4. `self.human_attribute_enum_value` から enum の値が返ってくる。

**`send`** とは、レシーバの持っているメソッドを呼び出し、そのメソッドの戻り値を返します。
例えば、下記のように status カラムが `"draft"` の空のインスタンスを作成します。

```bash
$ book = Book.new(status: "draft")
```

ここで、変数 `book` に対して send メソッドを実行すると下記のようになります。

```bash
$ book.send("status")
=> "draft"
```

send メソッドの引数にカラムの名前を渡すことで、そのカラムに格納されている値が取得できます。
この取得した値とカラム名を `self.human_attribute_enum_value` に渡すことで、無事日本語化された enum が得られるというわけですね。

ここで一つ疑問が湧きます。
なぜわざわざ send メソッドを使うのか。`self.send("#{attr_name}")` ではなく、`self.attr_name` と書いてはいけないのか。

結論からお伝えすると、`self.attr_name` と書くと **NoMethodError** が発生します。
なぜなら、`self.attr_name` とすると **`self` に対して attr_name メソッドを実行する** と解釈されてしまうからです。当然、`self` には attr_name メソッドが定義されていません。したがって、`self.attr_name` と書くことはできないのです。
では、`self."#{attr_name}"` と書けばよいのではないでしょうか。
残念ながらこちらも動作しません。**SyntaxError** となります。

以上より、今回は send メソッドを使うのがベストとなります。


## ビューに反映

メソッドが定義できましたので、早速ビューで使ってみましょう。
本の一覧を表示するビューで使ってみます。

```erb:app/views/books/index.html.erb
<% @books.each do |book| %>
  ...略
  <%= book.human_attribute_enum(:status) %>
  ...略
<% end %>
```

上記のコードを記述すると、下記のように enum の値が日本語化されるはずです。

![](https://storage.googleapis.com/zenn-user-upload/653c49044374-20230203.png)

gem なしで enum の値を日本語化することができました 🎉


# おわりに

いかがだったでしょうか。
gem を使わずに enum の値を日本語化する一例を示しました。
手軽に導入できると思いますので、是非使ってみてください。


# 補足 - セレクトボックスに渡したい場合

日本語化した enum の値をセレクトボックスにも反映させたい場合があると思います。
その場合は、下記のようなメソッドを `app/models/application_record.rb` 定義すれば OK です。

```ruby
def self.enum_options_for_select(attr_name)
  self.send(attr_name.to_s.pluralize).map { |k, _| [self.human_attribute_enum_value(attr_name, k), k] }.to_h
end
```

定義したメソッドは、下記のように使用することができます。

```bash
$ Book.enum_options_for_select(:status)
=> {"下書き"=>"draft", "無料で公開"=>"free_pub", "有料で公開"=>"paid_pub"}
```

キーが日本語化された enum の値、バリューが元々の enum の値であるハッシュが返ってきます。
このハッシュをセレクトボックスに渡せば、表示が日本語になるはずです。

フォームで使ってみましょう。

```erb
<%= form_with model: book  do |f| %>
  ...略
  <%= f.select :status, Book.enum_options_for_select(:status) %>
  ...略
<% end %>
```

上記のコードを記述すると、下記のように enum の値が全て日本語化されるはずです。

![](https://storage.googleapis.com/zenn-user-upload/27de054f3237-20230203.png)


# 参考文献

https://api.rubyonrails.org/classes/ActiveModel/Translation.html