Source for file I18n.php

Documentation is available at I18n.php

  1. <?php
  2. // vim: foldmethod=marker
  3. /**
  4.  *  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: 54aec858beccb8f96493cd60340c0166a6515278 $
  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. {
  21.     /**#@+
  22.      *  @access protected
  23.      */
  24.  
  25.     /** @protected    array  解析済みトークン  */
  26.     protected $tokens = array();
  27.  
  28.     /** @protected    string   ロケール名  */
  29.     protected $locale;
  30.  
  31.     /** @protected    boolean  gettext利用フラグ  */
  32.     protected $use_gettext;
  33.  
  34.     /** @protected    boolean  既存ファイルが存在した場合にtrue */
  35.     protected $file_exists;
  36.  
  37.     /** @protected    string   実行時のUnix Time(ファイル名生成用) */
  38.     protected $time;
  39.  
  40.     /**
  41.      *  プロジェクトのメッセージカタログを生成する
  42.      *
  43.      *  @access public
  44.      *  @param  string  $locale         生成するカタログのロケール
  45.      *  @param  int     $use_gettext    gettext 使用フラグ
  46.      *                                   true ならgettext のカタログ生成
  47.      *                                   false ならEthna組み込みのカタログ生成
  48.      *  @param  array   $ext_dirs       走査する追加のディレクトリの配列
  49.      *  @return true|Ethna_Error       true:成功 Ethna_Error:失敗
  50.      */
  51.     function generate($locale$use_gettext$ext_dirs array())
  52.     {
  53.         $this->time = time();
  54.         $this->locale = $locale;
  55.         $this->use_gettext = $use_gettext;
  56.  
  57.         $outfile_path $this->_get_output_file();
  58.  
  59.         //
  60.         //  既存ファイルが存在した場合は、以下の動きをする
  61.         //
  62.         //  1. Ethna 組み込みのカタログの場合、既存のiniファイル
  63.         //  の中身を抽出し、既存の翻訳を可能な限りマージする
  64.         //  2. gettext 利用の場合は、新たにファイルを作らせ、
  65.         //  既存翻訳とのマージは msgmergeプログラムを使わせる
  66.         //
  67.         if ($this->file_exists{
  68.             $msg ($this->use_gettext)
  69.                  ? ("[NOTICE]: Message catalog file already exists! "
  70.                   . "CREATING NEW FILE ...\n"
  71.                   . "You can run msgmerge program to merge translation.\n"
  72.                    )
  73.                  : ("[NOTICE]: Message catalog file already exists!\n"
  74.                   . "This is overwritten and existing translation is merged automatically.\n");
  75.              print "\n-------------------------------\n"
  76.                  . $msg
  77.                  . "-------------------------------\n\n";
  78.         }
  79.  
  80.         // app ディレクトリとテンプレートディレクトリを
  81.         // 再帰的に走査する。ユーザから指定があればそれも走査
  82.         $app_dir $this->ctl->getDirectory('app');
  83.         $template_dir $this->ctl->getDirectory('template');
  84.         $scan_dir array(
  85.             $app_dir"${template_dir}/${locale}",
  86.         );
  87.         $scan_dir array_merge($scan_dir$ext_dirs);
  88.  
  89.         //  ディレクトリを走査
  90.         foreach ($scan_dir as $dir{
  91.             if (is_dir($dir=== false{
  92.                 Ethna::raiseNotice("$dir is not Directory."E_GENERAL);
  93.                 continue;
  94.             }
  95.             $r $this->_analyzeDirectory($dir);
  96.             if (Ethna::isError($r)) {
  97.                 return $r;
  98.             }
  99.         }
  100.  
  101.         //  解析済みトークンを元に、カタログファイルを生成
  102.         $r $this->_generateFile();
  103.         if (Ethna::isError($r)) {
  104.             return $r;
  105.         }
  106.  
  107.         $true true;
  108.         return $true;
  109.     }
  110.  
  111.     /**
  112.      *  出力ファイル名を取得します。
  113.      *
  114.      *  @access private
  115.      *  @return string  出力ファイル名
  116.      */
  117.     function _get_output_file()
  118.     {
  119.         $locale_dir $this->ctl->getDirectory('locale');
  120.         $ext ($this->use_gettext'po' 'ini';
  121.         $filename $this->locale . ".${ext}";
  122.         $new_filename NULL;
  123.  
  124.         $outfile_path "${locale_dir}/"
  125.                       . $this->locale
  126.                       . "/LC_MESSAGES/$filename";
  127.  
  128.         $this->file_exists = (file_exists($outfile_path));
  129.         if ($this->file_exists && $this->use_gettext{
  130.             $new_filename $this->locale . '_' $this->time . ".${ext}";
  131.             $outfile_path "${locale_dir}/"
  132.                           . $this->locale
  133.                           . "/LC_MESSAGES/$new_filename";
  134.         }
  135.  
  136.         return $outfile_path;
  137.     }
  138.  
  139.     /**
  140.      *  指定されたディレクトリを再帰的に走査します。
  141.      *
  142.      *  @access protected
  143.      *  @param  string  $dir     走査対象ディレクトリ
  144.      *  @return true|Ethna_Errortrue:成功 Ethna_Error:失敗
  145.      */
  146.     function _analyzeDirectory($dir)
  147.     {
  148.         $dh opendir($dir);
  149.         if ($dh == false{
  150.             return Ethna::raiseWarning(
  151.                        "unable to open Directory: $dir"E_GENERAL
  152.                    );
  153.         }
  154.  
  155.         //  走査対象はテンプレートとPHPスクリプト
  156.         $php_ext $this->ctl->getExt('php');
  157.         $tpl_ext $this->ctl->getExt('tpl');
  158.         $r NULL;
  159.  
  160.         //  ディレクトリなら再帰的に走査
  161.         //  ファイルならトークンを解析する
  162.         while(($file readdir($dh)) !== false{
  163.             if (is_dir("$dir/$file")) {
  164.                 if (strpos($file'.'!== 0{  // 隠しファイルは対象外
  165.                    $r $this->_analyzeDirectory("$dir/$file");
  166.                 }
  167.             else {
  168.                 if (preg_match("#\.${php_ext}\$#i"$file0{
  169.                     $r $this->_analyzeFile("$dir/$file");
  170.                 }
  171.                 if (preg_match("#\.${tpl_ext}\$#i"$file0{
  172.                     $r $this->_analyzeTemplate("$dir/$file");
  173.                 }
  174.             }
  175.             if (Ethna::isError($r)) {
  176.                 return $r;
  177.             }
  178.         }
  179.  
  180.         closedir($dh);
  181.         return true;
  182.     }
  183.  
  184.     /**
  185.      *  指定されたPHPスクリプトを調べ、メッセージ処理関数の呼び出し
  186.      *  箇所を取得します。
  187.      *
  188.      *  NOTICE: このメソッドは、指定ファイルがPHPスクリプト
  189.      *          (テンプレートファイル)として正しいものかどう
  190.      *          かはチェックしません。
  191.      *
  192.      *  @access protected
  193.      *  @param  string  $file     走査対象ファイル
  194.      *  @return true|Ethna_Errortrue:成功 Ethna_Error:失敗
  195.      */
  196.     function _analyzeFile($file)
  197.     {
  198.         $file_path realpath($file);
  199.         printf("Analyzing file ... %s\n"$file);
  200.  
  201.         //  ファイルを開けないならエラー
  202.         $fp @fopen($file_path'r');
  203.         if ($fp === false{
  204.             return Ethna::raiseWarning(
  205.                        "unable to open file: $file"E_GENERAL
  206.                    );
  207.         }
  208.         fclose($fp);
  209.  
  210.         //  トークンを全て取得。
  211.         $file_tokens token_get_all(
  212.                            file_get_contents($file_path)
  213.                        );
  214.         $token_num count($file_tokens);
  215.         $in_et_function false;
  216.  
  217.         //  アクションディレクトリは特別扱いするため、それ
  218.         //  を取得
  219.         $action_dir $this->ctl->getActionDir(GATEWAY_WWW);
  220.  
  221.         //  トークンを走査し、関数呼び出しを解析する
  222.         for ($i 0$i $token_num$i++{
  223.  
  224.             $token $file_tokens[$i];
  225.             $token_idx false;
  226.             $token_str NULL;
  227.             $token_linenum false;
  228.  
  229.             //   面倒を見るのは、トークンの場合のみ
  230.             //   単純な文字列は読み飛ばす
  231.             if (is_array($token)) {
  232.                 $token_idx array_shift($token);
  233.                 $token_str array_shift($token);
  234.  
  235.                 //  PHP 5.2.2 以降のみ行番号を取得可能
  236.                 //  @see http://www.php.net/token_get_all
  237.                 if (version_compare(PHP_VERSION'5.2.2'>= 0{
  238.                     $token_linenum array_shift($token);
  239.                 }
  240.                 //  i18n 呼び出し関数の場合、フラグを立てる
  241.                 if ($token_idx == T_STRING && $token_str == '_et'{
  242.                     $in_et_function true;
  243.                     continue;
  244.                 }
  245.                 //  i18n 呼び出しの後、定数文字列が来たら、
  246.                 //  それを引数と看做す。PHPの文法的にvalid
  247.                 //  か否かはこのルーチンでは面倒を見ない
  248.                 if ($in_et_function == true
  249.                  && $token_idx == T_CONSTANT_ENCAPSED_STRING{
  250.                     $token_str substr($token_str1);     // 最初のクォートを除く
  251.                     $token_str substr($token_str0-1)// 最後のクォートを除く
  252.                     $this->tokens[$file_path][array(
  253.                                                       'token_str' => $token_str,
  254.                                                       'linenum' => $token_linenum,
  255.                                                       'translation' => ''
  256.                                                   );
  257.                     $in_et_function false;
  258.                     continue;
  259.                 }
  260.             }
  261.         }
  262.  
  263.         //  アクションスクリプト の場合は、
  264.         //  ActionForm の $form メンバ解析
  265.         $php_ext $this->ctl->getExt('php');
  266.         $action_dir_regex $action_dir;
  267.         if (ETHNA_OS_WINDOWS{
  268.             $action_dir_regex str_replace('\\''\\\\'$action_dir);
  269.             $action_dir_regex str_replace('/''\\\\'$action_dir_regex);
  270.         }
  271.         if (preg_match("#$action_dir_regex#"$file_path)
  272.         && !preg_match("#.*Test\.${php_ext}$#"$file_path)) {
  273.             $this->_analyzeActionForm($file_path);
  274.         }
  275.  
  276.         //  Ethna組み込みのメッセージカタログであれば翻訳をマージする
  277.         $this->_mergeEthnaMessageCatalog();
  278.  
  279.         return true;
  280.     }
  281.  
  282.     /**
  283.      *  指定されたPHPスクリプトを調べ、メッセージ処理関数の呼び出し
  284.      *  箇所を取得します。
  285.      *
  286.      *  NOTICE: このメソッドは、指定ファイルがPHPスクリプトとして
  287.      *          正しいものかどうかはチェックしません。
  288.      *
  289.      *  @access protected
  290.      *  @param  string  $file_path  走査対象ファイル
  291.      *  @return true|Ethna_Errortrue:成功 Ethna_Error:失敗
  292.      */
  293.     function _analyzeActionForm($file_path)
  294.     {
  295.         //   アクションスクリプトのトークンを取得
  296.         $tokens token_get_all(
  297.                       file_get_contents($file_path)
  298.                   );
  299.  
  300.         //   クラスのトークンのみを取り出す
  301.         $class_names array();
  302.         $class_started false;
  303.         for ($i 0$i count($tokens)$i++{
  304.             $token $tokens[$i];
  305.             if (is_array($token)) {
  306.                 $token_name array_shift($token);
  307.                 $token_str array_shift($token);
  308.  
  309.                 if ($token_name == T_CLASS{  //  クラス定義開始
  310.                     $class_started true;
  311.                     continue;
  312.                 }
  313.                 //    T_CLASS の直後に来た T_STRING をクラス名と見做す
  314.                 if ($class_started === true && $token_name == T_STRING{
  315.                     $class_started false;
  316.                     $class_names[$token_str;
  317.                 }
  318.             }
  319.         }
  320.  
  321.         //  アクションフォームのクラス名を特定
  322.         $af_classname NULL;
  323.         foreach ($class_names as $name{
  324.             $action_name $this->ctl->actionFormToName($name);
  325.             if (!empty($action_name)) {
  326.                 $af_classname $name;
  327.                 break;
  328.             }
  329.         }
  330.  
  331.         //  特定したクラスをインスタンス化し、フォーム定義を解析する
  332.         printf("    Analyzing ActionForm class ... %s\n"$af_classname);
  333.         require_once $file_path;
  334.         $af new $af_classname($this->ctl);
  335.         $form_def $af->getDef();
  336.         $translatable_code array('name''required_error''type_error''min_error',
  337.                                    'max_error''regexp_error'
  338.                              );
  339.         foreach ($form_def as $key => $def{
  340.             //    対象となるのは name, *_error
  341.             //    但し、定義されていた場合のみ対象にする
  342.             //    @see http://ethna.jp/ethna-document-dev_guide-form-message.html
  343.             foreach ($translatable_code as $code{
  344.                 if (array_key_exists($code$def)) {
  345.                     $token_str $def[$code];
  346.                     $this->tokens[$file_path][array(
  347.                                                       'token_str' => $token_str,
  348.                                                       'linenum' => false// 行番号は取得しない
  349.                                                       'translation' => ''
  350.                                                   );
  351.                 }
  352.             }
  353.         }
  354.     }
  355.  
  356.     /**
  357.      *  指定されたテンプレートファイルを調べ、メッセージ処理関数
  358.      *  の呼び出し箇所を取得します。
  359.      *
  360.      *  @access protected
  361.      *  @param  string  $file    走査対象ファイル
  362.      *  @return true|Ethna_Errortrue:成功 Ethna_Error:失敗
  363.      */
  364.     function _analyzeTemplate($file)
  365.     {
  366.         //  デフォルトはSmartyのテンプレートと看做す
  367.         $renderer $this->ctl->getRenderer();
  368.         $engine $renderer->getEngine();
  369.         $engine_name get_class($engine);
  370.         if (strncasecmp('Smarty'$engine_name6!== 0{
  371.             return Ethna::raiseError(
  372.                        "You seems to use template engine other than Smarty ... : $engine_name"
  373.                    );
  374.         }
  375.  
  376.         printf("Analyzing Template file ... %s\n"$file);
  377.  
  378.         //  use smarty internal function :)
  379.         $compile_path $engine->_get_compile_path($file);
  380.         $compile_result NULL;
  381.         if ($engine->_is_compiled($file$compile_path)
  382.          || $engine->_compile_resource($file$compile_path)) {
  383.             $compile_result file_get_contents($compile_path);
  384.         }
  385.  
  386.         if (empty($compile_result)) {
  387.             return Ethna::raiseError(
  388.                        "could not compile template file : $file"
  389.                    );
  390.         }
  391.  
  392.         //  コンパイル済みのテンプレートを解析する
  393.         $tokens token_get_all($compile_result);
  394.  
  395.         for ($i 0$i count($tokens)$i++{
  396.             $token $tokens[$i];
  397.             if (is_array($token)) {
  398.                 $token_name array_shift($token);
  399.                 $token_str array_shift($token);
  400.  
  401.                 if ($token_name == T_STRING
  402.                  && strcmp($token_str'smarty_modifier_i18n'=== 0{
  403.                     $i18n_str $this->_find_template_i18n($tokens$i);
  404.                     if (!empty($i18n_str)) {
  405.                         $i18n_str substr($i18n_str1);     // 最初のクォートを除く
  406.                         $i18n_str substr($i18n_str0-1)// 最後のクォートを除く
  407.                         $this->tokens[$file][array(
  408.                                                       'token_str' => $i18n_str,
  409.                                                       'linenum' => false,
  410.                                                       'translation' => '',
  411.                                                  );
  412.                     }
  413.                 }
  414.             }
  415.         }
  416.     }
  417.  
  418.     /**
  419.      *  テンプレートのトークンを逆順に走査し、
  420.      *  翻訳トークンを取得します。
  421.      *
  422.      *  @param $tokens 解析対象トークン
  423.      *  @param $index  インデックス
  424.      *  @access private
  425.      */
  426.     function _find_template_i18n($tokens$index)
  427.     {
  428.         for ($j $index$j 0$j--{
  429.             $tmp_token $tokens[$j];
  430.  
  431.             if (is_array($tmp_token)) {
  432.                 $tmp_token_name array_shift($tmp_token);
  433.                 $tmp_token_str array_shift($tmp_token);
  434.                 if ($tmp_token_name == T_CONSTANT_ENCAPSED_STRING
  435.                  && !preg_match('#^["\']i18n["\']$#'$tmp_token_str)) {
  436.                     $prev_token $tokens[$j 1];
  437.                     if (!is_array($prev_token&& $prev_token == '='{
  438.                         return $tmp_token_str;
  439.                     }
  440.                 }
  441.             }
  442.         }
  443.         return NULL;
  444.     }
  445.  
  446.     /**
  447.      *  Ethna組み込みのメッセージカタログファイルを、上書き
  448.      *  する場合にマージします。
  449.      *
  450.      *  @access private
  451.      */
  452.     function _mergeEthnaMessageCatalog()
  453.     {
  454.         if (!($this->file_exists && !$this->use_gettext)) {
  455.             return;
  456.         }
  457.         $outfile_path $this->_get_output_file();
  458.  
  459.         $i18n $this->ctl->getI18N();
  460.         $existing_catalog $i18n->parseEthnaMsgCatalog($outfile_path);
  461.  
  462.         foreach ($this->tokens as $file_path => $tokens{
  463.             for ($i 0$i count($tokens)$i++{
  464.                 $token $tokens[$i];
  465.                 $token_str $token['token_str'];
  466.                 if (array_key_exists($token_str$existing_catalog)) {
  467.                     $this->tokens[$file_path][$i]['translation'$existing_catalog[$token_str];
  468.                 }
  469.             }
  470.         }
  471.     }
  472.  
  473.     /**
  474.      *  解析済みのメッセージ処理関数の情報を元に、カタログファイ
  475.      *  ルを生成します。 生成先は以下のパスになる。
  476.      *  [appid]/[locale_dir]/[locale_name]/LC_MESSAGES/[locale_name].[ini|po]
  477.      *
  478.      *  @access protected
  479.      *  @param  string  $locale         生成するカタログのロケール
  480.      *  @param  int     $use_gettext    gettext 使用フラグ
  481.      *                                   true ならgettext のカタログ生成
  482.      *                                   false ならEthna組み込みのカタログ生成
  483.      *  @return true|Ethna_Errortrue:成功 Ethna_Error:失敗
  484.      */
  485.     function _generateFile($skel null$entity null$macro null$overwrite false)
  486.     {
  487.         $outfile_path $this->_get_output_file();
  488.  
  489.         $skel ($this->use_gettext)
  490.               ? 'locale/skel.msg.po'
  491.               : 'locale/skel.msg.ini';
  492.         $resolved $this->_resolveSkelfile($skel);
  493.         if ($resolved === false{
  494.             return Ethna::raiseError("skelton file [%s] not found.\n"$skel);
  495.         else {
  496.             $skel $resolved;
  497.         }
  498.  
  499.         $contents file_get_contents($skel);
  500.         $macro['project_id'$this->ctl->getAppId();
  501.         $macro['locale_name'$this->locale;
  502.         $macro['now_date'strftime('%Y-%m-%d %H:%M%z');
  503.         foreach ($macro as $k => $v{
  504.             $contents preg_replace("/{\\\$$k}/"$v$contents);
  505.         }
  506.  
  507.         //  generate file contents
  508.         foreach ($this->tokens as $file_path => $tokens{
  509.             $is_first_loop false;
  510.             foreach ($tokens as $token{
  511.                 $token_str $token['token_str'];
  512.                 $token_line $token['linenum'];
  513.                 $token_line ($token_line !== false":${token_line}'';
  514.                 $translation $token['translation'];
  515.  
  516.                 if ($this->use_gettext{
  517.                     $contents .= (
  518.                         "#: ${file_path}${token_line}\n"
  519.                       . "msgid \"${token_str}\"\n"
  520.                       . "msgstr \"${translation}\"\n\n"
  521.                     );
  522.                 else {
  523.                     if ($is_first_loop === false{
  524.                         $contents .= "\n; ${file_path}\n";
  525.                         $is_first_loop true;
  526.                     }
  527.                     $contents .= "\"${token_str}\" = \"${translation}\"\n";
  528.                 }
  529.             }
  530.         }
  531.  
  532.         //  finally write.
  533.         $outfile_dir dirname($outfile_path);
  534.         if (!is_dir($outfile_dir)) {
  535.             Ethna_Util::mkdir($outfile_dir0755);
  536.         }
  537.         $wfp @fopen($outfile_path"w");
  538.         if ($wfp == null{
  539.             return Ethna::raiseError("unable to open file: $outfile_path");
  540.         }
  541.         if (fwrite($wfp$contents=== false{
  542.             fclose($wfp);
  543.             return Ethna::raiseError("unable to write contents to $outfile_path");
  544.         }
  545.         fclose($wfp);
  546.         printf("Message catalog template successfully created [%s]\n"$outfile_path);
  547.  
  548.         return true;
  549.     }
  550. }
  551. // }}}

Documentation generated on Fri, 11 Nov 2011 03:58:19 +0900 by phpDocumentor 1.4.3