@youtyan/code-viewer 0.1.45 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/app.js CHANGED
@@ -436,7 +436,7 @@
436
436
  screen: "help",
437
437
  range,
438
438
  lang: params.get("lang") || "en",
439
- section: params.get("section") || "keybindings"
439
+ section: params.get("section") || "overview"
440
440
  };
441
441
  case "/history": {
442
442
  const commit = params.get("commit") || "";
@@ -479,7 +479,7 @@
479
479
  const params = new URLSearchParams;
480
480
  if (route.lang && route.lang !== "en")
481
481
  params.set("lang", route.lang);
482
- if (route.section && route.section !== "keybindings")
482
+ if (route.section && route.section !== "overview")
483
483
  params.set("section", route.section);
484
484
  const qs = params.toString();
485
485
  return `/help${qs ? `?${qs}` : ""}`;
@@ -8936,12 +8936,204 @@ ${frontmatter.yaml}
8936
8936
 
8937
8937
  // web-src/views/help-page.ts
8938
8938
  var HELP_LANGUAGES = ["en", "ja"];
8939
- var HELP_SECTIONS = ["keybindings"];
8939
+ var HELP_SECTIONS = [
8940
+ "overview",
8941
+ "annotations",
8942
+ "skills",
8943
+ "keybindings"
8944
+ ];
8940
8945
  var HELP_CONTENT = {
8941
8946
  en: {
8942
8947
  languageLabel: "Language",
8943
8948
  title: "Help",
8944
8949
  sections: {
8950
+ overview: {
8951
+ nav: "Getting Started",
8952
+ title: "Getting Started",
8953
+ intro: "code-viewer is a local browser UI for reading a repository, reviewing diffs, and letting AI agents attach explanations to exact code lines.",
8954
+ groups: [
8955
+ {
8956
+ title: "Start the viewer",
8957
+ blocks: [
8958
+ {
8959
+ kind: "paragraph",
8960
+ text: "Run the command from inside a Git repository. The server prints a localhost URL; open it in your browser."
8961
+ },
8962
+ {
8963
+ kind: "command",
8964
+ title: "Run without installing",
8965
+ command: "npx @youtyan/code-viewer"
8966
+ },
8967
+ {
8968
+ kind: "command",
8969
+ title: "Open another repository",
8970
+ command: "code-viewer --cwd /path/to/repo --open"
8971
+ }
8972
+ ]
8973
+ },
8974
+ {
8975
+ title: "Read a diff",
8976
+ blocks: [
8977
+ {
8978
+ kind: "paragraph",
8979
+ text: "Arguments after the options are passed to Git diff. With no arguments, code-viewer compares HEAD with the working tree."
8980
+ },
8981
+ {
8982
+ kind: "command",
8983
+ title: "Compare two refs",
8984
+ command: "code-viewer HEAD~1 HEAD"
8985
+ },
8986
+ {
8987
+ kind: "command",
8988
+ title: "Inspect staged changes",
8989
+ command: "code-viewer --staged"
8990
+ }
8991
+ ]
8992
+ },
8993
+ {
8994
+ title: "Browse files",
8995
+ blocks: [
8996
+ {
8997
+ kind: "paragraph",
8998
+ text: "Use the sidebar or file palette to open source files, Markdown previews, images, PDFs, and other browser-safe media. Large text files automatically switch to virtual mode."
8999
+ }
9000
+ ]
9001
+ }
9002
+ ]
9003
+ },
9004
+ annotations: {
9005
+ nav: "AI Annotations",
9006
+ title: "AI Code Annotations",
9007
+ intro: "Annotations let an AI coding agent guide you through code in the browser. Each annotation points to a file and line range, and open tabs jump there live.",
9008
+ groups: [
9009
+ {
9010
+ title: "What you ask the AI to do",
9011
+ blocks: [
9012
+ {
9013
+ kind: "paragraph",
9014
+ text: 'Ask the agent to explain code with code-viewer annotations. For example: "Use annotate to walk me through the hardest part of this system."'
9015
+ },
9016
+ {
9017
+ kind: "steps",
9018
+ items: [
9019
+ "Start code-viewer for the repository and keep it running.",
9020
+ "Ask the AI agent for an annotated walkthrough.",
9021
+ "Open the annotation panel in the browser and follow the entries in order."
9022
+ ]
9023
+ }
9024
+ ]
9025
+ },
9026
+ {
9027
+ title: "Commands the agent uses",
9028
+ blocks: [
9029
+ {
9030
+ kind: "command",
9031
+ title: "Start a walkthrough session",
9032
+ command: 'code-viewer annotate start --title "How the cache invalidation works"'
9033
+ },
9034
+ {
9035
+ kind: "command",
9036
+ title: "Add an explanation to a line range",
9037
+ command: 'code-viewer annotate add --file src/cache.ts --line 120-145 --title "Entry point" --body "Writes land here first."'
9038
+ },
9039
+ {
9040
+ kind: "command",
9041
+ title: "Inspect posted annotations",
9042
+ command: "code-viewer annotate list"
9043
+ }
9044
+ ]
9045
+ },
9046
+ {
9047
+ title: "Writing useful annotations",
9048
+ blocks: [
9049
+ {
9050
+ kind: "table",
9051
+ rows: [
9052
+ [
9053
+ "One idea per annotation",
9054
+ "Prefer several focused notes over one long explanation."
9055
+ ],
9056
+ [
9057
+ "Always pass --line",
9058
+ "Use the smallest range that covers the idea. The body is rendered under the last line."
9059
+ ],
9060
+ [
9061
+ "Use sessions",
9062
+ "Start a new session for each walkthrough topic so the history stays readable."
9063
+ ],
9064
+ [
9065
+ "Fix in place",
9066
+ "Use annotate edit when a note is wrong so the walkthrough order and IDs remain stable."
9067
+ ]
9068
+ ]
9069
+ }
9070
+ ]
9071
+ }
9072
+ ]
9073
+ },
9074
+ skills: {
9075
+ nav: "Agent Skill",
9076
+ title: "Agent Skill Setup",
9077
+ intro: "The package includes a code-viewer annotate skill so AI agents know when and how to create browser walkthroughs.",
9078
+ groups: [
9079
+ {
9080
+ title: "Install the skill",
9081
+ blocks: [
9082
+ {
9083
+ kind: "command",
9084
+ title: "Install for Claude Code in the current project",
9085
+ command: "npx -y @youtyan/code-viewer skill install"
9086
+ },
9087
+ {
9088
+ kind: "command",
9089
+ title: "Install for Codex and Gemini",
9090
+ command: "npx -y @youtyan/code-viewer skill install --agent codex,gemini"
9091
+ },
9092
+ {
9093
+ kind: "command",
9094
+ title: "Install for all supported agents",
9095
+ command: "npx -y @youtyan/code-viewer skill install --agent all"
9096
+ }
9097
+ ]
9098
+ },
9099
+ {
9100
+ title: "What the skill teaches",
9101
+ blocks: [
9102
+ {
9103
+ kind: "table",
9104
+ rows: [
9105
+ [
9106
+ "When to annotate",
9107
+ "Code reviews, onboarding walkthroughs, and explanations of changes."
9108
+ ],
9109
+ [
9110
+ "How to target code",
9111
+ "Use --file and --line, and include --from / --to when explaining a diff range."
9112
+ ],
9113
+ [
9114
+ "How to keep history clean",
9115
+ "Use one session per topic, edit wrong notes in place, and avoid unrelated cleanup."
9116
+ ]
9117
+ ]
9118
+ }
9119
+ ]
9120
+ },
9121
+ {
9122
+ title: "Agent reference",
9123
+ blocks: [
9124
+ {
9125
+ kind: "paragraph",
9126
+ text: "Agents can print the full built-in guide from the CLI when they need exact command details."
9127
+ },
9128
+ {
9129
+ kind: "command",
9130
+ title: "Show the agent guide",
9131
+ command: "code-viewer annotate agent-help"
9132
+ }
9133
+ ]
9134
+ }
9135
+ ]
9136
+ },
8945
9137
  keybindings: {
8946
9138
  nav: "Keybindings",
8947
9139
  title: "Keyboard Shortcuts",
@@ -8949,38 +9141,58 @@ ${frontmatter.yaml}
8949
9141
  groups: [
8950
9142
  {
8951
9143
  title: "Global",
8952
- rows: [
8953
- ["Ctrl+K", "Open file palette"],
8954
- ["Ctrl+G", "Open grep palette"],
8955
- ["/", "Focus file filter"],
8956
- ["t", "Toggle theme"],
8957
- ["[ / ]", "Previous / next annotation"]
9144
+ blocks: [
9145
+ {
9146
+ kind: "table",
9147
+ rows: [
9148
+ ["Ctrl+K", "Open file palette"],
9149
+ ["Ctrl+G", "Open grep palette"],
9150
+ ["/", "Focus file filter"],
9151
+ ["t", "Toggle theme"],
9152
+ ["[ / ]", "Previous / next annotation"]
9153
+ ]
9154
+ }
8958
9155
  ]
8959
9156
  },
8960
9157
  {
8961
9158
  title: "Panels",
8962
- rows: [
8963
- ["Ctrl+H", "Focus sidebar"],
8964
- ["Ctrl+L", "Focus main panel"]
9159
+ blocks: [
9160
+ {
9161
+ kind: "table",
9162
+ rows: [
9163
+ ["Ctrl+H", "Focus sidebar"],
9164
+ ["Ctrl+L", "Focus main panel"]
9165
+ ]
9166
+ }
8965
9167
  ]
8966
9168
  },
8967
9169
  {
8968
9170
  title: "Sidebar",
8969
- rows: [
8970
- ["j / k", "Move selection down / up"],
8971
- ["Ctrl+D / Ctrl+U", "Move selection by half a page"],
8972
- ["gg / Shift+G", "Move to top / bottom"],
8973
- ["Enter", "Open selected item"],
8974
- ["h / l", "Collapse / expand directory"]
9171
+ blocks: [
9172
+ {
9173
+ kind: "table",
9174
+ rows: [
9175
+ ["j / k", "Move selection down / up"],
9176
+ ["Ctrl+D / Ctrl+U", "Move selection by half a page"],
9177
+ ["gg / Shift+G", "Move to top / bottom"],
9178
+ ["Enter", "Open selected item"],
9179
+ ["h / l", "Collapse / expand directory"]
9180
+ ]
9181
+ }
8975
9182
  ]
8976
9183
  },
8977
9184
  {
8978
9185
  title: "Main Panel",
8979
- rows: [
8980
- ["j / k", "Move code cursor down / up"],
8981
- ["Ctrl+D / Ctrl+U", "Move code cursor by half a page"],
8982
- ["gg / Shift+G", "Move code cursor to top / bottom"],
8983
- ["gp / gc", "Switch to Preview / Code tab"]
9186
+ blocks: [
9187
+ {
9188
+ kind: "table",
9189
+ rows: [
9190
+ ["j / k", "Move code cursor down / up"],
9191
+ ["Ctrl+D / Ctrl+U", "Move code cursor by half a page"],
9192
+ ["gg / Shift+G", "Move code cursor to top / bottom"],
9193
+ ["gp / gc", "Switch to Preview / Code tab"]
9194
+ ]
9195
+ }
8984
9196
  ]
8985
9197
  }
8986
9198
  ]
@@ -8991,6 +9203,193 @@ ${frontmatter.yaml}
8991
9203
  languageLabel: "言語",
8992
9204
  title: "ヘルプ",
8993
9205
  sections: {
9206
+ overview: {
9207
+ nav: "はじめに",
9208
+ title: "はじめに",
9209
+ intro: "code-viewer は、ローカルのリポジトリをブラウザで読み、diff を確認し、AI エージェントにコード行へ説明を付けさせるためのツールです。",
9210
+ groups: [
9211
+ {
9212
+ title: "ビューアを起動する",
9213
+ blocks: [
9214
+ {
9215
+ kind: "paragraph",
9216
+ text: "Git リポジトリの中でコマンドを実行します。サーバが localhost の URL を表示するので、それをブラウザで開きます。"
9217
+ },
9218
+ {
9219
+ kind: "command",
9220
+ title: "インストールせずに起動",
9221
+ command: "npx @youtyan/code-viewer"
9222
+ },
9223
+ {
9224
+ kind: "command",
9225
+ title: "別のリポジトリを開く",
9226
+ command: "code-viewer --cwd /path/to/repo --open"
9227
+ }
9228
+ ]
9229
+ },
9230
+ {
9231
+ title: "diff を読む",
9232
+ blocks: [
9233
+ {
9234
+ kind: "paragraph",
9235
+ text: "オプションの後ろに置いた引数は Git diff に渡されます。引数なしなら HEAD と作業ツリーを比較します。"
9236
+ },
9237
+ {
9238
+ kind: "command",
9239
+ title: "2つの ref を比較",
9240
+ command: "code-viewer HEAD~1 HEAD"
9241
+ },
9242
+ {
9243
+ kind: "command",
9244
+ title: "ステージ済み変更を見る",
9245
+ command: "code-viewer --staged"
9246
+ }
9247
+ ]
9248
+ },
9249
+ {
9250
+ title: "ファイルを読む",
9251
+ blocks: [
9252
+ {
9253
+ kind: "paragraph",
9254
+ text: "サイドバーやファイルパレットから、ソース、Markdown プレビュー、画像、PDF などを開けます。大きいテキストファイルは自動で軽量な仮想表示に切り替わります。"
9255
+ }
9256
+ ]
9257
+ }
9258
+ ]
9259
+ },
9260
+ annotations: {
9261
+ nav: "AI注釈",
9262
+ title: "AI コード注釈",
9263
+ intro: "注釈機能を使うと、AI コーディングエージェントがブラウザ上の特定ファイル・特定行へ説明を付けられます。開いているタブは注釈先へライブで移動します。",
9264
+ groups: [
9265
+ {
9266
+ title: "AI に頼む言い方",
9267
+ blocks: [
9268
+ {
9269
+ kind: "paragraph",
9270
+ text: "AI エージェントには、code-viewer の annotate 機能を使って説明して、と頼みます。例: 「このシステムで一番むずかしい処理を annotate でウォークスルーして」"
9271
+ },
9272
+ {
9273
+ kind: "steps",
9274
+ items: [
9275
+ "対象リポジトリで code-viewer を起動したままにします。",
9276
+ "AI エージェントに注釈付きの解説を依頼します。",
9277
+ "ブラウザの注釈パネルを開き、履歴を上から順に追います。"
9278
+ ]
9279
+ }
9280
+ ]
9281
+ },
9282
+ {
9283
+ title: "AI が使うコマンド",
9284
+ blocks: [
9285
+ {
9286
+ kind: "command",
9287
+ title: "ウォークスルー用セッションを作る",
9288
+ command: 'code-viewer annotate start --title "キャッシュ無効化の流れ"'
9289
+ },
9290
+ {
9291
+ kind: "command",
9292
+ title: "行範囲へ説明を追加する",
9293
+ command: 'code-viewer annotate add --file src/cache.ts --line 120-145 --title "入口" --body "書き込みはここから入ります。"'
9294
+ },
9295
+ {
9296
+ kind: "command",
9297
+ title: "投稿済み注釈を確認する",
9298
+ command: "code-viewer annotate list"
9299
+ }
9300
+ ]
9301
+ },
9302
+ {
9303
+ title: "読みやすい注釈にするコツ",
9304
+ blocks: [
9305
+ {
9306
+ kind: "table",
9307
+ rows: [
9308
+ [
9309
+ "1注釈1テーマ",
9310
+ "巨大な説明を1つ置くより、短い注釈を順番に並べます。"
9311
+ ],
9312
+ [
9313
+ "必ず --line を付ける",
9314
+ "説明に必要な最小行範囲を指定します。本文は範囲の最後の行の下に表示されます。"
9315
+ ],
9316
+ [
9317
+ "セッションを分ける",
9318
+ "1つの解説テーマごとにセッションを作ると履歴が読みやすくなります。"
9319
+ ],
9320
+ [
9321
+ "間違いは edit で直す",
9322
+ "削除して追加し直すより、注釈IDと順番を保ったまま修正します。"
9323
+ ]
9324
+ ]
9325
+ }
9326
+ ]
9327
+ }
9328
+ ]
9329
+ },
9330
+ skills: {
9331
+ nav: "スキル登録",
9332
+ title: "Agent Skill の登録",
9333
+ intro: "このパッケージには、AI エージェントが code-viewer の annotate 機能を正しく使うためのスキルが同梱されています。",
9334
+ groups: [
9335
+ {
9336
+ title: "スキルをインストールする",
9337
+ blocks: [
9338
+ {
9339
+ kind: "command",
9340
+ title: "現在のプロジェクトへ Claude Code 用に登録",
9341
+ command: "npx -y @youtyan/code-viewer skill install"
9342
+ },
9343
+ {
9344
+ kind: "command",
9345
+ title: "Codex と Gemini 用に登録",
9346
+ command: "npx -y @youtyan/code-viewer skill install --agent codex,gemini"
9347
+ },
9348
+ {
9349
+ kind: "command",
9350
+ title: "対応エージェントすべてに登録",
9351
+ command: "npx -y @youtyan/code-viewer skill install --agent all"
9352
+ }
9353
+ ]
9354
+ },
9355
+ {
9356
+ title: "スキルが教えること",
9357
+ blocks: [
9358
+ {
9359
+ kind: "table",
9360
+ rows: [
9361
+ [
9362
+ "いつ注釈するか",
9363
+ "コードレビュー、オンボーディング、変更内容の解説で使うこと。"
9364
+ ],
9365
+ [
9366
+ "どこへ注釈するか",
9367
+ "--file と --line を使い、diff 範囲を説明するときは --from / --to も渡すこと。"
9368
+ ],
9369
+ [
9370
+ "履歴をきれいに保つ方法",
9371
+ "1テーマ1セッション、間違いは edit で修正、無関係な削除をしないこと。"
9372
+ ]
9373
+ ]
9374
+ }
9375
+ ]
9376
+ },
9377
+ {
9378
+ title: "AI 向けリファレンス",
9379
+ blocks: [
9380
+ {
9381
+ kind: "paragraph",
9382
+ text: "AI エージェントは、詳細な手順が必要なときに CLI から組み込みガイドを表示できます。"
9383
+ },
9384
+ {
9385
+ kind: "command",
9386
+ title: "AI 向けガイドを表示",
9387
+ command: "code-viewer annotate agent-help"
9388
+ }
9389
+ ]
9390
+ }
9391
+ ]
9392
+ },
8994
9393
  keybindings: {
8995
9394
  nav: "キーバインド",
8996
9395
  title: "キーバインド",
@@ -8998,38 +9397,58 @@ ${frontmatter.yaml}
8998
9397
  groups: [
8999
9398
  {
9000
9399
  title: "グローバル",
9001
- rows: [
9002
- ["Ctrl+K", "ファイルパレットを開く"],
9003
- ["Ctrl+G", "grep パレットを開く"],
9004
- ["/", "ファイルフィルターへフォーカス"],
9005
- ["t", "テーマ切り替え"],
9006
- ["[ / ]", " / 次の注釈へ移動"]
9400
+ blocks: [
9401
+ {
9402
+ kind: "table",
9403
+ rows: [
9404
+ ["Ctrl+K", "ファイルパレットを開く"],
9405
+ ["Ctrl+G", "grep パレットを開く"],
9406
+ ["/", "ファイルフィルターへフォーカス"],
9407
+ ["t", "テーマ切り替え"],
9408
+ ["[ / ]", "前 / 次の注釈へ移動"]
9409
+ ]
9410
+ }
9007
9411
  ]
9008
9412
  },
9009
9413
  {
9010
9414
  title: "パネル",
9011
- rows: [
9012
- ["Ctrl+H", "サイドバーへフォーカス"],
9013
- ["Ctrl+L", "メインパネルへフォーカス"]
9415
+ blocks: [
9416
+ {
9417
+ kind: "table",
9418
+ rows: [
9419
+ ["Ctrl+H", "サイドバーへフォーカス"],
9420
+ ["Ctrl+L", "メインパネルへフォーカス"]
9421
+ ]
9422
+ }
9014
9423
  ]
9015
9424
  },
9016
9425
  {
9017
9426
  title: "サイドバー",
9018
- rows: [
9019
- ["j / k", "選択を下 / 上へ移動"],
9020
- ["Ctrl+D / Ctrl+U", "半ページ分選択を移動"],
9021
- ["gg / Shift+G", "先頭 / 末尾へ移動"],
9022
- ["Enter", "選択項目を開く"],
9023
- ["h / l", "ディレクトリを閉じる / 開く"]
9427
+ blocks: [
9428
+ {
9429
+ kind: "table",
9430
+ rows: [
9431
+ ["j / k", "選択を下 / 上へ移動"],
9432
+ ["Ctrl+D / Ctrl+U", "半ページ分選択を移動"],
9433
+ ["gg / Shift+G", "先頭 / 末尾へ移動"],
9434
+ ["Enter", "選択項目を開く"],
9435
+ ["h / l", "ディレクトリを閉じる / 開く"]
9436
+ ]
9437
+ }
9024
9438
  ]
9025
9439
  },
9026
9440
  {
9027
9441
  title: "メインパネル",
9028
- rows: [
9029
- ["j / k", "コードカーソルを下 / 上へ移動"],
9030
- ["Ctrl+D / Ctrl+U", "コードカーソルを半ページ分移動"],
9031
- ["gg / Shift+G", "コードカーソルを先頭 / 末尾へ移動"],
9032
- ["gp / gc", "Preview / Code タブへ切り替え"]
9442
+ blocks: [
9443
+ {
9444
+ kind: "table",
9445
+ rows: [
9446
+ ["j / k", "コードカーソルを下 / 上へ移動"],
9447
+ ["Ctrl+D / Ctrl+U", "コードカーソルを半ページ分移動"],
9448
+ ["gg / Shift+G", "コードカーソルを先頭 / 末尾へ移動"],
9449
+ ["gp / gc", "Preview / Code タブへ切り替え"]
9450
+ ]
9451
+ }
9033
9452
  ]
9034
9453
  }
9035
9454
  ]
@@ -9041,7 +9460,60 @@ ${frontmatter.yaml}
9041
9460
  return route.screen === "help" && HELP_LANGUAGES.includes(route.lang) ? route.lang : "en";
9042
9461
  }
9043
9462
  function helpSectionFromRoute(route) {
9044
- return route.screen === "help" && HELP_SECTIONS.includes(route.section) ? route.section : "keybindings";
9463
+ return route.screen === "help" && HELP_SECTIONS.includes(route.section) ? route.section : "overview";
9464
+ }
9465
+ function renderHelpCommand(block2) {
9466
+ const wrap = document.createElement("div");
9467
+ wrap.className = "gdp-help-command";
9468
+ const title = document.createElement("div");
9469
+ title.className = "gdp-help-command-title";
9470
+ title.textContent = block2.title;
9471
+ const pre = document.createElement("pre");
9472
+ const code2 = document.createElement("code");
9473
+ code2.textContent = block2.command;
9474
+ pre.appendChild(code2);
9475
+ wrap.append(title, pre);
9476
+ return wrap;
9477
+ }
9478
+ function renderHelpTable(rows) {
9479
+ const table2 = document.createElement("table");
9480
+ rows.forEach(([keys, description]) => {
9481
+ const tr = document.createElement("tr");
9482
+ const keyCell = document.createElement("th");
9483
+ keyCell.scope = "row";
9484
+ keys.split(" / ").forEach((key, index) => {
9485
+ if (index > 0)
9486
+ keyCell.append(" / ");
9487
+ const kbd = document.createElement("kbd");
9488
+ kbd.textContent = key;
9489
+ keyCell.appendChild(kbd);
9490
+ });
9491
+ const desc = document.createElement("td");
9492
+ desc.textContent = description;
9493
+ tr.append(keyCell, desc);
9494
+ table2.appendChild(tr);
9495
+ });
9496
+ return table2;
9497
+ }
9498
+ function renderHelpBlock(block2) {
9499
+ if (block2.kind === "paragraph") {
9500
+ const p2 = document.createElement("p");
9501
+ p2.textContent = block2.text;
9502
+ return p2;
9503
+ }
9504
+ if (block2.kind === "steps") {
9505
+ const ol = document.createElement("ol");
9506
+ ol.className = "gdp-help-steps";
9507
+ block2.items.forEach((item) => {
9508
+ const li = document.createElement("li");
9509
+ li.textContent = item;
9510
+ ol.appendChild(li);
9511
+ });
9512
+ return ol;
9513
+ }
9514
+ if (block2.kind === "command")
9515
+ return renderHelpCommand(block2);
9516
+ return renderHelpTable(block2.rows);
9045
9517
  }
9046
9518
  function createHelpPage(deps) {
9047
9519
  function renderHelpPage() {
@@ -9054,7 +9526,7 @@ ${frontmatter.yaml}
9054
9526
  deps.$("#meta").textContent = "";
9055
9527
  deps.$("#totals").textContent = "";
9056
9528
  deps.$("#filelist").textContent = "";
9057
- const lang = helpLanguageFromRoute(deps.getRoute());
9529
+ const lang = deps.getRoute().screen === "help" && new URLSearchParams(window.location.search).has("lang") ? helpLanguageFromRoute(deps.getRoute()) : deps.getLanguage();
9058
9530
  const section = helpSectionFromRoute(deps.getRoute());
9059
9531
  const content = HELP_CONTENT[lang];
9060
9532
  const sectionContent = content.sections[section];
@@ -9075,15 +9547,8 @@ ${frontmatter.yaml}
9075
9547
  langSelect.appendChild(option);
9076
9548
  });
9077
9549
  langSelect.addEventListener("change", () => {
9078
- deps.setRoute({
9079
- screen: "help",
9080
- lang: langSelect.value,
9081
- section,
9082
- range: deps.currentRange()
9083
- });
9084
- deps.setPageMode();
9085
- renderHelpPage();
9086
- deps.syncHeaderMenu();
9550
+ const nextLang = langSelect.value;
9551
+ deps.setLanguage(nextLang);
9087
9552
  });
9088
9553
  header.append(title, langSelect);
9089
9554
  const layout = document.createElement("div");
@@ -9119,24 +9584,10 @@ ${frontmatter.yaml}
9119
9584
  groupSection.className = "gdp-help-group";
9120
9585
  const groupTitle = document.createElement("h3");
9121
9586
  groupTitle.textContent = group.title;
9122
- const table2 = document.createElement("table");
9123
- group.rows.forEach(([keys, description]) => {
9124
- const tr = document.createElement("tr");
9125
- const keyCell = document.createElement("th");
9126
- keyCell.scope = "row";
9127
- keys.split(" / ").forEach((key, index) => {
9128
- if (index > 0)
9129
- keyCell.append(" / ");
9130
- const kbd = document.createElement("kbd");
9131
- kbd.textContent = key;
9132
- keyCell.appendChild(kbd);
9133
- });
9134
- const desc = document.createElement("td");
9135
- desc.textContent = description;
9136
- tr.append(keyCell, desc);
9137
- table2.appendChild(tr);
9587
+ groupSection.append(groupTitle);
9588
+ group.blocks.forEach((block2) => {
9589
+ groupSection.appendChild(renderHelpBlock(block2));
9138
9590
  });
9139
- groupSection.append(groupTitle, table2);
9140
9591
  article.appendChild(groupSection);
9141
9592
  });
9142
9593
  layout.append(helpNav, article);
@@ -15134,6 +15585,8 @@ ${frontmatter.yaml}
15134
15585
  const SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX = "gdp:scope-omit-dirs:";
15135
15586
  const SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX = "gdp:scope-exclude-names:";
15136
15587
  const CODE_FONT_SIZE_STORAGE_KEY = "gdp:code-font-size";
15588
+ const VIEWER_LANGUAGE_STORAGE_KEY = "gdp:language";
15589
+ const VIEWER_LANGUAGES = ["en", "ja"];
15137
15590
  const CLIENT_SCOPE_OMIT_DIRS_DEFAULT = [
15138
15591
  "node_modules",
15139
15592
  ".venv",
@@ -15293,6 +15746,16 @@ ${frontmatter.yaml}
15293
15746
  function normalizeViewerFontSize(value) {
15294
15747
  return value === "compact" || value === "large" || value === "xlarge" ? value : "regular";
15295
15748
  }
15749
+ function normalizeViewerLanguage(value) {
15750
+ return VIEWER_LANGUAGES.includes(value) ? value : "en";
15751
+ }
15752
+ function savedViewerLanguage() {
15753
+ return normalizeViewerLanguage(localStorage.getItem(VIEWER_LANGUAGE_STORAGE_KEY));
15754
+ }
15755
+ function viewerLanguageFromSearch(search) {
15756
+ const raw = new URLSearchParams(search).get("lang");
15757
+ return raw ? normalizeViewerLanguage(raw) : null;
15758
+ }
15296
15759
  function savedCodeFontSize() {
15297
15760
  return normalizeViewerFontSize(localStorage.getItem(CODE_FONT_SIZE_STORAGE_KEY));
15298
15761
  }
@@ -15329,11 +15792,14 @@ ${frontmatter.yaml}
15329
15792
  from: localStorage.getItem("gdp:from") || DEFAULT_RANGE.from,
15330
15793
  to: localStorage.getItem("gdp:to") || DEFAULT_RANGE.to
15331
15794
  };
15795
+ const savedLanguage = viewerLanguageFromSearch(window.location.search) || savedViewerLanguage();
15332
15796
  const parsedRoute = parseRoute(window.location.pathname, window.location.search, fallbackRange);
15333
- const route = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
15797
+ const routeBase = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
15798
+ const route = routeBase.screen === "help" && !new URLSearchParams(window.location.search).has("lang") ? { ...routeBase, lang: savedLanguage } : routeBase;
15334
15799
  return {
15335
15800
  layout: localStorage.getItem("gdp:layout") || "side-by-side",
15336
15801
  theme: localStorage.getItem("gdp:theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"),
15802
+ language: savedLanguage,
15337
15803
  sbView: localStorage.getItem("gdp:sbview") || "tree",
15338
15804
  sbWidth: parseInt(localStorage.getItem("gdp:sbwidth") ?? "", 10) || 308,
15339
15805
  sidebarHidden: localStorage.getItem("gdp:sidebar-hidden") === "1",
@@ -15532,6 +15998,324 @@ ${frontmatter.yaml}
15532
15998
  getServerGeneration: () => SERVER_GENERATION
15533
15999
  });
15534
16000
  const { openSearchPalette, isPaletteOpen, paletteMode, clearRepoFileCache } = SEARCH_PALETTE;
16001
+ const UI_TEXT = {
16002
+ en: {
16003
+ nav: {
16004
+ repo: "Repository",
16005
+ diff: "Diff Viewer",
16006
+ history: "History",
16007
+ help: "Help"
16008
+ },
16009
+ global: {
16010
+ annotations: "code annotations",
16011
+ settings: "viewer settings",
16012
+ theme: "toggle theme",
16013
+ product: "code viewer"
16014
+ },
16015
+ topbar: {
16016
+ resetRange: "reset to HEAD .. worktree",
16017
+ reload: "reload diff (R)",
16018
+ layout: "layout",
16019
+ unified: "unified",
16020
+ split: "split",
16021
+ ignoreWs: "ignore whitespace changes (-w)",
16022
+ syntaxLoading: "loading...",
16023
+ syntaxOn: "syntax on",
16024
+ syntaxOff: "syntax off",
16025
+ syntaxOnTitle: "syntax highlighting on",
16026
+ syntaxLoadingTitle: "loading syntax highlighter",
16027
+ syntaxErrorTitle: "failed to load syntax highlighter",
16028
+ syntaxOffTitle: "syntax highlighting off",
16029
+ hideTests: "hide test files (test|spec)"
16030
+ },
16031
+ sidebar: {
16032
+ files: "Files",
16033
+ actions: "sidebar actions",
16034
+ expandAll: "expand all folders",
16035
+ collapseAll: "collapse all folders",
16036
+ view: "view",
16037
+ tree: "tree",
16038
+ flat: "flat",
16039
+ filter: "Filter files...",
16040
+ filterTitle: "Filter files. Use /pattern/ for regex. Cmd/Ctrl+K focuses this field.",
16041
+ hide: "hide sidebar"
16042
+ },
16043
+ history: {
16044
+ title: "Commits",
16045
+ filter: "Filter commits...",
16046
+ filterTitle: "Filter commits by message, SHA, author:name, or path:file."
16047
+ },
16048
+ settings: {
16049
+ title: "Viewer Settings",
16050
+ close: "close viewer settings",
16051
+ display: "Display",
16052
+ language: "Language",
16053
+ fileListFontSize: "File list font size",
16054
+ codeFontSize: "Code font size",
16055
+ sizeSmall: "Small",
16056
+ sizeRegular: "Regular",
16057
+ sizeLarge: "Large",
16058
+ sizeExtraLarge: "Extra Large",
16059
+ displaySource: "Applies to all projects in this browser.",
16060
+ excludedDirectories: "Excluded directories",
16061
+ omitDirs: "Skip these directory names while browsing and searching",
16062
+ excludeNames: "Hide these file or directory names completely",
16063
+ reset: "Reset",
16064
+ save: "Save",
16065
+ scopeSource: (project, source) => `Saved for project "${project}" in this browser. Source: ${source}. Used by tree, Ctrl+K, and Ctrl+G. Reset removes the browser override.`,
16066
+ browserOverride: "Browser override",
16067
+ serverDefault: "Server default"
16068
+ },
16069
+ annotations: {
16070
+ title: "Code annotations",
16071
+ follow: "follow",
16072
+ followTitle: "jump to new annotations as they arrive",
16073
+ clear: "clear",
16074
+ close: "close",
16075
+ sessions: "Sessions"
16076
+ }
16077
+ },
16078
+ ja: {
16079
+ nav: {
16080
+ repo: "リポジトリ",
16081
+ diff: "Diff ビューア",
16082
+ history: "履歴",
16083
+ help: "ヘルプ"
16084
+ },
16085
+ global: {
16086
+ annotations: "コード注釈",
16087
+ settings: "ビューア設定",
16088
+ theme: "テーマ切り替え",
16089
+ product: "code viewer"
16090
+ },
16091
+ topbar: {
16092
+ resetRange: "HEAD .. worktree に戻す",
16093
+ reload: "diff を再読み込み (R)",
16094
+ layout: "レイアウト",
16095
+ unified: "unified",
16096
+ split: "split",
16097
+ ignoreWs: "空白差分を無視 (-w)",
16098
+ syntaxLoading: "読み込み中...",
16099
+ syntaxOn: "syntax on",
16100
+ syntaxOff: "syntax off",
16101
+ syntaxOnTitle: "シンタックスハイライト有効",
16102
+ syntaxLoadingTitle: "シンタックスハイライトを読み込み中",
16103
+ syntaxErrorTitle: "シンタックスハイライトの読み込みに失敗",
16104
+ syntaxOffTitle: "シンタックスハイライト無効",
16105
+ hideTests: "test/spec ファイルを隠す"
16106
+ },
16107
+ sidebar: {
16108
+ files: "ファイル",
16109
+ actions: "サイドバー操作",
16110
+ expandAll: "すべてのフォルダを開く",
16111
+ collapseAll: "すべてのフォルダを閉じる",
16112
+ view: "表示",
16113
+ tree: "ツリー",
16114
+ flat: "一覧",
16115
+ filter: "ファイルを絞り込み...",
16116
+ filterTitle: "ファイルを絞り込みます。正規表現は /pattern/。Cmd/Ctrl+K でフォーカス。",
16117
+ hide: "サイドバーを隠す"
16118
+ },
16119
+ history: {
16120
+ title: "コミット",
16121
+ filter: "コミットを絞り込み...",
16122
+ filterTitle: "メッセージ、SHA、author:name、path:file でコミットを絞り込みます。"
16123
+ },
16124
+ settings: {
16125
+ title: "ビューア設定",
16126
+ close: "ビューア設定を閉じる",
16127
+ display: "表示",
16128
+ language: "言語",
16129
+ fileListFontSize: "ファイル一覧の文字サイズ",
16130
+ codeFontSize: "コードの文字サイズ",
16131
+ sizeSmall: "小",
16132
+ sizeRegular: "標準",
16133
+ sizeLarge: "大",
16134
+ sizeExtraLarge: "特大",
16135
+ displaySource: "このブラウザのすべてのプロジェクトに適用されます。",
16136
+ excludedDirectories: "除外ディレクトリ",
16137
+ omitDirs: "閲覧と検索でスキップするディレクトリ名",
16138
+ excludeNames: "完全に非表示にするファイル名またはディレクトリ名",
16139
+ reset: "リセット",
16140
+ save: "保存",
16141
+ scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。ツリー、Ctrl+K、Ctrl+G で使われます。リセットするとブラウザ側の上書きを削除します。`,
16142
+ browserOverride: "ブラウザ側の上書き",
16143
+ serverDefault: "サーバ既定値"
16144
+ },
16145
+ annotations: {
16146
+ title: "コード注釈",
16147
+ follow: "追従",
16148
+ followTitle: "新しい注釈が届いたら移動する",
16149
+ clear: "削除",
16150
+ close: "閉じる",
16151
+ sessions: "セッション"
16152
+ }
16153
+ }
16154
+ };
16155
+ function uiText() {
16156
+ return UI_TEXT[STATE.language];
16157
+ }
16158
+ function setElementText(selector, text2) {
16159
+ const el = document.querySelector(selector);
16160
+ if (el)
16161
+ el.textContent = text2;
16162
+ }
16163
+ function setButtonLabel(button, text2) {
16164
+ if (button)
16165
+ button.textContent = text2;
16166
+ }
16167
+ function setOptionText(select, labels) {
16168
+ select?.querySelectorAll("option").forEach((option) => {
16169
+ const label = labels[option.value];
16170
+ if (label)
16171
+ option.textContent = label;
16172
+ });
16173
+ }
16174
+ function localizeViewerChrome() {
16175
+ const text2 = uiText();
16176
+ document.documentElement.lang = STATE.language;
16177
+ document.querySelectorAll(".app-menu-item").forEach((link2) => {
16178
+ const route = link2.dataset.route;
16179
+ if (route && text2.nav[route])
16180
+ link2.textContent = text2.nav[route];
16181
+ });
16182
+ setElementText(".global-help-link[data-route='help']", text2.nav.help);
16183
+ setElementText(".product-label", text2.global.product);
16184
+ const annotationsToggle = document.querySelector("#annotations-toggle");
16185
+ if (annotationsToggle) {
16186
+ annotationsToggle.title = text2.global.annotations;
16187
+ annotationsToggle.setAttribute("aria-label", text2.global.annotations);
16188
+ }
16189
+ const viewerSettings = document.querySelector("#viewer-settings");
16190
+ if (viewerSettings) {
16191
+ viewerSettings.title = text2.global.settings;
16192
+ viewerSettings.setAttribute("aria-label", text2.global.settings);
16193
+ }
16194
+ const theme = document.querySelector("#theme");
16195
+ if (theme)
16196
+ theme.title = text2.global.theme;
16197
+ const refReset = document.querySelector("#ref-reset");
16198
+ if (refReset)
16199
+ refReset.title = text2.topbar.resetRange;
16200
+ const reload = document.querySelector("#reload-prom");
16201
+ if (reload)
16202
+ reload.title = text2.topbar.reload;
16203
+ const layoutGroup = document.querySelector("#topbar .seg");
16204
+ layoutGroup?.setAttribute("aria-label", text2.topbar.layout);
16205
+ setElementText('#topbar .seg button[data-layout="line-by-line"]', text2.topbar.unified);
16206
+ setElementText('#topbar .seg button[data-layout="side-by-side"]', text2.topbar.split);
16207
+ const ignoreWs = document.querySelector("#ignore-ws");
16208
+ if (ignoreWs)
16209
+ ignoreWs.title = text2.topbar.ignoreWs;
16210
+ const hideTests = document.querySelector("#hide-tests");
16211
+ if (hideTests)
16212
+ hideTests.title = text2.topbar.hideTests;
16213
+ setHighlightButton(STATE.syntaxHighlight && getHljs() ? "loaded" : "idle");
16214
+ setElementText(".sb-title", text2.sidebar.files);
16215
+ const sidebarActions = document.querySelector(".sb-actions");
16216
+ sidebarActions?.setAttribute("aria-label", text2.sidebar.actions);
16217
+ const expandAll = document.querySelector("#sb-expand-all");
16218
+ if (expandAll) {
16219
+ expandAll.title = text2.sidebar.expandAll;
16220
+ expandAll.setAttribute("aria-label", text2.sidebar.expandAll);
16221
+ }
16222
+ const collapseAll = document.querySelector("#sb-collapse-all");
16223
+ if (collapseAll) {
16224
+ collapseAll.title = text2.sidebar.collapseAll;
16225
+ collapseAll.setAttribute("aria-label", text2.sidebar.collapseAll);
16226
+ }
16227
+ const sbView = document.querySelector(".sb-view-seg");
16228
+ sbView?.setAttribute("aria-label", text2.sidebar.view);
16229
+ setElementText('.sb-view-seg button[data-view="tree"]', text2.sidebar.tree);
16230
+ setElementText('.sb-view-seg button[data-view="flat"]', text2.sidebar.flat);
16231
+ const filter = document.querySelector("#sb-filter");
16232
+ if (filter) {
16233
+ filter.placeholder = text2.sidebar.filter;
16234
+ filter.title = text2.sidebar.filterTitle;
16235
+ }
16236
+ const sidebarToggle = document.querySelector("#sidebar-toggle");
16237
+ if (sidebarToggle) {
16238
+ sidebarToggle.title = text2.sidebar.hide;
16239
+ sidebarToggle.setAttribute("aria-label", text2.sidebar.hide);
16240
+ }
16241
+ setElementText(".sidebar-toggle-label", text2.sidebar.files);
16242
+ setElementText(".history-title", text2.history.title);
16243
+ const historyPanel = document.querySelector("#history-panel");
16244
+ historyPanel?.setAttribute("aria-label", text2.history.title);
16245
+ const historyFilter = document.querySelector("#history-filter");
16246
+ if (historyFilter) {
16247
+ historyFilter.placeholder = text2.history.filter;
16248
+ historyFilter.title = text2.history.filterTitle;
16249
+ }
16250
+ setElementText(".scope-settings-head strong", text2.settings.title);
16251
+ const settingsClose = document.querySelector("#scope-settings-close");
16252
+ settingsClose?.setAttribute("aria-label", text2.settings.close);
16253
+ const settingsSections = document.querySelectorAll(".scope-settings-section-title");
16254
+ if (settingsSections[0])
16255
+ settingsSections[0].textContent = text2.settings.display;
16256
+ if (settingsSections[1])
16257
+ settingsSections[1].textContent = text2.settings.excludedDirectories;
16258
+ const labelMap = {
16259
+ "viewer-language": text2.settings.language,
16260
+ "sidebar-font-size": text2.settings.fileListFontSize,
16261
+ "code-font-size": text2.settings.codeFontSize,
16262
+ "scope-omit-dirs": text2.settings.omitDirs,
16263
+ "scope-exclude-names": text2.settings.excludeNames
16264
+ };
16265
+ Object.entries(labelMap).forEach(([id, label]) => {
16266
+ const labelEl = document.querySelector(`label[for="${id}"]`);
16267
+ if (labelEl)
16268
+ labelEl.textContent = label;
16269
+ });
16270
+ setOptionText(document.querySelector("#sidebar-font-size"), {
16271
+ compact: text2.settings.sizeSmall,
16272
+ regular: text2.settings.sizeRegular,
16273
+ large: text2.settings.sizeLarge,
16274
+ xlarge: text2.settings.sizeExtraLarge
16275
+ });
16276
+ setOptionText(document.querySelector("#code-font-size"), {
16277
+ compact: text2.settings.sizeSmall,
16278
+ regular: text2.settings.sizeRegular,
16279
+ large: text2.settings.sizeLarge,
16280
+ xlarge: text2.settings.sizeExtraLarge
16281
+ });
16282
+ setElementText("#display-settings-source", text2.settings.displaySource);
16283
+ setButtonLabel(document.querySelector("#scope-omit-reset"), text2.settings.reset);
16284
+ setButtonLabel(document.querySelector("#scope-omit-save"), text2.settings.save);
16285
+ setElementText(".annotation-panel-head strong", text2.annotations.title);
16286
+ const followLabel = document.querySelector(".annotation-follow-label");
16287
+ if (followLabel) {
16288
+ followLabel.title = text2.annotations.followTitle;
16289
+ const input = followLabel.querySelector("input");
16290
+ followLabel.replaceChildren();
16291
+ if (input)
16292
+ followLabel.append(input, ` ${text2.annotations.follow}`);
16293
+ }
16294
+ setButtonLabel(document.querySelector("#annotation-clear"), text2.annotations.clear);
16295
+ setButtonLabel(document.querySelector("#annotation-panel-close"), text2.annotations.close);
16296
+ setElementText(".annotation-list-head strong", text2.annotations.sessions);
16297
+ }
16298
+ function setViewerLanguage(language, persist = true) {
16299
+ const next = normalizeViewerLanguage(language);
16300
+ STATE.language = next;
16301
+ if (persist)
16302
+ localStorage.setItem(VIEWER_LANGUAGE_STORAGE_KEY, next);
16303
+ const select = document.querySelector("#viewer-language");
16304
+ if (select)
16305
+ select.value = next;
16306
+ localizeViewerChrome();
16307
+ if (STATE.route.screen === "help") {
16308
+ setRoute({
16309
+ screen: "help",
16310
+ lang: next,
16311
+ section: helpSectionFromRoute(STATE.route),
16312
+ range: currentRange()
16313
+ }, true);
16314
+ renderHelpPage();
16315
+ } else {
16316
+ syncHeaderMenu();
16317
+ }
16318
+ }
15535
16319
  function setStatus(s2) {
15536
16320
  const el = $("#status");
15537
16321
  el.classList.remove("live", "refreshing", "error");
@@ -15557,11 +16341,12 @@ ${frontmatter.yaml}
15557
16341
  const btn = $("#syntax-highlight");
15558
16342
  if (!btn)
15559
16343
  return;
16344
+ const text2 = uiText();
15560
16345
  btn.classList.toggle("active", STATE.syntaxHighlight);
15561
16346
  btn.classList.toggle("loading", state === "loading");
15562
- btn.textContent = state === "loading" ? "loading..." : STATE.syntaxHighlight ? "syntax on" : "syntax off";
16347
+ btn.textContent = state === "loading" ? text2.topbar.syntaxLoading : STATE.syntaxHighlight ? text2.topbar.syntaxOn : text2.topbar.syntaxOff;
15563
16348
  btn.setAttribute("aria-pressed", STATE.syntaxHighlight ? "true" : "false");
15564
- btn.title = STATE.syntaxHighlight ? "syntax highlighting on" : state === "loading" ? "loading syntax highlighter" : state === "error" ? "failed to load syntax highlighter" : "syntax highlighting off";
16349
+ btn.title = STATE.syntaxHighlight ? text2.topbar.syntaxOnTitle : state === "loading" ? text2.topbar.syntaxLoadingTitle : state === "error" ? text2.topbar.syntaxErrorTitle : text2.topbar.syntaxOffTitle;
15565
16350
  }
15566
16351
  function loadSyntaxHighlighter() {
15567
16352
  const existing = getHljs();
@@ -15620,7 +16405,7 @@ ${frontmatter.yaml}
15620
16405
  el.innerHTML = '<svg class="octicon octicon-chevron-down" viewBox="0 0 12 12" width="12" height="12" fill="currentColor" aria-hidden="true">' + '<path fill="currentColor" d="' + CHEVRON_DOWN_12_PATH + '"></path></svg>';
15621
16406
  }
15622
16407
  function scopeOmitSourceLabel() {
15623
- return savedScopeOmitDirs() != null || savedScopeExcludeNames() != null ? "Browser override" : "Server default";
16408
+ return savedScopeOmitDirs() != null || savedScopeExcludeNames() != null ? uiText().settings.browserOverride : uiText().settings.serverDefault;
15624
16409
  }
15625
16410
  function refreshRepositoryTreeAfterSettings() {
15626
16411
  clearRepoFileCache();
@@ -15639,19 +16424,22 @@ ${frontmatter.yaml}
15639
16424
  const excludeInput = document.querySelector("#scope-exclude-names");
15640
16425
  const sidebarFontSize = document.querySelector("#sidebar-font-size");
15641
16426
  const codeFontSize = document.querySelector("#code-font-size");
16427
+ const viewerLanguage = document.querySelector("#viewer-language");
15642
16428
  const source = document.querySelector("#scope-omit-source");
15643
- if (!pop || !input || !excludeInput || !sidebarFontSize || !codeFontSize || !source)
16429
+ if (!pop || !input || !excludeInput || !sidebarFontSize || !codeFontSize || !viewerLanguage || !source)
15644
16430
  return;
15645
16431
  await loadSettings();
16432
+ localizeViewerChrome();
16433
+ viewerLanguage.value = STATE.language;
15646
16434
  sidebarFontSize.value = savedSidebarFontSize();
15647
16435
  codeFontSize.value = savedCodeFontSize();
15648
16436
  input.value = effectiveScopeOmitDirs().join(`
15649
16437
  `);
15650
16438
  excludeInput.value = effectiveScopeExcludeNames().join(`
15651
16439
  `);
15652
- source.textContent = 'Saved for project "' + (PROJECT_NAME || "default") + '" in this browser. Source: ' + scopeOmitSourceLabel() + ". Used by tree, Ctrl+K, and Ctrl+G. Reset removes the browser override.";
16440
+ source.textContent = uiText().settings.scopeSource(PROJECT_NAME || "default", scopeOmitSourceLabel());
15653
16441
  pop.hidden = false;
15654
- sidebarFontSize.focus();
16442
+ viewerLanguage.focus();
15655
16443
  }
15656
16444
  function closeScopeSettings() {
15657
16445
  const pop = document.querySelector("#scope-settings-popover");
@@ -15671,8 +16459,10 @@ ${frontmatter.yaml}
15671
16459
  const excludeInput = document.querySelector("#scope-exclude-names");
15672
16460
  const sidebarFontSize = document.querySelector("#sidebar-font-size");
15673
16461
  const codeFontSize = document.querySelector("#code-font-size");
15674
- if (!input || !excludeInput || !sidebarFontSize || !codeFontSize)
16462
+ const viewerLanguage = document.querySelector("#viewer-language");
16463
+ if (!input || !excludeInput || !sidebarFontSize || !codeFontSize || !viewerLanguage)
15675
16464
  return;
16465
+ setViewerLanguage(normalizeViewerLanguage(viewerLanguage.value));
15676
16466
  localStorage.setItem(SIDEBAR_FONT_SIZE_KEY, normalizeViewerFontSize(sidebarFontSize.value));
15677
16467
  localStorage.setItem(CODE_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(codeFontSize.value));
15678
16468
  applySidebarFontSize();
@@ -15683,6 +16473,7 @@ ${frontmatter.yaml}
15683
16473
  refreshRepositoryTreeAfterSettings();
15684
16474
  }
15685
16475
  function resetScopeSettings() {
16476
+ setViewerLanguage("en");
15686
16477
  localStorage.removeItem(SIDEBAR_FONT_SIZE_KEY);
15687
16478
  localStorage.removeItem(CODE_FONT_SIZE_STORAGE_KEY);
15688
16479
  applySidebarFontSize("regular");
@@ -15865,7 +16656,7 @@ ${frontmatter.yaml}
15865
16656
  if (link2.dataset.route === "help") {
15866
16657
  link2.href = buildRoute({
15867
16658
  screen: "help",
15868
- lang: helpLanguageFromRoute(STATE.route),
16659
+ lang: STATE.route.screen === "help" ? helpLanguageFromRoute(STATE.route) : STATE.language,
15869
16660
  section: helpSectionFromRoute(STATE.route),
15870
16661
  range: currentRange()
15871
16662
  });
@@ -15969,7 +16760,9 @@ ${frontmatter.yaml}
15969
16760
  removeStandaloneSource,
15970
16761
  clearLoadQueue: () => DIFF_VIEW.clearLoadQueue(),
15971
16762
  currentRange,
15972
- syncHeaderMenu
16763
+ syncHeaderMenu,
16764
+ getLanguage: () => STATE.language,
16765
+ setLanguage: (language) => setViewerLanguage(language)
15973
16766
  });
15974
16767
  const { setupHunkExpand } = createHunkExpand({
15975
16768
  trackLoad,
@@ -16044,10 +16837,18 @@ ${frontmatter.yaml}
16044
16837
  $("#scope-settings-close")?.addEventListener("click", closeScopeSettings);
16045
16838
  $("#scope-omit-save")?.addEventListener("click", saveScopeSettings);
16046
16839
  $("#scope-omit-reset")?.addEventListener("click", resetScopeSettings);
16840
+ $("#viewer-language")?.addEventListener("change", (event) => {
16841
+ const select = event.currentTarget;
16842
+ setViewerLanguage(normalizeViewerLanguage(select.value));
16843
+ const source = document.querySelector("#scope-omit-source");
16844
+ if (source)
16845
+ source.textContent = uiText().settings.scopeSource(PROJECT_NAME || "default", scopeOmitSourceLabel());
16846
+ });
16047
16847
  $("#scope-settings-popover")?.addEventListener("keydown", (e2) => {
16048
16848
  if (e2.key === "Escape")
16049
16849
  closeScopeSettings();
16050
16850
  });
16851
+ localizeViewerChrome();
16051
16852
  prepareKeyboardPanels();
16052
16853
  const contentPanel = document.querySelector("#content");
16053
16854
  contentPanel?.addEventListener("focusin", () => setPanelFocusScope("main"));
@@ -16483,7 +17284,11 @@ ${frontmatter.yaml}
16483
17284
  restoreRangeAfterHistory();
16484
17285
  }
16485
17286
  const parsedRoute = parseRoute(window.location.pathname, window.location.search, currentRange());
16486
- STATE.route = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
17287
+ const routeLanguage = viewerLanguageFromSearch(window.location.search);
17288
+ if (routeLanguage && routeLanguage !== STATE.language)
17289
+ setViewerLanguage(routeLanguage);
17290
+ const nextRoute = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
17291
+ STATE.route = nextRoute.screen === "help" && !new URLSearchParams(window.location.search).has("lang") ? { ...nextRoute, lang: STATE.language } : nextRoute;
16487
17292
  STATE.from = STATE.route.range.from;
16488
17293
  STATE.to = STATE.route.range.to;
16489
17294
  if (STATE.route.screen === "repo")