@miphamai/cli 0.19.4 → 0.20.0

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.
@@ -10,9 +10,23 @@ import type { SkillsLoader } from '../skills/loader'
10
10
  import type { PluginManager } from '../plugin/plugin-manager'
11
11
  import type { Message } from '../shared/types.js'
12
12
  import { McpClient } from '../mcp/client'
13
- import { NPM_INSTALL_COMMAND, NPM_UPDATE_COMMAND, PACKAGE_VERSION } from '../shared/index.ts'
13
+ import { NPM_UPDATE_COMMAND, PACKAGE_VERSION } from '../shared/index.ts'
14
14
  import { getPreference } from '../config/preferences'
15
15
  import { stripIndent } from './strip-indent.js'
16
+ import { createT } from '@mipham/shared/i18n/t'
17
+ import type { TranslationMap } from '@mipham/shared/i18n/types'
18
+ import enUS from '@mipham/shared/i18n/locales/en-US.json' with { type: 'json' }
19
+ import zhCN from '@mipham/shared/i18n/locales/zh-CN.json' with { type: 'json' }
20
+
21
+ // Default t() fallback for plain-function contexts (tests, bootstrap).
22
+ // When called from the React tree, ctx.t is populated from useI18n().
23
+ const defaultT = createT(enUS as unknown as TranslationMap, zhCN as unknown as TranslationMap)
24
+
25
+ /** Resolve the best available t() function: prefer ctx.t (locale-aware from React),
26
+ * fall back to module-level defaultT (en-US). */
27
+ function resolveT(ctx: CommandContext): (key: string, params?: Record<string, string>) => string {
28
+ return ctx.t && typeof ctx.t === 'function' ? ctx.t : defaultT
29
+ }
16
30
  import {
17
31
  initCmd,
18
32
  setupCmd,
@@ -42,6 +56,9 @@ export interface CommandContext {
42
56
  setUltracodeMode: (on: boolean) => void
43
57
  skillsLoader?: SkillsLoader
44
58
  pluginManager?: PluginManager
59
+ /** i18n translate function — populated from React tree via useI18n().
60
+ * Falls back to module-level defaultT (en-US) in tests / bootstrap. */
61
+ t: (key: string, params?: Record<string, string>) => string
45
62
  }
46
63
 
47
64
  export interface CommandResult {
@@ -228,7 +245,8 @@ const versionCmd: CommandHandler = (ctx) => {
228
245
 
229
246
  const clearCmd: CommandHandler = (ctx) => {
230
247
  ctx.engine.getContext().clear()
231
- return { content: '✓ Conversation cleared. Context reset.', clearMessages: true }
248
+ const t = resolveT(ctx)
249
+ return { content: t('commands.clear.confirmed'), clearMessages: true }
232
250
  }
233
251
 
234
252
  const exitCmd: CommandHandler = () => ({ content: '', exit: true })
@@ -238,63 +256,68 @@ const exitCmd: CommandHandler = () => ({ content: '', exit: true })
238
256
  // ═══════════════════════════════════════════════════════════════
239
257
 
240
258
  const compactCmd: CommandHandler = async (ctx) => {
259
+ const t = resolveT(ctx)
241
260
  const context = ctx.engine.getContext()
242
261
  const before = context.getEstimatedTokens()
243
262
  await context.compact('user requested compaction')
244
263
  const after = context.getEstimatedTokens()
245
264
  return {
246
- content: `✓ Context compacted.\nTokens: ${before.toLocaleString()} → ${after.toLocaleString()} (saved ${((1 - after / before) * 100).toFixed(0)}%)`,
265
+ content: `${t('commands.compact.confirmed')}\n${t('commands.compact.tokens', { before: before.toLocaleString(), after: after.toLocaleString(), saved: ((1 - after / before) * 100).toFixed(0) })}`,
247
266
  }
248
267
  }
249
268
 
250
269
  const contextCmd: CommandHandler = (ctx) => {
270
+ const t = resolveT(ctx)
251
271
  const c = ctx.engine.getContext()
252
272
  const tokens = c.getEstimatedTokens()
253
273
  const msgs = c.getMessages()
254
274
  const systemPromptLen = c.getSystemPrompt().length
255
275
  return {
256
276
  content: stripIndent`
257
- ── Context Stats ──
258
- Messages: ${msgs.length}
259
- Estimated tokens: ${tokens.toLocaleString()} / 200,000
260
- Usage: ${((tokens / 200_000) * 100).toFixed(1)}%
261
- System prompt: ${systemPromptLen.toLocaleString()} chars (~${Math.ceil(systemPromptLen / 4).toLocaleString()} tokens)
262
- Compaction: at 90% (${(200_000 * 0.9).toLocaleString()} tokens)
277
+ ${t('commands.context.title')}
278
+ ${t('commands.context.messages')} ${msgs.length}
279
+ ${t('commands.context.estimated_tokens')} ${tokens.toLocaleString()} / 200,000
280
+ ${t('commands.context.usage_pct')} ${((tokens / 200_000) * 100).toFixed(1)}%
281
+ ${t('commands.context.system_prompt')} ${systemPromptLen.toLocaleString()} chars (~${Math.ceil(systemPromptLen / 4).toLocaleString()} tokens)
282
+ ${t('commands.context.compaction')} at 90% (${(200_000 * 0.9).toLocaleString()} tokens)
263
283
  `,
264
284
  }
265
285
  }
266
286
 
267
287
  const statusCmd: CommandHandler = (ctx) => {
288
+ const t = resolveT(ctx)
268
289
  const c = ctx.engine.getContext()
269
290
  const tools = ctx.engine.getTools()
291
+ const runtime = typeof Bun !== 'undefined' ? 'Bun' : 'Node.js'
292
+ const runtimeVer = typeof Bun !== 'undefined' ? Bun.version : process.version
270
293
  return {
271
294
  content: stripIndent`
272
- ── Session Status ──
273
- Provider: ${ctx.providerId}
274
- Model: ${ctx.modelId}
275
- Messages: ${c.getMessages().length}
276
- Tokens: ~${c.getEstimatedTokens().toLocaleString()} / 200,000
277
- Tools: ${tools.size} loaded
278
- Permission: ${ctx.config.permission}
279
-
280
- ── System ──
281
- Platform: ${process.platform} ${process.arch}
282
- Runtime: ${typeof Bun !== 'undefined' ? 'Bun' : 'Node.js'} ${typeof Bun !== 'undefined' ? Bun.version : process.version}
283
- CWD: ${process.cwd()}
295
+ ${t('commands.status.session_title')}
296
+ ${t('commands.status.provider')} ${ctx.providerId}
297
+ ${t('commands.status.model')} ${ctx.modelId}
298
+ ${t('commands.status.messages')} ${c.getMessages().length}
299
+ ${t('commands.status.tokens')} ~${c.getEstimatedTokens().toLocaleString()} / 200,000
300
+ ${t('commands.status.tools')} ${tools.size} ${t('commands.status.loaded')}
301
+ ${t('commands.status.permission')} ${ctx.config.permission}
302
+
303
+ ${t('commands.status.system_title')}
304
+ ${t('commands.status.platform')} ${process.platform} ${process.arch}
305
+ ${t('commands.status.runtime')} ${runtime} ${runtimeVer}
306
+ ${t('commands.status.cwd')} ${process.cwd()}
284
307
  `,
285
308
  }
286
309
  }
287
310
 
288
311
  const costCmd: CommandHandler = (ctx) => {
312
+ const t = resolveT(ctx)
289
313
  const tokens = ctx.engine.getContext().getEstimatedTokens()
290
314
  return {
291
315
  content: stripIndent`
292
- ── Token Usage (estimated) ──
293
- Context tokens: ~${tokens.toLocaleString()} / 200,000
294
- Usage: ${((tokens / 200_000) * 100).toFixed(1)}%
316
+ ${t('commands.context_tokens.title')}
317
+ ${t('commands.context_tokens.context_tokens')} ~${tokens.toLocaleString()} / 200,000
318
+ ${t('commands.context_tokens.usage')} ${((tokens / 200_000) * 100).toFixed(1)}%
295
319
 
296
- Token counting is approximate (chars/4).
297
- Actual API usage depends on provider and model.
320
+ ${t('commands.context_tokens.footer')}
298
321
  `,
299
322
  }
300
323
  }
@@ -303,11 +326,13 @@ const costCmd: CommandHandler = (ctx) => {
303
326
  // Model & Provider
304
327
  // ═══════════════════════════════════════════════════════════════
305
328
 
306
- const modelCmd: CommandHandler = (ctx) => ({
307
- content: `Current model: ${ctx.modelId}\nProvider: ${ctx.providerId}\n\nUse /switch <provider> <model> to change.`,
308
- })
329
+ const modelCmd: CommandHandler = (ctx) => {
330
+ const t = resolveT(ctx)
331
+ return { content: t('commands.model.current', { model: ctx.modelId, provider: ctx.providerId }) }
332
+ }
309
333
 
310
334
  const modelsCmd: CommandHandler = (ctx) => {
335
+ const t = resolveT(ctx)
311
336
  const lines = ctx.config.providers
312
337
  .filter((p) => p.status !== 'upcoming')
313
338
  .flatMap((p) =>
@@ -320,40 +345,42 @@ const modelsCmd: CommandHandler = (ctx) => {
320
345
  )
321
346
 
322
347
  return {
323
- content: `Available models (${lines.length} active):\n\nProvider Model Context Vision\n${'-'.repeat(80)}\n${lines.join('\n')}\n\nUse /switch <provider> <model> to change.\nSee /providers for upcoming models.`,
348
+ content: `${t('commands.models.title', { count: String(lines.length) })}\n\n${t('commands.models.header')}\n${'-'.repeat(80)}\n${lines.join('\n')}\n\n${t('commands.models.hint')}`,
324
349
  }
325
350
  }
326
351
 
327
- const providerCmd: CommandHandler = (ctx) => ({
328
- content: `Current provider: ${ctx.providerId}\nModel: ${ctx.modelId}`,
329
- })
352
+ const providerCmd: CommandHandler = (ctx) => {
353
+ const t = resolveT(ctx)
354
+ return { content: t('commands.provider.current', { provider: ctx.providerId, model: ctx.modelId }) }
355
+ }
330
356
 
331
357
  const providersCmd: CommandHandler = (ctx) => {
358
+ const t = resolveT(ctx)
332
359
  const lines = ctx.config.providers.map(
333
360
  (p) =>
334
361
  ` ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol.padEnd(18)} ${p.models.length} models ${p.status === 'upcoming' ? '[upcoming]' : '✓'}`,
335
362
  )
336
363
  return {
337
- content: `Configured providers:\n\n${lines.join('\n')}\n\nCurrent: ${ctx.providerId}/${ctx.modelId}`,
364
+ content: `${t('commands.providers.title')}\n\n${lines.join('\n')}\n\n${t('commands.providers.current', { provider: ctx.providerId, model: ctx.modelId })}`,
338
365
  }
339
366
  }
340
367
 
341
368
  const switchCmd: CommandHandler = (ctx, args) => {
369
+ const t = resolveT(ctx)
342
370
  const [newProvider, newModel] = args
343
371
  if (!newProvider || !newModel) {
344
- return {
345
- content: 'Usage: /switch <provider> <model>\nExample: /switch deepseek deepseek-v4-pro',
346
- }
372
+ return { content: t('commands.switch.usage') }
347
373
  }
348
374
  ctx.engine.switchProvider(newProvider, newModel)
349
375
  return {
350
- content: `✓ Switched to ${newProvider}/${newModel}`,
376
+ content: t('commands.switch.confirmed', { provider: newProvider, model: newModel }),
351
377
  nextProvider: newProvider,
352
378
  nextModel: newModel,
353
379
  }
354
380
  }
355
381
 
356
382
  const configCmd: CommandHandler = (ctx) => {
383
+ const t = resolveT(ctx)
357
384
  const c = ctx.config
358
385
  const lines = [
359
386
  `version: ${c.version}`,
@@ -363,7 +390,7 @@ const configCmd: CommandHandler = (ctx) => {
363
390
  `providers: ${c.providers.length} configured`,
364
391
  ]
365
392
  return {
366
- content: `── Configuration ──\n${lines.join('\n')}\n\nEdit: ~/.mipham/config.yml or .mipham/config.yml`,
393
+ content: `${t('commands.config.title')}\n${lines.join('\n')}\n\n${t('commands.config.edit')}`,
367
394
  }
368
395
  }
369
396
 
@@ -372,6 +399,7 @@ const configCmd: CommandHandler = (ctx) => {
372
399
  // ═══════════════════════════════════════════════════════════════
373
400
 
374
401
  const toolsCmd: CommandHandler = (ctx) => {
402
+ const t = resolveT(ctx)
375
403
  const tools = ctx.engine.getTools()
376
404
  const categories: Record<string, string[]> = {
377
405
  file: [],
@@ -389,12 +417,13 @@ const toolsCmd: CommandHandler = (ctx) => {
389
417
  .filter(([, v]) => v!.length > 0)
390
418
  .map(([cat, items]) => `── ${cat.toUpperCase()} ──\n${items!.join('\n')}`)
391
419
 
392
- return { content: `Available tools (${tools.size}):\n\n${sections.join('\n\n')}` }
420
+ return { content: `${t('commands.tools.title', { count: String(tools.size) })}\n\n${sections.join('\n\n')}` }
393
421
  }
394
422
 
395
423
  const skillsCmd: CommandHandler = (ctx) => {
424
+ const t = resolveT(ctx)
396
425
  if (!ctx.skillsLoader) {
397
- return { content: 'SkillsLoader not available.' }
426
+ return { content: t('commands.skills.unavailable') }
398
427
  }
399
428
  const counts = ctx.skillsLoader.countByType()
400
429
  const standard = ctx.skillsLoader.listByType('standard')
@@ -407,7 +436,7 @@ const skillsCmd: CommandHandler = (ctx) => {
407
436
  `── Mipham Exclusive (${counts.mipham}) ──`,
408
437
  ...mipham.map((s) => ` ${s.name.padEnd(20)} ${s.description}`),
409
438
  '',
410
- `${counts.total} skills loaded. Use Skill tool to invoke.`,
439
+ t('commands.skills.loaded', { count: String(counts.total) }),
411
440
  ]
412
441
  return { content: lines.join('\n') }
413
442
  }
@@ -417,10 +446,10 @@ const skillsCmd: CommandHandler = (ctx) => {
417
446
  // ═══════════════════════════════════════════════════════════════
418
447
 
419
448
  const planCmd: CommandHandler = (_ctx, args) => {
449
+ const t = resolveT(_ctx)
420
450
  const description = args.join(' ') || undefined
421
451
  return {
422
- content:
423
- '── Plan Mode ──\n\nEntering plan mode — read-only analysis and design.\nUse EnterPlanMode to start, ExitPlanMode to submit for approval.',
452
+ content: t('commands.plan.content'),
424
453
  forwardToAI: description
425
454
  ? `Use EnterPlanMode with description: "${description}". Then explore, design, and use ExitPlanMode when ready for approval.`
426
455
  : 'Use EnterPlanMode to enter plan mode. Explore the codebase, design an approach, then use ExitPlanMode to submit for approval.',
@@ -428,18 +457,19 @@ const planCmd: CommandHandler = (_ctx, args) => {
428
457
  }
429
458
 
430
459
  const tddCmd: CommandHandler = (_ctx, args) => {
460
+ const t = resolveT(_ctx)
431
461
  const target = args.join(' ') || 'the current task'
432
462
  return {
433
463
  content: stripIndent`
434
- ── TDD Mode ──
435
- Starting Test-Driven Development workflow for: ${target}
464
+ ${t('commands.tdd.title')}
465
+ ${t('commands.tdd.start', { target })}
436
466
 
437
- Cycle: 🔴 RED → 🟢 GREEN → 🔵 REFACTOR
438
- RED: Write a failing test first
439
- GREEN: Write minimal code to pass
440
- REFACTOR: Clean up while keeping tests green
467
+ ${t('commands.tdd.cycle')}
468
+ ${t('commands.tdd.red')}
469
+ ${t('commands.tdd.green')}
470
+ ${t('commands.tdd.refactor')}
441
471
 
442
- The AI will guide you through each cycle.
472
+ ${t('commands.tdd.guide')}
443
473
  `,
444
474
  forwardToAI: `Follow the Test-Driven Development workflow for ${target}:
445
475
  1. RED — Write a failing test that defines the expected behavior. Use the project's test framework (Vitest/Jest/pytest). Show me the test code and confirm it fails.
@@ -450,17 +480,15 @@ Repeat for each behavior. Do NOT write implementation before tests.`,
450
480
  }
451
481
 
452
482
  const todosCmd: CommandHandler = (_ctx, args) => {
483
+ const t = resolveT(_ctx)
453
484
  const sub = args[0]
454
485
  if (sub === 'create') {
455
486
  const title = args.slice(1).join(' ')
456
487
  if (!title.trim()) {
457
- return {
458
- content:
459
- 'Usage: /todos create <task-title>\n\nExample: /todos create Add user authentication',
460
- }
488
+ return { content: t('commands.todos.usage_create') }
461
489
  }
462
490
  return {
463
- content: `── Create Task ──\n\nCreating task: "${title.trim()}"\n\nPassing to AI for structured task creation with TaskCreate...`,
491
+ content: `${t('commands.todos.create_title')}\n\nCreating task: "${title.trim()}"\n\nPassing to AI for structured task creation with TaskCreate...`,
464
492
  forwardToAI: `Create a new task using TaskCreate with subject "${title.trim()}". Set a clear description and activeForm.`,
465
493
  }
466
494
  }
@@ -468,12 +496,12 @@ const todosCmd: CommandHandler = (_ctx, args) => {
468
496
  if (sub === 'list' || !sub) {
469
497
  return {
470
498
  content: stripIndent`
471
- ── Task Management ──
472
- Fetching current task list...
499
+ ${t('commands.todos.list_title')}
500
+ ${t('commands.todos.list_fetching')}
473
501
 
474
502
  Shortcuts:
475
- /todos list Show all tasks
476
- /todos create <title> Create a new task
503
+ ${t('commands.todos.item_list')}
504
+ ${t('commands.todos.item_create')}
477
505
  `,
478
506
  forwardToAI:
479
507
  'Use TaskList to show all current tasks. Present them in a clear summary grouped by status (pending/in_progress/completed). If there are no tasks, suggest creating one.',
@@ -482,12 +510,11 @@ const todosCmd: CommandHandler = (_ctx, args) => {
482
510
 
483
511
  return {
484
512
  content: stripIndent`
485
- ── Task Management ──
486
- /todos list Show all tasks
487
- /todos create <title> Create a new task
513
+ ${t('commands.todos.default_title')}
514
+ ${t('commands.todos.item_list')}
515
+ ${t('commands.todos.item_create')}
488
516
 
489
- The AI manages task state via TaskCreate, TaskList, TaskUpdate, and TaskGet tools.
490
- Tasks appear in the /tasks view and persist across the session.
517
+ ${t('commands.todos.default_body')}
491
518
  `,
492
519
  forwardToAI: 'Use TaskList to show all current tasks, then present them clearly.',
493
520
  }
@@ -502,40 +529,30 @@ const todosCmd: CommandHandler = (_ctx, args) => {
502
529
  // ═══════════════════════════════════════════════════════════════
503
530
 
504
531
  const renameCmd: CommandHandler = (ctx, args) => {
532
+ const t = resolveT(ctx)
505
533
  const name = args.join(' ')
506
534
  if (!name.trim()) {
507
- return { content: 'Usage: /rename <session-name>\nExample: /rename Bug Fix Session' }
535
+ return { content: t('commands.rename.usage') }
508
536
  }
509
537
  ctx.setSessionTitle(name.trim())
510
- return { content: `✓ Session renamed to "${name.trim()}"` }
538
+ return { content: t('commands.rename.confirmed', { name: name.trim() }) }
511
539
  }
512
540
 
513
541
  const goalCmd: CommandHandler = (ctx, args) => {
542
+ const t = resolveT(ctx)
514
543
  const input = args.join(' ')
515
544
 
516
545
  // ── Show goal status ──
517
546
  if (!input.trim()) {
518
547
  const state = ctx.engine.getGoalState?.() || { goal: undefined, decompose: false, subtasks: [] }
519
548
  if (!state.goal) {
520
- return {
521
- content:
522
- `Usage: /goal <statement> [options]\n\n` +
523
- `Set a session-level completion condition with optional verification.\n\n` +
524
- `Options:\n` +
525
- ` --decompose Auto-decompose goal into subtasks\n` +
526
- ` --verify-script <path> Run a shell script to check completion\n` +
527
- ` --verify-skill <name> Use a skill for verification\n\n` +
528
- `Examples:\n` +
529
- ` /goal Fix all TS errors and make tests pass --decompose\n` +
530
- ` /goal Complete the build --verify-script ./check-build.sh\n` +
531
- ` /goal Security audit passes --verify-skill security-review`,
532
- }
549
+ return { content: t('commands.goal.usage') }
533
550
  }
534
- const lines = [`── Goal Status ──`, '', `🎯 Goal: ${state.goal}`]
535
- if (state.verifyScript) lines.push(`📜 Verification: ${state.verifyScript}`)
536
- if (state.verifySkill) lines.push(`🛠 Verification skill: ${state.verifySkill}`)
537
- if (state.decompose) lines.push(`📋 Decompose: enabled (${state.subtasks.length} subtasks)`)
538
- lines.push('', 'Type /goal without arguments to clear.')
551
+ const lines = [t('commands.goal.status_title'), '', t('commands.goal.status_goal', { goal: state.goal })]
552
+ if (state.verifyScript) lines.push(t('commands.goal.status_verify_script', { script: state.verifyScript }))
553
+ if (state.verifySkill) lines.push(t('commands.goal.status_verify_skill', { skill: state.verifySkill }))
554
+ if (state.decompose) lines.push(t('commands.goal.status_decompose', { count: String(state.subtasks.length) }))
555
+ lines.push('', t('commands.goal.status_clear_hint'))
539
556
  return { content: lines.join('\n') }
540
557
  }
541
558
 
@@ -567,15 +584,15 @@ const goalCmd: CommandHandler = (ctx, args) => {
567
584
 
568
585
  goal = goal.trim()
569
586
  if (!goal) {
570
- return { content: 'Goal text is required. Use /goal without arguments to see usage.' }
587
+ return { content: t('commands.goal.empty_set') }
571
588
  }
572
589
 
573
590
  ctx.setGoal(goal)
574
591
  ctx.engine.setGoal(goal, { verifyScript, verifySkill, decompose })
575
592
 
576
- const lines = [`✓ Goal set: "${goal}"`]
593
+ const lines = [t('commands.goal.set', { goal })]
577
594
  if (decompose) {
578
- lines.push('📋 Decomposition: enabled — AI will break goal into subtasks')
595
+ lines.push(t('commands.goal.decompose_enabled'))
579
596
  // Decompose by creating initial subtasks
580
597
  const decomposeMsg = `Break down this goal into 3-5 subtasks: "${goal}". For each subtask, use TaskCreate with the subject and description. Mark each as blocked by the previous one to create a dependency chain.`
581
598
  return {
@@ -583,27 +600,26 @@ const goalCmd: CommandHandler = (ctx, args) => {
583
600
  forwardToAI: decomposeMsg,
584
601
  }
585
602
  }
586
- if (verifyScript) lines.push(`📜 Verification: ${verifyScript}`)
587
- if (verifySkill) lines.push(`🛠 Verification skill: ${verifySkill}`)
603
+ if (verifyScript) lines.push(t('commands.goal.status_verify_script', { script: verifyScript }))
604
+ if (verifySkill) lines.push(t('commands.goal.status_verify_skill', { skill: verifySkill }))
588
605
  lines.push('')
589
- lines.push('Use /status to view progress. Type /goal without arguments to clear.')
606
+ lines.push(t('commands.goal.status_hint'))
590
607
 
591
608
  return { content: lines.join('\n') }
592
609
  }
593
610
 
594
611
  const recapCmd: CommandHandler = (ctx) => {
612
+ const t = resolveT(ctx)
595
613
  const c = ctx.engine.getContext()
596
614
  const msgs = c.getMessages()
597
615
  if (msgs.length === 0) {
598
- return { content: 'No conversation to recap.' }
616
+ return { content: t('commands.recap.no_conversation') }
599
617
  }
600
- // Show summary of conversation: count messages, roles, estimated tokens
601
618
  const userMsgs = msgs.filter((m) => m.role === 'user').length
602
619
  const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
603
620
  const tokens = c.getEstimatedTokens()
604
621
  const checkpointCount = c.getCheckpoints().length
605
622
 
606
- // Extract first few user messages as "topics"
607
623
  const topics = msgs
608
624
  .filter((m) => m.role === 'user' && typeof m.content === 'string')
609
625
  .slice(0, 5)
@@ -614,20 +630,21 @@ const recapCmd: CommandHandler = (ctx) => {
614
630
 
615
631
  return {
616
632
  content: stripIndent`
617
- ── Session Recap ──
618
- Messages: ${msgs.length} (${userMsgs} user, ${assistantMsgs} assistant)
619
- Est. tokens: ~${tokens.toLocaleString()}
620
- Checkpoints: ${checkpointCount}
633
+ ${t('commands.recap.title')}
634
+ ${t('commands.recap.messages')} ${msgs.length} (${userMsgs} user, ${assistantMsgs} assistant)
635
+ ${t('commands.recap.est_tokens')} ~${tokens.toLocaleString()}
636
+ ${t('commands.recap.checkpoints')} ${checkpointCount}
621
637
 
622
- Recent topics:
638
+ ${t('commands.recap.recent_topics')}
623
639
  ${topics.map((t, i) => ` ${i + 1}. ${t}`).join('\n')}
624
640
 
625
- Use /rewind to undo, /clear to reset, /export to save.
641
+ ${t('commands.recap.footer')}
626
642
  `,
627
643
  }
628
644
  }
629
645
 
630
646
  const usageCmd: CommandHandler = (ctx) => {
647
+ const t = resolveT(ctx)
631
648
  const c = ctx.engine.getContext()
632
649
  const estTokens = c.getEstimatedTokens()
633
650
  const msgs = c.getMessages()
@@ -653,46 +670,45 @@ const usageCmd: CommandHandler = (ctx) => {
653
670
  const apiTotal = summary.apiInputTokens + summary.apiOutputTokens
654
671
  const apiLine =
655
672
  summary.apiInputTokens > 0 || summary.apiOutputTokens > 0
656
- ? `API tokens: ${summary.apiInputTokens.toLocaleString().padStart(8)} in / ${summary.apiOutputTokens.toLocaleString().padStart(6)} out (${apiTotal.toLocaleString()} total)`
657
- : 'API tokens: (no API usage data yet)'
673
+ ? t('commands.usage.api_tokens', { in: summary.apiInputTokens.toLocaleString(), out: summary.apiOutputTokens.toLocaleString(), total: apiTotal.toLocaleString() })
674
+ : t('commands.usage.no_api')
658
675
 
659
- const toolSection = toolLines.length > 0 ? `\n── Per-Tool ──\n${toolLines.join('\n')}` : ''
676
+ const toolSection = toolLines.length > 0 ? `\n${t('commands.usage.tool_section')}\n${toolLines.join('\n')}` : ''
660
677
 
661
678
  return {
662
679
  content: stripIndent`
663
- ── Usage Dashboard ──
680
+ ${t('commands.usage.title')}
664
681
  ${apiLine}
665
- Context tokens: ~${estTokens.toLocaleString()} / ${maxTokens.toLocaleString()} (${pct}%)
666
- Messages: ${msgs.length}
667
- Provider: ${ctx.providerId}
668
- Model: ${ctx.modelId}
682
+ ${t('commands.usage.context_line', { est: estTokens.toLocaleString(), max: maxTokens.toLocaleString(), pct })}
683
+ ${t('commands.usage.messages_line', { count: String(msgs.length) })}
684
+ ${t('commands.usage.provider_line', { provider: ctx.providerId })}
685
+ ${t('commands.usage.model_line', { model: ctx.modelId })}
669
686
 
670
687
  ${'█'.repeat(Math.ceil(Number(pct) / 5))}${'░'.repeat(20 - Math.ceil(Number(pct) / 5))} ${pct}%
671
688
  ${toolSection}
672
689
 
673
- Use /context for detailed stats, /compact to free space.
690
+ ${t('commands.usage.hint')}
674
691
  `,
675
692
  }
676
693
  }
677
694
 
678
695
  const reloadSkillsCmd: CommandHandler = (ctx) => {
696
+ const t = resolveT(ctx)
679
697
  if (!ctx.skillsLoader) {
680
- return { content: 'SkillsLoader not available in this context.' }
698
+ return { content: t('commands.reload_skills.unavailable') }
681
699
  }
682
700
  try {
683
701
  const config = ctx.config
684
- // Re-load builtin skills from the skills directory
685
- // The base path is typically relative to the CLI package
686
702
  ctx.skillsLoader.loadBuiltin(process.cwd())
687
703
  if (config.skills?.paths) {
688
704
  ctx.skillsLoader.loadExternal(config.skills.paths)
689
705
  }
690
706
  const skills = ctx.skillsLoader.list()
691
707
  return {
692
- content: `✓ Skills reloaded — ${skills.length} loaded.\n\n${skills.map((s) => ` ${s.name.padEnd(28)} ${s.type.padEnd(10)} ${s.description}`).join('\n')}`,
708
+ content: `${t('commands.reload_skills.confirmed', { count: String(skills.length) })}\n\n${skills.map((s) => ` ${s.name.padEnd(28)} ${s.type.padEnd(10)} ${s.description}`).join('\n')}`,
693
709
  }
694
710
  } catch (err) {
695
- return { content: `Failed to reload skills: ${String(err)}` }
711
+ return { content: t('commands.reload_skills.failed', { error: String(err) }) }
696
712
  }
697
713
  }
698
714
 
@@ -700,7 +716,8 @@ const reloadSkillsCmd: CommandHandler = (ctx) => {
700
716
  // Skill Marketplace — Community skill registry + installation
701
717
  // ═══════════════════════════════════════════════════════════════
702
718
 
703
- const browseSkillsCmd: CommandHandler = async () => {
719
+ const browseSkillsCmd: CommandHandler = async (ctx) => {
720
+ const _t = resolveT(ctx)
704
721
  const { getAvailableSkills, listInstalledSkills } = await import('../skills/registry')
705
722
  const available = getAvailableSkills()
706
723
  const installed = new Set(
@@ -780,29 +797,19 @@ const removeSkillCmd: CommandHandler = async (_ctx, args) => {
780
797
  // ═══════════════════════════════════════════════════════════════
781
798
 
782
799
  const pluginsCmd: CommandHandler = (ctx) => {
800
+ const t = resolveT(ctx)
783
801
  const manager = ctx.pluginManager
784
802
  if (!manager) {
785
- return { content: 'PluginManager not available in this session.' }
803
+ return { content: t('commands.plugins.unavailable') }
786
804
  }
787
805
 
788
806
  const plugins = manager.list()
789
807
  if (plugins.length === 0) {
790
- return {
791
- content: [
792
- '── Installed Plugins ──',
793
- '',
794
- 'No plugins installed.',
795
- '',
796
- 'Install plugins:',
797
- ' /install-plugin <npm-package> Install from npm registry',
798
- ' /browse-plugins Browse community plugins',
799
- ' mipham plugin install <path> Install from local directory',
800
- ].join('\n'),
801
- }
808
+ return { content: t('commands.plugins.none') }
802
809
  }
803
810
 
804
811
  const lines: string[] = [
805
- '── Installed Plugins ──',
812
+ t('commands.plugins.title'),
806
813
  '',
807
814
  ...plugins.map((p) => {
808
815
  const status = p.enabled ? '✅ enabled' : '⛔ disabled'
@@ -955,29 +962,21 @@ const pluginDisableCmd: CommandHandler = (ctx, args) => {
955
962
  // ═══════════════════════════════════════════════════════════════
956
963
 
957
964
  const rewindCmd: CommandHandler = (ctx) => {
965
+ const t = resolveT(ctx)
958
966
  const c = ctx.engine.getContext()
959
967
  const checkpoints = c.getCheckpoints()
960
968
 
961
969
  if (checkpoints.length === 0) {
962
- return {
963
- content:
964
- 'No checkpoints available. Checkpoints are automatically saved after each AI response.',
965
- }
970
+ return { content: t('commands.rewind.no_checkpoints') }
966
971
  }
967
972
 
968
973
  const result = c.restoreCheckpoint()
969
974
  if (!result.restored) {
970
- return { content: 'No checkpoint to restore.' }
975
+ return { content: t('commands.rewind.no_restore') }
971
976
  }
972
977
 
973
978
  return {
974
- content: stripIndent`
975
- ✓ Rewound to checkpoint "${result.label}"
976
- Restored ${result.messageCount} messages.
977
-
978
- Remaining checkpoints: ${c.getCheckpoints().length}
979
- Use /rewind again to go back further, or continue chatting.
980
- `,
979
+ content: t('commands.rewind.confirmed', { label: result.label, count: String(result.messageCount), remaining: String(c.getCheckpoints().length) }),
981
980
  clearMessages: true,
982
981
  }
983
982
  }
@@ -985,12 +984,13 @@ const rewindCmd: CommandHandler = (ctx) => {
985
984
  const undoCmd: CommandHandler = rewindCmd
986
985
 
987
986
  const copyCmd: CommandHandler = (ctx, args) => {
987
+ const t = resolveT(ctx)
988
988
  const c = ctx.engine.getContext()
989
989
  const msgs = c.getMessages()
990
990
  const assistantMsgs = msgs.filter((m) => m.role === 'assistant')
991
991
 
992
992
  if (assistantMsgs.length === 0) {
993
- return { content: 'No assistant responses to copy.' }
993
+ return { content: t('commands.copy.no_responses') }
994
994
  }
995
995
 
996
996
  // Determine which response to copy: last N or last 1
@@ -998,9 +998,7 @@ const copyCmd: CommandHandler = (ctx, args) => {
998
998
  if (args[0]) {
999
999
  n = parseInt(args[0]!, 10)
1000
1000
  if (isNaN(n) || n < 1) {
1001
- return {
1002
- content: 'Usage: /copy [N]\nN = number of recent assistant responses to copy (default: 1)',
1003
- }
1001
+ return { content: t('commands.copy.usage') }
1004
1002
  }
1005
1003
  }
1006
1004
 
@@ -1010,17 +1008,18 @@ const copyCmd: CommandHandler = (ctx, args) => {
1010
1008
  .join('\n\n---\n\n')
1011
1009
 
1012
1010
  return {
1013
- content: `✓ Copied ${toCopy.length} assistant response(s) to clipboard (${text.length.toLocaleString()} chars).`,
1011
+ content: t('commands.copy.confirmed', { count: String(toCopy.length) }),
1014
1012
  copyContent: text,
1015
1013
  }
1016
1014
  }
1017
1015
 
1018
1016
  const diffCmd: CommandHandler = async (_ctx) => {
1017
+ const t = resolveT(_ctx)
1019
1018
  try {
1020
1019
  const { execSync } = await import('node:child_process')
1021
1020
  const output = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 })
1022
1021
  if (!output.trim()) {
1023
- return { content: 'No uncommitted changes (working tree clean).' }
1022
+ return { content: t('commands.diff.clean') }
1024
1023
  }
1025
1024
  // Get full diff but limit to reasonable size
1026
1025
  const fullDiff = execSync('git diff --no-color', { encoding: 'utf-8', timeout: 5000 })
@@ -1033,11 +1032,11 @@ const diffCmd: CommandHandler = async (_ctx) => {
1033
1032
  : fullDiff
1034
1033
 
1035
1034
  return {
1036
- content: `── Git Diff ──\n\n${truncated}`,
1035
+ content: `${t('commands.diff.title')}\n\n${truncated}`,
1037
1036
  }
1038
1037
  } catch {
1039
1038
  return {
1040
- content: 'Unable to run git diff. Ensure git is installed and you are in a repository.',
1039
+ content: t('commands.diff.error'),
1041
1040
  }
1042
1041
  }
1043
1042
  }
@@ -1047,60 +1046,38 @@ const diffCmd: CommandHandler = async (_ctx) => {
1047
1046
  // ═══════════════════════════════════════════════════════════════
1048
1047
 
1049
1048
  const fastCmd: CommandHandler = (ctx, args) => {
1049
+ const t = resolveT(ctx)
1050
1050
  const arg = args[0]?.toLowerCase()
1051
1051
  if (arg === 'on') {
1052
1052
  ctx.setFastMode(true)
1053
- return { content: '✓ Fast mode ON — responses will prioritize speed over depth.' }
1053
+ return { content: t('commands.fast.on') }
1054
1054
  } else if (arg === 'off') {
1055
1055
  ctx.setFastMode(false)
1056
- return { content: '✓ Fast mode OFF — standard quality mode.' }
1056
+ return { content: t('commands.fast.off') }
1057
1057
  } else if (arg) {
1058
- return { content: 'Usage: /fast [on|off]\nToggle fast mode for faster responses.' }
1058
+ return { content: t('commands.fast.usage') }
1059
1059
  } else {
1060
- // Toggle
1061
- // We can't read current state from context, so we just show usage
1062
- return {
1063
- content:
1064
- 'Usage: /fast [on|off]\n\nFast mode prioritizes speed over depth. Currently available as a configuration toggle.\n\nExample:\n /fast on — enable fast mode\n /fast off — disable fast mode',
1065
- }
1060
+ return { content: t('commands.fast.unknown') }
1066
1061
  }
1067
1062
  }
1068
1063
 
1069
1064
  const effortCmd: CommandHandler = (ctx, args) => {
1065
+ const t = resolveT(ctx)
1070
1066
  const VALID_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
1071
1067
  const level = args[0]?.toLowerCase()
1072
1068
 
1073
1069
  if (!level || !VALID_LEVELS.includes(level)) {
1074
- return {
1075
- content: stripIndent`
1076
- Usage: /effort <level>
1077
-
1078
- Set reasoning effort level:
1079
- low — Fast, simple tasks
1080
- medium — Balanced quality and speed
1081
- high — Thorough reasoning (default)
1082
- xhigh — Maximum depth for coding/agentic use
1083
- max — Absolute ceiling, very thorough
1084
-
1085
- Current model: ${ctx.modelId}
1086
- Effort levels require compatible providers (Anthropic Opus 4.6+, Sonnet 4.6).
1087
- `,
1088
- }
1070
+ return { content: t('commands.effort.usage', { model: ctx.modelId }) }
1089
1071
  }
1090
1072
 
1091
1073
  ctx.setEffort(level)
1092
- return { content: `✓ Reasoning effort set to "${level}"` }
1074
+ return { content: t('commands.effort.confirmed', { level }) }
1093
1075
  }
1094
1076
 
1095
1077
  const focusCmd: CommandHandler = (ctx) => {
1078
+ const t = resolveT(ctx)
1096
1079
  ctx.setFocusMode(true)
1097
- return {
1098
- content: stripIndent`
1099
- ✓ Focus mode ON — showing only the most recent exchange.
1100
- Previous messages are hidden but preserved.
1101
- Type /focus again to exit focus mode and show all messages.
1102
- `,
1103
- }
1080
+ return { content: t('commands.focus.on') }
1104
1081
  }
1105
1082
 
1106
1083
  // ═══════════════════════════════════════════════════════════════
@@ -1108,12 +1085,12 @@ const focusCmd: CommandHandler = (ctx) => {
1108
1085
  // ═══════════════════════════════════════════════════════════════
1109
1086
 
1110
1087
  const ultracodeCmd: CommandHandler = (ctx, args) => {
1088
+ const t = resolveT(ctx)
1111
1089
  const arg = args[0]?.toLowerCase()
1112
1090
  if (arg === 'on') {
1113
1091
  ctx.setUltracodeMode(true)
1114
1092
  return {
1115
- content:
1116
- '✓ Ultracode mode ON — multi-agent workflow orchestration enabled.\n\nThe AI will use Workflow tool to fan out, verify, and synthesize for all substantive tasks.',
1093
+ content: t('commands.ultracode.on'),
1117
1094
  forwardToAI: stripIndent`
1118
1095
  Ultracode mode is now ACTIVE. For every substantive task:
1119
1096
  - Use the Workflow tool to orchestrate multi-agent execution
@@ -1129,24 +1106,12 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
1129
1106
  } else if (arg === 'off') {
1130
1107
  ctx.setUltracodeMode(false)
1131
1108
  return {
1132
- content: '✓ Ultracode mode OFF.',
1109
+ content: t('commands.ultracode.off'),
1133
1110
  forwardToAI:
1134
1111
  'Ultracode mode is now OFF. Revert to standard single-agent execution. Do NOT use Workflow tool unless explicitly asked.',
1135
1112
  }
1136
1113
  }
1137
- return {
1138
- content: stripIndent`
1139
- Usage: /ultracode [on|off]
1140
-
1141
- Ultracode mode enables multi-agent workflow orchestration for every substantive task.
1142
- When ON, the AI uses the Workflow tool to decompose tasks, fan out parallel agents,
1143
- verify findings adversarially, and synthesize results with a top-tier model.
1144
-
1145
- Examples:
1146
- /ultracode on — enable multi-agent mode
1147
- /ultracode off — return to standard single-agent mode
1148
- `,
1149
- }
1114
+ return { content: t('commands.ultracode.usage') }
1150
1115
  }
1151
1116
 
1152
1117
  // ═══════════════════════════════════════════════════════════════
@@ -1154,6 +1119,7 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
1154
1119
  // ═══════════════════════════════════════════════════════════════
1155
1120
 
1156
1121
  const tasksCmd: CommandHandler = (ctx) => {
1122
+ const t = resolveT(ctx)
1157
1123
  const c = ctx.engine.getContext()
1158
1124
  const msgs = c.getMessages()
1159
1125
 
@@ -1169,15 +1135,11 @@ const tasksCmd: CommandHandler = (ctx) => {
1169
1135
 
1170
1136
  return {
1171
1137
  content: stripIndent`
1172
- ── Background Tasks ──
1138
+ ${t('commands.task_list.title')}
1173
1139
 
1174
- ${
1175
- toolUses.length > 0
1176
- ? `${toolUses.length} task operations detected in this session.\n\nUse Task tool (TaskCreate / TaskUpdate / TaskList) to manage structured task tracking.`
1177
- : 'No tasks tracked yet. Use TaskCreate, TaskUpdate, and TaskList tools to manage structured tasks.'
1178
- }
1140
+ ${toolUses.length > 0 ? t('commands.task_list.detected', { count: String(toolUses.length) }) : t('commands.task_list.no_tasks')}
1179
1141
 
1180
- Quick reference:
1142
+ ${t('commands.task_list.reference')}
1181
1143
  TaskCreate — create a new task
1182
1144
  TaskList — list all tasks
1183
1145
  TaskUpdate — update task status
@@ -1185,35 +1147,24 @@ const tasksCmd: CommandHandler = (ctx) => {
1185
1147
  TaskOutput — get background task output
1186
1148
  TaskStop — stop a running task
1187
1149
 
1188
- Type /todos for the legacy task interface.
1150
+ ${t('commands.task_list.legacy_hint')}
1189
1151
  `,
1190
1152
  }
1191
1153
  }
1192
1154
 
1193
1155
  const branchCmd: CommandHandler = (ctx, args) => {
1156
+ const t = resolveT(ctx)
1194
1157
  const name = args.join(' ') || `branch-${Date.now()}`
1195
1158
  const c = ctx.engine.getContext()
1196
1159
  const msgs = c.getMessages()
1197
1160
 
1198
1161
  if (msgs.length === 0) {
1199
- return { content: 'No conversation to branch. Start a conversation first.' }
1162
+ return { content: t('commands.branch.no_conversation') }
1200
1163
  }
1201
1164
 
1202
- // Save current session state as a named checkpoint
1203
1165
  const checkpointId = c.saveCheckpoint(name)
1204
1166
  return {
1205
- content: stripIndent`
1206
- ── Branch Created ──
1207
- Name: "${name}"
1208
- Checkpoint: #${checkpointId}
1209
- Messages: ${msgs.length} saved
1210
-
1211
- Current conversation continues from this point.
1212
- To return to this branch point later, use:
1213
- /rewind
1214
-
1215
- Note: Full session branching (separate concurrent sessions) requires session persistence, coming in a future release. For now, this saves a named checkpoint you can rewind to.
1216
- `,
1167
+ content: t('commands.branch.details', { name, checkpoint: String(checkpointId), messages: String(msgs.length) }),
1217
1168
  }
1218
1169
  }
1219
1170
 
@@ -1450,8 +1401,9 @@ const scheduleCmd: CommandHandler = async (_ctx) => {
1450
1401
  // ═══════════════════════════════════════════════════════════════
1451
1402
 
1452
1403
  const doctorCmd: CommandHandler = async (ctx) => {
1404
+ const t = resolveT(ctx)
1453
1405
  const lines: string[] = [
1454
- '── System Diagnostics ──',
1406
+ t('commands.doctor.title'),
1455
1407
  '',
1456
1408
  `Mipham Code v${ctx.version}`,
1457
1409
  `Runtime ${typeof Bun !== 'undefined' ? 'Bun ' + Bun.version : 'Node.js ' + process.version}`,
@@ -1459,12 +1411,12 @@ const doctorCmd: CommandHandler = async (ctx) => {
1459
1411
  `CWD ${process.cwd()}`,
1460
1412
  `PID ${process.pid}`,
1461
1413
  '',
1462
- '── Config ──',
1414
+ t('commands.doctor.config_section'),
1463
1415
  `Provider ${ctx.providerId} / ${ctx.modelId}`,
1464
1416
  `Permission ${ctx.config.permission}`,
1465
1417
  `Providers ${ctx.config.providers.length} configured (${ctx.config.providers.filter((p) => p.status !== 'upcoming').length} active)`,
1466
1418
  '',
1467
- '── Session ──',
1419
+ t('commands.doctor.session_section'),
1468
1420
  ]
1469
1421
 
1470
1422
  const c = ctx.engine.getContext()
@@ -1480,7 +1432,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
1480
1432
  try {
1481
1433
  const { execSync } = await import('node:child_process')
1482
1434
  lines.push('')
1483
- lines.push('── Git ──')
1435
+ lines.push(t('commands.doctor.git_section'))
1484
1436
  const branch = execSync('git branch --show-current', {
1485
1437
  encoding: 'utf-8',
1486
1438
  timeout: 3000,
@@ -1500,21 +1452,21 @@ const doctorCmd: CommandHandler = async (ctx) => {
1500
1452
  )
1501
1453
  } catch {
1502
1454
  lines.push('')
1503
- lines.push('── Git ──')
1504
- lines.push(' (not a git repository or git not available)')
1455
+ lines.push(t('commands.doctor.git_section'))
1456
+ lines.push(t('commands.doctor.no_git'))
1505
1457
  }
1506
1458
 
1507
1459
  // Skills info
1508
1460
  if (ctx.skillsLoader) {
1509
1461
  lines.push('')
1510
- lines.push('── Skills ──')
1462
+ lines.push(t('commands.doctor.skills_section'))
1511
1463
  try {
1512
1464
  const skills = ctx.skillsLoader.list()
1513
1465
  const standard = skills.filter((s: { type: string }) => s.type === 'standard').length
1514
1466
  const mipham = skills.filter((s: { type: string }) => s.type === 'mipham').length
1515
1467
  lines.push(`Loaded ${skills.length} (${standard} standard + ${mipham} mipham)`)
1516
1468
  } catch {
1517
- lines.push(' (skills info unavailable)')
1469
+ lines.push(t('commands.doctor.skills_unavailable'))
1518
1470
  }
1519
1471
  }
1520
1472
 
@@ -1527,68 +1479,67 @@ const doctorCmd: CommandHandler = async (ctx) => {
1527
1479
 
1528
1480
  function gitDiffBridgeCmd(opts: {
1529
1481
  label: string
1530
- noChangesHint: string
1531
- runningMsg: string
1482
+ noChangesKey: string
1483
+ runningKey: string
1484
+ errorKey: string
1532
1485
  forwardToAI: string | (() => string)
1533
1486
  }): CommandHandler {
1534
- return async () => {
1487
+ return async (ctx) => {
1488
+ const t = resolveT(ctx)
1535
1489
  try {
1536
1490
  const { execSync } = await import('node:child_process')
1537
1491
  const diff = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 }).trim()
1538
1492
  if (!diff) {
1539
- return { content: `─ ${opts.label} ─\n\n${opts.noChangesHint}` }
1493
+ return { content: `─ ${opts.label} ─\n\n${t(opts.noChangesKey)}` }
1540
1494
  }
1541
1495
  return {
1542
- content: `─ ${opts.label} ─\n\n${opts.runningMsg}\n\nChanged files:\n${diff}`,
1496
+ content: `─ ${opts.label} ─\n\n${t(opts.runningKey)}\n\nChanged files:\n${diff}`,
1543
1497
  forwardToAI: typeof opts.forwardToAI === 'function' ? opts.forwardToAI() : opts.forwardToAI,
1544
1498
  }
1545
1499
  } catch {
1546
- return {
1547
- content: `─ ${opts.label} ─\n\nCould not detect changes. Are you in a git repository?`,
1548
- }
1500
+ return { content: `─ ${opts.label} ─\n\n${t(opts.errorKey)}` }
1549
1501
  }
1550
1502
  }
1551
1503
  }
1552
1504
 
1553
1505
  const codeReviewCmd = gitDiffBridgeCmd({
1554
1506
  label: 'Code Review',
1555
- noChangesHint:
1556
- 'No uncommitted changes to review.\n\nTo review a specific file: /code-review path/to/file.ts',
1557
- runningMsg:
1558
- 'Reviewing uncommitted changes with the code-review skill (7 dimensions: correctness, security, performance, code quality, architecture, testing, language-specific)...',
1507
+ noChangesKey: 'commands.code_review.no_changes',
1508
+ runningKey: 'commands.code_review.running',
1509
+ errorKey: 'commands.code_review.error',
1559
1510
  forwardToAI: () =>
1560
1511
  `use the code-review skill to review all uncommitted changes. Check all 7 dimensions: correctness, security, performance, code quality, architecture & design, testing, and language-specific issues. Use effort level: ${getPreference('lastCodeReviewEffort', 'high')}.`,
1561
1512
  })
1562
1513
 
1563
1514
  const simplifyCmd = gitDiffBridgeCmd({
1564
1515
  label: 'Simplify',
1565
- noChangesHint:
1566
- 'No uncommitted changes to simplify.\n\nMake changes first, then run /simplify for cleanup review.',
1567
- runningMsg:
1568
- 'Running cleanup review — 4 passes: reuse, simplification, efficiency, abstraction level...',
1516
+ noChangesKey: 'commands.simplify.no_changes',
1517
+ runningKey: 'commands.simplify.running',
1518
+ errorKey: 'commands.simplify.error',
1569
1519
  forwardToAI:
1570
1520
  'use the self-review skill to review these uncommitted changes. Focus on 4 cleanup passes: 1) Reuse — find duplicated logic, replace with existing helpers; 2) Simplification — flatten nesting, remove redundant state and dead code; 3) Efficiency — fix repeated object creation, unnecessary I/O, memory issues; 4) Abstraction Level — ensure code sits at the right architectural layer. Apply equivalent transformations only — do NOT change logic or fix bugs.',
1571
1521
  })
1572
1522
 
1573
1523
  const verifyCmd = gitDiffBridgeCmd({
1574
1524
  label: 'Verify',
1575
- noChangesHint:
1576
- 'No uncommitted changes to verify.\n\nMake changes first, then run /verify for runtime verification.',
1577
- runningMsg:
1578
- 'Running runtime verification — observing actual execution behavior (not tests, not typecheck)...',
1525
+ noChangesKey: 'commands.verify.no_changes',
1526
+ runningKey: 'commands.verify.running',
1527
+ errorKey: 'commands.verify.error',
1579
1528
  forwardToAI:
1580
1529
  'verify these uncommitted changes through runtime observation only. For each change: 1) Find the user-facing surface (CLI command, API endpoint, UI interaction); 2) Drive the changed code to execute; 3) Push boundaries — pass null, repeated values, wrong types, interrupt mid-flow (Ctrl-C), resize window; 4) Report verdict per change: PASS (works as expected), FAIL (does not work or breaks something), BLOCKED (cannot reach observable state), SKIP (no runtime surface, e.g. pure documentation). Do NOT run the test suite — observe real execution behavior only.',
1581
1530
  })
1582
1531
 
1583
1532
  const designCmd: CommandHandler = (_ctx, args) => {
1533
+ const t = resolveT(_ctx)
1584
1534
  const topic = args.join(' ') || 'the current task'
1585
1535
  return {
1586
- content: `─ Design ─\n\nStarting architectural design session for: ${topic}\n\nExploring approaches, trade-offs, component breakdown, data flow...`,
1536
+ content: t('commands.design.start', { topic }),
1587
1537
  forwardToAI: `help me design the architecture for ${topic}. Explore 2-3 approaches with trade-offs, then present a design covering: component breakdown, data flow, interfaces between components, error handling strategy, and testing approach. Use the plan sub-agent if deeper analysis would help. Prefer simplicity — YAGNI.`,
1588
1538
  }
1589
1539
  }
1590
1540
 
1591
- const lintCmd: CommandHandler = async () => {
1541
+ const lintCmd: CommandHandler = async (ctx) => {
1542
+ const t = resolveT(ctx)
1592
1543
  try {
1593
1544
  const { execSync } = await import('node:child_process')
1594
1545
  const { existsSync } = await import('node:fs')
@@ -1620,19 +1571,17 @@ const lintCmd: CommandHandler = async () => {
1620
1571
 
1621
1572
  return {
1622
1573
  content: [
1623
- '── Lint Results ──',
1574
+ t('commands.lint.title'),
1624
1575
  '',
1625
1576
  truncated || '(no issues found)',
1626
1577
  '',
1627
1578
  hasPackageJson
1628
- ? 'To fix: type "fix the lint errors"'
1629
- : 'Set up linting: https://mipham.ai/code/docs/linting',
1579
+ ? t('commands.lint.fix_hint')
1580
+ : t('commands.lint.setup_hint'),
1630
1581
  ].join('\n'),
1631
1582
  }
1632
1583
  } catch {
1633
- return {
1634
- content: '── Lint ──\n\nCould not run linter. Ensure ESLint is installed and configured.',
1635
- }
1584
+ return { content: t('commands.lint.error') }
1636
1585
  }
1637
1586
  }
1638
1587
 
@@ -1640,7 +1589,8 @@ const lintCmd: CommandHandler = async () => {
1640
1589
  // Session Enhancement Commands (Claude Code parity)
1641
1590
  // ═══════════════════════════════════════════════════════════════
1642
1591
 
1643
- const filesCmd: CommandHandler = async () => {
1592
+ const filesCmd: CommandHandler = async (ctx) => {
1593
+ const t = resolveT(ctx)
1644
1594
  const { readdirSync, statSync } = await import('node:fs')
1645
1595
  const { join } = await import('node:path')
1646
1596
  const cwd = process.cwd()
@@ -1663,22 +1613,23 @@ const filesCmd: CommandHandler = async () => {
1663
1613
 
1664
1614
  return {
1665
1615
  content: [
1666
- '── Project Files ──',
1616
+ t('commands.files.title'),
1667
1617
  '',
1668
- `CWD: ${cwd}`,
1618
+ t('commands.files.cwd', { path: cwd }),
1669
1619
  '',
1670
1620
  ...items,
1671
- entries.length > 40 ? ` ... and ${entries.length - 40} more files` : '',
1621
+ entries.length > 40 ? t('commands.files.more_files', { count: String(entries.length - 40) }) : '',
1672
1622
  '',
1673
- 'Use Read/Glob/Grep tools to examine files.',
1623
+ t('commands.files.hint'),
1674
1624
  ].join('\n'),
1675
1625
  }
1676
1626
  } catch {
1677
- return { content: '── Files ──\n\nCould not read directory.' }
1627
+ return { content: t('commands.files.error') }
1678
1628
  }
1679
1629
  }
1680
1630
 
1681
1631
  const statsCmd: CommandHandler = (ctx) => {
1632
+ const t = resolveT(ctx)
1682
1633
  const c = ctx.engine.getContext()
1683
1634
  const msgs = c.getMessages()
1684
1635
  const tokens = c.getEstimatedTokens()
@@ -1690,26 +1641,27 @@ const statsCmd: CommandHandler = (ctx) => {
1690
1641
 
1691
1642
  return {
1692
1643
  content: [
1693
- '── Session Stats ──',
1644
+ t('commands.stats.title'),
1694
1645
  '',
1695
- `Messages: ${msgs.length} (${userMsgs} user, ${assistantMsgs} AI, ${systemMsgs} system)`,
1696
- `Tokens: ~${tokens.toLocaleString()} / 200,000`,
1697
- `Tools available: ${tools.size}`,
1698
- `Provider: ${ctx.providerId}`,
1699
- `Model: ${ctx.modelId}`,
1700
- `Permission: ${ctx.config.permission}`,
1646
+ t('commands.stats.messages', { total: String(msgs.length), user: String(userMsgs), assistant: String(assistantMsgs), system: String(systemMsgs) }),
1647
+ t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
1648
+ t('commands.stats.tools', { count: String(tools.size) }),
1649
+ t('commands.stats.provider', { provider: ctx.providerId }),
1650
+ t('commands.stats.model', { model: ctx.modelId }),
1651
+ t('commands.stats.permission', { permission: ctx.config.permission }),
1701
1652
  '',
1702
- `Usage: ${((tokens / 200_000) * 100).toFixed(1)}% of context window`,
1653
+ t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
1703
1654
  ].join('\n'),
1704
1655
  }
1705
1656
  }
1706
1657
 
1707
1658
  const summaryCmd: CommandHandler = (ctx) => {
1659
+ const t = resolveT(ctx)
1708
1660
  const c = ctx.engine.getContext()
1709
1661
  const msgs = c.getMessages()
1710
1662
 
1711
1663
  if (msgs.length === 0) {
1712
- return { content: '── Summary ──\n\nNo conversation to summarize.' }
1664
+ return { content: t('commands.summary.no_conversation') }
1713
1665
  }
1714
1666
 
1715
1667
  const userMsgs = msgs
@@ -1722,32 +1674,24 @@ const summaryCmd: CommandHandler = (ctx) => {
1722
1674
 
1723
1675
  return {
1724
1676
  content: [
1725
- '── Session Summary ──',
1677
+ t('commands.summary.title'),
1726
1678
  '',
1727
- `Total messages: ${msgs.length}`,
1728
- `Est. tokens: ~${c.getEstimatedTokens().toLocaleString()}`,
1679
+ t('commands.summary.total_messages', { count: String(msgs.length) }),
1680
+ t('commands.summary.est_tokens', { tokens: c.getEstimatedTokens().toLocaleString() }),
1729
1681
  '',
1730
- 'Recent topics:',
1682
+ t('commands.summary.recent_topics'),
1731
1683
  ...userMsgs.map((t, i) => ` ${i + 1}. ${t}`),
1732
1684
  '',
1733
- 'Use /export to save, /clear to reset.',
1685
+ t('commands.summary.footer'),
1734
1686
  ].join('\n'),
1735
1687
  }
1736
1688
  }
1737
1689
 
1738
1690
  const cdCmd: CommandHandler = async (ctx, args) => {
1691
+ const t = resolveT(ctx)
1739
1692
  const target = args[0]
1740
1693
  if (!target) {
1741
- return {
1742
- content: [
1743
- 'Usage: /cd <path>',
1744
- '',
1745
- 'Change the session working directory.',
1746
- 'Example: /cd ~/projects/my-app',
1747
- '',
1748
- `Current: ${process.cwd()}`,
1749
- ].join('\n'),
1750
- }
1694
+ return { content: t('commands.cd.usage', { cwd: process.cwd() }) }
1751
1695
  }
1752
1696
 
1753
1697
  const { existsSync } = await import('node:fs')
@@ -1755,13 +1699,12 @@ const cdCmd: CommandHandler = async (ctx, args) => {
1755
1699
  const resolved = resolve(target.replace(/^~/, process.env.HOME || '~'))
1756
1700
 
1757
1701
  if (!existsSync(resolved)) {
1758
- return { content: `❌ Directory not found: ${resolved}` }
1702
+ return { content: t('commands.cd.not_found', { path: resolved }) }
1759
1703
  }
1760
1704
 
1761
1705
  try {
1762
1706
  process.chdir(resolved)
1763
1707
 
1764
- // Persist cwd to active session (best-effort)
1765
1708
  try {
1766
1709
  const { SessionStore } = await import('../core/session-store')
1767
1710
  const saved = SessionStore.load(ctx.sessionId)
@@ -1776,47 +1719,26 @@ const cdCmd: CommandHandler = async (ctx, args) => {
1776
1719
  /* session persistence is best-effort */
1777
1720
  }
1778
1721
 
1779
- return {
1780
- content: [
1781
- '── Directory Changed ──',
1782
- '',
1783
- `New CWD: ${resolved}`,
1784
- '',
1785
- 'The AI will now work relative to this directory.',
1786
- 'Note: This changes the filesystem root for tools like Read, Write, Bash.',
1787
- ].join('\n'),
1788
- }
1722
+ return { content: t('commands.cd.content', { path: resolved }) }
1789
1723
  } catch (err) {
1790
- return { content: `❌ Failed to change directory: ${(err as Error).message}` }
1724
+ return { content: t('commands.cd.failed', { error: (err as Error).message }) }
1791
1725
  }
1792
1726
  }
1793
1727
 
1794
- const hooksCmd: CommandHandler = async () => {
1728
+ const hooksCmd: CommandHandler = async (ctx) => {
1729
+ const t = resolveT(ctx)
1795
1730
  const { existsSync, readdirSync } = await import('node:fs')
1796
1731
  const { join } = await import('node:path')
1797
1732
  const cwd = process.cwd()
1798
1733
  const hooksDir = join(cwd, '.mipham', 'hooks')
1799
1734
 
1800
1735
  if (!existsSync(hooksDir)) {
1801
- return {
1802
- content: [
1803
- '── Hooks ──',
1804
- '',
1805
- 'No hooks configured.',
1806
- '',
1807
- 'Create hook scripts in .mipham/hooks/:',
1808
- ' pre-tool-use.sh — runs before each tool',
1809
- ' post-tool-use.sh — runs after each tool',
1810
- ' stop.sh — runs when session ends',
1811
- '',
1812
- 'Use /loop init to scaffold hooks directory.',
1813
- ].join('\n'),
1814
- }
1736
+ return { content: t('commands.hooks.no_hooks') }
1815
1737
  }
1816
1738
 
1817
1739
  try {
1818
1740
  const files = readdirSync(hooksDir).filter((f) => f.endsWith('.sh'))
1819
- const lines: string[] = ['── Hooks ──', '', `Location: ${hooksDir}`, '']
1741
+ const lines: string[] = [t('commands.hooks.title'), '', t('commands.hooks.location', { path: hooksDir }), '']
1820
1742
 
1821
1743
  for (const f of files) {
1822
1744
  try {
@@ -1831,35 +1753,16 @@ const hooksCmd: CommandHandler = async () => {
1831
1753
  }
1832
1754
 
1833
1755
  lines.push('')
1834
- lines.push(`${files.length} hook(s) found.`)
1756
+ lines.push(t('commands.hooks.found', { count: String(files.length) }))
1835
1757
  return { content: lines.join('\n') }
1836
1758
  } catch {
1837
- return { content: '── Hooks ──\n\nCould not read hooks directory.' }
1759
+ return { content: t('commands.hooks.error') }
1838
1760
  }
1839
1761
  }
1840
1762
 
1841
- const batchCmd: CommandHandler = async () => {
1842
- return {
1843
- content: [
1844
- '── Batch Operations ──',
1845
- '',
1846
- 'Apply a change across multiple files or directories.',
1847
- '',
1848
- 'Usage: type "apply this change to all .ts files in src/"',
1849
- '',
1850
- 'The AI will:',
1851
- ' 1. Understand your change description',
1852
- ' 2. Find all matching files',
1853
- ' 3. Apply the change consistently',
1854
- ' 4. Report what was modified',
1855
- '',
1856
- 'Use this for:',
1857
- ' • Renaming symbols across codebase',
1858
- ' • Updating import patterns',
1859
- ' • Applying consistent formatting changes',
1860
- ' • Bulk config updates',
1861
- ].join('\n'),
1862
- }
1763
+ const batchCmd: CommandHandler = async (ctx) => {
1764
+ const t = resolveT(ctx)
1765
+ return { content: t('commands.batch.title') }
1863
1766
  }
1864
1767
 
1865
1768
  // ═══════════════════════════════════════════════════════════════
@@ -1867,6 +1770,7 @@ const batchCmd: CommandHandler = async () => {
1867
1770
  // ═══════════════════════════════════════════════════════════════
1868
1771
 
1869
1772
  const exportCmd: CommandHandler = async (ctx) => {
1773
+ const t = resolveT(ctx)
1870
1774
  const { writeFileSync } = await import('node:fs')
1871
1775
  const { join } = await import('node:path')
1872
1776
 
@@ -1897,7 +1801,7 @@ const exportCmd: CommandHandler = async (ctx) => {
1897
1801
 
1898
1802
  writeFileSync(filepath, lines.join('\n'), 'utf-8')
1899
1803
  return {
1900
- content: `✓ Session exported to:\n ${filepath}\n\n${msgs.length} messages · ${lines.length} lines`,
1804
+ content: t('commands.export.confirmed', { path: filepath, count: String(msgs.length), lines: String(lines.length) }),
1901
1805
  copyContent: filepath,
1902
1806
  }
1903
1807
  }
@@ -1976,21 +1880,19 @@ const prCommentsCmd: CommandHandler = async () => {
1976
1880
  // Resume
1977
1881
  // ═══════════════════════════════════════════════════════════════
1978
1882
 
1979
- const resumeLastCmd: CommandHandler = async () => {
1883
+ const resumeLastCmd: CommandHandler = async (ctx) => {
1884
+ const t = resolveT(ctx)
1980
1885
  const { SessionStore } = await import('../core/session-store')
1981
1886
 
1982
1887
  const latest = SessionStore.getLatest()
1983
1888
  if (!latest) {
1984
- return {
1985
- content:
1986
- '─ Resume Session ─\n\nNo saved sessions found.\n\nSessions are auto-saved to ~/.mipham/sessions/ when Mipham Code exits.',
1987
- }
1889
+ return { content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}` }
1988
1890
  }
1989
1891
 
1990
1892
  const session = SessionStore.load(latest.name)
1991
1893
  if (!session) {
1992
1894
  return {
1993
- content: `─ Load Failed ─\n\nCould not load session "${latest.name}". The file may have been removed.`,
1895
+ content: `${t('commands.resume.load_failed')}\n\nCould not load session "${latest.name}". The file may have been removed.`,
1994
1896
  }
1995
1897
  }
1996
1898
 
@@ -1999,109 +1901,86 @@ const resumeLastCmd: CommandHandler = async () => {
1999
1901
  const messages = truncated ? session.messages.slice(-MAX_RESUME) : session.messages
2000
1902
 
2001
1903
  const date = new Date(latest.updatedAt).toLocaleString()
1904
+ const truncatedStr = truncated ? ` (showing last ${MAX_RESUME})` : ''
1905
+
2002
1906
  return {
2003
1907
  content: [
2004
- '─ Session Restored ─',
1908
+ t('commands.resume.restored'),
2005
1909
  '',
2006
- `Name: ${latest.name}`,
2007
- `Messages: ${session.messages.length} total${truncated ? ` (showing last ${MAX_RESUME})` : ''}`,
2008
- `Provider: ${latest.provider} / ${latest.model}`,
2009
- `Updated: ${date}`,
1910
+ t('commands.resume.restored_content', { name: latest.name, total: String(session.messages.length), truncated: truncatedStr, provider: latest.provider, model: latest.model, date }),
2010
1911
  '',
2011
1912
  truncated
2012
- ? `${messages.length} of ${session.messages.length} messages loaded. Older context summarized above.`
2013
- : `${messages.length} messages loaded. Context has been restored.`,
1913
+ ? t('commands.resume.restored_footer', { loaded: String(messages.length), total: String(session.messages.length) })
1914
+ : t('commands.resume.restored_full_footer', { loaded: String(messages.length) }),
2014
1915
  ].join('\n'),
2015
1916
  forwardedMessages: messages,
2016
1917
  }
2017
1918
  }
2018
1919
 
2019
- const resumeDeleteCmd: CommandHandler = async (_ctx, args) => {
1920
+ const resumeDeleteCmd: CommandHandler = async (ctx, args) => {
1921
+ const t = resolveT(ctx)
2020
1922
  const name = args.join(' ').trim()
2021
1923
  if (!name) {
2022
- return {
2023
- content:
2024
- 'Usage: /resume delete <session-name>\n\nDelete a saved session. Use /resume to list all sessions.',
2025
- }
1924
+ return { content: t('commands.resume.delete_usage') }
2026
1925
  }
2027
1926
 
2028
1927
  const { SessionStore } = await import('../core/session-store')
2029
1928
  const deleted = SessionStore.delete(name)
2030
1929
 
2031
1930
  return deleted
2032
- ? { content: `✓ Session "${name}" deleted.` }
2033
- : { content: `✗ Session "${name}" not found. Use /resume to list all sessions.` }
1931
+ ? { content: t('commands.resume.delete_confirmed', { name }) }
1932
+ : { content: t('commands.resume.delete_not_found', { name }) }
2034
1933
  }
2035
1934
 
2036
- const resumeCmd: CommandHandler = async (_ctx, args) => {
1935
+ const resumeCmd: CommandHandler = async (ctx, args) => {
1936
+ const t = resolveT(ctx)
2037
1937
  const sub = args[0]?.toLowerCase()
2038
1938
 
2039
- // Sub-command: /resume last
2040
1939
  if (sub === 'last') {
2041
- return resumeLastCmd(_ctx, args.slice(1))
1940
+ return resumeLastCmd(ctx, args.slice(1))
2042
1941
  }
2043
1942
 
2044
- // Sub-command: /resume delete <name>
2045
1943
  if (sub === 'delete') {
2046
- return resumeDeleteCmd(_ctx, args.slice(1))
1944
+ return resumeDeleteCmd(ctx, args.slice(1))
2047
1945
  }
2048
1946
 
2049
1947
  const { SessionStore } = await import('../core/session-store')
2050
1948
 
2051
- // If a name is provided, show load instructions
2052
1949
  const targetName = args.join(' ')
2053
1950
  if (targetName) {
2054
1951
  const session = SessionStore.load(targetName)
2055
1952
  if (session) {
2056
1953
  return {
2057
- content: [
2058
- '─ Session Found ─',
2059
- '',
2060
- `Name: ${session.metadata.name}`,
2061
- `Messages: ${session.metadata.messageCount}`,
2062
- `Provider: ${session.metadata.provider} / ${session.metadata.model}`,
2063
- `Updated: ${session.metadata.updatedAt}`,
2064
- '',
2065
- 'To resume this session:',
2066
- ` /resume last — restore the most recent session`,
2067
- ` mipham --resume "${targetName}"`,
2068
- '',
2069
- 'Or restart Mipham Code — the most recent session loads automatically.',
2070
- ].join('\n'),
1954
+ content: `${t('commands.resume.found_title')}\n\n${t('commands.resume.found_content', { name: session.metadata.name, messages: String(session.metadata.messageCount), provider: session.metadata.provider, model: session.metadata.model, updated: session.metadata.updatedAt, target: targetName })}`,
2071
1955
  }
2072
1956
  }
2073
- return {
2074
- content: `─ Session Not Found ─\n\nNo session named "${targetName}".\n\nUse /resume without arguments to list all saved sessions.`,
2075
- }
1957
+ return { content: `${t('commands.resume.not_found_title')}\n\n${t('commands.resume.not_found_content', { name: targetName })}` }
2076
1958
  }
2077
1959
 
2078
1960
  const sessions = SessionStore.list()
2079
1961
 
2080
1962
  if (sessions.length === 0) {
2081
- return {
2082
- content:
2083
- '─ Resume Session ─\n\nNo saved sessions found.\n\nSessions are auto-saved to ~/.mipham/sessions/ when Mipham Code exits.\nStart a conversation — it will be saved automatically.',
2084
- }
1963
+ return { content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}` }
2085
1964
  }
2086
1965
 
2087
1966
  const recent = sessions.slice(0, 10)
2088
1967
 
2089
1968
  const lines: string[] = [
2090
- '─ Saved Sessions ─',
1969
+ t('commands.resume.saved_title'),
2091
1970
  '',
2092
1971
  ...recent.map(
2093
1972
  (s, i) =>
2094
1973
  ` ${(i + 1).toString().padStart(2)}. ${s.name.padEnd(45)} ${s.messageCount.toString().padStart(4)} msgs ${new Date(s.updatedAt).toLocaleString()}`,
2095
1974
  ),
2096
1975
  '',
2097
- `Total: ${sessions.length} session(s) • Location: ~/.mipham/sessions/`,
1976
+ t('commands.resume.total_footer', { count: String(sessions.length) }),
2098
1977
  '',
2099
- 'To resume a session: /resume <name>',
2100
- 'To resume most recent: /resume last',
2101
- 'To delete a session: /resume delete <name>',
2102
- 'To resume from CLI: mipham --resume "<name>"',
1978
+ t('commands.resume.resume_hint'),
1979
+ t('commands.resume.resume_last_hint'),
1980
+ t('commands.resume.delete_hint'),
1981
+ t('commands.resume.cli_hint'),
2103
1982
  '',
2104
- 'Sessions are auto-saved on exit. The most recent session loads automatically on restart.',
1983
+ t('commands.resume.auto_save_hint'),
2105
1984
  ]
2106
1985
 
2107
1986
  return { content: lines.join('\n') }
@@ -2111,7 +1990,8 @@ const resumeCmd: CommandHandler = async (_ctx, args) => {
2111
1990
  // Memory Management
2112
1991
  // ═══════════════════════════════════════════════════════════════
2113
1992
 
2114
- const memoryCmd: CommandHandler = async () => {
1993
+ const memoryCmd: CommandHandler = async (ctx) => {
1994
+ const t = resolveT(ctx)
2115
1995
  const { existsSync, readdirSync, readFileSync, statSync } = await import('node:fs')
2116
1996
  const { join } = await import('node:path')
2117
1997
 
@@ -2119,16 +1999,13 @@ const memoryCmd: CommandHandler = async () => {
2119
1999
  const memoryDir = join(home, '.mipham', 'memory')
2120
2000
 
2121
2001
  if (!existsSync(memoryDir)) {
2122
- return {
2123
- content:
2124
- '─ Memory ─\n\nNo memories stored yet.\n\nMemory is saved to ~/.mipham/memory/ by the AI when you ask it to remember something.\nTry: "remember that I prefer TypeScript"',
2125
- }
2002
+ return { content: t('commands.memories.no_memories') }
2126
2003
  }
2127
2004
 
2128
2005
  try {
2129
2006
  const files = readdirSync(memoryDir).filter((f) => f.endsWith('.md'))
2130
2007
  if (files.length === 0) {
2131
- return { content: '─ Memory ─\n\nNo memory files found in ~/.mipham/memory/' }
2008
+ return { content: t('commands.memories.empty') }
2132
2009
  }
2133
2010
 
2134
2011
  const memories: Array<{ file: string; size: number; mtime: Date; title: string }> = []
@@ -2149,23 +2026,22 @@ const memoryCmd: CommandHandler = async () => {
2149
2026
  memories.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
2150
2027
 
2151
2028
  const lines: string[] = [
2152
- '─ Memory ─',
2029
+ t('commands.memories.title'),
2153
2030
  '',
2154
- `Location: ${memoryDir}`,
2155
- `Total: ${memories.length} memor${memories.length === 1 ? 'y' : 'ies'}`,
2031
+ t('commands.memories.location', { path: memoryDir }),
2032
+ t('commands.memories.total', { count: String(memories.length), plural: memories.length === 1 ? 'y' : 'ies' }),
2156
2033
  '',
2157
2034
  ...memories.map(
2158
2035
  (m, i) =>
2159
2036
  ` ${(i + 1).toString().padStart(2)}. ${m.file.padEnd(35)} ${(m.size / 1024).toFixed(1)}KB ${m.mtime.toLocaleDateString()} ${m.title}`,
2160
2037
  ),
2161
2038
  '',
2162
- 'Memories are used by the AI to provide personalized context across sessions.',
2163
- 'Each file in ~/.mipham/memory/ represents one remembered fact or preference.',
2039
+ t('commands.memories.footer'),
2164
2040
  ]
2165
2041
 
2166
2042
  return { content: lines.join('\n') }
2167
2043
  } catch {
2168
- return { content: '─ Memory ─\n\nCould not read memory directory.' }
2044
+ return { content: t('commands.memories.error') }
2169
2045
  }
2170
2046
  }
2171
2047
 
@@ -2173,7 +2049,8 @@ const memoryCmd: CommandHandler = async () => {
2173
2049
  // Upgrade
2174
2050
  // ═══════════════════════════════════════════════════════════════
2175
2051
 
2176
- const upgradeCmd: CommandHandler = async () => {
2052
+ const upgradeCmd: CommandHandler = async (ctx) => {
2053
+ const t = resolveT(ctx)
2177
2054
  const { checkForUpdates, backupConfig, performUpdate, restoreConfig, getConfigPath } =
2178
2055
  await import('../shared/update')
2179
2056
 
@@ -2181,27 +2058,16 @@ const upgradeCmd: CommandHandler = async () => {
2181
2058
 
2182
2059
  if (!update.available) {
2183
2060
  return {
2184
- content: `── Upgrade Mipham Code ──
2185
-
2186
- Current version: v${update.current}
2187
- Latest: v${update.latest}
2188
-
2189
- ✓ Already up to date.
2190
-
2191
- To check manually: https://www.npmjs.com/package/@miphamai/cli`,
2061
+ content: t('commands.upgrade.uptodate', { current: update.current, latest: update.latest }),
2192
2062
  }
2193
2063
  }
2194
2064
 
2195
- // Update available — back up config and perform the upgrade
2196
2065
  const backupPath = backupConfig(`upgrade-v${update.current}`)
2197
2066
 
2198
2067
  const lines: string[] = [
2199
- '── Upgrade Mipham Code ──',
2068
+ t('commands.upgrade.title'),
2200
2069
  '',
2201
- `Current version: v${update.current}`,
2202
- `Latest: v${update.latest}`,
2203
- '',
2204
- `→ New version available! Updating...`,
2070
+ t('commands.upgrade.available', { current: update.current, latest: update.latest }),
2205
2071
  '',
2206
2072
  ]
2207
2073
 
@@ -2215,39 +2081,47 @@ To check manually: https://www.npmjs.com/package/@miphamai/cli`,
2215
2081
  const configPath = getConfigPath()
2216
2082
  const { existsSync } = await import('node:fs')
2217
2083
  lines.push('')
2218
- lines.push(`✓ Updated to @miphamai/cli v${update.latest}`)
2084
+ lines.push(t('commands.upgrade.updated', { version: update.latest }))
2219
2085
 
2220
2086
  if (existsSync(configPath)) {
2221
- lines.push(`✓ Config preserved: ${configPath}`)
2087
+ lines.push(t('commands.upgrade.config_preserved', { path: configPath }))
2222
2088
  } else if (backupPath) {
2223
2089
  if (restoreConfig(backupPath)) {
2224
- lines.push('✓ Config restored from backup.')
2090
+ lines.push(t('commands.upgrade.config_restored'))
2225
2091
  }
2226
2092
  }
2227
2093
 
2228
2094
  lines.push('')
2229
- lines.push('⚠ The running Mipham Code process is still the old version.')
2230
- lines.push(' Run `mipham` again to use the new version, or `mipham --version` to verify.')
2095
+ lines.push(t('commands.upgrade.old_version_warning'))
2231
2096
  } else {
2232
2097
  lines.push('')
2233
- lines.push('✗ Update failed.')
2234
- lines.push(` Try manually: ${NPM_UPDATE_COMMAND}`)
2235
- if (backupPath) {
2236
- lines.push(` Your config backup is at: ${backupPath}`)
2237
- }
2098
+ lines.push(t('commands.upgrade.update_failed', { command: NPM_UPDATE_COMMAND, path: backupPath || '' }))
2238
2099
  }
2239
2100
 
2240
2101
  return { content: lines.join('\n') }
2241
2102
  }
2242
2103
 
2104
+ // ═══════════════════════════════════════════════════════════════
2105
+ // Language — i18n locale control
2106
+ // ═══════════════════════════════════════════════════════════════
2107
+
2108
+ const langCmd: CommandHandler = (ctx, args) => {
2109
+ const t = resolveT(ctx)
2110
+ const requested = args[0]
2111
+ if (!requested || !['en-US', 'zh-CN'].includes(requested)) {
2112
+ return { content: t('commands.lang.current', { locale: 'en-US' }) }
2113
+ }
2114
+ return { content: t('commands.lang.set', { locale: requested }) }
2115
+ }
2116
+
2243
2117
  // ═══════════════════════════════════════════════════════════════
2244
2118
  // No-Plan — exit plan mode
2245
2119
  // ═══════════════════════════════════════════════════════════════
2246
2120
 
2247
- const noPlanCmd: CommandHandler = () => ({
2248
- content:
2249
- '✓ Plan mode exited. Your plan has been discarded.\n\nContinue chatting as normal, or type /plan to start a new plan.',
2250
- })
2121
+ const noPlanCmd: CommandHandler = (ctx) => {
2122
+ const t = resolveT(ctx)
2123
+ return { content: t('commands.no_plan.confirmed') }
2124
+ }
2251
2125
 
2252
2126
  // ═══════════════════════════════════════════════════════════════
2253
2127
  // Workflows
@@ -3310,6 +3184,7 @@ registry.set('/prompt-audit', promptAuditCmd)
3310
3184
 
3311
3185
  // Environment
3312
3186
  registry.set('/theme', themeCmd)
3187
+ registry.set('/lang', langCmd)
3313
3188
  registry.set('/ide', ideCmd)
3314
3189
  registry.set('/terminal-setup', terminalSetupCmd)
3315
3190
  registry.set('/release-notes', releaseNotesCmd)
@@ -3407,6 +3282,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
3407
3282
  '/fast': 'Toggle fast mode',
3408
3283
  '/effort': 'Set reasoning effort',
3409
3284
  '/theme': 'Set terminal theme',
3285
+ '/lang': 'Set display language (en-US, zh-CN)',
3410
3286
  '/tools': 'List available tools',
3411
3287
  '/skills': 'List loaded skills',
3412
3288
  '/reload-skills': 'Reload all skills',