Google Translation API v3 を Node で使ってみた


# はじめに
案件で使う機会があったので忘備録的な感じで記載していこうと思います。
諸々間違い、認識違いがあるかもしれませんが生暖かく見守っていただければと思います。


# 実装イメージ
* XServer X10プランを使用します。
* NodeでWebサーバ起動して云々はXServer上で実装するのは難しいのでphpで受けてコマンド呼び出しで動かします。
* 翻訳結果はjson形式で返却します。
  * 本来であれば翻訳結果をjavascriptが受け取りうまくゴニョゴニョしてhtml上で表現するが正しいと思いますが、残念ながら自分はPHPerでjavascriptが得意じゃないのでこの部分は割愛させていただきたく。

# 環境
* XServer X10プランで契約できるレンタルサーバ
* php
  * 7.2.17
* perl
  * 5.16
* nodebrew
  * 1.0.1
* Node
 * v12.10.0
* npm
  * 6.10.3

※php, perlのバージョンはXServerのデフォルト設定（2019/12/04時点）
※nodebrewは自分が試した時点での最新。
※Nodeはnodebrewにて指定する感じ、npmはnodeインストール時に一緒に入るもの。

# ディレクトリ構成
* XServerの基本構造をそのまま使用します。
* 翻訳機能についてはドキュメントルート直下には作りませんでした。直URLでアクセスされたときのことを考えたくなかったからです。
* 各ディレクトリ・ファイルについては後述で説明しますが、最終的には下記のような構成になります。

```markdown
/xxxxx.xsrv.jp/
    public_html/
        index.html
        translation.php
    node/
        json/
        node_modules/
        package-lock.json
        translation.js
        gcpprj-example-xxxxxx-yyyyyyyyyyyy.json
```

# nodebrew, node.js, npmのインストール
* [hokaccha/nodebrew](https://github.com/hokaccha/nodebrew)で記載があるコマンドをそのまま実行します。

```bash
$ cd ~/
$ wget git.io/nodebrew
$ perl nodebrew setup
 :
 :
$ vi .bashrc
export PATH=$HOME/.nodebrew/current/bin:$PATH   ※この行を.bashrcの末尾に追加
$ source ~/.bashrc
$ nodebrew help    ※このコマンドを叩くと諸々出てくれば成功
nodebrew 1.0.1
 :
 :
$ nodebrew install v12.10.0  ※これでnode.js v12.10.0がインストールされる
$ nodebrew use v12.10.0      ※これでnode.js v12.10.0を使うよと宣言する感じ
$ node -v
v12.10.0
$ npm -v
6.10.3
```


# Google Cloud Platformの設定及びGoogleTranslateAPIを使用するための準備
* GCP側でプロジェクトを作成します。
* 「請求アカウント」を作成し上記で作ったプロジェクトに紐付けてください。
* 使用APIに「Cloud Translation API」を追加してください。
* この[クイックスタート](https://cloud.google.com/translate/docs/quickstart-client-libraries-v3?hl=ja)のページの「プロジェクトをセットアップする」ボタンを押下しプロジェクトを指定すると「JSONとしての秘密鍵」がDownloadされます。
上記の「gcpprj-example-xxxxxx-yyyyyyyyyyyy.json」というのが「JSONとしての秘密鍵」になります。
* [クイックスタート](https://cloud.google.com/translate/docs/quickstart-client-libraries-v3?hl=ja)にはその後「環境変数なんちゃら」という記載がありますが、とりあえず今のところはスルー。
*  [クイックスタート](https://cloud.google.com/translate/docs/quickstart-client-libraries-v3?hl=ja)の次の手順「クライアント ライブラリのインストール」では「NODE.JS」を選択するとnpmのインストールコマンドが出てくるのでそれを上記のnodeディレクトリ配下で実行します。
  * nodeディレクトリはデフォルトでは無いのでmkdirで作っていきながら作業する感じです。 

```bash
$ cd ~/
$ cd xxxxx.xsrv.jp
$ mkdir node
$ cd node
$ mkdir json
$ npm install --save @google-cloud/translate   <== これがクイックスタート上に出てきたインストールコマンド
$ ls -1F
json/
node_modules/
package-lock.json
```

* このnodeディレクトリ配下に先程Downloadされた「JSONとしての秘密鍵」をFTP等でアップロードします。

```bash
$ cd ~/xxxxx.xsrv.jp/node
$ ls -1F
json/
node_modules/
package-lock.json
gcpprj-example-xxxxxx-yyyyyyyyyyyy.json
```

* [クイックスタート](https://cloud.google.com/translate/docs/quickstart-client-libraries-v3?hl=ja)の次の手順「テキストの翻訳」で、NODE.JSのサンプルコードをそのまんまコピーして上記で作ったnodeディレクトリにtranslation.jsとして保存。
* サンプルコードのままだと、翻訳対象文字列がコードに直書き状態なので引数で受け取って可変できるようにします。その時、翻訳対象文字列を直接引数で指定してしまうと「コマンドインジェクション」が発生する可能性が高く怖いので翻訳対象文字列を引数のjsonファイルから取得するような仕様に変更します。

* translation.jsの中身は下記の通り

```javascript
const projectId = 'XXXXX-yyyyy-zzzzz';  // GCP上のプロジェクトIDを記載
const location = 'global';

// jsonから読み込むように修正
const jsonPath = process.argv[2];
const json = require(jsonPath);
const text = json.text;

// Imports the Google Cloud Translation library
const {TranslationServiceClient} = require('@google-cloud/translate').v3beta1;

// Instantiates a client
const translationClient = new TranslationServiceClient();
async function translateText() {
  // Construct request
  const request = {
    parent: translationClient.locationPath(projectId, location),
    contents: [text],
    mimeType: 'text/plain', // mime types: text/plain, text/html
    sourceLanguageCode: 'ja',   // 日本語から。
    targetLanguageCode: 'en',   // 英語に。
  };

  // Run request
  const [response] = await translationClient.translateText(request);

  for (const translation of response.translations) {
    console.log(`${translation.translatedText}`);
  }
}

translateText();
```

* ディレクトリ内はこんな感じ

```bash
$ cd ~/xxxxx.xsrv.jp/node
$ ls -1F
json/  ※翻訳対象の文字列をjson形式にして保存する場所
node_modules/
package-lock.json
gcpprj-example-xxxxxx-yyyyyyyyyyyy.json
translation.js
```

* 試しにこの状態で下記のコマンドを叩いて強引に実行してみます。
* jsonで設定している日本語は「私は本を持っている」です。

```bash
$ cd ~/xxxxx.xsrv.jp/node
$ echo "{\"text\":\"\u79c1\u306f\u672c\u3092\u6301\u3063\u3066\u3044\u308b\"}" > json/example.json  ※ダミーjson作成
$ node translation.js /home/xxx/xxxxx.xsrv.jp/node/json/example.json
(node:395938) UnhandledPromiseRejectionWarning: Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync ...
     :
     :
(node:398472) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:398472) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

$
```

* うまく動きません。「環境変数なんちゃら」をスルーしたためです。
* 「環境変数を設定する」、「翻訳対象の文字列を取得し、jsonファイルを作る」という処理はこの後に記載するphpロジックにて実装することを想定しています。



# php, htmlで残りの処理を作成する。

* ドキュメントルート直下にindex.htmlを設置、翻訳対象文字列を入力するフォームを作ります。

```html
<html>
  <head>
    <meta charset="UTF-8" />
    <title>日本語を英語に翻訳する</title>
  </head>
  <boby>
    <form action="translation.php" method="post">
      日→英、翻訳テキスト：<input type="text" name="target">　<input type="submit" value="翻訳">
    </form>
  </body>
</html>
```

* translation.phpで下記機能を実装します。
  * 翻訳対象文字列の取得
  * jsonファイルの生成
  * 環境変数のセット
  * translation.jsの実行、結果取得
  * json形式にして結果を出力

```php
define('HOME_DIR',       '/home/xxx/');  // 各自環境のものを入れる
define('NODE',           HOME_DIR   . '.nodebrew/current/bin/node');  // nodeのインストール状況では違うものになるはず
define('DOMAIN_DIR',     HOME_DIR   . 'xxxxx.xsrv.jp/');
define('NODE_DIR',       DOMAIN_DIR . 'node/');
define('JSON_DIR',       NODE_DIR   . 'json/');
define('GCP_JSON',       NODE_DIR   . 'gcpprj-example-xxxxxx-yyyyyyyyyyyy.json');
define('TRANSLATION_JS', NODE_DIR   . 'translation.js');


// 引数が無い場合は何もしない
if (!isset($_POST['target']) || $_POST['target'] === '') {
    header("Location: https://xxxxx.xsrv.jp/index.html");
    exit();
}

// jsonファイルを生成
define('TEXT_RANGE', implode(array_merge(range(0, 9), range('a', 'z'), range('A', 'Z'))));
$rndtxt   = substr(str_shuffle(TEXT_RANGE), 0, 16);
$jsonfile = sprintf("%s%s.json",JSON_DIR, $rndtxt);
$json_ary['text'] = $_POST['target'];
$jsondata         = json_encode($json_ary);
file_put_contents($jsonfile, $jsondata);

// JSONの秘密鍵を環境変数にセット
putenv('GOOGLE_APPLICATION_CREDENTIALS='.GCP_JSON);

// コマンドの作成・実行
$command = sprintf("%s %s %s", NODE, TRANSLATION_JS, $jsonfile);
$out = $res = '';
exec($command, $out, $res);

// 結果解析
if (!empty($out[0])) {
    header('content-type: application/json; charset=utf-8');
    echo json_encode(['text' => $out[0]]);
} else {
    header('content-type: application/json; charset=utf-8');
    echo json_encode(['error' => 'error']);
}
exit();
```

* ここまでやると[ディレクトリ構成](#ディレクトリ構成)のような形になっていると思います。


# やってみる

* こんな感じで入れてみて…

![コメント 2019-12-06 205149.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316789/8708885a-b697-d032-cd5e-b6bdeb8a9369.png)

* こんな感じで出る。
* firefoxだとjson形式を開くと色々解析してくれる（が、今回はシンプル極まりないので特に意味なしですが）

![コメント 2019-12-06 205244_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316789/13503c71-4963-65d4-7de9-f5d3edb60bf6.png)


# 補足

* Google Translation API v3 はまだβ版なので注意しよう。
    * 早くphp版ライブラリでないかぁと思ったり。
* Google Translation API v3 には無料枠がある。
  * [FAQ](https://cloud.google.com/translate/faq)の「Cloud Translation API へのアクセス」の「無料の割り当てはありますか？」に記載あり。
  * 自分が見た[価格表](https://cloud.google.com/translate/pricing)はv3の特別枠の記載があったが消えている（2019/12/04更新したみたいでその時消えたっぽい）
  * こうやって記載しているそばから更新されていく可能性大（なぜならまだβ版だから）
  * 案件で使った経緯はこの「無料枠」があるところが大きかった。（が、今は消えているのでちょっとドキドキしています）

# まとめ
* 自分の担当案件ではよく「日英のHPをCMSで作る」という要件がよくあるのでこれからも多用していくかなと思っています。
* 正直、ロジックを作るところよりもGCPの設定のほうが難しかった。


---
[:christmas_tree: FORK Advent Calendar 2019](/advent-calendar/2019/fork)
:arrow_left: 9日目 [Vuetifyのdatepickerを使って【和暦】＋【年度/月】pickerを作ってみた ](/BigFly/items/7d6bcf649bedc2b91d3e) @BigFly 
:arrow_right: 11日目 [ブラウザであのレベルアップ音を奏でてみた(Tone.js)](/talow1/items/0ad112a698df7c21348d) @talow1