Ruby on RailsのI18nで使用する名前空間に関してのまとめと、ベストプラクティスの検討。
はじめに
Ruby on RailsにはI18nというgemが標準で同梱されており、特に何もせずとも文章の国際化を行うことができる仕組みになっています。ちなみに、I18nとはinternationalizationを表しており、iとnの間に18文字あるから `I18n` だそうです。
最近関わっているRailsのプロジェクトで国際化を行うタスクが発生し、I18nの辞書ファイルを定義する際に、定義ファイル中の名前空間(ロケール情報を定義する場所)について、どのような構造で配置するべきか調べる機会があったのでまとめておきます。
環境
- Rails4
想定環境
rails generateで name, email, urlを引数に指定してUserをscaffoldした環境を想定してロケールファイルを記述しています。
rails generate scaffold user name:string email:string url:string
Userモデル
User | name | 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行にでもなったら考えましょ。)
あくまでも オレオレベストプラクティス なので、おかしいところ、もっとこうする方がいいんじゃね?とかあると思います。もしそう思ったら指摘してくれると嬉しいです。
RubyでInteger値を持つEnum、Inumを作ってみた。
この記事はバージョン2.x系の記事で、現行バージョンの3.xとは互換性がありません。
RailsのEnumと相性が悪い人向けのInumをバージョンアップさせた話。 - 波打際のブログさん
こちらの記事がバージョン3.x系の記事となります。
はじめに
最近Rubyを使用する機会が増えてきたのですが、Rubyに列挙型の実装がありませんでした。そこで、Gemを探してみたものの、C言語ライクなInteger値を持つEnumで、思ったとおりの事が可能なGemが存在しなかったので、自前で実装する機会がありました。その実装の仕組みを参考に汎用的に使用可能なEnum(Inum)を作成してGemとして公開しました。
- 2013年11月28日、機能追加に伴い追記しました。
- 2013年12月03日、バージョン2.0.0リリースに伴い記事を修正しました。
- 2013年12月10日、バージョン2.0.5リリースに伴い記事を修正しました。
使い方
列挙型の定義
Inum::Baseを継承したクラス内で、define_enumメソッドを呼び出して列挙型を定義します。define_enumの第一引数には列挙型のラベル(名前)をSymbolで、第二引数には列挙型の値をIntegerでそれぞれ渡します。第二引数の列挙型の値は省略可能で、省略された場合0から自動的にインクリメントされます。
class AnimeType < Inum::Base define_enum :NyarukoSan, 0 define_enum :KiniroMosaic, 1 define_enum :KillMeBaby, 2 end
列挙型の使用方法
Inumを継承した列挙型は次のように使用出来ます。
p AnimeType::NyarukoSan # => NyarukoSan p AnimeType::NyarukoSan.to_i # => 0 # 数値や、シンボルから列挙型へのパーズが可能です。 type = AnimeType::parse(1) p type.equal?(AnimeType::KiniroMosaic) # => true # eql?メソッドを使用すると、比較対象のオブジェクトをパーズ後比較します。 p AnimeType::KillMeBaby.eql?('KillMeBaby') # => true # underscoreメソッドを使用すると、Enumのラベルをアンダースコアつなぎの文字列にします。 p AnimeType::NyarukoSan.underscore # => nyaruko_san # +と-の単項演算子も定義されています。 p AnimeType::NyarukoSan + 1 # => KiniroMosaic p AnimeType::KillMeBaby - 1 # => KiniroMosaic # eachによる繰り返しもできます。 AnimeType::each do |enum| p enum end
Enumerableモジュールをextendし、Comparableモジュールをincludeしていますので、mapなどのメソッドが利用可能です。
その他のメソッドについてはドキュメント(Class: Inum::Base — Documentation for alfa-jpn/inum (master))を参照してください。
その他の使い方
i18nを利用する
Enum(Inum)で to_t メソッドを利用することで、i18nでローカライズされた文字列を取得することが可能です。
次のような多言語化ファイルが存在した場合
ja: inum: anime_type: nyaruko_san: "這いよれ!ニャル子さん" kiniro_mosaic: "きんいろモザイク" kill_me_baby: "キルミーベイベー"
translateメソッドを用いると次の結果になります。
p AnimeType::NyarukoSan.translate # => 這いよれ!ニャル子さん p AnimeType::KiniroMosaic.translate # => きんいろモザイク p AnimeType::KillMeBaby.translate # => キルミーベイベー
i18nの辞書定義はデフォルトでは inum.{名前空間(モジュール名)}.{クラス名}.{メンバー名} を参照しますが、i18n_keyクラスメソッドをオーバーライドすることで参照先を書き換えることも可能です。
class AnimeType < Inum::Base define_enum :NyarukoSan, 0 define_enum :KiniroMosaic, 1 define_enum :KillMeBaby, 2 # i18n_keyをオーバーライド(内容はデフォルトの定義です。) def self.i18n_key(label) # underscoreメソッドでキャメルをアンダースコアつなぎにした上で、::を.に置き換えています。 Inum::Utils::underscore("inum::#{self.name}::#{label}") end end
Enumをクラスの変数に結びつける
DefineCheckMethodモジュールを利用すると、クラスの変数とEnumを関連付けし、判別用メソッドを自動的に定義することが可能です。
class Anime extend Inum::DefineCheckMethod attr_accessor :type define_check_method :type, AnimeType end
define_check_methodにカラム名と関連付けさせるInumを継承したEnumクラスを渡して判別用メソッドを自動的に定義しています。
anime = Anime.new anime.type = AnimeType::NyarukoSan p anime.nyaruko_san? # => true p anime.kiniro_mosaic? # => false # typeにはパーズ可能なメソッドなら何でも入ります。 anime.type = 1 p anime.kiniro_mosaic? # => true
また、define_check_methodの第三引数は省略可能オプションになっていて、prefixの設定が可能です。prefixを指定すると定義されるメソッドの先頭にprefixが付与されます。
class Anime extend Inum::DefineCheckMethod attr_accessor :type define_check_method :type, AnimeType, 'anime' end
prefixにanimeを指定すると、定義されるメソッドは次のようになります。
anime = Anime.new anime.type = AnimeType::NyarukoSan p anime.anime_nyaruko_san? # => true
おわりに
何か改善案あればpullrequest、issue気軽に投げてください、githubアカウントお持ちでない方はこのブログのコメント欄にもお気軽にどうぞ。
関連リンク
rbenv-railsを使用して、バージョン指定したrailsプロジェクトを30秒で生成する。
はじめに
以前、Bundlerを使ってローカルなRailsから新規プロジェクトを作る 方法を紹介しましたが、プロジェクトを生成するたびに実行するのは結構面倒です。そこでこの作業を自動的に行うシェルスクリプトを書いたので紹介します。 → 追記:せっかくなので rbenv のプラグインにしました。
インストール
rbenvのpluginsフォルダにrbenv-railsをgit cloneします。
# rbenvのプラグインフォルダにクローンする。
$ git clone https://github.com/alfa-jpn/rbenv-rails.git ~/.rbenv/plugins/rbenv-rails
インストールは以上です。
使い方
引数
rbenv rails <プロジェクト名> <railsのバージョン>
実行例
# rails 4.0.0 を使用して example_project を作成する。 rbenv rails example_project 4.0.0
もし、途中でsqlite3関連のエラーが出た際は libsqlite3-dev をパッケージマネージャからインストールしてください。
Playframework2.1のCSRF対策
はじめに
Playframework2.1では新機能としてCSRF対策が追加されたそうです。そもそもCSRF対策の機能はPlayframework1.x系には存在した機能ですが、2.0では機能そのものが消えていました。当然といえば当然の復活です。
公式の新機能紹介( Highlights )にも New Filter API and CSRF protection と大きく取り上げています。
新機能紹介のページを読み進めていくと、
The filters project that is part of the standard Play distribution contain a set of standard filter, such as the CSRF providing automatic token management against the CSRF security issue.
http://www.playframework.com/documentation/2.1.x/Highlights
素晴らしい!これでCSRF問題の苦悩から解放される・・・!と、感動すると同時にCSRFに関する説明はここで終わっていることに気づきます。
そうです、肝心なhow toや関連するリンクが一切ないのです!!
と、言うことで必死にぐぐって使えるようになったので結果をまとめておきます。
編集するファイル
project/build.scala
filtersを依存関係に追加します。appDependenciesの部分を下記のように編集します。
val appDependencies = Seq( // Add your project dependencies here, javaCore, javaJdbc, javaEbean, filters //←この行を追加 )
app/Global.java
アプリケーション全体の設定などを記述するGlobalクラスに、次のメソッドを追加します。Globalクラスは初期状態では存在しないので生成して、app直下(eclipse等では、default packages)に配置します。 Global.javaに関する詳しい記述は JavaGlobal を参照してください。
import play.*; import play.api.mvc.EssentialFilter; import play.filters.csrf.CSRFFilter; public class Global extends GlobalSettings { @Override @SuppressWarnings({"rawtypes", "unchecked"}) public <T extends EssentialFilter> Class<T>[] filters() { Class[] filters = {CSRFFilter.class}; return filters; } }
filters()でCSRFFilterのクラスを返すことで、リクエストをインターセプトした際に、フィルターが呼び出されて処理が行われるみたいです。
使い方
設定が終わったので、あとはフォームに設定を適用するだけで自動的にCSRF対策が行われます。具体的には helper関数を利用してフォームの宣言を行う際のactionに helper.CSRF関数を使用します。
@helper.form(action = helper.CSRF(routes.NyarukoController.postSanti)) { }
こんなかんじです。
Bundlerを使ってローカルなRailsから新規プロジェクトを作る
はじめに
本項では、ローカルなRailsから新規プロジェクトを作成し、gitで管理を行います。グローバルな環境にRailsをインストールしないことで、複数のバージョンのRailsを使い分けしやすくなります。
環境
- CentOS6.4
- rbenvを使って3分でRubyをインストールする方法まとめ。 - 波打際のブログさん を元にRubyがインストール済みであること。
Bundlerのインストール
Bundlerとは、RubyGemsのラッパーで、Gemのバージョン管理ツールです。Bundlerを使うことで複数の環境下で依存関係の解決が容易にできます。
# rbenvを使用してBundlerをインストール。 $ rbenv exec gem install bundler # rehashする。 $ rbenv rehash # インストールの確認。 $ rbenv exec bundle --version Bundler version 1.3.5
Bundlerを使ってローカルなRailsからプロジェクトを作る。
これ以降の手順を自動化するシェルスクリプトを書きました。詳細は バージョンを指定したRailsプロジェクトを自動で生成する。 - 波打際のブログさん を参照してください。
プロジェクトを生成したいディレクトリにGemfileを生成する。
$ bundle init
Gemfileの編集
initすると、カレントディレクトリにGemfileが生成されるので、それを編集してGemfileの依存関係にRailsを追加します。
$ echo 'gem "rails", "4.0.0"' >> Gemfile
Bundlerを利用してローカルにRailsをインストール
bundle installコマンドで、Gemfileの依存関係が解決されるのでRailsがインストールされます。
$ bundle install --path vendor/bundle
Railsプロジェクトをgitで管理できるようにする。
生成したRailsプロジェクトをgitで管理できるようにします。
依存関係の解決
生成されたRailsプロジェクトを使用可能にするために、bundle installを実行して、依存関係を解決します。
Nyaruko $ bundle install --path vendor/bundle
.gitignoreの設定
依存関係の解決のために生成されたファイル類は、gitにpushすべきものではないので、無視するように.gitignoreを修正します。
Nyaruko $ echo '/vendor/bundle' >> .gitignore
後はaddしてcommitすれば、gitで管理されたRailsプロジェクトの生成が完了です。
rbenvを使って3分でRubyをインストールする方法まとめ。
はじめに
最近モダンなrbenvを使って、3分の作業時間でRubyをインストールできたのでその方法をまとめておきます。
環境
- CentOS6.4
- SSHでgit(git-hub)が利用できる環境(本項では解説しません。)
rbenv
rbenvのインストール
インストールは非常に簡単で、公式リポジトリ(GitHub - rbenv/rbenv: Groom your app’s Ruby environment)をgitクローンしてbashにパスを通して、bash起動時に初期化されるように設定するだけで完了です。
# gitでローカルの ~/.rbenv にクローンを作る。 $ git clone git@github.com:rbenv/rbenv.git ~/.rbenv # bashにパスを通す。(ubuntuの場合は .bash_profile ではなく .profile) $ echo 'export RBENV_ROOT="$HOME/.rbenv"' >> ~/.bash_profile $ echo 'export PATH="$RBENV_ROOT/bin:$PATH"' >> ~/.bash_profile # bash起動時にrbenvの初期化を行うように設定。 $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile # シェルの再起動。 $ exec $SHELL -l # インストールできているか確認。 $ rbenv --version rbenv x.x.x-xx-xxxxxx # バージョンは環境によって異なります。
Apacheのログ設定まとめ
バーチャルホスト毎にログを記録する
バーチャルホストで複数のサービスを運営している際に、それぞれのホスト毎にログファイルを分けると管理が容易になります。
バーチャルホスト設定の編集
設定はとても簡単で、バーチャルホストの設定(conf/extra/httpd-vhosts.conf)にCustomLog(アクセスログ)とErrorLog(エラーログ)の設定を記述するだけです。
<VirtualHost *:80> DocumentRoot "/htdocs/nyaruko" ServerName www.nyaruko.cqc CustomLog logs/nyaruko_server_access_log common ErrorLog logs/nyaruko_server_error_log </VirtualHost>
ログのローテートを行う
日別や週別、月別などにログを分けて保存することをログのローテートと言います。ログのローテート方法はいくつか有りますが、今回はapacheに付属しているrotatelogsを利用する方法と、logrotateを利用する方法の2つを紹介します。
rotatelogsを利用する
apacheに付属しているログのローテートツールです。ログの出力先としてパイプすることでログを分けて管理することができます。
CustomLog "|/usr/local/apache2/bin/rotatelogs /usr/local/apache2/logs/nyaruko_access_log_%Y%m%d 86400 540" common
第一引数
ログの出力先を記述します。今回は /usr/local/apache2/logs/nyaruko_access_log_%Y%m%d と記述しているので、logs/nyaruko_access_log_20130814 という形式で出力されます。
第二引数
ログの周期を記述します。86400sec=24h
第三引数
時差を記述します。540min = 9h GMT(日本標準時)
logrotateを利用する
CentOS6.4に標準で搭載されているlogrotateを利用すると、apacheに限らず様々なログを容易にローテートできます。 /etc/logrotate.d ディレクトリ以下に、新たにファイルを作成して設定を記述することでローテートが可能です。今回は /etc/logrotate.d/httpd-nyaruko という名前でファイルを作成して設定を記述しました。
# 対象のログファイル /usr/local/apache2/logs/nyaruko_access_log { # ローテートの実行方式 # 移動&作成 : create # コピー : copy # コピー&削除 : copytruncate create # ログファイルが見つからない際の挙動 # エラーを吐く : nomissingok # エラーを吐かない : missingok missingok # ログファイルが空の際の動作 # 空でも実行 : ifempty # 空の場合は実行しない : noifempty notifempty # ローテーションの頻度 # 日単位 : daily # 週単位 : weekly # 月単位 : monthly daily # 生成されるログファイル数の上限 rotate 7 # 圧縮設定 # 圧縮 : compres # 未圧縮 : nocompress # 遅延圧縮 : delaycompress compres # ローテート時にスクリプト実行 sharedscripts postrotate /sbin/service httpd reload endscript }
apache+logrotateではローテート実行時に、sharedscripts設定を記述して再起動を行います。再起動しないでローテートする場合はcreateではなくcopytruncateを指定します。しかし、ローテート中にアクセスがあった場合のログが記録されないのでログに漏れが発生します。
nginx+logrotateの場合は再起動せずに、ログの漏れもなくローテートが実行できますが、その設定はまた次の機会に・・・。