バリデーションメッセージを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ファイルを作っておけば、このエラーメッセージのフィールド名も日本語などになります(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への記事投稿


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