dsh-ask-from-text 0.1.19 → 0.1.21

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/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to `dsh-ask-from-text` are documented here. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
5
5
  project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.1.21] - 2026-11-13
8
+
9
+ ### Added
10
+
11
+ - Runtime dictionaries and Plugin Manager metadata for Japanese, Spanish,
12
+ French, German, Russian, and Brazilian Portuguese, completing the eight DSH
13
+ languages. Unsupported locales fall back to English.
14
+
15
+ ## [0.1.20] - 2026-11-13
16
+
17
+ ### Added
18
+
19
+ - Runtime UI localization for Chinese and English. The active DSH language is
20
+ read through `ctx.locale`; other languages fall back to English. Native
21
+ conversation and Plugin Manager metadata continue to follow DSH.
22
+
7
23
  ## [0.1.19] - 2026-11-13
8
24
 
9
25
  ### Fixed
@@ -55,6 +71,22 @@ project follows [Semantic Versioning](https://semver.org/).
55
71
 
56
72
  - README screenshots now use absolute CDN URLs so they render on the npm package page.
57
73
 
74
+ ## [0.1.21] - 2026-11-13
75
+
76
+ ### Added
77
+
78
+ - Runtime dictionaries and Plugin Manager metadata for Japanese, Spanish,
79
+ French, German, Russian, and Brazilian Portuguese, completing the eight DSH
80
+ languages. Unsupported locales fall back to English.
81
+
82
+ ## [0.1.20] - 2026-11-13
83
+
84
+ ### Added
85
+
86
+ - Runtime UI localization for Chinese and English. The active DSH language is
87
+ read through `ctx.locale`; other languages fall back to English. Native
88
+ conversation and Plugin Manager metadata continue to follow DSH.
89
+
58
90
  ## [0.1.19] - 2026-11-13
59
91
 
60
92
  ### Fixed
@@ -194,6 +226,8 @@ project follows [Semantic Versioning](https://semver.org/).
194
226
  [0.1.17]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.17
195
227
  [0.1.18]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.18
196
228
  [0.1.19]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.19
229
+ [0.1.20]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.20
230
+ [0.1.21]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.21
197
231
  [0.1.13]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.13
198
232
  [0.1.14]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.14
199
233
  [0.1.15]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.15
@@ -201,3 +235,5 @@ project follows [Semantic Versioning](https://semver.org/).
201
235
  [0.1.17]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.17
202
236
  [0.1.18]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.18
203
237
  [0.1.19]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.19
238
+ [0.1.20]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.20
239
+ [0.1.21]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.21
package/README.md CHANGED
@@ -103,7 +103,7 @@ dsh plugin --profile web add /absolute/path/to/dsh-ask-from-text
103
103
  - 追问回答是真实的 DSH Session,可以使用思考、工具调用、Stop 和原生输入框。
104
104
  - 分支只看到父级和祖先内容,不看到兄弟分支,也不会自动获得父级后来新增的内容。
105
105
  - 分支对话会出现在普通 Session 列表中,这是正常现象。
106
- - 当前浮窗和舞台的运行时按钮文案为中文;插件管理器的标题/描述提供中英文。
106
+ - 界面文案跟随 DSH 语言,内置中文、英文、日语、西班牙语、法语、德语、俄语、巴西葡萄牙语;其他语言回退英文。原生对话区域继续跟随 DSH 自身的语言。
107
107
  - 目前主要面向 DSH Desktop;Web 共用同一套 UI,窄屏体验尚未专门优化。
108
108
 
109
109
  ## 常见问题
@@ -225,7 +225,7 @@ These sessions are grouped under a `追问分支` header. Click the header to co
225
225
  - A follow-up is a real DSH Session. It can use reasoning, tool calls, Stop, and the native composer.
226
226
  - A branch sees its parent and ancestors, not siblings, and does not automatically receive later turns added to its parent.
227
227
  - Branch conversations also appear in the normal Session list. This is expected.
228
- - Runtime UI labels are currently Chinese; Plugin Manager metadata is available in Chinese and English.
228
+ - Runtime UI follows the DSH language: Chinese, English, Japanese, Spanish, French, German, Russian, and Brazilian Portuguese are bundled; other languages fall back to English. The native conversation follows DSH.
229
229
  - DSH Desktop is the primary target. The Web client shares the same UI, but narrow-screen behavior has not been specifically polished.
230
230
 
231
231
  ### FAQ
package/client.js CHANGED
@@ -78,6 +78,13 @@ window.__ModuleLoader__.load({
78
78
  stages: '个节点',
79
79
  collapseDock: '收起列表',
80
80
  expandDock: '展开列表',
81
+ sidebarGroup: '追问分支',
82
+ context: '上下文',
83
+ contextStart: '上下文起点',
84
+ waiting: '待提问',
85
+ treeNote: '当前模型上下文:{context}。兄弟分支不在其中。',
86
+ anchorMissing: '#{n} 的原文当前不可见,可能已被折叠或尚未加载',
87
+ draftWait: '在下方输入浮窗中提问',
81
88
  },
82
89
  en: {
83
90
  chip: 'Ask',
@@ -126,8 +133,339 @@ window.__ModuleLoader__.load({
126
133
  stages: 'nodes',
127
134
  collapseDock: 'Collapse list',
128
135
  expandDock: 'Expand list',
136
+ sidebarGroup: 'Branches',
137
+ context: 'Context',
138
+ contextStart: 'Context root',
139
+ waiting: 'Waiting for question',
140
+ treeNote: 'Current model context: {context}. Sibling branches are not included.',
141
+ anchorMissing: 'Source text for #{n} is not visible; it may be folded or not loaded yet',
142
+ draftWait: 'Ask in the input window below',
129
143
  },
130
- }
144
+ ja: {
145
+ chip: '質問',
146
+ chipHint: '選択したテキストについて独立したブランチで質問',
147
+ branch: 'ブランチ',
148
+ branchN: 'ブランチ',
149
+ basedOn: '基準',
150
+ main: 'メイン会話',
151
+ independent: '独立ブランチ',
152
+ draft: '下書き',
153
+ sending: '作成中',
154
+ running: '実行中',
155
+ ready: '作成済み',
156
+ failed: '失敗',
157
+ send: '送信',
158
+ sendingAction: '送信中…',
159
+ quote: '引用済み',
160
+ defaultQuestion: 'この内容についてさらに説明してください。',
161
+ empty: 'ここに質問を追加…',
162
+ sendHint: 'Enter で送信 · Shift + Enter で改行',
163
+ collapse: '折りたたむ',
164
+ input: '入力',
165
+ deleteDraft: '下書きを削除',
166
+ close: '閉じる',
167
+ expand: '拡大',
168
+ restore: 'パネルに戻す',
169
+ back: '戻る',
170
+ forward: '進む',
171
+ stageHint: '戻る / 進むは表示中のブランチを切り替えるだけで、モデルのコンテキストは変わりません。',
172
+ path: 'パス',
173
+ all: 'すべて',
174
+ tree: 'ブランチツリー',
175
+ current: '現在',
176
+ dock: '折りたたんだブランチ',
177
+ open: '開く',
178
+ retry: '再試行',
179
+ error: '送信に失敗しました',
180
+ forkError: 'ブランチを作成できません',
181
+ noSession: '現在のセッションは利用できません',
182
+ tooLong: '選択が長すぎます。短くしてもう一度お試しください',
183
+ content: '本文',
184
+ quoteLabel: '引用元',
185
+ showQuote: '引用を表示',
186
+ hideQuote: '引用を隠す',
187
+ switchBranch: 'このブランチに切り替え',
188
+ stages: '個のノード',
189
+ collapseDock: 'リストを折りたたむ',
190
+ expandDock: 'リストを展開',
191
+ sidebarGroup: '質問ブランチ',
192
+ context: 'コンテキスト',
193
+ contextStart: 'コンテキストの起点',
194
+ waiting: '質問待ち',
195
+ treeNote: '現在のモデルコンテキスト:{context}。兄弟ブランチは含まれません。',
196
+ anchorMissing: '#{n} の原文は現在表示できません。折りたたまれているか、まだ読み込まれていない可能性があります',
197
+ draftWait: '下の入力ウィンドウで質問してください',
198
+ }, es: {
199
+ chip: 'Preguntar',
200
+ chipHint: 'Pregunta sobre el texto seleccionado en una rama independiente',
201
+ branch: 'Rama',
202
+ branchN: 'Rama',
203
+ basedOn: 'basado en',
204
+ main: 'Chat principal',
205
+ independent: 'Rama independiente',
206
+ draft: 'Borrador',
207
+ sending: 'Creando',
208
+ running: 'En ejecución',
209
+ ready: 'Creado',
210
+ failed: 'Falló',
211
+ send: 'Enviar',
212
+ sendingAction: 'Enviando…',
213
+ quote: 'Citado',
214
+ defaultQuestion: 'Explica este contenido con más detalle.',
215
+ empty: 'Añade tu pregunta…',
216
+ sendHint: 'Enter para enviar · Shift + Enter para nueva línea',
217
+ collapse: 'Contraer',
218
+ input: 'Entrada',
219
+ deleteDraft: 'Eliminar borrador',
220
+ close: 'Cerrar',
221
+ expand: 'Ampliar',
222
+ restore: 'Restaurar panel',
223
+ back: 'Atrás',
224
+ forward: 'Adelante',
225
+ stageHint: 'Atrás / Adelante solo cambia la rama que estás viendo; no cambia el contexto del modelo.',
226
+ path: 'Ruta',
227
+ all: 'Todo',
228
+ tree: 'Árbol de ramas',
229
+ current: 'Actual',
230
+ dock: 'Ramas contraídas',
231
+ open: 'Abrir',
232
+ retry: 'Reintentar',
233
+ error: 'Error al enviar',
234
+ forkError: 'No se pudo crear la rama',
235
+ noSession: 'Sesión no disponible',
236
+ tooLong: 'La selección es demasiado larga',
237
+ content: 'Texto',
238
+ quoteLabel: 'Citado de',
239
+ showQuote: 'Mostrar cita',
240
+ hideQuote: 'Ocultar cita',
241
+ switchBranch: 'Cambiar a esta rama',
242
+ stages: 'nodos',
243
+ collapseDock: 'Contraer lista',
244
+ expandDock: 'Expandir lista',
245
+ sidebarGroup: 'Ramas de seguimiento',
246
+ context: 'Contexto',
247
+ contextStart: 'Inicio del contexto',
248
+ waiting: 'Esperando pregunta',
249
+ treeNote: 'Contexto actual del modelo: {context}. Las ramas hermanas no están incluidas.',
250
+ anchorMissing: 'El texto original de #{n} no está visible; puede estar plegado o no haberse cargado',
251
+ draftWait: 'Pregunta en la ventana de entrada de abajo',
252
+ }, fr: {
253
+ chip: 'Demander',
254
+ chipHint: 'Posez une question sur le texte sélectionné dans une branche indépendante',
255
+ branch: 'Branche',
256
+ branchN: 'Branche',
257
+ basedOn: 'basé sur',
258
+ main: 'Conversation principale',
259
+ independent: 'Branche indépendante',
260
+ draft: 'Brouillon',
261
+ sending: 'Création',
262
+ running: 'En cours',
263
+ ready: 'Créé',
264
+ failed: 'Échec',
265
+ send: 'Envoyer',
266
+ sendingAction: 'Envoi…',
267
+ quote: 'Cité',
268
+ defaultQuestion: 'Explique ce passage plus en détail.',
269
+ empty: 'Ajoutez votre question…',
270
+ sendHint: 'Entrée pour envoyer · Maj + Entrée pour un saut de ligne',
271
+ collapse: 'Réduire',
272
+ input: 'Saisie',
273
+ deleteDraft: 'Supprimer le brouillon',
274
+ close: 'Fermer',
275
+ expand: 'Agrandir',
276
+ restore: 'Restaurer le panneau',
277
+ back: 'Retour',
278
+ forward: 'Suivant',
279
+ stageHint: 'Retour / Suivant change uniquement la branche affichée ; cela ne modifie pas le contexte du modèle.',
280
+ path: 'Chemin',
281
+ all: 'Tout',
282
+ tree: 'Arbre des branches',
283
+ current: 'Actuel',
284
+ dock: 'Branches réduites',
285
+ open: 'Ouvrir',
286
+ retry: 'Réessayer',
287
+ error: 'Échec de l\'envoi',
288
+ forkError: 'Impossible de créer la branche',
289
+ noSession: 'Session indisponible',
290
+ tooLong: 'La sélection est trop longue',
291
+ content: 'Texte',
292
+ quoteLabel: 'Cité de',
293
+ showQuote: 'Afficher la citation',
294
+ hideQuote: 'Masquer la citation',
295
+ switchBranch: 'Passer à cette branche',
296
+ stages: 'nœuds',
297
+ collapseDock: 'Réduire la liste',
298
+ expandDock: 'Développer la liste',
299
+ sidebarGroup: 'Branches de suivi',
300
+ context: 'Contexte',
301
+ contextStart: 'Racine du contexte',
302
+ waiting: 'En attente de question',
303
+ treeNote: 'Contexte actuel du modèle : {context}. Les branches sœurs ne sont pas incluses.',
304
+ anchorMissing: 'Le texte source de #{n} n\'est pas visible ; il est peut-être replié ou pas encore chargé',
305
+ draftWait: 'Posez votre question dans la fenêtre de saisie ci-dessous',
306
+ }, de: {
307
+ chip: 'Fragen',
308
+ chipHint: 'Frage zum ausgewählten Text in einem unabhängigen Zweig stellen',
309
+ branch: 'Zweig',
310
+ branchN: 'Zweig',
311
+ basedOn: 'basierend auf',
312
+ main: 'Hauptchat',
313
+ independent: 'Unabhängiger Zweig',
314
+ draft: 'Entwurf',
315
+ sending: 'Wird erstellt',
316
+ running: 'Läuft',
317
+ ready: 'Erstellt',
318
+ failed: 'Fehlgeschlagen',
319
+ send: 'Senden',
320
+ sendingAction: 'Wird gesendet…',
321
+ quote: 'Zitiert',
322
+ defaultQuestion: 'Erkläre diesen Inhalt genauer.',
323
+ empty: 'Frage hier ergänzen…',
324
+ sendHint: 'Enter zum Senden · Umschalt + Enter für neue Zeile',
325
+ collapse: 'Einklappen',
326
+ input: 'Eingabe',
327
+ deleteDraft: 'Entwurf löschen',
328
+ close: 'Schließen',
329
+ expand: 'Vergrößern',
330
+ restore: 'Panel wiederherstellen',
331
+ back: 'Zurück',
332
+ forward: 'Weiter',
333
+ stageHint: 'Zurück / Weiter wechselt nur den angezeigten Zweig; der Modellkontext bleibt unverändert.',
334
+ path: 'Pfad',
335
+ all: 'Alle',
336
+ tree: 'Zweigbaum',
337
+ current: 'Aktuell',
338
+ dock: 'Eingeklappte Zweige',
339
+ open: 'Öffnen',
340
+ retry: 'Erneut versuchen',
341
+ error: 'Senden fehlgeschlagen',
342
+ forkError: 'Zweig konnte nicht erstellt werden',
343
+ noSession: 'Sitzung nicht verfügbar',
344
+ tooLong: 'Auswahl ist zu lang',
345
+ content: 'Text',
346
+ quoteLabel: 'Zitiert aus',
347
+ showQuote: 'Zitat anzeigen',
348
+ hideQuote: 'Zitat ausblenden',
349
+ switchBranch: 'Zu diesem Zweig wechseln',
350
+ stages: 'Knoten',
351
+ collapseDock: 'Liste einklappen',
352
+ expandDock: 'Liste ausklappen',
353
+ sidebarGroup: 'Folgezweige',
354
+ context: 'Kontext',
355
+ contextStart: 'Kontextwurzel',
356
+ waiting: 'Warte auf Frage',
357
+ treeNote: 'Aktueller Modellkontext: {context}. Geschwisterzweige sind nicht enthalten.',
358
+ anchorMissing: 'Der Quelltext von #{n} ist nicht sichtbar; er ist möglicherweise eingeklappt oder noch nicht geladen',
359
+ draftWait: 'Stelle deine Frage im Eingabefenster unten',
360
+ }, ru: {
361
+ chip: 'Спросить',
362
+ chipHint: 'Задать вопрос по выделенному тексту в отдельной ветке',
363
+ branch: 'Ветка',
364
+ branchN: 'Ветка',
365
+ basedOn: 'на основе',
366
+ main: 'Основной чат',
367
+ independent: 'Независимая ветка',
368
+ draft: 'Черновик',
369
+ sending: 'Создание',
370
+ running: 'Выполняется',
371
+ ready: 'Создано',
372
+ failed: 'Ошибка',
373
+ send: 'Отправить',
374
+ sendingAction: 'Отправка…',
375
+ quote: 'Цитата',
376
+ defaultQuestion: 'Объясните этот фрагмент подробнее.',
377
+ empty: 'Добавьте вопрос…',
378
+ sendHint: 'Enter — отправить · Shift + Enter — новая строка',
379
+ collapse: 'Свернуть',
380
+ input: 'Ввод',
381
+ deleteDraft: 'Удалить черновик',
382
+ close: 'Закрыть',
383
+ expand: 'Развернуть',
384
+ restore: 'Вернуть панель',
385
+ back: 'Назад',
386
+ forward: 'Вперёд',
387
+ stageHint: 'Назад / Вперёд переключает только просматриваемую ветку; контекст модели не меняется.',
388
+ path: 'Путь',
389
+ all: 'Все',
390
+ tree: 'Дерево веток',
391
+ current: 'Текущая',
392
+ dock: 'Свёрнутые ветки',
393
+ open: 'Открыть',
394
+ retry: 'Повторить',
395
+ error: 'Не удалось отправить',
396
+ forkError: 'Не удалось создать ветку',
397
+ noSession: 'Сеанс недоступен',
398
+ tooLong: 'Выделение слишком длинное',
399
+ content: 'Текст',
400
+ quoteLabel: 'Цитата из',
401
+ showQuote: 'Показать цитату',
402
+ hideQuote: 'Скрыть цитату',
403
+ switchBranch: 'Перейти к этой ветке',
404
+ stages: 'узлов',
405
+ collapseDock: 'Свернуть список',
406
+ expandDock: 'Развернуть список',
407
+ sidebarGroup: 'Ветки вопросов',
408
+ context: 'Контекст',
409
+ contextStart: 'Начало контекста',
410
+ waiting: 'Ожидание вопроса',
411
+ treeNote: 'Текущий контекст модели: {context}. Ветки-сестры не включены.',
412
+ anchorMissing: 'Исходный текст #{n} не отображается; возможно, он свёрнут или ещё не загружен',
413
+ draftWait: 'Задайте вопрос в окне ввода ниже',
414
+ }, pt: {
415
+ chip: 'Perguntar',
416
+ chipHint: 'Pergunte sobre o texto selecionado em um ramo independente',
417
+ branch: 'Ramo',
418
+ branchN: 'Ramo',
419
+ basedOn: 'com base em',
420
+ main: 'Conversa principal',
421
+ independent: 'Ramo independente',
422
+ draft: 'Rascunho',
423
+ sending: 'Criando',
424
+ running: 'Em execução',
425
+ ready: 'Criado',
426
+ failed: 'Falhou',
427
+ send: 'Enviar',
428
+ sendingAction: 'Enviando…',
429
+ quote: 'Citado',
430
+ defaultQuestion: 'Explique este conteúdo com mais detalhes.',
431
+ empty: 'Adicione sua pergunta…',
432
+ sendHint: 'Enter para enviar · Shift + Enter para nova linha',
433
+ collapse: 'Recolher',
434
+ input: 'Entrada',
435
+ deleteDraft: 'Excluir rascunho',
436
+ close: 'Fechar',
437
+ expand: 'Ampliar',
438
+ restore: 'Restaurar painel',
439
+ back: 'Voltar',
440
+ forward: 'Avançar',
441
+ stageHint: 'Voltar / Avançar apenas altera o ramo exibido; não altera o contexto do modelo.',
442
+ path: 'Caminho',
443
+ all: 'Tudo',
444
+ tree: 'Árvore de ramos',
445
+ current: 'Atual',
446
+ dock: 'Ramos recolhidos',
447
+ open: 'Abrir',
448
+ retry: 'Tentar novamente',
449
+ error: 'Falha ao enviar',
450
+ forkError: 'Não foi possível criar o ramo',
451
+ noSession: 'Sessão indisponível',
452
+ tooLong: 'A seleção é muito longa',
453
+ content: 'Texto',
454
+ quoteLabel: 'Citado de',
455
+ showQuote: 'Mostrar citação',
456
+ hideQuote: 'Ocultar citação',
457
+ switchBranch: 'Mudar para este ramo',
458
+ stages: 'nós',
459
+ collapseDock: 'Recolher lista',
460
+ expandDock: 'Expandir lista',
461
+ sidebarGroup: 'Ramos de acompanhamento',
462
+ context: 'Contexto',
463
+ contextStart: 'Raiz do contexto',
464
+ waiting: 'Aguardando pergunta',
465
+ treeNote: 'Contexto atual do modelo: {context}. Os ramos irmãos não estão incluídos.',
466
+ anchorMissing: 'O texto original de #{n} não está visível; pode estar recolhido ou ainda não carregado',
467
+ draftWait: 'Faça sua pergunta na janela de entrada abaixo',
468
+ }, }
131
469
 
132
470
  let app = null
133
471
 
@@ -342,21 +680,21 @@ window.__ModuleLoader__.load({
342
680
  }
343
681
 
344
682
  function pathLabel(state, branch) {
345
- return `${DICT.zh.main}${pathOf(state, branch).map(item => ` › #${item.number}`).join('')}`
683
+ return `${app.t('main')}${pathOf(state, branch).map(item => ` › #${item.number}`).join('')}`
346
684
  }
347
685
 
348
686
  function branchBasis(state, branch) {
349
- if (!branch.parentBranchId) return `${DICT.zh.basedOn} ${DICT.zh.main}`
687
+ if (!branch.parentBranchId) return `${app.t('basedOn')} ${app.t('main')}`
350
688
  const parent = state.branches[branch.parentBranchId]
351
- return parent ? `${DICT.zh.basedOn} #${parent.number}` : `${DICT.zh.basedOn} ${DICT.zh.main}`
689
+ return parent ? `${app.t('basedOn')} #${parent.number}` : `${app.t('basedOn')} ${app.t('main')}`
352
690
  }
353
691
 
354
692
  function statusLabel(branch) {
355
- if (branch.status === 'sending') return DICT.zh.sending
356
- if (branch.status === 'running') return DICT.zh.running
357
- if (branch.status === 'error') return DICT.zh.failed
358
- if (branch.prompted) return DICT.zh.ready
359
- return DICT.zh.draft
693
+ if (branch.status === 'sending') return app.t('sending')
694
+ if (branch.status === 'running') return app.t('running')
695
+ if (branch.status === 'error') return app.t('failed')
696
+ if (branch.prompted) return app.t('ready')
697
+ return app.t('draft')
360
698
  }
361
699
 
362
700
  function colorVars(color) {
@@ -422,7 +760,15 @@ window.__ModuleLoader__.load({
422
760
  refs,
423
761
  chats,
424
762
  get main() { return main },
425
- t(key) { return (ctx.locale && ctx.locale.bind(NS)(key)) || DICT.zh[key] || key },
763
+ t(key, params) {
764
+ if (ctx.locale) {
765
+ const value = ctx.locale.bind(NS)(key, params)
766
+ if (value && value !== key) return value
767
+ }
768
+ const template = DICT.en[key] ?? key
769
+ if (!params) return template
770
+ return template.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match)
771
+ },
426
772
  subscribe(listener) {
427
773
  listeners.add(listener)
428
774
  return () => { listeners.delete(listener) }
@@ -902,7 +1248,7 @@ window.__ModuleLoader__.load({
902
1248
  const quote = ranges.map(part => part.quote).join('\n').trim()
903
1249
  if (!quote) { clearChip(); return }
904
1250
  if (quote.length > MAX_QUOTE) {
905
- notify(DICT.zh.tooLong)
1251
+ notify(app.t('tooLong'))
906
1252
  clearChip()
907
1253
  return
908
1254
  }
@@ -1102,7 +1448,7 @@ window.__ModuleLoader__.load({
1102
1448
 
1103
1449
  function serialize(quote, question) {
1104
1450
  const body = String(quote || '').trim().split(/\r?\n/).map(line => `> ${line}`).join('\n')
1105
- const text = String(question || '').trim() || DICT.zh.defaultQuestion
1451
+ const text = String(question || '').trim() || app.t('defaultQuestion')
1106
1452
  return `${body}\n\n${text}`
1107
1453
  }
1108
1454
 
@@ -1160,7 +1506,7 @@ window.__ModuleLoader__.load({
1160
1506
  await ensureReference(branch)
1161
1507
  }
1162
1508
  const reference = refs.get(branch.id)
1163
- if (!reference) throw new Error(DICT.zh.noSession)
1509
+ if (!reference) throw new Error(app.t('noSession'))
1164
1510
  const text = serialize(branch.anchor.quote, branch.question)
1165
1511
  const echo = reference.binding.session.beginSubmission({
1166
1512
  mode: 'queue',
@@ -1175,7 +1521,7 @@ window.__ModuleLoader__.load({
1175
1521
  )
1176
1522
  if (!result.ok) {
1177
1523
  branch.status = 'error'
1178
- branch.error = result.error?.message || DICT.zh.error
1524
+ branch.error = result.error?.message || app.t('error')
1179
1525
  emit()
1180
1526
  return false
1181
1527
  }
@@ -1204,7 +1550,7 @@ window.__ModuleLoader__.load({
1204
1550
  try {
1205
1551
  await ensureReference(branch)
1206
1552
  const reference = refs.get(branch.id)
1207
- if (!reference) throw new Error(DICT.zh.noSession)
1553
+ if (!reference) throw new Error(app.t('noSession'))
1208
1554
  const echo = reference.binding.session.beginSubmission({
1209
1555
  mode: 'queue',
1210
1556
  text,
@@ -1218,7 +1564,7 @@ window.__ModuleLoader__.load({
1218
1564
  )
1219
1565
  if (!result.ok) {
1220
1566
  branch.status = 'error'
1221
- branch.error = result.error?.message || DICT.zh.error
1567
+ branch.error = result.error?.message || app.t('error')
1222
1568
  emit()
1223
1569
  return false
1224
1570
  }
@@ -1310,7 +1656,7 @@ window.__ModuleLoader__.load({
1310
1656
  if (other !== header) other.remove()
1311
1657
  }
1312
1658
  if (header.nextSibling !== first) firstParent.insertBefore(header, first)
1313
- const label = `${state.sidebarCollapsed ? '▸' : '▾'} 追问分支 · ${rows.length}`
1659
+ const label = `${state.sidebarCollapsed ? '▸' : '▾'} ${app.t('sidebarGroup')} · ${rows.length}`
1314
1660
  if (header.textContent !== label) header.textContent = label
1315
1661
  for (const row of rows) row.classList.toggle('ifu-sidebar-hidden', state.sidebarCollapsed)
1316
1662
 
@@ -1424,7 +1770,7 @@ window.__ModuleLoader__.load({
1424
1770
  // giving up on this attempt.
1425
1771
  if (navigator && attempt === 0) navigator.click()
1426
1772
  if (attempt < 14) setTimeout(() => scrollBranchIntoView(branch, attempt + 1), 100)
1427
- else notify(`#${branch.number} 的原文当前不可见,可能已被折叠或尚未加载`)
1773
+ else notify(app.t('anchorMissing', { n: branch.number }))
1428
1774
  return
1429
1775
  }
1430
1776
  if (navigator && attempt === 0) navigator.click()
@@ -1852,14 +2198,14 @@ window.__ModuleLoader__.load({
1852
2198
  }
1853
2199
  return h('div', { className: 'ifu-compose' },
1854
2200
  h('div', { className: 'ifu-quote', title: branch.anchor.quote },
1855
- h('div', { className: 'ifu-quote-label' }, `${DICT.zh.quote} · ${DICT.zh.basedOn} ${branch.parentBranchId ? `#${app.state.branches[branch.parentBranchId]?.number ?? ''}` : DICT.zh.main}`),
2201
+ h('div', { className: 'ifu-quote-label' }, `${app.t('quote')} · ${app.t('basedOn')} ${branch.parentBranchId ? `#${app.state.branches[branch.parentBranchId]?.number ?? ''}` : app.t('main')}`),
1856
2202
  h('div', { className: 'ifu-quote-text' }, branch.anchor.quote.length > 220 ? `${branch.anchor.quote.slice(0, 219)}…` : branch.anchor.quote),
1857
2203
  ),
1858
2204
  h('textarea', {
1859
2205
  ref: areaRef,
1860
2206
  className: 'ifu-textarea',
1861
2207
  value: text,
1862
- placeholder: DICT.zh.empty,
2208
+ placeholder: app.t('empty'),
1863
2209
  rows: 3,
1864
2210
  onChange: event => { setText(event.target.value); app.setDraftText(branch.id, event.target.value) },
1865
2211
  onKeyDown: event => {
@@ -1871,14 +2217,14 @@ window.__ModuleLoader__.load({
1871
2217
  }),
1872
2218
  branch.error ? h('div', { className: 'ifu-error', role: 'alert' }, branch.error) : null,
1873
2219
  h('div', { className: 'ifu-compose-foot' },
1874
- h('span', { className: 'ifu-hint' }, DICT.zh.sendHint),
2220
+ h('span', { className: 'ifu-hint' }, app.t('sendHint')),
1875
2221
  h('div', { className: 'ifu-grow' }),
1876
2222
  h('button', {
1877
2223
  type: 'button',
1878
2224
  className: 'ifu-send',
1879
2225
  disabled: sending,
1880
2226
  onClick: submit,
1881
- }, sending ? DICT.zh.sendingAction : (branch.error ? DICT.zh.retry : DICT.zh.send)),
2227
+ }, sending ? app.t('sendingAction') : (branch.error ? app.t('retry') : app.t('send'))),
1882
2228
  ),
1883
2229
  )
1884
2230
  }
@@ -1887,7 +2233,7 @@ window.__ModuleLoader__.load({
1887
2233
  const { branch, SessionProvider, renderSlot } = props
1888
2234
  const reference = app.refs.get(branch.id)
1889
2235
  if (!reference) {
1890
- return h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending)
2236
+ return h('div', { className: 'ifu-loading' }, branch.error || app.t('sending'))
1891
2237
  }
1892
2238
  return h(SessionProvider, { session: reference },
1893
2239
  h('div', {
@@ -1935,18 +2281,18 @@ window.__ModuleLoader__.load({
1935
2281
  ...colorVars(branch.color),
1936
2282
  },
1937
2283
  role: 'dialog',
1938
- 'aria-label': `${DICT.zh.branchN} #${branch.number} ${DICT.zh.input}`,
2284
+ 'aria-label': `${app.t('branchN')} #${branch.number} ${app.t('input')}`,
1939
2285
  },
1940
2286
  h('header', { className: 'ifu-head', onPointerDown: event => app.startPanelDrag(branch.id, event, 'input') },
1941
2287
  h('span', { className: 'ifu-dot' }),
1942
2288
  h('div', { className: 'ifu-title-wrap' },
1943
- h('div', { className: 'ifu-title' }, `${DICT.zh.input} · #${branch.number}`),
2289
+ h('div', { className: 'ifu-title' }, `${app.t('input')} · #${branch.number}`),
1944
2290
  h('div', { className: 'ifu-sub' }, statusLabel(branch)),
1945
2291
  ),
1946
2292
  h('div', { className: 'ifu-grow' }),
1947
- h(IconButton, { title: DICT.zh.collapse, onClick: () => app.collapse(branch.id) }, '—'),
2293
+ h(IconButton, { title: app.t('collapse'), onClick: () => app.collapse(branch.id) }, '—'),
1948
2294
  branch.sessionId ? null : h(IconButton, {
1949
- title: DICT.zh.deleteDraft,
2295
+ title: app.t('deleteDraft'),
1950
2296
  className: 'ifu-danger',
1951
2297
  onClick: () => app.deleteDraft(branch.id),
1952
2298
  }, '⌫'),
@@ -1956,7 +2302,7 @@ window.__ModuleLoader__.load({
1956
2302
  ref: areaRef,
1957
2303
  className: 'ifu-textarea',
1958
2304
  value: text,
1959
- placeholder: DICT.zh.empty,
2305
+ placeholder: app.t('empty'),
1960
2306
  rows: 3,
1961
2307
  onChange: event => { setText(event.target.value); app.setInputText(branch.id, event.target.value) },
1962
2308
  onKeyDown: event => {
@@ -1968,14 +2314,14 @@ window.__ModuleLoader__.load({
1968
2314
  }),
1969
2315
  branch.error ? h('div', { className: 'ifu-error', role: 'alert' }, branch.error) : null,
1970
2316
  h('div', { className: 'ifu-compose-foot' },
1971
- h('span', { className: 'ifu-hint' }, DICT.zh.sendHint),
2317
+ h('span', { className: 'ifu-hint' }, app.t('sendHint')),
1972
2318
  h('div', { className: 'ifu-grow' }),
1973
2319
  h('button', {
1974
2320
  type: 'button',
1975
2321
  className: 'ifu-send',
1976
2322
  disabled: sending,
1977
2323
  onClick: submit,
1978
- }, sending ? DICT.zh.sendingAction : DICT.zh.send),
2324
+ }, sending ? app.t('sendingAction') : app.t('send')),
1979
2325
  ),
1980
2326
  ),
1981
2327
  ...['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'].map(direction => h('div', {
@@ -1991,7 +2337,7 @@ window.__ModuleLoader__.load({
1991
2337
  useVersion()
1992
2338
  if (!branch.prompted) return h(InputWindow, { key: `${branch.id}-input`, branch })
1993
2339
  const parent = branch.parentBranchId ? app.state.branches[branch.parentBranchId] : null
1994
- const basis = parent ? `${DICT.zh.basedOn} #${parent.number}` : `${DICT.zh.basedOn} ${DICT.zh.main}`
2340
+ const basis = parent ? `${app.t('basedOn')} #${parent.number}` : `${app.t('basedOn')} ${app.t('main')}`
1995
2341
  const reference = app.refs.get(branch.id)
1996
2342
  const showThread = branch.prompted && reference && !branch.loading
1997
2343
  const showDraft = !branch.prompted
@@ -2012,20 +2358,20 @@ window.__ModuleLoader__.load({
2012
2358
  ...colorVars(branch.color),
2013
2359
  },
2014
2360
  role: 'dialog',
2015
- 'aria-label': `${DICT.zh.branchN} #${branch.number}`,
2361
+ 'aria-label': `${app.t('branchN')} #${branch.number}`,
2016
2362
  },
2017
2363
  h('header', { className: 'ifu-head', onPointerDown: event => app.startPanelDrag(branch.id, event) },
2018
2364
  h('span', { className: 'ifu-dot' }),
2019
2365
  h('div', { className: 'ifu-title-wrap' },
2020
- h('div', { className: 'ifu-title' }, `${DICT.zh.branchN} #${branch.number}`),
2366
+ h('div', { className: 'ifu-title' }, `${app.t('branchN')} #${branch.number}`),
2021
2367
  h('div', { className: 'ifu-sub' }, `${basis} · ${statusLabel(branch)}`),
2022
2368
  ),
2023
2369
  h('div', { className: 'ifu-grow' }),
2024
- h(IconButton, { title: DICT.zh.expand, onClick: () => app.promote(branch.id), disabled: !branch.prompted || !branch.sessionId }, '↗'),
2025
- h(IconButton, { title: DICT.zh.collapse, onClick: () => app.collapse(branch.id) }, '—'),
2370
+ h(IconButton, { title: app.t('expand'), onClick: () => app.promote(branch.id), disabled: !branch.prompted || !branch.sessionId }, '↗'),
2371
+ h(IconButton, { title: app.t('collapse'), onClick: () => app.collapse(branch.id) }, '—'),
2026
2372
  branch.sessionId || branch.prompted
2027
- ? h(IconButton, { title: DICT.zh.close, onClick: () => app.collapse(branch.id) }, '✕')
2028
- : h(IconButton, { title: DICT.zh.deleteDraft, className: 'ifu-danger', onClick: () => app.deleteDraft(branch.id) }, '⌫'),
2373
+ ? h(IconButton, { title: app.t('close'), onClick: () => app.collapse(branch.id) }, '✕')
2374
+ : h(IconButton, { title: app.t('deleteDraft'), className: 'ifu-danger', onClick: () => app.deleteDraft(branch.id) }, '⌫'),
2029
2375
  ),
2030
2376
  h('div', { className: 'ifu-panel-body' },
2031
2377
  showThread
@@ -2033,12 +2379,12 @@ window.__ModuleLoader__.load({
2033
2379
  : showDraft
2034
2380
  ? h('div', { className: 'ifu-compose-center' },
2035
2381
  h('div', { className: 'ifu-draft-quote' },
2036
- h('div', { className: 'ifu-quote-label' }, `${DICT.zh.quote} · ${basis}`),
2382
+ h('div', { className: 'ifu-quote-label' }, `${app.t('quote')} · ${basis}`),
2037
2383
  h('div', { className: 'ifu-quote-text' }, branch.anchor.quote),
2038
- h('div', { className: 'ifu-draft-wait' }, '在下方输入浮窗中提问'),
2384
+ h('div', { className: 'ifu-draft-wait' }, app.t('draftWait')),
2039
2385
  ),
2040
2386
  )
2041
- : h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending),
2387
+ : h('div', { className: 'ifu-loading' }, branch.error || app.t('sending')),
2042
2388
  ),
2043
2389
  ...['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'].map(direction => h('div', {
2044
2390
  key: direction,
@@ -2053,11 +2399,11 @@ window.__ModuleLoader__.load({
2053
2399
  function StageQuote(props) {
2054
2400
  const { branch } = props
2055
2401
  const parent = branch.parentBranchId ? app.state.branches[branch.parentBranchId] : null
2056
- const basis = parent ? `#${parent.number}` : DICT.zh.main
2402
+ const basis = parent ? `#${parent.number}` : app.t('main')
2057
2403
  return h('div', { className: 'ifu-stage-quote', title: branch.anchor.quote },
2058
2404
  h('div', { className: 'ifu-stage-quote-label' },
2059
2405
  h('span', { className: 'ifu-dot' }),
2060
- `${DICT.zh.quote} · ${DICT.zh.basedOn} ${basis}`,
2406
+ `${app.t('quote')} · ${app.t('basedOn')} ${basis}`,
2061
2407
  ),
2062
2408
  h('div', { className: 'ifu-stage-quote-text' }, branch.anchor.quote),
2063
2409
  )
@@ -2079,7 +2425,7 @@ window.__ModuleLoader__.load({
2079
2425
  observer.observe(host, { childList: true, subtree: true, characterData: true })
2080
2426
  return () => observer.disconnect()
2081
2427
  }, [branch.id])
2082
- if (!reference) return h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending)
2428
+ if (!reference) return h('div', { className: 'ifu-loading' }, branch.error || app.t('sending'))
2083
2429
  return h(SessionProvider, { session: reference },
2084
2430
  h('div', {
2085
2431
  ref: hostRef,
@@ -2106,24 +2452,24 @@ window.__ModuleLoader__.load({
2106
2452
  ? rows.filter(row => pathIds.has(row.branch.id))
2107
2453
  : rows
2108
2454
  const contextText = path.length > 0
2109
- ? `${DICT.zh.main}${path.map(branch => ` → #${branch.number}`).join('')}`
2110
- : DICT.zh.main
2455
+ ? `${app.t('main')}${path.map(branch => ` → #${branch.number}`).join('')}`
2456
+ : app.t('main')
2111
2457
  return h('aside', { className: 'ifu-tree', 'data-ifu-no-select': '' },
2112
2458
  h('div', { className: 'ifu-tree-head' },
2113
- h('span', null, DICT.zh.tree),
2459
+ h('span', null, app.t('tree')),
2114
2460
  h('div', { className: 'ifu-tree-tabs' },
2115
2461
  h('button', {
2116
2462
  type: 'button',
2117
2463
  className: `ifu-tree-tab ${scope === 'path' ? 'is-active' : ''}`,
2118
2464
  onClick: () => setScope('path'),
2119
- }, '路径'),
2465
+ }, app.t('path')),
2120
2466
  h('button', {
2121
2467
  type: 'button',
2122
2468
  className: `ifu-tree-tab ${scope === 'all' ? 'is-active' : ''}`,
2123
2469
  onClick: () => setScope('all'),
2124
- }, '全部'),
2470
+ }, app.t('all')),
2125
2471
  ),
2126
- h('button', { type: 'button', className: 'ifu-icon', title: DICT.zh.close, onClick: () => app.toggleTree() }, '✕'),
2472
+ h('button', { type: 'button', className: 'ifu-icon', title: app.t('close'), onClick: () => app.toggleTree() }, '✕'),
2127
2473
  ),
2128
2474
  h('div', { className: 'ifu-tree-list' },
2129
2475
  h('div', {
@@ -2138,15 +2484,15 @@ window.__ModuleLoader__.load({
2138
2484
  },
2139
2485
  },
2140
2486
  h('span', { className: 'ifu-dot' }),
2141
- h('span', { className: 'ifu-tree-root-name' }, DICT.zh.main),
2487
+ h('span', { className: 'ifu-tree-root-name' }, app.t('main')),
2142
2488
  h('span', { className: 'ifu-tree-root-tag' }, 'ROOT'),
2143
- h('span', { className: 'ifu-tree-root-meta' }, '上下文起点'),
2489
+ h('span', { className: 'ifu-tree-root-meta' }, app.t('contextStart')),
2144
2490
  ),
2145
2491
  visibleRows.map(({ branch, depth }) => {
2146
2492
  const isDraft = !branch.sessionId && !branch.prompted
2147
2493
  const parent = branch.parentBranchId ? state.branches[branch.parentBranchId] : null
2148
- const parentLabel = parent ? `${DICT.zh.main} · #${parent.number}` : DICT.zh.main
2149
- const title = branch.question || branch.anchor.quote || '待提问'
2494
+ const parentLabel = parent ? `${app.t('main')} · #${parent.number}` : app.t('main')
2495
+ const title = branch.question || branch.anchor.quote || app.t('waiting')
2150
2496
  const inPath = pathIds.has(branch.id)
2151
2497
  return h('div', {
2152
2498
  key: branch.id,
@@ -2172,34 +2518,34 @@ window.__ModuleLoader__.load({
2172
2518
  } else if (branch.prompted && branch.sessionId) app.promote(branch.id)
2173
2519
  else app.openPanel(branch.id)
2174
2520
  },
2175
- title: `${DICT.zh.switchBranch} #${branch.number}`,
2521
+ title: `${app.t('switchBranch')} #${branch.number}`,
2176
2522
  },
2177
2523
  h('div', { className: 'ifu-tree-top' },
2178
2524
  h('span', { className: 'ifu-dot' }),
2179
2525
  h('span', { className: 'ifu-tree-num' }, `#${branch.number}`),
2180
2526
  h('span', { className: 'ifu-tree-title' }, snippet(title, 36)),
2181
2527
  branch.id === activeId
2182
- ? h('span', { className: 'ifu-tree-tag' }, DICT.zh.current)
2528
+ ? h('span', { className: 'ifu-tree-tag' }, app.t('current'))
2183
2529
  : inPath
2184
- ? h('span', { className: 'ifu-tree-tag is-context' }, '上下文')
2530
+ ? h('span', { className: 'ifu-tree-tag is-context' }, app.t('context'))
2185
2531
  : null,
2186
2532
  isDraft ? h('button', {
2187
2533
  type: 'button',
2188
2534
  className: 'ifu-tree-delete',
2189
- title: DICT.zh.deleteDraft,
2535
+ title: app.t('deleteDraft'),
2190
2536
  onClick: (event) => { event.stopPropagation(); app.deleteDraft(branch.id) },
2191
2537
  }, '⌫') : null,
2192
2538
  ),
2193
2539
  h('div', { className: 'ifu-tree-quote' }, snippet(branch.anchor.quote, 72)),
2194
2540
  h('div', { className: 'ifu-tree-meta' },
2195
- h('span', null, `${DICT.zh.basedOn} ${parentLabel}`),
2541
+ h('span', null, `${app.t('basedOn')} ${parentLabel}`),
2196
2542
  h('span', null, statusLabel(branch)),
2197
2543
  ),
2198
2544
  )
2199
2545
  }),
2200
2546
  ),
2201
2547
  h('div', { className: 'ifu-tree-note' },
2202
- `当前模型上下文:${contextText}。兄弟分支不在其中。`,
2548
+ app.t('treeNote', { context: contextText }),
2203
2549
  ),
2204
2550
  )
2205
2551
  }
@@ -2223,23 +2569,23 @@ window.__ModuleLoader__.load({
2223
2569
  type: 'button',
2224
2570
  className: 'ifu-stage-back',
2225
2571
  onClick: () => index <= 0 ? app.stageClose() : app.stageGo(-1),
2226
- title: index <= 0 ? DICT.zh.main : DICT.zh.back,
2227
- }, `← ${DICT.zh.back}`),
2572
+ title: index <= 0 ? app.t('main') : app.t('back'),
2573
+ }, `← ${app.t('back')}`),
2228
2574
  h('span', { className: 'ifu-stage-count' }, `${index + 1}/${total}`),
2229
2575
  h('button', {
2230
2576
  type: 'button',
2231
2577
  className: 'ifu-stage-back',
2232
2578
  disabled: index >= total - 1,
2233
2579
  onClick: () => app.stageGo(1),
2234
- }, DICT.zh.forward, ' →'),
2580
+ }, app.t('forward'), ' →'),
2235
2581
  h('span', { className: 'ifu-stage-divider' }),
2236
2582
  h('div', { className: 'ifu-crumbs' },
2237
2583
  h('button', {
2238
2584
  type: 'button',
2239
2585
  className: 'ifu-crumb-btn',
2240
2586
  onClick: () => app.stageClose(),
2241
- title: DICT.zh.main,
2242
- }, DICT.zh.main),
2587
+ title: app.t('main'),
2588
+ }, app.t('main')),
2243
2589
  ...path.flatMap(item => [
2244
2590
  h('span', { key: `sep-${item.id}`, className: 'ifu-crumb-sep' }, '›'),
2245
2591
  h('button', {
@@ -2250,23 +2596,23 @@ window.__ModuleLoader__.load({
2250
2596
  if (item.id === branch.id) return
2251
2597
  app.promote(item.id)
2252
2598
  },
2253
- title: `${DICT.zh.switchBranch} #${item.number}`,
2599
+ title: `${app.t('switchBranch')} #${item.number}`,
2254
2600
  }, `#${item.number}`),
2255
2601
  ]),
2256
2602
  ),
2257
- h('span', { className: 'ifu-context-chip' }, `上下文 · ${path.length} ${DICT.zh.stages}`),
2603
+ h('span', { className: 'ifu-context-chip' }, `${app.t('context')} · ${path.length} ${app.t('stages')}`),
2258
2604
  h('div', { className: 'ifu-grow' }),
2259
2605
  h('span', { className: 'ifu-stage-status' }, statusLabel(branch)),
2260
- h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.toggleTree() }, '▤ ', DICT.zh.tree),
2261
- h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageRestore() }, '↘ ', DICT.zh.restore),
2262
- h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageClose() }, '✕ ', DICT.zh.close),
2606
+ h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.toggleTree() }, '▤ ', app.t('tree')),
2607
+ h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageRestore() }, '↘ ', app.t('restore')),
2608
+ h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageClose() }, '✕ ', app.t('close')),
2263
2609
  ),
2264
- h('div', { className: 'ifu-stage-tip' }, DICT.zh.stageHint),
2610
+ h('div', { className: 'ifu-stage-tip' }, app.t('stageHint')),
2265
2611
  h('div', { className: 'ifu-stage-body' },
2266
2612
  h(StageQuote, { branch }),
2267
2613
  reference && !branch.loading
2268
2614
  ? h(StageThread, { key: branch.id, branch, SessionProvider, renderSlot })
2269
- : h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending),
2615
+ : h('div', { className: 'ifu-loading' }, branch.error || app.t('sending')),
2270
2616
  ),
2271
2617
  h(BranchTree),
2272
2618
  )
@@ -2403,7 +2749,7 @@ window.__ModuleLoader__.load({
2403
2749
  className: 'ifu-dock-collapsed',
2404
2750
  'data-ifu-dock': '',
2405
2751
  onClick: () => app.toggleDock(),
2406
- }, `◉ ${DICT.zh.dock} · ${branches.length}`)
2752
+ }, `◉ ${app.t('dock')} · ${branches.length}`)
2407
2753
  }
2408
2754
  const dockStyle = state.dock
2409
2755
  ? {
@@ -2417,11 +2763,11 @@ window.__ModuleLoader__.load({
2417
2763
  : {}
2418
2764
  return h('aside', { className: 'ifu-dock', 'data-ifu-dock': '', style: dockStyle },
2419
2765
  h('div', { className: 'ifu-dock-head', onPointerDown: event => app.startDockDrag(event) },
2420
- h('span', null, `${DICT.zh.dock} · ${branches.length}`),
2766
+ h('span', null, `${app.t('dock')} · ${branches.length}`),
2421
2767
  h('button', {
2422
2768
  type: 'button',
2423
2769
  className: 'ifu-dock-toggle',
2424
- title: DICT.zh.collapseDock,
2770
+ title: app.t('collapseDock'),
2425
2771
  onClick: () => app.closeDock(),
2426
2772
  }, '—'),
2427
2773
  ),
@@ -2440,7 +2786,7 @@ window.__ModuleLoader__.load({
2440
2786
  event.preventDefault()
2441
2787
  app.revealBranch(branch.id)
2442
2788
  },
2443
- title: `${DICT.zh.switchBranch}: #${branch.number}`,
2789
+ title: `${app.t('switchBranch')}: #${branch.number}`,
2444
2790
  },
2445
2791
  h('span', { className: 'ifu-dot', style: colorVars(branch.color) }),
2446
2792
  h('span', { className: 'ifu-dock-num' }, `#${branch.number}`),
@@ -2449,7 +2795,7 @@ window.__ModuleLoader__.load({
2449
2795
  isDraft ? h('button', {
2450
2796
  type: 'button',
2451
2797
  className: 'ifu-dock-delete',
2452
- title: DICT.zh.deleteDraft,
2798
+ title: app.t('deleteDraft'),
2453
2799
  onClick: (event) => { event.stopPropagation(); app.deleteDraft(branch.id) },
2454
2800
  }, '⌫') : null,
2455
2801
  )
@@ -2471,14 +2817,14 @@ window.__ModuleLoader__.load({
2471
2817
  className: 'ifu-chip',
2472
2818
  'data-ifu-chip': '',
2473
2819
  style: { left: `${chip.x}px`, top: `${chip.y}px` },
2474
- title: DICT.zh.chipHint,
2820
+ title: app.t('chipHint'),
2475
2821
  onMouseDown: event => event.preventDefault(),
2476
2822
  onClick: () => {
2477
2823
  const x = clamp(chip.x - 120, 12, window.innerWidth - PANEL_WIDTH - 12)
2478
2824
  const y = clamp(chip.y + 38, 12, window.innerHeight - 320)
2479
2825
  app.openDraft(chip.anchor, x, y)
2480
2826
  },
2481
- }, '✦ ', DICT.zh.chip)
2827
+ }, '✦ ', app.t('chip'))
2482
2828
  }
2483
2829
 
2484
2830
  function Notice() {
@@ -2706,7 +3052,8 @@ window.__ModuleLoader__.load({
2706
3052
  apply(ctx) {
2707
3053
  app = createApp(ctx)
2708
3054
  app.load()
2709
- ctx.effect(() => ctx.locale.register(NS, { zh: DICT.zh, en: DICT.en }), 'inline-followup: locale')
3055
+ ctx.effect(() => ctx.locale.register(NS, DICT), 'inline-followup: locale')
3056
+ ctx.effect(() => ctx.on('locale/change', () => app.emit()), 'inline-followup: locale-change')
2710
3057
  ctx.effect(() => app.install(), 'inline-followup: lifetime')
2711
3058
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
2712
3059
  name: 'shell.overlay',
package/locale/de.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Aus Text nachfragen",
4
+ "description": "Text in einer abgeschlossenen Assistant-Antwort auswählen, in einem unabhängigen Eingabefenster nachfragen und in einem echten Session-Zweig weiterarbeiten. Farbige Markierungen, Zweigbaum, Bühne und einklappbare Seitenleisten-Gruppe. Zweige sind echte Sessions; gesendete Zweige können nur archiviert werden."
5
+ }
6
+ }
package/locale/es.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Preguntar desde el texto",
4
+ "description": "Selecciona texto en una respuesta completa del assistant, pregunta en una ventana de entrada independiente y continúa en una rama de Session real. Incluye marcas de color, árbol de ramas, escenario y un grupo plegable en la barra lateral. Las ramas son Sessions reales; las ramas enviadas solo se pueden archivar."
5
+ }
6
+ }
package/locale/fr.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Demander depuis le texte",
4
+ "description": "Sélectionnez du texte dans une réponse finalisée de l’assistant, posez votre question dans une fenêtre de saisie indépendante et continuez dans une branche de Session réelle. Marques colorées, arbre des branches, scène et groupe repliable dans la barre latérale. Les branches sont de vraies Sessions ; les branches envoyées ne peuvent être qu’archivées."
5
+ }
6
+ }
package/locale/ja.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "テキストから質問",
4
+ "description": "完了した assistant の回答内のテキストを選択し、独立した入力ウィンドウで質問して、実際の Session ブランチとして続けられます。色付きマーク、ブランチツリー、ステージ表示、折りたたみ可能な左サイドバーのグループに対応。ブランチは実際の Session で、送信済みのブランチはアーカイブのみ可能です。"
5
+ }
6
+ }
package/locale/pt.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Perguntar a partir do texto",
4
+ "description": "Selecione texto em uma resposta concluída do assistant, pergunte em uma janela de entrada independente e continue em um ramo de Session real. Marcas coloridas, árvore de ramos, palco e grupo recolhível na barra lateral. Os ramos são Sessions reais; ramos enviados só podem ser arquivados."
5
+ }
6
+ }
package/locale/ru.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Спросить по тексту",
4
+ "description": "Выделите текст в завершённом ответе assistant, задайте вопрос в отдельном окне ввода и продолжите в настоящей ветке Session. Цветные метки, дерево веток, сцена и сворачиваемая группа в боковой панели. Ветки — это реальные Sessions; отправленные ветки можно только архивировать."
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ask-from-text",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Ask a follow-up from selected text in a DeepSeek Harness answer, in a real Session branch with a tree of context.",
6
6
  "exports": {