SLOPSHOPPER

korean-ui

Claude Code 화면의 영문(답변, 스피너, 입력창 힌트, 도구 묶음 줄, 안내 줄, 작업 알림, /config 메뉴, 명령 설명)을 한국어로 보여 준다

newspinnerrowspromptmodeltimer
v0.1.0no licenseupdated 2026-10-12jaeseongs95/claude-code-mods/korean-ui
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · korean-ui
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ 42초 동안 생각함 ✻ 궁리하는 중… ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Turn line
✻ 42초 동안 생각함
README

claude-code-mods

Claude Code mod 플러그인 두 개와, 이를 설치하는 마켓플레이스 local-mods를 담은 저장소입니다.

  • usage-line: 입력창 바로 위에 작업 상태와 사용량을 두 줄 띠로 보여 줍니다.
  • korean-ui: 화면에 보이는 영문을 한국어로 바꿉니다. 아래 절 참고.

usage-line

~/.claude/statusline-usage.sh 상태 줄에 나오던 정보를 mod API로 다시 그립니다. 스크립트를 실행하지는 않으며, 스크립트에 있던 vim 모드 표시는 mod API로 읽을 수 없어 뺐습니다.

mod(function hooks) API는 early access 기능이라 Claude Code 릴리스에 따라 바뀔 수 있습니다.

화면 구성

proj on  main [+1 ?3]  │  Opus 5.5 ⚡high  [status] [context] [diff] [compact]
  ctx 42% 84k/200k · cache 87%  │  $1.23  │  5h 30% →14:00 · 7d 12% · Fable 5%

1줄: 어디서, 무엇으로

  • 폴더 이름과 git 브랜치. 브랜치 뒤 괄호에는 스테이징(+), 수정(!), 미추적(?), ahead(⇡), behind(⇣) 개수를 0이 아닌 것만 표시합니다. detached 상태면 커밋 해시 앞 7자리를 보여 줍니다.
  • 모델 표시 이름(claude-opus-5-5 → Opus 5.5)과 effort. effort는 마지막 모델 요청 값을 쓰고, 아직 요청이 없으면 settings의 modelSettings[모델 ID].effortLevel, 그것도 없으면 effortLevel을 씁니다.
  • [status], [context] 버튼은 /status, /context를, [compact] 버튼은 이어서 작업할 수 있게 요약하라는 지시문을 붙여 /compact를 실행합니다. 응답 중에 누르면 응답이 끝난 뒤 실행됩니다.
  • 필요할 때만 보이는 버튼: 스테이징·수정한 파일이 있으면 [diff](/diff), 5h나 7d 사용률이 70% 이상이면 [usage](/usage)가 [compact] 앞에 나타납니다. 미추적 파일만 있을 때는 [diff]를 보이지 않습니다.
  • 버튼은 폭이 좁아도 줄어들지 않고, 대신 폴더·모델 쪽 글자가 …로 잘립니다. 버튼을 [ label ]로 그리면 폴더·모델에 40칸이 남지 않을 만큼 좁을 때는 [compact]를 뺀 버튼을 테두리 없는 흐린 라벨로 그립니다. 마우스를 올리거나 포커스가 가면 진하게 보입니다.

2줄: 얼마나

  • ctx: 컨텍스트 사용률과 토큰 수(사용/창 크기)
  • cache: 프롬프트 캐시 적중률. 이 대화의 메인 스레드 요청에서 누적한 cache_read 토큰을 전체 입력 토큰으로 나눈 값입니다. /clear로 세션 id가 바뀌거나 Claude Code를 새로 시작하면(resume 포함) 0부터 다시 셉니다.
  • 세션 비용(USD)
  • 5h(초기화 시각 포함), 7d 사용 한도와 모델별 주간 한도. 모델별 한도는 상태 줄 입력에 없어서 Claude Code가 ~/.claude.json에 캐시해 둔 값을 읽으며, 다른 계정의 캐시이거나 24시간 넘게 갱신되지 않았으면 표시하지 않습니다.

라벨과 부가 정보는 흐리게, 값은 기본 글자색으로 그립니다. 사용률은 한도에 가까울 때만 색을 바꿉니다.

항목노랑굵은 빨강
ctx60% 이상80% 이상
5h, 7d, 모델별 한도70% 이상90% 이상

띠에 두 줄을 그릴 자리가 없으면 한 줄로 합칩니다.

갱신 시점

  • 세션 시작 시, 그리고 그 뒤 10초마다(대기 중이거나 Claude 밖에서 git 상태가 바뀐 경우 반영)
  • 사용량이 다시 측정될 때(session.measure)
  • 메인 스레드의 턴이 끝날 때

git 정보는 git status --porcelain=v2 --branch 한 번으로 읽으므로 git이 PATH에 있어야 합니다. git 저장소가 아닌 폴더에서는 git 구간을 표시하지 않습니다.

korean-ui

플러그인 API가 바꿀 수 있게 열어 둔 화면 요소만 한국어로 바꿉니다.

위치원래바뀐 뒤
Claude 답변·질문·작업 목록영어로 답할 때가 있음시스템 프롬프트에 지시를 붙여 한국어로 답하게 함
작업 중 표시Sauteing…궁리하는 중…
턴 종료 줄✻ Baked for 3s✻ 3초 동안 궁리함
입력창 아래 힌트? for shortcuts, esc to interrupt? 단축키, esc 중단
모드 표시plan mode on계획 모드 켜짐
접힌 도구 묶음Read 3 files, ran 2 shell commands파일 3개 읽음, 명령 2개 실행 (ctrl+o 펼치기)
/config 메뉴 항목Auto-compact, Show tips자동 압축, 팁 표시, 사전에 없는 항목은 모델 번역
/ 자동완성 명령 설명Start a new session …빈 컨텍스트로 새 세션 시작 …
대화 중 엔진 안내 줄Conversation compacted, Session resumed without branch: main대화를 압축함, 그 밖의 문장은 모델 번역
백그라운드 작업 알림 줄Task … completed 같은 요약모델 번역
스킬·플러그인 명령 설명, 사전에 없는 기본 명령 설명/ 메뉴의 영문 설명모델 번역

번역은 두 단계로 합니다.

  1. 고정 사전: 입력창 힌트, 스피너, 모드 표시, /config 항목, 기본 명령 설명처럼 문구가 정해진 곳은 register.tsx·dict.ts의 사전으로 바로 바꿉니다. 사전 문구는 Claude Code 2.1.296 실행 파일에서 확인했습니다. 키 이름 뒤에 동작이 붙는 힌트(esc to close, / to search)는 키 + 동작 규칙으로 바꿉니다.
  2. 모델 번역: 안내 줄, 작업 알림, 스킬·플러그인 명령 설명, 사전에 없는 기본 명령 설명과 /config 항목 이름처럼 종류가 많거나 값이 섞여 드는 문장은 haiku 모델로 번역합니다. 결과는 $.store에 최근 500개까지 남겨 다시 쓰므로 같은 문장은 한 번만 번역합니다.
  3. 명령 설명과 /config 항목 이름은 세션이 시작되고 3초 뒤 한 번에 번역합니다(10개씩 묶어 요청). 처음 설치한 세션이나 새 스킬·새 버전의 문구가 생겼을 때만 토큰이 듭니다.
  4. 안내 줄과 작업 알림은 처음 나올 때 번역이 끝날 때까지 1~3초 늦게 표시될 수 있습니다. 번역이 실패하면 원문을 그대로 보여 주고 다음에 다시 시도합니다.
  5. 모델이 읽는 대화 내용은 바꾸지 않습니다. 안내 줄은 모델이 읽지 않는 행이고, 작업 알림은 화면에 그리는 글자만 바꿉니다.

사전에도 없고 모델 번역 대상도 아닌 문장은 섞인 문장이 되지 않도록 원문 그대로 둡니다. 코드, 명령어, 경로, 식별자는 번역하지 않습니다.

바꾸지 못하는 것: 시작 로고, 권한 확인 창, 질문 창의 고정 버튼(Other, Submit), /status·/usage 창, /config의 탭 이름과 설정값, 도구 실행 줄(Bash(...))과 결과 요약(Read 120 lines). 이 화면들은 플러그인 API에 바꿀 수 있는 지점이 없습니다.

/config 항목 이름과 기본 명령 설명은 Claude Code 2.1.296에서 뽑은 영문을 키로 씁니다. 버전이 바뀌어 사전에 없는 문구가 생기면 모델 번역으로 채우고, 번역을 고정하고 싶을 때만 korean-ui/hooks/dict.ts에 추가합니다. 입력창 힌트·스피너·모드 표시는 자주 다시 그려지는 짧은 문구라 사전으로만 바꾸므로, 새 문구는 영문으로 남습니다.

한국어 팁

스피너 아래 Tip: 줄은 플러그인 훅이 닿지 않아, Claude Code의 spinnerTipsOverride 설정으로 바꿉니다. korean-ui/tips.json에 2.1.291·2.1.296의 기본 팁을 옮긴 54개가 있습니다. 맥 전용, 기업·외부 클라우드 전용, 그때그때 값이 바뀌는 팁은 뺐습니다. ~/.claude/settings.json에 다음을 넣으면 기본 영어 팁 대신 이 목록이 팁: 머리말로 나옵니다.

"spinnerTipsOverride": {
  "tipsFile": "~/.claude/mods/korean-ui/tips.json",
  "excludeDefault": true,
  "label": "팁"
}

팁은 파일에서 바로 읽으므로 저장소를 git pull하면 갱신됩니다. Claude Code에 새 팁이 생기면 이 파일에 직접 추가해야 합니다(최대 200개, 각 500자).

설치

저장소를 내려받아 로컬 마켓플레이스로 추가한 뒤 플러그인을 설치합니다.

git clone https://github.com/jaeseongs95/claude-code-mods.git ~/.claude/mods
claude plugin marketplace add ~/.claude/mods
claude plugin install usage-line@local-mods
claude plugin install korean-ui@local-mods

필요한 플러그인만 설치해도 됩니다.

구조

.claude-plugin/marketplace.json   마켓플레이스 local-mods 정의
usage-line/
  .claude-plugin/plugin.json      플러그인 매니페스트
  hooks/hooks.json                훅 모듈 목록
  hooks/register.tsx              띠 그리기와 갱신 로직
  hooks/usage-line.test.ts        테스트
  types/index.d.ts                state 타입 선언
korean-ui/
  .claude-plugin/plugin.json      플러그인 매니페스트
  hooks/hooks.json                훅 모듈 목록
  hooks/register.tsx              화면 요소별 한글화 훅
  hooks/dict.ts                   /config 항목 이름·명령 설명 사전
  hooks/translate.ts              모델 번역 프롬프트와 응답 해석
  hooks/register.test.ts          테스트
  tips.json                       spinnerTipsOverride용 한국어 팁 목록

각 플러그인의 .claude-plugin/types/는 Claude Code가 mod를 불러올 때 생성하는 API 타입 선언이라 저장소에 포함하지 않습니다.

검사와 테스트

claude plugin validate usage-line
claude plugin test usage-line
claude plugin validate korean-ui
claude plugin test korean-ui
Source 3 files
hooks/register.tsx 309 lines
1import type { EngineInterface, Register, ToolGroupCall } from 'claude-code'
2
3import { COMMAND_DESCRIPTIONS, CONFIG_LABELS } from './dict'
4import { BATCH, needsTranslation, parseArray, SYSTEM, TRANSLATIONS_KEY, trim } from './translate'
5
6// 문자열 전체가 이 문구일 때만 바꾼다(문장 속 같은 단어는 건드리지 않는다).
7// 엔진 문구는 2.1.291 실행 파일에서 뽑고 2.1.296에서 다시 확인한 것이다. 끝의 말줄임표(…)는 떼고 찾는다.
8const WORDS: Record<string, string> = {
9  focus: '집중',
10  'memory paused': '메모리 일시 정지',
11  interrupted: '중단됨',
12  thinking: '생각 중',
13  waiting: '대기 중',
14  running: '실행 중',
15  retrying: '다시 시도하는 중',
16  summarizing: '요약하는 중',
17  'compacting conversation': '대화 압축 중',
18  'waiting for api response': 'API 응답 기다리는 중',
19  'esc again to clear': 'Esc를 한 번 더 누르면 지움',
20  'draft restored': '쓰던 글 복원됨',
21  'error compacting conversation': '대화 압축 오류',
22  'compaction blocked by precompact hook': 'PreCompact 훅이 압축을 막음',
23  'no background agents running': '실행 중인 백그라운드 에이전트 없음',
24  'ctrl+y to paste deleted text': 'Ctrl+Y로 지운 글 붙여넣기',
25  'plugins changed. run /reload-plugins to activate.': '플러그인이 바뀜. /reload-plugins로 적용하세요.',
26  'deeper reasoning requested for this turn': '이번 턴에 더 깊은 추론 요청됨',
27  'this prompt will launch an ultraplan cloud session': '이 프롬프트는 ultraplan 클라우드 세션을 시작함',
28  'clear the input to edit this queued shell command': '대기 중인 셸 명령을 고치려면 입력창을 비우세요',
29  'press up to edit queued messages': '↑ 키로 대기 중인 메시지 편집',
30  'press up to edit queued messages, enter to send them immediately': '↑ 키로 대기 중인 메시지 편집, Enter로 바로 보내기',
31  'press up to select a queued message to edit, or enter to send them now': '↑ 키로 고칠 메시지 선택, Enter로 지금 보내기',
32  'showing detailed transcript': '자세한 대화 기록 표시 중',
33  'conversation compacted': '대화를 압축함',
34  'session resumed': '세션 재개됨',
35  'session completed successfully': '세션이 끝남',
36}
37
38// 키 이름(ctrl+o, esc, enter, ↑ 등) 뒤에 오는 동작. 긴 것부터 맞춘다.
39const ACTIONS: Record<string, string> = {
40  'interrupt and send': '중단하고 보내기',
41  'run in background': '백그라운드 실행',
42  'show todos': '할 일 보기',
43  'hide todos': '할 일 숨기기',
44  'show tasks': '작업 보기',
45  'hide tasks': '작업 숨기기',
46  'go back': '뒤로',
47  'open in editor': '편집기에서 열기',
48  'switch mode': '모드 전환',
49  interrupt: '중단',
50  expand: '펼치기',
51  collapse: '접기',
52  cycle: '전환',
53  cancel: '취소',
54  edit: '편집',
55  confirm: '확인',
56  select: '선택',
57  close: '닫기',
58  continue: '계속',
59  save: '저장',
60  dismiss: '닫기',
61  view: '보기',
62  toggle: '전환',
63  stop: '중지',
64  delete: '삭제',
65  copy: '복사',
66  send: '보내기',
67  keep: '유지',
68  clear: '지우기',
69  back: '뒤로',
70  add: '추가',
71  skip: '건너뛰기',
72  open: '열기',
73  search: '검색',
74  retry: '다시 시도',
75  queue: '대기열에 넣기',
76  navigate: '이동',
77  submit: '제출',
78  undo: '되돌리기',
79  apply: '적용',
80  resume: '재개',
81  exit: '종료',
82}
83
84// 단어 중간(setup의 up 등)에서 시작하지 않도록 앞을 막는다.
85const KEY =
86  '(?<![\\w+-])((?:ctrl|shift|alt|option|cmd|meta)[+-]\\S+|(?:esc|escape|tab|enter|return|space|up|down|left|right|backspace)\\b|[↑↓←→/?!@#])'
87const key = (pattern: string) => new RegExp(pattern.replaceAll('KEY', KEY), 'gi')
88const actions = Object.keys(ACTIONS).sort((a, b) => b.length - a.length).join('|')
89
90// 엔진이 그리는 고정 문구. 앞에서부터 차례로 바꾸고, 사전에 없는 문장은 그대로 둔다.
91const PHRASES: ReadonlyArray<readonly [RegExp, string | ((...m: string[]) => string)]> = [
92  [/\? for shortcuts/gi, '? 단축키'],
93  [key('press KEY again to exit'), '종료하려면 $1 한 번 더'],
94  [key('press KEY again to clear'), '지우려면 $1 한 번 더'],
95  [key('KEY then KEY'), '$1 다음 $2'],
96  [key(`KEY to (${actions})\\b`), (_, k, action) => `${k} ${ACTIONS[action!.toLowerCase()]}`],
97  [/accept edits on/gi, '편집 자동 수락 켜짐'],
98  [/plan mode on/gi, '계획 모드 켜짐'],
99  [/bypass permissions on/gi, '권한 확인 생략 켜짐'],
100  [/auto mode on/gi, '자동 모드 켜짐'],
101  // /cost
102  [/total cost:/gi, '총 비용:'],
103  [/total duration \(API\):/gi, '총 소요 시간(API):'],
104  [/total duration \(wall\):/gi, '총 소요 시간(실제):'],
105  [/total code changes:/gi, '총 코드 변경:'],
106  [/(\d+) lines? added/gi, '$1줄 추가'],
107  [/(\d+) lines? removed/gi, '$1줄 삭제'],
108  [/usage by model:/gi, '모델별 사용량:'],
109  [/([\d.,]+[kmb]?) input\b/gi, '입력 $1'],
110  [/([\d.,]+[kmb]?) output\b/gi, '출력 $1'],
111  [/([\d.,]+[kmb]?) cache read\b/gi, '캐시 읽기 $1'],
112  [/([\d.,]+[kmb]?) cache write\b/gi, '캐시 쓰기 $1'],
113]
114
115export const ko = (text: string): string => {
116  const bare = text.trim().replace(/(…|\.\.\.)$/, '')
117  const word = WORDS[bare.toLowerCase()]
118  if (word) return word + (bare.length < text.trim().length ? '…' : '')
119  return PHRASES.reduce((s, [from, to]) => (typeof to === 'string' ? s.replace(from, to) : s.replace(from, to)), text)
120}
121
122// 사전에 없는 문장(안내 줄, 작업 알림, 스킬 설명)은 모델로 번역해 $.store에 남겨 다시 쓴다.
123const loadCache = async ($: EngineInterface) => ((await $.store.get(TRANSLATIONS_KEY)) ?? {}) as Record<string, string>
124
125const translateMany = async ($: EngineInterface, texts: readonly string[]): Promise<string[]> => {
126  const cache = await loadCache($)
127  const missing = [...new Set(texts.filter(t => needsTranslation(t) && !(t in cache)))]
128  let isChanged = false
129  for (let i = 0; i < missing.length; i += BATCH) {
130    const batch = missing.slice(i, i + BATCH)
131    const reply = await $.model
132      .complete({ model: 'haiku', system: SYSTEM, prompt: JSON.stringify(batch), maxTokens: 8000, effort: 'low', timeoutMs: 30_000 })
133      .catch(() => undefined)
134    const out = reply?.isAnswered ? parseArray(reply.text, batch.length) : undefined
135    if (!out) break // 실패하면 원문을 그대로 보여 주고 다음 기회에 다시 시도한다.
136    batch.forEach((t, j) => (cache[t] = out[j]!))
137    isChanged = true
138  }
139  if (isChanged) await $.store.set(TRANSLATIONS_KEY, trim(cache))
140  return texts.map(t => cache[t] ?? t)
141}
142
143// 같은 문장을 동시에 여러 번 요청하지 않도록 진행 중인 요청을 공유한다.
144const pending = new Map<string, Promise<string>>()
145
146// 사전으로 먼저 바꾸고, 못 바꾼 문장만 모델 번역으로 넘긴다.
147const toKorean = ($: EngineInterface, text: string): Promise<string> => {
148  const fixed = ko(text)
149  if (fixed !== text || !needsTranslation(text)) return Promise.resolve(fixed)
150  const running = pending.get(text)
151  if (running) return running
152  const job = translateMany($, [text])
153    .then(([out]) => out ?? text)
154    .finally(() => pending.delete(text))
155  pending.set(text, job)
156  return job
157}
158
159const SPINNER = ['생각하는 중', '궁리하는 중', '고민하는 중', '정리하는 중', '분석하는 중', '다듬는 중', '짜 맞추는 중', '빚는 중']
160const DONE = ['작업함', '생각함', '궁리함', '정리함', '다듬음', '빚어냄']
161
162// 같은 행은 다시 그려도 같은 단어가 나오도록 id로 고른다.
163const pick = (list: readonly string[], seed: string) => {
164  let n = 0
165  for (const c of seed) n = (n * 31 + c.charCodeAt(0)) >>> 0
166  return list[n % list.length]!
167}
168
169export const duration = (ms: number) => {
170  const s = Math.round(ms / 1000)
171  if (s < 60) return `${s}초`
172  if (s < 3600) return `${Math.floor(s / 60)}분 ${s % 60}초`
173  return `${Math.floor(s / 3600)}시간 ${Math.floor((s % 3600) / 60)}분`
174}
175
176const KINDS: Record<string, string> = {
177  Read: '파일 {n}개 읽음',
178  NotebookRead: '노트북 {n}개 읽음',
179  Grep: '{n}회 검색',
180  Glob: '{n}회 파일 찾기',
181  WebSearch: '웹 {n}회 검색',
182  WebFetch: '웹 페이지 {n}개 가져옴',
183  Bash: '명령 {n}개 실행',
184  PowerShell: '명령 {n}개 실행',
185  LS: '폴더 {n}개 확인',
186}
187
188export const groupLine = (calls: ReadonlyArray<Pick<ToolGroupCall, 'tool'>>) => {
189  const counts = new Map<string, number>()
190  for (const { tool } of calls) counts.set(tool, (counts.get(tool) ?? 0) + 1)
191  return [...counts]
192    .map(([tool, n]) => (KINDS[tool] ?? `${tool} {n}회`).replace('{n}', String(n)))
193    .join(', ')
194}
195
196
197const LANGUAGE = {
198  id: 'korean-ui:language',
199  scope: 'session',
200  text: [
201    '# 응답 언어',
202    '사용자에게 보이는 모든 글(답변, 진행 설명, 질문, 작업 목록, 요약)은 한국어로 쓴다.',
203    '코드, 명령어, 파일 경로, 식별자, URL, 오류 메시지 원문 인용은 번역하지 않는다.',
204  ].join('\n'),
205} as const
206
207export const register: Register = on => {
208  // 답변 본문: 화면에서 번역하지 않고 처음부터 한국어로 쓰게 한다.
209  on('prompt.compose', async ($, e, next) => {
210    const { sections } = await next(e)
211    return { sections: [...sections.filter(s => s.id !== LANGUAGE.id), LANGUAGE] }
212  })
213
214  on('ui.render', { component: 'Spinner' }, ($, e, next) =>
215    next({
216      ...e,
217      props: {
218        ...e.props,
219        word: pick(SPINNER, e.requestId + e.props.word),
220        message: e.props.message === null ? null : ko(e.props.message),
221      },
222    }),
223  )
224
225  on('ui.render', { component: 'TurnDuration' }, ($, e) => {
226    const { Text } = $.ui.resolve(e)
227    return <Text dimColor>✻ {duration(e.props.durationMs)} 동안 {pick(DONE, e.requestId)}</Text>
228  })
229
230  on('ui.render', { component: 'PromptHint' }, ($, e, next) =>
231    e.props.hint ? next({ ...e, props: { ...e.props, hint: ko(e.props.hint) } }) : next(e),
232  )
233
234  on('ui.render', { component: 'ToolProgress' }, ($, e, next) =>
235    next({ ...e, props: { ...e.props, hint: ko(e.props.hint) } }),
236  )
237
238  on('ui.render', { component: 'SessionMode' }, ($, e, next) =>
239    next({ ...e, props: { ...e.props, modes: e.props.modes.map(ko) } }),
240  )
241
242  on('ui.render', { component: 'InfoNotice' }, ($, e, next) =>
243    next({ ...e, props: { ...e.props, text: ko(e.props.text) } }),
244  )
245
246  on('ui.render', { component: 'CommandOutput' }, ($, e, next) =>
247    next({ ...e, props: { ...e.props, text: ko(e.props.text) } }),
248  )
249
250  // 대화 중 엔진 안내 줄(notice). 모델이 읽지 않는 행이라 저장되는 글자를 바꿔도 된다.
251  on('session.append', { door: 'notice' }, async ($, e, next) => {
252    const { message } = e
253    if (message.type !== 'system' || message.name === 'local_command') return next(e)
254    const content = await Promise.all(
255      message.content.map(async block =>
256        block.type === 'text' && typeof block.text === 'string' ? { ...block, text: await toKorean($, block.text) } : block,
257      ),
258    )
259    return next({ ...e, message: { ...message, content } })
260  }).catch(($, e, next) => next(e))
261
262  // 백그라운드 작업 알림 줄. 화면에 그리는 글자만 바꾸고 모델이 읽는 원문은 그대로 둔다.
263  on('ui.render', { component: 'UserMessage' }, async ($, e, next) =>
264    e.props.origin.kind === 'task-notification'
265      ? next({ ...e, props: { ...e.props, text: await toKorean($, e.props.text) } })
266      : next(e),
267  )
268
269  // /config 메뉴는 엔진 화면 그대로 두고 항목 이름만 바꾼다. 사전에 없는 항목은 모델 번역을 쓴다.
270  on('config.describe', async ($, e, next) => {
271    const row = await next(e)
272    return { ...row, label: CONFIG_LABELS[row.label] ?? (await loadCache($))[row.label] ?? row.label }
273  })
274
275  // 사전에 없는 명령 설명(스킬·플러그인, 새 버전의 기본 명령)과 /config 항목 이름은 시작 직후 한 번에
276  // 번역해 두고, 끝나면 메뉴가 다시 묻게 한다. 이미 한국어인 것은 needsTranslation이 거른다.
277  on('session.start', async ($, e, next) => {
278    const work = async () => {
279      const [commands, rows] = await Promise.all([$.command.list(), $.config.list()])
280      await translateMany($, [
281        ...commands.filter(c => !COMMAND_DESCRIPTIONS[c.name]).map(c => c.description),
282        ...rows.filter(r => !CONFIG_LABELS[r.label]).map(r => r.label),
283      ])
284      $.ui.invalidate('command.describe')
285      $.ui.invalidate('config.describe')
286    }
287    $.clock.after(3000, () => void work().catch(() => {}))
288    return next(e)
289  })
290
291  on('command.describe', async ($, e, next) => {
292    const row = await next(e)
293    const description = COMMAND_DESCRIPTIONS[e.command] ?? (await loadCache($))[row.description] ?? row.description
294    return { ...row, description }
295  })
296
297  // 접힌 도구 묶음(`Read 3 files, ran 2 shell commands`)만 한국어 한 줄로 다시 그린다.
298  on('ui.render', { component: 'ToolGroup' }, ($, e, next) => {
299    if (e.props.isExpanded) return next(e)
300    const { Text } = $.ui.resolve(e)
301    return (
302      <Text dimColor>
303        {groupLine(e.props.calls)}
304        {e.props.isActive ? '…' : ''} (ctrl+o 펼치기)
305      </Text>
306    )
307  })
308}
309
hooks/dict.ts 167 lines
1// /config 메뉴 항목 이름. 키는 엔진이 그리는 영문 label 그대로다(2.1.296 실행 파일에서 뽑음).
2export const CONFIG_LABELS: Record<string, string> = {
3  'Push when Claude decides': 'Claude 판단 시 푸시 알림',
4  'Agents view': '에이전트 보기',
5  Artifacts: '아티팩트',
6  'Question auto-continue timeout': '질문 자동 진행 대기 시간',
7  'Auto-compact': '자동 압축',
8  'Auto-connect to IDE (external terminal)': 'IDE 자동 연결(외부 터미널)',
9  'Continue automatically at usage limit': '사용 한도 도달 시 자동 계속',
10  'Auto-install IDE extension': 'IDE 확장 자동 설치',
11  'Auto-update channel': '자동 업데이트 채널',
12  'Rewind code (checkpoints)': '코드 되돌리기(체크포인트)',
13  'Skip the /copy picker': '/copy 선택 창 건너뛰기',
14  'Copy on select': '선택하면 복사',
15  'Messages from your other sessions': '다른 세션에서 온 메시지',
16  'Open agents view by default': '에이전트 보기로 시작',
17  'Start in agent view': '에이전트 보기로 시작',
18  'Default view': '기본 보기',
19  'Dialog expiry': '대화상자 만료',
20  'Diff tool': '비교 도구',
21  'Editor mode': '편집기 모드',
22  'Claude-drafted feedback': 'Claude가 쓴 피드백 초안',
23  'Respect .gitignore in file picker': '파일 선택에서 .gitignore 따르기',
24  'Push when actions required': '조치가 필요할 때 푸시 알림',
25  Language: '언어',
26  Model: '모델',
27  'Claude-proposed goals': 'Claude가 제안하는 목표',
28  'Local notifications': '로컬 알림',
29  Notifications: '알림',
30  'Synced project memory (this directory; applies next session)': '동기화된 프로젝트 메모리(이 폴더, 다음 세션부터 적용)',
31  'Output style': '출력 스타일',
32  'Default permission mode': '기본 권한 모드',
33  'Precompute compaction': '압축 미리 계산',
34  'Terminal progress bar': '터미널 진행률 표시',
35  'Prompt suggestions': '프롬프트 제안',
36  'Session recap': '세션 요약',
37  'Reduce motion': '움직임 줄이기',
38  'Enable Remote Control for all sessions': '모든 세션에서 원격 제어 켜기',
39  "Use this machine's settings in cloud sessions": '클라우드 세션에서 이 컴퓨터 설정 사용',
40  'Show status in terminal tab': '터미널 탭에 상태 표시',
41  Theme: '테마',
42  'Thinking mode': '생각 모드',
43  'Time format': '시간 형식',
44  'Show message timestamps': '메시지 시각 표시',
45  'Show tips': '팁 표시',
46  'Show turn duration': '턴 소요 시간 표시',
47  'Unattended commands from cloud sessions on this computer': '클라우드 세션의 무인 명령을 이 컴퓨터에서 실행',
48  'Use auto mode during plan': '계획 중에도 자동 모드 사용',
49  'Wait for limit to reset': '한도 초기화까지 대기',
50  'Ultracode keyword trigger': 'Ultracode 키워드로 실행',
51  'Dynamic workflow size': '동적 워크플로 규모',
52  'Dynamic workflows': '동적 워크플로',
53  'Worktree base ref': '워크트리 기준 ref',
54  'Verbose output': '자세한 출력',
55  'Auto-scroll': '자동 스크롤',
56  'Show last response in external editor': '마지막 답변을 외부 편집기로 보기',
57  'Show PR status footer': 'PR 상태 줄 표시',
58  'Claude in Chrome enabled by default': 'Claude in Chrome 기본으로 켜기',
59  'External CLAUDE.md includes': '외부 CLAUDE.md 가져오기 허용',
60}
61
62// 슬래시 명령 자동완성·/help 설명. 키는 명령 이름(슬래시 없이).
63export const COMMAND_DESCRIPTIONS: Record<string, string> = {
64  'add-dir': '작업 폴더 추가',
65  advisor: '중요한 순간에 더 강한 모델에게 자문',
66  agents: '서브에이전트 만들기·관리',
67  alias: '명령 별칭 만들기·보기',
68  artifacts: '게시하거나 공유받은 아티팩트 보기',
69  'auto-mode-setup': '자동 모드에 작업 환경 알려 주기',
70  autocompact: '자동 요약을 시작할 컨텍스트 사용량 설정',
71  'autofix-pr': '현재 PR의 문제를 지켜보고 자동 수정',
72  background: '이 세션을 백그라운드로 보내고 터미널 비우기',
73  branch: '지금 시점에서 대화를 분기',
74  brief: '간단 응답 모드 전환',
75  btw: '대화를 끊지 않고 짧은 질문하기',
76  bug: '버그 신고 또는 대화 공유',
77  cd: '세션을 다른 작업 폴더로 옮기기',
78  chrome: 'Claude in Chrome 설정 열기',
79  clear: '빈 컨텍스트로 새 세션 시작(이전 세션은 /resume으로 다시 열 수 있음)',
80  'cloud-plugins': '클라우드 세션에서 이 컴퓨터의 플러그인 사용 여부 선택',
81  color: '이 세션의 입력창 색 설정',
82  compact: '지금까지의 대화를 요약해 컨텍스트 확보',
83  config: '설정 열기',
84  context: '컨텍스트 사용량을 색 격자로 보기',
85  copy: 'Claude의 마지막 답변을 클립보드에 복사(/copy N은 N번째 최근 답변)',
86  cost: '세션 비용 보기',
87  daemon: '백그라운드 서비스와 루틴 관리',
88  'design-consent': 'Claude 에이전트에게 Design 프로젝트 접근 허용',
89  'design-revoke': 'Claude 에이전트의 Design 프로젝트 접근 해제',
90  desktop: '이 세션을 Claude Desktop에서 이어 하기',
91  doctor: 'Claude Code 설치 상태 진단',
92  effort: '모델 추론 강도 설정',
93  exit: 'Claude Code 종료',
94  export: '대화를 파일이나 클립보드로 내보내기',
95  'extra-usage': '/usage-credits로 이름이 바뀜',
96  feedback: 'Anthropic에 의견 보내기 또는 버그 신고',
97  focus: '집중 보기 전환: 프롬프트·요약·답변만 표시',
98  fork: '이 대화를 새 백그라운드 세션으로 복사하고 여기서 계속',
99  goal: 'Claude가 멈추기 전에 확인할 목표 설정',
100  heapdump: 'JS 힙을 바탕 화면에 덤프(Linux에서 바탕 화면 폴더가 없으면 홈 폴더)',
101  help: '도움말과 사용할 수 있는 명령 보기',
102  hooks: '도구 이벤트 훅 설정 보기',
103  ide: 'IDE 연동 관리와 상태 보기',
104  import: '다른 AI 코딩 에이전트의 설정 가져오기',
105  init: '코드베이스 설명을 담은 CLAUDE.md 만들기',
106  insights: 'Claude Code 세션 분석 보고서 만들기',
107  install: 'Claude Code 네이티브 빌드 설치',
108  'install-github-app': '저장소에 Claude GitHub Actions 설정',
109  'install-slack-app': 'Claude Slack 앱 설치',
110  keybindings: '단축키 설정 파일 열기',
111  'list-agents': '메시지를 보낼 수 있는 에이전트·세션 목록',
112  login: 'Anthropic 계정으로 로그인',
113  logout: 'Anthropic 계정에서 로그아웃',
114  loops: '반복 작업 목록·만들기·삭제',
115  mcp: 'MCP 서버 관리',
116  memory: 'CLAUDE.md 파일과 메모리 설정 편집',
117  mobile: 'Claude 모바일 앱 다운로드 QR 코드 보기',
118  model: '이 세션의 모델 설정',
119  'output-style': '출력 스타일 목록 보기 또는 바꾸기',
120  permissions: '도구 허용·거부 규칙 관리',
121  plan: '계획 모드 켜기 또는 현재 계획 보기',
122  plugin: 'Claude Code 플러그인 관리',
123  powerup: '짧은 대화형 강좌로 Claude Code 기능 익히기',
124  'privacy-settings': '개인정보 설정 보기·변경',
125  radio: 'Claude FM 로파이 라디오 듣기',
126  'rate-limit-options': '사용 한도와 업그레이드 옵션 관리',
127  recap: '지금 세션 한 줄 요약 만들기',
128  'release-notes': '릴리스 노트 보기',
129  'reload-plugins': '바뀐 플러그인을 현재 세션에 적용',
130  'reload-skills': '디스크에서 추가·변경된 스킬 다시 읽기',
131  'remote-control': '휴대폰이나 claude.ai/code에서 이 세션 조작',
132  'remote-env': '클라우드 에이전트의 기본 환경 선택',
133  rename: '현재 대화 이름 바꾸기',
134  resume: '이전 대화 이어 하기',
135  rewind: '대화와 코드를 이전 시점으로 되돌리기',
136  schedule: '예약된 원격 Claude Code 에이전트 만들기·관리',
137  'scroll-speed': '마우스 휠 스크롤 속도 조절',
138  'security-review': '현재 브랜치의 변경 사항 보안 검토',
139  session: '클라우드 세션 URL과 QR 코드 보기',
140  'setup-bedrock': 'Amazon Bedrock 인증·리전·고정 모델 다시 설정',
141  'setup-vertex': 'Google Vertex AI 인증·프로젝트·리전·고정 모델 다시 설정',
142  skills: '사용할 수 있는 스킬 목록',
143  'skill-doctor': '쓰이지 않고 컨텍스트만 차지하는 스킬 보기',
144  statusline: '상태 줄 설정',
145  status: '버전·모델·계정·API 연결·도구 상태 보기',
146  stickers: 'Claude Code 스티커 주문',
147  stop: '이 백그라운드 세션 멈추기(대화 기록과 워크트리는 남김)',
148  subtask: '전체 컨텍스트를 가진 서브에이전트에 일을 맡기고 결과 받기',
149  tasks: '백그라운드에서 실행 중인 작업 보기·관리',
150  'team-onboarding': '내 사용 기록으로 팀원용 Claude Code 안내서 만들기',
151  teleport: '이 세션을 클라우드로 보내거나 claude.ai의 세션 이어 하기',
152  'terminal-setup': '터미널 줄바꿈 키(Shift+Enter) 설정',
153  theme: '테마 바꾸기',
154  tui: '터미널 화면 방식 설정(default | fullscreen)',
155  ultraplan: '클라우드 세션이 계획 초안을 쓰고 직접 고쳐 승인',
156  ultrareview: '클라우드 세션으로 브랜치의 버그 찾기·검증',
157  upgrade: 'Max로 업그레이드해 사용 한도 늘리기',
158  usage: '세션 비용·요금제 사용량·활동 통계 보기',
159  'usage-credits': '사용 크레딧 설정 또는 관리자에게 요청',
160  version: '이 세션의 버전 보기',
161  vim: 'Vim 편집 모드 전환',
162  voice: '음성 모드 전환',
163  'web-setup': 'GitHub 계정으로 클라우드 세션 설정',
164  wellbeing: '휴식 알림과 방해 금지 시간 설정',
165  workflows: '실행 중이거나 끝난 워크플로 보기',
166}
167
hooks/translate.ts 30 lines
1// 모델 번역에 쓰는 순수 함수와 상수. $를 쓰는 부분은 엔진 규칙상 register.tsx 안에 있다.
2
3export const TRANSLATIONS_KEY = 'translations'
4export const MAX_CACHED = 500
5export const BATCH = 10
6
7export const SYSTEM = [
8  'You translate Claude Code CLI interface text into natural Korean.',
9  'Input: a JSON array of English strings. Output: only a JSON array of Korean strings, same length and order.',
10  'Keep unchanged: code, commands, slash commands, file paths, URLs, identifiers, key names (ctrl+o, Esc), numbers, and product names (Claude Code, GitHub, MCP).',
11  'Use short, plain UI wording. Do not add explanations.',
12].join('\n')
13
14const HANGUL = /[가-힣]/
15const WORD = /[A-Za-z]{2,}/
16
17export const needsTranslation = (text: string) => !HANGUL.test(text) && WORD.test(text)
18
19export const parseArray = (text: string, length: number): string[] | undefined => {
20  try {
21    const value: unknown = JSON.parse(text.match(/\[[\s\S]*\]/)?.[0] ?? '')
22    return Array.isArray(value) && value.length === length && value.every(v => typeof v === 'string') ? value : undefined
23  } catch {
24    return undefined
25  }
26}
27
28// ponytail: 최근 MAX_CACHED개만 남긴다. 넘치면 오래 전에 넣은 것부터 버린다.
29export const trim = (cache: Record<string, string>) => Object.fromEntries(Object.entries(cache).slice(-MAX_CACHED))
30