Railsで多言語対応(i18n)をする前にi18n-tasksなど色々と知っておきたかった話
仕事で多言語対応(i18n対応)をしたのですが、なかなか苦労しました。今回はi18nの話です。
Railsのi18の基本
HTML上に出すテキストの言語や、システム内部で扱う文字列を条件によって変えたい場合、RailsではI18n.tまたは#tヘルパーを使います。
これは色々なサンプルでも使われていますし、Rails初心者でも把握している場合が多いかと思います。一応ここを読んでおくと一通り把握できて良いです。
https://railsguides.jp/i18n.html
何点か不足している情報もあるので、少し補足します。
YAMLの予約語の一部はYAML ver 1.2で仕様から消えるが、Railsが扱うYAMLのverは1.1
# config/locales/en.yml
en:
success:
'true': 'True!'
'on': 'On!'
'false': 'False!'
failure:
true: 'True!'
off: 'Off!'
false: 'False!'
Railsでこれを読み込むとt(‘success.true’)は値が返ってくるけど、t(‘failure.true’)はエラーになります。
YAML 1.1ではtrueやyesという文字列は予約語で、boolとして扱われます。勝手に変換されます。
この辺り参考。色々予約語があるので、ご注意。
https://yaml.org/type/bool.html
これはYAML 1.2では廃止されていて、
https://yaml.org/spec/1.2/spec.html
このように定義が無くなっています。true or falseだけです。
ところがRailsは内部的にyamlライブラリを使っていてこれは内部的にpsychを使っていますが、psychはYAML ver 1.1前提になっています。
https://docs.ruby-lang.org/ja/latest/library/psych.html
RailsではYAML 1.1ということを覚えておくと良いかと思います。
パラメータで変数を埋め込み可能だが、Rails式を直接評価はできない
# config/locales/en.yml
en:
msg:
welcome: 'ようこそ商品Aのサイトへ。 値段:%{price}円'
# code
price = 110
t('msg.welcome', price: price)
こういう感じで書くと文字列内に値を埋め込めるので便利です。よく使います。
ところで、たまにこういうことをしたくなるのですが
# config/locales/en.yml
en:
msg:
# ※これは動きません
welcome: 'ようこそ商品Aのサイトへ。 値段:#{ItemPrice::ITEM_A_PRICE}円'
# config/initializers/item_price.rb
module ItemPrice
ITEM_A_PRICE = 110
end
# code
t('msg.welcome', price: price)
残念ながら動きません。YAML上で式を評価できませんし、I18n.tにevalする機能もありません。しかし工夫するとできるようです。
https://qiita.com/azusanakano/items/4c8385df9cbd864bf61d
evalしてますね。なるほど、上手く動きそう(自分は使っていません)。
scopeは後述のi18n-tasksやIDEのサポートを受けられなくなるのでオススメしない
en:
view:
user:
setting:
email:
label: 'Email'
というように深くネストしている場合に使いたくなるテクニックです。
t('view.user.setting.email.label')
scope = 'view.user.setting.email'
t('label', scope: scope)
この2つは同じ値を返します。
‘view.user.setting.email’をscopeというような名称の変数に入れて使い回せば便利かもしれないですね。
しかし、こうすると後述するi18n-tasksでチェックできなくなったり、IDEのサポートが受けられなくなります。例えば私が使っているIntellijというIDEでは’view.user.setting.email.label’という部分をOption+Clickすると定義元のファイルにジャンプできます。scopeを使っているとこの機能は使えません。
また、t(‘view.user.setting.email’ + ‘.label’)みたいな単純な結合だったり、if文などで動的に文字列を構築する方式でも同様にサポートは受けられなくなります。
極力フルパス指定でやったほうが良いと思います。
一部のkeyは特殊
key名がhtmlや.*_htmlだとHTMLとして扱われたり、
activerecord.modelsやactiverecord.errorsで一部挙動を変えられたり、
<mailer_scope>.<action_name>.subjectでメールの件名を設定したり、
date.formats.*やtime.formats.*で日付の文字列変換の挙動を変えたり、
色々とあります。注意しましょう。
i18n-tasksで定義ファイルをきっちり管理する
多言語対応を行うときに便利なツールが公開されているので、それを使います。
https://github.com/glebm/i18n-tasks
Railsにはデフォルトでi18n-tasksは含まれていないです。
READMEに従ってインストールします。
$ cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
$ cp $(i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
これらのコマンドでテンプレートファイルをコピーしていますね。
specの方はコピーしたら変える必要はないです。このspecによって使われていないキーがあるかどうか、Viewなどで使われているがYAML常に定義がないか、YAMLが標準化(keyの辞書順ソート)されているか、がチェックされます。
rubocopを使っている場合はエラーになってしまうので
# rubocop.yml
AllCops:
Exclude:
- 'spec/i18n_spec.rb'
で、除外しておくと良いと思います。
肝心なのはi18n-tasks.ymlの方で、これを調整しながi18n-tasksコマンドを使うことになります。
i18n-tasksコマンドの使い方
https://qiita.com/fakestarbaby/items/d9ad517fe674059041cd
こちらの記事が参考になります。
基本的によく使うのはunused, missing, normalizeあたりですね。findやremove-unusedも人によってはよく使うかもしれません。
i18n-tasks unusedで使われていないkeyの一覧が出てくるので、必要に応じて消しましょう。
しかし以下のようなコードは実際はkeyを使っているにも関わらず、使っていないと出てきます。
type_num = 1
if type_num == 0
role = 'user'
else
role = 'admin'
end
t("common.roles.#{role}")
この場合は、i18n-tasks.ymlに定義を追加します。これにより警告を回避可能です。
ignore_unused:
- 'common.roles.*'
# common.roles.{user,admin} とかでもOK
同様に以下のケースもunusedとして出てきますので、必要に応じてignoreに追加します。ただ、ignore追加するの面倒なので、できれば普段から(できるだけ)、keyはフルパスで指定しておいた方が良いですね。
# config/locales/en.yml
en:
msg:
msg1: 'Message1'
# code
# OK これは警告出ない
t('msg.msg1')
# OK
t('msg1', scope: 'msg')
# NG
scope = 'msg'
t('msg1', scope: scope)
# NG
t('msg' + '.msg1')
# NG
t('msg'\
'.msg1')
+したり分割したstringも無理なようですね。残念。
デフォルトでは全てのlocaleファイルに対してチェックが行われるかと思いますが、例えばja.ymlを作っていて、en.ymlはごく一部のみ定義が存在する、みたいなケースの場合、i18n-tasks missingでたくさんmissingが出てしまいます。
もし、他のものはチェックしたくない、と思う場合、localesの設定を使うと良いです。
その他、大体のものはテンプレートファイルに書いてあるので、コメントをよく読むといいと思います。
https://github.com/glebm/i18n-tasks/blob/master/templates/config/i18n-tasks.yml
以上です。
今開発しているシステムはi18n-tasksは使っているものの、specでmissingしかチェックしていなかったので、これからはunusedなどもチェックするようにしました。上記の問題を知っていたら初めからkeyは(できる限り)フルパスで書いていきたかったです。