FirebaseのCloud FunctionsからGoogle Cloud Translationを試してみる

AWS EC2インスタンスからFirebaseの環境をセットアップして、Google Cloud FunctionsからGoogle Cloud Translationにアクセスしてみます。

[前回](https://qiita.com/suzuki-navi/items/b12e1e662b2dbd288929)はローカルのNode.js環境からGoogle Cloud TranslationのAPIにアクセスしましたが、今回はGoogle Cloud Functionsの環境からです。


# firebaseインストールと認証設定

Node.jsはインストール済みとします。

```console
$ sudo npm install -g firebase-tools
```

```console
$ firebase --version
9.8.0
```

インストール後、認証設定が必要です。

```console
$ firebase login
```

これは対話型のコマンドです。

Google認証用のURLが表示されますので、同じようにブラウザでアクセスします。

ブラウザ上で承認すると、gcloudの認証設定と違って `http://localhost:9005/?...` にリダイレクトされます。`firebase login`コマンドをローカルで実行している場合は、コマンドが9005番ポートを一時的に待ち受けているので、ブラウザからコマンドに承認の情報が伝わって、勝手にコマンドが完了します。

`firebase login`コマンドをリモートで実行し、ブラウザだけローカルの場合には、これができません。ブラウザに表示されているURLをコピーして、リモートに別ターミナルで接続して `curl "http://localhost:9005/?..."` を実行します。curlコマンドは以下のようなHTMLを出力します。これで認証設定が完了です。

```html
      <h1>Firebase CLI Login Successful</h1>
      <p>You are logged in to the Firebase Command-Line interface. You can immediately close this window and continue using the CLI.</p>
```


# firebaseプロジェクト作成

まず空のディレクトリに移ります。

```console
$ mkdir sample
$ cd sample
```

プロジェクト設定。

```console
$ firebase init
```

また対話型のコマンドです。

`Which Firebase CLI features do you want to set up for this folder?` に対して Functions を選択。

`Please select an option` に対しては、Google CloudのプロジェクトですでにFirebaseを使ったことがあれば `Use an existing project` を選択します。Firebaseを使うのが初めてのGoogle Cloudプロジェクトであれば `Create a new project` を選択します。いずれもGoogle Cloudのプロジェクトはすでに存在する前提です。

あとは、 JavaScript or TypeScript の言語選択など、聞かれたことを答えます。

すると、ディレクトリの中にソースコードなどが生成されています。 `.gitignore` も生成されていますので、せっかくなので、

```console
$ git init

$ git add  -A

$ git ls-files
.firebaserc
.gitignore
firebase.json
functions/.gitignore
functions/index.js
functions/package-lock.json
functions/package.json
```

生成されたソースコードはこの7ファイルのようです。


# Cloud Translation APIを有効化

Google Cloudのコンソールにブラウザでアクセスして、使用するプロジェクトのCloud Translation APIを有効化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/1999b149-b1fc-a207-1c9b-4ed82def4a79.png)


# ソースコード編集

`functions/index.js` に以下のコードを書きました。

```javascript
const functions = require("firebase-functions");
const { TranslationServiceClient } = require("@google-cloud/translate").v3;

const projectId = "xxxxxxxx";
const location = "us-central1";

// 言語判定
async function detectLanguage(text) {
    const translationClient = new TranslationServiceClient();
    const req = {
        parent: translationClient.locationPath(projectId, location),
        content: text,
        mimeType: "text/plain"
    };
    const res = await translationClient.detectLanguage(req);
    let sourceLang = null;
    for (const elem of res) {
        if (elem == null) // なぜかnullがレスポンスに含まれる
            continue;
        return elem["languages"][0]["languageCode"];
    }
}

// 翻訳
async function translate(text, sourceLang, targetLang) {
    const translationClient = new TranslationServiceClient();
    const req = {
        parent: translationClient.locationPath(projectId, location),
        contents: [text],
        mimeType: "text/plain",
        sourceLanguageCode: sourceLang,
        targetLanguageCode: targetLang,
    };
    const res = await translationClient.translateText(req);
    for (const elem of res) {
        if (elem == null) // なぜかnullがレスポンスに含まれる
            continue;
        return elem["translations"][0]["translatedText"];
    }
}

async function sample(text) {
    const result = {};
    result["original"] = text;

    // 言語判定
    const sourceLang = await detectLanguage(text);

    // 翻訳
    for (const targetLang of ["en", "ja", "zh-TW", "zh-CN", "ko"]) {
        if (targetLang == sourceLang) // Target language can't be equal to source language. というエラーを防ぐため
            continue;
        const targetText = await translate(text, sourceLang, targetLang);
        result[targetLang] = targetText;
    }

    return result;
}

exports.translation_demo = functions.https.onRequest(async (request, response) => {
  let query = request.query["q"];
  if (!query)
    query = "";
  sample(query).then(result => {
    functions.logger.info(result);
    response.send(result);
  }).catch(err => {
    functions.logger.info(err);
  });
});
```

`functions/package.json` にはGoogle Cloud Translation用のライブライを追記します。

```diff
   "dependencies": {
     "firebase-admin": "^9.2.0",
-    "firebase-functions": "^3.11.0"
+    "firebase-functions": "^3.11.0",
+    "@google-cloud/translate": "*"
   },
```


# デプロイ

デプロイ前に `npm install` が必要みたいです。

```console
$ (cd functions; npm install)
```

`firebase deploy`します。これでGoogle Cloud Functionsが作成されます。JavaScriptのソースコード中に `exports.translation_demo` と書いていますので、 `translation_demo` という名前のFunctionが作られます。

```console
$ firebase deploy
```

（デプロイに時間がかかるのはどうにかならないだろうか・・・）


# 実行

```console
$ curl -Ssf 'https://us-central1-xxxxxxxx.cloudfunctions.net/translation_demo?q=%E5%8F%91%E7%94%9F%E5%9C%A82021%E5%B9%B43%E6%9C%8823%E6%97%A5%E5%9F%83%E5%8F%8A%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4%E4%B8%8A%E5%8D%887%E6%97%B640%E5%88%86%EF%BC%8C400%E7%B1%B3%E9%95%BF%E7%9A%84%E9%95%BF%E8%8D%A3%E6%B5%B7%E8%BF%90%E9%9B%86%E8%A3%85%E7%AE%B1%E8%88%B9%E9%95%BF%E8%B5%90%E8%BD%AE%E5%9C%A8%E5%9F%83%E5%8F%8A%E8%8B%8F%E4%BC%8A%E5%A3%AB%E8%BF%90%E6%B2%B3%E6%90%81%E6%B5%85%E3%80%82' | jq .
{
  "original": "发生在2021年3月23日埃及标准时间上午7时40分，400米长的长荣海运集装箱船长赐轮在埃及苏伊士运河搁浅。",
  "en": "At 7:40 am Egypt Standard Time on March 23, 2021, the 400-meter-long Evergreen container ship Captain Ci was stranded on the Suez Canal in Egypt.",
  "ja": "2021年3月23日のエジプト標準時午前7時40分、長さ400メートルのエバーグリーンコンテナ船のキャプテンCiがエジプトのスエズ運河で立ち往生しました。",
  "zh-TW": "發生在2021年3月23日埃及標準時間上午7時40分，400米長的長榮海運集裝箱船長賜輪在埃及蘇伊士運河擱淺。",
  "ko": "2021 년 3 월 23 일 이집트 표준시 오전 7시 40 분, 400 미터 길이의 에버그린 컨테이너 선 선장 Ci가 이집트의 수에즈 운하에 좌초했습니다."
}
```

(固有名詞の翻訳がうまくできてなさそうだけど、まあいいか・・・）