ある OSS ドキュメント翻訳事例 「Crystal のドキュメントを翻訳した」


この記事は [Crystal Advent Calendar 2015](http://www.adventar.org/calendars/800) の21日目の記事です。

昨日は、[mihyaeru21](http://qiita.com/mihyaeru21) さんによる [Crystal 0.7.4の時代に作ったbotを0.9.1に対応させてStreamingを使う](http://qiita.com/mihyaeru21/items/6e3a069ca7f0064c2921) でした。破壊的変更を大胆に入れられるのも今でこそだと思うので、より良くなっていくなら後方互換性を切っていくのも大歓迎です。ですが、ハマっちゃうことも多そうなのでこのように実際の事例でまとまってると嬉しいですね。あとは、ライブラリ作者の方々にはほんとに頭が下がります…

## Crystal のドキュメント翻訳

さて、タイトルの通り、Crystal のドキュメントの翻訳をしたので、そのやり方なんかをまとめます。自分でも忘れそうだし、あとは「コントリビュートよろしくね！」って意味も込めて。

で、あくまでこれは Crystal のドキュメントに関する話になりますが、有志による OSS のドキュメント翻訳的なやつだと大体似たような形で進めることが多いのではないでしょうか。

ただ、そういう場合の具体的な事例に関する情報がかなり少ないので、どちらかというとそういった翻訳事例としての面をアピっていきたいと思います。

Crystal に限らず、「翻訳で OSS に貢献したいぜ」と思っている方の参考になれば嬉しいです。

## 翻訳の流れ

OSS のドキュメントのように「継続的に更新される」ことが前提であるものを翻訳する場合、大きく「初回の翻訳」と、以後の「継続的な翻訳」とに分けて考えることができます。

そして、この記事は、どちらというと後者の「継続的な翻訳」にフォーカスした内容になります。

これは、「継続的な翻訳」を維持することこそが、OSS のドキュメント翻訳において大いに頭を悩ませてくれるところであることと、また、単純に Crystal のドキュメント翻訳はすでに「初回の翻訳」が完了しているから、ということが理由です。

### 初回の翻訳

初回の翻訳は普通です。普通に目の前にある他言語の文章 (主にこれはファイル) を翻訳すればいいです。これは特に OSS のドキュメント翻訳の場合に限らないので、特に大きくは触れません。

といっても、初回の翻訳はもちろん超重要です。ここで訳語の選択や文体のノリが決定され、おそらくは以後の翻訳に対してもずーっと影響し続けます。

もう1つは、初回の翻訳の時点で、必ず翻訳メモリツールを使って翻訳し、翻訳メモリを作っておく必要があります。OSS のドキュメント翻訳に限らず、これをやっておかないと後でかなり辛いと思います。

Crystal のドキュメント翻訳では、最初から OmegaT という翻訳メモリツールを利用しました。ただ、翻訳メモリについては後述するのでここではこれ以上触れません。

### 継続的な翻訳

この記事のメインテーマ (のつもり) です。Crystal のドキュメント翻訳はすでに一度全体を翻訳し終わっていますが、原文の方は日々更新され続けています。したがって、翻訳が up-to-date になるように原文の更新を継続的に反映しなければいけません。

そのために、以下のような流れで翻訳を更新しています。

1. ドキュメントの原文が更新されたかどうかチェック
2. 更新がある場合、それを翻訳メモリツールにインポートする
3. 翻訳メモリツールで更新分を翻訳する
4. 翻訳メモリツールからエクスポートし、翻訳ドキュメントに反映する

こう書くと簡単に聞こえます。確かに流れとしては単純なものでその通りなのですが、例えば、

- 翻訳している対象の原文 (例えばバージョン) を厳密に把握する
- 複数人でコラボレートして翻訳する

などを考慮しなければならず、実際にやってみるとこれがマジで面倒なことだらけでやってらんねー、って感じになります。

したがって、ここからは、より具体的な手順を通して、Crystal のドキュメント翻訳ではどのように上記の流れを行っているか、について書いていきます。

## 登場人物

これから出てくる登場人物を紹介するよ！

似たような名前がいっぱい登場するしてわけわかんなくなるから、まずは見通しが良くなるように、翻訳作業用のディレクトリ構成を書いておきましょうか。実際にはもっと他にもファイルなどがありますが、説明するのに必要なものだけ。

```
ja.crystal-lang.org-workspace
├── crake.cr
├── crystal (submodule)
├── ja.crystal-lang.org (submodule)
└── ja.crystal-lang.org-omegat (submodule)
```

### ja.crystal-lang.org-workspace リポジトリ

Crystal の翻訳作業をするための作業場所です。要は翻訳で必要になる外部のリポジトリやタスクをまとめるためのプロジェクトとして利用します。

この作業場所自体が1つの [Git リポジトリ](https://github.com/crystal-jp/ja.crystal-lang.org-workspace)になっていて、配下にある `crystal` や `ja.crystal-lang.org` などは submodule として追加された外部のリポジトリです。

### crystal リポジトリ (submodule)

本家 [Crystal](https://github.com/manastech/crystal) のリポジトリです。翻訳元となる原文ファイルの取得に使用します。

Crystal はドキュメントが別ブランチで管理されているので、必要になる [`gh-pages`](https://github.com/manastech/crystal/tree/gh-pages) ブランチのみを submodule として含めています。

### ja.crystal-lang.org リポジトリ (submodule)

実際に [Crystal 日本語ページ](http://ja.crystal-lang.org)として公開するファイルのための[リポジトリ](https://github.com/crystal-jp/ja.crystal-lang.org)です。

翻訳したドキュメントをこのリポジトリに push することで公開されます。

### ja.crystal-lang.org-omegat リポジトリ (submodule)

OmegaT という翻訳メモリツールのプロジェクトのためのリポジトリです。

詳しくは OmegaT のところで。

### crake.cr

[CRake](https://github.com/MakeNowJust/crake) という、Crystal 製の Rake-like なライブラリのタスクを定義したプログラムです。

継続的な翻訳で何度も実行する工程 (主に色んな Git コマンドの実行やファイルコピー) をタスクとしてラッピングして手間なく実行できるようにしています。後で実際にどういったタスクがあるのかを紹介します。

### OmegaT

これまで何度も出てきた翻訳メモリツールの [OmegaT](http://www.omegat.org) です。翻訳作業では主役と言えるでしょう。

翻訳メモリツールというもの自体については詳しく説明しませんが、「翻訳メモリ」という「原文」「訳文」が文節でペアとなったデータを利用することで、翻訳を再利用可能にするものです。

特に継続的な翻訳においては、何かしらの翻訳メモリツールを利用するのが必須だと思います。翻訳メモリツールには、他にも [Google Translator Toolkit (GTT)](https://translate.google.com/toolkit/) などがありますが、OmegaT が、

- OSS である
- 翻訳メモリツールとして最低限の機能がある
- デスクトップアプリケーションとしてファイルシステムベースで動作する
- UI も直感的で使いやすい

などの理由から、この OmegaT を選択しています。

~~このように優秀なツールではあるのですが、OmegaT には細かい設定ができないという結構致命的なところもあって、一番困るのは Git 連携を切れないというものです。そのせいで、「OmegaT プロジェクトを Git 管理にすると勝手にリモート master に push する」といった動作がデフォルトになってしまいます。ブランチの変更すら許されず、remote ブランチを削除するとプロジェクト自体が開けなくなったりします。これは正直マジで困るので、通常であれば「OmegaT プロジェクトは Git 管理しない」といった判断も必要になります。~~

~~私の場合は、[OmegaT 自体に Git 連携しないようにパッチを当てたもの](https://github.com/5t111111/omegat)を自前でビルドして使っています。もし一緒に翻訳してくれる人がいたら、(もっと上手いやり方が見つからない限りは) 同様にこの野良ビルド版を使ってもらうことになると思います。~~

(2015.12.27 追記) [YU-TANG](https://twitter.com/yu_tang_) さんに教えてもらって、コマンドライン引数 `--no-team` を与えることにより Git 連携を無効にできるとのことです！

```text
$ java -jar OmegaT.jar /path/to/project --no-team
```

わざわざコードいじってビルドし直す必要はありませんでした…でもこれは嬉しい。ありがとうございます！

## 継続的な翻訳を行なうための具体的な手順

さて、ようやくここから翻訳作業の具体的な手順を見ていきます。

### 作業環境の準備

前述のように、各工程でタスクを実行するために Crystal 製の CRake というものを使っています。実行時にコンパイルするため、Crystal のコンパイラが必要になります。

以下を参考にインストールしてください。
http://ja.crystal-lang.org/docs/installation/index.html

一応、翻訳が Crystal のプロジェクトなので Crystal を使っていますが、Rake などのツールで書き換えるのも簡単だと思います。

また、初めての場合には作業用のリポジトリを clone してください (submodule を含むため `--recursive` が必要) 。

```text
$ git clone --recursive git@github.com:crystal-jp/ja.crystal-lang.org-workspace.git
```

clone したら、CRake などの Crystal パッケージをインストールするために以下を実行します。

```
$ crystal deps
```

### 本家 (原文) の更新をチェックする

作業場所が準備できたら、まずは本家 (原文) の更新をチェックします (以降、本家のリポジトリを upstream と呼びます) 。チェックするためのタスクは `update:check` なので、以下のようにして実行します。

```text
$ crystal crake.cr -- update:check
```

upstream に更新があった場合、以下のように出力されます。

![スクリーンショット 2015-12-17 16.08.04.png](https://qiita-image-store.s3.amazonaws.com/0/29846/24d52213-60c2-12ab-2e01-227dd5c11d55.png "スクリーンショット 2015-12-17 16.08.04.png")

このように、

- 前回 pull した時点から新たに積まれたコミット履歴
- 変更の差分 (翻訳対象となる Markdown ファイルのみ

が一望できてなかなか便利です。

### upstream の更新を merge

upstream に更新があることがわかったら、その更新を pull してsubmodule にも反映します。これも `update:pull` としてタスク化されています。

```text
$ crystal crake.cr -- update:pull
```

このとき、以下のように出力されます。

![スクリーンショット 2015-12-17 16.25.17.png](https://qiita-image-store.s3.amazonaws.com/0/29846/ce53a3bb-c46e-3172-5da0-a1fc6a0ddee7.png "スクリーンショット 2015-12-17 16.25.17.png")

これで新しいコミットが pull され、ローカルの `crystal` リポジトリが更新されます。

また、画面にあるように、コミットが pull されたら、`.config.yml` というファイルへの書き込みが行われます。このファイルは現在の原文ファイルに対応するコミットを特定したり、作業場所の状態をトラッキングするために利用します。内容は、`status` タスクでいつでも確認可能です。

```text
$ crystal crake.cr -- status
HEAD: 7fcb9fd
Copied?: false
```

### 更新ファイルの OmegaT へのインポート

upstream の更新を持ってきたら、それらを翻訳元の原文ファイルとして OmegaT にインポートします。これは `udpate:copy` というタスクです。

```text
$ crystal crake.cr -- update:copy
```

これにより、`ja.crystal-lang.org-omegat` の `source` ディレクトリに更新ファイルがコピーされ、同時に `.config.yml` の `Copied?` フラグに `true` がセットされます。

```text
$ crystal crake.cr -- status
HEAD: 7fcb9fd
Copied?: true
```

これで、翻訳する準備ができました。

### OmegaT で翻訳をする

OmegaT で、`ja.crystal-lang.org-omegat` プロジェクトを開きます。

![スクリーンショット 2015-12-20 9.18.39.png](https://qiita-image-store.s3.amazonaws.com/0/29846/5218a684-9d55-69bb-6491-97fc372f2efe.png "スクリーンショット 2015-12-20 9.18.39.png")

更新や追加があった文節は、以下のように未翻訳の状態 (青) になっているので、右ペインに表示される「参考訳文」なども参考にしながら気合で翻訳します。

![スクリーンショット 2015-12-20 9.22.41.png](https://qiita-image-store.s3.amazonaws.com/0/29846/963a4c40-4b91-f00a-63cf-bc45051f295e.png "スクリーンショット 2015-12-20 9.22.41.png")

翻訳が完了したら、「訳文ファイルの生成」メニューから訳文ファイルを生成します。生成された訳文ファイルは、`ja.crystal-lang.org-omegat` の `target` ディレクトリに保存されます。

### 翻訳したファイルを取り込む

翻訳が完了したので、次はそれらの翻訳済みファイルを `ja.crystal-lang` リポジトリに取り込みます。

この工程も `release:copy` としてタスク化されています。

```text
$ crystal crake.cr -- release:copy
```

これを実行すると、`ja.crystal-lang.org-omegat` の `target` ディレクトリから、`ja.crystal-lang` の該当するディレクトリにファイルがコピーされます。

### 翻訳したドキュメントを公開する

これで翻訳が完了しました！この後の工程は、

1. 翻訳したファイルを commit して pull request を出す
2. pull request をマージする
3. 翻訳したファイル (Markdown) から HTML をビルド
4. ビルドした HTML を commit して push
5. 公開！

という流れになりますが、「3. 翻訳したファイル (Markdown) から HTML をビルド」以降の工程は CI によって自動化されているため、実際に行うのは「2. pull request をマージする」までとなります。

では、まずは期待通りに翻訳したファイルがコミットされるのか、`release:check` というタスクで念のためチェックしてみましょう。

```text
$ crystal crake.cr -- release:check
* Files to be committed ********************************************************
 M _gitbook/syntax_and_semantics/as.md
 M _gitbook/syntax_and_semantics/literals/char.md
 M _gitbook/syntax_and_semantics/literals/string.md
 M _gitbook/syntax_and_semantics/operators.md
 M _posts/2014-04-27-type-inference-rules.md
```

うん、大丈夫そうですね。では commit しましょう。`release:commit` というタスクを使います。

```text
$ crystal crake.cr -- release:commit
HEAD: 7fcb9fd
Copied?: true

* Branch **********************************************************************
Switched to a new branch '7fcb9fd-2015-12-20'
7fcb9fd-2015-12-20

* Commit message **************************************************************
Translate _gitbook/syntax_and_semantics/as.md, _gitbook/syntax_and_semantics/literals/char.md, _gitbook/syntax_and_semantics/literals/string.md, _gitbook/syntax_and_semantics/operators.md, _posts/2014-04-27-type-inference-rules.md (7fcb9fd)
```

なんと嬉しいことに、このタスクを実行することで、翻訳したベースコミットや日付からトピックブランチ (上記の例の場合は `7fcb9fd-2015-12-20`) を自動的に作った上で commit してくれます。

次はこのトピックブランチを push して、普通に pull request を出しましょう。ここは残念ながらまだ手動です…

![スクリーンショット 2015-12-20 9.50.50.png](https://qiita-image-store.s3.amazonaws.com/0/29846/e64e6daa-4b4b-fdfa-34c9-2fe38c272f7a.png "スクリーンショット 2015-12-20 9.50.50.png")

あとはこの pull request を `gh-pages` ブランチにマージしたら、自動的に Travis CI によって HTML のビルドが走り、生成された HTML ファイルが自動的に commit されます。

こうして、めでたくドキュメントが最新の状態に更新されました！

![スクリーンショット 2015-12-20 9.58.25.png](https://qiita-image-store.s3.amazonaws.com/0/29846/1d721199-b5a0-c96d-10af-91057881d8c9.png "スクリーンショット 2015-12-20 9.58.25.png")

## まとめ

このように、翻訳には複数のツールやプロジェクトが絡むため、何かしらそれをまとめる枠組みが必要になると考えています。今回の事例では、作業場所として GitHubに リポジトリを用意し、submodule 無双で翻訳プロジェクトを管理する選択をしました。その上で CRake のタスクとして手順をラップすることで、ややこしい操作をある程度シンプルにすることができたと思っています。

ただ、もちろん課題がいくつも残っていて、

- 自動化が中途半端
- 実際に複数人でコラボしたわけじゃないので、このやり方がうまく機能するか不明
- 覚えることが多くて敷居が高い

などについて改善しないとなー、と感じています。

とはいえ、最初にも書いた通り、こういった事例の具体的な手順というのは結構レアなので、ぶっちゃけわりと貴重な記事になっているのではないかと自画自賛しているところです。

## Crystal のドキュメント翻訳にコントリビュートしたい！

ドキュメントに間違いを見つけちゃいましたか？訳語でもっと適切なものがありましたか？いつまで待っても up-to-date になりませんか？

ぜひコントリビュートしてください！issue で指摘していただいてもよいですが、直接修正していただけるともっと嬉しいです。やり方は2つあります。

### ja.crystal-lang.org をフォークして pull request を出す

普通に GitHub で `crystal-jp/ja.crystal-lang.org` をフォークして、修正した内容で pull request を出してください。

修正をマージさせてもらった後、こちらで翻訳メモリに反映します。

### OmegaT を使ってガッツリやりたい

この記事で記載した手順でやっていただけるということですね！ひとまず、[Crystal-JP の Slack](http://crystal.pine.moe) や [Gitter](https://gitter.im/crystal-jp/ja.crystal-lang.org) などでお知らせください！ 

## おわりに

こないだ RailsGuides の翻訳などで知られる [@yasulab](https://twitter.com/yasulab) さんと話したとき、かなり素敵な継続的翻訳システムを開発している、との話を聞かせてもらいました ([このスライド](https://speakerdeck.com/yasulab/continuous-translation-system-at-rwc2015?utm_content=buffer2aa3c)で見ることができます) 。これ、本当にライフチェンジングな翻訳システムになりそうな期待感があるので楽しみにしています。

※「Crystal で使わせてださい！」って言ってたんですけど、ぶっちゃけ Crystal のドキュメントは今のところあんまり更新がないので、せっかく使わせてもらっても微妙かも…と思ってしまいました。

さて、[Crystal Advent Calendar 2015](http://www.adventar.org/calendars/800)、明日は tebakane さんの記事の予定です！