注意

この記事の内容は、開発途中のものです。 今後、変更などを行う可能性が高いのでご注意ください。

はじめに

Steamで配信されているゲーム「Drag’n Wash」には、TomXVさんが作っている多言語化MOD「Drag’n Wash Localization」があります。

今週は、このMODに2つの形で関わりました。

  • Pull Requestを2本出したあと、MODのリポジトリ全体をコード監査し、見つかった不具合を11本のPull Requestにまとめて取り込んでもらった
  • 翻訳者向けのエディター「dwloc」を新しく作った(223n/dragnwash-localization-editor

MODとエディターは、どちらも非公式のファン制作物で、ゲームの開発元とは関わりがありません。 なお、MOD本体の設計や今週のリリース(v0.6.0からv1.2.0)は、TomXVさんの作業です。

Drag’n Wash Localizationの仕組み

Drag’n Wash Localizationは、BepInEx 5のプラグインとして動く翻訳MODです。 ゲームのファイルは書き換えず、表示される直前の文字列を訳に差し替えます。

翻訳はTranslations/<言語>/strings.csvを編集するだけで追加でき、コードを書く必要はありません。 今週の時点で、日本語や簡体字中国語をはじめ16の言語に対応しています。

このMODには、英語の台本をリポジトリに置かないという方針があります。 CSVには英語の原文ではなく、そのハッシュだけを載せています。 翻訳者はゲーム内のツールウィンドウで、原文つきの作業用ファイルを書き出し(Export working copy)、訳を書いたら原文を除いた公開用のファイルに作り直して(Hash for commit)からコミットします。 製品版を持っている人だけが翻訳できる仕組みです。

最初の2本のPull Request

配布物を作る前の参照アセンブリの確認

最初に出したPull Request(#5)は、1行の修正でした。

リリース用のzipを作るスクリプトは、ビルドの前に、必要な参照アセンブリがそろっているかを確かめます。 その一覧から、Yarn.Google.Protobuf.dllだけが抜けていました。 このDLLだけが欠けた環境では、事前の確認を通り抜けたあと、ビルドが10件のエラーで失敗します。 しかもエラーの文面は参照アセンブリの置き場所に一言も触れないので、原因にたどり着きにくい状態でした。

Defenderの誤検知に当たった人の逃げ道を整える

2本目(#7)は、Microsoft Defenderの誤検知への対応です。

当時のインストーラー(Install.exe)は、DefenderにTrojan:Script/Wacatac.B!mlとして検出されることがありました。 末尾の!mlは、既知のウイルスと一致したのではなく、機械学習で怪しいと推定されたことを表します。

検出そのものは、このPull Requestでは直していません。 代わりに、検出された人がInstall.exeなしでも導入を進められるように、次の点を整えました。

  • Install.exeの代わりに使うInstall.cmdを、zipを展開せずに実行すると何も表示せずに終わっていたので、Install.exeと同じ案内を出すようにする
  • ダウンロードしたzipのSHA-256を、TrueFalseで答える1行のコマンドで照合できるようにする(それまでは、大文字と小文字や接頭辞の違う64文字を目で見比べる手順だった)
  • 隔離されたときに何をすればよいかの案内を足す

その後、TomXVさんがインストーラーを毎回同じバイト列でビルドされるように直し、v1.1.0ではModFramework共通のインストーラーに置き換わっています。

リポジトリ全体のコード監査

範囲と分類

2本のPull Requestのあと、リポジトリ全体を監査しました。

対象はC#のプラグイン(20ファイルで約4,300行)、macOS用の実験的なインストーラー、PythonとPowerShellの道具です。 さらにGitHub Actionsのワークフロー、翻訳データ(監査の時点では13言語分)、文書も含めました。

指摘は重大度で「高」「中」「低」と、不具合ではない「整理」に分け、それぞれを「現象」「根拠」「再現条件」「修正案」の順に書きました。

実害が大きかった指摘

確定した指摘は38件です。 その中で実害が大きいと判断したのは、翻訳者の作業が失われる経路の4件でした。

指摘 起きること
公開ファイルを作り直すPowerShellのスクリプトが、ゲーム内のボタンと違う動きをする すでに公開していた訳が消える
作業用ファイルの書き出しをやり直すと、まだ公開用に作り直していない訳を上書きする 書いたばかりの訳が消える
翻訳ファイルの書き込みがアトミックでない 書き込みの途中で落ちると、ファイルが壊れる
コメント行の除き方が、C#とスクリプトで食い違う 同じファイルでも、道具によって読める行が変わる

このほか、フォークからのPull Requestが作った成果物を、書き込み権限のあるワークフローがそのまま信用していた点も「高」に分類しました。 また、先頭が#の値をCSVへ書くとき引用符で囲まず、読み直すとコメント行と見なされて行ごと消える不具合も見つかりました。

指摘そのものを疑う

監査の指摘は、間違っていることもあります。 そこで、各指摘に対して、反証を試みる検証と、具体的な失敗の手順を組み立てる検証の2通りをかけました。

最初の17件のうち、6件に反証が出ました。 内訳は、前提の誤りによる取り下げが2件、主張の訂正が4件です。 また、3件を不具合ではなく整理として位置づけ直しました。 たとえば取り下げた1件は「空行を壊れた行として数えている」という指摘でしたが、PowerShellのConvertFrom-Csvはデータ行の間の空行をもともと読み飛ばすため、前提から成り立っていませんでした。

あとから4つの領域(macOSのインストーラー、ツールウィンドウ、翻訳データの整合性、文書の整合性)を追加で監査したときも、28件のうち6件を取り下げています。 このときの検証で見つかった誤りは事実の誤り、根拠の誤り、修正案の誤り、主張の過大の4種類でした。 このうち修正案の誤りは、指摘は正しいのに修正案が効かないというもので、採用した指摘にも含まれていました。 指摘が正しいかどうかと、修正案が正しいかどうかは、分けて確かめる必要があります。

実機で動かして分かった、修正の欠陥

監査をした環境では、C#のビルドとPowerShellの実行ができなかったため、この2つの指摘はコードの読解だけで進めました。 macOSのインストーラーの指摘は、該当する箇所を切り出してbashnodeで実行し、修正の前後を比べました(macOSの実機では動かしていません)。 その後、Windows 11の実機でdotnetpwsh(PowerShell 7)を使って修正を動かすと、修正そのものの欠陥が見つかりました。

  • ファイルをアトミックに書き込むための修正が、ほかのプロセスが開いているファイルへの書き込みを失敗させていた(修正による退行)
  • コメント行の扱いを直した修正が、PowerShellでは改行を含む値を1つのレコードとして読めず、直そうとした食い違いがそのまま残っていた
  • 版の番号の食い違いを検査する修正が、検査すべき項目を1つ見落としていた

このうち書き込みとコメント行の件は、実行して確かめないまま、修正を断定的に書いていたことが原因です。 監査のレポートにも、そのとおり書き残しました。

さらに、実機での検証を受けて書き直したコメント行の修正にも、退行がありました。 ヘッダーの上でコメント行の直後に空行があるだけで公開ファイルが空になり、翻訳の1,680行がすべて消えたうえ、正常終了してしまうというものです。 この修正は実データで出力が変わらないことを確かめていましたが、試したファイルにはこの並びがありませんでした。

最後に、実際にゲームを起動して、修正を1本ずつ入れ替えながら前後の挙動を比べました。 PowerShellのスクリプトが作る公開ファイルは、ゲーム内の「Hash for commit」が作るものと、改行コードを除いて1,721行すべてが一致しました。 全部の修正を入れたビルドでも、BepInExのログにエラーと警告は1件も出ませんでした。

37本のPull Requestを11本にまとめ直す

修正は当初、その時点で確定していた37件の指摘と1対1で、37本のPull Requestに分けていました。 ところが、同じファイルに触れる修正が多くありました。 当時はレポート本体と、取り下げた指摘に対応する2本を含めて、40本のPull Requestがありました。 このうち変更するファイルが重なる60組をgit merge-treeで試すと、4組が衝突しました。 しかも4組とも、衝突した箇所で片側の変更だけを採ると、結果が誤りになる形でした。

そこで、変更するファイルの範囲で修正をまとめ直し、11本に再編成しました。 まとめ直す途中でコードを読み直し、#で始まる値が行の先頭に来るとCSVの往復で行ごと消える不具合を、38件目の指摘として見つけました。 これも11本のうちの1本で直しています。 11本のすべての組み合わせ(55組)で衝突が0件になり、全部を統合したツリーのビルドも警告0件、エラー0件でした。 取り込み順の制約もなくなり、残ったのは「道具の修正を文書の修正より先に取り込む」という推奨だけでした。

11本はTomXVさんに確認してもらい、9月17日にすべてマージされました。 監査のレポート本体はMODのリポジトリには入れず、修正のPull Requestだけを取り込んでもらっています。

翻訳者向けのエディター「dwloc」

なぜ専用の道具が要るのか

翻訳者の作業には、専用の道具がないと扱いにくい点が3つあります。

  • 公開用のCSVは、#で始まるコメント行や見出しを含む生成物で、Excelなどで開いて保存すると壊れる
  • 保存は単純な書き戻しではなく、ゲームの台本の順に並べ直して作り直す
  • ゲームの外で公開用に作り直す道具はPowerShellのスクリプトで、macOSやLinuxの翻訳者は、PowerShellを別に入れないと使えない

そこで、これらの作業を1つにまとめるエディター「dwloc」をGoで作りました。 Windows、macOS、Linuxで動く実行ファイル1つだけで、ほかに入れるものはありません。

画面をブラウザーに任せた理由

UIの作り方は、次の5つを比べました。

方式 評価
ローカルのWebサーバーとブラウザー 採用。IMEと右から左に書く言語をブラウザーに任せられる
Wails v3 調査の時点でベータ版
Fyne IMEの不具合が長く未解決
Gio 実装の手間が大きい
TUI(端末の画面) IMEと右から左に書く言語の扱いが難しい

このMODにはヘブライ語のように右から左に書く言語があり、日本語や中国語の翻訳者はIMEで入力します。 この2つをブラウザーに任せれば、自分で書かずに済みます。 また、CGOに依存しないので、1台の開発機からWindows、macOS、Linuxのそれぞれx64とARM64向けの6種類のバイナリを作れます。

翻訳の共同作業でよく使われるWeblateのようなサービスは採れませんでした。 英語の原文をサーバーに置く必要があり、英語の台本を再配布しないというMODの方針と衝突するためです。

同じ方針から、ブラウザーの翻訳機能が原文を外部へ送らないよう、dwlocの画面には翻訳を止める指定を付けています。 また、翻訳者ひとりが手元で使う道具なので、画面は127.0.0.1だけを待ち受け先にし、起動のたびに作るトークンを持つブラウザーからしか操作を受け付けません。

主な4つのサブコマンド

コマンド 役割 元の道具
dwloc publish 公開用のCSVを作り直す tools/hash-strings.ps1
dwloc validate 公開用のCSVの形式を検査する tools/check-translations.py
dwloc diff 未翻訳の行、台本から消えた行、訳の引き継ぎ候補を並べる なし
dwloc edit ブラウザーで訳を書き換える なし

publishが元の道具と同じものを作れているかは、MODのリポジトリにある16言語すべての公開用CSVを入力にして通し、入力とバイト単位で一致するかで確かめています。

diffの引き継ぎ候補は、ゲームの更新で原文のハッシュだけが変わった行を見つけて、前の訳を引き継げるようにする機能です。 9月14日のゲームの更新では25行のキーが変わりました。 これを再現して試すと、候補を24件出し、誤った候補は0件でした。

訳を書き換える画面

MODのリポジトリを手元に取得したフォルダー(Translationsフォルダーと同じ場所)にdwlocを置き、サブコマンドを付けずに起動する(実行ファイルをダブルクリックする)と、ブラウザーで編集の画面が開きます。 ゲームのフォルダーはSteamのライブラリの情報から自分で探すので、多くの場合は場所を指定しなくて済みます。 候補が2つ以上見つかったときや、日本語などを含む場所にSteamを入れているときは、--gameでフォルダーを指定します。

  • 入力が1.5秒止まるか、入力欄から離れると自動で保存する
  • 保存では、書き換えた行の訳の欄だけを差し替え、それ以外は1バイトも変えない
  • 保存先がゲーム側の作業用ファイルなら、ゲームが約2秒で読み直し、画面に反映する(ゲームが編集中の言語で動いていて、ModFrameworkのDeveloper toolsがオンのとき)
  • 未翻訳や要確認などの条件で行を絞り込み、原文や訳を検索できる(検索語はサーバーに送らない)
  • Enterで確定して次の行へ、/で検索欄へ、とキーボードだけで進められる

IMEで入力する人のために、変換中はキーを横取りしないようにしました。 さらに、変換を確定してから100ミリ秒以内のEnterは、次の行へ進む操作として扱いません。 変換を確定したEnterが、そのまま行送りとして届いてしまう場合があるためです。 この振る舞いは、Windowsの実機で実際のIMEを通して確かめました。

保存の直前には、ファイル全体のSHA-256を照合します。 画面を開いたあとにゲームなどがファイルを書き換えていたら、保存せずに止め、どちらの訳を採るかを選んでもらいます。

訳を消さないための安全装置

監査で見た「訳が黙って消える」経路は、dwlocでも起こりえます。 そのため、書き込む前に止まる条件を設けました。 publishが止まるときは、全言語について1バイトも書きません。

  • publish:作り直しで訳が1つでも消えるなら止まる(抜け道の設定は用意していない)
  • publish:ゲーム側の作業用ファイルを入力にしたとき、ゲームに入っている公開用のCSVの訳がリポジトリのものと食い違っているなら止まる
  • 編集の画面:届いた文字が正しいUTF-8でなければ、保存を断る

1つ目を入れる前は、古い作業用ファイルが残っていると、日本語の公開用CSVが約191KBから約39KBに縮み、しかも正常終了していました。 いまは、消えるはずだった1,361件を報告して止まります。

2つ目は、実機で3件の訳が黙って前の版に巻き戻ったことから足しました。 3つ目は、UTF-8として正しくないバイト列をブラウザー抜きで直接送ると、警告も出ないまま保存されていたことから足しました。 JSONを読む時点で、Goの標準ライブラリが壊れたバイトを置換文字(U+FFFD)へ置き換えるためです。 画面から使うかぎり、ブラウザーが常にUTF-8で送るため、この確認に引っかかることはありません。

配布と署名

リリースには、6種類のバイナリの書庫と、チェックサムの一覧を添付しています。

最初のv0.4.0では、添付が0件のまま公開されてしまいました。 リリースを公開してから書庫を載せる順番にしていたため、公開後のリリースを変更できない設定(immutable releases)に阻まれたのが原因です。 v0.4.1で、下書きのリリースに7つのファイルを載せてから公開する順番に直しました。

コード署名は、いまはしていません。 macOSでは、Apple Developer Programへの登録に年99ドルかかるためです。 Windowsでは、調査の時点で比較的安価な署名サービス(Azure Trusted Signing)が日本から使えず、個人で取れる証明書を買ってもSmartScreenの警告はすぐには消えないためです。

今週出した版

日付 主な内容
v0.4.0 2026-09-17 publishvalidatediffedit、6種類のバイナリ(添付は0件)
v0.4.1 2026-09-17 公開後に変更できないリリースでも、書庫を添付できるように修正
v0.4.2 2026-09-17 サブコマンドを省いたら編集の画面を始める、IMEの実機確認を記録
v0.5.0 2026-09-18 ゲームのフォルダーの検出、訳の消失と古い翻訳の検出、壊れた文字の拒否
v0.6.0 2026-09-18 上部の帯を畳めるように、3つのコマンドがゲームのフォルダーを探すように

まだ確かめていないこと

韓国語と中国語のIME、FirefoxとSafari、macOSとLinuxのファイルマネージャーから起動したときの振る舞いは、まだ確かめていません。

また、Drag’n Wash Localization自体が、BepInEx側の既知の不具合によりmacOSでは動きません。 そのため、macOSではdwlocがゲームのフォルダーを見つけられず、編集の画面は原文の欄が空のまま始まります。

おわりに

今週は、ほかの人のMODへの協力と、そのMODのための道具作りを並行して進めました。

監査では、指摘を出すだけでなく、その指摘と修正案が本当に正しいかを確かめる作業を重ねました。 コードを読むだけで書いた修正に、実機で動かして初めて分かる欠陥が残っていたことは、次に監査をするときにも覚えておきたい点です。

dwlocは、翻訳に協力してくれる人が、OSを問わず同じ手順で作業できることを目指しています。 使ってみておかしなところがあれば、リポジトリのIssueで教えてください。

参考サイト