zigmini 0.9.0-beta.3 → 1.0.0-beta.2

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.
Files changed (143) hide show
  1. package/README.ko.md +36 -4
  2. package/README.md +36 -4
  3. package/assets/agent/gateway-rules.md +29 -19
  4. package/assets/android/zigmini.apk +0 -0
  5. package/assets/android/zigmini.apk.json +3 -3
  6. package/assets/build.json +1 -1
  7. package/docs/manual.ko.md +60 -11
  8. package/docs/manual.md +61 -12
  9. package/host/omo-bridge/agent-control.ts +274 -0
  10. package/host/omo-bridge/bridge.ts +1 -1
  11. package/host/omo-bridge/continuity-control.ts +1 -1
  12. package/host/omo-bridge/task-control.ts +26 -16
  13. package/host/omo-check/patches/omo-5.1.51-compaction-long-context.patch +92 -0
  14. package/host/omo-check/patches/omo-5.1.51-compaction-port.patch +2635 -0
  15. package/host/omo-check/patches/omo-5.1.51-compaction-recovery.patch +911 -0
  16. package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +363 -0
  17. package/host/omo-check/patches/omo-5.1.51-provider-failover.patch +50 -0
  18. package/host/omo-check/patches/omo-5.1.51-provider-port.patch +441 -0
  19. package/host/omo-check/patches/omo-5.1.51-rpc-port.patch +799 -0
  20. package/host/omo-check/patches/omo-5.1.51-runtime-compat.patch +31 -0
  21. package/package.json +3 -2
  22. package/src/agent/api.ts +215 -0
  23. package/src/agent/definition.ts +177 -0
  24. package/src/agent/fields.ts +42 -0
  25. package/src/agent/files.ts +192 -0
  26. package/src/agent/legacy-files.ts +46 -0
  27. package/src/agent/list.ts +21 -0
  28. package/src/agent-rules.ts +97 -13
  29. package/src/approvals.ts +6 -1
  30. package/src/cli/doctor.ts +7 -0
  31. package/src/cli/main.ts +38 -0
  32. package/src/cli/messages.ts +10 -2
  33. package/src/cli/rotate-main.ts +2 -1
  34. package/src/cli/rules.ts +70 -0
  35. package/src/cli/scan.ts +414 -0
  36. package/src/cli/security-import.ts +155 -0
  37. package/src/compact.ts +22 -0
  38. package/src/decisions.ts +6 -4
  39. package/src/engine/compat.json +3 -3
  40. package/src/engine/index.ts +1 -0
  41. package/src/engine/schema-guard.ts +12 -4
  42. package/src/engine/sdk.ts +8 -0
  43. package/src/formal-verification.ts +366 -0
  44. package/src/gc.ts +37 -6
  45. package/src/hub.ts +339 -79
  46. package/src/main.ts +95 -9
  47. package/src/maintenance.ts +1 -1
  48. package/src/manual.ts +9 -2
  49. package/src/profile-prompt.ts +73 -0
  50. package/src/profile.ts +33 -10
  51. package/src/protocol.ts +11 -2
  52. package/src/question-notifications.ts +4 -1
  53. package/src/reply-notifications.ts +7 -1
  54. package/src/repo-security-setting.ts +33 -0
  55. package/src/runtime/paths.ts +16 -0
  56. package/src/security/baseline.ts +172 -0
  57. package/src/security/dex-strings.ts +242 -0
  58. package/src/security/repo-scan.ts +369 -0
  59. package/src/security/revision-probe.ts +144 -0
  60. package/src/security/terms.ts +97 -0
  61. package/src/security-allow.ts +1 -0
  62. package/src/security-baseline.ts +113 -0
  63. package/src/security-check.ts +91 -16
  64. package/src/server-text.ts +38 -0
  65. package/src/store.ts +142 -16
  66. package/src/transcript.ts +7 -1
  67. package/src/ui-language.ts +33 -0
  68. package/src/update/check.ts +7 -3
  69. package/src/update/handoff-system.ts +24 -16
  70. package/src/update/handoff.ts +23 -12
  71. package/src/update/helper.ts +72 -36
  72. package/src/update/request.ts +46 -30
  73. package/src/update/state.ts +21 -11
  74. package/src/update/strategy.ts +12 -5
  75. package/src/update/target.ts +32 -15
  76. package/src/usage.ts +43 -20
  77. package/src/version.ts +39 -16
  78. package/src/voice/agents.ts +8 -3
  79. package/src/voice/install.ts +29 -18
  80. package/src/work-watch.ts +40 -18
  81. package/web/agent-icon.js +15 -9
  82. package/web/agents.d.ts +201 -0
  83. package/web/agents.js +813 -0
  84. package/web/app-download.js +32 -19
  85. package/web/app.js +1407 -278
  86. package/web/autocopy.js +6 -1
  87. package/web/boot.js +27 -8
  88. package/web/commands.js +20 -11
  89. package/web/decisions.js +70 -49
  90. package/web/devlog.js +25 -19
  91. package/web/diagram.js +8 -3
  92. package/web/enroll.js +43 -22
  93. package/web/gc.js +32 -19
  94. package/web/i18n/agent-icon.js +24 -0
  95. package/web/i18n/agents.js +48 -0
  96. package/web/i18n/app-download.js +26 -0
  97. package/web/i18n/autocopy.js +5 -0
  98. package/web/i18n/boot.js +24 -0
  99. package/web/i18n/commands.js +34 -0
  100. package/web/i18n/decisions.js +24 -0
  101. package/web/i18n/devlog.js +60 -0
  102. package/web/i18n/diagram.js +12 -0
  103. package/web/i18n/enroll.js +22 -0
  104. package/web/i18n/gc.js +38 -0
  105. package/web/i18n/lastsent.js +10 -0
  106. package/web/i18n/main.d.ts +4 -0
  107. package/web/i18n/main.js +527 -0
  108. package/web/i18n/manual.d.ts +4 -0
  109. package/web/i18n/manual.js +14 -0
  110. package/web/i18n/memo.js +30 -0
  111. package/web/i18n/notifications.js +16 -0
  112. package/web/i18n/profile.js +13 -0
  113. package/web/i18n/reference.js +26 -0
  114. package/web/i18n/security-inbox.js +40 -0
  115. package/web/i18n/slash.js +38 -0
  116. package/web/i18n/turn-stats.js +5 -0
  117. package/web/i18n/update.js +42 -0
  118. package/web/i18n/voice.js +28 -0
  119. package/web/i18n/wake.js +16 -0
  120. package/web/i18n/workid.js +10 -0
  121. package/web/i18n/workstatus.js +20 -0
  122. package/web/i18n.d.ts +20 -0
  123. package/web/i18n.js +63 -0
  124. package/web/index.html +156 -96
  125. package/web/lastsent.js +9 -4
  126. package/web/manual.d.ts +1 -1
  127. package/web/manual.js +11 -5
  128. package/web/memo.js +18 -13
  129. package/web/notifications.js +12 -7
  130. package/web/profile.js +42 -17
  131. package/web/reference.js +20 -15
  132. package/web/security-inbox.d.ts +18 -1
  133. package/web/security-inbox.js +173 -4
  134. package/web/slash.js +23 -13
  135. package/web/style.css +132 -3
  136. package/web/tooltip.js +5 -2
  137. package/web/turn-stats.js +6 -2
  138. package/web/update.js +31 -23
  139. package/web/voice.js +18 -8
  140. package/web/wake.d.ts +4 -0
  141. package/web/wake.js +7 -3
  142. package/web/workid.js +12 -2
  143. package/web/workstatus.js +36 -32
package/web/index.html CHANGED
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="ko">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -11,20 +11,23 @@
11
11
  <aside class="sidebar">
12
12
  <header class="sidebar-head">
13
13
  <span class="sidebar-title">zigmini</span>
14
- <span class="badge" id="omo-status" title="엔진 연결 상태">엔진</span>
14
+ <span class="badge" id="omo-status" data-i18n="side.engine" data-i18n-attr="title:side.engine.tip">Engine</span>
15
15
  </header>
16
16
  <div class="sidebar-section">
17
- <div class="section-label">
18
- <span>대화</span>
19
- <button class="icon-btn" id="new-conv" title="새 대화">+</button>
17
+ <ul class="conv-list main-list" id="conv-list"></ul>
18
+ <div class="agent-group" id="agent-group">
19
+ <div class="section-label">
20
+ <span data-i18n="side.agents">Agents</span>
21
+ <button type="button" class="icon-btn agent-manage" id="agent-manage" data-i18n-attr="title:side.agentManage.tip" aria-haspopup="dialog" aria-controls="agents" aria-expanded="false">⋯</button>
22
+ </div>
23
+ <ul class="conv-list agent-list" id="agent-list" data-i18n-attr="aria-label:side.agentList.label"></ul>
20
24
  </div>
21
- <ul class="conv-list" id="conv-list"></ul>
22
25
  </div>
23
26
  <section class="work-status" id="work-status" aria-labelledby="work-status-title" tabindex="-1">
24
27
  <div class="ws-head">
25
- <button type="button" class="ws-back" id="status-back" aria-label="채팅으로 돌아가기" title="채팅으로 돌아가기"><img src="zigmini-profile.svg" alt="" width="26" height="26" /></button>
26
- <span class="ws-title" id="work-status-title">작업 상태</span>
27
- <button type="button" class="ws-fold" id="work-status-fold" aria-controls="work-status-body" aria-expanded="true" aria-label="작업 상태 숨기기" title="작업 상태 숨기기"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg></button>
28
+ <button type="button" class="ws-back" id="status-back" data-i18n-attr="aria-label:status.backToChat;title:status.backToChat"><img src="zigmini-profile.svg" alt="" width="26" height="26" /></button>
29
+ <span class="ws-title" id="work-status-title" data-i18n="status.title">Work status</span>
30
+ <button type="button" class="ws-fold" id="work-status-fold" aria-controls="work-status-body" aria-expanded="true" data-i18n-attr="aria-label:status.fold;title:status.fold"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg></button>
28
31
  </div>
29
32
  <div class="ws-body" id="work-status-body">
30
33
  <div class="ws-graph" id="work-status-graph"></div>
@@ -34,193 +37,250 @@
34
37
  <div class="ws-tip" id="work-status-tip" role="tooltip" hidden></div>
35
38
  </section>
36
39
  <footer class="sidebar-foot">
37
- <div class="me" id="owner-profile" title="더블클릭해서 닉네임 바꾸기">
38
- <div class="avatar me-avatar">나</div>
40
+ <div class="me" id="owner-profile" data-i18n-attr="title:profile.editNickname.tip">
41
+ <div class="avatar me-avatar">Me</div>
39
42
  <div class="me-text">
40
- <div class="me-name">나</div>
41
- <div class="me-sub" id="conn-status">연결 중</div>
43
+ <div class="me-name">Me</div>
44
+ <div class="me-sub" id="conn-status">Connecting</div>
42
45
  </div>
43
46
  </div>
44
- <div class="me agent" id="agent-profile" title="더블클릭해서 에이전트 이름 바꾸기">
45
- <div class="avatar agent-avatar" title="더블클릭해서 에이전트 아이콘 바꾸기"><img src="zigmini-profile.svg" alt="" width="36" height="36" /></div>
47
+ <div class="me agent" id="agent-profile" data-i18n-attr="title:profile.editAgentName.tip">
48
+ <div class="avatar agent-avatar" data-i18n-attr="title:profile.editAgentIcon.tip"><img src="zigmini-profile.svg" alt="" width="36" height="36" /></div>
46
49
  <div class="me-text">
47
- <div class="me-name agent-name">지그</div>
48
- <div class="me-sub">에이전트</div>
50
+ <div class="me-name agent-name">Zig</div>
51
+ <div class="me-sub" data-i18n="profile.agent">Default agent</div>
49
52
  </div>
50
53
  </div>
51
54
  </footer>
52
55
  </aside>
53
56
  <main class="chat">
54
57
  <header class="chat-head">
55
- <button type="button" class="head-track" id="toggle-status" aria-label="작업 상태 보기" title="작업 상태 보기" aria-controls="work-status" aria-expanded="false"><img class="head-avatar" id="head-avatar" src="zigmini-profile.svg" alt="" width="30" height="30" /></button>
58
+ <button type="button" class="head-track" id="toggle-status" data-i18n-attr="aria-label:status.open;title:status.open" aria-controls="work-status" aria-expanded="false"><img class="head-avatar" id="head-avatar" src="zigmini-profile.svg" alt="" width="30" height="30" /></button>
56
59
  <span id="conv-title" hidden>main</span>
60
+ <div class="agent-head" id="agent-head" hidden>
61
+ <span class="agent-head-icon" id="agent-head-icon" aria-hidden="true"></span>
62
+ <span class="agent-head-name" id="agent-head-name"></span>
63
+ <button type="button" class="agent-stale" id="agent-stale" data-i18n="agent.staleBadge" hidden>Definition changed · applies from the next session</button>
64
+ </div>
57
65
  <span class="chat-sub" id="conv-thread"></span>
58
- <span class="transition-status" id="session-transition" role="status" aria-live="polite" hidden>세션 교체 중</span>
59
- <span class="login-notice" id="login-notice" role="status" hidden>모델 계정에 로그인하세요</span>
66
+ <span class="transition-status" id="session-transition" role="status" aria-live="polite" data-i18n="head.sessionTransition" hidden>Switching session</span>
67
+ <span class="login-notice" id="login-notice" role="status" data-i18n="head.loginNotice" hidden>Sign in to your model account</span>
60
68
  <span class="login-notice" id="schema-notice" role="alert" hidden></span>
61
- <button type="button" class="update-notice" id="update-notice" title="설정의 업데이트 열기" hidden></button>
69
+ <button type="button" class="update-notice" id="update-notice" data-i18n-attr="title:head.updateNotice.tip" hidden></button>
62
70
  <div class="chat-actions">
63
- <select class="model-select" id="model-select" title="엔진이 쓸 모델" disabled><option>모델 불러오는 중</option></select>
71
+ <select class="model-select" id="model-select" data-i18n-attr="title:model.select.tip" disabled><option data-i18n="model.loading">Loading models</option></select>
64
72
  <span class="model-pending" id="model-pending" hidden></span>
65
- <button class="text-btn" id="rotate-session" data-tip-keep title="이 대화의 엔진 세션을 새로 시작합니다. 화면의 기록은 그대로 남습니다.">새 세션</button>
66
- <button class="text-btn" id="toggle-voice" data-tip-keep title="이 게이트웨이의 음성 서버(음성 인식·음성 합성)를 켜고 끄기. 끄면 마이크·호출어·박수 감지·음성 답변을 쓸 수 없습니다." aria-pressed="true">음성 켜짐</button>
67
- <button class="text-btn" id="voice-install" data-tip-keep title="음성 서버(uv, Python 환경, 음성 모델)를 이 맥의 zigmini 폴더에 내려받아 설치합니다" hidden>음성 설치</button>
68
- <button class="text-btn" id="toggle-clap" data-tip-keep title="박수를 두 번 치면 호출어 대기를 켜고 끄기" aria-pressed="false">박수 감지 꺼짐</button>
69
- <button class="text-btn" id="toggle-wake" data-tip-keep title="&quot;헤이 지그&quot;라고 부르면 받아 적어 입력창에 채우기" aria-pressed="false">호출어 꺼짐</button>
70
- <button class="text-btn" id="toggle-tts" data-tip-keep title="답변을 음성으로 읽기" aria-pressed="false">음성 답변 꺼짐</button>
71
- <button type="button" class="text-btn inbox-toggle" id="toggle-inbox" data-tip-keep title="답을 기다리는 질문과 승인 요청 (Ctrl+C)" aria-haspopup="dialog" aria-expanded="false" aria-controls="inbox" aria-keyshortcuts="Control+C">확인함<span class="inbox-badge" id="inbox-badge" hidden>0</span></button>
72
- <button type="button" class="text-btn" id="toggle-dev" data-tip-keep title="이벤트 패널 (Ctrl+E)" aria-expanded="false" aria-controls="dev" aria-keyshortcuts="Control+E">이벤트</button>
73
- <button type="button" class="text-btn" id="toggle-settings" title="설정: 폰 기기 등록 QR과 등록된 기기" aria-haspopup="dialog" aria-expanded="false" aria-controls="settings">설정</button>
73
+ <button class="text-btn" id="rotate-session" data-tip-keep data-i18n="head.newSession" data-i18n-tip="head.newSession.tip">New session</button>
74
+ <button class="text-btn" id="toggle-voice" data-tip-keep data-i18n-tip="head.voice.tip" aria-pressed="true">Voice on</button>
75
+ <button class="text-btn" id="voice-install" data-tip-keep data-i18n-tip="head.voiceInstall.tip" hidden>Install voice</button>
76
+ <button class="text-btn" id="toggle-clap" data-tip-keep data-i18n-tip="head.clap.tip" aria-pressed="false">Clap detection off</button>
77
+ <button class="text-btn" id="toggle-wake" data-tip-keep data-i18n-tip="head.wake.tip" aria-pressed="false">Wake word off</button>
78
+ <button class="text-btn" id="toggle-tts" data-tip-keep data-i18n-tip="head.tts.tip" aria-pressed="false">Spoken replies off</button>
79
+ <button type="button" class="text-btn inbox-toggle" id="toggle-inbox" data-tip-keep data-i18n-tip="head.inbox.tip" aria-haspopup="dialog" aria-expanded="false" aria-controls="inbox" aria-keyshortcuts="Control+C"><span data-i18n="inbox.title">Inbox</span><span class="inbox-badge" id="inbox-badge" hidden>0</span></button>
80
+ <button type="button" class="text-btn" id="toggle-dev" data-tip-keep data-i18n="dev.title" data-i18n-tip="head.events.tip" aria-expanded="false" aria-controls="dev" aria-keyshortcuts="Control+E">Events</button>
81
+ <button type="button" class="text-btn" id="toggle-agents" data-i18n="side.agents" data-i18n-attr="title:head.agents.tip" aria-haspopup="dialog" aria-expanded="false" aria-controls="agents">Agents</button>
82
+ <button type="button" class="text-btn" id="toggle-settings" data-i18n="settings.title" data-i18n-attr="title:head.settings.tip" aria-haspopup="dialog" aria-expanded="false" aria-controls="settings">Settings</button>
74
83
  </div>
75
84
  </header>
76
- <div class="last-sent" id="last-sent" role="button" tabindex="0" aria-expanded="false" aria-label="마지막으로 보낸 메시지" hidden></div>
85
+ <div class="last-sent" id="last-sent" role="button" tabindex="0" aria-expanded="false" data-i18n-attr="aria-label:chat.lastSent.label" hidden></div>
77
86
  <div class="device-requests" id="device-requests"></div>
78
87
  <div class="messages-wrap">
79
88
  <section class="messages" id="messages" aria-live="polite"></section>
80
89
  <div class="memo" id="memo" hidden>
81
- <div class="memo-head"><span>쪽지</span><span id="memo-image-count">0/2</span><button class="text-btn" id="memo-add-image" type="button">이미지 보관</button></div>
82
- <textarea class="memo-text" id="memo-text" placeholder="자주 쓰는 프롬프트나 메모를 적어 두세요. 자동 저장됩니다." spellcheck="false"></textarea>
83
- <input id="memo-image-input" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple aria-label="쪽지에 보관할 이미지 선택" hidden>
90
+ <div class="memo-head"><span data-i18n="memo.title">Memo</span><span id="memo-image-count">0/2</span><button class="text-btn" id="memo-add-image" type="button" data-i18n="memo.addImage">Keep an image</button></div>
91
+ <textarea class="memo-text" id="memo-text" data-i18n-attr="placeholder:memo.placeholder" spellcheck="false"></textarea>
92
+ <input id="memo-image-input" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple data-i18n-attr="aria-label:memo.imageInput.label" hidden>
84
93
  <div id="memo-images" class="memo-images" hidden></div>
85
94
  <div id="memo-status" class="memo-status" role="status" aria-live="polite"></div>
86
- <div class="memo-resize" id="memo-resize" title="드래그해서 크기 조절"></div>
95
+ <div class="memo-resize" id="memo-resize" data-i18n-attr="title:memo.resize.tip"></div>
87
96
  </div>
88
97
  <div class="scroll-nav">
89
- <button type="button" class="nav-btn" id="nav-prev" title="이전 내 메시지로" aria-label="이전 내 메시지로">▲</button>
90
- <button type="button" class="nav-btn" id="nav-next" title="다음 내 메시지로 · 더블클릭하면 맨 아래로" aria-label="다음 내 메시지로, 더블클릭하면 맨 아래로">▼</button>
98
+ <button type="button" class="nav-btn" id="nav-prev" data-i18n-attr="title:chat.nav.prev;aria-label:chat.nav.prev">▲</button>
99
+ <button type="button" class="nav-btn" id="nav-next" data-i18n-attr="title:chat.nav.next;aria-label:chat.nav.next.label">▼</button>
91
100
  </div>
92
101
  </div>
93
102
  <div class="ref-tray" id="ref-tray" hidden>
94
- <ul class="ref-list" id="ref-list" aria-label="참조할 메시지"></ul>
103
+ <ul class="ref-list" id="ref-list" data-i18n-attr="aria-label:refs.label"></ul>
95
104
  <span class="ref-note" id="ref-note" role="status" aria-live="polite"></span>
96
105
  </div>
97
106
  <div class="attach-tray" id="attach-tray" hidden></div>
98
107
  <form class="composer" id="composer">
99
- <div class="slash-menu" id="slash-menu" role="listbox" aria-label="명령과 스킬" hidden></div>
100
- <button type="button" class="mic" id="toggle-memo" title="쪽지 열기/닫기" aria-pressed="false">쪽지</button>
101
- <button type="button" class="mic" id="attach" title="이미지 첨부 (최대 4장 · 붙여넣기나 끌어놓기도 됩니다)">이미지</button>
108
+ <div class="slash-menu" id="slash-menu" role="listbox" data-i18n-attr="aria-label:slash.label" hidden></div>
109
+ <button type="button" class="mic" id="toggle-memo" data-i18n="memo.title" data-i18n-attr="title:memo.toggle.tip" aria-pressed="false">Memo</button>
110
+ <button type="button" class="mic" id="attach" data-i18n="attach.button" data-i18n-attr="title:attach.tip">Image</button>
102
111
  <input type="file" id="attach-input" accept="image/png,image/jpeg,image/gif,image/webp" multiple hidden>
103
- <button type="button" class="mic" id="mic" data-tip-keep title="누르고 말하기 (다시 누르면 전송)">마이크</button>
104
- <textarea id="input" rows="1" placeholder="메시지 보내기" title="Enter 전송, Shift+Enter 줄바꿈"></textarea>
105
- <button type="submit" class="send" id="send">전송</button>
106
- <button type="button" class="stop" id="interrupt" title="진행 중인 작업 중단" hidden>중단</button>
112
+ <button type="button" class="mic" id="mic" data-tip-keep data-i18n-tip="composer.mic.tip">Mic</button>
113
+ <textarea id="input" rows="1" data-i18n-attr="placeholder:composer.placeholder;title:composer.input.tip"></textarea>
114
+ <button type="submit" class="send" id="send">Send</button>
115
+ <button type="button" class="stop" id="interrupt" data-i18n="composer.stop" data-i18n-attr="title:composer.stop.tip" hidden>Stop</button>
107
116
  </form>
108
117
  </main>
109
118
  <aside class="inbox" id="inbox" role="dialog" aria-labelledby="inbox-title" hidden>
110
119
  <header class="inbox-head">
111
- <h2 class="inbox-title" id="inbox-title" tabindex="-1">확인함</h2>
120
+ <h2 class="inbox-title" id="inbox-title" tabindex="-1" data-i18n="inbox.title">Inbox</h2>
112
121
  <span class="inbox-summary" id="inbox-summary"></span>
113
- <button type="button" class="icon-btn inbox-close" id="inbox-close" aria-label="확인함 닫기" title="닫기 (Esc)">×</button>
122
+ <button type="button" class="icon-btn inbox-close" id="inbox-close" data-i18n-attr="aria-label:inbox.close;title:common.closeEsc">×</button>
114
123
  </header>
115
124
  <div class="inbox-status" id="inbox-status" role="status" aria-live="polite"></div>
116
- <ul class="inbox-list" id="inbox-list" aria-label="미결정 항목"></ul>
125
+ <ul class="inbox-list" id="inbox-list" data-i18n-attr="aria-label:inbox.list.label"></ul>
117
126
  <footer class="inbox-foot" hidden>
118
- <button type="button" class="text-btn" id="inbox-more">더 보기</button>
127
+ <button type="button" class="text-btn" id="inbox-more" data-i18n="common.more">Show more</button>
119
128
  </footer>
120
129
  </aside>
121
130
  <aside class="settings" id="settings" role="dialog" aria-labelledby="settings-title" hidden>
122
131
  <div class="settings-card">
123
132
  <header class="settings-head">
124
- <h2 class="settings-title" id="settings-title" tabindex="-1">설정</h2>
125
- <button type="button" class="text-btn" id="manual-open" title="사용자 매뉴얼: 화면, 단축키, 기능" aria-haspopup="dialog" aria-expanded="false" aria-controls="manual">매뉴얼</button>
126
- <button type="button" class="icon-btn settings-close" id="settings-close" aria-label="설정 닫기" title="닫기 (Esc)">×</button>
133
+ <h2 class="settings-title" id="settings-title" tabindex="-1" data-i18n="settings.title">Settings</h2>
134
+ <button type="button" class="text-btn" id="manual-open" data-i18n="manual.title" data-i18n-attr="title:manual.open.tip" aria-haspopup="dialog" aria-expanded="false" aria-controls="manual">Manual</button>
135
+ <button type="button" class="icon-btn settings-close" id="settings-close" data-i18n-attr="aria-label:settings.close;title:common.closeEsc">×</button>
127
136
  </header>
128
137
  <div class="settings-body">
138
+ <section class="settings-section" id="language-settings" aria-labelledby="language-title">
139
+ <div class="section-label"><label id="language-title" for="ui-language" data-i18n="settings.language.title">Language</label></div>
140
+ <div class="access-form">
141
+ <select class="access-input" id="ui-language" aria-describedby="language-description"></select>
142
+ </div>
143
+ <p class="update-note" id="language-description" data-i18n="settings.language.note">Applies to the web UI and the app of this gateway. The manual opens in this language too.</p>
144
+ </section>
129
145
  <section class="settings-section update" id="update" aria-labelledby="update-title">
130
- <div class="section-label"><span id="update-title">업데이트</span></div>
146
+ <div class="section-label"><span id="update-title" data-i18n="settings.update.title">Update</span></div>
131
147
  <p class="update-line" id="update-current"></p>
132
148
  <p class="update-line" id="update-latest" role="status" aria-live="polite"></p>
133
149
  <p class="update-note" id="update-error" role="status" hidden></p>
134
150
  <p class="update-note" id="update-note" hidden></p>
135
- <a class="update-link" id="update-changelog" target="_blank" rel="noopener noreferrer" hidden>변경 내용 보기</a>
151
+ <a class="update-link" id="update-changelog" target="_blank" rel="noopener noreferrer" data-i18n="settings.update.changelog" hidden>See what changed</a>
136
152
  <p class="update-note" id="update-warning" hidden></p>
137
153
  <div class="update-ask update-questions" id="update-questions" hidden>
138
154
  <p class="update-line" id="update-questions-line" role="status" aria-live="polite"></p>
139
- <button type="button" class="update-run" id="update-questions-open">확인함 열기</button>
155
+ <button type="button" class="update-run" id="update-questions-open" data-i18n="settings.update.openInbox">Open the inbox</button>
140
156
  </div>
141
157
  <div class="update-buttons">
142
- <button type="button" class="update-run" id="update-check">지금 확인</button>
143
- <button type="button" class="update-run" id="update-run" hidden>업데이트</button>
158
+ <button type="button" class="update-run" id="update-check" data-i18n="settings.update.checkNow">Check now</button>
159
+ <button type="button" class="update-run" id="update-run" data-i18n="settings.update.run" hidden>Update</button>
144
160
  </div>
145
161
  <div class="update-ask" id="update-ask" hidden>
146
162
  <p class="update-line" id="update-ask-message"></p>
147
163
  <pre class="update-ask-text" id="update-ask-text" hidden></pre>
148
- <input class="update-ask-input" id="update-ask-input" type="text" autocomplete="off" aria-label="동의하려면 동의를 입력하세요" hidden>
164
+ <input class="update-ask-input" id="update-ask-input" type="text" autocomplete="off" data-i18n-attr="aria-label:settings.update.consentInput.label" hidden>
149
165
  <button type="button" class="update-run" id="update-ask-go"></button>
150
166
  </div>
151
167
  <p class="update-line" id="update-progress" role="status" aria-live="polite" hidden></p>
152
168
  </section>
153
169
  <section class="settings-section enroll" id="enroll" aria-labelledby="enroll-title">
154
- <div class="section-label"><span id="enroll-title">기기 등록</span><span class="enroll-left" id="enroll-left" role="timer" aria-live="off"></span></div>
155
- <div class="enroll-qr" id="enroll-qr" role="img" aria-label="폰 등록 QR" hidden></div>
170
+ <div class="section-label"><span id="enroll-title" data-i18n="settings.enroll.title">Device enrollment</span><span class="enroll-left" id="enroll-left" role="timer" aria-live="off"></span></div>
171
+ <div class="enroll-qr" id="enroll-qr" role="img" data-i18n-attr="aria-label:settings.enroll.qrLabel" hidden></div>
156
172
  <p class="enroll-message" id="enroll-message" role="status" aria-live="polite"></p>
157
- <div class="enroll-actions"><button type="button" class="text-btn" id="enroll-renew" hidden>새로 만들기</button></div>
158
- <div class="section-label"><span>등록된 기기</span></div>
159
- <ul class="device-list" id="device-list" aria-label="등록된 기기"></ul>
160
- <div class="section-label"><span id="access-title">외부 접속</span></div>
173
+ <div class="enroll-actions"><button type="button" class="text-btn" id="enroll-renew" data-i18n="settings.enroll.renew" hidden>Make a new one</button></div>
174
+ <div class="section-label"><span data-i18n="settings.devices.title">Registered devices</span></div>
175
+ <ul class="device-list" id="device-list" data-i18n-attr="aria-label:settings.devices.title"></ul>
176
+ <div class="section-label"><span id="access-title" data-i18n="settings.access.title">External access</span></div>
161
177
  <p class="enroll-message" id="access-current"></p>
162
178
  <form class="access-form" id="access-form" aria-labelledby="access-title">
163
- <select class="access-input" id="access-choice" aria-label="외부 접속 방식">
164
- <option value="lan">집 Wi-Fi만</option>
165
- <option value="tailscale">Tailscale</option>
166
- <option value="domain">자기 도메인(https)</option>
179
+ <select class="access-input" id="access-choice" data-i18n-attr="aria-label:settings.access.choice.label">
180
+ <option value="lan" data-i18n="settings.access.homeWifi">Home Wi-Fi only</option>
181
+ <option value="tailscale" data-i18n="settings.access.tailscale">Tailscale</option>
182
+ <option value="domain" data-i18n="settings.access.domainOption">Your own domain (https)</option>
167
183
  </select>
168
- <input class="access-input" id="access-domain" type="url" placeholder="https://도메인" aria-label="자기 도메인 주소" hidden>
169
- <button type="submit" class="text-btn" id="access-save">바꾸기</button>
184
+ <input class="access-input" id="access-domain" type="url" data-i18n-attr="placeholder:settings.access.domain.placeholder;aria-label:settings.access.domain.label" hidden>
185
+ <button type="submit" class="text-btn" id="access-save" data-i18n="common.change">Change</button>
170
186
  </form>
171
187
  <p class="enroll-message" id="access-message" role="status" aria-live="polite"></p>
172
188
  </section>
173
189
  <section class="settings-section enroll app-download" id="app-download" aria-labelledby="apk-title">
174
- <div class="section-label"><span id="apk-title">앱 내려받기</span><span class="enroll-left" id="apk-left" role="timer" aria-live="off"></span></div>
175
- <div class="enroll-qr" id="apk-qr" role="img" aria-label="앱 내려받기 QR" hidden></div>
190
+ <div class="section-label"><span id="apk-title" data-i18n="settings.apk.title">Download the app</span><span class="enroll-left" id="apk-left" role="timer" aria-live="off"></span></div>
191
+ <div class="enroll-qr" id="apk-qr" role="img" data-i18n-attr="aria-label:settings.apk.qrLabel" hidden></div>
176
192
  <p class="apk-version" id="apk-version" hidden></p>
177
193
  <p class="enroll-message" id="apk-message" role="status" aria-live="polite"></p>
178
- <div class="enroll-actions"><button type="button" class="text-btn" id="apk-renew" hidden>새로 만들기</button></div>
194
+ <div class="enroll-actions"><button type="button" class="text-btn" id="apk-renew" data-i18n="settings.enroll.renew" hidden>Make a new one</button></div>
195
+ </section>
196
+ <section class="settings-section formal-verification" id="formal-verification" aria-labelledby="formal-verification-title">
197
+ <div class="section-label"><span id="formal-verification-title" data-i18n="settings.formal.title">Formal verification</span></div>
198
+ <ul class="formal-tools" id="formal-tools" data-i18n-attr="aria-label:settings.formal.tools.label"></ul>
179
199
  </section>
180
200
  <section class="settings-section security-check" id="security-check" aria-labelledby="security-check-title">
181
- <div class="section-label"><span id="security-check-title">보안검사</span></div>
182
- <p class="update-line">보낸 메시지에 비밀키, 토큰, 비밀번호, 홈 폴더 경로, 내부 IP 같은 값이 있으면 엔진에 보내지 않고 보류한 뒤, 값을 보여 주고 보낼지 묻습니다.</p>
183
- <div class="update-buttons"><button type="button" class="text-btn" id="toggle-security" aria-pressed="true">보안검사 켜짐</button></div>
184
- <div class="section-label">기억한 정확한 값</div>
185
- <p class="update-note">이 설치에서 승인한 값만 다음 검사부터 허용합니다. 제거·전체 삭제는 실행 시점의 값을 지웁니다. 다음 검사에서 다시 보류되며 이미 시작한 전송은 취소하지 않습니다. 저장소·릴리스 보안검사는 바뀌지 않습니다.</p>
201
+ <div class="section-label"><span id="security-check-title" data-i18n="settings.security.title">Security check</span></div>
202
+ <div class="switch-row">
203
+ <span class="switch-label" id="toggle-security-label" data-i18n="settings.security.message.title">Message check before the engine</span>
204
+ <button type="button" class="info-tip" data-tip-keep data-i18n-tip="settings.security.message.detail" data-i18n-attr="aria-label:common.details">i</button>
205
+ <button type="button" class="switch" id="toggle-security" role="switch" aria-checked="true" aria-labelledby="toggle-security-label" aria-describedby="toggle-security-summary"></button>
206
+ </div>
207
+ <p class="update-note switch-summary" id="toggle-security-summary" data-i18n="settings.security.message.summary">A message that carries a secret, a home folder path or an internal IP is held, and you are asked before it goes to the engine.</p>
208
+ <div class="switch-row">
209
+ <span class="switch-label" id="toggle-repo-security-label" data-i18n="settings.security.repo.title">Output check</span>
210
+ <button type="button" class="info-tip" data-tip-keep data-i18n-tip="settings.security.repo.detail" data-i18n-attr="aria-label:common.details">i</button>
211
+ <button type="button" class="switch" id="toggle-repo-security" role="switch" aria-checked="true" aria-labelledby="toggle-repo-security-label" aria-describedby="toggle-repo-security-summary"></button>
212
+ </div>
213
+ <p class="update-note switch-summary" id="toggle-repo-security-summary" data-i18n="settings.security.repo.summary">zigmini scan finds secrets, personal data and the security baseline values in work output (repositories) before they leave for GitHub.</p>
214
+ <div class="section-label"><span data-i18n="settings.security.allowed.title">Remembered exact values</span></div>
215
+ <p class="update-note" data-i18n="settings.security.allowed.note">Only values approved on this installation are allowed from the next check on. Remove and Delete all drop the values as of that moment; they are held again at the next check, and a transfer already started is not cancelled. Allowing a transfer does not affect the repository and release security checks. Only a value with "Also allowed in repository checks" turned on passes the repository check; a value equal to an automatic item (home folder path, account name) cannot be turned on.</p>
186
216
  <div class="update-buttons">
187
- <button type="button" class="text-btn" id="security-allowed-refresh">목록 확인 / 다시 시도</button>
188
- <button type="button" class="text-btn" id="security-allowed-clear" disabled>전체 삭제</button>
217
+ <button type="button" class="text-btn" id="security-allowed-refresh" data-i18n="settings.security.refreshList">Check the list / retry</button>
218
+ <button type="button" class="text-btn" id="security-allowed-clear" disabled data-i18n="settings.security.allowed.clearAll">Delete all</button>
189
219
  </div>
190
220
  <p class="enroll-message" id="security-allowed-status" role="status" aria-live="polite"></p>
191
- <ul class="security-allowed-list" id="security-allowed-list" aria-label="기억한 정확한 값"></ul>
221
+ <ul class="security-allowed-list" id="security-allowed-list" data-i18n-attr="aria-label:settings.security.allowed.title"></ul>
192
222
  <div class="inbox-dismiss mode-cancel" id="security-allowed-confirm" hidden>
193
- <p class="inbox-dismiss-text">선택한 값 또는 실행 시점의 전체 값을 제거합니다. 다음 검사부터 다시 보류되며 이미 시작한 전송은 취소되지 않습니다.</p>
194
- <button type="button" class="text-btn danger" id="security-allowed-remove">제거 확인</button>
195
- <button type="button" class="text-btn" id="security-allowed-keep">그대로 두기</button>
223
+ <p class="inbox-dismiss-text" data-i18n="settings.security.allowed.confirmText">Removes the selected value, or every value as of now. They are held again from the next check; a transfer already started is not cancelled.</p>
224
+ <button type="button" class="text-btn danger" id="security-allowed-remove" data-i18n="settings.security.allowed.confirmRemove">Confirm removal</button>
225
+ <button type="button" class="text-btn" id="security-allowed-keep" data-i18n="common.keep">Keep</button>
226
+ </div>
227
+ <div class="section-label"><span id="security-baseline-title" data-i18n="settings.security.baseline.title">Security baseline</span></div>
228
+ <p class="update-note" data-i18n="settings.security.baseline.note">The private strings the repository and release security checks look for. The gateway writes this list out as the baseline file, and `zigmini scan` checks with that file. The home folder path and the account name are automatic items and cannot be removed. The values show only on this screen.</p>
229
+ <form class="access-form" id="security-baseline-form" aria-labelledby="security-baseline-title">
230
+ <input class="access-input" id="security-baseline-input" type="text" autocomplete="off" spellcheck="false" data-i18n-attr="placeholder:settings.security.baseline.input;aria-label:settings.security.baseline.input">
231
+ <button type="submit" class="text-btn" id="security-baseline-add" disabled data-i18n="common.add">Add</button>
232
+ <button type="button" class="text-btn" id="security-baseline-refresh" disabled data-i18n="settings.security.refreshList">Check the list / retry</button>
233
+ </form>
234
+ <p class="enroll-message" id="security-baseline-status" role="status" aria-live="polite"></p>
235
+ <p class="enroll-message" id="security-baseline-export"></p>
236
+ <ul class="security-allowed-list" id="security-baseline-list" data-i18n-attr="aria-label:settings.security.baseline.list.label"></ul>
237
+ <div class="inbox-dismiss mode-cancel" id="security-baseline-confirm" hidden>
238
+ <p class="inbox-dismiss-text" data-i18n="settings.security.baseline.confirmText">Removes this term from the baseline. The next repository check no longer looks for it. A check already started does not change.</p>
239
+ <button type="button" class="text-btn danger" id="security-baseline-remove" data-i18n="settings.security.baseline.confirmRemove">Confirm deletion</button>
240
+ <button type="button" class="text-btn" id="security-baseline-keep" data-i18n="common.keep">Keep</button>
196
241
  </div>
197
242
  </section>
198
243
  <section class="settings-section" id="notification-settings" aria-labelledby="notification-title">
199
- <div class="section-label"><label id="notification-title" for="notification-mode">알림</label></div>
244
+ <div class="section-label"><label id="notification-title" for="notification-mode" data-i18n="settings.notification.title">Notifications</label></div>
200
245
  <div class="access-form">
201
246
  <select class="access-input" id="notification-mode" aria-describedby="notification-description notification-status" disabled>
202
- <option value="inbox-only" selected>확인함 알림만</option>
203
- <option value="all-replies">모든 답장 알림</option>
247
+ <option value="inbox-only" selected data-i18n="settings.notification.inboxOnly">Inbox notifications only</option>
248
+ <option value="all-replies" data-i18n="settings.notification.allReplies">All reply notifications</option>
204
249
  </select>
205
250
  </div>
206
- <p class="update-note" id="notification-description">모든 답장 알림은 확인함 알림과 메인·작업의 완료된 답장 알림을 포함합니다. 이 게이트웨이에 연결된 모든 기기에 적용됩니다.</p>
251
+ <p class="update-note" id="notification-description" data-i18n="settings.notification.description">All reply notifications include the inbox notifications and the finished-reply notifications of main and of works. Applies to every device connected to this gateway.</p>
207
252
  <p class="enroll-message" id="notification-status" role="status" aria-live="polite"></p>
208
253
  </section>
209
254
  <section class="settings-section gc" id="gc" aria-labelledby="gc-title">
210
- <div class="section-label"><span id="gc-title">정리</span><span class="gc-head"><span class="gc-summary" id="gc-summary" role="status" aria-live="polite"></span><label class="gc-all" id="gc-all-label" hidden><input type="checkbox" id="gc-all">전체 선택</label></span></div>
211
- <ul class="gc-list" id="gc-list" aria-label="정리 후보"></ul>
255
+ <div class="section-label"><span id="gc-title" data-i18n="settings.gc.title">Cleanup</span><span class="gc-head"><span class="gc-summary" id="gc-summary" role="status" aria-live="polite"></span><label class="gc-all" id="gc-all-label" hidden><input type="checkbox" id="gc-all"><span data-i18n="settings.gc.selectAll">Select all</span></label></span></div>
256
+ <ul class="gc-list" id="gc-list" data-i18n-attr="aria-label:settings.gc.list.label"></ul>
212
257
  <p class="gc-message" id="gc-message" role="status" aria-live="polite"></p>
213
- <div class="gc-actions"><button type="button" class="text-btn" id="gc-clean" disabled>선택한 항목 정리</button></div>
258
+ <div class="gc-actions"><button type="button" class="text-btn" id="gc-clean" disabled data-i18n="settings.gc.clean">Clean the selected items</button></div>
214
259
  </section>
215
260
  </div>
216
261
  </div>
217
262
  </aside>
263
+ <aside class="settings agents" id="agents" role="dialog" aria-labelledby="agents-title" hidden>
264
+ <div class="settings-card agents-card">
265
+ <header class="settings-head agents-head">
266
+ <h2 class="settings-title" id="agents-title" tabindex="-1" data-i18n="side.agents">Agents</h2>
267
+ <nav class="agents-nav" data-i18n-attr="aria-label:agents.nav.label">
268
+ <button type="button" class="text-btn" id="agents-nav-list" data-view="list" data-i18n="agents.list">List</button>
269
+ <button type="button" class="text-btn" id="agents-nav-defaults" data-view="defaults" data-i18n="agents.defaults">Shared defaults</button>
270
+ <button type="button" class="text-btn" id="agents-nav-trash" data-view="trash" data-i18n="agents.trash">Archive</button>
271
+ </nav>
272
+ <button type="button" class="icon-btn settings-close" id="agents-close" data-i18n-attr="aria-label:agents.close;title:common.closeEsc">×</button>
273
+ </header>
274
+ <p class="agents-status" id="agents-status" role="status" aria-live="polite"></p>
275
+ <div class="settings-body agents-body" id="agents-body"></div>
276
+ </div>
277
+ </aside>
218
278
  <aside class="settings manual" id="manual" role="dialog" aria-labelledby="manual-title" hidden>
219
279
  <div class="settings-card manual-card">
220
280
  <header class="settings-head">
221
- <h2 class="settings-title" id="manual-title" tabindex="-1">매뉴얼</h2>
281
+ <h2 class="settings-title" id="manual-title" tabindex="-1" data-i18n="manual.title">Manual</h2>
222
282
  <button type="button" class="text-btn" id="manual-lang">English</button>
223
- <button type="button" class="icon-btn settings-close" id="manual-close" aria-label="매뉴얼 닫기" title="닫기 (Esc)">×</button>
283
+ <button type="button" class="icon-btn settings-close" id="manual-close" data-i18n-attr="aria-label:manual.close;title:common.closeEsc">×</button>
224
284
  </header>
225
285
  <p class="manual-status" id="manual-status" role="status" aria-live="polite" hidden></p>
226
286
  <div class="manual-body msg-text" id="manual-body"></div>
@@ -228,8 +288,8 @@
228
288
  </aside>
229
289
  <aside class="dev" id="dev">
230
290
  <header class="dev-head">
231
- <span>이벤트</span>
232
- <button class="text-btn" id="clear-dev">지우기</button>
291
+ <span data-i18n="dev.title">Events</span>
292
+ <button class="text-btn" id="clear-dev" data-i18n="dev.clear">Clear</button>
233
293
  </header>
234
294
  <div class="dev-log" id="dev-log"></div>
235
295
  </aside>
package/web/lastsent.js CHANGED
@@ -1,3 +1,8 @@
1
+ import dict from "./i18n/lastsent.js";
2
+ import { registerStrings, t } from "./i18n.js";
3
+
4
+ registerStrings(dict);
5
+
1
6
  // The one-line strip under the chat header shows the newest message the user sent in the current conversation.
2
7
  // History pages arrive newest-first and then older ones on scroll, so only an event with a higher seq replaces it.
3
8
 
@@ -22,14 +27,14 @@ export function nextLastSent(prev, event) {
22
27
  export function lastSentLabel(rec) {
23
28
  if (!rec) return "";
24
29
  const text = rec.text.replace(/\s+/g, " ").trim();
25
- const image = rec.images > 0 ? `[이미지 ${rec.images}장]` : "";
26
- const references = rec.references > 0 ? `[참조 ${rec.references}개]` : "";
30
+ const image = rec.images > 0 ? t("lastsent.images", { count: rec.images }) : "";
31
+ const references = rec.references > 0 ? t("lastsent.references", { count: rec.references }) : "";
27
32
  return [references, image, text].filter(Boolean).join(" ");
28
33
  }
29
34
 
30
35
  export function lastSentTooltip(rec) {
31
36
  if (!rec) return "";
32
- const image = rec.images > 0 ? `[이미지 ${rec.images}장]` : "";
33
- const references = rec.references > 0 ? `[참조 ${rec.references}개]` : "";
37
+ const image = rec.images > 0 ? t("lastsent.images", { count: rec.images }) : "";
38
+ const references = rec.references > 0 ? t("lastsent.references", { count: rec.references }) : "";
34
39
  return [references, image, rec.text.trim()].filter(Boolean).join("\n");
35
40
  }
package/web/manual.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type ManualLang = "ko" | "en";
2
2
  export declare const MANUAL_PATH: string;
3
- export declare const MANUAL_MISSING_TEXT: string;
3
+ export declare const MANUAL_MISSING_KEY: string;
4
4
  export declare function manualUrl(lang: string): string;
5
5
  export declare function otherLang(lang: string): ManualLang;
6
6
  export declare function manualLinkLang(href: string | null | undefined): ManualLang | null;
package/web/manual.js CHANGED
@@ -1,8 +1,14 @@
1
1
  // The user manual view of 설정 "매뉴얼": decisions only, with no DOM, so test/unit/manual.test.ts and the mutation run
2
- // drive them directly; app.js fetches GET /api/manual and renders the Markdown with the chat renderer.
2
+ // drive them directly; app.js fetches GET /api/manual and renders the Markdown with the chat renderer. The texts follow
3
+ // the UI language (web/i18n.js) from web/i18n/manual.js.
4
+
5
+ import { registerStrings, t } from "./i18n.js";
6
+ import manualStrings from "./i18n/manual.js";
7
+
8
+ registerStrings(manualStrings);
3
9
 
4
10
  export const MANUAL_PATH = "/api/manual";
5
- export const MANUAL_MISSING_TEXT = "이 게이트웨이 버전에는 매뉴얼이 없습니다. 게이트웨이를 업데이트하면 볼 수 있습니다.";
11
+ export const MANUAL_MISSING_KEY = "manual.missing";
6
12
 
7
13
  export function manualUrl(lang) {
8
14
  return `${MANUAL_PATH}?lang=${lang === "en" ? "en" : "ko"}`;
@@ -23,7 +29,7 @@ export function manualLinkLang(href) {
23
29
 
24
30
  /** The line for a failed load: [status] is the HTTP status, or 0 when the request itself failed. */
25
31
  export function manualFailureText(status) {
26
- if (status === 404) return MANUAL_MISSING_TEXT;
27
- if (status === 0) return "매뉴얼을 불러오지 못했습니다. 게이트웨이 연결을 확인하세요.";
28
- return `매뉴얼을 불러오지 못했습니다 (HTTP ${status}).`;
32
+ if (status === 404) return t(MANUAL_MISSING_KEY);
33
+ if (status === 0) return t("manual.loadFailedOffline");
34
+ return t("manual.loadFailedHttp", { status });
29
35
  }