clawt 3.9.13 → 3.10.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 (106) hide show
  1. package/README.md +80 -1
  2. package/README.zh-CN.md +81 -0
  3. package/dist/index.js +1935 -592
  4. package/dist/postinstall.js +1626 -283
  5. package/docs/config-file.md +2 -0
  6. package/docs/config.md +2 -1
  7. package/docs/init.md +3 -2
  8. package/docs/project-config.md +10 -1
  9. package/docs/spec.md +69 -2
  10. package/package.json +1 -1
  11. package/src/commands/alias.ts +5 -4
  12. package/src/commands/completion.ts +2 -1
  13. package/src/commands/config.ts +25 -7
  14. package/src/commands/cover-validate.ts +3 -2
  15. package/src/commands/create.ts +8 -7
  16. package/src/commands/home.ts +2 -1
  17. package/src/commands/init.ts +13 -6
  18. package/src/commands/list.ts +6 -4
  19. package/src/commands/merge.ts +8 -7
  20. package/src/commands/projects.ts +5 -3
  21. package/src/commands/remove.ts +7 -6
  22. package/src/commands/reset.ts +3 -2
  23. package/src/commands/resume.ts +10 -7
  24. package/src/commands/run.ts +8 -7
  25. package/src/commands/status.ts +16 -11
  26. package/src/commands/sync.ts +4 -3
  27. package/src/commands/tasks.ts +8 -6
  28. package/src/commands/validate.ts +7 -6
  29. package/src/constants/ai-prompts.ts +11 -11
  30. package/src/constants/config.ts +30 -0
  31. package/src/constants/index.ts +3 -2
  32. package/src/constants/messages/alias.ts +44 -14
  33. package/src/constants/messages/cli-descriptions.ts +91 -0
  34. package/src/constants/messages/common.ts +221 -36
  35. package/src/constants/messages/completion.ts +43 -14
  36. package/src/constants/messages/config.ts +61 -18
  37. package/src/constants/messages/cover-validate.ts +43 -14
  38. package/src/constants/messages/create.ts +16 -5
  39. package/src/constants/messages/home.ts +19 -6
  40. package/src/constants/messages/index.ts +2 -0
  41. package/src/constants/messages/init.ts +45 -14
  42. package/src/constants/messages/interactive-panel.ts +183 -29
  43. package/src/constants/messages/merge.ts +140 -38
  44. package/src/constants/messages/post-create.ts +59 -19
  45. package/src/constants/messages/projects.ts +51 -14
  46. package/src/constants/messages/remove.ts +50 -15
  47. package/src/constants/messages/reset.ts +14 -4
  48. package/src/constants/messages/resume.ts +116 -19
  49. package/src/constants/messages/run.ts +165 -35
  50. package/src/constants/messages/status.ts +84 -23
  51. package/src/constants/messages/sync.ts +54 -17
  52. package/src/constants/messages/tasks.ts +21 -7
  53. package/src/constants/messages/update.ts +35 -11
  54. package/src/constants/messages/validate.ts +218 -57
  55. package/src/constants/progress.ts +17 -6
  56. package/src/constants/project-config.ts +17 -0
  57. package/src/constants/prompt.ts +18 -2
  58. package/src/constants/tasks-template.ts +56 -2
  59. package/src/hooks/post-create.ts +5 -2
  60. package/src/index.ts +6 -5
  61. package/src/types/config.ts +2 -0
  62. package/src/utils/alias.ts +2 -1
  63. package/src/utils/claude.ts +10 -9
  64. package/src/utils/config-strategy.ts +3 -3
  65. package/src/utils/dry-run.ts +2 -2
  66. package/src/utils/formatter.ts +18 -11
  67. package/src/utils/i18n.ts +63 -0
  68. package/src/utils/index.ts +2 -0
  69. package/src/utils/interactive-panel-render.ts +6 -3
  70. package/src/utils/progress-render.ts +11 -9
  71. package/src/utils/prompt.ts +2 -1
  72. package/src/utils/task-executor.ts +10 -7
  73. package/src/utils/task-file.ts +2 -1
  74. package/src/utils/terminal.ts +9 -9
  75. package/src/utils/ui-prompts.ts +4 -3
  76. package/src/utils/update-checker.ts +1 -1
  77. package/src/utils/validate-branch.ts +16 -9
  78. package/src/utils/validate-core.ts +2 -1
  79. package/src/utils/validate-runner.ts +2 -2
  80. package/src/utils/worktree-matcher.ts +9 -7
  81. package/tests/unit/commands/alias.test.ts +4 -0
  82. package/tests/unit/commands/completion.test.ts +14 -0
  83. package/tests/unit/commands/config.test.ts +61 -28
  84. package/tests/unit/commands/cover-validate.test.ts +13 -2
  85. package/tests/unit/commands/init.test.ts +6 -2
  86. package/tests/unit/commands/merge.test.ts +14 -0
  87. package/tests/unit/commands/run.test.ts +17 -0
  88. package/tests/unit/commands/tasks.test.ts +39 -9
  89. package/tests/unit/constants/config.test.ts +16 -1
  90. package/tests/unit/constants/messages-post-create.test.ts +93 -1
  91. package/tests/unit/constants/messages.test.ts +85 -1
  92. package/tests/unit/hooks/post-create.test.ts +32 -0
  93. package/tests/unit/utils/alias.test.ts +14 -0
  94. package/tests/unit/utils/claude.test.ts +24 -4
  95. package/tests/unit/utils/config-strategy.test.ts +21 -0
  96. package/tests/unit/utils/conflict-resolver.test.ts +24 -4
  97. package/tests/unit/utils/formatter.test.ts +21 -0
  98. package/tests/unit/utils/i18n.test.ts +91 -0
  99. package/tests/unit/utils/progress.test.ts +39 -18
  100. package/tests/unit/utils/prompt.test.ts +25 -2
  101. package/tests/unit/utils/task-file.test.ts +73 -10
  102. package/tests/unit/utils/terminal-cmux.test.ts +19 -4
  103. package/tests/unit/utils/update-checker.test.ts +2 -0
  104. package/tests/unit/utils/validate-branch.test.ts +26 -1
  105. package/tests/unit/utils/validation.test.ts +2 -2
  106. package/tests/unit/utils/worktree-matcher.test.ts +2 -0
@@ -6,6 +6,7 @@ import { MESSAGES } from '../constants/index.js';
6
6
  import { loadProjectConfig } from '../utils/project-config.js';
7
7
  import { getMainWorktreePath } from '../utils/git.js';
8
8
  import { printInfo, printSuccess, printWarning } from '../utils/formatter.js';
9
+ import { getCurrentLanguage } from '../utils/i18n.js';
9
10
  import type { WorktreeInfo, ResolvedHook, PostCreateHookResult } from '../types/index.js';
10
11
 
11
12
  /** 项目仓库中的 postCreate 脚本相对路径 */
@@ -86,7 +87,9 @@ export function resolvePostCreateHook(): ResolvedHook | null {
86
87
  * @returns {string} 来源描述文本
87
88
  */
88
89
  function getSourceLabel(hook: ResolvedHook): string {
89
- return hook.source === 'projectConfig' ? '项目配置 (postCreate)' : '.clawt/postCreate.sh';
90
+ return hook.source === 'projectConfig'
91
+ ? (getCurrentLanguage() === 'en' ? 'Project config (postCreate)' : '项目配置 (postCreate)')
92
+ : '.clawt/postCreate.sh';
90
93
  }
91
94
 
92
95
  /**
@@ -122,7 +125,7 @@ function executeOneHook(worktree: WorktreeInfo, hook: ResolvedHook): Promise<Pos
122
125
  child.on('close', (code) => {
123
126
  if (code !== null && code !== 0) {
124
127
  result.success = false;
125
- result.error = `命令退出码: ${code}`;
128
+ result.error = getCurrentLanguage() === 'en' ? `Command exit code: ${code}` : `命令退出码: ${code}`;
126
129
  logger.error(`postCreate hook 失败: ${hook.command} (退出码: ${code}) @ ${worktree.path}`);
127
130
  } else {
128
131
  logger.info(`postCreate hook 成功: ${hook.command} @ ${worktree.path}`);
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { ClawtError } from './errors/index.js';
4
4
  import { logger, enableConsoleTransport } from './logger/index.js';
5
5
  import { EXIT_CODES } from './constants/index.js';
6
6
  import { printError, ensureClawtDirs, loadConfig, applyAliases, checkForUpdates, setNonInteractive } from './utils/index.js';
7
+ import { getCurrentLanguage } from './utils/i18n.js';
7
8
  import { registerListCommand } from './commands/list.js';
8
9
  import { registerCreateCommand } from './commands/create.js';
9
10
  import { registerRemoveCommand } from './commands/remove.js';
@@ -34,10 +35,10 @@ const program = new Command();
34
35
 
35
36
  program
36
37
  .name('clawt')
37
- .description('本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具')
38
+ .description(getCurrentLanguage() === 'en' ? 'Run multiple Claude Code Agent tasks in parallel — a CLI tool integrating Git Worktree with Claude Code CLI' : '本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具')
38
39
  .version(version)
39
- .option('--debug', '输出详细调试信息到终端')
40
- .option('-y, --yes', '跳过所有交互式确认,适用于脚本/CI 环境');
40
+ .option('--debug', getCurrentLanguage() === 'en' ? 'Output detailed debug information to terminal' : '输出详细调试信息到终端')
41
+ .option('-y, --yes', getCurrentLanguage() === 'en' ? 'Skip all interactive confirmations, suitable for scripts/CI environments' : '跳过所有交互式确认,适用于脚本/CI 环境');
41
42
 
42
43
  // 在子命令 action 执行前检查 --debug 选项,按需启用控制台日志
43
44
  program.hook('preAction', (thisCommand) => {
@@ -81,7 +82,7 @@ process.on('uncaughtException', (error) => {
81
82
  logger.error(error.message);
82
83
  process.exit(error.exitCode);
83
84
  }
84
- printError(error.message || '未知错误');
85
+ printError(error.message || (getCurrentLanguage() === 'en' ? 'Unknown error' : '未知错误'));
85
86
  logger.error(`未捕获异常: ${error.message}\n${error.stack}`);
86
87
  process.exit(EXIT_CODES.ERROR);
87
88
  });
@@ -93,7 +94,7 @@ process.on('unhandledRejection', (reason) => {
93
94
  logger.error(error.message);
94
95
  process.exit(error.exitCode);
95
96
  }
96
- printError(error.message || '未知错误');
97
+ printError(error.message || (getCurrentLanguage() === 'en' ? 'Unknown error' : '未知错误'));
97
98
  logger.error(`未处理的 Promise 拒绝: ${error.message}`);
98
99
  process.exit(EXIT_CODES.ERROR);
99
100
  });
@@ -1,5 +1,7 @@
1
1
  /** clawt 全局配置 */
2
2
  export interface ClawtConfig {
3
+ /** 界面语言:en(英文)、zh-CN(中文) */
4
+ language: 'en' | 'zh-CN';
3
5
  /** 移除 worktree 时是否自动删除对应本地分支 */
4
6
  autoDeleteBranch: boolean;
5
7
  /** Claude Code CLI 启动指令(不传 --tasks 时在 worktree 中直接打开交互式界面) */
@@ -1,6 +1,7 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { ClawtConfig } from '../types/index.js';
3
3
  import { logger } from '../logger/index.js';
4
+ import { getCurrentLanguage } from './i18n.js';
4
5
 
5
6
  /**
6
7
  * 根据配置中的别名映射,为已注册的命令添加 Commander.js 别名
@@ -14,7 +15,7 @@ export function applyAliases(program: Command, aliases: ClawtConfig['aliases']):
14
15
  targetCmd.alias(alias);
15
16
  logger.debug(`已注册别名: ${alias} → ${commandName}`);
16
17
  } else {
17
- logger.warn(`别名 "${alias}" 的目标命令 "${commandName}" 不存在,已跳过`);
18
+ logger.warn(getCurrentLanguage() === 'en' ? `Alias "${alias}" targets non-existent command "${commandName}", skipped` : `别名 "${alias}" 的目标命令 "${commandName}" 不存在,已跳过`);
18
19
  }
19
20
  }
20
21
  }
@@ -2,10 +2,11 @@ import { spawnSync } from 'node:child_process';
2
2
  import { existsSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { ClawtError } from '../errors/index.js';
5
- import { CLAUDE_PROJECTS_DIR } from '../constants/index.js';
5
+ import { CLAUDE_PROJECTS_DIR, MESSAGES } from '../constants/index.js';
6
6
  import { resolveClaudeCodeCommand } from './project-config.js';
7
7
  import { printInfo, printWarning } from './formatter.js';
8
8
  import { openCommandInNewTerminalTab } from './terminal.js';
9
+ import { getCurrentLanguage } from './i18n.js';
9
10
  import type { WorktreeInfo } from '../types/index.js';
10
11
 
11
12
  /**
@@ -64,12 +65,12 @@ export function launchInteractiveClaude(worktree: WorktreeInfo, options: LaunchC
64
65
  args.push('--continue');
65
66
  }
66
67
 
67
- printInfo(`正在 worktree 中启动 Claude Code 交互式界面...`);
68
- printInfo(` 分支: ${worktree.branch}`);
69
- printInfo(` 路径: ${worktree.path}`);
70
- printInfo(` 指令: ${commandStr}`);
68
+ printInfo(MESSAGES.STARTING_CLAUDE_INTERACTIVE);
69
+ printInfo(` ${MESSAGES.BRANCH_LABEL} ${worktree.branch}`);
70
+ printInfo(` ${MESSAGES.PATH_LABEL_RESUME} ${worktree.path}`);
71
+ printInfo(` ${MESSAGES.COMMAND_LABEL} ${commandStr}`);
71
72
  if (options.autoContinue) {
72
- printInfo(` 模式: ${hasPreviousSession ? '继续上次对话' : '新对话'}`);
73
+ printInfo(` ${MESSAGES.MODE_LABEL} ${hasPreviousSession ? MESSAGES.CONTINUE_SESSION : MESSAGES.NEW_SESSION}`);
73
74
  }
74
75
  printInfo('');
75
76
 
@@ -79,11 +80,11 @@ export function launchInteractiveClaude(worktree: WorktreeInfo, options: LaunchC
79
80
  });
80
81
 
81
82
  if (result.error) {
82
- throw new ClawtError(`启动 Claude Code 失败: ${result.error.message}`);
83
+ throw new ClawtError(MESSAGES.CLAUDE_START_FAILED(result.error.message));
83
84
  }
84
85
 
85
86
  if (result.status !== null && result.status !== 0) {
86
- printWarning(`Claude Code 退出码: ${result.status}`);
87
+ printWarning(getCurrentLanguage() === 'en' ? `Claude Code exit code: ${result.status}` : `Claude Code 退出码: ${result.status}`);
87
88
  }
88
89
  }
89
90
 
@@ -121,7 +122,7 @@ export function buildClaudeCommand(worktree: WorktreeInfo, hasPreviousSession: b
121
122
  */
122
123
  export function launchInteractiveClaudeInNewTerminal(worktree: WorktreeInfo, hasPreviousSession: boolean): void {
123
124
  const command = buildClaudeCommand(worktree, hasPreviousSession);
124
- const modeLabel = hasPreviousSession ? '继续' : '新对话';
125
+ const modeLabel = hasPreviousSession ? MESSAGES.CONTINUE_SESSION : MESSAGES.NEW_SESSION;
125
126
  const tabTitle = `clawt: ${worktree.branch}`;
126
127
 
127
128
  openCommandInNewTerminalTab(command, tabTitle);
@@ -112,7 +112,7 @@ export async function promptConfigValue(
112
112
  */
113
113
  export function formatConfigValue(value: unknown): string {
114
114
  if (value === undefined || value === null) {
115
- return chalk.dim('(未设置)');
115
+ return chalk.dim(MESSAGES.NOT_SET);
116
116
  }
117
117
  if (typeof value === 'boolean') {
118
118
  return value ? chalk.green('true') : chalk.yellow('false');
@@ -136,7 +136,7 @@ export async function interactiveConfigEditor<T extends object>(
136
136
  ): Promise<{ key: keyof T; newValue: unknown }> {
137
137
  // 非交互模式下无法进行配置编辑,引导使用 config set 命令
138
138
  if (isNonInteractive()) {
139
- throw new ClawtError('非交互模式下无法使用交互式配置编辑器,请使用 clawt config set <key> <value>');
139
+ throw new ClawtError(MESSAGES.NON_INTERACTIVE_CONFIG_EDITOR);
140
140
  }
141
141
 
142
142
  const keys = Object.keys(definitions);
@@ -203,7 +203,7 @@ async function promptNumberValue(key: string, currentValue: number): Promise<num
203
203
  message: MESSAGES.CONFIG_INPUT_PROMPT(key),
204
204
  initial: String(currentValue),
205
205
  validate: (val: string) => {
206
- if (Number.isNaN(Number(val))) return '请输入有效的数字';
206
+ if (Number.isNaN(Number(val))) return MESSAGES.INVALID_NUMBER_PROMPT;
207
207
  return true;
208
208
  },
209
209
  }).run();
@@ -69,11 +69,11 @@ export function printDryRunPreview(branchNames: string[], tasks: string[], concu
69
69
  }
70
70
 
71
71
  // 路径行(2空格缩进)
72
- printInfo(` ${chalk.gray('路径:')} ${worktreePath}`);
72
+ printInfo(` ${chalk.gray(MESSAGES.PATH_LABEL)} ${worktreePath}`);
73
73
 
74
74
  // 任务描述行(2空格缩进,非交互式模式)
75
75
  if (!isInteractive) {
76
- printInfo(` ${chalk.gray('任务:')} ${truncateTaskDesc(tasks[i])}`);
76
+ printInfo(` ${chalk.gray(MESSAGES.TASK_LABEL)} ${truncateTaskDesc(tasks[i])}`);
77
77
  }
78
78
 
79
79
  printInfo('');
@@ -3,6 +3,7 @@ import { MESSAGES } from '../constants/index.js';
3
3
  import { createInterface } from 'node:readline';
4
4
  import type { WorktreeStatus } from '../types/index.js';
5
5
  import { isNonInteractive } from './interactive.js';
6
+ import { getCurrentLanguage } from './i18n.js';
6
7
 
7
8
  /**
8
9
  * 输出成功信息
@@ -89,8 +90,11 @@ export function confirmAction(question: string, nonInteractiveDefault: boolean =
89
90
  * @returns {Promise<boolean>} 用户是否确认
90
91
  */
91
92
  export function confirmDestructiveAction(dangerousCommand: string, description: string): Promise<boolean> {
92
- printWarning(`即将执行 ${chalk.red.bold(dangerousCommand)},${description}`);
93
- return confirmAction('是否继续?');
93
+ const lang = getCurrentLanguage();
94
+ const continuePrompt = lang === 'en' ? 'Continue?' : '是否继续?';
95
+ const warningPrefix = lang === 'en' ? `About to execute ${chalk.red.bold(dangerousCommand)}, ` : `即将执行 ${chalk.red.bold(dangerousCommand)},`;
96
+ printWarning(`${warningPrefix}${description}`);
97
+ return confirmAction(continuePrompt);
94
98
  }
95
99
 
96
100
  /**
@@ -111,14 +115,16 @@ export function isWorktreeIdle(status: WorktreeStatus): boolean {
111
115
  * @returns {string} 格式化后的状态字符串
112
116
  */
113
117
  export function formatWorktreeStatus(status: WorktreeStatus): string {
118
+ const lang = getCurrentLanguage();
114
119
  const parts: string[] = [];
115
120
 
116
121
  // 提交数(黄色)
117
- parts.push(chalk.yellow(`${status.commitCount} 个提交`));
122
+ const commitLabel = lang === 'en' ? `${status.commitCount} commit${status.commitCount !== 1 ? 's' : ''}` : `${status.commitCount} 个提交`;
123
+ parts.push(chalk.yellow(commitLabel));
118
124
 
119
125
  // 变更统计
120
126
  if (status.insertions === 0 && status.deletions === 0) {
121
- parts.push('无变更');
127
+ parts.push(lang === 'en' ? 'No changes' : '无变更');
122
128
  } else {
123
129
  const diffParts: string[] = [];
124
130
  diffParts.push(chalk.green(`+${status.insertions}`));
@@ -128,7 +134,7 @@ export function formatWorktreeStatus(status: WorktreeStatus): string {
128
134
 
129
135
  // 未提交修改提示(灰色)
130
136
  if (status.hasDirtyFiles) {
131
- parts.push(chalk.gray('(未提交修改)'));
137
+ parts.push(chalk.gray(lang === 'en' ? '(uncommitted changes)' : '(未提交修改)'));
132
138
  }
133
139
 
134
140
  return parts.join(' ');
@@ -157,6 +163,7 @@ export function formatDuration(ms: number): string {
157
163
  * @returns {string | null} 中文相对时间描述,无效日期时返回 null
158
164
  */
159
165
  export function formatRelativeTime(isoDateString: string): string | null {
166
+ const lang = getCurrentLanguage();
160
167
  const date = new Date(isoDateString);
161
168
  const now = new Date();
162
169
  const diffMs = now.getTime() - date.getTime();
@@ -168,7 +175,7 @@ export function formatRelativeTime(isoDateString: string): string | null {
168
175
 
169
176
  // 未来时间或不到 1 分钟
170
177
  if (diffMs < 0 || diffMs < 60 * 1000) {
171
- return '刚刚';
178
+ return lang === 'en' ? 'just now' : '刚刚';
172
179
  }
173
180
 
174
181
  const diffMinutes = Math.floor(diffMs / (1000 * 60));
@@ -176,20 +183,20 @@ export function formatRelativeTime(isoDateString: string): string | null {
176
183
  const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
177
184
 
178
185
  if (diffHours < 1) {
179
- return `${diffMinutes} 分钟前`;
186
+ return lang === 'en' ? `${diffMinutes} min ago` : `${diffMinutes} 分钟前`;
180
187
  }
181
188
  if (diffDays < 1) {
182
- return `${diffHours} 小时前`;
189
+ return lang === 'en' ? `${diffHours} hr ago` : `${diffHours} 小时前`;
183
190
  }
184
191
  if (diffDays < 30) {
185
- return `${diffDays} 天前`;
192
+ return lang === 'en' ? `${diffDays} day${diffDays > 1 ? 's' : ''} ago` : `${diffDays} 天前`;
186
193
  }
187
194
  if (diffDays < 365) {
188
195
  const months = Math.floor(diffDays / 30);
189
- return `${months} 个月前`;
196
+ return lang === 'en' ? `${months} month${months > 1 ? 's' : ''} ago` : `${months} 个月前`;
190
197
  }
191
198
  const years = Math.floor(diffDays / 365);
192
- return `${years} 年前`;
199
+ return lang === 'en' ? `${years} year${years > 1 ? 's' : ''} ago` : `${years} 年前`;
193
200
  }
194
201
 
195
202
  /**
@@ -0,0 +1,63 @@
1
+ import { loadConfig } from './config.js';
2
+
3
+ /** 支持的语言类型 */
4
+ export type Language = 'en' | 'zh-CN';
5
+
6
+ /** 当前语言缓存 */
7
+ let currentLanguage: Language | null = null;
8
+
9
+ /**
10
+ * 获取当前语言配置
11
+ * 优先使用缓存,缓存不存在时从配置文件读取
12
+ * @returns {Language} 当前语言
13
+ */
14
+ export function getCurrentLanguage(): Language {
15
+ if (currentLanguage !== null) {
16
+ return currentLanguage;
17
+ }
18
+ try {
19
+ const config = loadConfig();
20
+ currentLanguage = (config.language as Language) || 'en';
21
+ } catch {
22
+ currentLanguage = 'en';
23
+ }
24
+ return currentLanguage;
25
+ }
26
+
27
+ /**
28
+ * 设置当前语言(用于测试和 CLI 初始化时)
29
+ * @param {Language} lang - 语言代码
30
+ */
31
+ export function setCurrentLanguage(lang: Language): void {
32
+ currentLanguage = lang;
33
+ }
34
+
35
+ /**
36
+ * 重置语言缓存(配置变更后调用,使下次读取时重新加载)
37
+ */
38
+ export function resetLanguageCache(): void {
39
+ currentLanguage = null;
40
+ }
41
+
42
+ /**
43
+ * 从 i18n 双语映射条目中提取当前语言对应的值类型
44
+ */
45
+ type ExtractLang<T> = T extends { en: infer V; 'zh-CN': infer V } ? V : never;
46
+
47
+ /**
48
+ * 创建国际化消息对象
49
+ * 根据当前语言从双语映射中选择对应的文本或函数
50
+ * 保留精确的键名和值类型,确保合并后的 MESSAGES 对象类型正确
51
+ * @param {T} i18nMap - 双语消息映射,每个键包含 en 和 zh-CN 两个版本
52
+ * @returns {{ [K in keyof T]: ExtractLang<T[K]> }} 当前语言的消息对象,键名和值类型与原始映射一致
53
+ */
54
+ export function createMessages<T extends Record<string, { en: any; 'zh-CN': any }>>(
55
+ i18nMap: T
56
+ ): { [K in keyof T]: ExtractLang<T[K]> } {
57
+ const lang = getCurrentLanguage();
58
+ const result: any = {};
59
+ for (const key of Object.keys(i18nMap)) {
60
+ result[key] = i18nMap[key][lang];
61
+ }
62
+ return result;
63
+ }
@@ -98,4 +98,6 @@ export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDa
98
98
  export type { PanelLine } from './interactive-panel-render.js';
99
99
  export { buildConflictResolvePrompt, invokeClaudeForConflictResolve, resolveConflictsWithAI, determineConflictResolveMode, handleMergeConflict } from './conflict-resolver.js';
100
100
  export { resolvePostCreateHook, executePostCreateHooks, runPostCreateHooks } from '../hooks/index.js';
101
+ export { getCurrentLanguage, setCurrentLanguage, resetLanguageCache, createMessages } from './i18n.js';
102
+ export type { Language } from './i18n.js';
101
103
 
@@ -11,6 +11,7 @@ import {
11
11
  UNKNOWN_DATE_GROUP,
12
12
  VALIDATE_BRANCH_PREFIX,
13
13
  } from '../constants/index.js';
14
+ import { getCurrentLanguage } from './i18n.js';
14
15
  import {
15
16
  PANEL_FOOTER_SHORTCUTS,
16
17
  PANEL_FOOTER_COUNTDOWN,
@@ -218,7 +219,8 @@ export function renderDateSeparator(dateKey: string): string {
218
219
  return `${leftPad}${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk.bold.hex(PANEL_DATE_COLOR)(PANEL_UNKNOWN_DATE)} ${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
219
220
  }
220
221
  const relativeDate = formatRelativeDate(dateKey);
221
- return `${leftPad}${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk.bold.hex(PANEL_DATE_COLOR)(dateKey)}${chalk.hex(PANEL_DATE_COLOR)(`(${relativeDate})`)} ${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
222
+ const relativeDateText = getCurrentLanguage() === 'en' ? `(${relativeDate})` : `(${relativeDate})`;
223
+ return `${leftPad}${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk.bold.hex(PANEL_DATE_COLOR)(dateKey)}${chalk.hex(PANEL_DATE_COLOR)(relativeDateText)} ${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
222
224
  }
223
225
 
224
226
  /**
@@ -324,12 +326,13 @@ function renderConfiguredBranchLine(main: MainWorktreeStatus): string {
324
326
  * @returns {string} 格式化的 diff 信息
325
327
  */
326
328
  function renderMainBranchDiff(main: MainWorktreeStatus): string {
329
+ const workingDirLabel = getCurrentLanguage() === 'en' ? 'Working dir:' : '工作区:';
327
330
  if (main.insertions === 0 && main.deletions === 0) {
328
- return `工作区: ${chalk.green(MESSAGES.STATUS_CHANGE_CLEAN)}`;
331
+ return `${workingDirLabel} ${chalk.green(MESSAGES.STATUS_CHANGE_CLEAN)}`;
329
332
  }
330
333
  const insertText = chalk.green(`+${main.insertions}`);
331
334
  const deleteText = chalk.red(`-${main.deletions}`);
332
- return `工作区: ${insertText} ${deleteText}`;
335
+ return `${workingDirLabel} ${insertText} ${deleteText}`;
333
336
  }
334
337
 
335
338
  /**
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import stringWidth from 'string-width';
3
- import { TASK_STATUS_ICONS, TASK_STATUS_LABELS } from '../constants/index.js';
3
+ import { TASK_STATUS_ICONS, getTaskStatusLabels } from '../constants/index.js';
4
4
  import { formatDuration } from './formatter.js';
5
5
 
6
6
  /** ANSI 颜色/样式转义序列的匹配正则 */
@@ -106,27 +106,28 @@ export function getMaxPathWidth(tasks: TaskProgress[]): number {
106
106
  export function renderTaskLine(task: TaskProgress, total: number, maxPathWidth: number, spinnerChar: string): string {
107
107
  const indexStr = `[${task.index}/${total}]`;
108
108
  const pathStr = task.path.padEnd(maxPathWidth);
109
+ const labels = getTaskStatusLabels();
109
110
 
110
111
  switch (task.status) {
111
112
  case 'pending': {
112
- return `${indexStr} ${pathStr} ${chalk.gray(TASK_STATUS_ICONS.PENDING)} ${chalk.gray(TASK_STATUS_LABELS.PENDING)}`;
113
+ return `${indexStr} ${pathStr} ${chalk.gray(TASK_STATUS_ICONS.PENDING)} ${chalk.gray(labels.PENDING)}`;
113
114
  }
114
115
  case 'running': {
115
116
  const elapsed = formatDuration(Date.now() - task.startedAt);
116
117
  // 仅显示活动信息,不显示路径(路径已在第二列显示)
117
118
  const detail = task.activity ? ` ${chalk.dim(task.activity)}` : '';
118
- return `${indexStr} ${pathStr} ${chalk.cyan(spinnerChar)} ${chalk.cyan(TASK_STATUS_LABELS.RUNNING)} ${chalk.gray(elapsed)}${detail}`;
119
+ return `${indexStr} ${pathStr} ${chalk.cyan(spinnerChar)} ${chalk.cyan(labels.RUNNING)} ${chalk.gray(elapsed)}${detail}`;
119
120
  }
120
121
  case 'done': {
121
122
  const duration = task.durationMs != null ? formatDuration(task.durationMs) : 'N/A';
122
123
  const cost = task.costUsd != null ? `$${task.costUsd.toFixed(2)}` : '';
123
124
  const preview = task.resultPreview ? ` ${chalk.dim(task.resultPreview)}` : '';
124
- return `${indexStr} ${pathStr} ${chalk.green(TASK_STATUS_ICONS.DONE)} ${chalk.green(TASK_STATUS_LABELS.DONE)} ${chalk.gray(duration)} ${chalk.yellow(cost)}${preview}`;
125
+ return `${indexStr} ${pathStr} ${chalk.green(TASK_STATUS_ICONS.DONE)} ${chalk.green(labels.DONE)} ${chalk.gray(duration)} ${chalk.yellow(cost)}${preview}`;
125
126
  }
126
127
  case 'failed': {
127
128
  const duration = task.durationMs != null ? formatDuration(task.durationMs) : 'N/A';
128
129
  const preview = task.resultPreview ? ` ${chalk.dim(task.resultPreview)}` : '';
129
- return `${indexStr} ${pathStr} ${chalk.red(TASK_STATUS_ICONS.FAILED)} ${chalk.red(TASK_STATUS_LABELS.FAILED)} ${chalk.gray(duration)}${preview}`;
130
+ return `${indexStr} ${pathStr} ${chalk.red(TASK_STATUS_ICONS.FAILED)} ${chalk.red(labels.FAILED)} ${chalk.gray(duration)}${preview}`;
130
131
  }
131
132
  }
132
133
  }
@@ -143,12 +144,13 @@ export function renderSummaryLine(tasks: TaskProgress[], total: number): string
143
144
  const done = tasks.filter((t) => t.status === 'done').length;
144
145
  const failed = tasks.filter((t) => t.status === 'failed').length;
145
146
  const pending = tasks.filter((t) => t.status === 'pending').length;
147
+ const labels = getTaskStatusLabels();
146
148
 
147
149
  const parts: string[] = [];
148
- if (running > 0) parts.push(chalk.cyan(`${running}/${total} ${TASK_STATUS_LABELS.RUNNING}`));
149
- if (done > 0) parts.push(chalk.green(`${done}/${total} ${TASK_STATUS_LABELS.DONE}`));
150
- if (failed > 0) parts.push(chalk.red(`${failed}/${total} ${TASK_STATUS_LABELS.FAILED}`));
151
- if (pending > 0) parts.push(chalk.gray(`${pending}/${total} ${TASK_STATUS_LABELS.PENDING}`));
150
+ if (running > 0) parts.push(chalk.cyan(`${running}/${total} ${labels.RUNNING}`));
151
+ if (done > 0) parts.push(chalk.green(`${done}/${total} ${labels.DONE}`));
152
+ if (failed > 0) parts.push(chalk.red(`${failed}/${total} ${labels.FAILED}`));
153
+ if (pending > 0) parts.push(chalk.gray(`${pending}/${total} ${labels.PENDING}`));
152
154
 
153
155
  return `[${parts.join(', ')}]`;
154
156
  }
@@ -1,6 +1,7 @@
1
1
  import Enquirer from 'enquirer';
2
2
  import { isNonInteractive } from './interactive.js';
3
3
  import { ClawtError } from '../errors/index.js';
4
+ import { MESSAGES } from '../constants/index.js';
4
5
 
5
6
  /**
6
7
  * 多行交互式输入框
@@ -45,7 +46,7 @@ export async function promptCommitMessage(
45
46
 
46
47
  // 空输入视为无效
47
48
  if (!result.trim()) {
48
- throw new ClawtError('提交信息不能为空');
49
+ throw new ClawtError(MESSAGES.COMMIT_MESSAGE_NOT_EMPTY);
49
50
  }
50
51
 
51
52
  return result.trim();
@@ -3,6 +3,7 @@ import { logger } from '../logger/index.js';
3
3
  import { MESSAGES } from '../constants/index.js';
4
4
  import type { ClaudeCodeResult, TaskResult, TaskSummary, WorktreeInfo } from '../types/index.js';
5
5
  import { spawnProcess, killAllChildProcesses } from './shell.js';
6
+ import { getCurrentLanguage } from './i18n.js';
6
7
  import { cleanupWorktrees } from './worktree.js';
7
8
  import { getConfigValue } from './config.js';
8
9
  import { printSuccess, printWarning, printInfo, printDoubleSeparator, confirmAction } from './formatter.js';
@@ -107,7 +108,7 @@ function executeClaudeTask(worktree: WorktreeInfo, task: string, onActivity?: Ac
107
108
  worktreePath: worktree.path,
108
109
  success,
109
110
  result: finalResult,
110
- error: success ? undefined : stderr || '任务执行失败',
111
+ error: success ? undefined : stderr || MESSAGES.TASK_FAILED,
111
112
  });
112
113
  });
113
114
 
@@ -132,11 +133,11 @@ function executeClaudeTask(worktree: WorktreeInfo, task: string, onActivity?: Ac
132
133
  */
133
134
  function printTaskSummary(summary: TaskSummary): void {
134
135
  printDoubleSeparator();
135
- printInfo(`全部任务已完成 (${summary.total}/${summary.total})`);
136
- printInfo(` 成功: ${summary.succeeded}`);
137
- printInfo(` 失败: ${summary.failed}`);
138
- printInfo(` 总耗时: ${(summary.totalDurationMs / 1000).toFixed(1)}s`);
139
- printInfo(` 总花费: $${summary.totalCostUsd.toFixed(2)}`);
136
+ printInfo(MESSAGES.ALL_TASKS_COMPLETED(summary.total));
137
+ printInfo(` ${MESSAGES.SUCCESS_LABEL} ${summary.succeeded}`);
138
+ printInfo(` ${MESSAGES.FAILURE_LABEL} ${summary.failed}`);
139
+ printInfo(` ${MESSAGES.TOTAL_DURATION_LABEL} ${(summary.totalDurationMs / 1000).toFixed(1)}s`);
140
+ printInfo(` ${MESSAGES.TOTAL_COST_LABEL} $${summary.totalCostUsd.toFixed(2)}`);
140
141
  printDoubleSeparator();
141
142
  }
142
143
 
@@ -342,7 +343,9 @@ export async function executeBatchTasks(
342
343
  // 校验 continueFlags 长度与 worktrees 一致,防止调用方传入不匹配的数组
343
344
  if (continueFlags && continueFlags.length !== count) {
344
345
  throw new ClawtError(
345
- `continueFlags 长度 (${continueFlags.length}) 与任务数 (${count}) 不一致`,
346
+ getCurrentLanguage() === 'en'
347
+ ? `continueFlags length (${continueFlags.length}) does not match task count (${count})`
348
+ : `continueFlags 长度 (${continueFlags.length}) 与任务数 (${count}) 不一致`,
346
349
  );
347
350
  }
348
351
 
@@ -2,6 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { ClawtError } from '../errors/index.js';
4
4
  import { MESSAGES } from '../constants/index.js';
5
+ import { getCurrentLanguage } from './i18n.js';
5
6
  import type { TaskFileEntry, ParseTaskFileOptions } from '../types/index.js';
6
7
 
7
8
  /** 匹配任务块的正则:<!-- CLAWT-TASKS:START --> ... <!-- CLAWT-TASKS:END --> */
@@ -11,7 +12,7 @@ const TASK_BLOCK_REGEX = /<!-- CLAWT-TASKS:START -->([\s\S]*?)<!-- CLAWT-TASKS:E
11
12
  const BRANCH_LINE_REGEX = /^#\s*branch:\s*(.+)$/;
12
13
 
13
14
  /** 任务列表为空时的错误提示 */
14
- const EMPTY_TASKS_MESSAGE = '任务列表不能为空';
15
+ const EMPTY_TASKS_MESSAGE = getCurrentLanguage() === 'en' ? 'Task list cannot be empty' : '任务列表不能为空';
15
16
 
16
17
  /**
17
18
  * 从命令行 --tasks 选项中解析出有效的任务列表
@@ -2,8 +2,9 @@ import { execFileSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { ClawtError } from '../errors/index.js';
4
4
  import { logger } from '../logger/index.js';
5
- import { VALID_TERMINAL_APPS, ITERM2_APP_PATH } from '../constants/index.js';
5
+ import { VALID_TERMINAL_APPS, ITERM2_APP_PATH, MESSAGES } from '../constants/index.js';
6
6
  import { getConfigValue } from './config.js';
7
+ import { getCurrentLanguage } from './i18n.js';
7
8
 
8
9
  /** 终端应用类型 */
9
10
  type TerminalApp = 'iterm2' | 'terminal' | 'cmux';
@@ -124,8 +125,7 @@ function openCommandInCmuxSurface(command: string, title: string): void {
124
125
  // 环境检查:只需要检查 WORKSPACE_ID
125
126
  if (!isCmuxEnvironment()) {
126
127
  throw new ClawtError(
127
- '当前不在 cmux 环境中,无法创建 surface\n' +
128
- '请确保在 cmux 终端中执行 clawt resume 命令,或修改 terminalApp 配置'
128
+ MESSAGES.NOT_IN_CMUX
129
129
  );
130
130
  }
131
131
 
@@ -152,7 +152,7 @@ function openCommandInCmuxSurface(command: string, title: string): void {
152
152
  // 需要灵活匹配
153
153
  const match = newSurfaceResult.match(/(?:OK\s+)?(surface:\d+)/i);
154
154
  if (!match) {
155
- throw new Error(`无法解析 cmux new-split 输出: ${newSurfaceResult}`);
155
+ throw new Error(getCurrentLanguage() === 'en' ? `Failed to parse cmux new-split output: ${newSurfaceResult}` : `无法解析 cmux new-split 输出: ${newSurfaceResult}`);
156
156
  }
157
157
  const surfaceRef = match[1];
158
158
 
@@ -173,7 +173,7 @@ function openCommandInCmuxSurface(command: string, title: string): void {
173
173
 
174
174
  } catch (error) {
175
175
  const message = error instanceof Error ? error.message : String(error);
176
- throw new ClawtError(`在 cmux 中创建 surface 失败: ${message}`);
176
+ throw new ClawtError(getCurrentLanguage() === 'en' ? `Failed to create surface in cmux: ${message}` : `在 cmux 中创建 surface 失败: ${message}`);
177
177
  }
178
178
  }
179
179
 
@@ -195,9 +195,9 @@ function executeAppleScript(script: string, terminalApp: 'iterm2' | 'terminal'):
195
195
  } catch (error) {
196
196
  const message = error instanceof Error ? error.message : String(error);
197
197
  const accessibilityHint = terminalApp === 'terminal'
198
- ? '\n提示:Terminal.app 需要辅助功能权限,请在「系统设置 → 隐私与安全性 → 辅助功能」中授权终端应用'
198
+ ? MESSAGES.TERMINAL_ACCESSIBILITY_HINT
199
199
  : '';
200
- throw new ClawtError(`打开终端 Tab 失败: ${message}${accessibilityHint}`);
200
+ throw new ClawtError(getCurrentLanguage() === 'en' ? `Failed to open terminal tab: ${message}${accessibilityHint}` : `打开终端 Tab 失败: ${message}${accessibilityHint}`);
201
201
  }
202
202
  }
203
203
 
@@ -212,7 +212,7 @@ function executeAppleScript(script: string, terminalApp: 'iterm2' | 'terminal'):
212
212
  */
213
213
  export function openCommandInNewTerminalTab(command: string, tabTitle: string): void {
214
214
  if (process.platform !== 'darwin') {
215
- throw new ClawtError('批量 resume 目前仅支持 macOS 平台');
215
+ throw new ClawtError(MESSAGES.BATCH_RESUME_MACOS_ONLY);
216
216
  }
217
217
 
218
218
  const terminalApp = detectTerminalApp();
@@ -230,6 +230,6 @@ export function openCommandInNewTerminalTab(command: string, tabTitle: string):
230
230
  executeAppleScript(terminalScript, 'terminal');
231
231
  break;
232
232
  default:
233
- throw new ClawtError(`不支持的终端类型: ${terminalApp}`);
233
+ throw new ClawtError(getCurrentLanguage() === 'en' ? `Unsupported terminal type: ${terminalApp}` : `不支持的终端类型: ${terminalApp}`);
234
234
  }
235
235
  }
@@ -5,6 +5,7 @@ import {
5
5
  } from '../constants/index.js';
6
6
  import type { WorktreeInfo } from '../types/index.js';
7
7
  import { groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap } from './worktree-matcher.js';
8
+ import { getCurrentLanguage } from './i18n.js';
8
9
  import { isNonInteractive } from './interactive.js';
9
10
  import { ClawtError } from '../errors/index.js';
10
11
 
@@ -45,7 +46,7 @@ export type GroupedChoice = { name: string; message: string } | MultiSelectSepar
45
46
  export async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
46
47
  // 非交互模式下无法进行交互选择,要求用户通过 -b 精确指定
47
48
  if (isNonInteractive()) {
48
- throw new ClawtError('非交互模式下无法进行分支选择,请通过 -b 参数精确指定分支名');
49
+ throw new ClawtError(getCurrentLanguage() === 'en' ? 'Non-interactive mode cannot select branch, please specify branch name via -b' : '非交互模式下无法进行分支选择,请通过 -b 参数精确指定分支名');
49
50
  }
50
51
  // @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
51
52
  const selectedBranch: string = await new Enquirer.Select({
@@ -70,7 +71,7 @@ export async function promptSelectBranch(worktrees: WorktreeInfo[], message: str
70
71
  export async function promptMultiSelectBranches(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo[]> {
71
72
  // 非交互模式下无法进行交互多选,要求用户通过 -b 精确指定
72
73
  if (isNonInteractive()) {
73
- throw new ClawtError('非交互模式下无法进行分支多选,请通过 -b 参数精确指定分支名');
74
+ throw new ClawtError(getCurrentLanguage() === 'en' ? 'Non-interactive mode cannot multi-select branches, please specify branch name via -b' : '非交互模式下无法进行分支多选,请通过 -b 参数精确指定分支名');
74
75
  }
75
76
  // 构建 choices 列表,顶部插入全选选项
76
77
  const branchChoices = worktrees.map((wt) => ({
@@ -143,7 +144,7 @@ export async function promptGroupedMultiSelectBranches(
143
144
  ): Promise<WorktreeInfo[]> {
144
145
  // 非交互模式下无法进行交互多选,要求用户通过 -b 精确指定
145
146
  if (isNonInteractive()) {
146
- throw new ClawtError('非交互模式下无法进行分支多选,请通过 -b 参数精确指定分支名');
147
+ throw new ClawtError(getCurrentLanguage() === 'en' ? 'Non-interactive mode cannot multi-select branches, please specify branch name via -b' : '非交互模式下无法进行分支多选,请通过 -b 参数精确指定分支名');
147
148
  }
148
149
  const groups = groupWorktreesByDate(worktrees);
149
150
  const choices = buildGroupedChoices(groups);
@@ -144,7 +144,7 @@ function printUpdateNotification(currentVersion: string, latestVersion: string):
144
144
  chalk.green(latestVersion),
145
145
  );
146
146
  const pm = detectPackageManager();
147
- const updateCommand = UPDATE_COMMANDS[pm] || UPDATE_COMMANDS.npm;
147
+ const updateCommand = (UPDATE_COMMANDS as Record<string, string>)[pm] || UPDATE_COMMANDS.npm;
148
148
  const commandText = UPDATE_MESSAGES.UPDATE_HINT(chalk.cyan(updateCommand));
149
149
 
150
150
  const updateTextWidth = stringWidth(updateText);