私的に日英翻訳APIを利用して、個人辞書を作ってみた

# はじめに

組織内での日本語文書を機械的に翻訳し、その結果を蓄積する辞書サービスを目標に試作した時のログです。

今回は基本的に固有名称や部分的なセンテンスの翻訳・蓄積に特化しつつ、大枠を作るための最初のアプリを開発していきます。

結果として、ここで紹介するアプリは最初に提示したような機能をフルに実装していません。DeepL APIとGoogle Cloud TranslationにアクセスするRuby/Sinatra + Vue(v2)を利用したサンプルアプリとしてみてください。

Microsoft Translatorについては、ここでは触れていませんが、この記事の続編として公開しています。

* [Microsoft Translator APIを使ってみた](https://qiita.com/YasuhiroABE/items/a68d8b50f50389992229)

## セキュリティ上の考慮点

APIを利用する上ではCredential(認証情報)を保存しているファイルや文字列などをアプリケーションコードに含めて配布してしまうケースが度々問題になります。

今回は自分だけが利用するアプリケーションを想定していますが、JSONファイルやAPIキーは環境変数(GOOGLE_APPLICATION_CREDENTIALS、GOOGLE_PROJECT_ID、DEEPL_AUTH_KEY等)を利用してアプリケーションコードとは独立させています。

くれぐれも自分自身に固有なCredentialをDockerfileやMakefileなどに含めて、GitHubで公開することなどがないように注意してください。

# 今回説明する内容の完成品

作成する過程をメモしていますが、完成したものは次のリンク先に配置しています。各セクションの最後で使い方などは説明してます。

* [GitHub - YasuhiroABE/docker-example-transapi](https://github.com/YasuhiroABE/docker-example-transapi)
* [docker.io/yasuhiroabe/example-transapi](https://hub.docker.com/repository/docker/yasuhiroabe/example-transapi)
* [GitHub - YasuhiroABE/docker-transapi-frontend](https://github.com/YasuhiroABE/docker-transapi-frontend)

なお Microsoft Translatorについては別記事で解説していますが、現在のdoocker-example-traansa (tag:v1.0.0) には、Microsoft Translatorのコードも含まれています。

もしDeepLだけを動かすといった場合には、default_api.rb を変更して、gct.rb等のコードを呼び出さないようにするだけで動作するはずです。

# 利用する候補となるサービス

いくつか利用可能な候補を出してみます。

* [Google Translation API](https://cloud.google.com/translate?hl=ja) (Google Cloud Translation, GCT)
* [Microsoft Translator](https://azure.microsoft.com/ja-jp/services/cognitive-services/translator/)
* [DeepL API](https://www.deepl.com/pro#developer)

この記事の中では、Microsoft Translatorは利用していませんが、最新版のGitHubのmainブランチには、Microsoft Translatorを利用するコードも追加しています。

## それぞれのサービスの特徴

Google (Cloud) Translation API (GCT)は単純な翻訳サービスと、ドメイン固有の翻訳情報を蓄積・学習させるAIを供えたサービスを提供するなど、ユーザーのニーズに合わせてサービスを提供しています。

課金対象は50万文字/月までは無料で、基本的には言語判定に使用した文字数と、その次の翻訳に使用した文字数の両方が合算されるため、単純な文字数ではありません。また空のリクエストは1文字分、空白文字やタグなどの装飾文字も、APIに与えた文字数としてカウントされることになっています。

無料分を超過すると、100万文字あたり20ドルが追加されます。この他に1000ペア未満の辞書を与えてAI機能を有効にするとトレーニングに9000〜13500円程度追加で必要となり、後は翻訳の都度、通常の料金体系が適用されるようです。

Microsoft Translatorは200万文字/月は無料で、100万文字毎に1120円がかかります。ただ料金体系は少し不明瞭で、使用する機能毎に単価が異なるようです。

DeepL API (DeepL)は630円/月の固定料金の他に、100万文字あたり2500円というシンプルな設定になっています。

これまでDeepL Proの個人版を利用していて、Google Cloud PlatformではAppEngineやGKEを利用してクレジットカード情報などは登録しているので、今回はDeepLとGoogleの2つのサービスを利用してみようと思います。

# プロトタイプの作成

単純なWeb APIをOpenAPIで考えて、実装はRuby/Sinatraで作成することにします。

1. 同じ単語を繰り返しAPIに送信することがないように管理すること
2. 日→英の翻訳のみをサポートすること
3. 任意の日・英のペアを手動で登録できること
4. 間違いなど不適切な情報を削除できること
5. DeppL APIとGoogle Translation APIの2つの結果を保存すること

1ページに収まりそうな仕様になりそうです。

## 画面イメージ

実装はnode.jsやHTML5を想定していますが、まだ決めていません。

画面イメージはだいたいこんな感じかなと思います。
日本語入力窓に文字が入力される度に内部DBに既存の検索結果が保存されているか確認し、
該当する情報を表示することにします。

```text:画面イメージ
+--------------+   +--------------+  +--------------+
|  日本語入力窓  | → | 英語入出力用窓 |  | 手動登録ボタン |
+--------------+   +--------------+  +--------------+
+--------------+  +-------------+
| 翻訳開始ボタン |  | 再検索チェック | (← チェックをすると再度検索APIに問い合わせ、課金が発生)
+--------------+  +-------------+
(↑ 検索結果が存在する場合は押せないようにする) 

【検索結果 (非同期表示)】
+----------------------------------+------------+
| No | 日本語 |  英語 | DeepL/Google | 検索日      |
+----------------------------------+------------+
| 1. | 規　   | method      | D&G   |  2020/11/1 |
| 2. | 規定   | regulations | D     |  2020/11/4 | 登録ボタン |
| 3. | 規定   | rules       | G     |  2020/11/4 |
```

これぐらいだと翻訳APIを叩かなくても手動登録だけで良いでしょうという感じですが、まぁとりあえず翻訳API機能をこれから利用していく露払いだと思って作ってみることにします。

## APIの仕様

慣れているOpenAPIを使用して、openapi-generatorでruby/sinatraのテンプレートを出力するようにします。

とりあえず画面イメージを充足させるため、次のようなAPIを準備することを想定しています。

/dictで内部DBへの問い合わせ、/transで課金が発生するDeepLとGCTへの問い合わせを行なうこととします。

* GET /dict?q=日本語
* PUT /dict?ja=日本語&en=English
* GET /trans?q=日本語

今回のアプリケーションは、以前作成したopenapi ruby/sinatraのサンプルをベースに作成していきます。

* https://github.com/YasuhiroABE/docker-sccp-sinatra-sample

具体的なアプリケーションを作成する前に、各APIを試してみます。

## DeepL APIを試す

* [DeepL APIガイド](https://www.deepl.com/docs-api/accessing-the-api/)

単純なWebクエリにauth_keyなどをセットすれば良いので、これについて特に記述はしません。
ただ実際に利用すると、日本語の単語を中国語と判断する場合があるので、source_languageは常に指定するのがお勧めです。

## Google Translation APIを試す

* [Google Translation APIガイド](https://cloud.google.com/translate/docs/setup?hl=ja#ruby)
* [テキストの翻訳(Basic)](https://cloud.google.com/translate/docs/basic/quickstart?hl=ja)

ガイドによればRubyからGoogle Translation APIを呼び出すためには、ライブラリの導入が必要です。
また平行して**gcloud**コマンドも準備するよう書かれています。

```bash:gcloudコマンドによる初期化
$ curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-315.0.0-linux-x86_64.tar.gz
$ tar xvzf google-cloud-sdk-315.0.0-linux-x86_64.tar.gz
$ cd google-cloud-sdk
$ ./install.sh
$ ./bin/gcloud init
$ export GOOGLE_APPLICATION_CREDENTIALS=/home/......./xxxx-xxxxxxxxx.json ## Cloud Consoleで作成したサービスアカウントキーファイルを指定する
$ gcloud auth application-default print-access-token
```

Rubyから呼び出すためにbundleを使用して必要なライブラリの準備などを進める。

```text:Gemfile
source 'https://rubygems.org'
gem "google-cloud-translate"
```

```bash:rubyライブラリのインストール
$ bundle config set path lib
$ bundle install
...
$ ls -F
Gemfile  Gemfile.lock  lib/
```

### サンプルアプリの実装

Gemfileなどを配置したディレクトリにexample.rbを配置します。
GCTのガイドにあるサンプルにproject_idなどを設定する他に、ライブラリをロードするための設定を加えます。

```ruby:example.rb
#!/usr/bin/ruby

project_id    = "Your Google Cloud project ID"
text          = "The text you would like to translate."
language_code = "ja"

ENV['GOOGLE_APPLICATION_CREDENTIALS'] = "/home/.../xxxx-xxxxxxxxx.json"
require 'bundler/setup'
Bundler.require

translate   = Google::Cloud::Translate.translation_v2_service project_id: project_id
translation = translate.translate text, to: language_code

puts "Translated '#{text}' to '#{translation.text.inspect}'"
puts "Original language: #{translation.from} translated to: #{translation.to}"
```

このexample.rbを実行してみます。

```bash:example.rbの実行例
$ bundle config set path lib
$ bundle install
$ ruby example.rb
Translated 'The text you would like to translate.' to '"翻訳したいテキスト。"'
Original language: en translated to: ja
```

とりあえずAPIは利用できるようになりました。

# Web APIを実装してみる

Web APIの部分は、OpenAPIを利用して、Ruby/Sinatraで実装します。

## openapi.yamlファイルの準備

サンプルアプリを展開し、先ほどの想定したAPIを定義したopenapi.yamlファイルを準備します。

```bash:
$ git clone https://github.com/YasuhiroABE/docker-sccp-sinatra-sample example-transapi
$ cd example-transapi
```

emacsやviで既に配置されているopenapi.yamlファイルを編集していきます。

```yaml:openapi.yaml
openapi: 3.0.3
info:
  title: Unified Translation API
  version: 1.0.0
  description: Simplified Intranet-Web Japanese to English Dictionary and Translation API
  contact:
    name: YasuhiroABE
    url: https://yadiary.net/
    email: yasu@yasundial.org
servers:
- url: http://localhost:8080
paths:
  /.spec:
    get:
      description: providing the openapi schema YAML file.
      responses:
        200:
          description: "200 response"
  /dict: 
    get:
      description: e.g. /dict?q=query-words
      parameters:
      - in: query
        name: q
        required: true
        schema:
          type: string
      responses:
        200:
          description: "200 response"
    post:
      description: e.g. /dict?ja=japanese-words&en=english-words
      parameters:
      - in: query
        name: ja
        required: true
        schema:
          type: string
      - in: query
        name: en
        required: true
        schema:
          type: string
      responses:
        200:
          description: "200 response"
  /trans: 
    get:
      description: e.g. /trnas?q=query-words
      parameters:
      - in: query
        name: q
        required: true
        schema:
          type: string
      responses:
        200:
          description: "200 response"
```

使用しているサンプルにはopenapi-spec-validatorを呼び出すtaskがMakefileに登録されています。
未導入であれば次の手順で導入可能です。pip3のコマンド名は環境によって変化する可能性があります。

```bash:Ubuntu20.04でopenapi-spec-validatorを~/.local/bin/に導入する
$ pip3 install openapi-spec-validator --user
```

作成したopenapi.yamlが正しいかどうか、確認します。

```bash:validateタスクを実行する
$ make validate
/home/user01/.local/bin/openapi-spec-validator openapi.yaml
OK
```

## スケルトンコードの生成

正しいopenapi.yamlが作成できたらRuby/Sinatraのスケルトンコードを生成します。

```bash:rubyコードの生成
$ make gen-code
openapi-generator-cli generate -g ruby-sinatra -o code -i openapi.yaml
...
$ ls -F
api.default_api.rb  code/  _docker/  Makefile  openapi.yaml
## 不要なapi.default_api.rbを削除する (任意)
$ rm api.default_api.rb
```

スケルトンコードは、./codeディレクトリに出力されています。

ここから先の作業は、./codeディレクトリに移動して行います。

```bash:作業ディレクトリへの移動
$ cd code/
```

## Gemfileの編集

Gemfileを編集して、GCTが利用できるようにします。

```text:code/Gemfile
source 'https://rubygems.org'
gem "puma"
gem "sinatra"
gem "sinatra-cross_origin"
gem "httpclient"
gem "google-cloud-translate"
gem "json"
gem "rsolr"
```

DeepL APIの処理は、低レベルなhttpclientを利用します。
今回はデータベースにRDBMSではなく、Solrを検討しているため、rsolr を利用します。

## DeepL APIとGoogle Translation APIを処理するクラスを作成

後からAPIを切り替えられるように、2つのクラスを同一の基底クラスから派生させることにします。(とはいえ、この規模のコードではあまり意味はありませんが…)

```ruby:抽象クラスDataAPIの定義(code/lib/dataapi.rb)
class DataAPI
  JA = "ja"
  EN = "en"
  def initialize
    raise "Nobody can create the instance of DataAPI class."
  end

  def trans(string, to_lang = DataAPI::EN)
    ret = { :original_text => string,
            :translate_text => "",
            :to_lang => to_lang }
    return ret
  end
end
```

一応、英→日の可能性も残しているのと、文字列が7bit ascii codeの範囲だったら無条件に英→日という方法も考えられるのですが、とりあえずこういう動きにしておきます。

```ruby:GoogleCloudTranslateクラス(code/lib/gct.rb)

require "google/cloud/translate"

class GoogleCloudTranslate < DataAPI
  def initialize
    @project_id  = ENV.has_key?('GOOGLE_PROJECT_ID') ? ENV['GOOGLE_PROJECT_ID'] : ""
  end

  def trans(string, to_lang: GoogleCloudTranslate::EN)
    ret = { :original_text => string,
            :translate_text => "",
            :to_lang => to_lang }
    translate   = Google::Cloud::Translate.translation_v2_service project_id: @project_id
    translation = translate.translate string, to: to_lang
    ret[:translate_text] = translation.text.inspect
    return ret
  end
end
```

```ruby:DeepLAPIクラス(code/lib/deeplapi.rb)
# coding: utf-8
require 'json'
require 'uri'
require 'httpclient'

class DeepLAPI < DataAPI
  DEEPL_TRANS_PATH = "/v2/translate"
  def initialize
    @deepl_host  = ENV.has_key?('DEEPL_HOST') ? ENV['DEEPL_HOST'] : ""
    @auth_key  = ENV.has_key?('DEEPL_AUTHKEY') ? ENV['DEEPL_AUTHKEY'] : ""
  end

  def query_api(query)
    ret = {}
    header = {
      "Authorization" => "DeepL-Auth-Key #{@auth_key}"
    }
    begin
      client = HTTPClient.new
      url = "https://#{@deepl_host}#{DEEPL_TRANS_PATH}"
      resp = client.post(url, query, header)
      ret = JSON.parse(resp.body)
    rescue
      puts "[error] query_api: failed to query: #{:host},#{:path},#{:query}."
      ret = {}
    end
    return ret
  end

  def query_trans(source, to_lang, src_lang)
    q = {
      :text => source,
      :target_lang => to_lang,
      :source_lang => src_lang
    }
    query_api(q)
  end

  def trans(string, to_lang: DeepLAPI::EN, src_lang: DeepLAPI::JA)
    s = CGI::unescapeHTML(string)
    ret = { :original_text => CGI::unescapeHTML(string),
            :translate_text => "",
            :to_lang => to_lang }
    trans_result =  query_trans(s, to_lang, src_lang)
    ret[:translate_text] = trans_result["translations"][0]["text"]
    return ret
  end
end
```

これらのファイルは lib ディレクトリに配置しておきます。

```bash:
$ ls lib

dataapi.rb  deeplapi.rb  gct.rb  openapiing.rb  ruby/
```

## Sinatraプロジェクトの設定

### config.ruファイルの修正

配置したrbファイルを読み込むように設定しておきます。

```ruby:code/config.ru
require 'bundler/setup'
Bundler.require
require './my_app'
run MyApp
```

config.ruが読み込むmy_app.rbファイルを修正する手段もありますが、そちらにはバージョン番号が埋め込まれるので、openapi.yamlファイルを修正してコードを再生成する都度、内容が変化するので、変化しないconfig.ruファイルに追記しています。

### code/api/default_api.rbの修正

openapi-generatorがスケルトンコードを出力するので、実際の処理をここに記述していきます。
MyApp.add_route('GET', '/trans', から始まるメソッド内部に次のようなコードを加えています。

```ruby:code/api/default_api.rbの修正箇所
  cross_origin
  param = {}
  param[:q] = params.has_key?(:q) ? Rack::Utils.escape_html(params[:q]) : ""

  ## prepare the data structure to be returned.
  ret = {
    :version => "1.0",
    :results => []
  }
  ## escape if query is empty.
  return ret.to_json if param[:q] == ""

  deepl = DeepLAPI.new
  gct = GoogleCloudTranslate.new
  for result in [["deepl", deepl.trans(param[:q])], ["gct", gct.trans(param[:q])]]
    MyUtil::add_data(result[0], ret[:results], result[1])
  end
  ret.to_json
```

このメソッドの内部で検索結果をret変数に格納するためにMyUtil.add_dataを準備しています。
lib/myutil.rbを配置して次のような内容にしています。

```ruby:code/lib/myutil.rb
class MyUtil
  def self.add_data(label, array, result)
    array.append({ :engine => label,
                   :original_text => result[:original_text],
                   :translate_text => result[:translate_text],
                   :to_lang => result[:to_lang]
                 })
    return array
  end
end
```

とりあえず /trans エンドポイントに対応するコードは、ここで説明したようなコードをベースに構築しています。

### /dict エンドポイントに対応する処理

/dictエンドポイントは検索結果を保持するためのデータベース機能を持っています。
最終的にk8s環境でホストすることを考えると、SQLiteのようなホストベースのデータベースでは複数インスタンスに対応させることが難しいので、今回はSolrをデータベースとして利用します。

Solrはdockerを利用してデフォルト設定で利用します。

## Solrの設定

この作業は、code/ ではなく、docker-ruby-transapi/ で作業してください。

テスト用のSolrはDockerを利用して、次のURLでアクセスできるようにしています。
あらかじめ var.solr/ディレクトリを準備しています。公式Dockerイメージのsolr:8.7は一般ユーザー(uid=8983)で動作するため書き込み権限を与えておきます。

```bash:
$ mkdir -p var.solr
$ mkdir chmod 1777 var.solr
```

docker-ruby-transapi/Makefile にSolrを起動・停止するためのタスクを追加します。

```makefile:solrコンテナの起動のためMakefileに追加
solr-run:
        sudo docker run -it --rm -d \
                -p 8983:8983 \
                -v `pwd`/var.solr:/var/solr \
                --name solr \
                solr:8.7

solr-stop:
	sudo docker stop solr
```

作業のため、Solrを起動します。

```bash:solrの起動
$ make solr-run
```

正常に起動したら、solrコマンドを使用して"solrbook" coreを作成します。
Web UIからのcore作成は失敗する場合があるので、コマンドラインからの作成がお勧めです。

```bash
$ sudo docker exec -it solr /opt/solr/bin/solr create_core -c solrbook
```

成功すると、次のURLでAPIにアクセスできるようになります。

* URL: http://127.0.0.1:8983/solr/solrbook

### 【方法1】filed-type, filedの設定

デフォルトでは明示的にfieldを準備しなくても、指定したJSON形式のデータをそのまま保存してくれますが、日本語を適切に処理するためには手動でfield-typeとfieldを追加する必要が出てきます。

次のような形式でfield-typeやfieldの定義をテキストファイルで準備しておきます。


```json:ft-text_ja_edgengram.json
{
  "field-type": {
    "name": "text_ja_edgengram",
    "class": "solr.TextField",
    "autoGeneratePhraseQueries": "true",
    "positionIncrementGap": "100",
    "analyzer": {
      "charFilter": [
        {
          "class": "solr.ICUNormalizer2CharFilterFactory"
        }
      ],
      "tokenizer": {
        "class": "solr.EdgeNGramTokenizerFactory",
        "minGramSize": "1",
        "maxGramSize": "1"
      },
      "filter": [
        {
          "class": "solr.CJKWidthFilterFactory",
          "class": "solr.LowerCaseFilterFactory"
        }
      ]
    }
  }
}
```

```json:f-content_edgengram.json
{
  "field" : {
    "name" : "content_edgengram",
    "type" : "text_ja_edgengram",
    "multiValued" : "true",
    "indexed" : "true",
    "required" : "true",
    "stored" : "true"
  }
}
```

データは"field-type"か"field"いずれか1つのkeyを持つ構造になっています。
追加する時には"add-field-type"などに変換し、削除する時には、"delete-field-type"に変換した上で、"name"だけをKeyに持つ構造を取るようにします。

手動で作業するのは面倒なので、次のようなスクリプトで処理をしています。

```text:Gemfile
source 'https://rubygems.org'
gem "httpclient"
gem "json"
```

```$ bundle install --path lib``` で./lib/ディレクトリに必要なライブラリを導入しておきます。

```ruby:setup-solr-schema.rb
#!/usr/bin/ruby
# coding: utf-8

require 'bundler/setup'
Bundler.require

OPTDEL_LABEL = "delete"
OPTADD_LABEL = "add"
OPTUPD_LABEL = "update"
OPERATIONS = [ OPTDEL_LABEL, OPTADD_LABEL ]
KEY_LABEL = { "field" => { OPTDEL_LABEL => "delete-field",
                           OPTADD_LABEL => "add-field",
                           OPTUPD_LABEL => "replace-field" },
              "field-type" => { OPTDEL_LABEL => "delete-field-type",
                                OPTADD_LABEL => "add-field-type",
                                OPTUPD_LABEL => "replace-field-type" }
            }

SOLR_URL = "http://localhost:8983/solr/solrbook/schema"

operation = nil
if ARGV.length > 0 and OPERATIONS.include?(ARGV[0])
  operation = ARGV.shift
else
  exit 1
end

ARGV.each { |f|
  ret = {}
  json = nil
  open(f) {|data|
    json = JSON.load(data)
  }
  if json and json.keys.length == 1
    ## prepare variables
    data_type = json.keys[0] ## one of "field" or "field-type"
    key_name = KEY_LABEL[data_type][operation]
    ## setup the binary data
    if operation == OPTDEL_LABEL
      ret[key_name] = {}
      ret[key_name]["name"] = json[data_type]["name"]
    elsif operation == OPTADD_LABEL or operation == OPTUPD_LABEL
      ret[key_name] = json[data_type]
    end
  end
  puts ret
  client = HTTPClient.new
  resp = client.post(SOLR_URL, ret.to_json, "Content-Type" => "application/json")
  puts resp.body
}
```

準備したファイルを利用して、solrにschemaを追加します。

```bash
$ bundle config set path lib
$ bundle install
$ ./setup-solr-schema.rb add ft-ja_text_edgengram.json f-content_edgengram.json
```

### 【方法2】setup-solr-coreを利用したcontent_edgengramフィールドの設定

一連の作業を簡単にするために、GitHubにスクリプトを登録しています。

* [GitHub - YasuhiroABE/setup-solr-core](https://github.com/YasuhiroABE/setup-solr-core)

setup-solr-coreを利用する場合は、ft-ja_text_edgengram.json f-content_edgengram.json を利用して、次のよう操作します。

```bash:setup-solr-coreを利用した場合の例
$ git colone https://github.com/YasuhiroABE/setup-solr-core.git
$ cd setu-solr-core
$ make setup
$ mkdir -p setup-solrbook/{field,field-type}
$ cd setup-solrbook
$ cp ../examples/Makefile .
## catコマンドでファイルを作成する。copy-and-pasteの後にC-dで抜けるか、不得手ならemacs,vimなどでファイルを作成すること
$ cat > field-type/ft-ja_text_edgengram.json
$ cat > field/f-content_edgengram.json
$ make example-add
```

もし任意のフィールドタイプやフィールドを追加したいのであれば、定義用のjsonファイルをディレクトリ下に配置して、再度 ``make example-add`` を実行してください。
単純な変更であれば、``make example-update`` で更新処理が可能です。

## SinatraからSolrに接続する

再び openapi-generator-cli で生成したcodeディレクトリに移動して作業を進めます。

```bash:setup-solr-core/setup-solrbook/にいる場合は、../../codeに置き換えること
$ cd code
```

code/lib/mysolr.rbにsolrに接続するためのコードをまとめます。

```ruby:code/lib/mysolr.rb

# -*- coding:utf-8;mode:ruby -*-

class MySolr
  require 'rsolr'
  def initialize
    @solr_uri = ENV.has_key?("SOLR_URI") ? ENV["SOLR_URI"] : "http://127.0.0.1:8983/solr/solrbook"
  end
  
  def search(query)
    solr = RSolr.connect :url => @solr_uri
    ret = {}
    begin
      ret = solr.get 'select', :params => {
                            :q => query,
                            :wt => "json" ## never use the @param_wt, the @param_wt will effect output only.
                          }
    rescue => ex
      puts ex.to_s
    end
    return ret
  end

  def add(document, update = false)
    require 'rsolr'
    ret = {}
    solr = RSolr.connect :url => @solr_uri
    begin
      puts document
      if update
        ret = solr.update(document)
      else
        ret = solr.add(document)
      end
      solr.commit
      ret = true
    rescue => ex
      puts ex.to_s
    end
    return ret
  end
end
```

code/api/default_api.rbファイルに作成したMySolrクラスを利用するコードを追加します。
今度は、```MyApp.add_route('GET', '/dict', {...}) do```の内部に以下のようなコードを追加します。

```ruby:api/default_api.rbの('GET','/dict')に追加したコード抜粋
  cross_origin
  param = {}
  param[:q] = params.has_key?(:q) ? Rack::Utils.escape_html(params[:q]) : ""
  
  solr = MySolr.new
  solr.search("id:#{param[:q]}").to_json
end 
```

同様に```MyApp.add_route('POST', '/dict', {...}) do```の内部に以下のようなコードを追加します。

```ruby:code/api/default_api.rbの('POST','/dict')に追加したコード
  cross_origin
  ret = { :result => false }
  
  param = {}
  param[:ja] = params.has_key?(:ja) ? Rack::Utils.escape_html(params[:ja]) : ""
  param[:en] = params.has_key?(:en) ? Rack::Utils.escape_html(params[:en]) : ""

  solr = MySolr.new
  ret[:result]  = solr.add({ :id => param[:ja], :translation => param[:en],
                             :content_edgengram => param[:ja].to_s,
                             :updated => DateTime.now.iso8601 }, false)
end
```

# 配置したファイルの読み込み

code/lib/mysolr.rb と code/lib/myutil.rb を利用するための、require文が必要です。
code/config.ru ファイルを次のように変更します。

```ruby:code/config.ruの全体
require 'bundler/setup'
Bundler.require

require './lib/mysolr'
require './lib/myutil'
require './lib/dataapi'
require './lib/deeplapi'
require './lib/gct'

require './my_app'
run MyApp
```

なおGemfileに記入しているライブラリはBundler.requireが読み込んでくれるので、default_api.rbなどにある  ``require 'json'`` 行は、削除しても問題なく動作します。

# 作成したサーバーコードのテスト

code/ディレクトリの中ではサーバー起動用のタスクもMakefileには記述されています。
このコマンドを実行する前に、example.rbで環境変数 GOOGLE_APPLICATION_CREDENTIALS を利用したように、適切に環境変数を設定する必要があります。

```bash:サーバーの起動(code/ディレクトリで実行）
$ export GOOGLE_PROJECT_ID="your-project-id"
$ export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/xxxx.json"
$ export DEEPL_HOST="api.deepl.com"
$ export DEEPL_AUTHKEY="xxxx-xxxx-xxxx-xxxx"

$ make run

...
bundle exec rackup --host 0.0.0.0 --port 8080
Puma starting in single mode...
* Puma version: 5.2.2 (ruby 2.7.0-p0) ("Fettisdagsbulle")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 14501
* Listening on http://0.0.0.0:8080
Use Ctrl-C to stop
```

もしくは ``make run`` の前に、code/Makefile を編集して、runタスクに環境変数を設定します。

```makefile:code/Makefileへのrunタスクの追加
run: bundle-install
	env GOOGLE_APPLICATION_CREDENTIALS="$$(pwd)/xxxx.json" \
	    GOOGLE_PROJECT_ID="xxxxxxxxx" \
	    DEEPL_HOST="api.deepl.com" \
	    DEEPL_AUTHKEY="xxxx-xxxx-xxxx-xxxx" \
		bundle exec rackup --host $(HOST) --port $(PORT)
```

## /trans APIのテスト

curlを利用して、”はじめまして”をテストする場合、次のような方法でテストが可能です。

```bash:curlによる/transのテスト
$ curl --get --data-urlencode 'q="はじめまして"'  'http://localhost:8080/trans'

{"version":"1.0","results":[{"engine":"deepl","original_text":"&quot;はじめまして&quot;","translate_text":"","
to_lang":"en"},{"engine":"gct","original_text":"&quot;はじめまして&quot;","translate_text":"\"&quot;Nice to me
et you&quot;\"","to_lang":"en"}]}
```

curlを利用する場合は、そのままURLで'http://localhost:8080/trans?q="はじめまして"'のような指定をしても、自動では引数をURLエンコードしないため、このような方法を取る必要があります。

ちなみに、この時のサーバー側の出力を確認すると次のようになっています。

```text:pumaサーバーの出力
127.0.0.1 - - [03/May/2021:23:57:08 +0900] "GET /trans?q=%22%E3%81%AF%E3%81%98%E3%82%81%E3%81%BE%E3%81%97%E3%81%A6%22 HTTP/1.1" 200 265 1.7633
```

## /dict APIのテスト

/dictにはGETとPOSTの2つのメソッドを定義しています。
まず先ほど検索した結果を保存する場合、POSTメソッドを経由して、次のような方法でcurlコマンドが利用できます。

```bash:curlによる/dictへのPOSTリクエストの生成
$ curl --data-urlencode "ja=はじめまして" --data-urlencode 'en="Nice to meet you"' 'http://localhost:8080/dict'
```

curlは--data-urlencodeが指定された場合、デフォルトではPOSTメソッドを発行するため、特にオプションは指定していません。
次にGETリクエストで保存した結果を確認します。

```bash:curlによる/dictへのGETリクエストの生成
$ curl --get --data-urlencode "q=はじめまして" 'http://localhost:8080/dict'

{"responseHeader":{"status":0,"QTime":1,"params":{"q":"id:はじめまして","wt":"json"}},"response":{"numFound":1
,"start":0,"numFoundExact":true,"docs":[{"id":"はじめまして","translation":["&quot;Nice to meet you&quot;"],"c
ontent_edgengram":["はじめまして"],"updated":["2021-05-03T15:06:10Z"],"_version_":1698750131859357696}]}}
```

ここまでで、サーバー側の基本的な動作は完成しました。

## 完成したサーバーコードのサンプル

GitHubに、ここまでの作業を終えたプロジェクトを登録しています。

* [GitHub - YasuhiroABE/docker-example-transapi](https://github.com/YasuhiroABE/docker-example-transapi)

次のように利用してください。

```bash:
$ git clone https://github.com/YasuhiroABE/docker-example-transapi.git
$ cd docker-example-transapi
$ make gen-code
$ cd code
## 環境に応じて適切に環境変数を設定する
$ export GOOGLE_APPLICATION_CREDENTIALS=/home/......./xxxx-xxxxxxxxx.json
$ export GOOGLE_PROJECT_ID="xxxxxxxxx"
$ export DEEPL_HOST="api.deepl.com"
$ export DEEPL_AUTHKEY="xxxx-xxxx-xxxx-xxxx"
$ make run
```

# クライアントアプリケーションの作成

今回はVue.jsを利用したクライアントを作成してみることにしました。
vueコマンド(vue-cli)を利用してスケルトンコードを生成します。

あらかじめ、nコマンドでLTSバージョンのnode.jsをインストールしておきます。
globalにインストールするのは好きではないので、自分のホームディレクトリ以下に配置します。

```text:~/.npmrcファイルの内容
prefix=/home/user01/.local
```

```bash:vueコマンドのインストール
$ npm install @vue/cli --user
```

~/.bashrcに~/.local/bin/をPATHに加えておきます。

```bash:~/.bashrcへの追記分
PATH="${PATH}:${HOME}/.local/bin"
export PATH
```

vue自体はnode.jsを前提とはしていませんが、今回はelectronにも展開することを念頭に作業を進めます。

## 参考資料

* https://qiita.com/quzq/items/608fa811f7ff2c1ae7f3

## 準備作業

vue.jsを試すために必要なディレクトリを作成します。
Webブラウザでもelectronでも動作できるようにしておきます。

```bash:~/.local/bin/vueコマンドによるセットアップ
$ vue create my-project
## 画面にDefaultの選択肢が表示された場合、Default ([Vue 2] babel, eslint) を選択しています。
📄  Generating README.md...

🎉  Successfully created project my-project.
👉  Get started with the following commands:

$ cd my-project
$ vue add electron-builder

## Choose Electron Version では、11.0.0を選択しています。
Run `npm audit` for details.
⚓  Running completion hooks...

✔  Successfully invoked generator for plugin: vue-cli-plugin-electron-builder
$ 
```

package.jsonの"scripts"の内容を確認すると、```$ npm run```コマンドの引数に渡せるタスクが指定できます。

```bash:electronの起動
$ npm run electron:serve
```

Webブラウザから確認するには、```$ npm run serve```を実行し、http://localhost:8080/などの画面に表示されるURLを通して動作が確認できます。

## 作成したプロジェクトの構成

作成した"my-project"以下には、設定ファイルやライブラリなどが配置されます。
```$ npm run electron:serve```を実行すると、dist_electron/ディレクトリにAppImageによって生成されたバイナリが配置されます。

この場合のVue.jsは単一ファイルコンポーネントの構造を持つ、template,script,styleな.vueサフィックスファイルの利用を意識したものとなっています。関連のコードは主にpublic/とsrc/ディレクトリに配置されます。

public/にはVueのel:に指定するエントリポイントを持つような通常の静的なHTMLコンテンツが配置されています。

src/には.vueサッフィクスを持つJavaScript関連のファイルが配置されています。

自動的には配置されるテンプレートは、src/App.vueからsrc/components/HelloWorld.vueを呼び出しているので、Vue.jsの動きを観察する目的でも良いサンプルになると思います。

Vueのインスタンスの数だけ、src/直下に.vueファイルが配置され、各Vueインスタンスで再利用するようなUI部分は、src/components/以下に配置することになります。

## APIに接続するVueコンポーネントの作成

src/components/Translation.vue ファイルを作成し、一切の処理はここにまとめたいと思います。
axiosを利用してWeb APIを叩いていますので、package.jsonファイルにはaxiosをdependenciesに加えています。

```json:package.jsonの差分
  },
  "main": "background.js",
  "dependencies": {
    "axios": "^0.21.4",
    "core-js": "^3.6.5",
    "vue": "^2.6.11"
  },
  "devDependencies": {
```

もちろんaxiosを ```npm install axios``` のように加えることも可能です。

```vue:src/components/Translation.vue
<template>
  <div class="translation">
    <a target="_blank" href="http://localhost:8983/">Open Solr Console</a>
    <br />
    <textarea class="input-ja" v-model="text_ja" @keydown.enter="search_api" placeholder="日本語" />
    <textarea class="input-en" v-model="text_en" placeholder="English" />
    <br />
    <button class="button-search" type="button" v-on:click="exec_trans" >翻訳開始</button>
    <button class="button-regist" v-on:click="exec_regist" type="button">手動登録</button>
    <label>再翻訳</label>: <input v-model="is_exec_trans" type="checkbox" v-on:click="clicked_is_exec_trans" /> <p>{{ this.trans_warning }}</p>

    <h2>Ans. of {{ this.text_ja }}</h2>
    <table class="result">
    <thead>
    <tr>
      <th>No.</th>
      <th>日本語</th>
      <th>English</th>
    </tr>
    </thead>
    <tbody>
    <tr v-for="(item, index) in info" :key="[item.id,item.translation]" @click="overwrite_jaen_input_field(item.id, item.translation[0])">
      <td>{{ index + 1 }}</td>
      <td>{{ item.id }}</td>
      <td>{{ item.translation[0] }}</td>
    </tr>
    </tbody>
    </table>

    <h2>Translation Result</h2>
    <table class="result">
    <thead>
    <tr>
      <th>No.</th>
      <th>Engine</th>
      <th>日本語</th>
      <th>English</th>
    </tr>
    </thead>
    <tbody>
    <tr v-for="(item, index) in trans_result" :key="[item.engine,item.original_text,item.translate_text]" @click="overwrite_jaen_input_field(item.original_text, item.translate_text)">
      <td>{{ index + 1 }}</td>
      <td>{{ item.engine }}</td>
      <td>{{ item.original_text }}</td>
      <td>{{ item.translate_text }}</td>
    </tr>
    </tbody>
    </table>

    <h2>Registration Results</h2>
    <p>{{ results }}</p>

  </div>
</template>

<script>
/* from axios */
import axios from "axios"

export default {
  name: 'Translation',
  props: {
      text_ja: { type: String },
      text_en: { type: String },
      is_exec_trans: { type: Boolean },
      info: { type: String },
      results: { type: String }, 
      trans_result: { type: String },
      trans_warning: { type: String },
  },
  methods: {
    exec_regist() {
       axios
      .post("http://127.0.0.1:8080/dict?ja=" + this.text_ja + "&en=" + this.text_en, '')
      .then(response => (this.results = response));
    },
    exec_trans() {
      this.trans_warning = "";
      if(this.is_exec_trans) {
        axios
          .get("http://127.0.0.1:8080/trans?q=" + this.text_ja )
          .then(response => (this.trans_result = response.data.results));
        this.is_exec_trans = false;
      } else {
        this.trans_warning = "翻訳機能を利用する際はチェックをオンにしてください。";
      }
    },
    search_api() {
      axios
      .get("http://127.0.0.1:8080/dict?q=" + this.text_ja )
      .then(response => (this.info = response.data.response.docs));
    },
    overwrite_jaen_input_field(ja_text, en_text) {
      this.text_ja = ja_text;
      this.text_en = en_text;
    },
    clicked_is_exec_trans() {
      this.trans_warning = "";
    },
  },
}
</script>

<style scoped lang="css">
.input-ja {
  margin: 0;
}

.input-en {
  margin: 0;
  margin: 0;
}

.button-search {
  background: #efeeff;
  margin: 1em;
}

.result {
  margin: 0 auto;
}
</style>
```

作成したコンポーネントをHelloWorldの代りに呼び出すため、src/App.vueファイルを書き換えます。

```vue:src/App.vue
<template>
  <div id="app">
    <Translation />
  </div>
</template>

<script>
import Translation from './components/Translation.vue'

export default {
  name: 'App',
  components: {
    Translation
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

## プロジェクトの起動

一部の機能はまだ予定どおりではありませんが、日本語のDeepLとGCTを利用した翻訳と、結果のSolrへの登録については

あらかじめSolrとdocker-example-transapiを起動しておきます。docker-example-transapi/codeに移動して``make solr-run``を実行するか、次のようにsolrを起動し、core/solrbook を作成しておきます。

```bash:solrとアプリの起動
## solrの起動
$ cd docker-example-transapi
$ make solr-run

## transapiの起動
$ cd code
$ make run
```

solrが起動して必要なcoreが起動していれば、次の要領で稼動を確認します。

```bash:vueプロジェクトの起動
$ npm install
$ npm run serve
...
  App running at:
  - Local:   http://localhost:8081/
  - Network: http://192.168.85.129:8081/
```

ブラウザから ```http://localhost:8081/``` にアクセスし、動作を確認します。

``npm run electron:serve`` を利用すれば、ブラウザの代りにelectronが立ち上がりGUIからアクセスできるようになります。

## GitHubからのgit clone

ここまでのVueのコードは、GitHub上で公開しています。

* [GitHub - YasuhiroABE/docker-transapi-frontend](https://github.com/YasuhiroABE/docker-transapi-frontend)

# さいごに

Vueを始めて利用したので露払いとして簡単なアプリケーションを作成してみました。
現在のDeepLのWindowsクライアントは日本語入力時に、うまく文字が入力できない問題が発生しているので、しばらくこちらのクライアントを修正しつつ利用してみようと考えています。

GCTでコンパイルしたものは、ダブルクォーテーション(")が付くため、保存する際に削除するなどしないと、&quote;のようなエスケープされた文字列がSolrの検索結果に表示されてしまいます。

まだまだ拡張は必要ですが、とりあえず試せるようにDockerイメージやGitHubでのコード公開などを追記しました。

以上