波打際のブログさん

主に、プログラミング備忘録など。

Rails4のバリデータをテストまでしっかり書いた備忘録

はじめに

 Rails4には様々なバリデータが用意されていますが、プロダクトを実装する過程で自作のバリデータを定義して使いたくなることが稀によくあります。稀によくある事なので、その都度使い方を調べていていますが、バリデータの実装からロケールファイルの定義、テストの書き方までまとまっているページがないので備忘録程度にまとめておきます。

バリデータ

今回作るバリデータについて

 バリデーション対象のカラムの内容が hoge であるかチェックするHogeValidatorを作成します。バリデーションのオプションで hoge が連続する回数を指定できるようにします。

# text が `hoge` であることのバリデーション
validates :text, :hoge => {:count => 1}

# text が `hogehoge` であることのバリデーション
validates :text, :hoge => {:count => 2}


こんな感じで使えるバリデータを作ります。

バリデータの定義

 `app/validators` フォルダに `hoge_validator.rb` を作ります。

class HogeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless ('hoge' * options[:count]) == value
      record.errors.add(attribute, :hoge, options)
    end
  end
end

ロケールの定義

 通常のバリデーションエラーメッセージと同様に記述していきます。

ja:
  activerecord:
    errors:
      messages:
        hoge: 'hoge成分が足りません。'

テスト

 `spec/validators/` フォルダに `hoge_validator_spec.rb` を作ります。

require 'spec_helper'

describe HogeValidator do
  let :validator do
    HogeValidator.new({:attributes => [attribute]}, :count => 1)
  end

  let :errors do
    ActiveModel::Errors.new(mock_class)
  end

  let :attribute do
    :text
  end

  let :mock_class do
    double('model').tap do |mock|
      allow(mock).to receive(:class) { ActiveRecord::Base }
      allow(mock).to receive(:errors) {errors}
      allow(mock).to receive(:read_attribute_for_validation) {attribute_value}
    end
  end

  subject do
    validator.validate_each(mock_class, attribute, attribute_value)
    errors.empty?
  end

  context 'ただしくHogeってる時' do
    let :attribute_value do
      'hoge'
    end

    it 'バリデーションが通ること' do
      expect(subject).to be_truthy
    end
  end

  context 'Hogeってないとき' do
    let :attribute_value do
      'fuga'
    end

    it 'バリデーションが通らない' do
      expect(subject).to be_falsey
    end
  end
end

 今回は省略しましたが、エラーメッセージに関するテストを行うとさらに良いと思います。

クラウドIDEを使ってみた Nitrous.io & Codeanywhere

はじめに

 お久しぶりです。昨年末から関ってきた某プロジェクトを5月にリリースし、最近やっと落ち着いてきました。落ち着いてきたついでに先週末ごろから開発環境を0から作り直しているのですが、開発環境の構築って面倒くさいですよね。検証環境なんかはVagrantを使えば良いかもしれません、しかし、開発環境となると話は別です。IDEをインストールしてエディタの設定をして...etc 面倒くささの極み!!!

そこで、私の中で今流行のクラウドIDEを使ってみることにしました。

クラウドIDE

 クラウド上の仮想マシンで、ブラウザから動作するIDEを使いソフトウェア開発ができる便利っ子です。今回は Codeanywhere と Nitrous.io を実際に使用してみました。

Nitrous.io

https://www.nitrous.io/
f:id:alfa-jpn:20140701034142p:plain

 上記の2つのサービスはウェブ上(HTML5)で動作するため、手持ちのSurfaceRTでも動作しました。

比較

 双方一番安い有料プランに登録しての比較です。
 

仮想環境について

Codeanywhere
  • Redhat
  • sudo権限あり
  • ブラウザ上のターミナル以外から接続する方法なし
Nitrous.io
  • Debian
  • sudo権限なし
  • SSHでログイン可能

 Nitrous.ioの仮想マシンはsudo権限がユーザに付与されていません。仮想マシン作成時に選択した開発言語に応じて、必要なソフトウェアが自動でインストールされます。また、オートパーツというブラウザ上のパッケージマネージャから、追加パッケージをインストールできます。

IDEについて

Codeanywhere
  • 自動でオートコンプリートリストが開く
Nitrous.io
  • ショートカットキー( ctrl + space )でオートコンプリートリストが開く

 Rubyを用いて簡単なチェックを行ったところ、双方ともオートコンプリート機能は備えています。ただし一般的なIDEのように標準関数や別ファイルの関数は補完できません。またリファクタリングなどの強力なサポートはありません。このあたりは双方ともにプロジェクトの概念が無いためかもしれません。

 積極的に人柱になって、プロジェクト概念の導入と、標準関数のオートコンプリートの要望を出していきましょう!

パフォーマンスについて

Codeanywhere
  • 非常に不安定
Nitrous.io

 Codeanywhereは無料版でも有料版でもパフォーマンスは悪く非常に不安定です。それに引き換え、Nitrous.io は非常に安定しています。仮想マシン作成時に設置場所を選択でき、EastAsiaを選択したところ、非常に安定しており、パフォーマンスも良いです。

料金について

Codeanywhere
  • $2から
  • マシンの設置可能台数がプランによって変化
Nitrous.io
  • $7から
  • N2Oというポイントをリソースに割り当てる

 Codeaanywhereは同一スペックのマシンを契約プランに応じた台数分設置できるという料金体系です。Nitrous.ioはN2Oというポイントを利用して仮想マシンを立てたり、仮想マシンのスペックをアップグレードしたり、ユーザが使用方法を自由に選べる料金体系です。

 Codeanywhereは月額$2からスタータープランが用意されているのに対し、Nitrous.ioは月額$7からのスタータープランになっています。同額料金を支払うならCodeanywhereのほうが圧倒的に多くのリソースが手に入ります。

まとめ

「リソースいっぱい使って、いくつも環境作りたい!」という人は料金体系的にCodeanywhereをお勧めしたいところですが、前述したとおり動作が非常に不安定です。日本国内から使用するなら安定性とパフォーマンスの問題から、現時点では Nitrous.io しか選択肢はなさそうです。

nginxモジュールのテストを書く

はじめに

 唐突ですが、仕事でシステムの構成を考えていると、nginxを拡張したいなーとか思うことってあると思います。nginxはC言語で割と簡単に拡張モジュールを書くことができます。(今回は拡張モジュールの書き方については触れませんが・・・)

 だがしかし、仕事なんだからテストコードは必須だよね(迫真)。既存のnginx用のモジュールのコードを眺めてみると、謎の`t`フォルダにテストコードが入っています。
`t`ってなんだよ`t`って!中身もhogehoge.tとかなってるし、t言語なんかあるの? というところから始まり、なんとかテストを書いて実行できるようになったので方法をまとめておきます。

 具体的にはperlのテストフレームワークagentzh/test-nginx · GitHub を利用してテストを書いていきます。

さっきからPerlっていう単語が・・・

 あれって2000年前半にオワコンの技術じゃ・・・と思う人も思いますが、割と進化を続けているようです。(Perlのエロ人、こんな説明ですみません、CGI時代にPerlをちょっと触ったにわかです許して下しあ><)

Perl使ったことないんだけど

 今回紹介するテスト方法はPerlのコードをガリガリ書く必要はないので安心してください。(ここまで簡単にかけるテストフレームワークはむしろ初めてでした。)

テストの準備

必要なもの

 nginxのテストをするために必要なソフトウェアやライブラリです。URLを掲載していないものはUbuntu13.10の公式リポジトリからSynapticを使ってインストールしました。

nginxのコンパイルに必要なもの

 テストを実行する上でecho-nginx-moduleとmemc-nginx-moduleは不可欠でした。
※`/home/develop` 下にecho-nginx-moduleとmemc-nginx-moduleをcloneしていると想定して記事を書いています。

perlでテストを実行するために必要なもの

test-nginxは適当なディレクトリにcloneしておき、テスト実行時にディレクトリを指定します。
※`/home/develop' 下にtest-nginxをcloneしていると想定して記事を書いています。

テストコードの書き方

 nginxモジュールのプロジェクトディレクトリに`t/`というディレクトリを作り、その中に`{任意の名前}.t`という形でテストコードを書いていきます。

use Test::Nginx::Socket;

# テストの実行回数の設定。
repeat_each(3);
plan tests => repeat_each() * 2 * blocks();

# 文字の表示をわかりやすくする。
no_long_string();

# テストの実行
run_tests();

__DATA__

=== TEST 1: ハローワールドのテスト
--- config
    location /hello {
        echo hello_world;
    }
--- request
    GET /hello HTTP/1.0
--- response_body
hello_world
--- error_code: 200

`--- config` にnginxの設定を記述し、`---request` でテストとして実行するリクエストを記述します。
 次に、リクエストに対して`--- response_body` で期待するレスポンスを記述し、 `--- error_code` に期待するレスポンスコードを記述します。

こんなに簡単にかけるテストは見たことはありません!幸せ。

テストの実行

大まかな流れ

 nginxのテストの大まかな流れは次のとおりです。

  1. 自分のモジュールを組み込んだnginxをコンパイル&インストールする。
  2. コンパイルしたnginxのパスを通す。
  3. プロジェクトのディレクトリでproveを実行する。

nginxのコンパイル

 テストを実行するために、自分のモジュールを組み込んだnginxをコンパイルします。私の場合は次のようなオプションでコンパイルしました。

CFLAGS="-O1 -Wall -g" ./configure --add-module=[自分のモジュール] 
   --add-module=/home/develop/echo-nginx-module
   --add-module=/home/develop/memc-nginx-module
   --prefix=/home/develop/nginx --with-debug

特筆すべき点は下記2点です。

  • CFLAGSはvalgrindを利用してプロファイリングを行うことを想定しています。
  • プレフィックスをつけて通常使用するnginxとは分けました。(当然といえば当然ですね。)

proveでテストの実行

 まず、先ほどコンパイルしたnginxにパスを通します。

export PATH=/home/develop/nginx/sbin:$PATH

そしてテストを実行します。

prove -I/home/develop/test-nginx/lib -r t

proveを実行する際にtest-nginxをincludeのがポイントです。

valgrindでプロファイリングする場合

 メモリリークや実行速度についての解析をvalgrindを用いて行う場合にはTEST_NGINX_USE_VALGRINDという環境変数にオプションを記述します。

TEST_NGINX_USE_VALGRIND="{オプション}" prove -I/home/develop/test-nginx/lib -r t

メモリリークのテストをする場合にはこんなかんじでしょうか。

TEST_NGINX_USE_VALGRIND="-v --tool=memcheck --leak-check=full" prove -I/home/develop/test-nginx/lib -r t

RailsのEnumと相性が悪い人向けのInumをバージョンアップさせた話。

はじめに

Railsの列挙型事情

 Rails4.1以前では公式に列挙型が実装されておらず、列挙型を実現するために様々なgemが生み出されてきましたが、Rails4.1でついにActiveRecord::Enumという列挙型のクラスがやっと実装され、長い列挙型宗教戦争が終わろうとしています。

ならオワコンサードパーティの列挙型なんていらないのでは?

 残念ながら妄想していた列挙型ではなかったのです。なのでActiveReocrd::Enumが登場してからもメンテナンスしています。

どうせI18n対応とかじゃないの?

 もちろんI18n対応も理由の一つですが、I18n対応だけで良ければ brainspec/enumerize · GitHub あたりを使うのがベストだと思います。それだけでは足りないほしがりさん向けです。

Inumができること

モデルにロジックを食わせてコレ以上肥満体型にしたくない!

 そんなあなたにInumダイエット。Inumでは列挙型を独立したクラスとして定義するので、モデルに列挙型に関するロジックを書いて太らせるなんてことはありません。(メンテナンス継続理由の大半を占めています。)

class AnimeType < Inum::Base
  define :NYARUKOSAN,   0
  define :NONNONBIYORI, 1
  define :KILLMEBABY,   2

  # セリフを表示する
  def serif
    case self
    when NYARUKOSAN
      "いつもニコニコあなたの隣に這いよる混沌、ニャルラトホテプ、です!"
    when NONNONBIYORI
      "にゃんぱすー"
    when KILLMEBABY
      "キルミーベイベーは復活する、絶対にだ!!"
    end 
  end
end

p AnimeType::KILLMEBABY.serif  # => "キルミーベイベーは復活する、絶対にだ!!"

各位@ActiveRecordとなかよく

 バージョン3.0.0には ActiveRecord support. by alfa-jpn · Pull Request #4 · alfa-jpn/inum · GitHub super friendly ActiveRecord. という内容のプルリクがマージされています。超仲いいです。

class Anime < ActiveRecord::Base
  bind_inum :column=> :anime_type, :class => AnimeType
end

この1行書くだけで、Animeモデルのanime_typeカラム(integer)に列挙型がバインドされ便利なメソッドが定義されます。

anime = Anime.new

# getter/setterで列挙型にラップされます。
anime.anime_type = AnimeType::KILLMEBABY
p anime.anime_type.value # => 2

# 判定用メソッドが追加されます。プレフィックスにカラム名が入りますが、bind_inumのオプションで消せます。
p anime.anime_type_nonnonbiyori? # => false

仲がいいものの依存はしておらず、ActiveRecordは必須ではありません。

I18nで表示名を付けたい

 I18nyamlでinum下に記述することでtranslateできます。

ja:
  inum:
    anime_type:
      nyarukosan:   '這いよれ!ニャル子さん'
      nonnonbiyori: 'のんのんびより'
      killmebaby:   'キルミーベイベー'

 translate メソッドもしくは t メソッドで翻訳できます。

p AnimeType::NONNONBIYORI.t # => "のんのんびより"

パワフルにパーズできる

 わりと何でもパーズしてくれるメソッドが内部にいるので、代入や比較ができます。

# パーサー
p AnimeType.parse(0).t              # => "這いよれ!ニャル子さん"
p AnimeType.parse('NONNONBIYORI').t # => "のんのんびより"
p AnimeType.parse(:KILLMEBABY).t    # => "キルミーベイベー"
p AnimeType.parse(999)              # => nil

# ActiveRecordのsetterでもパーサーが働くのでこんなことや
anime = Anime.new
anime.anime_type = "KILLMEBABY"  

# わりと色々な箇所に仕組んでいるのでこんな風にも使えます。
p (AnimeType::NONNONBIYORI + 1 ).t # => "キルミーベイベー"

EnamerableとComparebleをinclude

 わりと無双な感じ

p AnimeType::KILLMEBABY == 2 # => true
p AnimeType::NONNONBIYORI < AnimeType::KILLMEBABY # => true

AnimeType.each do |enum|
  p enum.t
end

参考サイト

Rails4と3で論理削除を行うためのGem Kakurenbo の紹介と今更論理削除Gemを実装した理由。

様々なわけがあってkakurenboは非推奨です。新規のプロジェクトではkakurenbo-putiの使用をおすすめします。

Kakurenboとは

 Rails4及びRails3で論理削除を行うためのGemです。paranoia及びacts_as_paranoidと互換性があるので、gemを置き換えるだけでそのまま動きます。(動かなかったらissue投げてくだしあ><)

なんで今更論理削除のGemを実装したのか

 今更開発しなくても論理削除のGemは山ほど出てきますが、なぜ開発しなくてはならなくなってしまったのか経緯に触れておきます。

acts_as_paranoid

 論理削除の本家と言ってもいいレベルの知名度です、だがしかしいつまで立ってもRails4に対応しない。

rails4_acts_as_paranoid

 既存のバグほったらかしで使えない。(メモリ関連のバグがありSQLクエリが永遠に結合されていきます。やばみ。プルリク送ったのに放置されてます。)

paranoia

 今Rails4で論理削除と言ったらコレ!ってくらい定番です。私もつい最近まではコレを利用していました。コールバック関連のバグを修正するプルリク送ったりして結構便利に使っていました。
 しかし、とある作業中にアソシエーションに深刻な問題があることに気付き、プルリクを送りましたが、仕様なのでマージする気はないとプルリク蹴られてしまいました。

 具体的にはリレーショナルで1対多の関係を設定している場合、親を論理削除して復元を行った場合、親が論理削除される以前に論理削除されている子まで復元されてしまうのです。例えば掲示板で、掲示板を本体を論理削除した後、何かの手違いだとわかり復元する時に、問題があって削除してた書き込みも全て復元されてしまうのです。

 「そもそも間違えて削除しなければいい話だろ」なんて運用でカバーするスタイルはRailsRubyらしくありません。だからこれは深刻な問題です。大切なことなのでもう一度言います。これは深刻な問題です。

結論

 paranoiaをフォークして作ろうと思ったものの、paranoiaのテストフォルダにはTest::Unitで書かれた、生SQLクエリでテーブル生成したり、実行順番依存で書かれた闇のテストコードが・・・ → Rspecで綺麗にテスト書きながら最初から作ろう。

 かくして、新しくKakurenbo*1を開発することになるのであった。。。。

インストール方法

 いつものようにGemfileに追加するとか、インストールするとか。

gem "kakurenbo"

使い方

 論理削除で管理したいモデルに deleted_at カラムを datetime 型で追加するだけで使えます。

rails generate migration add_deleted_at_to_models deleted_at:datetime

クラスの初期化時にカラムを読み取って、deleted_atがあれば論理削除と判別するので論理削除のモデルでいちいちacts_as_paranoidメソッドを呼び出す必要はありません。
(deleted_at以外のカラムを利用する際には、acts_as_paranoidをcolumnオプション付きで呼び出して設定してください。)

基本的な使い方

 paranoiaやacts_as_paranoidと互換性があるので説明はいらないかもしれませんが基本的な使い方です。

この記事は古くなっています、最新バージョン 0.2.x の使い方はこちらを参照してください。

論理削除Gem、 Kakurenbo をアップデートしました。 - 波打際のブログさん

論理削除

 普通にdestroyメソッドを呼び出すと論理削除されます。

model.destroy
復元

 論理削除したモデルはrestoreで復活できます。

model.restore!

# 次の方法も使えます。id指定、もしくはidの配列で一括で復元できます。
Model.restore(id)

before_restore, after_restoreコールバック使えます。

物理削除

 destroy!で物理削除できます。

model.destroy!
スコープ
# 削除されたモデルも取得
Model.with_deleted

# 削除されたモデルのみ取得
Model.only_deleted

# 削除されていないモデルのみ取得(デフォルトスコープです。)
Model.without_deleted

関連リンク

修正履歴

  • 2014年01月29日 誤字脱字を修正しました。

*1:RailsとかRubyのGemって、流行ってるのってみんな変な名前だよね!じゃぁ俺も変な名前のGem作ろう・・・論理削除・・・かくれみの・・・かくれんぼ!! → Kakurenbo

Ubuntu13.10でiBusを捨ててfcitxを使う

はじめに

 Ubuntu13.10がリリースされて3ヶ月ほど立ちました。Ubuntuのサポート期間はLTS版を除き、9ヶ月というとても短い設定となっています。今まで使用してたUbuntu13.04のサポート期限は去年の12月いっぱいなので、継続して使用するためにUbuntuのバージョンを13.10にアップグレードしたところ、インプットメソッドiBusが1.5にアップグレードされ、非常に使いにくいものとなっていました。
 はじめのうちはiBusと仲良くなる努力はしましたが不可能であることを悟り、今話題のfcitxを利用することにしました。これは導入に関するまとめです。

fcitxの良い所

  • iBus1.4と同様の感覚で日本語入力ができる。
  • iBusよりもカスタマイズできる箇所が多い。(候補ウィンドウの配色やフォント、テーマなど)
  • Mozcのユーティリティへのアクセスがしやすい。(設定ツールや辞書ツールをワンクリックで呼び出せる)

fcitxの悪い所

  • 半角/全角キーを長めに押さないとIMが切り替わらない事がある。(もしかしたら設定の問題?)

導入

パッケージのインストール

 Ubuntuソフトウェアセンターか、apt-getでfcitx-mozcをインストールすると、依存関係が設定されているので必要なものがすべて芋づる式でインストールされます。
f:id:alfa-jpn:20140113140804p:plain

デフォルトでfcitxを使用する設定にする

 パッケージのインストールが完了したら システム設定の言語サポートから、「キーボード入力に使うIMシステム」の設定をfcitxに設定します。設定が完了したらコンピュータを再起動させます。
f:id:alfa-jpn:20140113141130p:plain

iBusのインジケータを非表示にする

 再起動するとfcitxのインジケータと、iBusのインジケータが表示されていると思うので、システム設定のテキスト入力から、「メニューバーに現在の入力ソースを表示」のチェックボックスを外し、iBusのインジケータ表示を無効にします。
f:id:alfa-jpn:20140113141609p:plain

fcitxの設定

 この設定が完了すると、fcitxは既に使用できる状態になってますが、デフォルトでは半角/全角に割り当てられているキーマップが、mozcの半角全角と競合してしまっているために、おかしな挙動になってしまいます。そこで、mozcに割り当てられている半角/全角キーのキーマップを全て削除します。

 メニューバーのfcitxのアイコンをクリックし、Mozcツール(表示されていない場合はテキスト入力画面で半角/全角キーを押して、IMをMozcに切り替えてください)から設定ツールをクリックします。Mozc設定ツールが起動したら、一般タブの中のキー設定項目右側にある編集ボタンをクリックするとキー設定ウィンドウが表示されます。

 キー設定ウィンドウの上部にある入力キーのタブをクリックし、入力キーの名前順にリストを並び替え、Hankaku/Zenkakuの設定4つ全てを削除します。
f:id:alfa-jpn:20140113142200p:plain

以上で設定は完了です、設定が完了したら念の為に再起動を行います。

Ruby on RailsのI18nで使用する名前空間に関してのまとめと、ベストプラクティスの検討。

はじめに

 Ruby on RailsにはI18nというgemが標準で同梱されており、特に何もせずとも文章の国際化を行うことができる仕組みになっています。ちなみに、I18nとはinternationalizationを表しており、iとnの間に18文字あるから `I18n` だそうです。
 最近関わっているRailsのプロジェクトで国際化を行うタスクが発生し、I18nの辞書ファイルを定義する際に、定義ファイル中の名前空間(ロケール情報を定義する場所)について、どのような構造で配置するべきか調べる機会があったのでまとめておきます。

  • 2013年12月14日 Rubyの徳が高い同僚が、グローバルなattributesに関しての情報を見つけてくれたので追記しました。
  • 2014年07月04日 ロケールファイルの構成を一部見直しました。

環境

  • Rails4

想定環境

rails generateで name, email, urlを引数に指定してUserをscaffoldした環境を想定してロケールファイルを記述しています。

rails generate scaffold user name:string email:string url:string
Userモデル
User name email url

※idとかcreated_atとかupdated_atは気にしないでください。

app/views/users フォルダ
app/views/users
_form.html.erb
index.html.erb
show.html.erb
edit.html.erb

※他にも生成されますが、これくらいで許してください。

Rails公式ガイドのまとめ。

 Railsの公式ガイドを Rails Internationalization (I18n) API — Ruby on Rails Guides を読み解いた結果、ロケールファイルは次のような名前空間の構造で記述すれば良いことがわかりました。

ja:
  # モデルは全て activerecord 以下に記述する。
  # これにより、User.model_name.human / User.human_attribute_name({attr_name})でアクセス可能。
  activerecord:
    models:
      user: 'ユーザ情報'
    attributes:
      user:
        name: '名前'
        mail: 'メールアドレス'
        url:  'ウェブページ'
    errors:
      models:
        user:
          blank: 'urlかmailが空白です。'
          attributes:
            name:
              blank: '名前が空白です。'  # errosの中に同じ定義がある場合、ネストが深いほうが優先順位が高い
    
  
  # ビューはビューを格納しているフォルダ名を起点にし、ビュー名毎に記述する。
  # 対応するビューの中ではツリーを省略できる。<%= t '.title' %>
  users:
    index:
      title: 'ユーザ一覧'
    show:
      title: '%{user_name}さんのユーザ情報'
    edit:
      title: '%{user_name}さんのユーザ情報を編集'
 
 
  # グローバルな感じのエラーはerrorsを起点にする。
  errors:
    nanikano_error: 'よくわからないけど例外です。'

上記の形式で記述することで、Railsのヘルパーメソッドを利用して、容易にロケール情報へアクセスすることができます。

Modelのロケール情報へのアクセス

 モデル関連のロケール情報を、上記の形式で書くことで ActiveRecord::Base に定義されている、model_name.humanとhuman_attribute_name を利用して取得することができます。

p User.model_name.human # => `ユーザ情報` (activerecord.models.userを参照)

p User.human_attribute_name(:name) # => `名前` (activerecord.attributes.user.nameを参照)

Viewからロケール情報へのアクセス

 ビュー関連のロケール情報を、上記の形式で書くことで、対応するビューの中で t ヘルパーメソッドを利用して、キー名を省略した形で取得することができます。

# index.html.erbの中で実行
<% t '.title' %> # => 'ユーザ一覧' (users.index.titleを参照)

# show.html.erbの中で実行
<% t '.title', user_name: user.name %> # => '{ユーザ名}さんのユーザ情報' (users.show.titleを参照)

HTMLをロケールの中に記述する

 通常、ロケールファイルの中にHTMLタグを記述しても、タグがエスケープされて出力されます。しかし、キーの末尾を _html にすることで、ロケールファイル内に書かれたHTMLタグをそのまま出力することが可能です。

ja:
  users:
    index:
      hello_html: 'Hello!<strong>Ruby</strong>World!'

 ただし、ロケールにHTMLタグを持たせることには抵抗があります。言語ファイルがビューのスタイルに影響を及ぼしてしまうことはロケール情報の範疇を超えていると個人的には考えています。なので、他に綺麗に記述することができない、止む終えない場合の最終手段程度に留めておくことをお勧めします。

オレオレベストプラクティス

 Rails公式ガイドでは ヘルパー や コントローラ、共通のワード等の名前空間の構造には触れられていませんでした。そこで、stackoverflowや既存のロケールファイルを参考に、オレオレベストプラクティス的な物を考えてみました。

ja:
  # 共通の使いまわす可能性のあるワードは dictionary を起点にする。
  dictionary:
    messages:
      hello_user: 'こんにちは%{user_name}さん'
    words:
      user: &user 'ユーザ情報'        # &user はエイリアスで *user で参照できる。(同一ファイル内のみ有効)
      user_copy: *user                # => 'ユーザ情報' となる。
      site_name: '波打際のブログさん'
 
  # モデルは全て activerecord を起点にする。
  # これにより、User.model_name.human / User.human_attribute_name({attr_name})でアクセス可能。
  activerecord:
    models:
      user: 'ユーザー情報'
    attributes:
      user:
        name: '名前'
        mail: 'メールアドレス'
        url:  'ウェブページ'
    errors:
      nanikano_model_error: 'モデルで何かのエラー'  # 特定のモデルに属さないエラーはこの階層に書く。
      models:
        user:
          blank: 'urlかmailが空白です。' # モデル内で使いまわすエラーはこの階層に書く。
          attributes:
            name:
              blank: '名前が空白です。'  # errosの中に同じ定義がある場合、ネストが深いほうが優先順位が高い
  
  # 全モデルで共通のアトリビュートはattributesを起点にattribute名を直下に記述する。
  # validation errorやhuman_attribute_nameで取得できる。
  # (ネストの深いものが優先されるため、activerecord.attributes内の優先度が最も高い。)
  attributes:
    user_id: 'ユーザー'

  # ヘルパー関数はhelpersを起点にする。
  # ヘルパー関数内で tメソッドを使用すると、呼び出し元のビューに基づいたパスが呼び出される。
  # 例えばusersのshowから呼ばれたヘルパー関数内で t('.hoge') を実行した場合 users.show.hoge が参照される。
  # 呼び出し元によって文言を変えたい場合はビュー側に記述する。(そんなことあるかわからないけど。)
  helpers:
    user:
      welcome: 'ようこそ!'
  
  # ビューはビューを格納しているフォルダ名を起点にし、ビュー名毎に記述する。
  # 対応するビューの中ではツリーを省略できる。<%= t '.title' %>
  users:
    index:
      title: 'ユーザ一覧'
    show:
      title: '%{user_name}さんのユーザ情報'
    edit:
      title: '%{user_name}さんのユーザ情報を編集'

  # ActionMailerを継承したMailerはクラス名を起点にする。
  nanikano_mailer:
    alert_email:
      subject: 'タイトル'
      body:    '本文'

  # グローバルな感じのエラーはerrorsを起点にする。
  errors:
    nanikano_error: 'よくわからないけど例外です。'

 コントローラに関する記述が無いと思いますが、コントローラで固有の言語情報を持つ設計は、そもそも設計が誤りである可能性があります。もしコントローラにロケールしなければならない文字列がある場合には、設計を見直してみてください。
(redirect_toメソッド呼び出しの際の、noticeやalertメッセージに関しては、文量がさほど多くなく割と使い回しているため、dictionary.messages以下に突っ込んでしまっています。100行200行にでもなったら考えましょ。)

 あくまでも オレオレベストプラクティス なので、おかしいところ、もっとこうする方がいいんじゃね?とかあると思います。もしそう思ったら指摘してくれると嬉しいです。