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

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 (141) hide show
  1. package/README.ko.md +24 -4
  2. package/README.md +24 -4
  3. package/assets/agent/gateway-rules.md +1 -1
  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 +51 -11
  8. package/docs/manual.md +52 -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 +14 -5
  29. package/src/approvals.ts +6 -1
  30. package/src/cli/main.ts +33 -0
  31. package/src/cli/messages.ts +10 -2
  32. package/src/cli/rotate-main.ts +2 -1
  33. package/src/cli/scan.ts +414 -0
  34. package/src/cli/security-import.ts +155 -0
  35. package/src/compact.ts +22 -0
  36. package/src/decisions.ts +6 -4
  37. package/src/engine/compat.json +3 -3
  38. package/src/engine/index.ts +1 -0
  39. package/src/engine/schema-guard.ts +12 -4
  40. package/src/engine/sdk.ts +8 -0
  41. package/src/formal-verification.ts +366 -0
  42. package/src/gc.ts +37 -6
  43. package/src/hub.ts +339 -79
  44. package/src/main.ts +95 -9
  45. package/src/maintenance.ts +1 -1
  46. package/src/manual.ts +9 -2
  47. package/src/profile-prompt.ts +73 -0
  48. package/src/profile.ts +33 -10
  49. package/src/protocol.ts +11 -2
  50. package/src/question-notifications.ts +4 -1
  51. package/src/reply-notifications.ts +7 -1
  52. package/src/repo-security-setting.ts +33 -0
  53. package/src/runtime/paths.ts +16 -0
  54. package/src/security/baseline.ts +170 -0
  55. package/src/security/dex-strings.ts +242 -0
  56. package/src/security/repo-scan.ts +369 -0
  57. package/src/security/revision-probe.ts +144 -0
  58. package/src/security/terms.ts +97 -0
  59. package/src/security-allow.ts +1 -0
  60. package/src/security-baseline.ts +113 -0
  61. package/src/security-check.ts +91 -16
  62. package/src/server-text.ts +38 -0
  63. package/src/store.ts +142 -16
  64. package/src/transcript.ts +7 -1
  65. package/src/ui-language.ts +33 -0
  66. package/src/update/check.ts +7 -3
  67. package/src/update/handoff-system.ts +24 -16
  68. package/src/update/handoff.ts +23 -12
  69. package/src/update/helper.ts +72 -36
  70. package/src/update/request.ts +46 -30
  71. package/src/update/state.ts +21 -11
  72. package/src/update/strategy.ts +12 -5
  73. package/src/update/target.ts +32 -15
  74. package/src/usage.ts +43 -20
  75. package/src/version.ts +39 -16
  76. package/src/voice/agents.ts +8 -3
  77. package/src/voice/install.ts +29 -18
  78. package/src/work-watch.ts +40 -18
  79. package/web/agent-icon.js +15 -9
  80. package/web/agents.d.ts +201 -0
  81. package/web/agents.js +813 -0
  82. package/web/app-download.js +32 -19
  83. package/web/app.js +1407 -278
  84. package/web/autocopy.js +6 -1
  85. package/web/boot.js +27 -8
  86. package/web/commands.js +20 -11
  87. package/web/decisions.js +70 -49
  88. package/web/devlog.js +25 -19
  89. package/web/diagram.js +8 -3
  90. package/web/enroll.js +43 -22
  91. package/web/gc.js +32 -19
  92. package/web/i18n/agent-icon.js +24 -0
  93. package/web/i18n/agents.js +48 -0
  94. package/web/i18n/app-download.js +26 -0
  95. package/web/i18n/autocopy.js +5 -0
  96. package/web/i18n/boot.js +24 -0
  97. package/web/i18n/commands.js +34 -0
  98. package/web/i18n/decisions.js +24 -0
  99. package/web/i18n/devlog.js +60 -0
  100. package/web/i18n/diagram.js +12 -0
  101. package/web/i18n/enroll.js +22 -0
  102. package/web/i18n/gc.js +38 -0
  103. package/web/i18n/lastsent.js +10 -0
  104. package/web/i18n/main.d.ts +4 -0
  105. package/web/i18n/main.js +527 -0
  106. package/web/i18n/manual.d.ts +4 -0
  107. package/web/i18n/manual.js +14 -0
  108. package/web/i18n/memo.js +30 -0
  109. package/web/i18n/notifications.js +16 -0
  110. package/web/i18n/profile.js +13 -0
  111. package/web/i18n/reference.js +26 -0
  112. package/web/i18n/security-inbox.js +40 -0
  113. package/web/i18n/slash.js +38 -0
  114. package/web/i18n/turn-stats.js +5 -0
  115. package/web/i18n/update.js +42 -0
  116. package/web/i18n/voice.js +28 -0
  117. package/web/i18n/wake.js +16 -0
  118. package/web/i18n/workid.js +10 -0
  119. package/web/i18n/workstatus.js +20 -0
  120. package/web/i18n.d.ts +20 -0
  121. package/web/i18n.js +63 -0
  122. package/web/index.html +156 -96
  123. package/web/lastsent.js +9 -4
  124. package/web/manual.d.ts +1 -1
  125. package/web/manual.js +11 -5
  126. package/web/memo.js +18 -13
  127. package/web/notifications.js +12 -7
  128. package/web/profile.js +42 -17
  129. package/web/reference.js +20 -15
  130. package/web/security-inbox.d.ts +18 -1
  131. package/web/security-inbox.js +173 -4
  132. package/web/slash.js +23 -13
  133. package/web/style.css +132 -3
  134. package/web/tooltip.js +5 -2
  135. package/web/turn-stats.js +6 -2
  136. package/web/update.js +31 -23
  137. package/web/voice.js +18 -8
  138. package/web/wake.d.ts +4 -0
  139. package/web/wake.js +7 -3
  140. package/web/workid.js +12 -2
  141. package/web/workstatus.js +36 -32
@@ -0,0 +1,16 @@
1
+ export default {
2
+ en: {
3
+ "notifications.error.read": "Could not load the notification setting. It retries when the connection comes back.",
4
+ "notifications.error.save": "Could not save the notification setting. The confirmed setting stays.",
5
+ "notifications.saving": "Saving…",
6
+ "notifications.waiting": "Waiting for the gateway connection.",
7
+ "notifications.checking": "Checking the notification setting…",
8
+ },
9
+ ko: {
10
+ "notifications.error.read": "알림 설정을 불러오지 못했습니다. 다시 연결하면 재시도합니다.",
11
+ "notifications.error.save": "알림 설정을 저장하지 못했습니다. 확인된 설정을 유지합니다.",
12
+ "notifications.saving": "저장 중…",
13
+ "notifications.waiting": "게이트웨이 연결을 기다립니다.",
14
+ "notifications.checking": "알림 설정 확인 중…",
15
+ },
16
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ en: {
3
+ "profile.nickname.default": "Me",
4
+ "profile.nickname.label": "Profile nickname",
5
+ // The default names follow the UI language like the gateway's (src/profile.ts); web/profile.js treats the value of either language as the default.
6
+ "profile.agent.default": "Zig",
7
+ },
8
+ ko: {
9
+ "profile.nickname.default": "나",
10
+ "profile.nickname.label": "프로필 닉네임",
11
+ "profile.agent.default": "지그",
12
+ },
13
+ };
@@ -0,0 +1,26 @@
1
+ export default {
2
+ en: {
3
+ "reference.user": "Me",
4
+ "reference.duplicate": "This message is already referenced.",
5
+ "reference.full": "You can add up to {count} references at a time.",
6
+ "reference.locked": "References cannot be changed while images are uploading.",
7
+ "reference.images": "A message can include up to {count} images, including images in referenced messages. Remove references or attachments.",
8
+ "reference.imageCount": "[{count} images]",
9
+ "reference.button": "Reference",
10
+ "reference.buttonTitle": "Add this message as a reference to the input",
11
+ "reference.remove": "Remove reference",
12
+ "reference.removeLabel": "Remove reference: {label}",
13
+ },
14
+ ko: {
15
+ "reference.user": "나",
16
+ "reference.duplicate": "이미 참조한 메시지입니다.",
17
+ "reference.full": "참조는 한 번에 {count}개까지 넣을 수 있습니다.",
18
+ "reference.locked": "이미지를 올리는 동안에는 참조를 바꿀 수 없습니다.",
19
+ "reference.images": "참조한 메시지의 이미지를 포함해 한 번에 {count}장까지 보낼 수 있습니다. 참조나 첨부를 줄여 주세요.",
20
+ "reference.imageCount": "[이미지 {count}장]",
21
+ "reference.button": "참조",
22
+ "reference.buttonTitle": "이 메시지를 입력창에 참조로 넣기",
23
+ "reference.remove": "참조 빼기",
24
+ "reference.removeLabel": "참조 빼기: {label}",
25
+ },
26
+ };
@@ -0,0 +1,40 @@
1
+ export default {
2
+ en: {
3
+ "securityInbox.listChanged": "The list changed. Check again.",
4
+ "securityInbox.allowedReadFailed": "Could not read allowed values. Try again.",
5
+ "securityInbox.deleteUnconfirmed": "Could not confirm the deletion. Check the list again. The deletion will not be retried automatically.",
6
+ "securityInbox.scopeRejected": "The server rejected the change: {error} (the value may be automatic or have an invalid scope).",
7
+ "securityInbox.listRefreshed": "The list changed elsewhere, so the change was not applied. The latest list was loaded.",
8
+ "securityInbox.scopeUnconfirmed": "Could not confirm the repository-allowance change. Check the list again. The change will not be retried automatically.",
9
+ "securityInbox.autoUnavailable": "Automatic security-baseline entries are not available yet. Check the security-baseline list below before changing repository allowance.",
10
+ "securityInbox.autoProtected": "This value matches an automatic entry (home folder path or account name), so repository allowance cannot be enabled. Repository scans will continue to detect it.",
11
+ "securityInbox.rejectEmpty": "The value is empty",
12
+ "securityInbox.rejectShort": "The value is shorter than three characters",
13
+ "securityInbox.rejectLoopback": "The value is a loopback address that scans exclude",
14
+ "securityInbox.baselineReadFailed": "Could not read the security baseline. Try again.",
15
+ "securityInbox.termDuplicate": "This value is already in the baseline.",
16
+ "securityInbox.termRejected": "The server rejected the value: {reason} ({detail}).",
17
+ "securityInbox.rejectDefault": "This value cannot be added to the baseline",
18
+ "securityInbox.enterTerm": "Enter a value to add.",
19
+ "securityInbox.addUnconfirmed": "Could not confirm the addition. Check the list again. The addition will not be retried automatically.",
20
+ },
21
+ ko: {
22
+ "securityInbox.listChanged": "목록이 변경됐습니다. 다시 확인하세요.",
23
+ "securityInbox.allowedReadFailed": "허용 값을 확인하지 못했습니다. 다시 시도하세요.",
24
+ "securityInbox.deleteUnconfirmed": "삭제 결과를 확인하지 못했습니다. 목록을 다시 확인하세요. 삭제를 자동 반복하지 않습니다.",
25
+ "securityInbox.scopeRejected": "서버가 거부했습니다: {error} (자동 항목과 같거나 쓸 수 없는 범위일 수 있습니다).",
26
+ "securityInbox.listRefreshed": "다른 곳에서 목록이 바뀌어 변경을 반영하지 않았습니다. 최신 목록을 다시 읽었습니다.",
27
+ "securityInbox.scopeUnconfirmed": "저장소 허용 변경 결과를 확인하지 못했습니다. 목록을 다시 확인하세요. 변경을 자동 반복하지 않습니다.",
28
+ "securityInbox.autoUnavailable": "보안 기준의 자동 항목을 아직 읽지 못했습니다. 저장소 허용을 바꾸기 전에 아래 보안 기준 목록을 확인하세요.",
29
+ "securityInbox.autoProtected": "자동 항목(홈 폴더 경로·계정 이름)과 같은 값이라 저장소 허용을 켤 수 없습니다. 저장소 검사는 이 값을 계속 찾습니다.",
30
+ "securityInbox.rejectEmpty": "빈 값입니다",
31
+ "securityInbox.rejectShort": "3글자보다 짧습니다",
32
+ "securityInbox.rejectLoopback": "루프백 주소처럼 검사에서 제외되는 값입니다",
33
+ "securityInbox.baselineReadFailed": "보안 기준을 확인하지 못했습니다. 다시 시도하세요.",
34
+ "securityInbox.termDuplicate": "이미 기준에 있는 값입니다.",
35
+ "securityInbox.termRejected": "서버가 값을 거부했습니다: {reason} ({detail}).",
36
+ "securityInbox.rejectDefault": "기준에 넣을 수 없는 값입니다",
37
+ "securityInbox.enterTerm": "추가할 값을 입력하세요.",
38
+ "securityInbox.addUnconfirmed": "추가 결과를 확인하지 못했습니다. 목록을 다시 확인하세요. 추가를 자동 반복하지 않습니다.",
39
+ },
40
+ };
@@ -0,0 +1,38 @@
1
+ export default {
2
+ en: {
3
+ "slash.ttsOn": "Turn on spoken replies",
4
+ "slash.ttsOff": "Turn off spoken replies",
5
+ "slash.wakeOn": "Turn on wake-word listening (Hey Zig)",
6
+ "slash.wakeOff": "Turn off wake-word listening (Hey Zig)",
7
+ "slash.clapOn": "Turn on clap detection",
8
+ "slash.clapOff": "Turn off clap detection",
9
+ "slash.voiceOn": "Turn on voice features (start the voice server)",
10
+ "slash.voiceOff": "Turn off voice features (stop the voice server)",
11
+ "slash.options": "Show voice, spoken replies, wake-word, and clap settings",
12
+ "slash.read": "Read the full last reply aloud",
13
+ "slash.devices": "List registered devices (Mac only)",
14
+ "slash.deviceDelete": "Remove a registered device by number, e.g. /기기 삭제 2 (Mac only)",
15
+ "slash.skill": "Skill",
16
+ "slash.command": "Command",
17
+ "slash.template": "Template",
18
+ "slash.webZig": "Web Zig",
19
+ },
20
+ ko: {
21
+ "slash.ttsOn": "음성 답변 켜기",
22
+ "slash.ttsOff": "음성 답변 끄기",
23
+ "slash.wakeOn": "호출어(헤이 지그) 대기 켜기",
24
+ "slash.wakeOff": "호출어(헤이 지그) 대기 끄기",
25
+ "slash.clapOn": "박수 감지 켜기",
26
+ "slash.clapOff": "박수 감지 끄기",
27
+ "slash.voiceOn": "음성 기능 켜기(음성 서버 시작)",
28
+ "slash.voiceOff": "음성 기능 끄기(음성 서버 정지)",
29
+ "slash.options": "음성 기능·음성 답변·호출어·박수 감지 상태 보기",
30
+ "slash.read": "마지막 답변 전체를 음성으로 읽기",
31
+ "slash.devices": "등록된 기기 목록 (맥에서만)",
32
+ "slash.deviceDelete": "번호로 등록된 기기 삭제, 예: /기기 삭제 2 (맥에서만)",
33
+ "slash.skill": "스킬",
34
+ "slash.command": "명령",
35
+ "slash.template": "템플릿",
36
+ "slash.webZig": "웹 지그",
37
+ },
38
+ };
@@ -0,0 +1,5 @@
1
+ // The turn statistics line of web/turn-stats.js.
2
+ export default {
3
+ en: { "turnStats.model": "Model time {seconds} s", "turnStats.cache": "Cache hits {percent}%" },
4
+ ko: { "turnStats.model": "모델 처리 {seconds}초", "turnStats.cache": "캐시 적중 {percent}%" },
5
+ };
@@ -0,0 +1,42 @@
1
+ export default {
2
+ en: {
3
+ "update.notChecked": "No update check has run yet.",
4
+ "update.upToDate": "You are using the latest version.",
5
+ "update.checkout": "This installation is deployed from a repository. In-app updates are only available for npm installations.",
6
+ "update.restartNotice": "The gateway and bridge will restart during the update. Connections and active conversations or tasks may pause briefly.",
7
+ "update.noticeLine": "Version {latest} is available (current: {current}).",
8
+ "update.currentUnknown": "Current version: unknown",
9
+ "update.current": "Current version: {version}",
10
+ "update.latest": "Latest version: {version}",
11
+ "update.lastCheckFailed": "The last check failed ({error}). Press Check now to try again.",
12
+ "update.checkFailed": "Could not check for updates ({error}). Press Check now to try again.",
13
+ "update.gatewayNoResponse": "no gateway response",
14
+ "update.questionsPending": "{count} question(s) are waiting for an answer. Answer them in Confirmed first. You can update after that.",
15
+ "update.requestFailed": "Update request failed (HTTP {status})",
16
+ "update.started": "Update started: {from} → {to}",
17
+ "update.completed": "Update completed: {from} → {to}. Reload the page.",
18
+ "update.failed": "Update failed.",
19
+ "update.logPath": "(log: {path})",
20
+ "update.progress": "Updating ({from} → {to}): {label}",
21
+ },
22
+ ko: {
23
+ "update.notChecked": "아직 새 버전을 확인하지 않았습니다.",
24
+ "update.upToDate": "최신 버전을 쓰고 있습니다.",
25
+ "update.checkout": "이 설치는 저장소에서 배포합니다. 앱 안 업데이트는 npm 설치에서만 쓸 수 있습니다.",
26
+ "update.restartNotice": "업데이트하면 게이트웨이와 브리지가 다시 시작됩니다. 연결과 진행 중인 대화나 작업이 잠시 끊길 수 있습니다.",
27
+ "update.noticeLine": "새 버전 {latest}이 나왔습니다 (현재 {current}).",
28
+ "update.currentUnknown": "현재 버전: 알 수 없음",
29
+ "update.current": "현재 버전: {version}",
30
+ "update.latest": "새 버전: {version}",
31
+ "update.lastCheckFailed": "마지막 확인에 실패했습니다 ({error}). 지금 확인을 눌러 다시 시도하세요.",
32
+ "update.checkFailed": "새 버전을 확인하지 못했습니다 ({error}). 지금 확인을 눌러 다시 시도하세요.",
33
+ "update.gatewayNoResponse": "게이트웨이 응답 없음",
34
+ "update.questionsPending": "답을 기다리는 질문이 {count}개 있습니다. 확인함에서 먼저 답해 주세요. 답한 뒤 업데이트할 수 있습니다.",
35
+ "update.requestFailed": "업데이트 요청 실패 (HTTP {status})",
36
+ "update.started": "업데이트를 시작했습니다: {from} → {to}",
37
+ "update.completed": "업데이트를 마쳤습니다: {from} → {to}. 페이지를 새로 고치세요.",
38
+ "update.failed": "업데이트에 실패했습니다.",
39
+ "update.logPath": "(로그: {path})",
40
+ "update.progress": "업데이트 중 ({from} → {to}): {label}",
41
+ },
42
+ };
@@ -0,0 +1,28 @@
1
+ export default {
2
+ en: {
3
+ "voice.disabledLine": "Voice is off",
4
+ "voice.offStatusLine": "Options: voice off (voice replies, wake word and clap detection are unavailable)",
5
+ "voice.toggle.on": "Voice on",
6
+ "voice.toggle.off": "Voice off",
7
+ "voice.install.running": "Installing voice {index}/{total}: {message}",
8
+ "voice.install.size": "about {gb} GB",
9
+ "voice.install.retry": "Retry voice install ({size})",
10
+ "voice.install.start": "Install voice ({size})",
11
+ "voice.state.on": "on",
12
+ "voice.state.off": "off",
13
+ "voice.status.line": "Options: voice on · voice replies {tts} · wake word {wake} · clap detection {clap}",
14
+ },
15
+ ko: {
16
+ "voice.disabledLine": "음성 기능이 꺼져 있습니다",
17
+ "voice.offStatusLine": "옵션: 음성 꺼짐 (음성 답변·호출어·박수 감지를 쓸 수 없습니다)",
18
+ "voice.toggle.on": "음성 켜짐",
19
+ "voice.toggle.off": "음성 꺼짐",
20
+ "voice.install.running": "음성 설치 중 {index}/{total}: {message}",
21
+ "voice.install.size": "약 {gb}GB",
22
+ "voice.install.retry": "음성 설치 다시 시도 ({size})",
23
+ "voice.install.start": "음성 설치 ({size})",
24
+ "voice.state.on": "켜짐",
25
+ "voice.state.off": "꺼짐",
26
+ "voice.status.line": "옵션: 음성 켜짐 · 음성 답변 {tts} · 호출어 {wake} · 박수 감지 {clap}",
27
+ },
28
+ };
@@ -0,0 +1,16 @@
1
+ // The wake word and cancel words web/wake.js listens for. Both sides are matched whatever the UI language is (speech
2
+ // can be either), so wake.js reads both sides of this dictionary. A value is a comma list.
3
+ export default {
4
+ en: {
5
+ "wake.hey": "hey,hi",
6
+ "wake.hey.triple": "hey",
7
+ "wake.name": "zig,jig",
8
+ "wake.cancel": "cancel,stop",
9
+ },
10
+ ko: {
11
+ "wake.hey": "헤이,해이,헤의,해의,에이",
12
+ "wake.hey.triple": "헤이,해이,헤의,해의,에이",
13
+ "wake.name": "지그,지급,지구,지기,찌그,치그,지크,직",
14
+ "wake.cancel": "취소,그만,아니야,아니,멈춰",
15
+ },
16
+ };
@@ -0,0 +1,10 @@
1
+ export default {
2
+ en: {
3
+ "workid.copied": "ID copied",
4
+ "workid.copyFailed": "Copy failed",
5
+ },
6
+ ko: {
7
+ "workid.copied": "ID 복사함",
8
+ "workid.copyFailed": "복사 실패",
9
+ },
10
+ };
@@ -0,0 +1,20 @@
1
+ export default {
2
+ en: {
3
+ "workstatus.fold.expand": "Show work status", "workstatus.fold.hide": "Hide work status",
4
+ "workstatus.state.running": "Running", "workstatus.state.waiting_input": "Waiting for input", "workstatus.state.waiting": "Waiting", "workstatus.state.unresponsive": "Unresponsive", "workstatus.state.broken": "Failed", "workstatus.state.unknown": "Checking", "workstatus.state.idle": "Idle", "workstatus.state.completed": "Completed", "workstatus.state.failed": "Failed", "workstatus.state.cancelled": "Cancelled", "workstatus.state.children": "Waiting for child tasks",
5
+ "workstatus.kind.session": "Session", "workstatus.kind.work": "Work",
6
+ "workstatus.time.now": "Just now", "workstatus.time.minutes": "{count} min ago", "workstatus.time.hours": "{count} hr ago", "workstatus.time.days": "{count} days ago",
7
+ "workstatus.caption.unavailable": "Checking work status…", "workstatus.caption.empty": "No active work",
8
+ "workstatus.user": "User", "workstatus.user.me": "Me", "workstatus.slot.empty": "Empty", "workstatus.slot.clear": "Select to clear this slot.", "workstatus.slot.clear.short": "Select to clear",
9
+ "workstatus.queued.badge": "+{count} waiting", "workstatus.queued.title": "{count} work items waiting for a free slot", "workstatus.queued.item": "{count} work items waiting for a free slot",
10
+ },
11
+ ko: {
12
+ "workstatus.fold.expand": "작업 상태 펼치기", "workstatus.fold.hide": "작업 상태 숨기기",
13
+ "workstatus.state.running": "진행 중", "workstatus.state.waiting_input": "입력 대기", "workstatus.state.waiting": "대기 중", "workstatus.state.unresponsive": "응답 없음", "workstatus.state.broken": "고장", "workstatus.state.unknown": "확인 중", "workstatus.state.idle": "쉬는 중", "workstatus.state.completed": "완료", "workstatus.state.failed": "실패", "workstatus.state.cancelled": "취소됨", "workstatus.state.children": "하위 작업 대기",
14
+ "workstatus.kind.session": "세션", "workstatus.kind.work": "작업",
15
+ "workstatus.time.now": "방금", "workstatus.time.minutes": "{count}분 전", "workstatus.time.hours": "{count}시간 전", "workstatus.time.days": "{count}일 전",
16
+ "workstatus.caption.unavailable": "작업 상태를 확인하는 중입니다.", "workstatus.caption.empty": "지금 하고 있는 작업이 없습니다.",
17
+ "workstatus.user": "사용자", "workstatus.user.me": "나", "workstatus.slot.empty": "빈 칸", "workstatus.slot.clear": "누르면 이 칸을 비웁니다.", "workstatus.slot.clear.short": "눌러 비우기",
18
+ "workstatus.queued.badge": "+{count} 대기", "workstatus.queued.title": "빈 칸을 기다리는 작업 {count}개", "workstatus.queued.item": "빈 칸을 기다리는 작업 {count}개",
19
+ },
20
+ };
package/web/i18n.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type UiLanguage = "en" | "ko";
2
+ export type Strings = Readonly<Record<string, string>>;
3
+ export type Dictionary = { readonly en: Strings; readonly ko: Strings };
4
+ export type I18nElement = {
5
+ getAttribute(name: string): string | null;
6
+ setAttribute(name: string, value: string): void;
7
+ textContent: string | null;
8
+ };
9
+ export type I18nRoot = {
10
+ querySelectorAll(selector: string): Iterable<I18nElement>;
11
+ matches?(selector: string): boolean;
12
+ };
13
+ export const UI_LANGUAGES: readonly UiLanguage[];
14
+ export function isUiLanguage(value: unknown): value is UiLanguage;
15
+ export function registerStrings(dict: Partial<Dictionary>): void;
16
+ export function getLanguage(): UiLanguage;
17
+ export function t(key: string, params?: Readonly<Record<string, string | number>>): string;
18
+ export function applyI18n(root?: I18nRoot | null): void;
19
+ export function setLanguage(lang: unknown, doc?: (I18nRoot & { documentElement?: { lang: string } }) | null): boolean;
20
+ export function onLanguageChange(fn: (lang: UiLanguage) => void): () => boolean;
package/web/i18n.js ADDED
@@ -0,0 +1,63 @@
1
+ // The web UI language (English or Korean). Each area keeps its strings in web/i18n/<area>.js
2
+ // (`export default { en: {...}, ko: {...} }`) and registers them here with registerStrings, so this file never lists them.
3
+
4
+ export const UI_LANGUAGES = ["en", "ko"];
5
+
6
+ const strings = { en: {}, ko: {} };
7
+ const listeners = new Set();
8
+ let current = "en";
9
+
10
+ export function isUiLanguage(value) {
11
+ return UI_LANGUAGES.includes(value);
12
+ }
13
+
14
+ export function registerStrings(dict) {
15
+ for (const lang of UI_LANGUAGES) Object.assign(strings[lang], dict?.[lang] ?? {});
16
+ }
17
+
18
+ export function getLanguage() {
19
+ return current;
20
+ }
21
+
22
+ export function t(key, params = {}) {
23
+ const text = strings[current][key] ?? strings.en[key] ?? key;
24
+ return text.replace(/\{(\w+)\}/g, (placeholder, name) => Object.hasOwn(params, name) ? String(params[name]) : placeholder);
25
+ }
26
+
27
+ function translate(el) {
28
+ const text = el.getAttribute("data-i18n");
29
+ if (text) el.textContent = t(text);
30
+ const tip = el.getAttribute("data-i18n-tip");
31
+ if (tip) el.setAttribute("data-tip", t(tip));
32
+ const attrs = el.getAttribute("data-i18n-attr");
33
+ if (!attrs) return;
34
+ for (const pair of attrs.split(";")) {
35
+ const at = pair.indexOf(":");
36
+ if (at <= 0) continue;
37
+ const name = pair.slice(0, at).trim();
38
+ const key = pair.slice(at + 1).trim();
39
+ if (name && key) el.setAttribute(name, t(key));
40
+ }
41
+ }
42
+
43
+ const SELECTOR = "[data-i18n],[data-i18n-tip],[data-i18n-attr]";
44
+
45
+ export function applyI18n(root = globalThis.document) {
46
+ if (!root) return;
47
+ if (typeof root.matches === "function" && root.matches(SELECTOR)) translate(root);
48
+ for (const el of root.querySelectorAll(SELECTOR)) translate(el);
49
+ }
50
+
51
+ export function setLanguage(lang, doc = globalThis.document) {
52
+ if (!isUiLanguage(lang)) return false;
53
+ current = lang;
54
+ if (doc?.documentElement) doc.documentElement.lang = lang;
55
+ applyI18n(doc);
56
+ for (const fn of [...listeners]) fn(lang);
57
+ return true;
58
+ }
59
+
60
+ export function onLanguageChange(fn) {
61
+ listeners.add(fn);
62
+ return () => listeners.delete(fn);
63
+ }