@miphamai/cli 0.19.5 → 0.20.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.
@@ -13,6 +13,20 @@ import { McpClient } from '../mcp/client'
13
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 '../i18n-core/t'
17
+ import type { TranslationMap } from '../i18n-core/types'
18
+ import enUS from '../i18n-core/locales/en-US.json' with { type: 'json' }
19
+ import zhCN from '../i18n-core/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,44 @@ 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 {
355
+ content: t('commands.provider.current', { provider: ctx.providerId, model: ctx.modelId }),
356
+ }
357
+ }
330
358
 
331
359
  const providersCmd: CommandHandler = (ctx) => {
360
+ const t = resolveT(ctx)
332
361
  const lines = ctx.config.providers.map(
333
362
  (p) =>
334
363
  ` ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol.padEnd(18)} ${p.models.length} models ${p.status === 'upcoming' ? '[upcoming]' : '✓'}`,
335
364
  )
336
365
  return {
337
- content: `Configured providers:\n\n${lines.join('\n')}\n\nCurrent: ${ctx.providerId}/${ctx.modelId}`,
366
+ content: `${t('commands.providers.title')}\n\n${lines.join('\n')}\n\n${t('commands.providers.current', { provider: ctx.providerId, model: ctx.modelId })}`,
338
367
  }
339
368
  }
340
369
 
341
370
  const switchCmd: CommandHandler = (ctx, args) => {
371
+ const t = resolveT(ctx)
342
372
  const [newProvider, newModel] = args
343
373
  if (!newProvider || !newModel) {
344
- return {
345
- content: 'Usage: /switch <provider> <model>\nExample: /switch deepseek deepseek-v4-pro',
346
- }
374
+ return { content: t('commands.switch.usage') }
347
375
  }
348
376
  ctx.engine.switchProvider(newProvider, newModel)
349
377
  return {
350
- content: `✓ Switched to ${newProvider}/${newModel}`,
378
+ content: t('commands.switch.confirmed', { provider: newProvider, model: newModel }),
351
379
  nextProvider: newProvider,
352
380
  nextModel: newModel,
353
381
  }
354
382
  }
355
383
 
356
384
  const configCmd: CommandHandler = (ctx) => {
385
+ const t = resolveT(ctx)
357
386
  const c = ctx.config
358
387
  const lines = [
359
388
  `version: ${c.version}`,
@@ -363,7 +392,7 @@ const configCmd: CommandHandler = (ctx) => {
363
392
  `providers: ${c.providers.length} configured`,
364
393
  ]
365
394
  return {
366
- content: `── Configuration ──\n${lines.join('\n')}\n\nEdit: ~/.mipham/config.yml or .mipham/config.yml`,
395
+ content: `${t('commands.config.title')}\n${lines.join('\n')}\n\n${t('commands.config.edit')}`,
367
396
  }
368
397
  }
369
398
 
@@ -372,6 +401,7 @@ const configCmd: CommandHandler = (ctx) => {
372
401
  // ═══════════════════════════════════════════════════════════════
373
402
 
374
403
  const toolsCmd: CommandHandler = (ctx) => {
404
+ const t = resolveT(ctx)
375
405
  const tools = ctx.engine.getTools()
376
406
  const categories: Record<string, string[]> = {
377
407
  file: [],
@@ -389,12 +419,15 @@ const toolsCmd: CommandHandler = (ctx) => {
389
419
  .filter(([, v]) => v!.length > 0)
390
420
  .map(([cat, items]) => `── ${cat.toUpperCase()} ──\n${items!.join('\n')}`)
391
421
 
392
- return { content: `Available tools (${tools.size}):\n\n${sections.join('\n\n')}` }
422
+ return {
423
+ content: `${t('commands.tools.title', { count: String(tools.size) })}\n\n${sections.join('\n\n')}`,
424
+ }
393
425
  }
394
426
 
395
427
  const skillsCmd: CommandHandler = (ctx) => {
428
+ const t = resolveT(ctx)
396
429
  if (!ctx.skillsLoader) {
397
- return { content: 'SkillsLoader not available.' }
430
+ return { content: t('commands.skills.unavailable') }
398
431
  }
399
432
  const counts = ctx.skillsLoader.countByType()
400
433
  const standard = ctx.skillsLoader.listByType('standard')
@@ -407,7 +440,7 @@ const skillsCmd: CommandHandler = (ctx) => {
407
440
  `── Mipham Exclusive (${counts.mipham}) ──`,
408
441
  ...mipham.map((s) => ` ${s.name.padEnd(20)} ${s.description}`),
409
442
  '',
410
- `${counts.total} skills loaded. Use Skill tool to invoke.`,
443
+ t('commands.skills.loaded', { count: String(counts.total) }),
411
444
  ]
412
445
  return { content: lines.join('\n') }
413
446
  }
@@ -417,10 +450,10 @@ const skillsCmd: CommandHandler = (ctx) => {
417
450
  // ═══════════════════════════════════════════════════════════════
418
451
 
419
452
  const planCmd: CommandHandler = (_ctx, args) => {
453
+ const t = resolveT(_ctx)
420
454
  const description = args.join(' ') || undefined
421
455
  return {
422
- content:
423
- '── Plan Mode ──\n\nEntering plan mode — read-only analysis and design.\nUse EnterPlanMode to start, ExitPlanMode to submit for approval.',
456
+ content: t('commands.plan.content'),
424
457
  forwardToAI: description
425
458
  ? `Use EnterPlanMode with description: "${description}". Then explore, design, and use ExitPlanMode when ready for approval.`
426
459
  : 'Use EnterPlanMode to enter plan mode. Explore the codebase, design an approach, then use ExitPlanMode to submit for approval.',
@@ -428,18 +461,19 @@ const planCmd: CommandHandler = (_ctx, args) => {
428
461
  }
429
462
 
430
463
  const tddCmd: CommandHandler = (_ctx, args) => {
464
+ const t = resolveT(_ctx)
431
465
  const target = args.join(' ') || 'the current task'
432
466
  return {
433
467
  content: stripIndent`
434
- ── TDD Mode ──
435
- Starting Test-Driven Development workflow for: ${target}
468
+ ${t('commands.tdd.title')}
469
+ ${t('commands.tdd.start', { target })}
436
470
 
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
471
+ ${t('commands.tdd.cycle')}
472
+ ${t('commands.tdd.red')}
473
+ ${t('commands.tdd.green')}
474
+ ${t('commands.tdd.refactor')}
441
475
 
442
- The AI will guide you through each cycle.
476
+ ${t('commands.tdd.guide')}
443
477
  `,
444
478
  forwardToAI: `Follow the Test-Driven Development workflow for ${target}:
445
479
  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 +484,15 @@ Repeat for each behavior. Do NOT write implementation before tests.`,
450
484
  }
451
485
 
452
486
  const todosCmd: CommandHandler = (_ctx, args) => {
487
+ const t = resolveT(_ctx)
453
488
  const sub = args[0]
454
489
  if (sub === 'create') {
455
490
  const title = args.slice(1).join(' ')
456
491
  if (!title.trim()) {
457
- return {
458
- content:
459
- 'Usage: /todos create <task-title>\n\nExample: /todos create Add user authentication',
460
- }
492
+ return { content: t('commands.todos.usage_create') }
461
493
  }
462
494
  return {
463
- content: `── Create Task ──\n\nCreating task: "${title.trim()}"\n\nPassing to AI for structured task creation with TaskCreate...`,
495
+ content: `${t('commands.todos.create_title')}\n\nCreating task: "${title.trim()}"\n\nPassing to AI for structured task creation with TaskCreate...`,
464
496
  forwardToAI: `Create a new task using TaskCreate with subject "${title.trim()}". Set a clear description and activeForm.`,
465
497
  }
466
498
  }
@@ -468,12 +500,12 @@ const todosCmd: CommandHandler = (_ctx, args) => {
468
500
  if (sub === 'list' || !sub) {
469
501
  return {
470
502
  content: stripIndent`
471
- ── Task Management ──
472
- Fetching current task list...
503
+ ${t('commands.todos.list_title')}
504
+ ${t('commands.todos.list_fetching')}
473
505
 
474
506
  Shortcuts:
475
- /todos list Show all tasks
476
- /todos create <title> Create a new task
507
+ ${t('commands.todos.item_list')}
508
+ ${t('commands.todos.item_create')}
477
509
  `,
478
510
  forwardToAI:
479
511
  '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 +514,11 @@ const todosCmd: CommandHandler = (_ctx, args) => {
482
514
 
483
515
  return {
484
516
  content: stripIndent`
485
- ── Task Management ──
486
- /todos list Show all tasks
487
- /todos create <title> Create a new task
517
+ ${t('commands.todos.default_title')}
518
+ ${t('commands.todos.item_list')}
519
+ ${t('commands.todos.item_create')}
488
520
 
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.
521
+ ${t('commands.todos.default_body')}
491
522
  `,
492
523
  forwardToAI: 'Use TaskList to show all current tasks, then present them clearly.',
493
524
  }
@@ -502,40 +533,37 @@ const todosCmd: CommandHandler = (_ctx, args) => {
502
533
  // ═══════════════════════════════════════════════════════════════
503
534
 
504
535
  const renameCmd: CommandHandler = (ctx, args) => {
536
+ const t = resolveT(ctx)
505
537
  const name = args.join(' ')
506
538
  if (!name.trim()) {
507
- return { content: 'Usage: /rename <session-name>\nExample: /rename Bug Fix Session' }
539
+ return { content: t('commands.rename.usage') }
508
540
  }
509
541
  ctx.setSessionTitle(name.trim())
510
- return { content: `✓ Session renamed to "${name.trim()}"` }
542
+ return { content: t('commands.rename.confirmed', { name: name.trim() }) }
511
543
  }
512
544
 
513
545
  const goalCmd: CommandHandler = (ctx, args) => {
546
+ const t = resolveT(ctx)
514
547
  const input = args.join(' ')
515
548
 
516
549
  // ── Show goal status ──
517
550
  if (!input.trim()) {
518
551
  const state = ctx.engine.getGoalState?.() || { goal: undefined, decompose: false, subtasks: [] }
519
552
  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
- }
553
+ return { content: t('commands.goal.usage') }
533
554
  }
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.')
555
+ const lines = [
556
+ t('commands.goal.status_title'),
557
+ '',
558
+ t('commands.goal.status_goal', { goal: state.goal }),
559
+ ]
560
+ if (state.verifyScript)
561
+ lines.push(t('commands.goal.status_verify_script', { script: state.verifyScript }))
562
+ if (state.verifySkill)
563
+ lines.push(t('commands.goal.status_verify_skill', { skill: state.verifySkill }))
564
+ if (state.decompose)
565
+ lines.push(t('commands.goal.status_decompose', { count: String(state.subtasks.length) }))
566
+ lines.push('', t('commands.goal.status_clear_hint'))
539
567
  return { content: lines.join('\n') }
540
568
  }
541
569
 
@@ -567,15 +595,15 @@ const goalCmd: CommandHandler = (ctx, args) => {
567
595
 
568
596
  goal = goal.trim()
569
597
  if (!goal) {
570
- return { content: 'Goal text is required. Use /goal without arguments to see usage.' }
598
+ return { content: t('commands.goal.empty_set') }
571
599
  }
572
600
 
573
601
  ctx.setGoal(goal)
574
602
  ctx.engine.setGoal(goal, { verifyScript, verifySkill, decompose })
575
603
 
576
- const lines = [`✓ Goal set: "${goal}"`]
604
+ const lines = [t('commands.goal.set', { goal })]
577
605
  if (decompose) {
578
- lines.push('📋 Decomposition: enabled — AI will break goal into subtasks')
606
+ lines.push(t('commands.goal.decompose_enabled'))
579
607
  // Decompose by creating initial subtasks
580
608
  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
609
  return {
@@ -583,27 +611,26 @@ const goalCmd: CommandHandler = (ctx, args) => {
583
611
  forwardToAI: decomposeMsg,
584
612
  }
585
613
  }
586
- if (verifyScript) lines.push(`📜 Verification: ${verifyScript}`)
587
- if (verifySkill) lines.push(`🛠 Verification skill: ${verifySkill}`)
614
+ if (verifyScript) lines.push(t('commands.goal.status_verify_script', { script: verifyScript }))
615
+ if (verifySkill) lines.push(t('commands.goal.status_verify_skill', { skill: verifySkill }))
588
616
  lines.push('')
589
- lines.push('Use /status to view progress. Type /goal without arguments to clear.')
617
+ lines.push(t('commands.goal.status_hint'))
590
618
 
591
619
  return { content: lines.join('\n') }
592
620
  }
593
621
 
594
622
  const recapCmd: CommandHandler = (ctx) => {
623
+ const t = resolveT(ctx)
595
624
  const c = ctx.engine.getContext()
596
625
  const msgs = c.getMessages()
597
626
  if (msgs.length === 0) {
598
- return { content: 'No conversation to recap.' }
627
+ return { content: t('commands.recap.no_conversation') }
599
628
  }
600
- // Show summary of conversation: count messages, roles, estimated tokens
601
629
  const userMsgs = msgs.filter((m) => m.role === 'user').length
602
630
  const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
603
631
  const tokens = c.getEstimatedTokens()
604
632
  const checkpointCount = c.getCheckpoints().length
605
633
 
606
- // Extract first few user messages as "topics"
607
634
  const topics = msgs
608
635
  .filter((m) => m.role === 'user' && typeof m.content === 'string')
609
636
  .slice(0, 5)
@@ -614,20 +641,21 @@ const recapCmd: CommandHandler = (ctx) => {
614
641
 
615
642
  return {
616
643
  content: stripIndent`
617
- ── Session Recap ──
618
- Messages: ${msgs.length} (${userMsgs} user, ${assistantMsgs} assistant)
619
- Est. tokens: ~${tokens.toLocaleString()}
620
- Checkpoints: ${checkpointCount}
644
+ ${t('commands.recap.title')}
645
+ ${t('commands.recap.messages')} ${msgs.length} (${userMsgs} user, ${assistantMsgs} assistant)
646
+ ${t('commands.recap.est_tokens')} ~${tokens.toLocaleString()}
647
+ ${t('commands.recap.checkpoints')} ${checkpointCount}
621
648
 
622
- Recent topics:
649
+ ${t('commands.recap.recent_topics')}
623
650
  ${topics.map((t, i) => ` ${i + 1}. ${t}`).join('\n')}
624
651
 
625
- Use /rewind to undo, /clear to reset, /export to save.
652
+ ${t('commands.recap.footer')}
626
653
  `,
627
654
  }
628
655
  }
629
656
 
630
657
  const usageCmd: CommandHandler = (ctx) => {
658
+ const t = resolveT(ctx)
631
659
  const c = ctx.engine.getContext()
632
660
  const estTokens = c.getEstimatedTokens()
633
661
  const msgs = c.getMessages()
@@ -653,46 +681,50 @@ const usageCmd: CommandHandler = (ctx) => {
653
681
  const apiTotal = summary.apiInputTokens + summary.apiOutputTokens
654
682
  const apiLine =
655
683
  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)'
684
+ ? t('commands.usage.api_tokens', {
685
+ in: summary.apiInputTokens.toLocaleString(),
686
+ out: summary.apiOutputTokens.toLocaleString(),
687
+ total: apiTotal.toLocaleString(),
688
+ })
689
+ : t('commands.usage.no_api')
658
690
 
659
- const toolSection = toolLines.length > 0 ? `\n── Per-Tool ──\n${toolLines.join('\n')}` : ''
691
+ const toolSection =
692
+ toolLines.length > 0 ? `\n${t('commands.usage.tool_section')}\n${toolLines.join('\n')}` : ''
660
693
 
661
694
  return {
662
695
  content: stripIndent`
663
- ── Usage Dashboard ──
696
+ ${t('commands.usage.title')}
664
697
  ${apiLine}
665
- Context tokens: ~${estTokens.toLocaleString()} / ${maxTokens.toLocaleString()} (${pct}%)
666
- Messages: ${msgs.length}
667
- Provider: ${ctx.providerId}
668
- Model: ${ctx.modelId}
698
+ ${t('commands.usage.context_line', { est: estTokens.toLocaleString(), max: maxTokens.toLocaleString(), pct })}
699
+ ${t('commands.usage.messages_line', { count: String(msgs.length) })}
700
+ ${t('commands.usage.provider_line', { provider: ctx.providerId })}
701
+ ${t('commands.usage.model_line', { model: ctx.modelId })}
669
702
 
670
703
  ${'█'.repeat(Math.ceil(Number(pct) / 5))}${'░'.repeat(20 - Math.ceil(Number(pct) / 5))} ${pct}%
671
704
  ${toolSection}
672
705
 
673
- Use /context for detailed stats, /compact to free space.
706
+ ${t('commands.usage.hint')}
674
707
  `,
675
708
  }
676
709
  }
677
710
 
678
711
  const reloadSkillsCmd: CommandHandler = (ctx) => {
712
+ const t = resolveT(ctx)
679
713
  if (!ctx.skillsLoader) {
680
- return { content: 'SkillsLoader not available in this context.' }
714
+ return { content: t('commands.reload_skills.unavailable') }
681
715
  }
682
716
  try {
683
717
  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
718
  ctx.skillsLoader.loadBuiltin(process.cwd())
687
719
  if (config.skills?.paths) {
688
720
  ctx.skillsLoader.loadExternal(config.skills.paths)
689
721
  }
690
722
  const skills = ctx.skillsLoader.list()
691
723
  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')}`,
724
+ 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
725
  }
694
726
  } catch (err) {
695
- return { content: `Failed to reload skills: ${String(err)}` }
727
+ return { content: t('commands.reload_skills.failed', { error: String(err) }) }
696
728
  }
697
729
  }
698
730
 
@@ -700,7 +732,8 @@ const reloadSkillsCmd: CommandHandler = (ctx) => {
700
732
  // Skill Marketplace — Community skill registry + installation
701
733
  // ═══════════════════════════════════════════════════════════════
702
734
 
703
- const browseSkillsCmd: CommandHandler = async () => {
735
+ const browseSkillsCmd: CommandHandler = async (ctx) => {
736
+ const _t = resolveT(ctx)
704
737
  const { getAvailableSkills, listInstalledSkills } = await import('../skills/registry')
705
738
  const available = getAvailableSkills()
706
739
  const installed = new Set(
@@ -780,29 +813,19 @@ const removeSkillCmd: CommandHandler = async (_ctx, args) => {
780
813
  // ═══════════════════════════════════════════════════════════════
781
814
 
782
815
  const pluginsCmd: CommandHandler = (ctx) => {
816
+ const t = resolveT(ctx)
783
817
  const manager = ctx.pluginManager
784
818
  if (!manager) {
785
- return { content: 'PluginManager not available in this session.' }
819
+ return { content: t('commands.plugins.unavailable') }
786
820
  }
787
821
 
788
822
  const plugins = manager.list()
789
823
  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
- }
824
+ return { content: t('commands.plugins.none') }
802
825
  }
803
826
 
804
827
  const lines: string[] = [
805
- '── Installed Plugins ──',
828
+ t('commands.plugins.title'),
806
829
  '',
807
830
  ...plugins.map((p) => {
808
831
  const status = p.enabled ? '✅ enabled' : '⛔ disabled'
@@ -955,29 +978,25 @@ const pluginDisableCmd: CommandHandler = (ctx, args) => {
955
978
  // ═══════════════════════════════════════════════════════════════
956
979
 
957
980
  const rewindCmd: CommandHandler = (ctx) => {
981
+ const t = resolveT(ctx)
958
982
  const c = ctx.engine.getContext()
959
983
  const checkpoints = c.getCheckpoints()
960
984
 
961
985
  if (checkpoints.length === 0) {
962
- return {
963
- content:
964
- 'No checkpoints available. Checkpoints are automatically saved after each AI response.',
965
- }
986
+ return { content: t('commands.rewind.no_checkpoints') }
966
987
  }
967
988
 
968
989
  const result = c.restoreCheckpoint()
969
990
  if (!result.restored) {
970
- return { content: 'No checkpoint to restore.' }
991
+ return { content: t('commands.rewind.no_restore') }
971
992
  }
972
993
 
973
994
  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
- `,
995
+ content: t('commands.rewind.confirmed', {
996
+ label: result.label,
997
+ count: String(result.messageCount),
998
+ remaining: String(c.getCheckpoints().length),
999
+ }),
981
1000
  clearMessages: true,
982
1001
  }
983
1002
  }
@@ -985,12 +1004,13 @@ const rewindCmd: CommandHandler = (ctx) => {
985
1004
  const undoCmd: CommandHandler = rewindCmd
986
1005
 
987
1006
  const copyCmd: CommandHandler = (ctx, args) => {
1007
+ const t = resolveT(ctx)
988
1008
  const c = ctx.engine.getContext()
989
1009
  const msgs = c.getMessages()
990
1010
  const assistantMsgs = msgs.filter((m) => m.role === 'assistant')
991
1011
 
992
1012
  if (assistantMsgs.length === 0) {
993
- return { content: 'No assistant responses to copy.' }
1013
+ return { content: t('commands.copy.no_responses') }
994
1014
  }
995
1015
 
996
1016
  // Determine which response to copy: last N or last 1
@@ -998,9 +1018,7 @@ const copyCmd: CommandHandler = (ctx, args) => {
998
1018
  if (args[0]) {
999
1019
  n = parseInt(args[0]!, 10)
1000
1020
  if (isNaN(n) || n < 1) {
1001
- return {
1002
- content: 'Usage: /copy [N]\nN = number of recent assistant responses to copy (default: 1)',
1003
- }
1021
+ return { content: t('commands.copy.usage') }
1004
1022
  }
1005
1023
  }
1006
1024
 
@@ -1010,17 +1028,18 @@ const copyCmd: CommandHandler = (ctx, args) => {
1010
1028
  .join('\n\n---\n\n')
1011
1029
 
1012
1030
  return {
1013
- content: `✓ Copied ${toCopy.length} assistant response(s) to clipboard (${text.length.toLocaleString()} chars).`,
1031
+ content: t('commands.copy.confirmed', { count: String(toCopy.length) }),
1014
1032
  copyContent: text,
1015
1033
  }
1016
1034
  }
1017
1035
 
1018
1036
  const diffCmd: CommandHandler = async (_ctx) => {
1037
+ const t = resolveT(_ctx)
1019
1038
  try {
1020
1039
  const { execSync } = await import('node:child_process')
1021
1040
  const output = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 })
1022
1041
  if (!output.trim()) {
1023
- return { content: 'No uncommitted changes (working tree clean).' }
1042
+ return { content: t('commands.diff.clean') }
1024
1043
  }
1025
1044
  // Get full diff but limit to reasonable size
1026
1045
  const fullDiff = execSync('git diff --no-color', { encoding: 'utf-8', timeout: 5000 })
@@ -1033,11 +1052,11 @@ const diffCmd: CommandHandler = async (_ctx) => {
1033
1052
  : fullDiff
1034
1053
 
1035
1054
  return {
1036
- content: `── Git Diff ──\n\n${truncated}`,
1055
+ content: `${t('commands.diff.title')}\n\n${truncated}`,
1037
1056
  }
1038
1057
  } catch {
1039
1058
  return {
1040
- content: 'Unable to run git diff. Ensure git is installed and you are in a repository.',
1059
+ content: t('commands.diff.error'),
1041
1060
  }
1042
1061
  }
1043
1062
  }
@@ -1047,60 +1066,38 @@ const diffCmd: CommandHandler = async (_ctx) => {
1047
1066
  // ═══════════════════════════════════════════════════════════════
1048
1067
 
1049
1068
  const fastCmd: CommandHandler = (ctx, args) => {
1069
+ const t = resolveT(ctx)
1050
1070
  const arg = args[0]?.toLowerCase()
1051
1071
  if (arg === 'on') {
1052
1072
  ctx.setFastMode(true)
1053
- return { content: '✓ Fast mode ON — responses will prioritize speed over depth.' }
1073
+ return { content: t('commands.fast.on') }
1054
1074
  } else if (arg === 'off') {
1055
1075
  ctx.setFastMode(false)
1056
- return { content: '✓ Fast mode OFF — standard quality mode.' }
1076
+ return { content: t('commands.fast.off') }
1057
1077
  } else if (arg) {
1058
- return { content: 'Usage: /fast [on|off]\nToggle fast mode for faster responses.' }
1078
+ return { content: t('commands.fast.usage') }
1059
1079
  } 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
- }
1080
+ return { content: t('commands.fast.unknown') }
1066
1081
  }
1067
1082
  }
1068
1083
 
1069
1084
  const effortCmd: CommandHandler = (ctx, args) => {
1085
+ const t = resolveT(ctx)
1070
1086
  const VALID_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
1071
1087
  const level = args[0]?.toLowerCase()
1072
1088
 
1073
1089
  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
- }
1090
+ return { content: t('commands.effort.usage', { model: ctx.modelId }) }
1089
1091
  }
1090
1092
 
1091
1093
  ctx.setEffort(level)
1092
- return { content: `✓ Reasoning effort set to "${level}"` }
1094
+ return { content: t('commands.effort.confirmed', { level }) }
1093
1095
  }
1094
1096
 
1095
1097
  const focusCmd: CommandHandler = (ctx) => {
1098
+ const t = resolveT(ctx)
1096
1099
  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
- }
1100
+ return { content: t('commands.focus.on') }
1104
1101
  }
1105
1102
 
1106
1103
  // ═══════════════════════════════════════════════════════════════
@@ -1108,12 +1105,12 @@ const focusCmd: CommandHandler = (ctx) => {
1108
1105
  // ═══════════════════════════════════════════════════════════════
1109
1106
 
1110
1107
  const ultracodeCmd: CommandHandler = (ctx, args) => {
1108
+ const t = resolveT(ctx)
1111
1109
  const arg = args[0]?.toLowerCase()
1112
1110
  if (arg === 'on') {
1113
1111
  ctx.setUltracodeMode(true)
1114
1112
  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.',
1113
+ content: t('commands.ultracode.on'),
1117
1114
  forwardToAI: stripIndent`
1118
1115
  Ultracode mode is now ACTIVE. For every substantive task:
1119
1116
  - Use the Workflow tool to orchestrate multi-agent execution
@@ -1129,24 +1126,12 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
1129
1126
  } else if (arg === 'off') {
1130
1127
  ctx.setUltracodeMode(false)
1131
1128
  return {
1132
- content: '✓ Ultracode mode OFF.',
1129
+ content: t('commands.ultracode.off'),
1133
1130
  forwardToAI:
1134
1131
  'Ultracode mode is now OFF. Revert to standard single-agent execution. Do NOT use Workflow tool unless explicitly asked.',
1135
1132
  }
1136
1133
  }
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
- }
1134
+ return { content: t('commands.ultracode.usage') }
1150
1135
  }
1151
1136
 
1152
1137
  // ═══════════════════════════════════════════════════════════════
@@ -1154,6 +1139,7 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
1154
1139
  // ═══════════════════════════════════════════════════════════════
1155
1140
 
1156
1141
  const tasksCmd: CommandHandler = (ctx) => {
1142
+ const t = resolveT(ctx)
1157
1143
  const c = ctx.engine.getContext()
1158
1144
  const msgs = c.getMessages()
1159
1145
 
@@ -1169,15 +1155,11 @@ const tasksCmd: CommandHandler = (ctx) => {
1169
1155
 
1170
1156
  return {
1171
1157
  content: stripIndent`
1172
- ── Background Tasks ──
1158
+ ${t('commands.task_list.title')}
1173
1159
 
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
- }
1160
+ ${toolUses.length > 0 ? t('commands.task_list.detected', { count: String(toolUses.length) }) : t('commands.task_list.no_tasks')}
1179
1161
 
1180
- Quick reference:
1162
+ ${t('commands.task_list.reference')}
1181
1163
  TaskCreate — create a new task
1182
1164
  TaskList — list all tasks
1183
1165
  TaskUpdate — update task status
@@ -1185,35 +1167,28 @@ const tasksCmd: CommandHandler = (ctx) => {
1185
1167
  TaskOutput — get background task output
1186
1168
  TaskStop — stop a running task
1187
1169
 
1188
- Type /todos for the legacy task interface.
1170
+ ${t('commands.task_list.legacy_hint')}
1189
1171
  `,
1190
1172
  }
1191
1173
  }
1192
1174
 
1193
1175
  const branchCmd: CommandHandler = (ctx, args) => {
1176
+ const t = resolveT(ctx)
1194
1177
  const name = args.join(' ') || `branch-${Date.now()}`
1195
1178
  const c = ctx.engine.getContext()
1196
1179
  const msgs = c.getMessages()
1197
1180
 
1198
1181
  if (msgs.length === 0) {
1199
- return { content: 'No conversation to branch. Start a conversation first.' }
1182
+ return { content: t('commands.branch.no_conversation') }
1200
1183
  }
1201
1184
 
1202
- // Save current session state as a named checkpoint
1203
1185
  const checkpointId = c.saveCheckpoint(name)
1204
1186
  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
- `,
1187
+ content: t('commands.branch.details', {
1188
+ name,
1189
+ checkpoint: String(checkpointId),
1190
+ messages: String(msgs.length),
1191
+ }),
1217
1192
  }
1218
1193
  }
1219
1194
 
@@ -1450,8 +1425,9 @@ const scheduleCmd: CommandHandler = async (_ctx) => {
1450
1425
  // ═══════════════════════════════════════════════════════════════
1451
1426
 
1452
1427
  const doctorCmd: CommandHandler = async (ctx) => {
1428
+ const t = resolveT(ctx)
1453
1429
  const lines: string[] = [
1454
- '── System Diagnostics ──',
1430
+ t('commands.doctor.title'),
1455
1431
  '',
1456
1432
  `Mipham Code v${ctx.version}`,
1457
1433
  `Runtime ${typeof Bun !== 'undefined' ? 'Bun ' + Bun.version : 'Node.js ' + process.version}`,
@@ -1459,12 +1435,12 @@ const doctorCmd: CommandHandler = async (ctx) => {
1459
1435
  `CWD ${process.cwd()}`,
1460
1436
  `PID ${process.pid}`,
1461
1437
  '',
1462
- '── Config ──',
1438
+ t('commands.doctor.config_section'),
1463
1439
  `Provider ${ctx.providerId} / ${ctx.modelId}`,
1464
1440
  `Permission ${ctx.config.permission}`,
1465
1441
  `Providers ${ctx.config.providers.length} configured (${ctx.config.providers.filter((p) => p.status !== 'upcoming').length} active)`,
1466
1442
  '',
1467
- '── Session ──',
1443
+ t('commands.doctor.session_section'),
1468
1444
  ]
1469
1445
 
1470
1446
  const c = ctx.engine.getContext()
@@ -1480,7 +1456,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
1480
1456
  try {
1481
1457
  const { execSync } = await import('node:child_process')
1482
1458
  lines.push('')
1483
- lines.push('── Git ──')
1459
+ lines.push(t('commands.doctor.git_section'))
1484
1460
  const branch = execSync('git branch --show-current', {
1485
1461
  encoding: 'utf-8',
1486
1462
  timeout: 3000,
@@ -1500,21 +1476,21 @@ const doctorCmd: CommandHandler = async (ctx) => {
1500
1476
  )
1501
1477
  } catch {
1502
1478
  lines.push('')
1503
- lines.push('── Git ──')
1504
- lines.push(' (not a git repository or git not available)')
1479
+ lines.push(t('commands.doctor.git_section'))
1480
+ lines.push(t('commands.doctor.no_git'))
1505
1481
  }
1506
1482
 
1507
1483
  // Skills info
1508
1484
  if (ctx.skillsLoader) {
1509
1485
  lines.push('')
1510
- lines.push('── Skills ──')
1486
+ lines.push(t('commands.doctor.skills_section'))
1511
1487
  try {
1512
1488
  const skills = ctx.skillsLoader.list()
1513
1489
  const standard = skills.filter((s: { type: string }) => s.type === 'standard').length
1514
1490
  const mipham = skills.filter((s: { type: string }) => s.type === 'mipham').length
1515
1491
  lines.push(`Loaded ${skills.length} (${standard} standard + ${mipham} mipham)`)
1516
1492
  } catch {
1517
- lines.push(' (skills info unavailable)')
1493
+ lines.push(t('commands.doctor.skills_unavailable'))
1518
1494
  }
1519
1495
  }
1520
1496
 
@@ -1527,68 +1503,67 @@ const doctorCmd: CommandHandler = async (ctx) => {
1527
1503
 
1528
1504
  function gitDiffBridgeCmd(opts: {
1529
1505
  label: string
1530
- noChangesHint: string
1531
- runningMsg: string
1506
+ noChangesKey: string
1507
+ runningKey: string
1508
+ errorKey: string
1532
1509
  forwardToAI: string | (() => string)
1533
1510
  }): CommandHandler {
1534
- return async () => {
1511
+ return async (ctx) => {
1512
+ const t = resolveT(ctx)
1535
1513
  try {
1536
1514
  const { execSync } = await import('node:child_process')
1537
1515
  const diff = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 }).trim()
1538
1516
  if (!diff) {
1539
- return { content: `─ ${opts.label} ─\n\n${opts.noChangesHint}` }
1517
+ return { content: `─ ${opts.label} ─\n\n${t(opts.noChangesKey)}` }
1540
1518
  }
1541
1519
  return {
1542
- content: `─ ${opts.label} ─\n\n${opts.runningMsg}\n\nChanged files:\n${diff}`,
1520
+ content: `─ ${opts.label} ─\n\n${t(opts.runningKey)}\n\nChanged files:\n${diff}`,
1543
1521
  forwardToAI: typeof opts.forwardToAI === 'function' ? opts.forwardToAI() : opts.forwardToAI,
1544
1522
  }
1545
1523
  } catch {
1546
- return {
1547
- content: `─ ${opts.label} ─\n\nCould not detect changes. Are you in a git repository?`,
1548
- }
1524
+ return { content: `─ ${opts.label} ─\n\n${t(opts.errorKey)}` }
1549
1525
  }
1550
1526
  }
1551
1527
  }
1552
1528
 
1553
1529
  const codeReviewCmd = gitDiffBridgeCmd({
1554
1530
  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)...',
1531
+ noChangesKey: 'commands.code_review.no_changes',
1532
+ runningKey: 'commands.code_review.running',
1533
+ errorKey: 'commands.code_review.error',
1559
1534
  forwardToAI: () =>
1560
1535
  `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
1536
  })
1562
1537
 
1563
1538
  const simplifyCmd = gitDiffBridgeCmd({
1564
1539
  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...',
1540
+ noChangesKey: 'commands.simplify.no_changes',
1541
+ runningKey: 'commands.simplify.running',
1542
+ errorKey: 'commands.simplify.error',
1569
1543
  forwardToAI:
1570
1544
  '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
1545
  })
1572
1546
 
1573
1547
  const verifyCmd = gitDiffBridgeCmd({
1574
1548
  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)...',
1549
+ noChangesKey: 'commands.verify.no_changes',
1550
+ runningKey: 'commands.verify.running',
1551
+ errorKey: 'commands.verify.error',
1579
1552
  forwardToAI:
1580
1553
  '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
1554
  })
1582
1555
 
1583
1556
  const designCmd: CommandHandler = (_ctx, args) => {
1557
+ const t = resolveT(_ctx)
1584
1558
  const topic = args.join(' ') || 'the current task'
1585
1559
  return {
1586
- content: `─ Design ─\n\nStarting architectural design session for: ${topic}\n\nExploring approaches, trade-offs, component breakdown, data flow...`,
1560
+ content: t('commands.design.start', { topic }),
1587
1561
  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
1562
  }
1589
1563
  }
1590
1564
 
1591
- const lintCmd: CommandHandler = async () => {
1565
+ const lintCmd: CommandHandler = async (ctx) => {
1566
+ const t = resolveT(ctx)
1592
1567
  try {
1593
1568
  const { execSync } = await import('node:child_process')
1594
1569
  const { existsSync } = await import('node:fs')
@@ -1620,19 +1595,15 @@ const lintCmd: CommandHandler = async () => {
1620
1595
 
1621
1596
  return {
1622
1597
  content: [
1623
- '── Lint Results ──',
1598
+ t('commands.lint.title'),
1624
1599
  '',
1625
1600
  truncated || '(no issues found)',
1626
1601
  '',
1627
- hasPackageJson
1628
- ? 'To fix: type "fix the lint errors"'
1629
- : 'Set up linting: https://mipham.ai/code/docs/linting',
1602
+ hasPackageJson ? t('commands.lint.fix_hint') : t('commands.lint.setup_hint'),
1630
1603
  ].join('\n'),
1631
1604
  }
1632
1605
  } catch {
1633
- return {
1634
- content: '── Lint ──\n\nCould not run linter. Ensure ESLint is installed and configured.',
1635
- }
1606
+ return { content: t('commands.lint.error') }
1636
1607
  }
1637
1608
  }
1638
1609
 
@@ -1640,7 +1611,8 @@ const lintCmd: CommandHandler = async () => {
1640
1611
  // Session Enhancement Commands (Claude Code parity)
1641
1612
  // ═══════════════════════════════════════════════════════════════
1642
1613
 
1643
- const filesCmd: CommandHandler = async () => {
1614
+ const filesCmd: CommandHandler = async (ctx) => {
1615
+ const t = resolveT(ctx)
1644
1616
  const { readdirSync, statSync } = await import('node:fs')
1645
1617
  const { join } = await import('node:path')
1646
1618
  const cwd = process.cwd()
@@ -1663,22 +1635,25 @@ const filesCmd: CommandHandler = async () => {
1663
1635
 
1664
1636
  return {
1665
1637
  content: [
1666
- '── Project Files ──',
1638
+ t('commands.files.title'),
1667
1639
  '',
1668
- `CWD: ${cwd}`,
1640
+ t('commands.files.cwd', { path: cwd }),
1669
1641
  '',
1670
1642
  ...items,
1671
- entries.length > 40 ? ` ... and ${entries.length - 40} more files` : '',
1643
+ entries.length > 40
1644
+ ? t('commands.files.more_files', { count: String(entries.length - 40) })
1645
+ : '',
1672
1646
  '',
1673
- 'Use Read/Glob/Grep tools to examine files.',
1647
+ t('commands.files.hint'),
1674
1648
  ].join('\n'),
1675
1649
  }
1676
1650
  } catch {
1677
- return { content: '── Files ──\n\nCould not read directory.' }
1651
+ return { content: t('commands.files.error') }
1678
1652
  }
1679
1653
  }
1680
1654
 
1681
1655
  const statsCmd: CommandHandler = (ctx) => {
1656
+ const t = resolveT(ctx)
1682
1657
  const c = ctx.engine.getContext()
1683
1658
  const msgs = c.getMessages()
1684
1659
  const tokens = c.getEstimatedTokens()
@@ -1690,26 +1665,32 @@ const statsCmd: CommandHandler = (ctx) => {
1690
1665
 
1691
1666
  return {
1692
1667
  content: [
1693
- '── Session Stats ──',
1668
+ t('commands.stats.title'),
1694
1669
  '',
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}`,
1670
+ t('commands.stats.messages', {
1671
+ total: String(msgs.length),
1672
+ user: String(userMsgs),
1673
+ assistant: String(assistantMsgs),
1674
+ system: String(systemMsgs),
1675
+ }),
1676
+ t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
1677
+ t('commands.stats.tools', { count: String(tools.size) }),
1678
+ t('commands.stats.provider', { provider: ctx.providerId }),
1679
+ t('commands.stats.model', { model: ctx.modelId }),
1680
+ t('commands.stats.permission', { permission: ctx.config.permission }),
1701
1681
  '',
1702
- `Usage: ${((tokens / 200_000) * 100).toFixed(1)}% of context window`,
1682
+ t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
1703
1683
  ].join('\n'),
1704
1684
  }
1705
1685
  }
1706
1686
 
1707
1687
  const summaryCmd: CommandHandler = (ctx) => {
1688
+ const t = resolveT(ctx)
1708
1689
  const c = ctx.engine.getContext()
1709
1690
  const msgs = c.getMessages()
1710
1691
 
1711
1692
  if (msgs.length === 0) {
1712
- return { content: '── Summary ──\n\nNo conversation to summarize.' }
1693
+ return { content: t('commands.summary.no_conversation') }
1713
1694
  }
1714
1695
 
1715
1696
  const userMsgs = msgs
@@ -1722,32 +1703,24 @@ const summaryCmd: CommandHandler = (ctx) => {
1722
1703
 
1723
1704
  return {
1724
1705
  content: [
1725
- '── Session Summary ──',
1706
+ t('commands.summary.title'),
1726
1707
  '',
1727
- `Total messages: ${msgs.length}`,
1728
- `Est. tokens: ~${c.getEstimatedTokens().toLocaleString()}`,
1708
+ t('commands.summary.total_messages', { count: String(msgs.length) }),
1709
+ t('commands.summary.est_tokens', { tokens: c.getEstimatedTokens().toLocaleString() }),
1729
1710
  '',
1730
- 'Recent topics:',
1711
+ t('commands.summary.recent_topics'),
1731
1712
  ...userMsgs.map((t, i) => ` ${i + 1}. ${t}`),
1732
1713
  '',
1733
- 'Use /export to save, /clear to reset.',
1714
+ t('commands.summary.footer'),
1734
1715
  ].join('\n'),
1735
1716
  }
1736
1717
  }
1737
1718
 
1738
1719
  const cdCmd: CommandHandler = async (ctx, args) => {
1720
+ const t = resolveT(ctx)
1739
1721
  const target = args[0]
1740
1722
  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
- }
1723
+ return { content: t('commands.cd.usage', { cwd: process.cwd() }) }
1751
1724
  }
1752
1725
 
1753
1726
  const { existsSync } = await import('node:fs')
@@ -1755,13 +1728,12 @@ const cdCmd: CommandHandler = async (ctx, args) => {
1755
1728
  const resolved = resolve(target.replace(/^~/, process.env.HOME || '~'))
1756
1729
 
1757
1730
  if (!existsSync(resolved)) {
1758
- return { content: `❌ Directory not found: ${resolved}` }
1731
+ return { content: t('commands.cd.not_found', { path: resolved }) }
1759
1732
  }
1760
1733
 
1761
1734
  try {
1762
1735
  process.chdir(resolved)
1763
1736
 
1764
- // Persist cwd to active session (best-effort)
1765
1737
  try {
1766
1738
  const { SessionStore } = await import('../core/session-store')
1767
1739
  const saved = SessionStore.load(ctx.sessionId)
@@ -1776,47 +1748,31 @@ const cdCmd: CommandHandler = async (ctx, args) => {
1776
1748
  /* session persistence is best-effort */
1777
1749
  }
1778
1750
 
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
- }
1751
+ return { content: t('commands.cd.content', { path: resolved }) }
1789
1752
  } catch (err) {
1790
- return { content: `❌ Failed to change directory: ${(err as Error).message}` }
1753
+ return { content: t('commands.cd.failed', { error: (err as Error).message }) }
1791
1754
  }
1792
1755
  }
1793
1756
 
1794
- const hooksCmd: CommandHandler = async () => {
1757
+ const hooksCmd: CommandHandler = async (ctx) => {
1758
+ const t = resolveT(ctx)
1795
1759
  const { existsSync, readdirSync } = await import('node:fs')
1796
1760
  const { join } = await import('node:path')
1797
1761
  const cwd = process.cwd()
1798
1762
  const hooksDir = join(cwd, '.mipham', 'hooks')
1799
1763
 
1800
1764
  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
- }
1765
+ return { content: t('commands.hooks.no_hooks') }
1815
1766
  }
1816
1767
 
1817
1768
  try {
1818
1769
  const files = readdirSync(hooksDir).filter((f) => f.endsWith('.sh'))
1819
- const lines: string[] = ['── Hooks ──', '', `Location: ${hooksDir}`, '']
1770
+ const lines: string[] = [
1771
+ t('commands.hooks.title'),
1772
+ '',
1773
+ t('commands.hooks.location', { path: hooksDir }),
1774
+ '',
1775
+ ]
1820
1776
 
1821
1777
  for (const f of files) {
1822
1778
  try {
@@ -1831,35 +1787,16 @@ const hooksCmd: CommandHandler = async () => {
1831
1787
  }
1832
1788
 
1833
1789
  lines.push('')
1834
- lines.push(`${files.length} hook(s) found.`)
1790
+ lines.push(t('commands.hooks.found', { count: String(files.length) }))
1835
1791
  return { content: lines.join('\n') }
1836
1792
  } catch {
1837
- return { content: '── Hooks ──\n\nCould not read hooks directory.' }
1793
+ return { content: t('commands.hooks.error') }
1838
1794
  }
1839
1795
  }
1840
1796
 
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
- }
1797
+ const batchCmd: CommandHandler = async (ctx) => {
1798
+ const t = resolveT(ctx)
1799
+ return { content: t('commands.batch.title') }
1863
1800
  }
1864
1801
 
1865
1802
  // ═══════════════════════════════════════════════════════════════
@@ -1867,6 +1804,7 @@ const batchCmd: CommandHandler = async () => {
1867
1804
  // ═══════════════════════════════════════════════════════════════
1868
1805
 
1869
1806
  const exportCmd: CommandHandler = async (ctx) => {
1807
+ const t = resolveT(ctx)
1870
1808
  const { writeFileSync } = await import('node:fs')
1871
1809
  const { join } = await import('node:path')
1872
1810
 
@@ -1897,7 +1835,11 @@ const exportCmd: CommandHandler = async (ctx) => {
1897
1835
 
1898
1836
  writeFileSync(filepath, lines.join('\n'), 'utf-8')
1899
1837
  return {
1900
- content: `✓ Session exported to:\n ${filepath}\n\n${msgs.length} messages · ${lines.length} lines`,
1838
+ content: t('commands.export.confirmed', {
1839
+ path: filepath,
1840
+ count: String(msgs.length),
1841
+ lines: String(lines.length),
1842
+ }),
1901
1843
  copyContent: filepath,
1902
1844
  }
1903
1845
  }
@@ -1976,21 +1918,21 @@ const prCommentsCmd: CommandHandler = async () => {
1976
1918
  // Resume
1977
1919
  // ═══════════════════════════════════════════════════════════════
1978
1920
 
1979
- const resumeLastCmd: CommandHandler = async () => {
1921
+ const resumeLastCmd: CommandHandler = async (ctx) => {
1922
+ const t = resolveT(ctx)
1980
1923
  const { SessionStore } = await import('../core/session-store')
1981
1924
 
1982
1925
  const latest = SessionStore.getLatest()
1983
1926
  if (!latest) {
1984
1927
  return {
1985
- content:
1986
- '─ Resume Session ─\n\nNo saved sessions found.\n\nSessions are auto-saved to ~/.mipham/sessions/ when Mipham Code exits.',
1928
+ content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}`,
1987
1929
  }
1988
1930
  }
1989
1931
 
1990
1932
  const session = SessionStore.load(latest.name)
1991
1933
  if (!session) {
1992
1934
  return {
1993
- content: `─ Load Failed ─\n\nCould not load session "${latest.name}". The file may have been removed.`,
1935
+ content: `${t('commands.resume.load_failed')}\n\nCould not load session "${latest.name}". The file may have been removed.`,
1994
1936
  }
1995
1937
  }
1996
1938
 
@@ -1999,79 +1941,71 @@ const resumeLastCmd: CommandHandler = async () => {
1999
1941
  const messages = truncated ? session.messages.slice(-MAX_RESUME) : session.messages
2000
1942
 
2001
1943
  const date = new Date(latest.updatedAt).toLocaleString()
1944
+ const truncatedStr = truncated ? ` (showing last ${MAX_RESUME})` : ''
1945
+
2002
1946
  return {
2003
1947
  content: [
2004
- '─ Session Restored ─',
1948
+ t('commands.resume.restored'),
2005
1949
  '',
2006
- `Name: ${latest.name}`,
2007
- `Messages: ${session.messages.length} total${truncated ? ` (showing last ${MAX_RESUME})` : ''}`,
2008
- `Provider: ${latest.provider} / ${latest.model}`,
2009
- `Updated: ${date}`,
1950
+ t('commands.resume.restored_content', {
1951
+ name: latest.name,
1952
+ total: String(session.messages.length),
1953
+ truncated: truncatedStr,
1954
+ provider: latest.provider,
1955
+ model: latest.model,
1956
+ date,
1957
+ }),
2010
1958
  '',
2011
1959
  truncated
2012
- ? `${messages.length} of ${session.messages.length} messages loaded. Older context summarized above.`
2013
- : `${messages.length} messages loaded. Context has been restored.`,
1960
+ ? t('commands.resume.restored_footer', {
1961
+ loaded: String(messages.length),
1962
+ total: String(session.messages.length),
1963
+ })
1964
+ : t('commands.resume.restored_full_footer', { loaded: String(messages.length) }),
2014
1965
  ].join('\n'),
2015
1966
  forwardedMessages: messages,
2016
1967
  }
2017
1968
  }
2018
1969
 
2019
- const resumeDeleteCmd: CommandHandler = async (_ctx, args) => {
1970
+ const resumeDeleteCmd: CommandHandler = async (ctx, args) => {
1971
+ const t = resolveT(ctx)
2020
1972
  const name = args.join(' ').trim()
2021
1973
  if (!name) {
2022
- return {
2023
- content:
2024
- 'Usage: /resume delete <session-name>\n\nDelete a saved session. Use /resume to list all sessions.',
2025
- }
1974
+ return { content: t('commands.resume.delete_usage') }
2026
1975
  }
2027
1976
 
2028
1977
  const { SessionStore } = await import('../core/session-store')
2029
1978
  const deleted = SessionStore.delete(name)
2030
1979
 
2031
1980
  return deleted
2032
- ? { content: `✓ Session "${name}" deleted.` }
2033
- : { content: `✗ Session "${name}" not found. Use /resume to list all sessions.` }
1981
+ ? { content: t('commands.resume.delete_confirmed', { name }) }
1982
+ : { content: t('commands.resume.delete_not_found', { name }) }
2034
1983
  }
2035
1984
 
2036
- const resumeCmd: CommandHandler = async (_ctx, args) => {
1985
+ const resumeCmd: CommandHandler = async (ctx, args) => {
1986
+ const t = resolveT(ctx)
2037
1987
  const sub = args[0]?.toLowerCase()
2038
1988
 
2039
- // Sub-command: /resume last
2040
1989
  if (sub === 'last') {
2041
- return resumeLastCmd(_ctx, args.slice(1))
1990
+ return resumeLastCmd(ctx, args.slice(1))
2042
1991
  }
2043
1992
 
2044
- // Sub-command: /resume delete <name>
2045
1993
  if (sub === 'delete') {
2046
- return resumeDeleteCmd(_ctx, args.slice(1))
1994
+ return resumeDeleteCmd(ctx, args.slice(1))
2047
1995
  }
2048
1996
 
2049
1997
  const { SessionStore } = await import('../core/session-store')
2050
1998
 
2051
- // If a name is provided, show load instructions
2052
1999
  const targetName = args.join(' ')
2053
2000
  if (targetName) {
2054
2001
  const session = SessionStore.load(targetName)
2055
2002
  if (session) {
2056
2003
  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'),
2004
+ 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
2005
  }
2072
2006
  }
2073
2007
  return {
2074
- content: `─ Session Not Found ─\n\nNo session named "${targetName}".\n\nUse /resume without arguments to list all saved sessions.`,
2008
+ content: `${t('commands.resume.not_found_title')}\n\n${t('commands.resume.not_found_content', { name: targetName })}`,
2075
2009
  }
2076
2010
  }
2077
2011
 
@@ -2079,29 +2013,28 @@ const resumeCmd: CommandHandler = async (_ctx, args) => {
2079
2013
 
2080
2014
  if (sessions.length === 0) {
2081
2015
  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.',
2016
+ content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}`,
2084
2017
  }
2085
2018
  }
2086
2019
 
2087
2020
  const recent = sessions.slice(0, 10)
2088
2021
 
2089
2022
  const lines: string[] = [
2090
- '─ Saved Sessions ─',
2023
+ t('commands.resume.saved_title'),
2091
2024
  '',
2092
2025
  ...recent.map(
2093
2026
  (s, i) =>
2094
2027
  ` ${(i + 1).toString().padStart(2)}. ${s.name.padEnd(45)} ${s.messageCount.toString().padStart(4)} msgs ${new Date(s.updatedAt).toLocaleString()}`,
2095
2028
  ),
2096
2029
  '',
2097
- `Total: ${sessions.length} session(s) • Location: ~/.mipham/sessions/`,
2030
+ t('commands.resume.total_footer', { count: String(sessions.length) }),
2098
2031
  '',
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>"',
2032
+ t('commands.resume.resume_hint'),
2033
+ t('commands.resume.resume_last_hint'),
2034
+ t('commands.resume.delete_hint'),
2035
+ t('commands.resume.cli_hint'),
2103
2036
  '',
2104
- 'Sessions are auto-saved on exit. The most recent session loads automatically on restart.',
2037
+ t('commands.resume.auto_save_hint'),
2105
2038
  ]
2106
2039
 
2107
2040
  return { content: lines.join('\n') }
@@ -2111,7 +2044,8 @@ const resumeCmd: CommandHandler = async (_ctx, args) => {
2111
2044
  // Memory Management
2112
2045
  // ═══════════════════════════════════════════════════════════════
2113
2046
 
2114
- const memoryCmd: CommandHandler = async () => {
2047
+ const memoryCmd: CommandHandler = async (ctx) => {
2048
+ const t = resolveT(ctx)
2115
2049
  const { existsSync, readdirSync, readFileSync, statSync } = await import('node:fs')
2116
2050
  const { join } = await import('node:path')
2117
2051
 
@@ -2119,16 +2053,13 @@ const memoryCmd: CommandHandler = async () => {
2119
2053
  const memoryDir = join(home, '.mipham', 'memory')
2120
2054
 
2121
2055
  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
- }
2056
+ return { content: t('commands.memories.no_memories') }
2126
2057
  }
2127
2058
 
2128
2059
  try {
2129
2060
  const files = readdirSync(memoryDir).filter((f) => f.endsWith('.md'))
2130
2061
  if (files.length === 0) {
2131
- return { content: '─ Memory ─\n\nNo memory files found in ~/.mipham/memory/' }
2062
+ return { content: t('commands.memories.empty') }
2132
2063
  }
2133
2064
 
2134
2065
  const memories: Array<{ file: string; size: number; mtime: Date; title: string }> = []
@@ -2149,23 +2080,25 @@ const memoryCmd: CommandHandler = async () => {
2149
2080
  memories.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
2150
2081
 
2151
2082
  const lines: string[] = [
2152
- '─ Memory ─',
2083
+ t('commands.memories.title'),
2153
2084
  '',
2154
- `Location: ${memoryDir}`,
2155
- `Total: ${memories.length} memor${memories.length === 1 ? 'y' : 'ies'}`,
2085
+ t('commands.memories.location', { path: memoryDir }),
2086
+ t('commands.memories.total', {
2087
+ count: String(memories.length),
2088
+ plural: memories.length === 1 ? 'y' : 'ies',
2089
+ }),
2156
2090
  '',
2157
2091
  ...memories.map(
2158
2092
  (m, i) =>
2159
2093
  ` ${(i + 1).toString().padStart(2)}. ${m.file.padEnd(35)} ${(m.size / 1024).toFixed(1)}KB ${m.mtime.toLocaleDateString()} ${m.title}`,
2160
2094
  ),
2161
2095
  '',
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.',
2096
+ t('commands.memories.footer'),
2164
2097
  ]
2165
2098
 
2166
2099
  return { content: lines.join('\n') }
2167
2100
  } catch {
2168
- return { content: '─ Memory ─\n\nCould not read memory directory.' }
2101
+ return { content: t('commands.memories.error') }
2169
2102
  }
2170
2103
  }
2171
2104
 
@@ -2173,7 +2106,8 @@ const memoryCmd: CommandHandler = async () => {
2173
2106
  // Upgrade
2174
2107
  // ═══════════════════════════════════════════════════════════════
2175
2108
 
2176
- const upgradeCmd: CommandHandler = async () => {
2109
+ const upgradeCmd: CommandHandler = async (ctx) => {
2110
+ const t = resolveT(ctx)
2177
2111
  const { checkForUpdates, backupConfig, performUpdate, restoreConfig, getConfigPath } =
2178
2112
  await import('../shared/update')
2179
2113
 
@@ -2181,27 +2115,16 @@ const upgradeCmd: CommandHandler = async () => {
2181
2115
 
2182
2116
  if (!update.available) {
2183
2117
  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`,
2118
+ content: t('commands.upgrade.uptodate', { current: update.current, latest: update.latest }),
2192
2119
  }
2193
2120
  }
2194
2121
 
2195
- // Update available — back up config and perform the upgrade
2196
2122
  const backupPath = backupConfig(`upgrade-v${update.current}`)
2197
2123
 
2198
2124
  const lines: string[] = [
2199
- '── Upgrade Mipham Code ──',
2125
+ t('commands.upgrade.title'),
2200
2126
  '',
2201
- `Current version: v${update.current}`,
2202
- `Latest: v${update.latest}`,
2203
- '',
2204
- `→ New version available! Updating...`,
2127
+ t('commands.upgrade.available', { current: update.current, latest: update.latest }),
2205
2128
  '',
2206
2129
  ]
2207
2130
 
@@ -2215,39 +2138,49 @@ To check manually: https://www.npmjs.com/package/@miphamai/cli`,
2215
2138
  const configPath = getConfigPath()
2216
2139
  const { existsSync } = await import('node:fs')
2217
2140
  lines.push('')
2218
- lines.push(`✓ Updated to @miphamai/cli v${update.latest}`)
2141
+ lines.push(t('commands.upgrade.updated', { version: update.latest }))
2219
2142
 
2220
2143
  if (existsSync(configPath)) {
2221
- lines.push(`✓ Config preserved: ${configPath}`)
2144
+ lines.push(t('commands.upgrade.config_preserved', { path: configPath }))
2222
2145
  } else if (backupPath) {
2223
2146
  if (restoreConfig(backupPath)) {
2224
- lines.push('✓ Config restored from backup.')
2147
+ lines.push(t('commands.upgrade.config_restored'))
2225
2148
  }
2226
2149
  }
2227
2150
 
2228
2151
  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.')
2152
+ lines.push(t('commands.upgrade.old_version_warning'))
2231
2153
  } else {
2232
2154
  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
- }
2155
+ lines.push(
2156
+ t('commands.upgrade.update_failed', { command: NPM_UPDATE_COMMAND, path: backupPath || '' }),
2157
+ )
2238
2158
  }
2239
2159
 
2240
2160
  return { content: lines.join('\n') }
2241
2161
  }
2242
2162
 
2163
+ // ═══════════════════════════════════════════════════════════════
2164
+ // Language — i18n locale control
2165
+ // ═══════════════════════════════════════════════════════════════
2166
+
2167
+ const langCmd: CommandHandler = (ctx, args) => {
2168
+ const t = resolveT(ctx)
2169
+ const requested = args[0]
2170
+ if (!requested || !['en-US', 'zh-CN'].includes(requested)) {
2171
+ return { content: t('commands.lang.current', { locale: 'en-US' }) }
2172
+ }
2173
+ return { content: t('commands.lang.set', { locale: requested }) }
2174
+ }
2175
+
2243
2176
  // ═══════════════════════════════════════════════════════════════
2244
2177
  // No-Plan — exit plan mode
2245
2178
  // ═══════════════════════════════════════════════════════════════
2246
2179
 
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
- })
2180
+ const noPlanCmd: CommandHandler = (ctx) => {
2181
+ const t = resolveT(ctx)
2182
+ return { content: t('commands.no_plan.confirmed') }
2183
+ }
2251
2184
 
2252
2185
  // ═══════════════════════════════════════════════════════════════
2253
2186
  // Workflows
@@ -3310,6 +3243,7 @@ registry.set('/prompt-audit', promptAuditCmd)
3310
3243
 
3311
3244
  // Environment
3312
3245
  registry.set('/theme', themeCmd)
3246
+ registry.set('/lang', langCmd)
3313
3247
  registry.set('/ide', ideCmd)
3314
3248
  registry.set('/terminal-setup', terminalSetupCmd)
3315
3249
  registry.set('/release-notes', releaseNotesCmd)
@@ -3407,6 +3341,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
3407
3341
  '/fast': 'Toggle fast mode',
3408
3342
  '/effort': 'Set reasoning effort',
3409
3343
  '/theme': 'Set terminal theme',
3344
+ '/lang': 'Set display language (en-US, zh-CN)',
3410
3345
  '/tools': 'List available tools',
3411
3346
  '/skills': 'List loaded skills',
3412
3347
  '/reload-skills': 'Reload all skills',