# 翻訳に参加する

**誰でも訳を追加・修正できます。許可も報告も要りません。**
このファイルは「どこをどう直せば反映されるか」だけを書いたものです。

> **手を動かさずに伝えるだけでもいい。**
> Steam のディスカッションに日本語のスレッドがあります（作者の許可を得て立てたものです）。
> 「ここの訳が変」「この画面がまだ英語」だけでも十分助かります。
> https://steamcommunity.com/app/4977360/discussions/0/587309829716975351/

AIの使用を**推奨**します。理由と手順は §4 に書きました。

---

## 1. 3分でわかる仕組み

ゲームは `index.html` **1ファイル**で、テキストは全部 JavaScript の文字列リテラルとして
直書きされています。i18n の仕組みはありません。

そこでこのキットは、こうしています:

```
index.html の文字列を全部走査
        ↓
tm.json（原文 → 訳文の辞書）を引く
        ↓
訳があれば、その位置に差し戻す
        ↓
build/index.html
```

つまり **`tm.json` を書き換えれば訳が変わります。** それだけです。

```jsonc
{
  "The way down is barred by the Maw-boar.": "下りの道は大口猪に塞がれている。",
  ...8,798件
}
```

---

## 2. 訳を直す（いちばん簡単な参加のしかた）

1. `tools/tm.json` をテキストエディタで開く
2. 直したい訳文を書き換える（**キー＝英語原文のほうは絶対に変えない**）
3. 保存して、`tools` フォルダで:

```sh
node build-ja.mjs
```

4. `build/index.html` をブラウザで開いて確認
5. Steam版に入れるなら、キットのルートで `適用する.ps1` を実行

**DeepL の文字数は消費しません。** APIキーも要りません。`tm.json` を読むだけです。

### UI語・固有名詞は `overrides.json` へ

`overrides.json` は `tm.json` より**優先**される上書き層です。
ボタン名・種族名・人名など「絶対にこう訳したい」ものはこちらに書いてください。
`tm.json` は将来 DeepL で追加翻訳したときに増えますが、`overrides.json` は上書きされません。

```jsonc
{
  "Folk": "種族",
  "Roll": "振る",
  "Master Pell": "ペル親方"
}
```

---

## 3. 訳の方針（守ってくれると嬉しいこと）

本作の英文は**1981年頃のTRPGモジュールを模した古語調**です。その文体が作品の核です。

- 「クリティカル率 +3%」ではなく "+3% clean strokes"（+3%の清き一撃）
- 「移動速度 +8%」ではなく "Walking legs run 8% shorter"（歩みの脚が8%短く走る）

ゲーム側には Options の **Simplicity（平明表記）スイッチ**があり、
機構の説明行だけを現代語に切り替えられます。**訳もこの二層を壊さないでください。**

| 層 | 何が入るか | 訳の方針 |
|---|---|---|
| **地の文・ログ・図鑑・イベント** | 世界の描写 | **古語調を保つ。** ここが作品 |
| **機構の説明行**（Simplicity ON で平明語になる行） | 数値の説明 | 数字を先頭に、一文、短く、句点で止める |
| **UIラベル・ボタン** | 操作 | 迷ったら短く。`overrides.json` へ |

### 用語集

`glossary.tsv` に107語あります。**訳語の揺れを防ぐ要です。**

```
chronicle        年代記
Roll of Honour   名誉録
prestige         名誉点
Long Charter     長憲章
the Maw          大口
Master Pell      ペル親方
clean stroke     清き一撃
```

新しい語を足すときは、ここにも追記してください。

---

## 4. AIの使い方（推奨）

このキットは DeepL と Claude で作られました。**あなたもAIを使ってください。**

44万文字を個人が手で訳すのは現実的ではありません。
AIがなければこのキットは存在しませんでした。
**叩き台を出すのがAI、文体を決めるのが人間**、という分担が現実的です。

### 手順: 文体を整える

`tm.json` から直したい範囲を切り出し、AIに**原文と現在の訳を対で**渡します。

プロンプトの例:

```
これはゲーム『Lowmoor』の日本語化データです。
原文は1981年頃のテーブルトークRPGのモジュールを模した古語調の英語で、
その文体こそが作品の核です。現在の訳は機械翻訳の一次出力で、文体が現代的すぎます。

次のルールで訳文だけを直してください。

- 古語調・書き言葉。講談調ではなく、翻訳された古い洋物TRPGの訳文の質感
- 「〜だ」「〜である」を基本に、一人称は使わない
- 用語は添付の用語集に必ず従う（chronicle=年代記 / the Maw=大口 など）
- 原文にある <b> などのタグは同じ数・同じ位置で残す
- {name} のような波括弧のプレースホルダは1文字も変えない
- 意訳してよいが、情報を足さない・削らない
- 出力はJSONのみ。キー（英語原文）は絶対に変更しない

用語集:
（glossary.tsv を貼る）

対象:
（tm.json から抜き出した一部を貼る）
```

### 気をつけること

- **一度に全部投げない。** 100〜300件ずつ。壊れたときに戻しやすい
- **キー（英語原文）を変えさせない。** 1文字でも変わると当たらなくなります
- **タグとプレースホルダを数える。** `patch.mjs` が検証しますが、事前に見ておくと早い
- **必ず `node build-ja.mjs` を通す。** 最後に JS の構文チェックが走ります。
  `SYNTAX CHECK: OK` が出れば壊れていません
- **AIの訳をそのまま信じない。** 固有名詞と数字は目で確認してください

### 検証は自動で入っています

`patch.mjs` は差し戻す前に各訳を検証し、落ちたものは**英語のまま残します**。

| 検証 | 落ちる条件 |
|---|---|
| `tag-mismatch` | `<b>` などのタグが原文と違う |
| `placeholder-mismatch` | `{name}` の数や種類が違う |
| `interpolation-mismatch` | `${...}` の数が違う |
| `entity-injected` | AIが `"` を `&quot;` にしてしまった |
| `untranslated` | 日本語が1文字も入っていない |
| `identifier-property` | 原文が識別子1語で、かつどこかで `obj.名前` として読まれている |
| `browser-api-value` | ブラウザAPIに渡る文字列・列挙値（`"nearest"`、CSSセレクタなど） |
| `internal-key` | `ash` / `relic` / `torch` などセーブや分岐のキーになる語 |
| `runtime-compared` | `location.protocol` などブラウザが与える値と `===` で比べている文字列 |
| `css-value` | CSSの関数・単位の形をした文字列（`inset(` `translate(` `12px` `50%` …） |
| `composite-key` | `mines:2` のような `語:語` 形の合成キー |
| `structural-key` | 構造を組み立てる側のキー（1.6.0 で追加） |
| `template-expression` | 原文にJSの式 `${...}` が入っている |

**壊れた日本語を画面に出さない**ための仕組みです。信頼して大胆に試してください。

### ⚠ 短い英単語1語を訳すときの落とし穴

**ここは実際にゲームをクラッシュさせた箇所です。必ず読んでください。**

`Shield` や `Helm` のような装備部位名は、ゲームの中で2通りに使われています。

```js
["Weapon","Shield","Helm", ...]          // 表示用の文字列リテラル → 訳される
ARMOR_BASES = { Shield:[...], Helm:[...] }  // クォートなしのオブジェクトキー → 訳されない
```

片方だけ日本語にすると `ARMOR_BASES["盾"]` が `undefined` になり、
**装備をドロップした瞬間にゲームが落ちます。** 最初のビルドはこれで必ずクラッシュしました。

`patch.mjs` には対策が2つ入っています。

1. **クォートなしキーの追随** — 訳した語が識別子としてキーに使われている箇所を検出して
   `{Shield:` → `{"盾":` と同時に書き換える（現在27箇所を自動修正）
2. **プロパティ名の保護** — `LEGACY.mods.open` のようにドット記法で読まれている名前と
   同じ文字列リテラルは書き換えようがないので、**最初から訳さない**（現在150件を保護）

同じ種類の事故はほかにも2件ありました。

- `scrollIntoView({block:"nearest"})` の `"nearest"` を訳してしまい、階層表示の更新のたびに例外
- `querySelectorAll("canvas[data-plate]")` の `plate` が訳され、CSSセレクタが壊れた

どちらも**構文としては正しいので構文チェックを通り、実行して初めて落ちます。**
そこで `patch.mjs` に、出力を読み直して**ブラウザAPIの引数位置に日本語が立っていないか**を
検査する lint（10種）を入れました。ビルドの最後に `LINT: clean` と出れば通っています。

つまり `overrides.json` に短い英単語1語を足すときは、**その語がコードの中で
オブジェクトのキー・プロパティ名・APIの引数・セーブのキーとしても使われていないか**を
意識してください。3つの門と lint が自動で面倒を見てくれますが、万能ではありません。
足したあとは必ず §「動かして確かめる」の手順を通してください。

### ⚠ 例外も出ずに黙って壊れる種類がある

**ここがいちばん厄介です。** 構文チェックも lint も実行時エラーも全部通るのに、
画面を見ると壊れている、という壊れ方が実際に2件ありました。

**1. デスクトップ版が「小窓」になった**

```js
if (location.protocol === "file:") document.documentElement.classList.add("app");
```

この `"file:"` が「ファイル：」に訳され、比較が永久に偽になりました。
`html.app` が付かないので、枠なし全画面のはずが「デスクトップの上に乗った小窓」のまま。
**エラーは1つも出ません。**

**2. ゲージの塗りが全部消えた**

```js
const clip = "inset(0 " + (100 - frac*100).toFixed(1) + "% 0 0)";
p.fill.style.clipPath = clip;
```

`"inset(0 "` が「インセット(0」に訳されました。`clip-path` に不正な値が入ると、
ブラウザは**例外を出さずに黙って値を捨てます**。HP・MP・敵HP・負担・進捗、全ゲージが白いまま。

どちらも「代入先から辿れない」ので、門を**値の形**で見分ける方式に足しました
（`runtime-compared` / `css-value` / `composite-key`）。
それでも取りこぼす可能性はあるので、`cdptest.mjs` は**描画結果そのものを測ります**。

```
"bars": { "total": 18, "visible": 11, "badClip": [], "zeroWidth": [] }
```

`clip-path` が `inset(...)` になっていない塗りと、幅ゼロの塗りを数えます。
**訳を足したら、必ずこの数字も見てください。**

### 動かして確かめる

構文チェックだけでは、上のような**実行時のクラッシュは捕まりません。**
`tools/cdptest.mjs` を同梱してあります。Chrome を CDP で操作し、
例外と `console.error` を拾いながら通しでプレイさせるスクリプトです。

```sh
node cdptest.mjs build/index.html ja
```

Node 22 以上が必要です（グローバル `WebSocket` を使うため）。
Chrome の場所が違う場合は環境変数 `CHROME_PATH` で指定してください。

出力される JSON で、少なくとも次を確認してください。

- `errors` が空（未捕捉例外ゼロ）
- `console` が空
- `mojibake.tofu` / `mojibake.dollarBrace` / `mojibake.quotEnt` がすべて 0
- `saveRoundTrip.ok` が `true`
- `opened` が全部 `clicked`

---

## 4.5 語順は「訳」では直らない ― REORDER 規則

英語は「動詞 → 目的語」、日本語は「目的語 → を → 動詞」です。
本作は文を `+` で継ぎ足して組み立てている箇所が多く、
**リテラルを訳すだけでは英語の語順のまま残ります。**

初版の戦闘ログは実際こうなっていました。

> レイアウトされた 病弱な ゴブリン・ドリーマー 清き一撃で. Took 夢のチョークの棒、それに加えてもう一つ.

そこで `patch.mjs` に、**組み立てそのものを日本語の語順に書き換える規則**（`REORDER`）を入れてあります。
現在:

> 淡いメローを清き一撃で帳消しにした。とげのある銛の穂先（上物）を得た。さらにもう一つ。

書き換え済みの箇所: 討伐ログ / 進行中の仕事 / 掲示板の依頼 / 獣を極めた一行 / 功績の一行 /
拾得品 / 技能点の獲得 / 売却ログ / タイトルバー / 商店の口上。
加えて `art()`（英語の不定冠詞 a・an）を無効化し、「a メロー」のような表示を消しています。

**新しく英語の語順が残っている箇所を見つけたら、`tm.json` ではなく `REORDER` に足してください。**
訳文をいじっても直りません。

### 複数形の `s` も同じ理由で残る

掲示板に「ゴブリンsを追い返す」と出ていたことがあります。原因は二つでした。

1. **ゲーム側のコード。** 英語名詞に s を足す関数が日本語名詞にも効いていた
   （`const plural = s => s;` に潰してあります）
2. **翻訳の側。** 原文が `goblins` のとき、モデルが「ゴブリンs」と返すことがある
   （日本語の直後に裸で立つ `s` / `es` を落とす後処理を入れてあります）

`(n===1?"":"s")` の形の単複分岐も6箇所あり、**分岐の形のほうで拾って**日本語に畳んであります。
訳文だけ直しても直らない類なので、見つけたら報告してください。

---

## 5. 大物: 英語で残っている980件

ここが**次の作業単位**であり、いちばん歓迎される貢献です。

`tools/out-fragment-unsafe.json` に980件・約61,000文字が入っています。
これはテンプレートリテラル、つまり **JSの式と地の文が同じ文字列に同居している**ものです。

```js
`Your voice is worth ${fmtNum(weight)} points, one to every badge you took here.`
```

機械翻訳に投げると `${fmtNum(weight)}` が壊れます。だから今は**丸ごと英語のまま**にしてあります。
プレイ中に「日本語と英語が混ざった行」が出るのは、ほぼこれが原因です。

### なぜ機械翻訳では解決しないか

日本語は語順が違うためです。

```
EN: "Your voice is worth " + N + " points"
JA: 「あなたの声は」 + N + 「点に値する」
```

**式の位置ごと組み替える**必要があり、これは翻訳ではなく編集作業です。
AIは得意ですが、1件ずつ確認が要ります。

### 手順

1. `${...}` を `%1` `%2` … に退避する
2. 退避した状態でAIに訳させる（語順の組み替えを明示的に指示する）
3. `%1` を元の式に戻す
4. `tm.json` に追加する
5. **`patch.mjs` の検証を1行ゆるめる**

現在 `patch.mjs` は、原文に `${` があれば内容を見ずに弾いています:

```js
if (en.includes('${')) return 'template-expression';
```

ここを、式の中身が完全に一致していれば通す、に変えてください:

```js
// 式そのものが原文と一文字も違わないことを条件に、テンプレートリテラルも通す
const exprs = s => (s.match(/\$\{[^}]*\}/g) || []).sort().join('|');
if (en.includes('${') && exprs(en) !== exprs(ja)) return 'template-expression';
```

> ⚠️ ここを変えると**ゲームが起動しなくなる訳を通せるようになります。**
> 必ず `node build-ja.mjs` の最後で `SYNTAX CHECK: OK` が出ることを確認してください。
> 失敗した場合は該当の訳を `tm.json` から消せば元に戻ります。

---

## 5.5 大きな更新のときに踏んだこと（1.6.0 の記録）

ゲームが 2.03MB → 3.97MB とほぼ倍になった更新で、実際に起きたことです。
同じ規模の更新が来たときの参考にしてください。

**① 抽出の絞り込みがスクリプトに入っていなかった。**
その場かぎりのコマンドで作っていた絞り込みが4種あり、更新時に前版のファイルが使われて、
**増えたツールチップがまるごと抽出対象から漏れました。** 日本語比率が 92% → 68% に落ちて気づき、
真の未訳は 903件ではなく 2,012件でした。4つの抽出スクリプトすべてに組み込み済みです。

**② `（CON）` を「残った伏せ字」と誤検出。**
伏せ字の検出が「括弧＋大文字1〜3字」を無条件に拾うため、能力値の略号に反応し、
**正しく訳せている文を50件中47件も突き返していました。** 1.6.0 で説明文にこの書き方が増えて表面化。

**③ キーの書き換えが暴走して起動時クラッシュ。**
「クォートなしのオブジェクトキーも一緒に訳す」処理の対象が 27 → 237件に増え、
その中に `CHART_QUARTERS[q.id]` のように**変数で引くキー**が混ざりました。変数側は data から
来るので書き換えようがなく、署名した瞬間に落ちます。
参照経路を確かめた**11語だけの許可リスト**に変えてあります（表示の日本語を少し諦める側に倒した）。

**④ CSS が翻訳された。** `"background:var(--desk); margin:0;"` が
「背景: var(--desk); マージン: 0;」に。**これは lint が検出しました。**
`prop:value` の形も門で止めるよう広げてあります。

**⑤ 書き換え規則（REORDER）4件が空振り。**
作者が `log()` に第三引数を足す、選択待ちボタンに印を追加する等の変更を入れていたためです。
`miss` が出てもビルド自体は通り、**その箇所が英語のまま残るだけ**なので、後から直せます。

---

## 5.6 未訳が残るのは、たいてい「抽出の穴」です

1.6.0 の追い込みで **291件 → 17件** まで減らしたときの記録です。
**見つかった原因は、ひとつも訳文の側ではありませんでした。全部が抽出側の穴です。**

**① テンプレートリテラルの中が丸ごと未抽出だった（最大の穴）**

```js
`<div class="armstore"><button data-arm-equip="${escAttr(r.id)}">Equip</button></div>`
```

抽出器は「`${` があるテンプレートは触らない」で避けていたので、この `Equip` は
**一度も抽出されたことがありませんでした。** 画面には1,032箇所出ていました。
`extract4.mjs` を新設し、`${}` の入れ子を数えてテンプレートを切り出し、
穴を1文字の番兵に置き換えた「仮想HTML」から、テキストノードと属性値だけを取り出します。

**② `${...}` の中の文字列リテラルも未抽出**

```js
`<span class="m">${esc(has ? petGrowthSay(p) : "not yet taken")}</span>`
```

バッククォートからバッククォートまでを一つの文字列として読んでいたため、
穴の中の `"not yet taken"` が見えていませんでした。`jsscan.mjs` で310件を回収。

**③ 絞り込みの正規表現が「A␣」で始まる文言を全部捨てていた**

SVGパスを除く判定が先頭1文字だけを見ており、`A` が円弧コマンドなので
`A Good Boy` `A Larger Satchel` のような実績名がまとめて捨てられていました。176件を発掘。

**④ 語彙表の差し替えで鍵が20個消えていた**

`lexicon.mjs` は語順のために語彙表を書き下ろす作りですが、中身が旧版のままでした。
原文の表が8鍵→28鍵に増えたため差し替えで20鍵が失われ、
`+1 keeneye.` `+0.03 procCeil.` のような生の鍵名がツールチップに並んでいました。
**「差し替えで原文の鍵が失われたらビルドを止める」検査**を入れてあります。
黙って通っていたのが本当の問題でした。

**⑤ 綴りで守ると表示側まで巻き添えになる**

`el.style.display = "none"` を守る門が、`textContent = ... : "none"` まで止めていました。
**「位置からして表示だと分かる箇所」は別の門にかける**方式に変えてあります。

### つまり

**「まだ英語が残っている」と思ったら、まず `sweep-en.mjs` で数えてください。**
全メニューを巡回して、画面に出ている英語を数える道具です。
訳文をいくら足しても減らないときは、その文字列がそもそも抽出されていない可能性が高いです。

---

## 6. ゲームが更新されたとき

Steam が更新すると `app.asar` は英語に戻ります。`適用する.ps1` を再実行してください。

**訳が消えることはありません。** `tm.json` は原文をキーにした辞書なので、
文が変わっていない箇所はそのまま当たります。増えた分・変わった分だけが英語で残ります。

差分だけ翻訳したい場合（DeepL APIキーが必要）:

```sh
node extract.mjs                          # 分類しなおし
node extract-html.mjs
node translate.mjs usage                  # 残り文字数の確認
node translate.mjs run out-sentence.json  # 未訳分だけ翻訳して tm.json に追記
node build-ja.mjs
```

`translate.mjs run` は実行前に残クォータを確認し、足りなければ**払わずに中断**します。
バッチごとに `tm.json` を保存するので、途中で落ちても払った分は失われません。

DeepL 無料プランの上限は月50万文字です（100万ではありません）。

---

## 7. 他の言語に持っていく

**歓迎します。** この仕組みは日本語固有ではありません。

1. `tm.json` を空にする（`{}`）
2. `glossary.tsv` を対象言語で作り直す
3. `translate.mjs` の言語コード（`target_lang`）を変える
4. `patch.mjs` の `untranslated` 判定（現在は日本語の文字種を見ている）を対象言語に合わせる
5. フォントの追加行（`patch.mjs` 末尾）を必要に応じて調整する

英語→その言語の対応表さえ作れば、あとは同じパイプラインが動きます。

---

## 8. セーブの内部値について（訳を足す前に知っておくこと）

セーブの中身には、種族・職業・装備部位・獣の名前が**そのまま値として**入ります。
つまり**訳語を変えると、その語を含む既存のセーブと互換性が切れます。**

- 公開済みの訳語（特に `overrides.json` の種族・職業・出自・装備部位）を変えるのは、
  既存プレイヤーの年代記を壊す行為になります。**変えるなら理由と一緒に告知してください**
- 地の文・図鑑・イベント文はセーブに入らないので、いくら直しても安全です。
  **まずはそちらから手を入れるのを勧めます**
- 根本的に解消するには、書き出し／読み込み時に内部値を英日変換する層が要ります
  （`overrides.json` の逆引きで実装できます）。ここを作ってくれる人がいたら大歓迎です

## 9. 禁止事項

「利用条件.md」に書いてあります。**必ず読んでください。**

要点だけ: **マルウェアの運搬に使わない。作者に損害を与えない。他人を欺く目的で使わない。**

日本語化パッチはこの手口に繰り返し悪用されてきた類のものです。
改変版を配るときは、受け取る人が中身を読めるよう**必ずソースのまま**配ってください。
