冷やかしで作った翻訳コマンド(TUI)が便利すぎるのでガチ使い！

GAS をラップした翻訳ツール (TUI command) を Go 言語で書きました。
<img width="500" alt="tran_1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/86ea47b4-cc04-1cde-7088-684776c8cb70.gif">

* 【2020 年 10 月 12 日追記】
    * [-a オプションと -e オプション](#オプション)
    * [設定ファイル](#設定ファイル2020-年-10-月-12-日追記)
    * [go-tran の課題](#6-go-tran-の課題)

# 対象読者

以下に該当する方にとっては読む価値があるかもしれません。

* 英語（や他の外国語）が苦手で翻訳ツールが手放せない
* ターミナルで作業する時間が長い
* translate-shell よりシンプルで視認性の良いコマンドを探している
* GAS (Google Apps Script) に興味がある
* Golang (Go 言語) の TUI ツールの実装に興味がある

# 目次

* [1. はじめに](#1-はじめに)
* [2. go-tran の概要](#2-go-tran-の概要)
* [3. go-tran の使い方](#3-go-tran-の使い方)
* [4. translate-shell との比較](#4-translate-shell-との比較)
* [5. 作り方は簡単](#5-作り方は簡単)
* [6. go-tran の課題](#6-go-tran-の課題)
* [7. おわりに](#7-おわりに)

# 1. はじめに

英語が苦手な僕は、ドキュメントを読むときも書くときも、辞書サイトや翻訳サイトが手放せません。
普段、ターミナルで作業をすることが多いんですが、Web ブラウザとターミナルソフトの間を行ったり来たりしているので、それはそれはストレスが溜まります。

そんなある日、GAS (Google Apps Script) を触ったことがなかった僕は、後学のためにちょっと試してみようと思い、LanguageApp をラップしたお手軽翻訳コマンドを作ってみたんです。

最初は洒落で作り始めたつもりだったんですが、意外にもこれが便利に使えることがわかったので、記事にして紹介したいと思います。

コマンドは以下のリポジトリで公開しています。

* [**GO-TRAN (GitHub)**](https://github.com/y-bash/go-tran)

類似するものに [translate-shell](https://github.com/soimort/translate-shell) というツールがあります。使っている方も多いと思いますが、本稿の後半で go-tran と translate-shell の違いについても触れています。

# 2. go-tran の概要

## 対応OS

Linux と Windows で動作確認しています。
(Mac でも動くと思います)

## 対話型インターフェス

tran コマンドを引数なしで実行すると、対話型シェルが起動します。

### 翻訳する

デフォルトでは、実行環境のカレント・ロケールの言語へ翻訳されるので、あとは翻訳したい外国語を入力するだけです。
以下は、英語、フランス語、イタリア語、スペイン語、韓国語、中国語を日本語へ翻訳する例です。
<img width="500" alt="tran_1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/86ea47b4-cc04-1cde-7088-684776c8cb70.gif">
上記画像ではプロンプトが ```:ja> ``` となっています。
これは ```(Source):(Target)> ``` の形式になっていて、変換元 (Source) の言語が自動 (Auto) で、変換先 (Target) の言語が日本語 (ja) という意味です。例えば、日本語から英語へ翻訳する場合は ```ja:en> ``` と表示されます。

### 言語を切り替える

変換先の言語を切り替えるには、2 桁の言語コード (ISO639-1) を入力します。
以下は、ターゲットの言語を切り替えながら、日本語を英語 (en)、フランス語 (fr)、イタリア語 (it)、スペイン語 (es)、韓国語 (ko)、中国語 (zh) へ翻訳する例です。
<img width="500" alt="tran_2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/26256045-4df7-e1f4-e49b-fd37a9feef17.gif">

### 言語コードが分からない場合

言語コードが分からない場合は l (エル) コマンド (l: Language codes) で調べられます。 

l コマンドは、入力した文字列が ISO639-1 コードと同じか、ISO 言語名称 (英名) に部分一致したものの一覧を表示します。どちらにも引っかからなかった場合は、内部で英語に変換してから再検索をかけるため、例えば日本語で入力した名称でも、目的の言語コードを比較的簡単に見つけることができます。

以下は「 l コマンドで "nor" を含むもの、"nese" を含むもの、"ドイツ語"、"クルド語"、"ズールー語" を検索し、その一覧は表示できたが、"カンブリア語" は見つからなかった」という例です。
<img width="500" alt="tran_3.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/16402c7c-c54b-3e81-3fe3-8a6a24faa930.gif">

## バッチ・インターフェス

コマンドライン引数にファイル名を指定したり、コマンドの標準入力へパイプやリダイレクトで繋いだ場合はバッチ・コマンドとして実行されます。

### ファイルの指定

引数にファイルのパス名を渡して go-tran を実行すると、そのファイルを翻訳してくれます。これも変換先の言語（デフォルト値）は実行環境のカレント・ロケールにより決まります。
<img width="500" alt="tran_5.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/69461fd3-6629-9d34-dc25-693b673df06e.gif">

### パイプやリダイレクト

標準入力がターミナルではない場合、go-tran はバッチモードで実行されます。

例えば、僕の Ubuntu 環境は man が日本語で表示されるようになっているのですが、日本語のマニュアルが用意されていないコマンドもあります。普段あまり使わないコマンドほど日本語化されていなかったりしますよね。これまでは、知らない英単語に出くわしたときはわざわざ Web ブラウザを開いて調べていました。

でもこれからは man を go-tran にリダイレクトするだけで済みます。
<img width="500" alt="tran_4.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/a6999c6c-ce38-54ec-d55c-58dad8b23225.gif">

# 3. go-tran の使い方

## インストール

**Go の環境がある場合**

```bash:install&nbsp;go-tran
$ go get -u github.com/y-bash/go-tran
$ cd $GOPATH/src/github.com/y-bash/go-tran
$ go install ./...
```

**Go の環境がなく、ソースコードのみ取得する場合**

```bash:get&nbsp;source&nbsp;code
$ git clone https://github.com/y-bash/go-tran.git
```

もし、Go の環境はないけどバイナリを使ってみたいという方がいたら、方法を考えますので、気軽に声をかけてくださいね。

## 対話型で使う

### 起動する

実行ファイルは tran という名前です。

```bash
$ tran
Welcome to the GO-TRAN!
...
```

### コマンドの説明

入力した文字は概ね以下のように判断されます。

|文字列|説明|
|:--|:--|
|1 文字|コマンドとして判断。|
|2 文字|Target 言語の切り替え。|
|3 文字以上|入力したテキストを現在のモードに従って翻訳。|

コマンドの種類は以下のとおりです。

|コマンド|説明|例|
|:-:|:--|:--|
|h|ヘルプを表示します。||
|l [str]|言語コードと名称の一覧を表示します。<br>（言語コードは ISO639-1）<br><ul><li>str を指定した場合<br>マッチする言語コードの一覧を表示</li><li>str を省略した場合<br>すべてのコード (184 件) を表示</li></ul>|<ul><li>コード、名称に "en" を含むもの<br>```l en```</li><li>名称に "nor" を含むもの<br>```l nor```</li><li>名称に英名 "Welsh" を含むもの<br>```l ウェールズ語```</li></ul>|
|s [str]|変換元の言語 (Source) を切り替えます。<br><ul><li>str を指定した場合<br>マッチする言語へ切り替え</li><li>str を省略した場合<br>Auto (自動モード）へ切り替え<br>※通常は Auto で問題なし</li></ul>|<ul><li>英語へ<br>```s en```</li><li>スペイン語へ<br>```s spanish```</li><li>ポルトガル語へ<br>```s ポルトガル語```</li><li>Auto(自動モード) へ<br>```s```</li></ul>|
|[t] code<br><br>t [str]|変換先の言語 (Target) を切り替えます。<br><ul><li>t を省略した場合<br>2 桁のコード (ISO639-1) で<br>指定した言語へ切り替え</li><li>t の後に str を指定した場合<br>マッチする言語へ切り替え</li><li>t の後の str を省略した場合<br>実行環境のカレント・ロケール<br>へ切り替え</li></ul>|<ul><li>カレント・ロケールへ<br>```t```</li><li>英語へ<br>```t en```</li><li>ラテン語へ<br>```t ラテン語```</li><li>C 言語へ<br>```c```</li></ul>|
|q|終了||

### キー操作

主なキーバインドは以下のとおりです。
（bash 互換です）

#### カーソル移動

|キー|説明|代替キー|
|:-:|:--|:-:|
|Ctrl-a|行頭へ移動|Home|
|Ctrl-e|行末へ移動|End|
|Ctrl-b|1文字後退|←|
|Ctrl-f|1文字前進|→|

#### 文字削除

|キー|説明|代替キー|
|:-:|:--|:-:|
|Ctrl-h|カーソル位置の手前の文字を削除|Back<br>space|
|Ctrl-d|カーソル位置の文字を削除|Delete|
|Ctrl-k|カーソル位置から行末まで削除||
|Ctrl-u|行頭からカーソル位置の手前まで削除||

#### 履歴操作

入力したコマンドやテキストはヒストリに保存されるので、キー操作でそれを辿ることができます。

|キー|説明|代替キー|
|:-:|:--|:-:|
|Ctrl-p|入力履歴を遡る|↑|
|Ctrl-n|入力履歴を進む|↓|
|Ctrl-r|入力履歴のインクリメンタルサーチ||


##バッチコマンドとして使う

### 書式

コマンドの書式は以下のとおりです。

```:format
tran [options] [path...]
```

### オプション

【2020 年 10 月 12 日】-a オプションと -e オプションを追記

オプションは以下のとおりです。

|option|説明|例|
|:-:|:--|:--|
|-a|ユーザの GAS プロジェクトに登録する<br>スクリプトを表示します。登録した<br>スクリプトの URL を設定ファイルに<br>記載することで自分専用の API サーバを<br>呼び出すことができます。<br>このオプションはバッチモード専用です。|```$ tran -a```|
|-e|原文と翻訳文を 1 行ごとに交互に出力します。<br>このオプションはバッチモード専用です。|```$ tran -e english.txt```|
|-h|ヘルプを表示します。<br>このオプションはバッチモード専用です。|```$ tran -h```|
|-l|言語コードと名称の一覧 (184件) を表示します。<br>（言語コードは ISO639-1）<br>このオプションはバッチモード専用です。|```$ tran -l```|
|-s code|変換元の言語 (Source) を<br>言語コード（ISO639-1）で指定します。<br>省略した場合は自動判別。|```$ tran -s en```|
|-t code|変換先の言語 (Target) を<br>言語コード（ISO639-1）で指定します。<br>省略した場合はカレント・ロケールへ変換。|```$ tran -t en```|

### パス

path の指定方法は以下のとおりです。

|path|説明|例|
|:-:|:--|:--|
|指定|指定したファイルを翻訳して標準出力へ出力します。|```$ tran a.txt b.txt```|
|省略|<ul><li>標準入力がターミナルの場合<br>対話型で起動</li><li>標準入力がパイプやリダイレクトの場合<br>翻訳した結果を標準出力へ出力</li></ul>|<ul><li>ss コマンドの man を翻訳<br>```$ man ss｜tran｜less```</li><li>Go のコンパイルエラーを翻訳<br>```$ go build . 2>&1｜tran```</li><li>ファイルをリダイレクト<br>```$ tran < a.txt```</li></ul><br>※セル内の "｜" は全角文字です。<br>コピペの際はご注意ください。|

### 使用例

難しい英単語を取り急ぎ確認

```bash:
$ echo "penpineappleapplepen" | tran
ペンパイナッポーアップルペン
```

日本語のファイルを英語にする

```bash:
$ tran -t en Japanese.txt
this is a pen.
```

日本語のファイルをラテン語にして保存

```bash:
$ tran -t la Japanese.txt > Latin.txt
```

自動翻訳の結果を日本語に還元して、しっくりくるか確認

```bash:
$ echo "僕はりんごが大好きです" | tran -t en | tran
りんごが大好き
```

strings.Builder (Golang) のソースコードの全コメント行を日本語にする

```bash:
$ grep '^\s*//' ./builder.go | sed -e 's/^\s*..\s*//' | tran
Copyright 2017 The GoAuthors。全著作権所有。
このソースコードの使用は、BSDスタイルによって管理されています
LICENSEファイルにあるライセンス。
Builderは、Writeメソッドを使用して文字列を効率的に構築するために使用されます。
...
```

カレントディレクトリ配下の全ログファイルのエラー行を日本語にする

```bash:
$ find . -name '*.log' | xargs grep 'error:' | tran
./tool/vim/src/auto/config.log:conftest.c:154:12：エラー： 'GETACLCNT'が宣言されていません（この関数での最初の使用）
./tool/vim/src/auto/config.log:conftest.c:154:26：エラー： 'NULL'が宣言されていません（この関数での最初の使用）
...
```

## 設定ファイル【2020 年 10 月 12 日追記】

Version 1.0.1 でサポートした設定ファイルの使い方について説明します。

### 設定ファイルのパス

設定ファイルのパスは以下のとおりです。
（OS により異なります）

|OS|パス|
|:-:|:--|
|POSIX|$HOME/.config/y-bash/config.toml|
|Windows|%AppData%\y-bash\tran\config.toml|

### 設定できる項目

|テーブル|キー|説明|初期値|
|:--|:--|:--|:--|
|[default]|source|変換元の言語コード (ISO639-1) のデフォルト値。|""|
|[default]|target|変換先の言語コード (ISO639-1) のデフォルト値。|初回起動時のカレント・ロケール。|
|[api]|endpoint|GAS (Google Apps Script) プロジェクトの URI。ユーザが作成した GAS プロジェクトの URI を設定することにより自分専用の API サーバを指定できる。なお、GAS に設定するスクリプトは -a オプションで表示可能。<br>【例】 tran -a|これまでどおり。|
|[api]|limit_n_chars|1 度の API コールで送信する文字数の最大値。変換元テキストの文字数がこの値を超える場合、API は複数回に分けてコールされる。|4000|
|[colors]|info|インフォメーションメッセージの文字色|"#80a0d0"|
|[colors]|info|ステータス変更メッセージの文字色|"#60c060"|
|[colors]|error|エラーメッセージの文字色|"#d04040"|
|[colors]|result|変換されたテキストの文字色|"#ffc864"|

# 4. translate-shell との比較

実は僕が translate-shell のことを知ったのは、 go-tran を作り始めた後でした。
機能もほとんど似ており、コマンド名も同じ ```trans``` だったので驚きました。
（最初は go-trans という名前だったんです）

しかも、translate-shell は品詞などの付加情報を表示してくれたり、単語に複数の意味がある場合はそれを表示してくれたりと高機能です。

でも、少し使ってみると、go-tran には go-tran の良さがあることが次第にわかってきました。

## translate-shell のメリット

translate-shell は、翻訳結果だけでなく以下のような付加情報も表示してくれます。

* 変換元の言語
* 変換先の言語
* 品詞（単語の場合）
* 複数の意味（単語の場合）

translate-shell の実行例
<img width="500" alt="translate-shell_1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/d9407d0b-022e-c597-1bd4-fec5c7b1b12e.gif">

辞書のように使えるため、英作文などでは使い勝手が良いかもしれませんね。
とはいってもがっつり英文ドキュメントを書かなければならない場合はもっとしっかりした辞書が必要になると思います。

## go-tran のメリット

translate-shell に対する go-tran のアドバンテージは以下のとおりです。

* シンプル<br>余計な情報を出力しないため見やすいです。<br>品詞などの付加情報は時として邪魔になります。
* カラー表示<br>カラー表示なので、見たい箇所を瞬時に判別できます。
* 言語コードの検索機能<br>go-tran なら言語コードを簡単に見つけられます。<br>translate-shell では言語コードを記憶しておくか、ドキュメントを見なければならないでしょう。
* bash ライクなキーバインド<br>go-tran は文字編集がしやすいです。<br>translate-shell は標準入力を単に読み取っているだけのようで、文字編集に難があります。
* ヒストリ機能<br>go-tran は前に入力したコマンドやテキストを再表示し、それを編集して実行することができます。<br>これも translate-shell にはない機能です。
* Windows ネイティブ対応<br>go-tran は Windows のコマンドプロンプトで実行できます。<br>translate-shell では Cygwin などの UNIX 互換環境が必要なようです。

go-tran の使用イメージ
<img width="500" alt="tran_1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634588/86ea47b4-cc04-1cde-7088-684776c8cb70.gif">

# 5. 作り方は簡単

GAS (Google Apps Script) で作る Web API も、Go で作るクライアントもとても簡単です。

## GAS で Web API を作る

[tanabee](https://qiita.com/tanabee/) さんの大人気記事を参考にさせていただきました。

* [3 分で作る無料の翻訳 API with Google Apps Script](https://qiita.com/tanabee/items/c79c5c28ba0537112922)

参考にさせていただいたコードに、以下の処理を追記しています。

* json で返却
* GET/POST に対応
* 例外処理
* 例外メッセージを英語で出力

コードは以下のとおりです。

```js:Translate.gs
// 共通処理
// param e イベント
function process(e) {
    let body // レスポンスボディ
    try {
        const p = e.parameter                                       // リクエスト・パラメータ
        const s = LanguageApp.translate(p.text, p.source, p.target) // 翻訳
        body = {code: 200, text: s}                                 // レスポンス・ボディの作成
    } catch (e) { // param e 例外
        // 翻訳に失敗した
        try {
            const msg = LanguageApp.translate(e.toString(), "", "en") // エラーメッセージを英語へ
            body = {code: 400, message: msg}                          // レスポンス・ボディの作成
        } catch (e) { // param e 例外
            // これに失敗するということは内部エラー
            body = {code: 500, message: e.toString()}                 // レスポンス・ボディの作成
        }
    }
    let resp = ContentService.createTextOutput()    // レスポンス
    resp.setMimeType(ContentService.MimeType.JSON)  // MIME タイプを JSON 形式にして
    resp.setContent(JSON.stringify(body))           // レスポンス・ボディを JSON 形式の文字列に変換
    return resp;                                    // レスポンスを返却
}

// GET
// param e イベント
function doGet(e) {
    return process(e) // 共通処理の呼び出し
}

// POST
// param e イベント
function doPost(e) {
    return process(e) // 共通処理の呼び出し
}
```

上記コードで process(e) と 2 つの catch(e) で同名の変数を使っているのが気になる方は変数名を変更してください。

なお、スクリプトを修正した後に **Project version** を **New** にしないとプロダクトに反映されないということを知らなかったため、これで 20 分くらい悩みました。

## Go でクライアントを作る

GAS で作った Web API をラップするクライアント側の関数（Go 言語）のは以下のとおりです。

```golang:tran.go
package tran

import (
        "encoding/json"
        "errors"
        "io/ioutil"
        "net/http"
        "net/url"
        "strings"
)

const gURL = "https://script.google.com/macros/s/@@script@@/exec" // @@script@@ 要修正

// レスポンスの JSON をマップする構造体
type TransData struct {
        Code    int    `json:"code"`    // レスポンス・ステータス
        Text    string `json:"text"`    // 翻訳結果
        Message string `json:"message"` // エラーメッセージ
}

// 翻訳
func Translate(text, source, target string) (string, error) {
        // パラメータの作成
        v := url.Values{}
        v.Add("text", text)     // 変換元の文字列
        v.Add("srouce", source) // 変換元の言語 (ISO639-1)
        v.Add("target", target) // 変換先の言語 (ISO639-1)

        // 翻訳 API へ POST でリクエスト
        resp, err := http.PostForm(gURL, v)
        if resp != nil {
                defer resp.Body.Close()
        }
        if err != nil {
                return "", err
        }
        // レスポンス・ボディをバッファへ読み込む
        buf, err := ioutil.ReadAll(resp.Body)
        if err != nil {
                return "", err
        }
        // レスポンス・ボディ (JSON 形式) を構造体へマップ
        var td TransData
        if err := json.Unmarshal(buf, &td); err != nil {
                return "", err
        }
        // レスポンス・ステータスの検査
        if td.Code != 200 {
                // エラー処理 (プレフィクスの "Exception: " を取り除く)
                msg := td.Message
                prefix := "exception:"
                if strings.HasPrefix(strings.ToLower(msg), prefix) {
                        msg = string(msg[len(prefix):])
                        msg = strings.TrimSpace(msg)
                }
                return "", errors.New(msg)
        }
        // 正常処理
        return td.Text, nil // 構造体から変換後の文字列を取り出して返却
}
```

難しいことは何もしておらず、一番シンプルな方法で HTTP リクエスト (POST) を投げているだけなんですが、以下の点は注意してくださいね。

* 上記コードの ```@@script@@@``` の箇所は、それぞれの GAS プロジェクトに合わせて修正する必要があります。
* エラーが発生した際、メッセージのプレフィクス "Exception: " を取り除く処理を行っていますが、これは状況によっては不要な処理だと思います。

これ以外のコードについては、[こちら](https://github.com/y-bash/go-tran) をご覧ください。

# 6. go-tran の課題

現時点で以下のような課題がありますが、改善はそれほど難しくないと思います。

* レスポンス・タイム<br>現在、サーバ側は GAS の無料プランを使っているということもあり、トラフィックが集中した際のアクセス制限でレスポンスが悪くなることが考えられます。<br>これについては、コマンドの config 設定でユーザ固有の URL を指定できるようにしようと考えています。そうすれば、利用者が自分の Google Account に GAS スクリプトを登録する負担は増えますが、それと引き換えに他人のことを気にせずにコマンドを使うことができるようになるはずです。<br>**【2020 年 10 月 12 日追記】<br>この問題は version 1.0.1 で解消されました。設定ファイルにユーザ専用の GAS プロジェクトの URI を設定することができます。詳細は [こちら](#設定ファイル2020-年-10-月-12-日追記) をご覧ください。**

* バッチの出力形式<br>ログファイルやドキュメントを翻訳する場合はやはり原文と比較しながら見たいですよね。<br>これについては、原文と翻訳文を行ごとに交互に出力するオプションを用意する予定です。おそらく対応は難しくないでしょう。<br>**【2020 年 10 月 12 日追記】<br>この問題は version 1.0.1 で解消されました。-e オプションをつけて実行することで原文と翻訳文を交互に出力できます。詳細は [こちら](#オプション) をご覧ください。**

* 文字数制限<br>GAS の LanguageApp に 大量の文章を投入するとエラーが発生することがわかりました。これについては、API に 1 度に投入する文字数の最大値を config 設定できるようにし、大量の文章が投入された場合は分割して翻訳するようにプログラムを修正する予定です。<br>**【2020 年 10 月 12 日追記】<br>この問題は version 1.0.1 で解消されました。設定ファイルの limit_n_chars (初期値は 4000）で設定できます。詳細は [こちら](#設定ファイル2020-年-10-月-12-日追記) をご覧ください。**

# 7. おわりに

本稿では、Go 言語で翻訳ツール (TUI コマンド) を作ったという話と、そのコマンドの使い方を紹介しました。作り方についても簡単に紹介しました。また、既に似たツールが存在することを知り、最初はがっかりするも次第に自作コマンドも悪くないと思うようになったということを書きました。

GAS の翻訳 API は簡単に使える上に、皆に知れ渡っています。
最初は、そんな API を使ったプロダクトなんて特に面白いこともできないだろうと思っていましたが、実際に作ってみると意外にも便利なものを作ることができました（オレオレ仕様なのであたりまえなのですが…）。

また自作してみることで、人気ツールのメリットとデメリットの両方が見えるようになりました。

世に知れ渡ったソリューションは星の数ほどあります。
でも今回の経験は、そういったものの中にも、

* コモディティ化したように見えるが、まだ誰も気づいていない使い道がある
* デファクトスタンダードになっているが、まだ誰も気づいていない改善点がある

そういったことを改めて認識する機会となりました。

それでは！