2012年1月27日金曜日

[Rails3] コントローラの namespace を取得する

実行環境:
ruby 1.9.3
Rails 3.1.3
コントローラの名前は controller.controller_name で取得できますが namespace までは取得できないので、UsersController の場合も Admin::UsersController の場合も同じ users が返ってくる。

namespace まで一緒にとりたい場合は controller.controller_path を使います。 admin/users が返ってきます。
  def namespace
    controller.controller_path.split('/')[-2]    # admin
  end

もしくは controller.class.name でも Admin::UsersController のように namespace 付きで得られます。
  def namespace
    controller.class.name.split('::')[-2]        # Admin
  end
どちらも namespace がついてない場合は nil が返ります。

2012年1月26日木曜日

[Rails3] 汎用的なScaffoldを作ってみる

実行環境:
ruby 1.9.3
Rails 3.1.3
ActiveRecord 3.1.3
複数の Scaffold を作っていて、それらの動作や見栄えを変更しようとすると View テンプレートをいちいちテーブルのカラムを1つ1つちくちく直したりというのが結構面倒です。

1つアプリの中に多数のモデル(DBのテーブル)を作っている場合、管理画面は変更が面倒という理由でデフォルトの Scaffold のままだったりします。ちょっと思いたって少しいじろうかなと思っても、修正するファイルが多すぎてすぐにへこたれてしまいます。なにせ1つの Scaffold に対して Controller が1ファイル、Viewが5ファイルあるので、20の Scaffold があると120ファイルくらいあるわけですから。 (>_<)

どの Scaffold でも Controller 名と Model 名、カラム名が違うだけでファイルの構成もやってることもたいてい同じなので、Controller 名やら Model 名やらカラム名やらを動的に取得して共通化してやれば、たくさんのファイルを管理しなくてもよいんじゃないか?

そんなことを考えて Controller と View のファイルから個別のモデルに関する記述を取り除いて、どの Scaffold でも継承するだけで使えるようなものを作ってみたいと思います。

基本方針

Controller について
ApplicationController を継承した CommonScaffoldController(名前はなんでもいいですが)を作り、index や edit などのアクションメソッドを汎用性を持たせた形で書きこみます。各 Scaffold (以下では例として Users で説明します。)のコントローラは CommonScaffoldController を継承させるだけで中身を空っぽにします。アクションは全て CommonScaffoldController で定義するという作戦です。
/app/controller/common_scaffold_controller.rb
class CommonScaffoldController < ApplicationController
  def index
    # ここできっちりと定義しておく
  end

  # アクションは全てこのファイルで定義します
 
end
/app/controller/users_controller.rb
class UsersController < CommonScaffoldController  # 個別のコントローラは継承するだけ
end

View について
デフォルトの View ファイルの構成をそのまま使います。ajax を使えばもうちょっときれいにまとめられると思いますが、今回の趣旨とは関係ないので今回はそのままです。app/views/ の直下に common_scaffold フォルダを作り、その中に index.html.erb やら _form.html.erb やらを入れます。
/app/views/common_scaffold/
[common_scaffold]
   ├ index.html.erb
   ├ show.html.erb
   ├ new.html.erb
   ├ edit.html.erb
   └ _form.html.erb

上記の CommonScafffoldController の中でこのディレクトリの中の View ファイル達を render に使います。つまりどの Scaffold でも同じ View ファイルが使われるようにします。

具体的なコード

CommonScaffoldController
各 Scaffold のコントローラがこの CommonScaffoldController を継承します。controller_name で個別のコントローラ名が取得でkるのでそれを元に対応するモデル名、モデルクラスを取得します。
/app/controller/common_scaffold_controller.rb
class CommonScaffoldController < ApplicationController

  before_filter get_model_class  # @model_class, @model_instance_name を取得

  def index
    @objects = @model_class.all
 end

  def show
    @object = @model_class.find(params[:id])
    # @object = @model_class.where("? = ?", @model_class.primary_key, params[:id]).first
  end

  def new
    @object = @model_class.new
  end

  def edit
    @object = @model_class.find(params[:id])
  end

  def create
    @object = @model_class.new(params[@model_instance_name])

    respond_to do |format|
      if @object.save
        format.html { redirect_to @object, notice: "%s was successfully created."%(@model_instance_name) }
      else
        format.html { render 'new' }
      end
    end
  end

  def update
    @object = @model_class.find(params[:id])

    respond_to do |format|
      if @object.save
        format.html { redirect_to @object, notice: "%s was successfully updated."%(@model_instance_name) }
      else
        format.html { render 'edit' }
      end
    end
  end

  def destroy
    @object = @model_class.find(params[:id])
    @object.destroy

    respond_to do |format|
      format.html { redirect_to :action => :index }
    end
  end

private
  def get_model_class
    @model_class = controller_name.classify.constantize        # モデルクラス(Userなど)を取得
    @model_instance_name = @model_class.model_name.underscore  # モデルのインスタンス名を取得
  end
end
common_scaffold/*.html.erb
Viewファイルの中身からも個別の Scaffold に関わる名前を駆逐します。

index.html.erb
app/views/common_scaffold/index.html.erb
<h1>Listing <%= @model_instance_name.plurarize %></h1>

<table>
  <tr>
    <%- @model_class.columns.each do |column| -%>
      <th><%= @model_class.human_attribute_name(column.name) %></th>
    <%- end -%>
  </tr>

  <%- @objects.each do |object| -%>
    <tr>
      <%- @model_class.columns.each do |column| -%>
        <td><%= @model_class.send(column.name) %></td>
      <%- end -%>
      <td><%= link_to 'Show', object %></td>
      <td><%= link_to 'Edit', :controller => controller.controller_name, :action => :edit, :id => object.id %></td>
      <td><%= link_to 'Destroy', object, confirm: 'Are you sure?', method: :delete %></td>
      <td></td>
    </tr>
  <%- end -%>
</table>

<%= link_to "New %s"%(@model_instance_name), :controller => controller.controller_name, :action => :new %>

show.html.erb
app/views/common_scaffold/show.html.erb
<p id="notice"><%= notice %></p>

<%- @model_class.columns.each do |column| -%>
<p>
  <b><%= column.name.humanize %></b>
  <%= @object.name %>
</p>
<%- end -%>

<%= link_to 'Edit', :controller => controller.controller_name, :action => :edit, :id => @object.id %></td>
<%= link_to 'Back', :controller => controller.controller_name, :action => :index %></td>

new.html.erb
app/views/common_scaffold/new.html.erb
<h1>New <%= @model_instance_name %></h1>

<%= render 'form' %>

<%= link_to 'Back', :controller => controller.controller_name, :action => :index %>

edit.html.erb
app/views/common_scaffold/edit.html.erb
<h1>Edit <%= @model_instance_name %></h1>

<%= render 'form' %>

<%= link_to 'Show', @object %>
<%= link_to 'Back', :controller => controller.controller_name, :action => :index %>

_form.html.erb
app/views/common_scaffold/_form.html.erb
<%= form_for(@object) do |f| %>
  <% if @object.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@object.errors.count, "error") %> prohibited this <%= @model_instance_name %> from being saved:</h2>

    <ul>
    <% @object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>

  <%- @model_class.columns.each do |column| -%>
  <div class="field">
    <%= f.label column.name %><br />
    <%= f.text_field column.name %>
  </div>
  <%- end -%>
  <div class="action">
    <%= f.submit %>
  </div>
<% end %>

個別の Scaffold の設定
Controller は CommonScaffoldController を継承するだけ、Model は標準の Scaffold で作られるもののまま、View ファイルは削除します。

Controller
Controller は CommonScaffoldController を継承するだけで、メソッドの定義は一切不要。
/app/controller/users_controller.rb
class UsersController < CommonScaffoldController
end
Views
すべて app/views/common_scaffold/ の中のファイルを見るので、個別の Scaffold 用の Views ディレクトリ(app/views/users)は削除します。View ファイルを探す際に app/views/users/*, app/views/common_scaffold/* の順に探されるので消しておかないと app/views/common_scaffold/* のファイルを使ってくれません。

Model
デフォルトの Model ファイルのまま、特に変更する必要はありません。
/app/models/user.rb
class User < ActiveRecord::Base
end

まとめ
一部妄想で書いている部分があるので動かないところがあるかもしれませんが、大まかな考え方はこんな感じです。Viewファイル1セットを変えれば全て変わるので管理画面の変更も簡単です(^_^)
※今回は rails generate scaffold したまんまの View を使いましたが、そのうち Ajax 版も作ってみたいと思います。

2012年1月9日月曜日

[Rails3] 現在のURLを取得(request オブジェクト)

実行環境:
ruby 1.9.3
Rails 3.1.3
Rails アプリケーションの中で現在処理中(アクセス中)の URL を取得するには request オブジェクトを参照します。

プロトコル名(http://)を含めたフル URL を取得する。
request.url                  # http://example.com/users/search?q=hoge&p=1

URL のホスト名以降の文字列を取得する。
request.request_uri          # /users/search?q=hoge&p=1

URL のホスト名以降のパス文字列を取得する。クエリ文字列(?以降)は含まない。
request.path_info            # /users/search


requestオブジェクトについて
URL 以外にも request オブジェクトを使うと HTTP_REQUEST に入っている様々な情報を取得できます。
request.auth_type            # AUTH_TYPE
request.content_length       # CONTENT_LENGTH
request.content_type         # CONTENT_TYPE
request.gateway_interface    # GATEWAY_INTERFACE
request.path_info            # PATH_INFO
request.path_translated      # PATH_TRANSLATED
request.query_string         # QUERY_STRING
request.remote_addr          # REMOTE_ADDR
request.remote_host          # REMOTE_HOST
request.remote_ident         # REMOTE_IDENT
request.remote_user          # REMOTE_USER
request.request_method       # REQUEST_METHOD
request.script_name          # SCRIPT_NAME
request.server_name          # SERVER_NAME
request.server_port          # SERVER_PORT
request.server_protocol      # SERVER_PROTOCOL
request.server_software      # SERVER_SOFTWARE
request.accept               # HTTP_ACCEPT
request.accept_charset       # HTTP_ACCEPT_CHARSET
request.accept_encoding      # HTTP_ACCEPT_ENCODING
request.accept_language      # HTTP_ACCEPT_LANGUAGE
request.cache_control        # HTTP_CACHE_CONTROL
request.from                 # HTTP_FROM
request.host                 # HTTP_HOST
request.negotiate            # HTTP_NEGOTIATE
request.pragma               # HTTP_PRAGMA
request.referer              # HTTP_REFERER
request.user_agent           # HTTP_USER_AGENT
request.raw_cookie           # HTTP_COOKIE
request.raw_cookie2          # HTTP_COOKIE2
request.request_uri          # REQUEST_URI

2012年1月8日日曜日

[Rails3] 国際化 I18n のまとめ(その3:辞書ファイルの使い方)

実行環境:
ruby 1.9.3
Rails 3.1.3
I18n
Railsアプリで表示を担当するのはビューなので国際化するのは当然ビューですね。ページに表示される文字すべてが対象となります。

ページに表示される文字をざっと分類してみましょう。
  1. DBに格納されている文字列
  2. HTMLに直接書く単語や文章
  3. モデルの名前や属性の名前などモデルに関連する文字列
  4. Railsのエラーメッセージ

1. DBに格納されている文字列

DBに格納されているデータを国際化(多言語化)する場合は、各ロケールごとのデータを用意する必要があります(日本語だけなら不要です)。
シンプルな方法としては、テーブルに複数のロケール用データをカラムを増やして各ロケールのデータを格納します。
TABLEの構造
+------+----------+------------+
|  id  | hello_en |  hello_ja  |
+------+----------+------------+
|   1  |  Hello   | こんにちは |
+------+----------+------------+
もしくはハッシュの形で各ロケールのデータを1つにまとめて(=シリアライズして)DBの1つのカラムに突っ込むという手もあります。
#{RAILS.ROOT}/app/models/greetings.rb
class Greeting < ActiveRecord::Base
  serialize :data, Hash
end
  @greeting.data = {:en => "Hello", :ja => "こんにちは"}
いずれの方法にせよ、表示の際にロケールにあわせて表示し分けるようにします。

2. HTMLに直接書く単語や文章

とりあえず自由に辞書を定義する
HTMLテンプレートに書くタイトルや文章などを辞書ファイルで定義していく、というのがとりあえず思いつくところでしょうか。辞書ファイルはハッシュの形で定義します。基本的にキーを自由につけることができますし、整理のために自由に階層をつけることができます。
#{RAILS.ROOT}/config/locale/ja.yml
ja:
    hello: こんにちは
    foods:
        vegetables:
            cucumber: きゅうり
参照するには I18n.t メソッドを使います。
Controllerの中では
#{RAILS.ROOT}/app/controllers/users_controller.rb
  I18n.t('hello')
  I18n.t('foods.vegetables.cucumber')
Viewの中では
#{RAILS.ROOT}/app/views/users/index.html.erb
  <%= t('hello') %>
  <%= t('foods.vegetables.cucumber') %>
という形で参照します。

「きゅうり」のように階層になっている用語については、上位の階層をscopeと見ることもでき、
#{RAILS.ROOT}/app/views/users/index.html.erb
  <%= t('cucumber', :scope => 'foods.vegetables') %>
  <%= t('vegetables.cucumber', :scope => 'foods') %>
という書き方も可能。

キーの指定は文字列だけでなく、シンボルでもいけます。
#{RAILS.ROOT}/app/views/users/index.html.erb
  <%= t(:hello) %>
  <%= t(:foods, :scope => 'vegetables.cucumber') %>
  <%= t(:foods, :scope => [:vegetables, :cucumber]) %>

ビュー別の定義
特定のビューで使用する辞書定義は、ルールに従って定義するとビューファイルの中で簡単に参照できます。
#{RAILS.ROOT}/config/locale/ja.yml
ja:
  user:        # コントローラ名
    index:     # アクション名
      title: ユーザー一覧
    show:      # アクション名
      title: ユーザーの詳細
このように定義するとビューファイルの中で <%= t('.title') %> で参照でき、index.html.erb と show.html.erb でそれぞれの定義が使われます。

ロケール別のHTMLテンプレート
単語・文章レベルで表示を差し替えればよいだけであれば上記の方法で対応できますが、ロケールによってページ内の構成が大幅に異なる場合などはロケールごとに別々にHTMLテンプレートを用意しておくこともできます。
#{RAILS.ROOT}/app/views/users/
[users]
  |-- index.en.html.erb
  |-- index.ja.html.erb
  |-- index.de.html.erb
ロケールにあわせて適切なHTMLテンプレートを選択してくれます。

3. モデルの名前や属性の名前などモデルに関連する文字列

モデルに関連する名前は次のように辞書ファイルに書きます。
#{RAILS.ROOT}/config/locale/ja.yml
ja:
  activerecord:
    models:
      user: ユーザー             # モデル名
    attributes:
      user:                      # モデル名
        name: 名前               # モデルの属性名 
        password: パスワード     # モデルの属性名 
モデル名、モデルの各属性名は
User.model_name.human            # モデル名
User.human_attribute_name(name)  # モデルの属性名 
のように簡単に参照できます。もちろん通常通り I18n.t('activerecord.models.attributes.name') でも参照できます。

form の中ではモデルの属性値を表す label が自動的に辞書ファイルの内容で置き換えられます。
#{RAILS.ROOT}/app/views/edit.html.erb
  <%= form_for(@user) do |f| %>
    <%= f.label :name %>    # 「名前」
  <%= end %>

※個人的には、毎回 User.model_name.human、User.human_attribute_name(name)とか書くのは若干面倒だなぁと思うので、こんなヘルパーメソッドを作ってモデル名やモデルの属性名が超簡単に拾えるようにしてます。
#{RAILS.ROOT}/app/helpers/application_helper.rb
def t_ar(label)
  arr = label.split(/./)
  if arr.length <= 2 then
    begin
      model = arr[0].constantize
    else
      if arr.length == 1
        return model.model_name.human
      else
        return model.human_attribute_name(arr[1])
      end
    end
  end
  label
end
  t_ar('User')        # モデル名(User.model_name.human)
  t_ar('User.name')   # 属性名(User.human_attribute_name(name))

4. Railsのエラーメッセージ

エラーメッセージも辞書ファイルで定義することで日本語化できます。
#{RAILS.ROOT}/config/locale/ja.yml
ja:
  activerecord:
    errors:
      messages: ユーザー一覧
        blank: "が記入されていません。"   # ①
        invalid: "が不正な値です。"
        confirmation: "が一致しません。"
        ...
エラーの種類に対応したエラーメッセージをこのように定義します。この定義はすべてのモデルのエラーをカバーします。

一方、特定のモデルや特定の属性について別のエラーメッセージを指定するには次のようにします。
特定のモデル用のエラーメッセージ
#{RAILS.ROOT}/config/locale/ja.yml
ja:
  activerecord:
    errors:
      models:
        user:
          blank: "が記入されていません。"   # ②
          ...
特定のモデルの特定の属性用のエラーメッセージ
#{RAILS.ROOT}/config/locale/ja.yml
ja:
  activerecord:
    errors:
      models:
        user:
          attributes:
            name:
              blank: "が記入されていません。"   # ③
              ...
より細かいところでの定義が優先されます。
Userモデルのname属性についてのvalidationが以下のように定義されていてnameに値にblankエラーが発生した場合、③ → ② → ① の順に探して最初に見つかった定義が使われます。
#{RAILS.ROOT}/app/models/user.rb
class User < ActiveRecord::Base
  validates_presence_of :name
end

2011年12月28日水曜日

[Rails3] 国際化 I18n のまとめ(その2:ロケールの切り替え)

実行環境:
ruby 1.9.3
Rails 3.1.3
I18n
文字列を国際化する I18n の使い方のまとめです。
今回は、その2としてロケールの切り替えについて考えてみたいと思います。

表示ロケールを決定する流れ

複数のロケールを定義している場合、表示するロケールがアクセス毎に判断することになります。それをどうやって(どの情報を使って)判断するか、というお話です。
  1. ロケール値が明示的に指定されていたら、そのロケールで表示
    • ロケール値を URL のクエリ(パラメータ)で指定
    • ロケール値を URL のパス名で指定(実態はクエリと同じ)
    • ロケール値をセッション情報で指定
  2. アクセス先のURLのドメイン名で判別(複数のドメイン名で運用している場合のみ)
    • TLD(Top Level Domain)で判別
    • サブドメインで判別
  3. 接続元の情報に応じて、適切なロケールで表示
    • HTTP_ACCEPT_LANGUAGEから判断
    • IPアドレスから判断
  4. (とりあえず)デフォルトのロケール(config.I18n.default_locale)で表示

1.〜4.の方法を適当に組み合わせてやります。(4.は方法と呼べるものでもないが。) ミニマムにやるなら 1. と 4. 、ちゃんとやるんなら1. と 3. と 4.の組み合わせが一般的だと思います。 2. は「yoursite.com と yoursite.jp」(TLDで判別)、あるいは「ja.yoursite.com と fr.yousite.com] (サブドメインで判別) など複数の(サブ)ドメイン名で運用している場合のみ可能な技です。
# 純日本語なサイトでロケールを全く切り替えないのであれば、当然のことながら I18n.locale の値をデフォルト(config.I18n.default_locale)のまま変更しなくてOKです。

ロケールの切り替えのざっくりイメージは
  • ミニマムにやる場合(1. と 4.)
     - サイトに最初にアクセスするとデフォルトのロケールで表示
     - 途中で日本語での表示に変更したいユーザーは日の丸アイコンをクリック
    という感じ
  • ちゃんとやる場合(1. と 3. と 4.)
     - 接続元の情報から定義済みのロケールがあれば、そのロケールを使用して表示
     - なければデフォルトのロケールで表示
     - ユーザーが別の表示に変更したい場合は、表示したい言語の国旗アイコンをクリック
    という感じ


ロケール値の受け渡し

複数のロケールをユーザーが切り替えられるようにするには、同じユーザーの連続したアクセス(同一セッション)ではロケール値を引き継げないといけません。じゃないと、一度ロケールを切り替えてもアクセスする度にロケールの設定が元に戻ってしまうことになります。

ロケール値の受け渡しの方法のあれこれ(もないけど…)
session情報(または cookie)でロケール値を受け渡す
現在のロケール値を session 情報に格納し、以降のアクセスについては session 情報から取り出して使用します。
ロケール値が切り替わった時に session 情報に格納
session[:locale] = I18n.locale
アクセスの度に session 情報から locale 値をセットする
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base before_filter :restore_locale private def restore_locale I18n.locale = session[:locale] if session[:locale] end end
同一セッション内で共有する情報なので、session 情報としてとっておく、というのはストレートな考え方です。やり方も簡単。 でもあまり推奨されないらしいです(^^;
RESTful な考え方だと同じURLでアクセスしたら基本的に表示される内容は同じであるべき、ってことらしい。
# 個人的には「ま、いいんじゃない?」って気がするけど。
URLのクエリのパラメータとして毎回ロケール値を渡す
基本となるシンプルな考え方です。
アクセスする時のイメージはこんな感じ
URL の例
http://www.yoursite.com/topics?locale=ja
アクセスの度に params から locale 値をセットする #{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base before_filter :restore_locale private def restore_locale I18n.locale = params[:locale] if params[:locale] end end
URLのパラメータで渡すということは、別のページに移動したりする際に毎回 locale の値をパラメータに含めてあげる必要があります。
#{RAILS.ROOT}/app/views/people/show.html.erb
<%= link_to people_path(:locale => I18n.locale) %>
と、こんな感じでいけるでしょう。
ですが、これをすべてのリンクに追加してまわるのはちょっとイケてないので、常にパラメータに locale を追加してもらえるように ApplicationController で設定をします。
#{#RAILS.RPPT}/app/controllers/application_controller.rb
def url_options { :locale => I18n.locale }.merge(super) end
これでOKです。

でも URL に毎回「?locale=ja」とかくっついてあんまし格好よくなくね?と思った方は、もう一手間を加えて URL のパスの一部としてロケール値を渡すようにしましょう(次項へ続く)
URLのパスの一部でロケール値を渡す(実質的にはクエリで渡すのと同じ)
前項のパラメータでロケール値を渡すの続きです。Rails3の強力なルーティングの力を借りてクエリの形をしているパラメータをパスの一部の形に変えることができます。config/routes.rb をちょいと変更するだけです。

ルーティング変更前↓
#{RAILS.ROOT}/config/routes.rb
resources :people
ルーティング変更後↓
#{RAILS.ROOT}/config/routes.rb
scope "/:locale" do resources :people end
これで URL が以下のように変わります。
URL変更前↓
http://yoursite.com/people?locale=ja
URL変更後↓
http://yousite.com/ja/people
ルーティングの設定は、アクセスされたURLの解析だけでなく、Railsアプリケーション内でリンクを作る場合にも使用されるので <%= link_to 'People', people_path %> で作成されるパスも自動的にかわります(めちゃ便利!)

これで完璧ですぅ(^^)

ドメイン名(サブドメイン名)からのロケールの判定

ホスト名の一部からロケールとして使用する文字列を抜き出します。
TLD名からロケールを判定
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base before_filter :set_locale private def set_locale I18n.locale = extrace_locale_from_tld end def extract_locale_from_tld request.domain.split('.').last end end
サブドメイン名からロケールを判定
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base before_filter :set_locale private def set_locale I18n.locale = extrace_locale_from_subdomain end def extract_locale_from_subdomain request.subdomains.first end end
こんな感じでやればOKです。とりあえず引っ張ってきた文字列のロケールはRailsアプリケーション内に用意されているという前提です。そこんとこをチェックする場合は次項参照。

接続元に応じたロケールの判断

接続元の情報ですので HTTP_REQUEST の内容で判断します。
HTTP_ACCEPT_LANGUAGE で判断
HTTP_REQUEST の中の HTTP_ACCEPT_LANGUAGE の最初の言語名(=最初の2文字)をとってきます。
HTTP_ACCEPT_LANGUAGE にはカンマで区切って複数の言語が優先度とともに書いてあるので、優先度の高い順にロケールの準備の有り/無しを判定しながら決めていくのが筋のような気がしますが、ここでは省略。
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base before_filter :set_locale private def set_locale extracted_locale = extract_locale_from_accept_language I18n.locale = (I18n::available_locales.include? extracted_locale.to_sym) ? extracted_locale : I18n.default_locale end def extract_locale_from_accept_language request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first end end
I18n::available_locales 配列の中に定義済みのロケールのリストがあるので、そこと比較して定義されていればそのロケールを、なければデフォルトロケールを使用します。
IP アドレスで判断
HTTP_REQUEST の中のリモートIPアドレスをとってきます。
IPアドレスから国を判断する必要がありますが、MaxMind 社の GeoIP ライブラリが使えます。 ruby のライブラリや Apache モジュール(mod_geoip)、Gem も複数存在するようなので好みの方法を使用してください。
以下は cjheath/geoip の Gem を使ったサンプルです。
#{RAILS.ROOT}/Gemfile
gem 'geoip'
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base require 'geoip' before_filter :set_locale private def set_locale extracted_locale = extract_locale_from_ip I18n.locale = (I18n::available_locales.include? extracted_locale.to_sym) ? extracted_locale : I18n.default_locale end def extract_locale_from_ip geoip ||= GeoIP.new(Rails.root.join(“lib/GeoIP.dat”)) country_location = @geoip.country(request.remote_ip) country_location.country_code2.downcase end end


まとめ(=全部盛り)

ロケールの判定を上記の諸々をまとめてやる場合は以下のように書けばよいかと。上で説明したきたロケールの取得方法をそれぞれ準備してあげて、優先順位を決めて1つめの方法で取得できなければ、次の方法、次の方法…といった感じで extracted_locale 変数にセットします。取得したロケールが定義済みロケールのリスト(I18n::available_locales)にあるかどうかをチェックし、なければデフォルトを使う、という流れです。
#{RAILS.ROOT}/app/controller/application_controller.rb
ApplicationController < ActionController::Base require 'geoip' before_filter :set_locale private def set_locale extracted_locale = params[:locale] || extract_locale_from_subdomain || extract_locale_from_tld || extract_locale_from_accept_language || extract_locale_from_ip I18n.locale = (I18n::available_locales.include? extracted_locale.to_sym) ? extracted_locale : I18n.default_locale end def extract_locale_from_subdomain request.subdomains.first end def extract_locale_from_tld request.domain.split('.').last end def extract_locale_from_accept_language request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first end def extract_locale_from_ip geoip ||= GeoIP.new(Rails.root.join(“lib/GeoIP.dat”)) country_location = @geoip.country(request.remote_ip) country_location.country_code2.downcase end end
必要のないものは抜いてくださいまし。

2011年12月27日火曜日

[Rails3] 国際化 I18n のまとめ(その1:準備編)

実行環境:
ruby 1.9.3
Rails 3.1.3
I18n
文字列を国際化する I18n の使い方のまとめです。
ここでいう国際化とは、要するに複数言語表示対応という意味です。
※ちなみに I18n というのは、I で始まり、間に18文字を挟んで、nで終わる文字列という意味で Internationalization の略称です。

「おらぁ日本語しか使わねーから関係ねえだよ」と思う方もいるかと思いますが、View 用の日本語文字列をコードと切り離しておく場合、I18n の枠組みに乗っかると便利です。

今回は、その1として I18n を使った国際化の大まかなイメージを説明しつつ、日本語ロケールを使えるように設定します。

ロケールとは使用する言語の設定であり、1つの Rails アプリケーションの中で英語(:en)、日本語(:ja)、スペイン語(:es)などなど複数のロケールを定義することができます。

表示する時点のロケール値(I18n.locale)によって表示する言語を切り替える仕組みになっています。I18.locale の値が :en なら英語表記、I18n.locale の値が :ja なら日本語表記で表示される、という具合です。
動作確認のテスト1
適当な scaffold を作って(ここでは book とします)、View の中で以下のように書いてみて下さい。
#{RAILS.ROOT}/app/views/books/index.html.erb
<%=t :hello %>
I18n が働いていれば
hello world
と表示されるはずです。
なぜこうなるかと言えば、既に #{RAILS.ROOT}/config/locales/en.yml の中で「hello: hello world」と書かれているからです。
#{RAILS.ROOT}/config/locales/en.yml
en: hello: hello world
I18n.translation というのがキーと値の対応付けをしてくれるメソッドで :hello(もしくは 'hello')を渡すと 'hello world' を返します。
t というヘルパーメソッドが、この I18n のメソッドの別名として定義されています。

en.yml は辞書ファイルと呼ばれ、このファイルの中にロケールの定義を書いていきます。
通常、ファイル名のベースネーム部分(拡張子より前の部分)はロケールを表すアルファベット2文字にします。日本語なら ja.yml、英語なら en.yml、ドイツ語なら de.yml、フランス語なら fr.yml という具合です。こうすると1つのファイルには1つのロケールについての定義を記載し、複数のロケールについて定義する場合はそれぞれのロケール名に応じたファイルを作成することになります。
※実際にはファイル名は何でもよいです。複数のロケールの定義を1つのファイルにまとめて書いてもOK。どのロケールの定義かは、ファイル名ではなくymlファイルの中身(ハッシュ定義の最上位のキー)で判断されます。どのファイルを読み込むかも「*.yml」って感じで設定されているので拡張子さえあっていればなんでもいいのです。

デフォルトロケールの設定

I18n は表示をする際にその時のロケールの値、I18n.locale の値をみて表示するロケールを判断します。 ロケール値はコードの中で明示的に変更しない限り、デフォルトの値が使用されます。 ロケールのデフォルトは通常 :en、つまり英語になっています。 そのため上記のテスト1ではロケール値が :en となり en.yml の中で設定した内容が表示されたわけです。
動作確認のテスト2
先ほどのテスト1と同じファイルで日本語ロケールのテストをしてみましょう。 まず en.yml を同じディレクトリにコピーして ja.yml という名前にし、テキストを以下のように変更します。
#{RAILS.ROOT}/config/locales/ja.yml
ja: hello: こんにちは、世界
続いて View ファイルを編集します。
#{RAILS.ROOT}/app/views/books/index.html.erb
<%= "[%s]:"%(I18n.locale.to_s) > <%=t :hello %> <br /> <% I18n.locale = :ja > <%= "[%s]:"%(I18n.locale.to_s) > <%=t :hello %>
サーバを再起動してから実行します。(辞書ファイルを更新した場合はサーバの再起動が必要です)
[en]hello world
[ja]こんにちは、世界
と表示されたでしょうか?このようにロケール値を変えることで表示する文字列を変えるわけです。

日本語をメインで使用するので日本語をデフォルトのロケールに設定したいと思います。config/application.rb を下記のように編集します。
#{RAILS.ROOT}/config/application.rb
config.i18n.default_locale = :ja
これでデフォルトのロケールが日本語(:ja)になりました。


辞書の置き場所

辞書ファイルはたいてい各ロケールごとにymlファイルを作成します。まじめに国際化をしようとすると1ファイル内に書くと定義する項目が多くなってきて見通しが悪くなりますので、1つのロケールの定義を複数のファイルに分割したりします。ディレクトリをきってロケールの分割された辞書ファイルを格納することも可能です。
#{RAISL.ROOT}/config/locales/ 以下のディレクトリ構成(例1)
[locales] |-- defaults.en.yml |-- defaults.ja.yml |-- models.ja.yml |-- models.en.yml |-- views.ja.yml |-- views.en.yml
#{RAISL.ROOT}/config/locales/ 以下のディレクトリ構成(例2)
[locales] |-- [defaults] | |-- en.yml | |-- ja.yml |-- [models] | |-- [defaults] | | |-- en.yml | | |-- ja.yml | |-- [users] | | |-- en.yml | | |-- ja.yml | |-- [books] | | |-- en.yml | | |-- ja.yml |-- [views] |-- [defaults] | |-- en.yml | |-- ja.yml |-- [users] | |-- en.yml | |-- ja.yml |-- [books] |-- en.yml |-- ja.yml
辞書ファイルの格納場所は標準では #{RAISL.ROOT}/config/locales/ の直下となっていて locales 以下のサブディレクトリの中までは読んでくれません。locales 以下のサブディレクトリの中までをすべて読むように設定します。
変更前
#{RAILS.ROOT}/config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '*.{rb,yml}')]
変更後
#{RAILS.ROOT}/config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
これで準備完了です。

辞書ファイルの中身についてはその3以降でまとめていく予定です。

2011年12月26日月曜日

[Rails3] コードに日本語を書くとエラーになる件

実行環境:
ruby 1.9.3
Rails 3.1.3
Controller や Helper など ruby のコードの中に日本語を書くと
invalid multibyte char (US-ASCII)
というようなエラーが出ます。

変数名やメソッド名だけでなく、文字列として埋め込んでもダメです。
うっかりデバッグ用に日本語を埋め込むとはじかれてしまいます。

このエラーを回避する方法は、日本語を書いたファイルの先頭に
# encoding: utf-8
もしくは
# -*- encoding: UTF-8 -*-
というおまじないを書けばOKです。

※Rails2 の時は大丈夫だったのに… というか ruby 1.9.x になってからか。

ちなみに、コメント文であれば日本語問題なく通ります。
View のファイルの中(HTMLなパート)も大丈夫です。


ま、そもそもコードの中にがっつり固定文字列を埋め込むのはよろしくないので、 「i18n 化をきちんとやりなさい」というありがたい天のお告げだと思うことにします。(^^)