Rails 2.2から導入されたI18nモジュールだが、さすがにバージョン0.0.1だけあって、実際に日本語でアプリを作ってみると細かいところでちょっとずつイケてなかったりするところが目につく。

以下に、僕が今までに気になったところを挙げてみよう。

1. ARのerror_messages_forでカラム名とメッセージの間に半角スペースが入る

active_record-2.2.2/lib/active_record/validations.rb 208行目より抜粋

  1
full_messages << attr_name + ' ' + message

半角スペースハードコード!! このおかげで、例えば、「名前」カラムの必須チェックのメッセージが

「名前 を入力してください。」

というように表示されてしまう。半角スペースがめちゃめちゃ気になりますよね。

そこで、こんなチケットをあげて超いいかげんなパッチを投げてみたところ、どうやらそのままコミットされてしまったようだ。

これは、上記でハードコードされた半角スペースをリソースに出しちゃって各ロケールで再定義できるようにしちゃおう、という強引な解決法で、たとえば日本語だったら

  1
  2
  3
  4
activerecord:
    errors:
      format:
        separator: ""

な感じで空文字を設定してやると、無事に 「名前を入力してください。」 が出力できるようになる。 どんな言語でも ’ ’ か ” 以外の文字がここに設定されることは無いような気がするので、かなりムダな拡張ではあるのだが、他の部分のキー名との整合性を考えるとこんな感じにならざるを得ないところか。
どなたか他にもっと自然な実装があったら教えてください。Rails 2.3はまだRCだから今なら変更可能かもしれません。

2. Array#to_sentenceで半角スペースが入る

ActiveSupportには、Array#to_sentence という、少なくとも日本語のアプリでは誰も使ってないと思われるマイナー(かつ、Matzいわく、「ブレーキが壊れてる)な機能がある。
で、Rails I18nプロジェクトでは当然これもローカライズの対象だったわけだが、Rails 2.2.2の実装では、どんなロケールでも

’, ’

でつなぐのは決め打ちで、最後の要素をつなぐフレーズだけandだったりetだったりundだったりできる、というだけ。で、「最後の要素の前に『カンマ』を入れるかどうか」をskip_last_comma ていう値で指定できるようになっていた。
これについては、もうちょっと汎用的にしようよ、ということで、’, ’ の部分も含めてロケールごとに設定できるように変更したパッチを送りつけてみた。 これで、Rails 2.3だと無事にこういうふうになった。

  1
  2
>> %w[部屋 Yシャツ 私].to_sentence
=> "部屋とYシャツと私"

あんま嬉しくない?

3. 容量の単位のやつで半角スペースが入る

これもマイナーな機能なのでどうせ誰も使ってないだろうし、なんか説明がめんどくさくなってきたから簡単に書くと、以下のような機能があるのだが、

  1
  2
>> helper.number_to_human_size 1024 * 1024 * 100
=> "100 MB"

これもRails 2.2.2では数値と単位の間の半角スペースがハードコードされていたのが、こちらの掲示板で「どうよ?」って訊かれたので、「日本人的にはスペース空かないほうが嬉しい」って要望を伝えたら、そのとおりに実装してくれた。
なので、Rails 2.3からは

  1
  2
>> helper.number_to_human_size 10000
=> "9.8キロバイト"

とかそんなふうに出力できるようになってます。GJ, Yaroslav!

4. Ruby 1.9に対応してない

Rails 2.2のI18nモジュールはRubyの最新安定版である1.9系では動作しない。
が、このへんのコミットで、Rails 2.3.1ぐらいからやっと Ruby 1.9に対応できたはず。こんにちは、ゆきひろさん!

5. datetime_selectが致命的にダサい

I18n.locale = :ja 状態で Svenのリポジトリ内の日本語リソースファイルを使って date_selectやdatetime_selectを表示してみると、以下のような感じで、致命的にダサくて悲しいコントロールが描画される。 datetime_select
これを解消するにはまぁとりあえず

  1
ja.date.abbr_month_names を [~, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

とかにしてやると「月」は出なくなるのだが、これでも数字っぽいドロップダウンが3つとか並んでるだけで、なんのコントロールなんだかよくわからない。やっぱり日本人向けにはドロップダウンの間に「年」とか「月」とかっていう字が入ってるコントロールが欲しいわけだが、これはさすがに日本ローカル過ぎてActionPackに入れてもらうには無理がある。
つまり、今のRailsの標準部品ではどうやっても僕らが望む形のdate_selectは作れないので、日付のヘルパーにはelm200さんのdatetime_helper_jaを使うがよいです。datetime_helper_jaはRails 2.3でもばっちり動作します。

6. YAMLファイルを手で書くのがつらすぎる

i18n_generatorsを使えばいいと思うよ。i18n_generatorsはRails 2.3にも完全対応しています。

./script/generate i18n ja

結論

というわけで結論。
Rails 2.2系で日本語なアプリを作ってる人は、上記のようにI18nがいろいろアレでイマイチなので、とっとと2.3に移行しちゃうがよいと思います。

先週行われたOSC Tokyo 2009/Springの日本Rubyの会のコマで、3週間ほど前にリリースされた新しいRuby、Ruby 1.9.1について、Ruby 1.9コンサルタント(自称)としてお話をしてきた。

浅草ではRuby 1.9.1を使ってます

資料はこちら。

今回は新機能などをいちいち細かく紹介するのはやめて、もっとざっくりと全体像をなるべくユーザーの視点から語ってみることにした。 つまり、今あなたがRuby 1.9.1をダウンロードしてインストールして毎日のスクリプティングに使い始めると何が嬉しいのか?何が楽しいのか?どんな良いことがあるのか?というのがメインテーマ。
事実、僕自身は毎日Ruby 1.9を使っていて非常に楽しい思いをさせてもらっているし、この機会にその楽しさをみんなに伝えたい、と思ったのだが、それって、何か理由を一つ挙げるとすると何故なんだろう?と考えてみると意外に説明が難しい。
で、僕なりに考え直してみた結果が、EnumeratorがどうしたとかVMがどうしたとかそういう難しいところじゃなくて(そういうのはささだ先生にお任せして)、「新しいことはいいことだ」で語った内容だと思う。
(以下、デンパ注意)

“Ruby”は人類の英知の結晶だ。”Ruby”は単にいちプログラミング言語の名前であるのみならず、僕らプログラマーが世の中をもっと良いものにしていく社会的ムーヴメントだ。
Rubyは、プログラミング言語としては必ずしもコンピューターサイエンス的な意味で最高に高度なものではないかも知れないが、どこかの大企業が自社製品を売り込むための思惑が込められているわけでもなければ、誰かの気まぐれな思いつき「だけ」で成り立っているわけでもなく、その代わりに、ユーザーがもっと楽しく、もっと豊かにプログラミングができるようにしたい!というみんなの想いがこれ以上ないぐらい高い密度で凝縮されてできている。
(特に太平洋の向こう側に住んでいる)一部の心ないRailerたちが過去のRubyの一部を切り取った”Ruby 1.8.6”という言語にしがみつこうとしているとかいうような話もあるようだが、おそらく彼らの曇った目にはRubyが”ちょっと仕事を便利にしてくれるプログラミング言語”というぐらいにしか映っていないのだろう。
でも僕らはそうじゃない。今たまたま日本に生まれてプログラマーという人生を選んでRubyという言語に出会ったおかげで、僕らはこの魔法のかかったムーヴメントの真っただ中に身を置くことができる。新しいRubyが毎日作り出されていく奇跡を肌で感じることができる。
さらに、何かしらの形で自分なりのアウトプットを先っちょのほうに投げ込んでみることによって、自分の中の何かがそんな魔法を作り出すエネルギーに変わって世界中を幸せにできたりするかも知れない。この”Ruby”という、世界中を巻き込みつつ未だかつてない規模で進行している「祭り」にリアルタイムで遭遇したというこのチャンスにあなたも参加しないなんてもったいない!
これが、あなたが今すぐにRuby 1.9.1を使い始めるべき最大の理由だ。

というようなことを語ろうと思ったけど、朝イチで眠かったし、ちょっと宗教じみてるかなー、とか思って自重してしまったっていうのもあったし、あと満員御礼(笑)で若干テンションが下がった、ってのもあって、ちょっと電波力を抑えすぎてしまったかな、と今では反省している。次の機会があったらもっと本気で電波出すことにしよう。

とにかく、あれを聞いて一人でも「Ruby 1.9.1をインストールしてみたよ!」とか「Asakusa.rb入りたい!」とか言ってくださる人が居たらいいなぁ。

仙台Ruby会議01

January 30th, 2009

仙台RubyKaigi01に参加した。仙台は、牛タンだけじゃなくて海の幸もすこぶるウマい素敵な街だった。
で、「Rails 2.3(もしくはMerb)で5分で作るトランプゲーム(仮)」というタイトルでLTした。とりあえず資料は以下。

ほんとうはRails 2.3というフレームワークの上で動くネットワーク対戦型トランプゲームフレームワーク、というという題材で、いかに業務ロジックを再利用可能なメタなフレームワーク部分とRubyっぽいDSL(Rails的な意味で)に分けて設計/実装できるか、というのが当初の構想で、いちおうその後の懇親会で見せたとおり、そのためのデモアプリも完成してはいたんだけど、素振りしてみたらこんなのを5分でデモを交えつつ喋るのはぜんぜんムリ、ということがわかったので、急遽方針変更してデモはばっさり落としてしまった。

結局、単に「メタとベタ」というフレーズ(1週間前のRails勉強会の懇親会で福井さんから聞いた)が使いたかっただけなんちゃうか的な感じになっちゃって、ちょっと内容が薄かったかな、と反省。

ちなみに、この問題のスライドだけど、
TF
これは自分で書いたわけじゃなくて、「会社を紹介するスライドをなんかください」って会社の人に言ったらこんなのが出てきた、という。 そんなわけで、もう一度言っとくけど、

どうやらT&Fカンパニーという会社でフェロー(笑)をやってることになってます。
よろしくお願いします。

あと、「トランプゲーム」そのものをえらく楽しみにしててくださった方もいたようで、

『トランプの「ト」の字もでてこなかったじゃないかー』

と、あとで大場nay3にお叱りを受けてしまった。どうもすいません。

いちおう、そっちのほうはもうちょっとコードが整理できたらGitHubに上げとくので、興味がある方は是非ご覧ください。

さて、Rails 2.2も 無事にリリース されたわけだが、今回の目玉はなんといっても “I18n” 対応だろう。

つまり、今回の大規模なI18n対応で、やっとこれが

# Rails 2.1.2 より抜粋
          options[:object_name] ||= params.first
          options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message)
          options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message)      
          error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join

こうなった、と。

# Rails 2.2.2 より抜粋
          I18n.with_options :locale => options[:locale], :scope => [:activerecord, :errors, :template] do |locale|
    (中略)
              locale.t :header, :count => count, :model => object_name
            end
    (後略)

参考 : 優しいRailsの育て方

そんなわけで、Rails 2.2からは従来のようにRailsのソースをごっそり置き換えたりするモンキーパッチのようなプラグインに頼らなくても、Rails自体の仕組みだけで日本語化(多言語化)が行えるようになった。

◆i18n_generators を使ってみる

さて、それではさっそく日本語化されたアプリを作ってみよう。
このあたり、まだ日本語の情報が全然少ないからよっぽど難しいのかと思われるかもしれないが、実はめちゃめちゃ簡単で、いくつかのコマンドを叩くだけであっさり実現できる。

まずは、何も考えずに、 Rails I18n公式Wikiに載っているi18n_generators という名前の魔法のgemをインストールしておく(名前の最後に “s” がつくのでご注意)。

# http://gems.github.comがgemのSOURCESに入ってない場合は以下を実行してから
% sudo gem so -a http://gems.github.com
# インストール
% sudo gem i amatsuda-i18n_generators
Successfully installed amatsuda-i18n_generators-0.2.0
1 gem installed

それから、Rails 2.2がインストールされている環境で、

% rails -v
Rails 2.2.2

アプリケーションを作成してcd。

% rails i18n_test
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
(略)
% cd i18n_test/

config/environment.rbを編集してsqlite3を有効に(※ 2.2からデフォルトOFFに変わった?ハマる人続出だと思うんだけど・・・)

じゃあまずはなんかscaffoldでもしてみよう。
以下のようにi18n_scaffoldジェネレーターを使うと、scaffoldがいい感じに他言語化されていたりする。 (081129 updated)

% ./script/generate i18n_scaffold user name:string

% rake db:migrate

日本語のリソースファイルを生成してくれる魔法のコマンドを叩いて(ロケール指定は “ja” で)、

% ./script/generate i18n ja
      exists  config/locales
      create  config/locales/ja.yml
      create  config/locales/models_ja.yml

あ、なんかUserモデルにバリデーションでも仕掛けとくか。
app/models/user.rb

class User < ActiveRecord::Base
  validates_presence_of :name
end

以上!!

サーバーを起動して確認してみると、

% ./script/server

おおおー、ばっちり日本語化されてるー!!!

ということで、詳しくはあとで書く。

とりあえず、このgem/pluginに関する質問、感想、フィードバックその他は大歓迎です。
あと、 『楽天テクノロジーカンファレンス2008』 というやつで、こいつについて何か話をすることになってるような気がします。

ブログ再開。
さて、先日Rails 2.2系のmasterに以下の変更がコミットされた。

http://github.com/rails/rails/commit/44a3009ff068bf080de6764a8c884fbf0ceb920e

そもそも今までのRailsの map.resourcesは、なんか routes.rbに1行書くだけで勝手にCRUD+αのroutesがうじゃうじゃ生成されて、邪魔くさいことこの上なかった。実際のアプリだと使われるURLの方が少ないぐらいなのに、rake routesの出力を無駄に汚されて不愉快きわまりない.

というのを、そういえばちょうど前回の Rails勉強会@東京#34 でも懸命に主張してみたりしたのだが、 REST信者 とかいう人たちには「気にするな」とか言われてまったく取り合ってもらえなくて悲しい思いをしたのだった。

しかしみんなにどんなにREST教の教えを説かれても僕にはRailsのこの挙動だけは納得いってなかったんだが、こうなってみるとどうやら僕の感覚の方が正しかったってことですかね。

ということで、以下実例。

  • リソースを作ってみたりする
% ./script/generate resource user

% ./script/generate resource group
  • rake routesを表示してみたところ。ごにゃごにゃしてて見づらすぎる
% rake routes
              groups GET    /groups                          {:controller=>"groups", :action=>"index"}
    formatted_groups GET    /groups.:format                  {:controller=>"groups", :action=>"index"}
                     POST   /groups                          {:controller=>"groups", :action=>"create"}
                     POST   /groups.:format                  {:controller=>"groups", :action=>"create"}
           new_group GET    /groups/new                      {:controller=>"groups", :action=>"new"}
 formatted_new_group GET    /groups/new.:format              {:controller=>"groups", :action=>"new"}
          edit_group GET    /groups/:id/edit                 {:controller=>"groups", :action=>"edit"}
formatted_edit_group GET    /groups/:id/edit.:format         {:controller=>"groups", :action=>"edit"}
               group GET    /groups/:id                      {:controller=>"groups", :action=>"show"}
     formatted_group GET    /groups/:id.:format              {:controller=>"groups", :action=>"show"}
                     PUT    /groups/:id                      {:controller=>"groups", :action=>"update"}
                     PUT    /groups/:id.:format              {:controller=>"groups", :action=>"update"}
                     DELETE /groups/:id                      {:controller=>"groups", :action=>"destroy"}
                     DELETE /groups/:id.:format              {:controller=>"groups", :action=>"destroy"}
               users GET    /users                           {:controller=>"users", :action=>"index"}
     formatted_users GET    /users.:format                   {:controller=>"users", :action=>"index"}
                     POST   /users                           {:controller=>"users", :action=>"create"}
                     POST   /users.:format                   {:controller=>"users", :action=>"create"}
            new_user GET    /users/new                       {:controller=>"users", :action=>"new"}
  formatted_new_user GET    /users/new.:format               {:controller=>"users", :action=>"new"}
           edit_user GET    /users/:id/edit                  {:controller=>"users", :action=>"edit"}
 formatted_edit_user GET    /users/:id/edit.:format          {:controller=>"users", :action=>"edit"}
                user GET    /users/:id                       {:controller=>"users", :action=>"show"}
      formatted_user GET    /users/:id.:format               {:controller=>"users", :action=>"show"}
                     PUT    /users/:id                       {:controller=>"users", :action=>"update"}
                     PUT    /users/:id.:format               {:controller=>"users", :action=>"update"}
                     DELETE /users/:id                       {:controller=>"users", :action=>"destroy"}
                     DELETE /users/:id.:format               {:controller=>"users", :action=>"destroy"}
  • そこで、routes.rbに以下の条件を追記
    % vi config/routes.rb
# 引数は配列でも
  map.resources :groups, :only => [:show, :index]
  # シンボル一つでも配列でもOK
  map.resources :users, :except => :destroy
  • routesがすっきりシンプルになった!
% rake routes
             groups GET  /groups                          {:controller=>"groups", :action=>"index"}
   formatted_groups GET  /groups.:format                  {:controller=>"groups", :action=>"index"}
              group GET  /groups/:id                      {:controller=>"groups", :action=>"show"}
    formatted_group GET  /groups/:id.:format              {:controller=>"groups", :action=>"show"}
              users GET  /users                           {:controller=>"users", :action=>"index"}
    formatted_users GET  /users.:format                   {:controller=>"users", :action=>"index"}
                    POST /users                           {:controller=>"users", :action=>"create"}
                    POST /users.:format                   {:controller=>"users", :action=>"create"}
           new_user GET  /users/new                       {:controller=>"users", :action=>"new"}
 formatted_new_user GET  /users/new.:format               {:controller=>"users", :action=>"new"}
          edit_user GET  /users/:id/edit                  {:controller=>"users", :action=>"edit"}
formatted_edit_user GET  /users/:id/edit.:format          {:controller=>"users", :action=>"edit"}
               user GET  /users/:id                       {:controller=>"users", :action=>"show"}
     formatted_user GET  /users/:id.:format               {:controller=>"users", :action=>"show"}
                    PUT  /users/:id                       {:controller=>"users", :action=>"update"}
                    PUT  /users/:id.:format               {:controller=>"users", :action=>"update"}

この機能は、ついさっきリリースされたRails 2.2から有効になっています。
明らかに良くなりましたね、こりゃ。これなら実案件でも使う気になってきた。

娘 1.0 released!

September 8th, 2008

2008年9月8日(聖母マリアが生まれた日だそうな)に、第1子が誕生した!!!
出生時の体重は2679g。思っていたよりもかなり小さい。
どれぐらい小さいかというと、これぐらい。


iPod Touch <=> 娘のあたま

出産には立ち会ってよかった。ほんとうに良かった。産まれた瞬間は今まで生きてきた中で一番感動した。
終電で家に帰って近所のスーパーで缶ビールを買って一人で祝杯をあげながら嬉しすぎて号泣しちゃってしばらく涙が止まんなくなるぐらい感動した。

それから、ついったーで、IRCで、メッセで、メールで、MLで、ブログで、携帯で、携帯メールで、ショートメールで、電話で、手紙で、贈り物で、ごちそうで、飲み会で、ライヴのチケットで、現ナマで、それからもちろんリアルで、もう数えきれないぐらいのたくさんの方々にありとあらゆる手段であたたかい祝福のお言葉をいただいた。本当にありがたい。
現代は昔よりもコミュニケーションが希薄になってきたとかよく言われるけど、実は全然そんなことはなくて、コミュニケーションの手段が多様化されているぶんだけ、より多くの人とこんなにも色々な形でつながりが築かれているんだ、ということに、こういう節目のイベントがあるとふと気付かされる。思えばすごい時代になったものだ。

そして我が娘はそんなすごい時代に産まれてきてしまったわけだ。僕らが子供だった頃から比べてもこの「1世代」でこんなにも世の中は進歩してしまった。
でもウチの娘はもっとすごい。彼女は、そんな世の中の30年の進歩に匹敵するぶん、おそらく生物としても進化している。
誰かさんが自らの姿に似せてヒトを創ったとか最初の人間は泥から生まれたとかみたいなおとぎ話を事実と混同していない限りは、現代人はある程度無条件にダーウィン科学を信仰するしかないわけで、つまりそうすると自分の娘は確実に僕ら自身の完全上位互換プロダクトだ、という事実を受け入れなければならない。
Windows NTに対する2000のように、Pentium IIに対するPentium IIIのように、僕ら旧世代モデルはどこをどう切っても自分の後継モデルには勝てない。そうでなければ、僕らは僕らの学んできた科学を裏切ることになってしまう。だから僕は自分の娘を、産まれてきた瞬間からめちゃめちゃ尊敬している。

いま、僕の腕の中に、万物の霊長とも呼ばれる大いなるプロダクトの最新バージョンの小さな小さな1インスタンスが横たわっている。僕もこのプロジェクトに参加して31年目にして初めて小さなコミットを記すことができたことになる。そして、僕らがこの子に毎日ミルクをあげ続けるというこの行為は、人類の最もEdgeなリビジョンを、文字通り日々buildし続ける行為に他ならないのだ・・・

第31回Rails勉強会@東京のセッション「Rails 2.2の I18nについてくわしく」の資料

●I18nモジュールとは

●ソース

github からこんな感じで検索してみる。
http://github.com/rails/rails/search?q=i18n&choice=grep

●重要そうなコミット

●基本機能

I18nというライブラリのクラスメソッドがいくつか提供されている。

>> [I18n.local_methods - Object.methods]
=> [["append_features", "backend", "backend=", "default_exception_handler", "default_locale", "default_locale=", "exception_handler=", "l", "locale", "locale=", "localize", "normalize_translation_keys", "populate", "store_translations", "t", "translate"]]

デフォルトの localeは en-USになっている。

>> I18n.locale
=> "en-US"

ja-JPに変更してみる。

>> I18n.default_locale = 'ja-JP'

詳しくはソースで。

●ソースの読みどころ

  • I18n gemの本体
    activesupport/lib/active_support/vendor/i18n-0.0.1/ 以下
  • 基本的な操作とか
    activesupport/lib/active_support/vendor/i18n-0.0.1/lib/i18n.rb
  • vendor/rails/activerecord/lib/active_record/validations.rb

●機能その1. translate

翻訳メソッド。

>> I18n.t 'hoge'
=> "translation missing: ja-JP, hoge"
>> I18n.t 'hoge', :default => 'ほげ'
=> "ほげ"
>> I18n.store_translations :'ja-JP', :hoge => 'ホゲ'
=> {:hoge=>"ホゲ"}
>> I18n.t 'hoge'
=> "ホゲ"
>> I18n.t 'hoge', :default => 'ほげ'
=> "ホゲ"

 

●機能その2. localize

日付や時刻、お金のフォーマット

% lv vendor/rails/activesupport/lib/active_support/locale/en-US.rb

 

I18n.backend.store_translations :'en-US', {
  :support => {
    :array => {
      :sentence_connector => 'and'
    }
  },
  :date => {
    :formats => {
      :default => "%Y-%m-%d",
      :short => "%b %d",
      :long => "%B %d, %Y",
    },
    :day_names => Date::DAYNAMES,
    :abbr_day_names => Date::ABBR_DAYNAMES,
    :month_names => Date::MONTHNAMES,
    :abbr_month_names => Date::ABBR_MONTHNAMES,
    :order => [:year, :month, :day]
  },
  :time => {
    :formats => {
      :default => "%a, %d %b %Y %H:%M:%S %z",
      :short => "%d %b %H:%M",
      :long => "%B %d, %Y %H:%M",
    },
    :am => 'am',
    :pm => 'pm'
  }
}

 

●サンプルアプリを作ってみる

  • ja-JP.rbという名前のファイルを $RAILS_ROOT/lib/locale/ に配置する。
    自分で作るのはめんどくさいので gettext-railsからパチっちゃえ。
    たぶんこんな感じで。
% cp vendor/rails/activerecord/lib/active_record/locale/en-US.rb lib/locale/

% cp /opt/local/lib/ruby/gems/1.8/gems/gettext-1.91.0/po/ja/rails.po lib/locale/

% fgrep '%{fn}' rails.po | ruby -e 'puts STDIN.read.gsub("msgid ", "s/").gsub("\nmsgstr ", "/").gsub(/%\{fn\} ?/, "").gsub(/$/, "/")' > trans.sed

% sed -f trans.sed en-US.rb | sed s/en-US/ja-JP/ > ja-JP.rb
  • application_controller に以下の filterを設定
before_filter :set_locale
 
  def set_locale
    locale = params[:locale] || 'ja-JP'
    I18n.locale = locale
    I18n.populate do
      require "lib/locale/#{locale}.rb"
    end
  end
  • つついて遊んでみる

5月に行われた前回(第30回)Rails勉強会@東京では、ふりかえりの際に 『次回から「前回の勉強会のまとめ」を開催告知Wikiページに載っけるようにしてみよう』という Tryが挙げられていた ので、第30回の時にやった「what’s new in Rails 2.1 セッション」のまとめを。

と言っても、2ヶ月も前のことを今更思い出せるわけないので、以前 GoogleDocsに揚げたやつ をそのまま貼るだけなんだけどね。
なので、以下の内容はあくまでも「2ヶ月前の最新情報」であり、現在の最新情報とは違うかもしれないのでご注意ください(以下こぴぺ)。
 


what’s new in Rails 2.1 セッション

セッション中に即席で書いてるんでだいぶいい加減な内容です。すみません・・・

■ネタ元

Rails2.1について情報が載ってそうなところあれこれ。

■Rails Edgeを cloneしてみよう

今のところ、Rails 2.1系を触ってみるには、以下のような方法がある。

  • RC1を使う
% rake rails:freeze:edge

zipで固められた最新ソースの玉を落として解凍してくれるように変更されたらしい
(たぶん Gitがまだ充分に普及していないから(?))。
が、zipファイルを上げる運用はまだ運用が開始されていない模様。

  • piston
    pistonが Gitに対応してくれたらこれが一番ラクかも。
  • gitコマンドで
% rails edge_080518
% cd edge_080518/rails
% git clone git://github.com/rails/rails.git

 

■Rails2.1の新機能たち

主立ったものを列挙してみます。これまた当日に思い出しベースで書いているので網羅はできてない気がしますが。

  • ARのレコードに dirty object機能
カラム名 + _changed? メソッドが例によって各 content_columnsに追加される ARマジック。

save時に発行される update文を、変更されたカラムのみを対象にすることができて、うまく使えばパフォーマンスの向上が見込まれる。
さらに、変更前のオリジナルの値も保持してくれている。
ADO .Netの DataTableにあったようなあの機能という理解で良いかと。
http://ryandaigle.com/articles/2008/3/31/what-s-new-in-edge-rails-dirty-objects
http://dev.rubyonrails.org/changeset/9157
http://blog.codefront.net/2008/04/05/living-on-the-edge-of-rails-14-the-extreme-edition-extremely-late/
http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-partial-updates

  • schema_infoテーブル → schema_migrations という新テーブル
  • ActiveModel
    2.1には正式に入るのかも?
  • TimeZone機能
rake time:zones:*

→ 非US系開発者から若干反感を買ってる模様(以下のコメント欄など参照)。
http://rails.lighthouseapp.com/projects/8994/tickets/195-convenience-methods-for-time-zones-outside-the-u-s

  • ドキュメントがすごい勢いで充実中
  • Ruby1.9 / 1.8.7 対応
    実はこれが一番嬉しい変更、という人も多いかも。
    1.9系のメソッドたちは ActiveSupportと激しくかぶるので、ActiveSupportにかなり修正が入っている。
  • インフラの変化
    Subversion → Git
    Trac → Github
    Trac → Lighthouse
  • script/plugin install が Gitのリポジトリに対応

(※懇親会で喋ってて思い出したので追記)

■ named_scopeを詳しく

以下、セッション中にデモで書いたコード +α をいちおう載っけておきます。
もう終わった勉強会のぽじぺを見る人って居るんかな?とは思いつつ・・・

  • Railsアプリを生成して Edge Railsを取得
    手順は上記のとおり。
  • こんな感じのいいかげんな modelを一丁 generate
% ./script/generate model person name:string age:integer gender:integer
% rake db:migrate
  • サンプルデータを何件か作って、
>> Person.create :name => 'person 1', :age => 20, :gender => 1

    >> Person.create do |p|
    >>   p.name = 'person 2'
    >>   p.age = 30
    >>   p.gender = 2
    >> end

    >> Person.create :name => 'person 3', :age => 3, :gender => 1
  • named_scopeをあれこれ作ってみる
class Person < ActiveRecord::Base
      # Hashで conditions。基本形。
      named_scope :male, :conditions => {:gender => 1}
      named_scope :female, :conditions => {:gender => 2}
      # Stringで conditions。
      named_scope :adult, :conditions => 'age >= 20'
      # Procで scopeを定義した形。例がへぼくてすいません。
      named_scope :name_start_with_p, lambda { {:conditions => ["name like ?", 'p%']} }
      # Proc方式だと scopeに引数も渡せる。
      named_scope :older_than, lambda {|n| {:conditions => ['age >= ?', n]} }
      # さっきのへぼい例もこれなら実用的。
      named_scope :name_start_with, lambda {|s| {:conditions => ["name like ?", "#{s}%"]} }
    end
  • 作ったnamed_scopeたちをつついて遊んでみる
>> Person.male
  Person Load (0.000603)   SELECT * FROM "people" WHERE ("people"."gender" = 1)
>> Person.adult
  Person Load (0.000596)   SELECT * FROM "people" WHERE (age >= 20)
>> Person.older_than(7)
  Person Load (0.000651)   SELECT * FROM "people" WHERE (age >= 7)

動いた。あれ?genderは予約語だったか?とか思いつつ。
こんな調子で、ちっちゃいクエリの断片をあらかじめ名前を付けて定義しておくことができる、というのが named_scope の基本機能。
これだけだと、昔 Javaが流行ってた頃に DAO って言われてたやつと何が違うの?と言われてしまいそうなところだが、

  • なんと、ネストして呼び出した時のSQLの発行は一回だけ!
>> Person.female.adult
      Person Load (0.000494)   SELECT * FROM "people" WHERE ((age >= 20) AND ("people"."gender" = 2))

とにかくこれがすごい。呼び出し側のコードの可読性がめちゃめちゃ高くてシビれる。
→ 流れるようなインターフェース(笑)

  • そういえばセッションでは説明し忘れたけど、countとか書いても勝手にネストしてくれるよ。
>> Person.older_than(10).female.name_start_with('').count
  SQL (0.000375)   SELECT count(*) AS count_all FROM "people" WHERE (((name like '松%') AND ("people"."gender" = 2)) AND (age >= 10))

join とか sortとか、その他もろもろは今回は省略。
ちなみに、実はそのあたりは今でも盛んにコミットが行われているところなので、仕様もまだ多少揺れる可能性があるかも。

  • named_scopeメソッドで作られるものたちの実体は?
>> pp Person.scopes
{:male => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :name_start_with => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :female => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :adult => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :name_start_with_p => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :scoped => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :older_than => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>}

こんな感じの、キー = 宣言した名前、値 = Proc のインスタンス、な Hash で保持されていることがわかる。

  • これを踏まえて、よくある検索画面を想定して動的にクエリを組み立ててみる
    さて、この named_scope機能が実際のアプリでどう使えるか、って考えてみると、
    よくある検索画面みたいなやつで条件に含める項目自体が動的に変化するようなケースを
    思いつくんじゃないだろうか。
    つまり、どの scopeをクエリに組み込むか、というのを動的に選んでいきながら複雑なクエリを組み立てて、
    最後に一発ドーンと実行するようなイメージ。
def self.search(params)
  exec_scopes = []
  if params[:is_male]
    exec_scopes << [:male, nil]
  end
  if params[:age_min]
    exec_scopes << [:older_than, params[:age_min]]
  end
  # (中略)
end

おまけ

セッションでは上記のように書いて、こんな感じで実行したい scopeを配列とかに溜めて
evalか injectぐらいでガツッと実行してやるしかないよねー、みたいな説明をしておいたが、
Yuumiさんから「かなりぐちゃぐちゃなコードになりそう」
というようなご意見を頂いた。うーん。そうなんですよねー。

さて、そこらへんはその場でコードは書ききっていなかったので、以下参考までに実装イメージを。

※ 実装案1

つまりこういう文字列を組み立てて、

exec_scopes_string = exec_scopes.map {|s| "#{s[0]}(#{s[1]})"}.join('.')

evalに食わせるという。
引数の型とか数とかまともにハンドリングしてないけどまぁ大体こんな流れで。

eval "Person.#{exec_scopes_string}"

※ 実装案2

injectでぐりぐり実行させる。

exec_scopes.inject(Person) {|p, s| scopes[s[0]].call p, *s[1]}

やっぱりこっちのほうがイケてそうかなぁ。

ということで、案2のほうで実装してみたサンプルがこちら。
引数を複数取る scopeにも対応したかったので、例としてこんな scopeを追加して、

# 複数の引数。
    named_scope :age_between, lambda {|from, to| {:conditions => ['age between ? and ?', from, to]} }

検索を実行するメソッドをこんなふうに。

def self.search(params)
      exec_scopes = []  # [実行するスコープ, [引数の配列]] な配列
      exec_scopes << [Person.scopes[:male], nil] if params[:is_male]
      exec_scopes << [Person.scopes[:older_than], params[:age_min]] if params[:age_min]
      exec_scopes << [Person.scopes[:age_between], [params[:age_between][0], params[:age_between][1]]] if params[:age_between]
      # あとは、こんな調子で他の paramsたちの解釈をずらずらと書く。ここでは省略。

      # これが呼び出し部
      exec_scopes.reverse.inject(Person) {|p, s| s[0].call p, *s[1]}.all
    end

実行結果はこちら。

>> Person.search({:is_male => true, :age_between => [10, 30]})
      Person Load (0.000518)   SELECT * FROM "people" WHERE (("people"."gender" = 1) AND (age between 10 and 30))

ということで、とりあえずめでたく動作。

けっこうスッキリ書けたと思うが、でもこれは確かに書いた本人にしかわからないコードかも・・・
ポイントは便利メソッド injectの()内引数の使いかたと、最後にさりげなく書かれた .all(scopeが1個も使われなかった場合のデフォルトscopeになる)の工夫。
やっぱり Rubyはこんな書き方ができちゃうから面白い。
ただ、このあたりは需要がありそうなので、きっとフレームワークがもうちょっと親切な仕組みを提供してくれることでしょう。

* セッション後、もろはしさんが言ってた「これって集合のイメージだよね」みたいな説明のしかたが一番わかりやすいかもですね。

Railsの fixturesはたいへん便利な仕組みなのだが、今までは決められた名前のディレクトリ($RAILS_ROOT/test/fixtures/)内の決められた名前(テーブル名と同じ名前)のファイルしか読み込めないという制約があったため、例えば複数パターンのテストデータを用意しおいて切り替えて DBに食わせたいような場合なんかに、ちょっと不便に感じることがあった。
が、最近の edgeでは2箇所ほど fixtures関連の修正が入っており、fixturesの使い勝手が向上している。
 

1. サブディレクトリ内の fixturesを読み込む rakeタスク

まず、fixturesをディレクトリで分けて複数用意してディレクトリ名を指定して読み込む rakeタスクが 実装された

例えばこんな fixturesファイルを用意して、

test/fixtures/users.yml

john:
  name: John

paul:
  name: Paul

ふつうに rakeタスクで loadすると、

$ rake db:fixtures:load

DBの中身はこうなる。

$ ./script/runner 'p User.all.map(&:name)'
["Paul", "John"]

 
と、ここまでは従来どおりだが、さらに Rails 2.2では、こんなディレクトリを用意して、

$ mkdir test/fixtures/ja

こんな fixturesファイルを作ったら、

test/fixtures/ja/users.yml

taro:
  name: 太郎

hanako:
  name: 花子

こんなコマンドで loadできるようになった。

$ rake db:fixtures:load FIXTURES_DIR=ja

結果はこのとおり。

$ ./script/runner 'p User.all.map(&:name)'
["花子", "太郎"]

 

2. fixturesメソッドでテーブル名と同じ名前以外の任意のファイルを読み込めるようになった

こちらの件は、折しも RubyKaigi 2008の会場で Rails勉強会@東京ブースのよろず質問コーナーで質問を頂いた内容だったりする。
Rails 2.2では このパッチのコミットのおかげで バッチリ解決できそうになったので、いまさらここで遅レスな回答を。

例えば今度はこんな fixturesファイルを読み込ませたい場合、

test/fixtures/elp.yml

keith:
  name: Keith

greg:
  name: Greg

carl:
  name: Carl

テストケースではこんなふうに書けばOKになった。

test/unit/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  set_fixture_class :elp => User
  fixtures :elp
  
  test 'ELP should be loaded through YAML file' do
    assert_equal %w[Carl Greg Keith], User.all.map(&:name).sort
  end
end

しかし、今の Railsのデフォルトでは test/test_helper.rb に

fixtures :all

とか書いてあったりするので、fixturesディレクトリ直下にこんなファイルが置いてあると、全てのテストケースでこの ymlファイルを同名のテーブルにロードしに行こうとしてガンガン落ちる。

そこで、上記の1番の例を思い出して、特定のテストケースでのみ使いたいようなテストデータはサブディレクトリに置いとけば良いじゃん、というのがトレンドになりそう。

$ mkdir test/fixtures/users
$ mv test/fixtures/elp.yml test/fixtures/users/

テストはこんなふうに書けば、

test/unit/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  set_fixture_class :'users/elp' => User
  fixtures :'users/elp'

  test 'ELP should be loaded through YAML file' do
    assert_equal %w[Carl Greg Keith], User.all.map(&:name).sort
  end
end
$ rake test:units
(中略)
1 tests, 1 assertions, 0 failures, 0 errors

カンペキ!
ポイントは、ディレクトリ名とファイル名をつなげたものを引用符で囲って書くところでしょうか。

以上、こんな感じで回答になってますでしょうか? > 川端さん

昨今の Gitブームを受けて、Rails 2.1 から script/plugin スクリプトが Gitに対応したのは記憶に新しいところ。

だが、このコマンドでは単に Gitリポジトリの最新版を取得してきてしまうため、みんな こんな 疑問を抱えながら使っていたわけだが、実は Gitでタグ付けされた安定版を導入しようと思ったら、script/pluginにはそんなオプションは用意されていなかったので、 こんなふうな 面倒な手順を踏まなければならなかった。

$ cd vendor/plugins
$ git clone git://github.com/dchelimsky/rspec.git
$ git clone git://github.com/dchelimsky/rspec-rails.git
$ cd rspec
$ git checkout 1.1.4
$ cd ../rspec-rails
$ git checkout 1.1.4
$ cd ..
$ rm -rf rspec/.git
$ rm -rf rspec-rails/.git
$ cd ../../
$ script/generate rspec

これを解消すべく こんな チケットが挙がっていた、というのは、これまた RubyKaigiの時に書いた これ に書いたとおりだが、その後この dchelimsky案よりもっとエレガントな実装が行われ、 先日コミットされた

これによって、結論としては、今の edge Railsでは以下のコマンドで任意のタグがついたバージョンのプラグインをインストールすることができるようになった。

$ ./script/plugin install git://github.com/dchelimsky/rspec.git -r 'tag 1.1.4'

しかも、試してみればわかるが、Subversionのころと比べると驚異的に速い!!
これでもはや Railsプラグインが Gitに移行しない理由はひとつもなくなったと言って良いのではないだろうか。

ちょっと前の話だが、 こちらのコミット にて、hashで条件指定された named_scopeを経由してモデルのインスタンス生成を行った場合、その条件の値があらかじめセットされたインスタンスが作られるようになった(この件については、 こちら の「注目の未解決チケットたち」に書いたとおり以前から 要望が挙がっていた もの)。
どういうことかというと、例えばこんな感じ。

こんなようなmodelがあるとして、

class User < ActiveRecord::Base
  belongs_to :prefecture
  # 引数を取らない named_scope
  Prefecture.all.each do |prefecture|
    named_scope "lives_in_#{prefecture.name.downcase}".to_sym, :conditions => {:prefecture_id => prefecture.id}
  end 
  # 引数を取る named_scope
  named_scope :name_is, Proc.new {|name| {:conditions => {:name => name}}}
  named_scope :aged, Proc.new {|age| {:conditions => {:age => age}}}
end

まずはふつうに検索。

User.lives_in_tokyo.name_is('Matsuda').aged(16)

発行されるSQL

User Load (0.000373)
   SELECT * FROM "users" WHERE ((("users"."age" = 16) AND ("users"."name" = 'Matsuda')) AND ("users"."prefecture_id" = 13))

ここまでは今までどおり。
これを newとか createとかにつなげるとこうなるッ!

user = User.lives_in_tokyo.name_is('Matsuda').aged(16).create

実行結果

User Create (0.001407)
   INSERT INTO "users" ("name", "created_at", "updated_at", "age", "prefecture_id") VALUES('Matsuda', '2008-07-23 18:09:27', '2008-07-23 18:09:27', 16, 13)
  #=> #<User id: 1, name: "Matsuda", age: 16, prefecture_id: 13, created_at: "2008-07-23 18:09:27", updated_at: "2008-07-23 18:09:27">

かっこいー!

RedCloth 4.0 Released

July 23rd, 2008

待望の RedCloth 4.0がリリースされた

さっそくこのブログのサーバにも入れてみたので、このポストが RedCloth 4.0で書く最初の文章になるかな。

そもそも、このブログは Mephisto 0.8 (Drax) の Gitの先っちょ版 で書いているのだが、この Mephistoはデフォルトでは RedClothの3.0.4をバンドルして使っていて、改行が <br> になってくれないという致命的に使いにくいブログシステムだった。
ので、 _why先生のリポジトリ から 3.301という開発版を取ってきてちょっと拡張して使っていたのだが、このたび 4.0が正式にリリースされたので全面的に乗り換えてみた(そこらへんの手順とかTIPSとかは自分のためにも気が向いたらあとで書く)。

この RedCloth 4.0が出たおかげで、Rubyで作られた Wikiとかブログとか CMSとかその手のツールもだいぶ使いやすくなって盛り上がってくるんじゃないかな。
やっぱりこういうやつってそこらへんの足回りがかなり重要なので。

そして、RedClothといえば、3.0.4をめっちゃオレオレ拡張して組み込んでる Redmine とかがどう対応するのかも気になるところですよね。

第1回Asakusa.rb のときに発見されて修正された Railsのバグをパッチにして投げてみたところ、
http://rails.lighthouseapp.com/projects/8994/tickets/623

そっこーで無事本体に取り込まれた。
http://github.com/rails/rails/commit/bb33432b0f5bf644713e696e4dafc7e7d3cc5808

ということで、さっそく成果が出たよ!めでたい!

なにしろこんなたった8文字書き加えるだけのパッチでも、僕一人ではどんなに頑張っても絶対に解決できなかったであろう、(僕にとっては)充分に高度な内容である。
そういう意味でも、みんなに集まってもらって会合を開いた価値はあったわけで、非常に喜ばしい。
しかし、エラーメッセージを一目見て解決してくださったのはささださんだし、
正しいデバッグの方法を指南してくださったのはgotoyuzoさんだし、
実際にキーボードを叩いてくださったのはかくたにさんだし1
それ以前にネットにつなげてくれたのは cojiさんだし、
という具合なので、実は僕は何もやってないんだけど、
でもコミットは何故か僕の名前でクレジットされてて恐縮だ2
そんな意味を込めてチケットの説明文には “We” って書いたんだけど、
でもなんの文脈もないので、「"We"って誰だよ?」って感じだよな。

そしてこれにて一件落着、かと思いきや、その直後にこんなアップデートが。
http://github.com/rails/rails/commit/3c282f3a0a7c1d5ab91241674251794ead5fa41d
なになに? uses more memory than eval with block ? そうなの?
Rubyは奥が深いなぁ・・・

1 このあたりについては かくたにさん的には何やらいろいろ言いたいことがあったらしい 。すみません><

2 しかも gitの configの user.name あたりの設定ができてなくて漢字でフルネームとかいって、恥ずかしすぎる><

Hash key in :conditions

July 2nd, 2008

昨日の 一件のコミット で、今まで ActiveRecordで不満に思ってたところがまた一つ解消された。

User.all :joins => :items, :conditions => {:age => 10, :items => {:color => 'black' }}
という例を見ればわかるだろうが、以下いちおう説明。
 
今まで AR#findのパラメータって、:joins とか :includes はなんぼでもテーブルを多段にネストして書けてたのに1
:conditions では何故か Hashで書けるのは自テーブル内のカラムまでだった。
 
つまり、今までは、
Item.all :conditions => {:color => 'black'}

だったら完全に Rubyの Hashの世界でキレイに書けるくせに、
ってことは、
SELECT "users".* FROM "users" INNER JOIN "items" ON items.user_id = users.id
WHERE (items.color = 'black')

を発行したかったら当然こうだよね、
User.all :joins => :items, :conditions => {:items => {:color => 'black'}}

と書いてみたらダメで、この場合
User.all :joins => :items, :conditions => ['items.color = ?', 'black']

とか書かなきゃいけなかった。
 
ところが今回のパッチのおかげでごくふつうにこんなふうに
User.all :joins => :items, :conditions => {:items => {:color => 'black'}}

なんでも Hashで書けるようになった、と。
めでたしめでたし。
Rails 2.1.1 も順当に良い改良がどんどん入ってて嬉しいですね。
 
#TODO code部分のスタイルが見にくくてしかも行間空きすぎなのをどうにかする。 (080702 done.)
 

1 参考: Cascaded Eager Loading が欲しい@くまくまー , Cascaded Eager Loading@くまくまー

なんと!
acts_as_state_machine が本日 Rails本家に取り込まれちゃった模様!

こいつを皮切りに、
http://github.com/rails/rails/commit/01db5ded54b0e3a2ea80d28e4841d40fcec23cdf
いくつかコミットが入っているが、なにしろサプライズなのが取り込まれた先のモジュール。

これがなんと、

ActiveModel::StateMachine

なのですよ!

いやー、aasmといえば最近 githubに載っかってきてて何人かで寄ってたかって gem化してて
しかも急激に機能が充実してきてて、という動きが気にはなってたけど、
でも中身見てみたら aasm_ とかダサいプレフィックスが付くようになってて、

なんじゃこりゃ?なにこの改悪?

とか思いつつ(こんなふうに)、
http://twitter.com/a_matsuda/statuses/844774379
http://twitter.com/a_matsuda/statuses/844778200
でもなんとなく目が離せないからぼちぼちウォッチを始めてたところに、この電撃 active_modelデビュー。
超ビックリ。named_scope以来のビックリ。

これは Rails 2.1.1の「新機能」の目玉は

ActiveModel

ということで、どうやら間違いないだろう。

あの、妙にカッチョイイけど機能的にはちょっと荒削りだった aasmの DSLが
本家に取り込まれてどう化けるのか、非常に楽しみですね。