EmacsからDeepLのAPIを叩くコマンドを実装する

([Lisp Advent Calendar 2021](https://qiita.com/advent-calendar/2021/lisp) 11日目参加記事)

自分はEmacsでメモを取るときやコード中のコメントなどで適当な英語が浮かばないときに結構な頻度でDeepLのお世話になることが多く、自分で書いた英文が正しく日本語訳されるかのチェックに使ったりもするので、日英翻訳と英日翻訳を両方使う。
この記事では自分のEmacsでのDeepLの活用法について紹介する。

DeepLは初期からAPIを公開していて、当初はPro版のみだったが、最近では1か月に50万文字までは無料でAPIを利用できるようになった。
ユーザ登録後にアカウントページからAPIキーを取得できる。

APIドキュメントは以下

https://www.deepl.com/docs-api

## 準備
EmacsのHTTPクライアントライブラリ`request`が必要なので `M-x package-install request` などでインストールしておく。

## コード本体
以下のコードを`deepl.el`などの名前でファイルに保存しておく。
gist: https://gist.github.com/masatoi/ec90d49331e40983427025f8167d01ee

```elisp
(require 'request)

(defvar deepl-auth-key) ; この変数にdeeplから発行されるキーを設定する
(defvar deepl-confirmation-threshold 3000)
(defvar deepl-endpoint "api.deepl.com") ; 無料版は api-free.deepl.com

(cl-defun confirm-send-long-string (&key retry)
  (let ((send-it-p
         (read-from-minibuffer
          (if retry
              "Please answer with \"yes\" or \"no\". [yes/no]: "
            (format "It's over %S characters, do you really want to send it? [yes/no]: "
                    deepl-confirmation-threshold)))))
    (cond ((equal send-it-p "yes") t)
          ((equal send-it-p "no") nil)
          (t (confirm-send-long-string :retry t)))))

(cl-defun deepl-translate-internal (text source-lang target-lang success-callback)
  (when (and (> (length text) deepl-confirmation-threshold)
             (not (confirm-send-long-string)))
    (cl-return-from deepl-translate-internal))

  (request (format "https://%s/v2/translate" deepl-endpoint)
    :type "POST"
    :data `(("auth_key" . ,deepl-auth-key)
            ("text" . ,text)
            ("source_lang" . ,source-lang)
            ("target_lang" . ,target-lang))
    :parser 'json-read
    :success success-callback))

(cl-defun deepl--output-to-messages (&key data &allow-other-keys)
  (let ((translated-text (cdr (assoc 'text (aref (cdr (assoc 'translations data)) 0)))))
    (kill-new translated-text)
    (message translated-text)))

(defun deepl-ej (start end)
  (interactive "r")
  (let ((region (buffer-substring start end)))
    (deepl-translate-internal region "EN" "JA" #'deepl--output-to-messages)))

(defun deepl-je (start end)
  (interactive "r")
  (let ((region (buffer-substring start end)))
    (deepl-translate-internal region "JA" "EN" #'deepl--output-to-messages)))

(defun ja-char-p (char)
  (or (<= #x3041 char #x309f) ; hiragana
      (<= #x30a1 char #x30ff) ; katakana
      (<= #x4e01 char #x9faf) ; kanji
      ))

(defun ja-string-p (str)
  (>= (cl-count-if #'ja-char-p str) 3))

(defun deepl-translate (start end)
  (interactive "r")
  (let ((region (buffer-substring start end)))
    (if (ja-string-p region)
        (deepl-translate-internal region "JA" "EN" #'deepl--output-to-messages)
      (deepl-translate-internal region "EN" "JA" #'deepl--output-to-messages))))
```

## 使い方
上記コードのファイルをinit.elからロードし、`deepl-translate`を適当なキーバインドに割り当てる。さらにAPIキーを設定する。

```elisp
(load-file "/path/to/deepl.el")
(global-set-key (kbd "C-c t") 'deepl-translate)
(setq deepl-auth-key "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
```

リージョンを選択した状態で`C-c t`を押すことで、ミニバッファに翻訳結果が表示され、同時にクリップボードにもコピーされる。
翻訳対象に日本語の文字が3文字以上含まれていれば日英翻訳になり、そうでなければ英日翻訳になる。
翻訳対象が3000字以上だと本当に送っていいかを確認するプロンプトが出るようにしている。

## 所感
実は似たようなものは既に色々とあり、見かけたものだとDeepLのWeb版を起動するものだったり、Emacsの拡張パッケージの言語判定ライブラリに依存するものなどがあった。
ブラウザを起動するものはWeb版のDeepLにコピペするので基本的に無料だが、Emacsの統合という意味ではいまいちに感じられる。また言語判定ライブラリを使うものは過剰に感じられた。
結果の出し方も、翻訳結果をポップアップウインドウに出したり、現在の編集領域の下に挿入するものが存在したが、ウインドウ構成や元のファイルに破壊的な変更を与えない方法がよかったので現在のような方式になった。
今のところ満足して使っている。