- 利用者用のページに英語の説明を付けたい
- 言語切替は不要
- 管理者ページでは英語不要
といった要望に対応した時のメモです。
基本方針
あとからやっぱり言語切替方式にしたいと言われても困らないよう、なるべくI18n標準のやり方から外れないようにして、最低限の手間で移行できるようにする。
通常の翻訳表示
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
翻訳を準備する。
ja:
example: 'メッセージ'
example2: '値: %{val}'
en:
example: 'Message'
example2: 'Value: %{val}'
viewで日英併記したい箇所をt()からjet()メソッドに変更する。
# そのまま
<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 = '<s>XSS!!!</s>' %>
<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()付ける必要があることも思い出しやすいので。
上の例はこんな感じで表示されます。
フォーム内のラベル
フォーム内の表示も基本的には上のjet()でカバーできますが、ラベル要素はうまくいかなかったので、ヘルパーに以下のメソッドを追加しました。
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()をモデル名と属性名を渡したものを指定します。
<%= 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 %>
折り返しや必須マークがずれて気になるのでこう書き換えました。
<%= f.label :mail, jel(SampleModel, :mail, ' /<br>').html_safe %>
エラーメッセージ
これはなかなか厄介で、viewに渡される時点ではすでに標準ロケールで翻訳済みのエラーメッセージしか入っていないため、メッセージを生成しているタイミングでなんとかするしかなさそうでした(他にいい方法あったらコメントください)。
というわけで、モデルのバリデーション時用のjet()メソッドを定義します。
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()に渡すように変更
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) }
<%= 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 %>
:
翻訳を準備します。
errors:
counter:
one: ! 'エラーが発生しました'
other: ! '%{count}個のエラーが発生しました'
errors:
counter:
one: ! '1 error prohibited'
other: ! '%{count} errors prohibited'
これでエラーメッセージも日英併記になりました。
おわりに
フォームのボタン等でセパレータにタグが使えないなどの問題は残っていますが、言語切替方式への移行はjet(),jel()を単なるt()へのラッパーに書き換えれば一瞬で済む、はず、たぶん。