Golangで自分用のcli翻訳ツール作ってみた

# 結論

Golang標準パッケージ整っててAPI叩くのやりやすい + TDDでやると見通し良くなる

# 導入

金曜日の夜に突如思い立って Golang の勉強に cli ツールを３つ作ることにした。
この記事は1つ目。

Golang で cliツールを作るにあたって `urfave/cli` というパッケージを使うのが楽らしいので、そこは乗っかることに。

```bash
go get github.com/urfave/cli
```

でローカル環境でとりあえず使えるようにした。

# やること

**コマンドラインでパッと変数名になる英単語を調べれる**

- テストベースで書く（TDDもどき）

## 目標

- `trans [日本語の単語]` で英語を返す
- `trans -e [英単語]` で日本語を返す

# やらないこと

- 熟語・短文を調べれるようにする(APIが賢いおかげで日本語→英語は余裕でできた！)
- 速度を追い求める
- オフラインでも使えるようにする

# 実装開始

ディレクトリ構造

```
$GOPATH/github.com/ahaha0807/translation
└── src
    └── script
        ├── Gopkg.lock
        ├── Gopkg.toml
        ├── main
        │   └── main.go
        ├── translation
        │   └── translation.go
        └── vendor

```

まずはサンプルコード書いてみた

```go
package main

import (
	"github.com/urfave/cli"
	"fmt"
	"os"
)

func main () {
	app := cli.NewApp()
	app.Name = "translation-ahaha0807"
	app.Usage = "translation on command"
	app.Version = "0.0.1"

	app.Action = func (context *cli.Context) error {
		fmt.Println("Hello world on cli")
		return nil
	}

	app.Run(os.Args)
}
```

実行してみる

```bash
go run ./translation.go
# ==> Hello world on cli
```

あと、`cli`パッケージを使ってることで、**`help`オプションを自動で作ってくれる**んだって。便利ー。

```bash
go run ./translation.go help
```

```
NAME: translation-ahaha0807 - translation on command

USAGE:
   translation [global options] command [command options] [arguments...]

VERSION:
   0.0.1

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version
```

# まずはベースとなるテストを書く

メソッドが定義されてないとコンパイルできないので、処理のないメソッドを定義する

```go:translation/translation.go

func ToEnglish(str string) string {
	return ""
}

func ToJapanese(str string) string {
	return ""
}
```

そして、テストを書く

```go:translation/translation_test.go
func TestToEnglish(t *testing.T) {
	actual := ToEnglish("バナナ")
	expected := "Banana"

	AssertTranslation(actual, expected, t)

	actual = ToEnglish("耳")
	expected = "Ear"

	AssertTranslation(actual, expected, t)

	actual = ToEnglish("変数")
	expected = "Variable"

	AssertTranslation(actual, expected, t)
}

func TestToJapanese(t *testing.T) {
	actual := ToJapanese("Banana")
	expected := "バナナ"

	AssertTranslation(actual, expected, t)

	actual = ToJapanese("ear")
	expected = "耳"

	AssertTranslation(actual, expected, t)

	actual = ToJapanese("VARIABLE")
	expected = "変数"

	AssertTranslation(actual, expected, t)

	actual = ToJapanese("CoFfEe")
	expected = "コーヒー"

	AssertTranslation(actual, expected, t)
}

func AssertTranslation(actual string, expected string, t *testing.T) {
	if actual == "" {
		t.Errorf("翻訳を実行できませんでした。")
	}

	if actual != expected {
		t.Errorf("翻訳結果が異なります %v, %v", actual, expected)
	}
}

```

`$ go test translation_test.go`
実装はまだ書いてないので、当然落ちる。

# メソッドの実装

## 翻訳部分の実装
翻訳部分は Microsoft の Translator API を利用する。

Translator APIの使い方とかは 公式サイト ( https://www.microsoft.com/ja-jp/translator/getstarted.aspx )をどうぞ

## Translator APIを使うためにAPIトークンを取得する

認証が必要なので、まず Azureのコンパネで見れるAPI Keyを使って API Tokenを取得するメソッドを作る

```go:translator/translator.go
func getAPIToken() string {
	apiKey := string(os.Getenv("MICROSOFT_TRANSLATE_APIKEY"))

	client := &http.Client{}

	req, _ := http.NewRequest("POST", "https://api.cognitive.microsoft.com/sts/v1.0/issueToken", nil)
	req.Header.Set("Ocp-Apim-Subscription-Key", string(apiKey))
	req.Header.Set("Content-Type", "application/json")

	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}

	apiToken, _ := ioutil.ReadAll(res.Body)
	defer res.Body.Close()

	return string(apiToken)
}
```

## 取得したトークンと翻訳したい文字列を送りつける

```go:translator/translator.go
func doTranslate(str string, srcLanguage string, destLanguage string, token string) string {
	host := "https://api.microsofttranslator.com"
	path := "/V2/Http.svc/Translate"

	values := url.Values{}
	values.Set("to", destLanguage)
	values.Add("from", srcLanguage)
	values.Add("text", str)

	option := values.Encode()

	client := &http.Client{}

	s := host + path + "?" + option

	req, _ := http.NewRequest("GET", s, nil)
	req.Header.Set("Authorization", "Bearer "+token)

	res, err := client.Do(req)
	if err != nil {
		return ""
	}
	defer res.Body.Close()

	body, _ := ioutil.ReadAll(res.Body)

	return string(body)
}

```

上記を実行するとレスポンスが 

```xml

<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Apple</string>

```
な感じで届く。

## レスポンスをパースする

これを `encode/xml` の `Unmarshal`メソッドで変換しようと思ったけど、1層目にデータがあるせいでうまく `Unmarshal` できなかったので、正規表現と文字列操作を使って力技で求めるデータだけを取り出す
 `format` メソッドを定義する。（んだけども、これに関しては MSさんが修正するか選択できるようにしてほしい…欲を言えば JSON で返してほしい…）

```go:translator/translator.go
func format(raw string) string {
	re := regexp.MustCompile(">.*<")
	result := string(re.Find([]byte(raw)))

	return result[1:len(result)-1]
}
```

# ここまでで定義したメソッドを繋ぐ（ToXXXXの中でそれぞれ呼び出す）

```go:translator/translator.go
func ToEnglish(str string) string {
	apiToken := getAPIToken()

	rawResult := doTranslate(str, "ja", "en", apiToken)
	result := format(rawResult)

	return result
}

func ToJapanese(str string) string {
	apiToken := getAPIToken()

	rawResult := doTranslate(str, "en", "ja", apiToken)
	result := format(rawResult)

	return result
}
```
これで基本的にはテストが通るようになる（はず。）

# ToXXXX系メソッドを cli から呼び出せるように繋ぐ

```go:main/main.go
app.Action = func(context *cli.Context) error {
	if len(context.Args()) == 0 {
		fmt.Println("USAGE: trans --[option] [words]")
		return nil
	}

	if !context.Bool("english") {
		result := translation.ToEnglish(context.Args().Get(0))
		fmt.Println(result)
		return nil
	} else {
		result := translation.ToJapanese(context.Args().Get(0))
		fmt.Println(result)
		return nil
	}
}
```

`!context.Bool("english")` でフラグがついているかを判断し、振り分けている

## オプションで 英語→日本語と日本語→英語 を切り替えれるようにする

```go:main/main.go
app.Flags = []cli.Flag{
	cli.BoolFlag{
		Name:  "english, e",
		Usage: "英語 to 日本語",
	},
}
```

# ビルド + PATHを通す
```bash
cd src/script/main/
go build -i -o trans main.go
```

これでビルドされ、 `./trans` でコマンドが使える。

```bash
./trans バナナ
Banana
./trans -e Banana
バナナ
./trans -h
# ~~~~~
```

あとはこれを `$GOPATH/bin` に移動させるだけ！

```bash
mv ./trans $GOPATH/bin

trans バナナ
Banana
```

# まとめ

単純なコマンドで、オフラインじゃ使えないまだまだ稚拙なこまんどだけど、自分で作ったやつが動くと嬉しい！
あと、 `$GOPATH` 使えば読み手の環境を色々考えなくてもよくって、共通化できててすごいこれ嬉しいのかも…！ｗ

次回は **コマンドからTwitterに呟ける** のを作って記事にします！

# リポジトリ
https://github.com/ahaha0807/translation-tool

（Golang勉強中なのでPRとかIssueとかご意見を募集してますー！）