Railsで日英併記

- 利用者用のページに英語の説明を付けたい
- 言語切替は不要
- 管理者ページでは英語不要

といった要望に対応した時のメモです。

## 基本方針

あとからやっぱり言語切替方式にしたいと言われても困らないよう、なるべくI18n標準のやり方から外れないようにして、最低限の手間で移行できるようにする。

## 通常の翻訳表示

application_helper.rbに以下のようなメソッドを追加。

```ruby:app/helpers/application_helper.rb
module ApplicationHelper
  def jet(tag, opts = {})
    sep = opts.delete(:sep) || opts.delete('sep') || ' / '
    t(tag, { locale: 'ja' }.merge(opts)) + sep + t(tag, { locale: 'en' }.merge(opts))
  end
end
```

翻訳を準備する。

```ruby:config/locale/ja.yml
ja:
  example: 'メッセージ'
  example2: '値: %{val}'
```

```ruby:config/locale/en.yml
en:
  example: 'Message'
  example2: 'Value: %{val}'
```

viewで日英併記したい箇所をt()からjet()メソッドに変更する。

```html+erb:app/views/sample/sample.html.erb
# そのまま
<p><%= jet('example') %></p>

# 改行する
<p><%= jet('example', sep: '<br/>').html_safe %></p>

# リスト内
<ul><li><%= jet('example') %></li></ul>

# 別要素にする
<ul><li><%= jet('example', sep: '</li><li>').html_safe %></li></ul>

# 引数付き
<%- @params = '
XSS!!!
' %>
<p><%= jet('example2', val: @params) %></p>

# ダメな例
<p><%= jet('example2', val: @params, sep: '<br/>').html_safe %></p>

# こう書く必要あり
<p><%= jet('example2', val: h(@params), sep: '<br/>').html_safe %></p>
```

セパレータをhtmlタグにした時便利だからとjet()メソッド内でhtml_safeしてしまうと、最後から3番目のようなケースがXSS脆弱性になってしまうので少し面倒でも安全側に振っています。'html_safe'付けるときにパラメータにもh()付ける必要があることも思い出しやすいので。

上の例はこんな感じで表示されます。

> ![0_SS 19.png](https://qiita-image-store.s3.amazonaws.com/0/154669/93f777f5-677c-90db-72b9-ca7bd8cb75e3.png)


## フォーム内のラベル

フォーム内の表示も基本的には上のjet()でカバーできますが、ラベル要素はうまくいかなかったので、ヘルパーに以下のメソッドを追加しました。

```ruby:app/helpers/application_helper.rb
module ApplicationHelper
  :
  def jel(model, attr, sep = ' / ')
    model.human_attribute_name(attr, locale: 'ja') + sep +
    model.human_attribute_name(attr, locale: 'en')
  end
end
```

で、labelの第2引数にjel()をモデル名と属性名を渡したものを指定します。

```html+erb:app/views/sample/sample_form.html.erb
<%= form_for(@obj, :html => { :class => 'form-horizontal' }) do |f| %>
  :
  <div class="form-group">
    <div class="col-sm-2 control-label required">
      <%= f.label :mail, jel(SampleModel, :mail) %>
    </div>
    <div class="col-sm-5 controls">
      <%= f.text_field :mail %>
    </div>
  :
<%= end %>
```

> ![0_SS 16.png](https://qiita-image-store.s3.amazonaws.com/0/154669/617dc251-6acf-93ee-02f8-97a10284ee12.png)

折り返しや必須マークがずれて気になるのでこう書き換えました。

```html+erb:app/views/sample/sample_form2.html.erb
      <%= f.label :mail, jel(SampleModel, :mail, ' /<br>').html_safe %>
```

> ![0_SS 17.png](https://qiita-image-store.s3.amazonaws.com/0/154669/5b2679a9-3a1d-bdea-0bf7-d32d2a9359cb.png)

## エラーメッセージ

これはなかなか厄介で、viewに渡される時点ではすでに標準ロケールで翻訳済みのエラーメッセージしか入っていないため、メッセージを生成しているタイミングでなんとかするしかなさそうでした（他にいい方法あったらコメントください）。

というわけで、モデルのバリデーション時用のjet()メソッドを定義します。

```ruby:app/models/concerns/model_helper.rb
module ModelHelper
  extend ActiveSupport::Concern

  included do
    def self.jet(attr, tag, opts = {} )
      sep = opts.delete(:sep) || opts.delete('sep') || ' / '
      (I18n.t(tag, { locale: 'ja' }.merge(opts)) + sep +
        (attr.nil? ? '' : (self.human_attribute_name(attr, locale: 'en') + ' ')) +
        I18n.t(tag, { locale: 'en' }.merge(opts)))
    end
  end
end
```
と、書いていて気付きましたが、Rails5ならapp/models/application_record.rb内にメソッド追加すれば次のincludeの必要も無いですね。とりあえずここではそのまま続けます。

エラーメッセージを日英併記にしたいモデルでModelHelperモジュールをincludeして、バリデーションのmessageオプションにjet()の結果を渡すように書き換えます。

- I18n.t()を使っていた場合はjet()に変更
- 標準エラーメッセージを使っていた場合は、タグを頑張って探してjet()に渡すように変更

```ruby:app/models/sample.rb
class Sample < ActiveRecord::Base
  include ModelHelper

  # validates :mail,    presence: true
  validates :mail,      presence: { message: jet(:mail, 'errors.messages.blank') }

  # validates :mail,      format: { with: RE_EMAIL_ADDRESS, message: I18n.t('errors.mail.is_invalid'), allow_blank: true }
  validates :mail,      format: { with: RE_EMAIL_ADDRESS, message: jet(:mail, 'errors.mail.is_invalid'), allow_blank: true }

  # validates :name,      length: { in: NAME_MIN..NAME_MAX, allow_blank: true }
  validates :name,      length: { in: NAME_MIN..NAME_MAX, allow_blank: true,
                                  too_short: jet(:name, 'errors.messages.too_short', count: NAME_MIN),
                                  too_long: jet(:name, 'errors.messages.too_long', count: NAME_MAX) }
```

```html+erb:app/views/sample/_form.html.erb
<%= form_for(@sample, :html => { :class => 'form-horizontal' }) do |f| %>
  <% if @sample.errors.any? %>
    <div id="error_explanation" class="panel panel-danger">
      <div class="panel-heading">
        <h4><%= jet('errors.counter', count: @sample.errors.count) %></h4>
      </div>

      <div class="panel-body">
        <ul>
        <% @obj.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
        </ul>
      </div>
    </div>
  <% end %>
  :
```

翻訳を準備します。

```xml:config/locals/ja.yml
  errors:
    counter:
      one: ! 'エラーが発生しました'
      other: ! '%{count}個のエラーが発生しました'
```

```xml:config/locals/en.yml
  errors:
    counter:
      one: ! '1 error prohibited'
      other: ! '%{count} errors prohibited'
```

これでエラーメッセージも日英併記になりました。

![0_SS 18.png](https://qiita-image-store.s3.amazonaws.com/0/154669/2fb8c97b-c8ed-0f39-eb93-4e06bd27a8a4.png)

## おわりに

フォームのボタン等でセパレータにタグが使えないなどの問題は残っていますが、言語切替方式への移行はjet(),jel()を単なるt()へのラッパーに書き換えれば一瞬で済む、はず、たぶん。