PHPソース中の文字列を列挙する

もしくは[token_get_all()](http://www.php.net/manual/ja/function.token-get-all.php)の使い方のようなもの
-------


多言語化の調査のために作ったスクリプトです。やっていることは非常に単純で、カレントディレクトリ以下の拡張子.phpファイル全てに対して、PHP内蔵のレキサーでトークンに分割、多バイト文字列(T_CONSTANT_ENCAPSED_STRING=$a[0])を抽出してファイル名と行数と該当行を出力するだけです。

まずはソースを。

```php
<?php

function filterMultibyte($tok)
{
    return array_filter($tok, function($x)
    {
        if ($x[0] === T_CONSTANT_ENCAPSED_STRING //文字列か？
            && !mb_check_encoding($x[1], 'ASCII') //ASCII外?
        ){
            return true;
        }
        return false;
    });
}

function findDir($dir = '.')
{
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir,
        FilesystemIterator::CURRENT_AS_FILEINFO |
        FilesystemIterator::KEY_AS_PATHNAME |
        FilesystemIterator::SKIP_DOTS
        )
    );

    $list = array();
    foreach ($iterator as $fileinfo) { // $fileinfoはSplFiIeInfoオブジェクト
        if ($fileinfo->isFile() && preg_match('/\.php$/', $fileinfo->getPathname()) === 1) {
            $source = file_get_contents($fileinfo->getPathname());
            $source_array = explode("\n", $source); // 該当行を出力するために配列化して保存しておく
            $tok = token_get_all($source);
            $strs = filterMultibyte($tok);
            foreach($strs as $s) {
                fputcsv(STDOUT, array($fileinfo->getPathname(),$s[2],trim($source_array[$s[2]-1]),$s[1]));
            }
        }
    }
}

findDir(dirname(__FILE__));

//以下テスト用
define('DEFAULT_PATH', 'c:/Users/田島昭宇/Documents');
function foo() {
    $tplValue = "魍魎戦記摩陀羅";
}
```

※指摘をうけてソース直しました。

上記スクリプトが上記スクリプトを解析した結果は以下のとおりです。（左からフルパス、行番号、該当行の全て、文字列の全て）

```csv
/home/vagrant/xxx/strout.php,43,"define('DEFAULT_PATH', 'c:/Users/田島昭宇/Documents');",'c:/Users/田島昭宇/Documents'
/home/vagrant/xxx/strout.php,45,"$tplValue = ""魍魎戦記摩陀羅"";","""魍魎戦記摩陀羅"""
```

[RecursiveDirectoryIterator](http://www.php.net/manual/ja/class.recursivedirectoryiterator.php)と[RecursiveIteratorIterator](http://jp1.php.net/manual/ja/class.recursiveiteratoriterator.php)がfind(1)的なことをするSPLのクラス。

token_get_allはトークン1個を```[トークンの種類, トークンそのものの文字列, 行番号]```として、二重配列で返します。token_get_allはシングルクォート、ダブルクォート、ヒアドキュメントも一様に文字定数として字句解析するみたいなので、とりあえず今回の用途には非常に楽でした。

（おまけ）Smartyテンプレート用の同じようなもの
------

行単位でチェックして出力するだけです。HTML/Smartyコメントは抽出したくないので、一応無視するようにしています。

```php
<?php

function filterMultibyte($ary) {
  $res = array();
  foreach ($ary as $k => $v) {
    if (preg_match('/[^\d\w\s_\/\'"=\?\.:,\*<>\-\(\)#\+\\\|\$\^\[\]@%!{};~&`]/', $v) === 1) {
      $item = array();
      $item[1] = $v;
      $item[2] = $k + 1;
      $res[] = $item;
    }
  }
  return $res;
}

function replaceToCRLF($str) {
  $i = substr_count($str[0], "\n");
  #echo $str[0],$i,"\n";
  return $i === 0 ? " "  : str_repeat(" \n", $i);
}

function replaceCommentToReturn($str) {
  $str = preg_replace_callback('/(\<\!\-\-.*?\-\-\>)/s', "replaceToCRLF", $str);
  $str = preg_replace_callback('/({\*.*?\*})/s', "replaceToCRLF", $str);
  return $str;
}

function findDir($dir = '.') {
  $iterator = new RecursiveDirectoryIterator($dir);
  $iterator = new RecursiveIteratorIterator($iterator);

  $matches = array();
  $list = array();
  foreach ($iterator as $fileinfo) { // $fileinfoはSplFiIeInfoオブジェクト
    if ($fileinfo->isFile() && preg_match('/\.tpl$/', $fileinfo->getPathname()) === 1) {
      $source = file_get_contents($fileinfo->getPathname());
      $source1 = replaceCommentToReturn($source);
      $source_array = explode("\n", $source1);
      $strs = filterMultibyte($source_array);
      foreach($strs as $s) {
        $matches[] = $fileinfo->getPathname();
        echo sprintf("%s:%d:%s\n", $fileinfo->getPathname(),$s[2],trim($source_array[$s[2]-1]));
      }
    }
  }

  echo "\n";
  $matches = array_unique($matches);

  foreach($matches as $m) {
    echo $m,"\n";
  }
}

findDir();

```


(追記)xgettextで全文字列定数を抜き出す
------

文字列を抜き出すだけならxgettextでも可能です。デフォルトだと他言語化作業済みの文字列（`_()`）だけ抽出しますが、-aをつけると全ての文字列を抽出してくれます。

```shell-session
$ find ./ -name '*.php' | xargs xgettext -j --language=php --from-code=UTF-8 -a
```

## 既存コードの他言語化について

以前多言語化を行った場合の作業としては以下でした。テンプレートの多言語化については、おそらくテンプレートエンジンに機能があると思うでそちらに従ってください。

1. 対象ファイルの調査
2. 原文抽出
3. 翻訳
4. コード変更。setlocale等初期化コードの追加と、文字列を全部_()で囲む作業。
5. poファイルの作成
6. msgfmtしてデプロイ

実際にi18n作業をするときは、[Poedit](http://poedit.net/)を使うと楽です。

多言語化全般ですが、文字列結合しているコードは非常に翻訳しづらいです。（`"計" . $price . "円"`のような）同様に、文字列内での変数展開はsprintfに置き換えるべきです。

元々ソースコード中のブラウザに出力する文字列を日本語でを書いていたプロダクトであれば、文字列は日本語のままにしておいて、gettextで英語メッセージに差し替えるほうが楽だと思います（日本語が正、他の言語が副）未翻訳の文字列はそのまま出力される為、英語にしておけば日本語より読める人が多いとは思いますが、作業量的にはかなりめんどくさくなります。