[cakePHP][model][バリデーション] バリデーションメッセージをDryにしつつ国際化

CakePHP1.2.3を利用しています。
CakePHPのバリデーションエラーメッセージは、各モデルに書いたりしますが、ここではgettextの__()を使った国際化の記述ができません。CakeBookにそのための回避策が一応書いてありました。
http://book.cakephp.org/ja/view/163/Localization-in-CakePHP
下記の記述をapp_model.phpに入れとけば、エラーメッセージ出力時に__()を付けてくれるので、言語ごとにエラーメッセージが切り替わります。

function invalidate($field, $value = true) {
return parent::invalidate($field, __($value, true));
}

上記が一番楽なパターンではあるのですが、国際化対応する箇所をコマンド一発で抽出してくれる"cake i18n"コマンドだと、バリデーションエラーメッセージは抽出されません(エラーメッセージ自体は__()で記述されていないので)
ちなみにcake i18nの使い方は下記が参考になります。
http://cakephp.seesaa.net/article/87269708.html

cake i18nコマンドでもエラーメッセージが抽出できる方法はないかとIRCでK1LoWさんと話してて、それを実現する簡単なapp_modelを作ったので公開します。
ダウンロードはこちらから
http://cake.eizoku.com/source/validation_i18n.zip

機能は下記の通りです(最初の2つはvalidationのメッセージ出力をDRYにしてみる by Writing Some Codeなどで実現されています)

  • 各モデルのバリデーションエラー定義を一箇所で管理可能
  • メッセージ中の数値などは、printf系の変換指定が可能(%dなど)
  • エラーメッセージは定義箇所に__()を使って、国際化の定義が可能(cake i18nで取得対象となる)
  • オプション:app_modelにまとめたエラーメッセージ定義を各モデルで上書き可能
  • オプション:エラーメッセージにフィールド名を自動付与

最後のエラーメッセージにフィールド名は、画面の上部にエラーメッセージをまとめて出力したい場合に、フィールド名がないと、どの項目のエラーか分からないので、そのための機能です。フィールド名も国際化して出力します。

ソースコード
基本の流れは、_getDefaultErrorMessagesI18n()メソッド内で連想配列にエラーメッセージをgettext形式で定義し、それをbeforeValidate()時にモデルのバリデーションエラーメッセージにセットしているだけです。
追記
この記事で説明するコードは色々と機能を付け足した後のやつで、もっとシンプルな初期段階のソースコードはここにあります。こっちの方が理解しやすいかもしれません。

<?php

class AppModel extends Model {

/**
* Concatenate a field name with each validation error message in replaceValidationErrorMessagesI18n().
* Field name is set with gettext __()
* true: Concatenate
* false: not Concatenate
*
* @var boolean
* @access protected
*/
var $_withFieldName = false;

/**
* Error messages
*
* @var array
* @access protected
*/
var $_error_messages = array();

/**
* Define default validation error messages
* $default_error_messages can include gettext __() value.
*
* @return array
* @access protected
*/
function _getDefaultErrorMessagesI18n(){
//Write Default Error Message
$default_error_messages = array(
'require' => 'Please be sure to input.',
'email_invalid' => __('Invalid Email address.',true),
'between' => __('Between %2$d and %3$d characters.',true),
);

return $default_error_messages;
}

/**
* Set validation error messages.
*
* To change default validation error messages,
* set $add_error_message in each model.
*
* @param array $add_error_message
* @param boolean $all_change_flag
* true: change all default validation error messages
* false: merge $add_error_message with default validation error messages
* @access public
*/
function setErrorMessageI18n( $add_error_message = null, $all_change_flag=false ) {

$default_error_messages = $this->_getDefaultErrorMessagesI18n();

if( !empty( $add_error_message ) && is_array( $add_error_message ) ){
if( $all_change_flag ){
$default_error_messages = $add_error_message;
}else{
$default_error_messages = array_merge( $default_error_messages, $add_error_message );
}
$this->_error_messages = $default_error_messages;

}elseif( empty($this->_error_messages) ){
$this->_error_messages = $default_error_messages;
}

}

/**
* get validation error messages
*
* @return array
* @access protected
*/
function _getErrorMessageI18n(){
return $this->_error_messages;
}

/**
* Replace validation error messages for i18n
*
* @access public
*/
function replaceValidationErrorMessagesI18n(){
$this->setErrorMessageI18n();

foreach( $this->validate as $fieldname => $ruleSet ){
foreach( $ruleSet as $rule => $rule_info ){

$rule_option = array();
if(!empty($this->validate[$fieldname][$rule]['rule'])) {
$rule_option = $this->validate[$fieldname][$rule]['rule'];
}

$error_message_list = $this->_getErrorMessageI18n();
$error_message = ( array_key_exists($rule, $error_message_list ) ? $error_message_list[$rule] : null ) ;

if( !empty( $error_message ) ) {
$this->validate[$fieldname][$rule]['message'] = vsprintf($error_message, $rule_option);

}elseif( !empty($this->validate[$fieldname][$rule]['message']) ){
$this->validate[$fieldname][$rule]['message'] =
__( $this->validate[$fieldname][$rule]['message'], true);
}

if( $this->_withFieldName && !empty($this->validate[$fieldname][$rule]['message']) ){
$this->validate[$fieldname][$rule]['message'] =
__( $fieldname ,true) . ' : ' . $this->validate[$fieldname][$rule]['message'];

}
}
}
}

function beforeValidate(){
$this->replaceValidationErrorMessagesI18n();
return true;
}

}

使い方 (基本編)
各モデルのバリデーション定義では、下記の規則に従って

var $validate = array(
'項目名' => array(
'規則名' => array( 'rule' => array( 'バリデーション関数' ) )
)
);

今までと同じように定義します(下記サンプル)

var $validate = array(
'email' => array(
"email_invalid" => array(
'rule' => VALID_EMAIL,
'required' => true,
),
),
)

app_modelの下記の箇所にシステムで共通して使うエラーメッセージ(今後はデフォルトエラーメッセージと呼ぶことにします)を連想配列で記述します。配列のキーは各モデルで定義するバリデーションの規則名になります。

<?php
function _getDefaultErrorMessagesI18n(){
//Write Default Error Message
$default_error_messages = array(
'require' => 'Please be sure to input.',
'email_invalid' => __('Invalid Email address.',true),
'between' => __('Between %2$d and %3$d characters.',true),
);
return $default_error_messages;
}

今回の例だと、email_invalidのルールでエラーになった場合は、「Invalid Email address.」が国際化されて表示されます。betweenのように何文字以上という数値が変わるものも、上記例のようにすれば対応できます。

使い方 (各モデルごとにエラーメッセージを変えたい)
デフォルトエラーメッセージは、上記のようにすれば一元管理可能ですが、同じバリデーションルールでも、あるモデルではエラーメッセージを変えたい場合は、モデル側のファイルを下記のようにして上書き(マージ)が可能です

<?php
class User extends AppModel {
function beforeValidate(){
$error_messages = array(
'email_invalid' => __('Oh, Invalid Email address!!!',true),

);
$this->setErrorMessageI18n($error_messages, false);
parent::beforeValidate();
return true;
}
}

$this->setErrorMessageI18n()をapp_modelのbeforeValidate前に( parent::beforeValidate()前に )実行すれば、上書きできます。この場合だと、Userモデルの場合のみ、メールのエラーメッセージがapp_modelで定義したものから変わります。
この方法は、デフォルトエラーメッセージを残しつつ上書きしたいものだけマージする方法ですが、デフォルトエラーメッセージを全て使いたくない場合は、マージではなくて入れ替えが可能です。$this->setErrorMessageI18n()の第2引数にtrueを入れればそれが実現可能です。

使い方 (エラーメッセージにフィールド名を自動付与)
エラーメッセージを画面上部に一括で出したい場合などのために、エラーメッセージ毎にフィールド名の自動付与が可能です。app_modelの下記のプロパティをtrueにしてください。

var $_withFieldName = true;

下記のような画面になります。viewファイル側で__('email')とかフィールド名を定義して、poファイルを作っておけば、このエラーメッセージのフィールド名もJapaneseなどになります(email:という箇所がメール:などに変わります)。

参考
今回の実装にあたり、下記を参考にさせていただきました。
validationのメッセージ出力をDRYにしてみる by Writing Some Code

CakePHPによる実践Webアプリケーション開発」の本(85ページあたり)

今後の予定
とりあえずプラグイン化しているので、プラグインとしてメンテしていく予定です。下記のgithubのmodels/behaviors/validation_error_i18n.phpがそれです。
http://github.com/ichikaway/cakeplus/tree

  • Task
    • デフォルトメッセージの充実
    • TestCaseの作成
    • bakeryへの記事投稿

もっと良いアイディアとか、使い方が分からないとかあれば、何でも良いのでコメント下さい。