OSDN Git Service

- create ethna command idea branche
[ethna/ethna.git] / Idea_Plugin_Extlib / class / Plugin / Generator / I18n.php
1 <?php
2 // vim: foldmethod=marker
3 /**
4  *  Ethna_Plugin_Generator_I18n.php
5  *
6  *  @author     Yoshinari Takaoka <takaoka@beatcraft.com> 
7  *  @license    http://www.opensource.org/licenses/bsd-license.php The BSD License
8  *  @package    Ethna
9  *  @version    $Id$
10  */
11
12 // {{{ Ethna_Plugin_Generator_I18n
13 /**
14  *  i18n 向け、メッセージカタログ生成クラスのスーパークラス
15  *
16  *  @author     Yoshinari Takaoka <takaoka@beatcraft.com> 
17  *  @access     public
18  *  @package    Ethna
19  */
20 class Ethna_Plugin_Generator_I18n extends Ethna_Plugin_Generator
21 {
22     /**#@+
23      *  @access protected 
24      */
25
26     /** @var    array  解析済みトークン  */ 
27     var $tokens = array();
28
29     /** @var    string   ロケール名  */ 
30     var $locale;
31
32     /** @var    boolean  gettext利用フラグ  */ 
33     var $use_gettext;
34    
35     /** @var    boolean  既存ファイルが存在した場合にtrue */ 
36     var $file_exists;
37
38     /** @var    string   実行時のUnix Time(ファイル名生成用) */ 
39     var $time;
40
41     /**
42      *  プロジェクトのメッセージカタログを生成する
43      *
44      *  @access public
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:失敗
51      */
52     function &generate($locale, $use_gettext, $ext_dirs = array())
53     {
54         $this->time = time();
55         $this->locale = $locale;
56         $this->use_gettext = $use_gettext;
57
58         $outfile_path = $this->_get_output_file();
59
60         //
61         //  既存ファイルが存在した場合は、以下の動きをする
62         //
63         //  1. Ethna 組み込みのカタログの場合、既存のiniファイル
64         //  の中身を抽出し、既存の翻訳を可能な限りマージする
65         //  2. gettext 利用の場合は、新たにファイルを作らせ、
66         //  既存翻訳とのマージは msgmergeプログラムを使わせる 
67         //
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"
73                    )
74                  : ("[NOTICE]: Message catalog file already exists!\n"
75                   . "This is overwritten and existing translation is merged automatically.\n");
76              print "\n-------------------------------\n"
77                  . $msg
78                  . "-------------------------------\n\n"; 
79         }
80
81         // app ディレクトリとテンプレートディレクトリを
82         // 再帰的に走査する。ユーザから指定があればそれも走査
83         $app_dir = $this->ctl->getDirectory('app');
84         $template_dir = $this->ctl->getDirectory('template');
85         $scan_dir = array(
86             $app_dir, "${template_dir}/${locale}",
87         );
88         $scan_dir = array_merge($scan_dir, $ext_dirs);
89
90         //  ディレクトリを走査
91         foreach ($scan_dir as $dir) {
92             if (is_dir($dir) === false) {
93                 Ethna::raiseNotice("$dir is not Directory.", E_GENERAL);
94                 continue;
95             }
96             $r = $this->_analyzeDirectory($dir);
97             if (Ethna::isError($r)) {
98                 return $r;
99             }
100         } 
101
102         //  解析済みトークンを元に、カタログファイルを生成
103         $r = $this->_generateFile();
104         if (Ethna::isError($r)) {
105             return $r;
106         }
107
108         $true = true;
109         return $true;
110     }
111
112     /**
113      *  出力ファイル名を取得します。 
114      *
115      *  @access private
116      *  @return string  出力ファイル名
117      */
118     function _get_output_file()
119     {
120         $locale_dir = $this->ctl->getDirectory('locale');
121         $ext = ($this->use_gettext) ? 'po' : 'ini';
122         $filename = $this->locale . ".${ext}";
123         $new_filename = NULL;
124
125         $outfile_path = "${locale_dir}/"
126                       . $this->locale
127                       . "/LC_MESSAGES/$filename";
128
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}/"
133                           . $this->locale
134                           . "/LC_MESSAGES/$new_filename";
135         }
136
137         return $outfile_path;
138     }
139  
140     /**
141      *  指定されたディレクトリを再帰的に走査します。
142      *
143      *  @access protected
144      *  @param  string  $dir     走査対象ディレクトリ 
145      *  @return true|Ethna_Error true:成功 Ethna_Error:失敗
146      */
147     function _analyzeDirectory($dir)
148     {
149         $dh = opendir($dir);
150         if ($dh == false) {
151             return Ethna::raiseWarning(
152                        "unable to open Directory: $dir", E_GENERAL
153                    );
154         }
155
156         //  走査対象はテンプレートとPHPスクリプト 
157         $php_ext = $this->ctl->getExt('php');
158         $tpl_ext = $this->ctl->getExt('tpl');
159         $r = NULL;
160     
161         //  ディレクトリなら再帰的に走査
162         //  ファイルならトークンを解析する
163         while(($file = readdir($dh)) !== false) {
164             if (is_dir("$dir/$file")) {
165                 if (strpos($file, '.') !== 0) {  // 隠しファイルは対象外
166                    $r = $this->_analyzeDirectory("$dir/$file");
167                 }
168             } else {
169                 if (preg_match("#\.${php_ext}\$#i", $file) > 0) {
170                     $r = $this->_analyzeFile("$dir/$file");
171                 }
172                 if (preg_match("#\.${tpl_ext}\$#i", $file) > 0) {
173                     $r = $this->_analyzeTemplate("$dir/$file");
174                 }
175             }
176             if (Ethna::isError($r)) {
177                 return $r;
178             }
179         }
180
181         closedir($dh);
182         return true;
183     }
184
185     /**
186      *  指定されたPHPスクリプトを調べ、メッセージ処理関数の呼び出し
187      *  箇所を取得します。
188      *
189      *  NOTICE: このメソッドは、指定ファイルがPHPスクリプト
190      *          (テンプレートファイル)として正しいものかどう
191      *          かはチェックしません。 
192      *
193      *  @access protected
194      *  @param  string  $file     走査対象ファイル
195      *  @return true|Ethna_Error true:成功 Ethna_Error:失敗
196      */
197     function _analyzeFile($file)
198     {
199         $file_path = realpath($file);
200         printf("Analyzing file ... %s\n", $file);
201
202         //  ファイルを開けないならエラー
203         $fp = @fopen($file_path, 'r');
204         if ($fp === false) {
205             return Ethna::raiseWarning(
206                        "unable to open file: $file", E_GENERAL
207                    );
208         }
209         fclose($fp);
210
211         //  トークンを全て取得。
212         $file_tokens = token_get_all(
213                            file_get_contents($file_path)
214                        );
215         $token_num = count($file_tokens);
216         $in_et_function = false;   
217
218         //  アクションディレクトリは特別扱いするため、それ
219         //  を取得
220         $action_dir = $this->ctl->getActionDir(GATEWAY_WWW);
221
222         //  トークンを走査し、関数呼び出しを解析する
223         for ($i = 0; $i < $token_num; $i++) {
224
225             $token = $file_tokens[$i];
226             $token_idx = false;
227             $token_str = NULL;
228             $token_linenum = false;
229
230             //   面倒を見るのは、トークンの場合のみ
231             //   単純な文字列は読み飛ばす
232             if (is_array($token)) {
233                 $token_idx = array_shift($token);
234                 $token_str = array_shift($token);
235
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);
240                 } 
241                 //  i18n 呼び出し関数の場合、フラグを立てる
242                 if ($token_idx == T_STRING && $token_str == '_et') {
243                     $in_et_function = true;
244                     continue; 
245                 }
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,
256                                                       'translation' => ''
257                                                   );
258                     $in_et_function = false;
259                     continue;
260                 }
261             }
262         }
263
264         //  アクションスクリプト の場合は、
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);
271         }
272         if (preg_match("#$action_dir_regex#", $file_path)
273         && !preg_match("#.*Test\.${php_ext}$#", $file_path)) {
274             $this->_analyzeActionForm($file_path); 
275         }
276
277         //  Ethna組み込みのメッセージカタログであれば翻訳をマージする
278         $this->_mergeEthnaMessageCatalog();
279
280         return true; 
281     }
282
283     /**
284      *  指定されたPHPスクリプトを調べ、メッセージ処理関数の呼び出し
285      *  箇所を取得します。
286      *
287      *  NOTICE: このメソッドは、指定ファイルがPHPスクリプトとして
288      *          正しいものかどうかはチェックしません。 
289      *
290      *  @access protected
291      *  @param  string  $file_path  走査対象ファイル
292      *  @return true|Ethna_Error true:成功 Ethna_Error:失敗
293      */
294     function _analyzeActionForm($file_path)
295     {
296         //   アクションスクリプトのトークンを取得 
297         $tokens = token_get_all(
298                       file_get_contents($file_path)
299                   );
300
301         //   クラスのトークンのみを取り出す
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);
309                 
310                 if ($token_name == T_CLASS) {  //  クラス定義開始
311                     $class_started = true;
312                     continue;
313                 }
314                 //    T_CLASS の直後に来た T_STRING をクラス名と見做す
315                 if ($class_started === true && $token_name == T_STRING) {
316                     $class_started = false;
317                     $class_names[] = $token_str;
318                 } 
319             }
320         }
321
322         //  アクションフォームのクラス名を特定
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;
328                 break;
329             }
330         }
331
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'
339                              );
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, // 行番号は取得しない
350                                                       'translation' => ''
351                                                   );
352                 }
353             }
354         } 
355     }
356
357     /**
358      *  指定されたテンプレートファイルを調べ、メッセージ処理関数
359      *  の呼び出し箇所を取得します。
360      *
361      *  @access protected
362      *  @param  string  $file    走査対象ファイル 
363      *  @return true|Ethna_Error true:成功 Ethna_Error:失敗
364      */
365     function _analyzeTemplate($file)
366     {
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"
374                    ); 
375         }
376
377         printf("Analyzing Template file ... %s\n", $file);
378
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);
385         }
386
387         if (empty($compile_result)) {
388             return Ethna::raiseError(
389                        "could not compile template file : $file"
390                    ); 
391         }
392
393         //  コンパイル済みのテンプレートを解析する
394         $tokens = token_get_all($compile_result);
395
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);
401                 
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,
410                                                       'linenum' => false,
411                                                       'translation' => '',
412                                                  );
413                     }
414                 }
415             }
416         }
417     }
418
419     /**
420      *  テンプレートのトークンを逆順に走査し、
421      *  翻訳トークンを取得します。
422      *
423      *  @param $tokens 解析対象トークン
424      *  @param $index  インデックス
425      *  @access private 
426      */
427     function _find_template_i18n($tokens, $index)
428     {
429         for ($j = $index; $j > 0; $j--) {
430             $tmp_token = $tokens[$j];
431
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;
440                     } 
441                 }
442             }
443         }
444         return NULL;
445     }
446
447     /**
448      *  Ethna組み込みのメッセージカタログファイルを、上書き
449      *  する場合にマージします。
450      *
451      *  @access private
452      */
453     function _mergeEthnaMessageCatalog()
454     {
455         if (!($this->file_exists && !$this->use_gettext)) {
456             return;
457         }
458         $outfile_path = $this->_get_output_file();
459
460         $i18n = $this->ctl->getI18N();
461         $existing_catalog = $i18n->parseEthnaMsgCatalog($outfile_path);
462
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];
469                 }
470             }
471         }
472     }
473
474     /**
475      *  解析済みのメッセージ処理関数の情報を元に、カタログファイ
476      *  ルを生成します。 生成先は以下のパスになる。
477      *  [appid]/[locale_dir]/[locale_name]/LC_MESSAGES/[locale_name].[ini|po]
478      *
479      *  @access protected 
480      *  @param  string  $locale         生成するカタログのロケール
481      *  @param  int     $use_gettext    gettext 使用フラグ
482      *                                  true ならgettext のカタログ生成
483      *                                  false ならEthna組み込みのカタログ生成
484      *  @return true|Ethna_Error true:成功 Ethna_Error:失敗
485      */
486     function _generateFile()
487     {
488         $outfile_path = $this->_get_output_file();
489
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);
496         } else {
497             $skel = $resolved;
498         }
499
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);
506         }
507
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'];
516
517                 if ($this->use_gettext) {
518                     $contents .= (
519                         "#: ${file_path}${token_line}\n"
520                       . "msgid \"${token_str}\"\n"
521                       . "msgstr \"${translation}\"\n\n"
522                     ); 
523                 } else {
524                     if ($is_first_loop === false) {
525                         $contents .= "\n; ${file_path}\n";
526                         $is_first_loop = true;
527                     }
528                     $contents .= "\"${token_str}\" = \"${translation}\"\n";
529                 }
530             }
531         } 
532
533         //  finally write.
534         $outfile_dir = dirname($outfile_path);
535         if (!is_dir($outfile_dir)) {
536             Ethna_Util::mkdir($outfile_dir, 0755);
537         }
538         $wfp = @fopen($outfile_path, "w");
539         if ($wfp == null) {
540             return Ethna::raiseError("unable to open file: $outfile_path");
541         }
542         if (fwrite($wfp, $contents) === false) {
543             fclose($wfp);
544             return Ethna::raiseError("unable to write contents to $outfile_path");
545         }
546         fclose($wfp);
547         printf("Message catalog template successfully created [%s]\n", $outfile_path);
548
549         return true;
550     }
551 }
552 // }}}
553
554 ?>