# Lowmoor 日本語化 — 実施報告と引き継ぎ

## 1. 結論

**日本語化できる。実際に動くものを作った。** 画面上の文字の **93%**（実測・自動テストで計測）が日本語になっており、
キャラクター作成・メインのプレイ画面・図鑑・実績・技能ツリー・各種ダイアログ・戦闘ログはすべて日本語で読める。

未訳のまま残しているのは、**構造上、機械翻訳すると必ず壊れる部分**だけ（後述 §5）。

## 2. なぜ「余地がある」のか — ゲーム側の構造

- ゲームは `resources/app.asar` 内の `game/index.html` **1ファイル**しかない。差し替えるファイルが1つで済む。
- ただし **i18n の仕組みは存在しない。** 全テキストがJSの文字列リテラルとして直書きされている。
- 一方で、この作者は「同じ意味の文を2通り持つ」仕組み（Options の *Simplicity* スイッチ、`mechRule(world, plain)`）を
  すでに実装している。**思想としては多言語化に近い設計をしているが、実装は言語切替ではなく呼び出し箇所ごとの二択**なので、
  そのまま日本語の受け皿には使えなかった。

## 3. 作ったもの

```
release/
  app.asar          日本語版に差し替え済みの app.asar（2.4MB）
  lowmoor-ja.html   同じ中身の単体HTML。ブラウザで直接開くだけでも遊べる
  install-ja.ps1    Steam版に適用（元ファイルは app.asar.en.bak に自動退避）
  uninstall-ja.ps1  英語版に戻す
```

パイプライン本体（再実行可能・翻訳メモリ付き）:

| ファイル | 役割 |
|---|---|
| `extract.mjs` | JSの文字列リテラルを、位置情報つきで抽出し4種に分類 |
| `extract-html.mjs` | 静的HTML側のテキストノードと `data-help` 等の属性を抽出 |
| `translate.mjs` | DeepL API 呼び出し。用語集適用・バッチ処理・**翻訳メモリ**（`tm.json`）でクォータの二重消費を防止 |
| `glossary.tsv` | 用語集 107 語（DeepL glossary として登録） |
| `overrides.json` | **手訳の上書き層**。DeepLより優先。UI語・固有名詞・誤訳の修正 193 件 |
| `patch.mjs` | 訳文を位置指定で差し戻し、検証に落ちたものは英語のまま残す。最後にJSの構文チェック |
| `tm.json` | 翻訳メモリ 4,642 件 / 原文 439,156 文字 |

再ビルドは `node patch.mjs` だけ。**DeepLの文字数は一切消費しない**（tm.json から読むため）。

## 4. 翻訳の内訳と DeepL クォータ

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

| 対象 | 件数 | 文字数 | 手段 |
|---|---|---|---|
| 完結した文（地の文・ログ・図鑑・イベント） | 2,392 | 357,916 | DeepL（用語集あり） |
| 単独で訳せる断片 | 656 | 34,961 | DeepL |
| UIラベル・アイテム名 | 1,299 | 21,027 | DeepL |
| 静的HTML（ツールチップ含む） | 295 | 16,347 | DeepL |
| **UI語・種族/職業/出自・称号・誤訳修正** | **193** | — | **手訳（Claude）** |
| **合計消費** | | **438,912 / 500,000** | 残 61,088 |

用語集で「chronicle→年代記」「Roll of Honour→名誉録」「Master Pell→ペル親方」「clean stroke→清き一撃」など
本作固有の語を固定してあるので、機械翻訳にありがちな訳語の揺れはかなり抑えられている。

DeepL が誤った箇所（例: `Folk`→「フォーク」、`Trade`→「貿易」、`Roll`（ダイスを振る）→「名誉録」）は
`overrides.json` で手訳に差し替え済み。

## 5. 意図的に英語のまま残した部分

`patch.mjs` は訳文を差し戻す前に検証を行い、落ちたものは**英語のまま残す**（壊れた日本語を出さない方針）。

| 理由 | 件数 | 内容 |
|---|---|---|
| `template-expression` | 980件・61,004文字 | ``` `...${expr}...` ``` 形式。JSの式と地の文が同じ文字列に同居しており、訳すと式が壊れる |
| 分類上スキップ | 983件・10,800文字 | 単語1つの固有名詞、マークアップ片、CSS片 |
| `tag-mismatch` | 19件 | DeepLが `<b>` 等を落とした |
| `entity-injected` | 5件 | DeepLが `"` を `&quot;` に変換した |

**具体的にプレイ中に英語で残るもの**:
- タイトルバーの `Lowmoor: 名前, 種族 職業 (Rank N) - 地域名`
- 冒険者の記録パネルの行キー（Name / Folk / Trade / Origin / Rank）
- ログの一部（「Halric Netherby, 人間, once a 写字生, is enrolled as 戦士 upon the Roll of...」のように
  日本語と英語が混ざる。文が `"..." + 変数 + "..."` で組み立てられているため）
- Muster（オンライン催事）のダイアログ本文

### これを潰すには

テンプレートリテラル側は、機械翻訳ではなく**1件ずつ手作業で日本語の語順に組み替える**必要がある。
980件・6万文字なので、やるならここが次の作業単位。`out-fragment-unsafe.json` にリストがある。
`${...}` を `%1` 等のプレースホルダに退避 → 訳す → 語順を組み替えて戻す、という手順になる。
DeepLの残クォータ 61,088 文字ではこの分（61,004文字）がちょうど収まるが、
**語順の問題は機械翻訳では解決しないので、ここは意訳＝手訳が必須**。

## 6. 表示まわりで追加した調整

- `font-family` に `"MS PGothic","Yu Gothic UI",Meiryo` を追加（3箇所）。
  Win98風UIに合わせるなら **MS Pゴシック**が当時のWindows日本語版の実際のUIフォントなので、先頭に置いてある。
- 日本語は単語間スペースで折り返せないため、ログ・ツールチップ・テーブルに `overflow-wrap:anywhere` を追加。
- UIのズーム機能（Ctrl +/-、設定の「大きさ」）は元から8段階あるので、11pxが小さければそこで拡大できる。

## 7. 動作確認（実行して検証済み）

### 7.1 検証で見つけて直した実バグ

最初のビルドは**プレイ中に必ずクラッシュしました。** 自動テストで捕まえた例外がこれです:

```
TypeError: Cannot read properties of undefined (reading 'length')
  at pick -> assizeSpend -> makeEquipName -> gainEquip -> completeTask -> tick
```

原因は典型的なローカライズ事故でした。`Shield` や `Helm` といった装備部位名が、

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

の両方で使われており、片方だけ日本語になった結果 `ARMOR_BASES["盾"]` が `undefined` になり、
装備をドロップした瞬間に落ちていました。

対策を2つ入れてあります（`patch.mjs`）:

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

### 7.2 自動テストの結果

Chrome を CDP で操作し、例外と `console.error` を全部拾いながら通しでプレイさせました（`cdptest.mjs`）。

| 項目 | 結果 |
|---|---|
| 未捕捉例外 | **0件** |
| console.error / warning | **0件** |
| 早送り 6 時間（`fastForward` を実エンジンで6回） | 第8階層 / ランク13 / 2,588 kill / 64,179 ゴールド |
| 文字化け（U+FFFD） | 0 |
| テンプレート式の露出（`${` が画面に出る） | 0 |
| HTMLエンティティの混入（`&quot;` 等） | 0 |
| セーブコードの往復 | OK（スキーマ v13、Base64 15,840 文字、復号成功） |
| 各ウィンドウの開閉 | 設定・獣譜・実績・統計・技能ツリー・商店・ギルドハウス すべて開閉OK |
| 画面上の日本語比率 | 67%（残りは §5 の未訳分と、動的生成される装備名） |

スクリーンショット: `shots/ja-01-enrol.png` 〜 `shots/ja-11-guildhouse.png`

### 7.3 パッケージ（app.asar）の検証

差し替え用 `app.asar` をもう一度展開して、オリジナルと全ファイルを突き合わせました。

```
diff -r --brief <オリジナル> <日本語版>
→ Files .../game/index.html differ        ← これ1行のみ
```

**`game/index.html` 以外は全ファイルがバイト単位で同一**です。
（ほかに `steam/builder`（steamcmd 150MB）と `steam/output`（ビルドログ）を除去していますが、
ゲームコードからは一切参照されていないことを確認済みです。）

### 7.4 未検証の部分 ― Electron の exe 起動

**この環境では検証できませんでした。** ゲームフォルダを丸ごとコピーした場所で `Lowmoor.exe` を起動しましたが、
ウィンドウを出さず即座に終了します。ただし**未改変のオリジナルでもまったく同じ挙動**だったため、
日本語化とは無関係な実行環境側の問題（自動化セッションからの起動と単一インスタンスロックまわり）と判断しました。

Electron ラッパがゲームに注入するのは `window.lowmoorBridge` だけで、
今回の変更は `game/index.html` の中身のみです。ゲームロジックはブラウザ検証で全て通っているので、
exe 側で新たに壊れる余地はほぼありませんが、**最終確認はご自身でダブルクリックしてお願いします。**

### 7.5 この検証で触れたもの／触れなかったもの

- Steam のインストールフォルダは**一切変更していません**（`app.asar` は 160,719,990 バイトのオリジナルのまま）。
  検証はすべて別ディレクトリへのコピー上で行い、コピーは削除済みです。
- ウィンドウ位置の設定ファイル（`%APPDATA%\lowmoor-desktop\settings.json`）を検証中に一時的に書き換えましたが、
  **元の値に復元済み**です。
- セーブデータには触れていません。

## 7.6 旧・動作確認メモ

Chrome headless で実際に起動し、スクリーンショットで確認済み（`shots/`）:

- `ja-enrol2.png` — 入会画面。種族/職業/出自の全36項目、能力値、ボタン類が日本語
- `ja-play2.png` — メインのプレイ画面。パネル見出し・装備部位・章立て・ログが日本語

`patch.mjs` の最後に `new Function(js)` による構文チェックが入っており、**現状 SYNTAX CHECK: OK**。

## 8. 注意事項

1. **Steamがゲームを更新すると `app.asar` は上書きされ、英語に戻ります。** その場合は `install-ja.ps1` を再実行。
2. **セーブの互換性に注意。** ファイル形式もスキーマ番号（v13）も変えていないので読み書きは問題ありませんが、
   セーブの中身には種族・職業・装備部位・獣の名前が**そのまま値として**入ります。
   日本語版で作った年代記は内部値が日本語になるため、**英語版に戻すと装備や種族の参照が外れます。**
   - 日本語版で始めた年代記は日本語版で続けてください。
   - Steam がゲームを更新して英語に戻った場合は、その年代記を再開する前に `install-ja.ps1` を再実行してください。
   - 英語版で作った既存の年代記を日本語版で開くのも同じ理由で避けてください（新規に始めるのが安全です）。
   - どちらの版でも `Export Save` のコードは取れるので、切り替える前にバックアップを取っておくのが確実です。

   これを解消するには、書き出し/読み込みの時に内部値を英↔日で変換する層を入れる必要があります
   （`overrides.json` の逆引きで実装できるので、次にやるとすればここが有力です）。
3. **同梱の `app.asar` は 160MB → 2.4MB に縮んでいます。** 誤って同梱されていた `steam/builder`（steamcmd 150MB）と
   ビルドログを除いたためです。ゲームコードからは一切参照されていないので動作に影響はありません。
   気になる場合は `patch.mjs` を通した `build/index.html` だけを元の app.asar に差し替えてください。
4. **配布について**: このパッチはゲーム本体のファイルを改変します。個人利用の範囲に留め、
   再配布する場合は作者（Bogurodzic）に Discord で許可を取ってください。
   デスクトップラッパは MIT ですが、ゲーム本体（`index.html`）のライセンスは明示されていません。

## 9. 品質について正直なところ

DeepL＋用語集の一次翻訳なので、**「読める」水準であって「作品として仕上がっている」水準ではありません。**
本作の英文は1981年のTRPGモジュールを模した古語調で、その文体こそが作品の核です（記事資料 §5 参照）。
そこは機械翻訳では出ません。文体を活かすなら、`tm.json` を叩き台として
重要度の高い順（図鑑の解説文 → イベント文 → ボス演出 → 地の文）に手入れしていくのが現実的です。
`tm.json` は原文をキーにしたただのJSONなので、直接書き換えて `node patch.mjs` で反映できます。


---

# 追補: 第2〜3次パスの記録（未訳部分の掃除）

初版は画面上の日本語比率が 62〜67% でした。プレイ画面で英語が残る箇所を洗い出して詰め、
**93%** まで引き上げました。以下、何が漏れていて、なぜ漏れたかの記録です。

## A. 分類が甘くて取りこぼしていたもの

| 漏れの原因 | 何が英語のまま残っていたか |
|---|---|
| 「The …」で始まる文を*連結片*と誤判定していた | ボス演出（BOSS_STAGES 4,481字）、選択肢カード（FATE_EVENTS）、ボス紹介文 |
| 1語だけの文字列をすべて内部IDとみなして除外していた | モンスター名、討伐動詞（Downed/Felled…）、装備の部位名・素材名 |
| 前後に片方だけタグが付いた文字列（`"</b> steadies your hand…"`）を「壊れたマークアップ」として除外していた | 戦闘ログの大半 |
| DeepL が `<b>` を落としたり `</b>` を足したりした訳を、検証で弾いてそのまま英語にしていた | 幕クリア時の長文、掲示板、統計画面 |

対策として抽出器を作り直し（`extract2.mjs` / `extract3.mjs`）、
DOM id・CSSクラス・`$("…")` の引数・`id:"…"` の保存キーを**ソースから機械的に集めて除外**するようにしました。
タグの過不足は `repair-tm.mjs` が原文のタグ構成に合わせて訳文を直します（25件を自動修復、残り2件）。

## B. 語順の問題（機械翻訳では直らない部分）

英語は「動詞 → 目的語」、日本語は「目的語 → を → 動詞」。文を `+` で継ぎ足して組み立てている箇所は、
リテラルを訳すだけでは英語の語順のまま残ります。実際、初版の戦闘ログはこうなっていました:

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

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

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

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

## C. 手訳したもの

DeepL では品質が出ない、あるいは訳語の統一が要る部分は自分で書きました（`overrides.json` 528件）。

- **モンスター名 179種すべて**（`monsters-ja.json`）。DeepL は "owlbear"→「フクロウ熊」、"chuul"→未訳 など不安定だったので、TRPG の慣用表記に寄せて全部当て直しました（アウルベア、チュール、エティン…）。
- 討伐動詞30種（「仕留めた」「斬り伏せた」「勘定を締めた」…）、狩りの動詞、個体の修飾語（「病んだ」「追い詰められた」…）
- 希少度6段（上物／希少／極稀／伝説級／神器）、品質8段（刃こぼれした／使い込まれた／…／嵐鍛えの）
- 素材20種、武器29種、防具の部位名28語（バシネット、ブリガンダイン、腋当て円板…）
- 種族・職業・出自の全36項目、メニュー、称号、8学派名

## D. 検証中に見つけて直した実バグ（3件）

1. **装備部位名のキー不整合**（初版・§7.1 に既述）— 装備ドロップで必ずクラッシュ。
2. **`"nearest"` → 「最も近い」**。`scrollIntoView({block:"nearest"})` の列挙値を訳してしまい、
   階層表示の更新のたびに例外。
3. **CSSセレクタの翻訳**。`querySelectorAll("canvas[data-plate]")` の `plate` が訳され、
   セレクタが壊れていました（`matchMedia("(pointer: coarse)")` も同様）。

2 と 3 は「構文としては正しいので SYNTAX CHECK を通ってしまい、実行して初めて落ちる」種類です。
そこで **出力を読み直して、ブラウザ API の引数位置に日本語が立っていないかを検査する lint** を
`patch.mjs` に追加しました（10種のパターン）。**現在 LINT: clean**。

さらに、訳してはいけない文字列を機械的に判定する門を3つ足しました:
`identifier-property`（`LEGACY.mods.open` のようにドット参照される名前）、
`browser-api-value`（API に渡る文字列・列挙値）、
`internal-key`（`ash`/`relic`/`torch` などセーブや分岐のキーになる語）。

## E. 現在の数字

| 項目 | 値 |
|---|---|
| 画面上の日本語比率 | **93%**（日本語 16,117 字 / 英字 1,258 字） |
| 適用したリテラル | 7,088件 ＋ 静的HTML 370件 ＋ 語順書き換え 11箇所 ＋ 文字列差し替え 25件 |
| 翻訳メモリ | 5,955件 |
| 手訳の上書き | 528件 |
| DeepL 消費 | 483,217 / 500,000（残 16,783） |
| 未捕捉例外 / console.error | **0件** |
| LINT | clean |
| SYNTAX CHECK | OK |
| セーブコードの往復 | OK（v13） |

残る英字 1,258 字の中身は、**冒険者名・雇い人名（意図的に英語のまま）**、`Lowmoor` の作品名、
そして `${…}` を含むテンプレート文字列のごく一部です。

## F. 再ビルドの手順（変更なし）

```sh
node patch.mjs      # tm.json + overrides.json + 語順規則 を適用し、lint と構文チェックまで走る
```

訳を直したいときは `tools/tm.json`（原文がキー）か `tools/overrides.json`（優先される手訳層）を
編集して、これを実行するだけです。DeepL の文字数は消費しません。


---

# 追補2: ウィンドウの不具合、複数形、PLaMo への全面切替

## G. デスクトップ版が「小窓」になっていた不具合（修正済み）

Steam 版が、枠なしで画面いっぱいに広がるはずのところ、青いデスクトップの上に
小さなウィンドウが乗った見た目になっていました。原因はこれです。

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

この **`"file:"` が「ファイル：」に翻訳されていました**。比較が永久に偽になるので
`html.app` が付かず、ブラウザ用の「デスクトップの上の窓」表示のままになります
（`html.app` が付くと余白・960px 幅・枠が外れて、client area にぴったり収まる）。

前回入れた三つの門（ドット参照・API 引数・内部キー）はどれもこれを捕まえませんでした。
`location.protocol` は**ブラウザが与える値**で、こちら側に対になるリテラルが無いためです。
四つ目の門を足しました:

- **`runtime-compared`** — `location` / `navigator` / `document` / `window` / `screen` /
  `history` / `process` 由来の値と `===` で突き合わせている文字列は訳さない。
  URL スキームの形（`^[a-z][a-z0-9+.-]*:$`）も同様に落とす。15 件が該当しました。
- lint にも同じパターンを追加（`location.protocol === "日本語"` を検出）。

修正後、`html.app` が正しく付いて端まで広がることをスクリーンショットで確認済み
（`shots/app-mode.png`）。

## H. 複数形の `s`

掲示板に「ゴブリンsを追い返す」「バグベアsを始末する」と出ていました。二重の原因です。

**1. ゲーム側のコード。** 依頼文を組み立てるとき、英語名詞に s を足す関数が
日本語名詞にもそのまま効いていました。

```js
const plural = s => /s$/.test(s) ? s+"es" : s+"s";   // 原文
const plural = s => s;                                // 日本語に複数形はない
```

**2. 翻訳の側。** 原文が `goblins` のとき、モデルが「ゴブリンs」と返すことがあります。
ゲートウェイに後処理を足しました: 日本語の直後に裸で立っている `s` / `es` を削る
（`%s` は伏せ字になっているので巻き込みません）。

あわせて、依頼の動詞句から `を` を抜きました。語順を「〜を＋動詞」に組み替えたので、
動詞句が `を` を含んでいると「ゼラチナス・キューブを数を減らす」と二重になるためです
（`Account for the` → 「数を減らす」を「掃討する」へ）。

## I. DeepL から PLaMo への全面切替

翻訳エンジンをローカルの PLaMo（`F:\PLaMo`）に替え、**5,955 件・487,081 文字を
すべて訳し直しました**。DeepL 版の訳文は `tm-deepl-backup.json` に残してあります。

切り替えた理由:

- 文字数の上限がない（DeepL 無料枠は月 50 万文字で、ほぼ使い切っていた）
- 用語集の効き方が強い。DeepL の用語集は「参考にする」だが、こちらは**伏せ字に
  置き換えて訳語を差し戻す**ので、訳語が選び直される余地がない
- 外部に原文が出ない

用語集は 559 語（動詞など 54 語は、伏せ字にすると文が壊れるので除外）。
`chronicle` `prestige` `bestiary` など、小文字 1 語だが訳語を揺らしたくない
中核語 17 個は「固定」に昇格させています。


---

# 追補3: PLaMo 通し翻訳の結果

## 数字

| 項目 | 値 |
|---|---|
| 翻訳対象 | 5,955 件 / 487,081 文字 |
| 訳出できた | **5,894 件（99.0%）** |
| 所要 | 37 分（RTX 5070 / Q6_K / 逐次） |
| 画面上の日本語比率 | **92%** |
| 未捕捉例外・console.error | **0 件** |
| LINT / 構文チェック | clean / OK |
| セーブコード往復 | OK（v13） |
| 複数形 s の残り | **0 件** |
| 伏せ字の残り | **0 件** |

訳せなかった 61 件は、JWT や CSS セレクタ、`${…}` を含むテンプレート断片など、
**そもそも訳してはいけないもの**です。門が正しく弾いています。

## 用語の一貫性

| 語 | 一致率 |
|---|---|
| Lich / Roll of Honour / prestige / bestiary / Master Pell / Old Hesper | 100% |
| Guild | 97% |
| chronicle / Maw | 93% |
| warden | 91% |
| Mereward | 88% |

## 文体

文体見本を一組置く方式で、**地の文はほぼ常体に揃いました**。
全体では敬体が 7% 残っていますが、その大半は**ツールチップ・ヘルプ・パッチノート**で、
日本語のソフトウェアとしてはむしろ敬体が自然な箇所です。
地の文で敬体が混ざるものも一部ありますが、少数です。

## 連結断片のタグ位置（新しく見つけて直した問題）

検証はタグの**集合**しか見ていなかったので、次のような訳が素通りしていました。

```
EN: "Over tea in the hermit's cell you are taught <b>"   ← 末尾が開始タグ
JA: "隠者の庵でお茶を飲みながら、あなたは<b>について教えられる"   ← <b> が文中へ移動
```

このあと `<b>` の位置に呪文名が差し込まれるので、画面では
「あなたは**について教えられる火球**」という表示になります。タグの数は合っているため、
既存の検証は通ってしまいます。

31 件を検出し、**後ろの断片と対にして手で書き直しました**。

```
A: "隠者の庵で茶を飲みながら教わったのは、<b>"
B: "</b>。その習得は苦もなく進む。"
→ 「隠者の庵で茶を飲みながら教わったのは、火球。その習得は苦もなく進む。」
```

検出は `tools/check-tags.mjs`、対の抽出は `tools/pair-fragments.mjs`、
差し替えは `tools/tagfix.mjs` に残してあります。

## 英語の単複の出し分け

`(n===1?"":"s")` の形が 6 箇所あり、「3選択s待っている」「技能点s」のように出ていました。
リテラルは訳されて中身が変わるので、**分岐の形のほうで拾って**日本語の言い回しに畳んでいます。
現在、英語が残る単複分岐は 0 件、日本語の直後に立つ s も 0 件です。


---

# 追補4: ゲージの塗りが消えた件（修正済み）

HP・MP・敵のHP・負担重量・依頼の進捗など、すべてのゲージが白いままになりました。

## 原因

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

この **`"inset(0 "` が「インセット(0」に翻訳されていました**。
`clip-path` に不正な値が入ると、ブラウザは**例外を出さずに黙って値を捨てます**。
CSS 側の初期値（全面クリップ）が残るので、塗りが一切見えなくなります。

構文チェックも lint も実行時エラーも、すべて通ります。**画面を見るまで分からない**種類の
壊れ方でした。前回の `"file:"` と同じ系統で、こちらは代入先が連結式なので
`.style.X = "…"` のパターンでも辿れませんでした。

## 同時に見つかったもの

```js
COURT_WEATHER_LINES[wb.group + ":" + wb.state]   // → "mines:2"
```

この `"mines:2"` も「鉱山：2」に訳されていました。キーの片方は実行時に data から
来るので、リテラルだけ訳すと永久に引けません（宮廷の天候の一部が出なくなる）。

## 対策

門を二つ足しました。

- **`css-value`** — CSS の関数・単位の形をした文字列は訳さない
  （`inset(` `translate(` `calc(` `rgba(` `12px` `50%` など）。
  代入先から辿れないので、**値の形**で見分ける。
- **`composite-key`** — `word:word` 形の合成キーは訳さない（`mines:2` `procs:sight` など）。

lint にも三つ追加: `.style.X =` / `setProperty(` / `cssText =` の式に日本語が混ざっていないか。

さらに、**テストで描画結果そのものを測る**ようにしました。lint と構文チェックだけでは
この種の不具合は捕まらないためです。

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

`clip-path` が `inset(...)` になっていない塗り、幅ゼロの塗りを数えます。現在いずれも 0 件。
