本日、前々から作りたいと思っていたBaldur's Gate 3用のmodの翻訳ツールをついにリリースしました。
利用するフレームワークの選定や「とりあえず動くか試してみる」的なプロトタイプの作成も入れると3ヶ月ぐらいかかっています。
開発の経緯
初めてこのツールを作ろうと考えたのは昨年の11月、BG3が公式でmodサポートを実装して少し経った頃からでした。
https://store.steampowered.com/news/app/1086940/view/4593196713074709213?l=japanese
以前からmod開発が活発だったBG3のmodコミュニティがこのアップデートで更に勢いを増し、毎日たくさんのmodがnexusmodsやmod.ioで公開されるようになってから個人的に気になった点が一点。
外国の方が作成したmodは当然内部の言語も英語なので日本語で表示したければ日本語化作業が必要なのですが、BG3にはこれといったmodの日本語化用ツールがありません。
modコミュニティ最大手のSkyrimにはxtranslatorというツールがあります。
単にmod内部のテキストの編集だけでなく、一度翻訳した文章のパターンを辞書データとして保存しエクスポートして外部で公開できるなどの機能が充実しており、mod翻訳において支持を得ています。

ベースとなるゲームの翻訳も取り込み可能
BG3でもこのようなツールを作れば便利じゃないかな、と考えツール制作に移りました。
制作にあたっての基盤技術とフレームワークの選定
アプリの全体的な目標(xTranslatorっぽいもの)が定まった所で実際に何を使って開発するかという検証を始めました。
だいたい1ヶ月ぐらい色々試してみて結局従来のアプリ開発と同じTauriを採用。
C#(.Net)と違って依存関係が無くElectronより遥かに軽量、かつフロントエンド部分を比較的学習コストの低いWebの技術で制作できる。というTauri独自の利点はやはり魅力的です。
次にフロントエンド部分に何を使うかという点ですが……ここが一番悩みました、大体2ヶ月ぐらい試行錯誤してます。
それまで使ってきたVueやSolidJS、Svelteなどはそもそもエコシステムが充実しておらず、いい感じに実装できるデータグリッドのコンポーネントライブラリが中々見つからないのです。
Tanstack tableを使って自前で実装するという手もありましたがそこまでやろうとすると自分のスキルでは余計に時間がかかってしまいかねない。
試行錯誤した結果、導入事例も多く実装が容易なMUI/DatagridのあるReactを使うことにしました。
ボタン・読み込み中のデータ等のフラグやデータ変更のための状態管理にはjotaiを使用しました。
四苦八苦してContext張らなくても一箇所でデータを管理出来るし何よりDevtoolが使いやすいです。
export const translationAtom = atom<translation[]>([])
export const loadingFileAtom = atom<undefined|string>()
export const messageAtom = atom<message>({type: 0, text: ""})
export const autoTranslationAtom = atomWithStorage<boolean>("autoTranslation", true)
export const unSavedTranslationAtom = atom<boolean>(false);
こんな感じで適当なtsxファイルにデータを管理するatomを宣言し、

別途スタンドアロンのDevtoolを立ち上げなくてもオーバーレイコンポーネントを設置するだけで値の変化を簡単に確認できます。
制作開始。
使用する技術の選定も済んだ所で早速アプリ開発を開始。
当初は「翻訳データ(xml)の処理はTauriのrust基盤側でやらせて処理を高速化しよう」などとカッコ付けたことも考えましたが辞めました。
実際に検証したところ、難読化されたmodの文字列データを処理するxTranslatorと違って平文のxmlを処理すればいいTauriのBG3エディターではtypescript上の処理でも十分高速だったからです。
おそらく世界で一番文章量が多いであろうBG3ゲーム本体の翻訳データ(20万行以上)でも5秒かからずに読み込みが完了したので。rust側の処理は一切実装せずに全てをTypescriptで完結させるという方式を取りました。

実際にゲーム本体の翻訳データを読み込んだ状態、読み込んでから3秒か4秒ぐらいで読み込み完了してます。
全てをTypescriptで実装するという方針が定まってからは比較的開発もスムーズに進み、実際に着手してから5日程度でversion1.0のリリースまでたどり着きました。
初期版リリース・改良にあたって軽いつまづき。
成果物が形になり、基本的な翻訳データの編集・出力ができるようになった段階でとりあえずNexusにアップロードしてみました。
流石にこの頃はBG3のプレイヤー人口も減ってきたのもあって閲覧数は中々伸びなかったのですが、そんな中でもmod翻訳者の方からお褒めの言葉を頂けたのもあったのでペースを上げて当初想定していた自動翻訳機能の実装にも着手。
ここで若干開発が難航します。
翻訳辞書データの保存後に別の翻訳ファイルをドラッグ&ドロップで読み込んだ際、トグルスイッチでon/offできる自動翻訳フラグを参照してonの場合だけ自動翻訳を実行するような処理を追加したのですが、何回試してもフラグ判定が反映されなかったのです。
export const autoTranslationAtom = atom<boolean>(false);//jotaiで管理している自動翻訳フラグ
const [autoTranslation, setAutoTranslation] = useAtom(autoTranslationAtom);//フラグの呼び出し
const unlisten = listen("tauri://drag-drop", async (event) => {
//ここでドラッグアンドドロップの処理を実装
if(autoTranslation){ //判定を実施
//ここで翻訳データの自動翻訳を実装
}
}
まるまる1日かけて色々調べてみた結果「もしやjotaiはReact Hooksの一部だから外部のイベントハンドラでは値が正しく反映されないのでは?」と思い至って下記のように修正。
jotaiで管理していたフラグをローカルストレージと同期させ、イベントハンドラからはローカルストレージを直接参照するようにしてフラグが正しく反映されるようになりました。
//jotaiの特殊なAtomでフラグの判定値をローカルストレージと同期
export const autoTranslationAtom = atomWithStorage<boolean>("autoTranslation", true)
const [autoTranslation, setAutoTranslation] = useAtom(autoTranslationAtom);//フラグの呼び出し
const unlisten = listen("tauri://drag-drop", async (event) => {
//ここでドラッグアンドドロップの処理を実装
const autoTrans = localStorage.getItem("autoTranslation");//ローカルストレージから値を直接取得
if(autoTrans){ //判定を実施
//ここで翻訳データの自動翻訳を実装
}
}
コンポーネント内で宣言してもTauriのイベントハンドラはReactからは独立しているんですね、
ここらへんは今後の開発でも度々引っかかりそうなので覚えておこうと思いました。
イベントハンドラ周り以外では大きな問題も起こらず順調に開発は進み、ついに自動翻訳機能まで完備した当初の想定通りのBG3 XMLエディターが完成しました。
まとめ
今回の開発はひとえにTauriの開発体験の快適さにひたすら助けられたと思います。
Rustを触らずともfsプラグインによるデータ読み込みだけで全機能が完結できたため、Typescript以外の部分を意識せずに楽々と開発が進みました。
Web由来の技術が使えるためreactと状態管理ツールを使えたのも非常に大きいです、C#フォームだと操作ごとに細かく有効・無効状態の切り替えを全ボタンに対して行う必要がありましたからね、フラグ一個を見せて自動で有効・無効を判別してくれるReactの方が圧倒的に簡単でした。
昨今はSvelteやSolidが注目されていますがやはりReactの巨大なエコシステムは強いです。望んだ機能を備えたライブラリが簡単に見つかるというのはやはり大きいなと感じました。