Ren'Pyで制作したゲームをDeepL API x Pythonで自動翻訳する

# 概要
Ren'pyで制作した日本語ゲームの英語版を、DeepL API x Pythonで自動で作ってみる記事です。
軽く触ってみた感覚としては割といけそうです。

2023/06追記：できた翻訳を見てみると以下のようになっていました。そのままリリースは確実にNGだが一から人手で翻訳するよりは楽になりそう、くらいの印象です。

- 主語や目的語のきちんと指定された地の文は良い感じにしてくれる
- 会話文や慣用句は壊滅する場合がある
    - 目的語の取り違いが頻繁に起こる(コンテキストを理解できないのでそれはそう……)
    - 慣用句が直訳になってしまう

# 手順
## Ren'Pyで翻訳ファイルの生成
ここはRen'Pyの公式ドキュメントにも書かれているのでそのままです。
Ren'Py Launcherで「翻訳の生成」をすれば翻訳用ファイルがgame/tl/<Language Name>下に生成されます。
「翻訳の生成」という名前ですが翻訳ファイルができるだけで、翻訳を勝手に作ってくれるわけではありません。
https://www.renpy.org/doc/html/translation.html

翻訳の生成時にオプションで「翻訳を空の文字列で生成する」が選べます。
選ぶと翻訳ファイルにデフォルトで空文字が入り、選ばないと元の文字列がそのまま入ります。

```Python:「空の文字列で生成」を選択した場合
translate english strings:

    # renpy/common/00accessibility.rpy:28
    old "Self-voicing disabled."
    new "Self-voicing disabled."
```

```Python:「空の文字列で生成」を選択しなかった場合
translate english strings:

    # renpy/common/00accessibility.rpy:28
    old "Self-voicing disabled."
    new ""
```

自分はcommon.rpyは選択なし、screens.rpyやscript.rpyは選択ありにしました。(1回選択ありで作って、common.rpyの翻訳ファイルを削除してから選択無しで作り直すという雑な方法を取りました。)

## DeepL APIの登録
今回はひとまずDeepL API Freeに登録します。
ゲームは公開済みで秘密にしたい内容を扱うわけではないためです。
プランの比較等は以下の記事が参考になりました。
https://qiita.com/rihok/items/962890b4b86ea052e54c

氏名・住所・クレカ番号の登録が完了するとアカウントページからAPI Keyが取得できるようになります。
## (Optional)用語集を作る
固有名詞やゲームに特徴的な語など、この語はこの語に翻訳したい！というのがあると思いますので、DeepL APIの用語集を作ります。
ウェブサイトやアプリの用語集とは互換性は無いらしいです。
2022/12/18現在、日本語からの翻訳で用語集が対応しているのは英語のみですので、残念ながらほかの言語への翻訳では用語集は使えません。
https://www.deepl.com/en/docs-api/glossaries

### 用語集ファイルを作る
csvかtsvで用語集を作ります。
こんな感じでいったん固有名詞を並べてみます。
```glossaries.csv
シルヴィア,Silvia
ソフィア,Sofia
ティノ,Tino
モニカ,Monica
```

### 用語集を登録する
APIはこちらに記載があります。
https://www.deepl.com/docs-api/glossaries/create-glossary/
Python用にdeeplというパッケージもあるらしいのでもちろんそれを使ってもいいです。
https://pypi.org/project/deepl/

requestsを使って書くとこんな感じになります。

```python:create
# 用語集ファイルを読み込む
filename = './glossary.tsv'
with open(filename, 'r', encoding='utf-8') as f:
    lines = f.readlines()
entries = ''.join(lines)
# シルヴィア\tSilvia\nソフィア\tSofia\nティノ\tTino\nモニカ\tMonica みたいになっている

import requests, json
url = 'https://api-free.deepl.com/v2/glossaries'
headers = {'Authorization': 'DeepL-Auth-Key <Your API Key>'}

data =  {
   'name': 'glossary',
    'source_lang': 'ja',
    'target_lang': 'en',
    'entries': entries,
    'entries_format':'tsv'
}

response = requests.post(url, headers=headers, data=data)
```
上手くいけばrenponseが以下のように返ってきます。
```
{'glossary_id': '<登録した用語集のID>',
 'name': 'glossary',
 'ready': True,
 'source_lang': 'ja',
 'target_lang': 'en',
 'creation_time': '2022-12-18T06:40:29.747054+00:00',
 'entry_count': 4}
```
これで用語集の登録は完了です。
編集はできないので修正したい場合は一度消して作り直す必要があると書かれていました。

## 翻訳する
本題です。
「翻訳を空の文字列で生成」していた場合、スクリプトファイルは以下のようになっていると思います。元の文言がコメントで入っているのは親切ですね。
```Python:tl/english/script.rpy
# TODO: Translation updated at 2022-12-18 14:03

# game/script.rpy:96
translate english main_0b40472a:

    # n "窓から眩しい朝日が差し込んでいる。門出に相応しい良い天気だ。"
    n ""

# game/script.rpy:97
translate english main_c6de125e:

    # n "いびきをかいて眠っている彼に気づかれないよう、そっとベッドから抜け出し、着古した祭服の袖に手を通す。"
    n ""
```
よってやりたいことは以下になります。

1. コメントから元の文言を取得
2. DeepL APIで翻訳を生成
3. 下の行に翻訳を書き込む

これを雑に書いてみます。

```Python: 翻訳して別のファイルに結果を書き込む
# 翻訳APIのURL
url = 'https://api-free.deepl.com/v2/translate'
headers = {'Authorization': 'DeepL-Auth-Key <Your API Key>'}

# textは後で追加する
data =  {
    'source_lang': 'ja', # 指定しなくても良い
    'target_lang': 'en',
    'glossary_id': '<登録した用語集のID>'
}

# 翻訳ファイルを読み込む
filename = './game/tl/english/script.rpy'
with open(filename, 'r', encoding='utf-8') as f:
    lines = f.readlines()

str_origin = ''
lines_t = []

# 元の文言を抽出して翻訳するループを回す
# 雑です
for i, line in enumerate(lines):
    if line.find('"') > 0:
        str_origin = line[line.index('"')+1:line.rindex('"')] # 最初の"と最後の"間を取る。regexでいい感じに書きたい
        if len(str_origin) > 0: # 空行は翻訳しない
            print(str_origin)
            
            # 翻訳本体はここ
            data['text'] = str_origin
            response = requests.post(url, headers=headers, data=data)
            translations = response.json()['translations']
            
            if len(translations) > 1: # 2個以上返ってくることがあるのかは知らない
                print("multiple translations found")
                print(translations)
            str_translated = translations[0]['text']
            print(str_translated)
            lines_t.append(line)
        else: # 空行("")があったら1つ前の行の翻訳結果を書き込む
            tmp = line[:line.index('"')]
            lines_t.append(tmp + '"' + str_translated + '"\n')
    else: # 翻訳と関係ないlineはそのまま
        lines_t.append(line)
    if i > 20:
        break

# 新しいファイルに翻訳済みのものコピーする
# 上書きしてもいいが、怖いので……
filename_t = './game/tl/english/script_t.rpy'
with open(filename_t, 'w', encoding='utf-8') as f:
    f.writelines(lines_t)
```
結果はこう！いい感じにできてますね。
```Python:script_t.rpy
# game/script.rpy:96
translate english main_0b40472a:

    # n "窓から眩しい朝日が差し込んでいる。門出に相応しい良い天気だ。"
    n "The morning sun shines brightly through the window. It is a beautiful day to start the new year."

# game/script.rpy:97
translate english main_c6de125e:

    # n "いびきをかいて眠っている彼に気づかれないよう、そっとベッドから抜け出し、着古した祭服の袖に手を通す。"
    n "I quietly slip out of bed and slip my hands into the sleeves of my worn-out ceremonial dress, so as not to be noticed by the snoring, sleeping man."

```

# やり残していること
- 台詞内のにescapeしたdouble quotesなどが含まれると、Pythonで読み込んだ時点で消えるので対処が必要
- 台詞内にタグが含まれる場合は翻訳にもそのままタグが残りそう
    - すべての場合で残るかは分からない
    - 2023/06追記：タグは変な場所に移動していたり一部消えたりする
- 英文になると文字数がすごく増えるので今までのダイアログボックスに入り切るか分からない(困ったぞ)
    - 2023/06追記：Ren'Pyでは翻訳だけ複数ダイアログに跨がせることも可能なので、問題なし！

# おまけ
この記事がためになった！という人はぜひぜひ著者の作ったビジュアルノベル「灰色の夜が明けたとき」をプレイしてくださいね！読んで応援！
検索すると出てきます。