ActiveScaffoldの地域化 - 2008-08-12 - ククログ

ククログ

株式会社クリアコード > ククログ > ActiveScaffoldの地域化

ActiveScaffoldの地域化

現在、Railsに対応した国際化の仕組みがいくつかあります。しかし、それぞれが 独自の方法で実現しているため、それらを組み合わせて使うと混沌 とした状態に陥ることも少なくありません。

ここでは、モデルから動的にきれいな画面とコントローラ部分を生 成するActiveScaffoldを用 いた場合の国際化(i18n)と地域化(l10n)の実現方法のひとつを 紹介します。この方法では、 ActiveScaffoldLocalizeRuby-GetText-Package を組み合わせます。混沌とする部分はそれなりになじませます。

国際化の仕組み

Railsで使用できる国際化の仕組みの比較はRails Wiki (英語)が詳しいです。

Ruby-GetText-Package には、以下のような地域化対象のメンテナン スのことを考慮した機能があるので、地域化対象メッセージが増加 したり更新される場合には有力な候補になるでしょう。

  • 地域化対象のメッセージを抽出する機能
    • テーブルにカラムを追加した場合、画面に表示するメッセージを追加・更新した場合などに利用
  • 抽出したメッセージを既存の翻訳済みメッセージにマージする機能
    • 翻訳者が新しい地域化対象のメッセージを翻訳する場合に利用
  • gettext用の翻訳支援ツールを利用可能(.po のフォーマットがGNU gettextと互換性があるため)
    • Emacs用のpo-modeや.po専用のエディタ

Railsやプラグインなどが提供しているメッセージだけを地域化した いなど、地域対象メッセージが変化しない場合はその他の仕組みも 有力な候補になるでしょう。例えば、ActiveScaffold用の ActiveScaffoldLocalizeがその場合です。

ActiveScaffoldLocalize

ActiveScaffoldは、国際化の仕組みとしてObject#as_を提供してい ます。その仕組みを利用して国際化・地域化を実現しているのが ActiveScaffoldLocalizeです。

ActiveScaffoldLocalizeには日本語用のメッセージも含まれている ので、以下のようにすればActiveScaffoldのメッセージを日本語に することができます。

% rails shelf
% cd shelf
% script/generate resource book title:string
% rake db:migrate
% script/plugin install git://github.com/activescaffold/active_scaffold.git
% script/plugin install git://github.com/edwinmoss/active_scaffold_localize.git

config/routes.rb:

-  map.resources :books
+  map.resources :books, :active_scaffold => true

app/controllers/books_controller.rb:

class BooksController < ApplicationController
  active_scaffold :book
end

app/views/layouts/application.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <title>ActiveScaffold l10n</title>
    <%= javascript_include_tag(:defaults) %>
    <%= active_scaffold_includes %>
  </head>

  <body>
    <h1>ActiveScaffold l10n</h1>
    <%= yield %>
  </body>
</html>

app/controllers/applications.rb:

class ApplicationController < ActionController::Base
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    ActiveScaffold::Localization.lang = "ja-jp"
    true
  end
end

サーバを起動してhttp://localhost:3000/books/にアクセスします。

% script/server
% firefox http://localhost:3000/books/

ActiveScaffold + ActiveScaffoldLocalize

見ての通り、「検索」などのメニューは日本語になりますが、テー ブル名からきている「Books」やカラム名の「Title」などは日本語 になりません。

ActiveScaffoldLocalizeの方針では、これらを日本語にするために以下のような内容の config/initializers/lang/ja-jp.rb1を作成します。

config/initializers/lang/ja-jp.rb:

# -*- coding: utf-8 -*-

ActiveScaffold::Localization.define('ja-jp') do |lang|
  lang["Books"] = "本一覧"
  lang["Book"] = "本"
  lang["Title"] = "タイトル"
end

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + ActiveScaffoldLocalize + モデルの地域化

(「本を作成」ではなく「本一覧を作成」になっているのはこ のパッチ で直ります。)

ActiveScaffoldLocalizeのこのやり方は手軽ですが、地域化対象の メッセージが変更になった場合(例: 「Title」から「Name」に変更) や、地域化対象のメッセージをtypoした場合(例: 「Title」ではな く「title」としていた)に気づきにくいという問題があります。 このような問題に対してはRuby-GetText-Packageが有効です。

ということで、ActiveScaffoldのメッセージは ActiveScaffoldLocalizeで地域化し、それ以外は Ruby-GetText-Packageで地域化するようにします。

Ruby-GetText-Package

ActiveScaffoldLocalizeとRuby-GetText-Packageのすみわけは上述 の通りですが、エラーメッセージの地域化はRuby-GetText-Package ではなく、ActiveScaffldLocalizeに任せます。これは、 ActiveScaffoldがエラーメッセージ部分を上書きしているため、 Ruby-GetText-Packageが提供するエラーメッセージ国際化処理とな じまないためです。

また、Ruby-GetText-Packageが取得したロケール情報を使って ActiveScaffoldLocalizeのlangを設定していることもコツのひとつ です。

config/environment.rb:

# ...
Rails::Initializer.run do |config|
  # ...
  config.gem "gettext", :lib => "gettext/rails"
  # ...
end

lib/active_scaffold_gettext.rb:

module ActiveScaffoldGetText
  include GetText::Rails

  bindtextdomain(GETTEXT_DOMAIN)
end

class Object
  def as__with_gettext(message, *args)
    return nil if message.nil?
    localized_message = ActiveScaffoldGetText.send(:sgettext, message)
    if localized_message == message
      as__without_gettext(message, *args)
    else
      localized_message % args
    end
  end
  alias_method_chain :as_, :gettext
end

module ActiveScaffold::DataStructures
  class Column
    def initialize_with_gettext(name, active_record_class)
      initialize_without_gettext(name, active_record_class)
      self.label = "#{active_record_class.name.demodulize}|#{@label.humanize}"
    end
    alias_method_chain :initialize, :gettext
  end
end

config/initializers/gettext.rb:

GETTEXT_DOMAIN = "your-rails-application"
require 'active_scaffold_gettext'

class ActiveRecord::Errors
  # restore default error messages overridden by Ruby-GetText-Package.
  @@default_error_messages = {
    :inclusion => "is not included in the list",
    :exclusion => "is reserved",
    :invalid => "is invalid",
    :confirmation => "doesn't match confirmation",
    :accepted  => "must be accepted",
    :empty => "can't be empty",
    :blank => "can't be blank",
    :too_long => "is too long (maximum is %d characters)",
    :too_short => "is too short (minimum is %d characters)",
    :wrong_length => "is the wrong length (should be %d characters)",
    :taken => "has already been taken",
    :not_a_number => "is not a number",
    :greater_than => "must be greater than %d",
    :greater_than_or_equal_to => "must be greater than or equal to %d",
    :equal_to => "must be equal to %d",
    :less_than => "must be less than %d",
    :less_than_or_equal_to => "must be less than or equal to %d",
    :odd => "must be odd",
    :even => "must be even"
  }

  alias_method :on, :on_without_gettext
  alias_method :[], :on
end

lib/tasks/gettext.rb:

namespace :gettext do
  namespace :po do
    desc "Update pot/po files."
    task :update => :environment do
      require 'gettext/utils'

      module GetText::ActiveRecordParser
        class << self
          alias_method :add_target_original, :add_target
          def add_target(targets, file, msgid)
            if /\|/ !~ msgid
              add_target_original(targets, file, msgid.classify)
              add_target_original(targets, file, msgid.classify.pluralize)
            end
            add_target_original(targets, file, msgid)
          end
        end
      end

      targets = Dir.glob("{app,config,components,lib}/**/*.{rb,erb,rjs}")
      GetText.update_pofiles(GETTEXT_DOMAIN, targets, "#{GETTEXT_DOMAIN} 0.0.1")
    end
  end

  namespace :mo do
    desc "Create mo-files"
    task :create do
      require 'gettext/utils'
      GetText.create_mofiles(true, "po", "locale")
    end
  end
end

app/controllers/application.rb:

class ApplicationController < ActionController::Base
  init_gettext GETTEXT_DOMAIN
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    posix_locale = GetText.locale.to_posix
    posix_locale = "#{posix_locale}-#{posix_locale}" if /_/ !~ posix_locale
    lang = posix_locale.gsub(/_/, '-').downcase
    ActiveScaffold::Localization.lang = lang
    true
  end
end

翻訳メッセージのファイルpoを作って翻訳します。

% rake gettext:po:update
% mkdir po/ja
% msginit -i po/your-rails-application.pot -o po/ja/your-rails-application.po -l ja_JP
# 途中でメールアドレスを聞かれるので入力する

po/ja/your-rails-application.po:

# ...
#: app/models/book.rb:-
msgid "Book"
msgstr "本"

#: app/models/book.rb:-
msgid "Books"
msgstr "本一覧"
# ...
#: app/models/book.rb:-
msgid "Book|Title"
msgstr "タイトル"
# ...

翻訳メッセージをmoにコンパイルしてアクセスするとテーブル名や カラム名などが日本語になります。

% rake gettext:mo:create

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + Ruby-GetText-Package

まとめ

ActiveScaffoldLocalizeとRuby-GetText-Packageを使って、 ActiveScaffoldを用いたアプリケーションの国際化・地域化を実現する方法 のひとつを紹介しました。

基本的に複数の国際化のしくみを同時に使うと問題が起きますが、 今回は以下のようにそれぞれの長所を活かすようにすみわけて、問 題を回避しています。

  • ActiveScaffoldが利用する固定のメッセージは ActiveScaffoldLocalizeで地域化
  • モデル関連や追加・更新が行われるメッセージについては Ruby-GetText-Packageで地域化
  1. config/initializers/lang/以下にファイルを作るというのはActiveScaffoldLocalizeの方針ではありません。ファイルの場所は特に方針はないようです。