DeepL API 変換モジュールで15ヶ国語のドキュメントを生成する

少し前にこういう記事を書いた。

https://qiita.com/kaz-utashiro/items/1c1a51a4591922e18250

テキストファイルの特定の範囲を DeepL の API にアクセスするコマンドを使って自動的に変換する  Greple コマンドのモジュールに関するものだ。これは、その続き。

## とりあえず15ヶ国語版を作った

せっかくのツールなので、自分自身のマニュアルを翻訳してみた。DeepL が翻訳可能な言語は `deepl languages` で見ることができて、現時点では31ヶ国語に対応している。この中から、ドイツ ([DE]), ギリシャ ([EL]), スペイン ([ES]), エストニア ([ET]), フランス ([FR]), インドネシア ([ID]), 日本 ([JA]), 韓国 ([KO]), オランダ ([NL]), ルーマニア ([RO]), ロシア ([RU]), トルコ ([TR]), ウクライナ ([UK]), 中国 ([ZH]) を選んで、元の英語を含めて15ヶ国語になる。GitHub の UI が変わりサイドペインが出るようになって使いやすくなった。

![スクリーンショット 2023-02-28 13.22.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/f2d72b68-548d-39a1-e833-bf1d52c1560a.png)

https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.JA.md

[DE]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.DE.md
[EL]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.EL.md
[ES]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.ES.md
[ET]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.ET.md
[FR]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.FR.md
[ID]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.ID.md
[JA]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.JA.md
[KO]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.KO.md
[NL]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.NL.md
[RO]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.RO.md
[RU]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.RU.md
[TR]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.TR.md
[UK]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.UK.md
[ZH]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/README.ZH.md

[Makefile]: https://github.com/kaz-utashiro/App-Greple-xlate/blob/main/docs/Makefile

`docs` ディレクトリの下に [Makefile] を用意してあるので、ここで `make` を実行すれば必要なファイルを更新する。ある程度汎用的に作ってあって、同様な構成のパッケージであれば、`Makefile` をコピーするだけで利用可能だ。ちょっと工夫すれば、GitHub Actions で自動的に生成することもできる。

各国語に変換した内容が正しいのかはまったくわからない。でも、日本語を読んで理解できる内容になっていれば、だいたい合っていると判断していいんじゃないだろうか。おかしいと思ったら、意図した内容に翻訳してくれるように元の英語を修正する。ファイルを更新すると、その部分だけを翻訳して結果も表示されるので、納得できる内容になるまでそれを繰り返せばいい。

## Makefile

最近、何でもコード化する流れがあるためか、ひと昔前よりシェルスクリプトを使う人は増えているように感じるが、make を使えばよさそうなところでベタベタとスクリプトを書いてたりする。結果として、ちょっとだけ違う似たようなスクリプトがあちこちにできて、管理が大変ということになる。make を使ったことがないとか、存在自体を知らない人もいるのかもしれないので、以下 [Makefile] の中身を少し解説する。

基本的には、次のコマンドを使って、指定したパートを `deepl` コマンドを使って翻訳した結果に置き換える。詳しくは[先の記事](https://qiita.com/kaz-utashiro/items/1c1a51a4591922e18250)を。これに加えるオプションや、どのファイルに対して実行して、結果をどのファイルに格納するかなどを相互の依存関係と共に管理するのが Makefile の役割だ。

```shell
greple -Mxlate::deepl --match-podtext --xlate-format=none --all --need=0
```

### 翻訳しないセクションを指定

```Makefile
IGNORE := AUTHOR LICENSE
```

AUTHOR と LICENSE のセクションは翻訳しないことを宣言している。こうすると

```Makefile
ifdef IGNORE
$(foreach ignore,$(IGNORE),$(eval \
  XLATE += --exclude '^=head\d[ ]+$(ignore)\b(?s:.*?)(?=^=|\z)'\
))
endif
```

によって `--exclude` オプションが追加される。これによって `=head1 AUTHOR` からはじまって、次の `=` で始まる行までが処理対象外になる。ちなみに、最初の宣言を `:=` ではなく `?=` にしておくと、設定されていない場合だけ初期化するという意味になるので、よりポータブルになる。

### 言語固有のフィルターを設定

以下の宣言で、日本語版だけに適用される専用のフィルターを指定している。韓国語に適用するフィルターを作りたければ、同様に `KO_FILTER` という変数を設定すればいい。

```Makefile
JA_FORM   ?= desumasu
JA_DICT   := $(wildcard *.dict)
JA_FILTER := greple -Mperl -Msubst::desumasu \
	$(if $(findstring $(JA_FORM).dict,$(JA_DICT)),--dict $(JA_FORM).dict) \
	--pod --subst --all --no-color --need=0 \
	--$(JA_FORM) 
```

DeepL による変換は、意味的には高品質だが、様々なデータを学習しているためなのか言葉遣いが不統一で、特に語尾の「ですます調」と「である調」がバラバラなのだ。`greple` の `subst::desumasu` モジュールを使って語尾を変換する。今は「ですます調」にしているが、`JA_FORM` を `dearu` に設定すれば「である調」になる。

「ですますフィルター」に関しては、この記事をどうぞ。この記事を書いたときより、辞書は少し更新してある。

https://qiita.com/kaz-utashiro/items/8f4878300043ce7b73e7

この時、`desumasu.dict` と `dearu.dict` というファイルが存在すれば、それが適用される。`desumasu` モジュールはなんちゃって実装なので、精度の高い処理は期待できない。でも、対応できない表現を発見したら、この辞書に追加して場当たり的に処理するのが得策だ。近いうちに、AI に頼めばよろしくやってくれるようになるだろう。

ファイルの存在を調べる関数が見当たらなかったので `wildcard` と `findstring` で対応した。もっとストレートな方法があるかもしれないので、よい方法があったら教えてください。

このフィルターを適用しているのは以下の部分。

```Makefile
define LANG_PM
  $(SRCPATH)%.$1.pm: $(SRCPATH)%.pm Makefile
  ifdef $1_FILTER
	$$(XLATE) --xlate-to $1 $$< | $$($1_FILTER) > $$@
  else
	$$(XLATE) --xlate-to $1 $$< > $$@
  endif
endef
$(foreach lang,$(LANGS),$(eval $(call LANG_PM,$(lang))))
```

`LANG_PM` を `call` すると、`$1` に各言語名が入ってくる。だから `JA_FILTER` という変数が設定されていれば、そのフィルターが実行されるという寸法だ。

### インデックスを作る

これは、リンクページを作るだけなので、内容としてはどうってことない。

```Makefile
README.md: Makefile
	exec > $@ && \
	printf '## Languages\n\n' && \
	for md in $(MDS) ; \
	do \
	    echo "- [$$md]($$md)" ; \
	done
```

このように、複数のコマンドの結果をファイルにリダイレクトしたい場合どうしているだろうか。よくあるのは、2番目以降のコマンドの出力を `>>` でリダイレクトして追加する方法だ。この方法には、リダイレクト先のファイル名を何度も書かなければならないという欠点がある。うっかりすると for ループの中の echo コマンドをリダイレクトしてしまうが、もちろん for 文全体をリダイレクトした方がいい。

ではと思いつくのは、全体を `(` `)` で囲んでサブシェルで実行して、全体の出力をリダイレクトする方法だ。ファイル名は一度だけですむが、あまり美しくないのと、そもそもサブシェルで実行すると都合が悪い場合もある。

ということで上の例だ。`exec > file` で、実行中のシェルの出力をリダイレクトすることができる。だから、その後で実行するコマンドの出力は、すべてそのファイルに収まることになる。`&&` でつないであるが、別にそうする必要はないけど、当然同一のシェルで実行する必要はある。

## おわりに

今までずっと作ったツールのマニュアルは英語で書いてきた。英語がうまいわけでも好きなわけでもないが、それがより多くの利用者に伝えるために最善の方法だと思うからだ。もちろん日本語と英語の両方で書くことができればいいに決まっているが、管理し切れるわけがないのは試さずともわかる。ブラウザの自動翻訳機能などを使えば、大体の意味を掴むことはできるが、やはりイマイチだ。特に、翻訳すべきではない部分が妙ちきりんな日本語になってたりすると興醒めで、読み続けるのが辛い。今回のように、少なくともどの部分を翻訳するかを指定することができれば、体裁としてはまあまあ悪くないものができることがわかった。見出しなんか訳す必要はないだろう。翻訳の精度は、放っておけば勝手に AI が高めてくれるに違いない。

こうしてみると、元のドキュメントを日本語で書いて、英語に翻訳させた方がマシなものができるかもしれないと思い始めている。