DeepL APIを使ってpythonに翻訳機能を埋め込む

## はじめに
この記事は、[DeepL Python Library](https://github.com/DeepLcom/deepl-python)を使って自作のpythonパッケージに翻訳機能を加える方法です。個人開発で得た知識を、いつでも見返せるように保存する目的で使用しているため、言葉足らずである箇所が多々あるかと思います。気付いた箇所がありましたら、ご一報いただけると幸いです。

## 作者の環境
作者は以下の環境を使用しています。そのほかの環境では試験してないので、動作の保証はありません。Ubuntu 22.04を使用する場合は、既知のエラーが発生しているようです[bug report](https://bugs.launchpad.net/ubuntu/+source/poetry/+bug/1958227)。
<table>
  <tr>
    <td> OS </td> <td>MacOS 13.0</td>
  </tr>
  <tr>
    <td> Python </td> <td>3.9.6</td>
  </tr>
</table>

上記Githubリンクの`README.md`を読むと、python versionは`>=3.6 && <=3.11`である必要があるようです。また、`request`モジュールを内部で使用しており`>=2.0`のバージョンが要求されます。

## 認証キー、DeepL Python Libraryを取得する
DeepLアカウントをまだ作成していない場合は、[公式サイト](https://www.deepl.com/pro?utm_source=github&utm_medium=github-python-readme#developer)から作成してください。作成後、ご自身のアカウントタブから認証キーを取得できます。認証キーは、個人毎に割り当てられているものなので、秘匿する必要があります。最終的に作成したコードをGit上にpushすることを鑑みて、本記事では`.zshrc`に環境変数として記載することにします。
```shell:.zshrc
export DEEPL_API_KEY="123456...." # ご自身が取得した認証キーを記載
```
次にDeepL Python Libraryをインストールします。コマンド一つで入手できます。
```python
pip install --upgrade deepl
```
余談ですが、DeepLは無料プランを提供くれているので、どなたでも気軽に試してみることができます。ただし、1ヶ月に50万字を超える利用には、有料プランに加入する必要があります。無料プランから有料プランには手動で変更する必要があるので、その点は安心です。詳しくは[公式サイト](https://www.deepl.com/pro?utm_source=github&utm_medium=github-python-readme#developer)をご覧ください。

## Pythonを書いていく
作成したファイルを載せます。本来、複数の単語帳(Glossary)を扱うことができるのですが、本classでは一つに限定しています。また、全ての言語ペアに対して単語帳を作成できるわけではないため注意が必要です。
```python:translator.py
import os
import deepl
from typing import Union

# 認証キーを取得
DEEPL_API_KEY = os.environ['DEEPL_API_KEY']

class DeepLTranslator():
  '''
  単語帳(glossary)は一つのみ扱うことのできるモデルとなってます。
  '''
  def __init__(self, api_key: Union[str, None]=None) -> None:
    self.DEEPL_API_KEY=api_key if not api_key is None else os.environ['DEEPL_API_KEY']
    self.translator = deepl.Translator(self.DEEPL_API_KEY)
    self.glossary = None

  # DeepLでサポートされている言語を取得する(source)
  def get_available_source_languages(self) -> None:
    for language in self.translator.get_source_languages():
      print(f'{language.name}: {language.code}')

  # DeepLでサポートされている言語を取得する(target)
  def get_available_target_languages(self) -> None:
    for language in self.translator.get_target_languages():
      if language.supports_formality:
        print(f'{language.name}: {language.code} supports formality')
      else:
        print(f'{language.name}: {language.code} not supports formality')

  # 単語帳でサポートされている言語ペア
  def get_glossary_languages(self) -> None:
    for language_pair in self.translator.get_glossary_languages():
      print(f'from {language_pair.source_lang} to {language_pair.target_lang}')

  # 辞書型オブジェクトを入力にとり、単語帳を作成
  def create_glossary_with_entries(self, entries: dict[str, str], source_lang: str='EN', target_lang: str='JA'):
    if not self.glossary is None:
      self.delete_glossary()
    self.glossary = self.translator.create_glossary(
      'My glossary',
      source_lang=source_lang,
      target_lang=target_lang,
      entries=entries,
    )
    print(
      f'Created: "{self.glossary.name}" ({self.glossary.glossary_id})\n'
      f'{self.glossary.source_lang} -> {self.glossary.target_lang}\n'
      f'containing {self.glossary.entry_count} entries\n'
    )

  # CSVファイルを入力に単語帳を作成することも可能
  def create_glossary_with_csv(self, csvfile: str, source_lang: str='EN', target_lang: str='JA') -> None:
    if not self.glossary is None:
      self.delete_glossary()
    with open(csvfile, 'r', encoding='utf-8') as f:
      f_data = f.read()
    self.glossary = self.translator.create_glossary_from_csv(
      'My glossary',
      source_lang=source_lang,
      target_lang=target_lang,
      csv_data=f_data,
    )
    print(
      f'Created: "{self.glossary.name}" ({self.glossary.glossary_id})\n'
      f'{self.glossary.source_lang} -> {self.glossary.target_lang}\n'
      f'containing {self.glossary.entry_count} entries\n'
    )

  # 作成した単語帳の中身を確認
  def get_glossary_entries(self, as_dict: bool=False) -> Union[dict[str, str], None]:
    entries = self.translator.get_glossary_entries(self.glossary.glossary_id)
    if as_dict:
      return entries
    else:
      for (key, value) in entries.items():
        print(f'{key} -> {value}')

  # 現在設定している単語帳を削除
  def delete_glossary(self) -> None:
    self.translator.delete_glossary(self.glossary)

  # 翻訳を開始する
  def translate(self, text: str, source_lang: str='EN', target_lang: str='JA', split_sentences: str='1') -> str:
    self.get_glossary_entries()
    results = self.translator.translate_text(
      text, source_lang=source_lang, target_lang=target_lang, split_sentences=split_sentences, glossary=self.glossary)
    return results.text
```

CSVファイルから単語帳を作成する場合は、DeepL Python Libraryによって指定されたフォーマットに従う必要があります。例えば以下のようなものが入力可能です。
```text:sample.csv
people,ホモサピエンス,EN,JA
person,ホモサピエンス達,EN,JA
```
実際に例文を入力してみます。
```
python
>>> from translator import DeepLTranslator
>>> translator = DeepLTranslator()
>>> translator.create_glossary_with_entries({'people': 'ホモサピエンス達'})
Created: "My glossary" (439ed570-7793-4558-a89f-138ef9c2a49b)
EN -> JA
containing 1 entries
>>> result = translator.translate(text='In this image, there are three people on what appears to be a set of a Japanese variety show.')
>>> print(result)
この画像では、日本のバラエティ番組のセットと思われる場所に3人のホモサピエンス達がいる。
```
良さそうです。筆者の英語力では到底太刀打ちできないほど洗練された結果を返してくれるので、DeepL様さまです。

## テキスト翻訳のオプション
最後にただREADME.mdを日本語に変換しただけですが、テキスト翻訳の際に設定可能な引数を列挙して本記事の締めとしたいと思います。
* source_lang: 入力言語の種別を指定する。自動で検出できる場合は省略可能。
* target_lang: 出力言語の種別を指定する。
* split_sentences: どのように入力テキストをセンテンスに分割するかを指定する。デフォルトでは'on'
  * on: 句読点、改行により入力テキストは分割される。
  * off: 入力テキストは分割されない。単一のセンテンスを持つ入力に用いるのが望ましい。
  * nonewlines: 句読点を用いたセンテンスの分割は行われるが、改行はしない。
* preserve_formatting: automatic-formatting-correctionに関する設定。デフォルトでは'True'
* formality: 翻訳機がフォーマルまたはインフォーマルのどちらを採用するか。限られた言語でのみ設定可能。(日本語は設定可能)
  * less: インフォーマル
  * more: フォーマル
* glossary: 用語集を使用するか否かを指定する。
* context: 自身は翻訳されないが、本文に影響を与えるであろう追加のコンテキストを入力に与えることができる。(Alpha feature)
* tag_handling: 内容が`html`か`xml`か (よく理解できていない...)

以上です。お読みいただきありがとうございました。