自動Gettext

Amrita2にGettextと連動する機能を実装しました。これを利用すると、ビューを最小限の手間で国際化することができます。

サンプルとして LoginEngineをAmrita2化+日本語化していますが、たとえば、homeというビューは次のようになります。

元のrhtmlのファイルは次のようなものです。

<div title="<%= title_helper %>" class="memo">
  <h3>Welcome</h3>
  <p>You are now logged into the system, <%= @fullname %>...</p>
  <p>Since you are here it's safe to assume the application never called store_location, otherwise you would have been redirected somewhere else after a successful login.</p>

  <%= link_to '&#171; logout', :action => 'logout' %>
</div>

これをAmrita2のAmXML形式に書き直すと、こうなります。

<<div class="form":|Attr[:title=>:title_helper]<
  <<h3<
    Welcome
  <<p<
    <% $_ = { :name => @fullname } %>
    You are now logged into the system,  %{name} ...
  <<p<
    Since you are here it's safe to assume the application never called store_location,
    otherwise you would have been redirected somewhere else after a successful login.

  <%= link_to '&#171; ' + _('logout'), :action => 'logout' %>

Rakefileに次のようなタスクを追加して、これを実行します。

task :updatepo do
  $: << 'vendor/plugins/amrita2/lib'
  require 'gettext/utils'
  require 'amrita2/gettext'
  require 'amrita2/macro'
  GetText.update_pofiles("login_engine", #テキストドメイン名(init_gettextで使用した名前)
                         Dir.glob("{app,config,components,lib}/**/*.{rb,rhtml,a2html}"),  #ターゲットとなるファイル
                         "logine_engine 1.0.0"  #アプリケーションのバージョン
                         )
end

すると、*.a2html形式のファイルに対して、Amrita2で追加したパーサが起動して、poファイルができます。これをja/*.potにコピーして、日本語を入れていきます。

potファイルから上記のビューに関連する所だけ抜き出すと

#: app/views/user/home.a2html:-
msgid "Welcome"
msgstr "ようこそ"

#: app/views/user/home.a2html:-
msgid ""
"Since you are here it's safe to assume the application never called "
"store_location,\n"
"    otherwise you would have been redirected somewhere else after a "
"successful login."
msgstr ""
"この画面が表示されるのはアプリケーションが store_location を呼んでないからで"
"す。呼んでいれば、ログインに成功した後には、他の画面にリダイレクトされるで"
"しょう"

#: app/views/user/home.a2html:-
msgid "You are now logged into the system,  %{name} ..."
msgstr "%{name}さん、あなたはこのシステムにログインしています。"

#: app/views/user/home.a2html:-
msgid "logout"
msgstr "ログアウト"

後は、以下のページにあるように通常通りの作業をすればOKです。

結果、次のように表示されます。

<div title="UserController home" class="form">
  <h3>ようこそ</h3>
  <p>  中島 拓さん、あなたはこのシステムにログインしています。</p>
  <p>この画面が表示されるのはアプリケーションが 
     store_location を呼んでないからです。呼んでいれば、
     ログインに成功した後には、他の画面にリダイレクト
     されるでしょう</p> 
  <a href="/user/logout"><<ログアウト</a> 
</div>

ポイントは、以下の通りです。

  1. ビュー内の文字列は全て自動的にGettext対応の文字列として抜き出す
  2. 可変部分の埋めこみもほぼ自動的
  3. erb形式との混在が可能

順番に説明していくと

  <<h3<
    Welcome

"Welcome"という文字列が自動的にpoファイルに抽出されています。

  <<p<
    <% $_ = { :name => @fullname } %>
    You are now logged into the system,  %{name} ...

<% ... %>内には、自由にRubyの処理を記述できます。各文字列は、内部的には以下のようなRubyソースにコンパイルされますから、この中で $_ にハッシュを設定することで、可変部の埋め込みが行われます。

    stream.concat _("You are now logged into the system,  %{name} ...") % $_

これを次のように書くこともできます。

  <<p:|ToHash[:name=>:fullname]<
    You are now logged into the system,  %{name} ...

なお、最初からAmrita2のテンプレートを想定して、コントローラ側でデータ構造をうまく設定すれば、このような処理( $_ への値の設定)は不要になります。

<%=....%>の中に翻訳すべき文字列がある場合には、一般のrhtmlファイルと同じように、文字列を_(...)で囲みます。

  <%= link_to '&#171; ' + _('logout'), :action => 'logout' %>

ビュー内の静的な文字列が特別な手間をかけることなく全て自動的に国際化されるので、アプリケーションによっては効果が大きいのではないかと思います。

マクロの利用例

http://f.hatena.ne.jp/images/fotolife/a/amrita2/20070711/20070711144132.png

これは、Amrita2+Gettextによって日本語化したLoginEngineのユーザ情報修正画面です。この上半分の部分テンプレートでは、マクロを活用してAmrita2化してみました。

元のrhtmlは次のようになっています。

<div class="user_edit">
  <table>
    <%= form_input changeable(user, "firstname"), "First Name", "firstname" %>
    <%= form_input changeable(user, "lastname"), "Last Name","lastname" %>
    <%= form_input changeable(user, "login"), "Login ID", "login", :size => 30 %>
    <%= form_input changeable(user, "email"), "Email", "email" %>
    <% if submit %>
      <%= form_input :submit_button, (user.new_record? ? 'Signup' : 'Change Settings'), :class => 'two_columns' %>
    <% end %>
  </table>
</div>

form_inputは、LoginEngine独自のhelperメソッドで、<tr><td>... を生成するものです。これと似た機能ををAmrita2のマクロとして作成してみました。

changeableも同じくLoginEngine独自のメソッドです。いくつかの条件によって text_field とread_only_fieldを切り替える処理ですが、この部分はこの例では省略しています。(常にtext_fieldになる)

class TwoColumns < Amrita2::Macro::Base
  TemplateText = <<-END
      <<table<
        <<tr class="two_columns":rows<
          <<td class="prompt"<
              <<label:title>>
          <<td class="value" :contents>>
  END

  def macro_data(element)
    rows = element.search("tr").collect do |c|
      title, contents = *c.search("td")
      {
        :title => title.contents,
        :contents => Amrita2::SanitizedString[contents.children.to_s],
      }
    end
    {
      :rows => rows,
    }
  end
end

このマクロは2カラムのテーブルを作成し、1つ目のカラムには<label>...</label>を埋めこむものです。

これを利用すると、上記のテンプレートは次のようになります。

<%(BeforeCompile)
  use_macro(TwoColumns)
%>

<<div class="user_edit"<
  <<two_columns<
    <<<-------------------------------------------------------------
      ||| First Name: | <%= text_field "user", "firstname" %>      |
    <<<-------------------------------------------------------------
      ||| Last Name:  | <%= text_field "user", "lastname" %>       |
    <<<-------------------------------------------------------------
      ||| Login ID:   | <%= text_field "user", "login" %>          |
    <<<-------------------------------------------------------------
      ||| Email:      | <%= text_field "user", "email" %>          |
  << ?[submit] <
    <%= submit_button 'user', (user.new_record? ? _('Signup') : _('Change Settings')), :class => 'two_columns' %>

区切られた表の部分は、<tr><td>を生成する略記法です。これによって生成されたxmlが一回分解されて、上記のマクロで再度テーブルに組み込まれます。

これを日本語化して表示すると次のようになります。

<div class = "user_edit">
  <table>
    <tr class = "two_columns">
      <td class = "prompt"><label></label></td>
      <td class = "value"> <input id="user_firstname" name="user[firstname]" size="30" type="text" value="中島" /> </td>
    </tr>
    <tr class = "two_columns">
      <td class = "prompt"><label>名前</label></td>
      <td class = "value"> <input id="user_lastname" name="user[lastname]" size="30" type="text" value="拓" /> </td>
    </tr>
    ....

「姓」や「名前」の部分が<label>...</label>に挟まれていることに注目してください。

マクロの作成は、はっきり言って難易度が高いですが、これを一回作ってしまうと、表の略記法と組み合わせることで、以下のようなメリットがあります。

  1. 個々のテンプレートを最終表示結果に近い直感的な形で記述できる
  2. マクロの展開方法もAmXMLのテンプレートになっているので、CSSとマクロ内テンプレートを調整することで、最終的なHTMLの調整を行なうことができる
  3. <table>によるレイアウトから<div>等によるレイアウトへの変更もマクロ変更のみで可能
  4. アプリ側(ビュー)の記述範囲とマクロの記述範囲を入れ子にできる(上記の<label>の付加はマクロの担当、<label>の中(セルの中身)はアプリ側の記述)

元ネタでは、helperメソッドを駆使してややトリッキーな方法によって似たようなことをやっていますが、この方式の方が意図が明確で、保守性も高いと思います。