2 // vim: foldmethod=marker
4 * Ethna_Plugin_Generator_I18n.php
6 * @author Yoshinari Takaoka <takaoka@beatcraft.com>
7 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
12 // {{{ Ethna_Plugin_Generator_I18n
14 * i18n 向け、メッセージカタログ生成クラスのスーパークラス
16 * @author Yoshinari Takaoka <takaoka@beatcraft.com>
20 class Ethna_Plugin_Generator_I18n extends Ethna_Plugin_Generator
26 /** @var array 解析済みトークン */
27 var $tokens = array();
29 /** @var string ロケール名 */
32 /** @var boolean gettext利用フラグ */
35 /** @var boolean 既存ファイルが存在した場合にtrue */
38 /** @var string 実行時のUnix Time(ファイル名生成用) */
42 * プロジェクトのメッセージカタログを生成する
45 * @param string $locale 生成するカタログのロケール
46 * @param int $use_gettext gettext 使用フラグ
47 * true ならgettext のカタログ生成
48 * false ならEthna組み込みのカタログ生成
49 * @param array $ext_dirs 走査する追加のディレクトリの配列
50 * @return true|Ethna_Error true:成功 Ethna_Error:失敗
52 function &generate($locale, $use_gettext, $ext_dirs = array())
55 $this->locale = $locale;
56 $this->use_gettext = $use_gettext;
58 $outfile_path = $this->_get_output_file();
61 // 既存ファイルが存在した場合は、以下の動きをする
63 // 1. Ethna 組み込みのカタログの場合、既存のiniファイル
64 // の中身を抽出し、既存の翻訳を可能な限りマージする
65 // 2. gettext 利用の場合は、新たにファイルを作らせ、
66 // 既存翻訳とのマージは msgmergeプログラムを使わせる
68 if ($this->file_exists) {
69 $msg = ($this->use_gettext)
70 ? ("[NOTICE]: Message catalog file already exists! "
71 . "CREATING NEW FILE ...\n"
72 . "You can run msgmerge program to merge translation.\n"
74 : ("[NOTICE]: Message catalog file already exists!\n"
75 . "This is overwritten and existing translation is merged automatically.\n");
76 print "\n-------------------------------\n"
78 . "-------------------------------\n\n";
81 // app ディレクトリとテンプレートディレクトリを
82 // 再帰的に走査する。ユーザから指定があればそれも走査
83 $app_dir = $this->ctl->getDirectory('app');
84 $template_dir = $this->ctl->getDirectory('template');
86 $app_dir, "${template_dir}/${locale}",
88 $scan_dir = array_merge($scan_dir, $ext_dirs);
91 foreach ($scan_dir as $dir) {
92 if (is_dir($dir) === false) {
93 Ethna::raiseNotice("$dir is not Directory.", E_GENERAL);
96 $r = $this->_analyzeDirectory($dir);
97 if (Ethna::isError($r)) {
102 // 解析済みトークンを元に、カタログファイルを生成
103 $r = $this->_generateFile();
104 if (Ethna::isError($r)) {
116 * @return string 出力ファイル名
118 function _get_output_file()
120 $locale_dir = $this->ctl->getDirectory('locale');
121 $ext = ($this->use_gettext) ? 'po' : 'ini';
122 $filename = $this->locale . ".${ext}";
123 $new_filename = NULL;
125 $outfile_path = "${locale_dir}/"
127 . "/LC_MESSAGES/$filename";
129 $this->file_exists = (file_exists($outfile_path));
130 if ($this->file_exists && $this->use_gettext) {
131 $new_filename = $this->locale . '_' . $this->time . ".${ext}";
132 $outfile_path = "${locale_dir}/"
134 . "/LC_MESSAGES/$new_filename";
137 return $outfile_path;
141 * 指定されたディレクトリを再帰的に走査します。
144 * @param string $dir 走査対象ディレクトリ
145 * @return true|Ethna_Error true:成功 Ethna_Error:失敗
147 function _analyzeDirectory($dir)
151 return Ethna::raiseWarning(
152 "unable to open Directory: $dir", E_GENERAL
156 // 走査対象はテンプレートとPHPスクリプト
157 $php_ext = $this->ctl->getExt('php');
158 $tpl_ext = $this->ctl->getExt('tpl');
163 while(($file = readdir($dh)) !== false) {
164 if (is_dir("$dir/$file")) {
165 if (strpos($file, '.') !== 0) { // 隠しファイルは対象外
166 $r = $this->_analyzeDirectory("$dir/$file");
169 if (preg_match("#\.${php_ext}\$#i", $file) > 0) {
170 $r = $this->_analyzeFile("$dir/$file");
172 if (preg_match("#\.${tpl_ext}\$#i", $file) > 0) {
173 $r = $this->_analyzeTemplate("$dir/$file");
176 if (Ethna::isError($r)) {
186 * 指定されたPHPスクリプトを調べ、メッセージ処理関数の呼び出し
189 * NOTICE: このメソッドは、指定ファイルがPHPスクリプト
190 * (テンプレートファイル)として正しいものかどう
194 * @param string $file 走査対象ファイル
195 * @return true|Ethna_Error true:成功 Ethna_Error:失敗
197 function _analyzeFile($file)
199 $file_path = realpath($file);
200 printf("Analyzing file ... %s\n", $file);
203 $fp = @fopen($file_path, 'r');
205 return Ethna::raiseWarning(
206 "unable to open file: $file", E_GENERAL
212 $file_tokens = token_get_all(
213 file_get_contents($file_path)
215 $token_num = count($file_tokens);
216 $in_et_function = false;
218 // アクションディレクトリは特別扱いするため、それ
220 $action_dir = $this->ctl->getActionDir(GATEWAY_WWW);
222 // トークンを走査し、関数呼び出しを解析する
223 for ($i = 0; $i < $token_num; $i++) {
225 $token = $file_tokens[$i];
228 $token_linenum = false;
232 if (is_array($token)) {
233 $token_idx = array_shift($token);
234 $token_str = array_shift($token);
236 // PHP 5.2.2 以降のみ行番号を取得可能
237 // @see http://www.php.net/token_get_all
238 if (version_compare(PHP_VERSION, '5.2.2') >= 0) {
239 $token_linenum = array_shift($token);
241 // i18n 呼び出し関数の場合、フラグを立てる
242 if ($token_idx == T_STRING && $token_str == '_et') {
243 $in_et_function = true;
246 // i18n 呼び出しの後、定数文字列が来たら、
247 // それを引数と看做す。PHPの文法的にvalid
248 // か否かはこのルーチンでは面倒を見ない
249 if ($in_et_function == true
250 && $token_idx == T_CONSTANT_ENCAPSED_STRING) {
251 $token_str = substr($token_str, 1); // 最初のクォートを除く
252 $token_str = substr($token_str, 0, -1); // 最後のクォートを除く
253 $this->tokens[$file_path][] = array(
254 'token_str' => $token_str,
255 'linenum' => $token_linenum,
258 $in_et_function = false;
265 // ActionForm の $form メンバ解析
266 $php_ext = $this->ctl->getExt('php');
267 $action_dir_regex = $action_dir;
268 if (ETHNA_OS_WINDOWS) {
269 $action_dir_regex = str_replace('\\', '\\\\', $action_dir);
270 $action_dir_regex = str_replace('/', '\\\\', $action_dir_regex);
272 if (preg_match("#$action_dir_regex#", $file_path)
273 && !preg_match("#.*Test\.${php_ext}$#", $file_path)) {
274 $this->_analyzeActionForm($file_path);
277 // Ethna組み込みのメッセージカタログであれば翻訳をマージする
278 $this->_mergeEthnaMessageCatalog();
284 * 指定されたPHPスクリプトを調べ、メッセージ処理関数の呼び出し
287 * NOTICE: このメソッドは、指定ファイルがPHPスクリプトとして
288 * 正しいものかどうかはチェックしません。
291 * @param string $file_path 走査対象ファイル
292 * @return true|Ethna_Error true:成功 Ethna_Error:失敗
294 function _analyzeActionForm($file_path)
296 // アクションスクリプトのトークンを取得
297 $tokens = token_get_all(
298 file_get_contents($file_path)
302 $class_names = array();
303 $class_started = false;
304 for ($i = 0; $i < count($tokens); $i++) {
305 $token = $tokens[$i];
306 if (is_array($token)) {
307 $token_name = array_shift($token);
308 $token_str = array_shift($token);
310 if ($token_name == T_CLASS) { // クラス定義開始
311 $class_started = true;
314 // T_CLASS の直後に来た T_STRING をクラス名と見做す
315 if ($class_started === true && $token_name == T_STRING) {
316 $class_started = false;
317 $class_names[] = $token_str;
323 $af_classname = NULL;
324 foreach ($class_names as $name) {
325 $action_name = $this->ctl->actionFormToName($name);
326 if (!empty($action_name)) {
327 $af_classname = $name;
332 // 特定したクラスをインスタンス化し、フォーム定義を解析する
333 printf(" Analyzing ActionForm class ... %s\n", $af_classname);
334 require_once $file_path;
335 $af = new $af_classname($this->ctl);
336 $form_def = $af->getDef();
337 $translatable_code = array('name', 'required_error', 'type_error', 'min_error',
338 'max_error', 'regexp_error'
340 foreach ($form_def as $key => $def) {
341 // 対象となるのは name, *_error
342 // 但し、定義されていた場合のみ対象にする
343 // @see http://ethna.jp/ethna-document-dev_guide-form-message.html
344 foreach ($translatable_code as $code) {
345 if (array_key_exists($code, $def)) {
346 $token_str = $def[$code];
347 $this->tokens[$file_path][] = array(
348 'token_str' => $token_str,
349 'linenum' => false, // 行番号は取得しない
358 * 指定されたテンプレートファイルを調べ、メッセージ処理関数
362 * @param string $file 走査対象ファイル
363 * @return true|Ethna_Error true:成功 Ethna_Error:失敗
365 function _analyzeTemplate($file)
367 // デフォルトはSmartyのテンプレートと看做す
368 $renderer =& $this->ctl->getRenderer();
369 $engine =& $renderer->getEngine();
370 $engine_name = get_class($engine);
371 if (strncasecmp('Smarty', $engine_name, 6) !== 0) {
372 return Ethna::raiseError(
373 "You seems to use template engine other than Smarty ... : $engine_name"
377 printf("Analyzing Template file ... %s\n", $file);
379 // use smarty internal function :)
380 $compile_path = $engine->_get_compile_path($file);
381 $compile_result = NULL;
382 if ($engine->_is_compiled($file, $compile_path)
383 || $engine->_compile_resource($file, $compile_path)) {
384 $compile_result = file_get_contents($compile_path);
387 if (empty($compile_result)) {
388 return Ethna::raiseError(
389 "could not compile template file : $file"
393 // コンパイル済みのテンプレートを解析する
394 $tokens = token_get_all($compile_result);
396 for ($i = 0; $i < count($tokens); $i++) {
397 $token = $tokens[$i];
398 if (is_array($token)) {
399 $token_name = array_shift($token);
400 $token_str = array_shift($token);
402 if ($token_name == T_STRING
403 && strcmp($token_str, 'smarty_modifier_i18n') === 0) {
404 $i18n_str = $this->_find_template_i18n($tokens, $i);
405 if (!empty($i18n_str)) {
406 $i18n_str = substr($i18n_str, 1); // 最初のクォートを除く
407 $i18n_str = substr($i18n_str, 0, -1); // 最後のクォートを除く
408 $this->tokens[$file][] = array(
409 'token_str' => $i18n_str,
420 * テンプレートのトークンを逆順に走査し、
423 * @param $tokens 解析対象トークン
424 * @param $index インデックス
427 function _find_template_i18n($tokens, $index)
429 for ($j = $index; $j > 0; $j--) {
430 $tmp_token = $tokens[$j];
432 if (is_array($tmp_token)) {
433 $tmp_token_name = array_shift($tmp_token);
434 $tmp_token_str = array_shift($tmp_token);
435 if ($tmp_token_name == T_CONSTANT_ENCAPSED_STRING
436 && !preg_match('#^["\']i18n["\']$#', $tmp_token_str)) {
437 $prev_token = $tokens[$j - 1];
438 if (!is_array($prev_token) && $prev_token == '=') {
439 return $tmp_token_str;
448 * Ethna組み込みのメッセージカタログファイルを、上書き
453 function _mergeEthnaMessageCatalog()
455 if (!($this->file_exists && !$this->use_gettext)) {
458 $outfile_path = $this->_get_output_file();
460 $i18n = $this->ctl->getI18N();
461 $existing_catalog = $i18n->parseEthnaMsgCatalog($outfile_path);
463 foreach ($this->tokens as $file_path => $tokens) {
464 for ($i = 0; $i < count($tokens); $i++) {
465 $token = $tokens[$i];
466 $token_str = $token['token_str'];
467 if (array_key_exists($token_str, $existing_catalog)) {
468 $this->tokens[$file_path][$i]['translation'] = $existing_catalog[$token_str];
475 * 解析済みのメッセージ処理関数の情報を元に、カタログファイ
476 * ルを生成します。 生成先は以下のパスになる。
477 * [appid]/[locale_dir]/[locale_name]/LC_MESSAGES/[locale_name].[ini|po]
480 * @param string $locale 生成するカタログのロケール
481 * @param int $use_gettext gettext 使用フラグ
482 * true ならgettext のカタログ生成
483 * false ならEthna組み込みのカタログ生成
484 * @return true|Ethna_Error true:成功 Ethna_Error:失敗
486 function _generateFile()
488 $outfile_path = $this->_get_output_file();
490 $skel = ($this->use_gettext)
491 ? 'locale/skel.msg.po'
492 : 'locale/skel.msg.ini';
493 $resolved = $this->_resolveSkelfile($skel);
494 if ($resolved === false) {
495 return Ethna::raiseError("skelton file [%s] not found.\n", $skel);
500 $contents = file_get_contents($skel);
501 $macro['project_id'] = $this->ctl->getAppId();
502 $macro['locale_name'] = $this->locale;
503 $macro['now_date'] = strftime('%Y-%m-%d %H:%M%z');
504 foreach ($macro as $k => $v) {
505 $contents = preg_replace("/{\\\$$k}/", $v, $contents);
508 // generate file contents
509 foreach ($this->tokens as $file_path => $tokens) {
510 $is_first_loop = false;
511 foreach ($tokens as $token) {
512 $token_str = $token['token_str'];
513 $token_line = $token['linenum'];
514 $token_line = ($token_line !== false) ? ":${token_line}" : '';
515 $translation = $token['translation'];
517 if ($this->use_gettext) {
519 "#: ${file_path}${token_line}\n"
520 . "msgid \"${token_str}\"\n"
521 . "msgstr \"${translation}\"\n\n"
524 if ($is_first_loop === false) {
525 $contents .= "\n; ${file_path}\n";
526 $is_first_loop = true;
528 $contents .= "\"${token_str}\" = \"${translation}\"\n";
534 $outfile_dir = dirname($outfile_path);
535 if (!is_dir($outfile_dir)) {
536 Ethna_Util::mkdir($outfile_dir, 0755);
538 $wfp = @fopen($outfile_path, "w");
540 return Ethna::raiseError("unable to open file: $outfile_path");
542 if (fwrite($wfp, $contents) === false) {
544 return Ethna::raiseError("unable to write contents to $outfile_path");
547 printf("Message catalog template successfully created [%s]\n", $outfile_path);