From 8a69993056c16e007b162085f4ae8964434ddf9f Mon Sep 17 00:00:00 2001 From: kimi Date: Sun, 12 Jul 2026 08:05:20 +0800 Subject: [PATCH 1/2] feat(i18n): add multi-language internationalization support - Add i18n engine with Chinese (zh) and English (en) locales - Add locale selector dialog for runtime language switching (/locale command) - Replace all hardcoded English strings with t() calls across CLI, TUI, and Web UI - Add comprehensive i18n test coverage - Add changeset for i18n-coverage --- .changeset/i18n-coverage.md | 7 + apps/kimi-code/package.json | 1 + apps/kimi-code/src/cli/run-prompt.ts | 67 +- apps/kimi-code/src/cli/run-shell.ts | 7 +- apps/kimi-code/src/cli/sub/doctor.ts | 29 +- apps/kimi-code/src/cli/sub/export.ts | 17 +- apps/kimi-code/src/cli/sub/login-flow.ts | 15 +- apps/kimi-code/src/cli/sub/provider.ts | 71 +- .../src/cli/sub/server/access-urls.ts | 9 +- apps/kimi-code/src/cli/sub/server/daemon.ts | 10 +- apps/kimi-code/src/cli/sub/server/kill.ts | 11 +- .../kimi-code/src/cli/sub/server/lifecycle.ts | 41 +- apps/kimi-code/src/cli/sub/server/ps.ts | 14 +- .../src/cli/sub/server/rotate-token.ts | 7 +- apps/kimi-code/src/cli/sub/server/run.ts | 39 +- apps/kimi-code/src/cli/sub/server/shared.ts | 8 +- apps/kimi-code/src/cli/sub/upgrade.ts | 5 +- apps/kimi-code/src/cli/sub/vis.ts | 19 +- apps/kimi-code/src/cli/update/preflight.ts | 16 +- apps/kimi-code/src/cli/update/prompt.ts | 21 +- apps/kimi-code/src/i18n/index.ts | 90 + apps/kimi-code/src/i18n/locales/en.ts | 1617 +++++++++++++++++ apps/kimi-code/src/i18n/locales/zh.ts | 1612 ++++++++++++++++ apps/kimi-code/src/main.ts | 34 +- apps/kimi-code/src/tui/commands/add-dir.ts | 29 +- apps/kimi-code/src/tui/commands/auth.ts | 33 +- apps/kimi-code/src/tui/commands/btw.ts | 8 +- apps/kimi-code/src/tui/commands/config.ts | 161 +- apps/kimi-code/src/tui/commands/dispatch.ts | 20 +- apps/kimi-code/src/tui/commands/goal.ts | 52 +- apps/kimi-code/src/tui/commands/info.ts | 3 +- apps/kimi-code/src/tui/commands/plugins.ts | 99 +- apps/kimi-code/src/tui/commands/prompts.ts | 21 +- apps/kimi-code/src/tui/commands/provider.ts | 47 +- apps/kimi-code/src/tui/commands/registry.ts | 584 +++--- apps/kimi-code/src/tui/commands/reload.ts | 7 +- apps/kimi-code/src/tui/commands/resolve.ts | 5 +- apps/kimi-code/src/tui/commands/session.ts | 43 +- apps/kimi-code/src/tui/commands/swarm.ts | 19 +- apps/kimi-code/src/tui/commands/undo.ts | 39 +- apps/kimi-code/src/tui/commands/web.ts | 25 +- .../dialogs/api-key-input-dialog.ts | 7 +- .../tui/components/dialogs/approval-panel.ts | 28 +- .../components/dialogs/approval-preview.ts | 9 +- .../tui/components/dialogs/choice-picker.ts | 12 +- .../src/tui/components/dialogs/compaction.ts | 13 +- .../dialogs/custom-registry-import.ts | 17 +- .../tui/components/dialogs/editor-selector.ts | 15 +- .../tui/components/dialogs/effort-selector.ts | 9 +- .../dialogs/experiments-selector.ts | 47 +- .../dialogs/feedback-input-dialog.ts | 9 +- .../components/dialogs/goal-queue-manager.ts | 27 +- .../dialogs/goal-start-permission-prompt.ts | 113 +- .../src/tui/components/dialogs/help-panel.ts | 46 +- .../tui/components/dialogs/locale-selector.ts | 41 + .../tui/components/dialogs/model-selector.ts | 64 +- .../components/dialogs/permission-selector.ts | 43 +- .../components/dialogs/platform-selector.ts | 5 +- .../components/dialogs/plugins-selector.ts | 262 ++- .../components/dialogs/provider-manager.ts | 20 +- .../tui/components/dialogs/question-dialog.ts | 76 +- .../tui/components/dialogs/session-picker.ts | 43 +- .../components/dialogs/settings-selector.ts | 90 +- .../dialogs/start-permission-prompt.ts | 3 +- .../dialogs/swarm-start-permission-prompt.ts | 50 +- .../dialogs/tabbed-model-selector.ts | 3 +- .../components/dialogs/task-output-viewer.ts | 43 +- .../tui/components/dialogs/tasks-browser.ts | 107 +- .../tui/components/dialogs/theme-selector.ts | 21 +- .../tui/components/dialogs/undo-selector.ts | 8 +- .../dialogs/update-preference-selector.ts | 11 +- .../tui/components/messages/agent-group.ts | 93 +- .../messages/agent-swarm-progress.ts | 74 +- .../messages/background-agent-status.ts | 3 +- .../tui/components/messages/cron-message.ts | 17 +- .../tui/components/messages/goal-format.ts | 16 +- .../tui/components/messages/goal-markers.ts | 25 +- .../src/tui/components/messages/goal-panel.ts | 53 +- .../components/messages/mcp-status-panel.ts | 78 +- .../src/tui/components/messages/plan-box.ts | 7 +- .../tui/components/messages/plugin-command.ts | 5 +- .../messages/plugins-status-panel.ts | 145 +- .../src/tui/components/messages/read-group.ts | 33 +- .../src/tui/components/messages/shell-run.ts | 14 +- .../components/messages/skill-activation.ts | 5 +- .../tui/components/messages/status-panel.ts | 52 +- .../tui/components/messages/step-summary.ts | 23 +- .../tui/components/messages/swarm-markers.ts | 7 +- .../src/tui/components/messages/thinking.ts | 5 +- .../src/tui/components/messages/tool-call.ts | 269 ++- .../messages/tool-renderers/goal.ts | 21 +- .../tui/components/messages/usage-panel.ts | 88 +- .../src/tui/components/panes/btw-panel.ts | 15 +- .../src/tui/components/panes/queue-pane.ts | 7 +- apps/kimi-code/src/tui/config.ts | 7 + apps/kimi-code/src/tui/constant/feedback.ts | 28 +- apps/kimi-code/src/tui/constant/kimi-tui.ts | 21 +- .../src/tui/controllers/auth-flow.ts | 4 +- .../src/tui/controllers/btw-panel.ts | 9 +- .../tui/controllers/clipboard-image-hint.ts | 3 +- .../src/tui/controllers/editor-keyboard.ts | 23 +- .../tui/controllers/session-event-handler.ts | 75 +- .../src/tui/controllers/session-replay.ts | 39 +- .../src/tui/controllers/streaming-ui.ts | 3 +- .../src/tui/controllers/tasks-browser.ts | 25 +- apps/kimi-code/src/tui/easter-eggs/dance.ts | 5 +- apps/kimi-code/src/tui/goal-queue-store.ts | 17 +- apps/kimi-code/src/tui/kimi-tui.ts | 135 +- .../src/tui/reverse-rpc/approval/adapter.ts | 93 +- apps/kimi-code/src/tui/types.ts | 1 + .../src/tui/utils/background-agent-status.ts | 8 +- .../src/tui/utils/background-task-status.ts | 22 +- apps/kimi-code/src/tui/utils/event-payload.ts | 3 +- .../src/tui/utils/goal-completion.ts | 8 +- .../src/tui/utils/mcp-server-status.ts | 12 +- .../test/cli/update/preflight.test.ts | 1 + apps/kimi-code/test/i18n/index.test.ts | 72 + apps/kimi-code/test/tui/activity-pane.test.ts | 1 + .../test/tui/commands/registry.test.ts | 4 +- .../test/tui/components/chrome/footer.test.ts | 1 + .../tui/components/chrome/welcome.test.ts | 1 + apps/kimi-code/test/tui/config.test.ts | 5 + .../test/tui/create-tui-state.test.ts | 1 + .../test/tui/kimi-tui-message-flow.test.ts | 3 +- .../test/tui/kimi-tui-startup.test.ts | 1 + .../kimi-code/test/tui/message-replay.test.ts | 1 + .../test/tui/signal-handlers.test.ts | 1 + .../src/components/ServerAuthDialog.vue | 11 +- .../src/components/chat/GoalStrip.vue | 127 +- .../kimi-web/src/components/ui/CommandBar.vue | 4 +- apps/kimi-web/src/components/ui/Dialog.vue | 5 +- apps/kimi-web/src/components/ui/Sheet.vue | 4 +- apps/kimi-web/src/i18n/locales/en/app.ts | 11 + apps/kimi-web/src/i18n/locales/zh/app.ts | 11 + docs/.vitepress/config.ts | 2 + 135 files changed, 6140 insertions(+), 1927 deletions(-) create mode 100644 .changeset/i18n-coverage.md create mode 100644 apps/kimi-code/src/i18n/index.ts create mode 100644 apps/kimi-code/src/i18n/locales/en.ts create mode 100644 apps/kimi-code/src/i18n/locales/zh.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/locale-selector.ts create mode 100644 apps/kimi-code/test/i18n/index.test.ts diff --git a/.changeset/i18n-coverage.md b/.changeset/i18n-coverage.md new file mode 100644 index 0000000000..80ea70951d --- /dev/null +++ b/.changeset/i18n-coverage.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Replace hardcoded English strings across CLI subcommands, TUI components, and update prompts with i18n `t()` calls so they respect the configured locale. Also fix ZH locale key parity (1375/1375) and a `slashCommands` path mismatch that caused raw key fallback. + +web: Replace hardcoded strings in ServerAuthDialog, GoalStrip, CommandBar, Sheet, and Dialog with i18n calls. diff --git a/apps/kimi-code/package.json b/apps/kimi-code/package.json index d2453bab00..aa291cd6ff 100644 --- a/apps/kimi-code/package.json +++ b/apps/kimi-code/package.json @@ -35,6 +35,7 @@ ], "type": "module", "imports": { + "#/i18n": "./src/i18n/index.ts", "#/tui/theme": "./src/tui/theme/index.ts", "#/cli/sub/server": "./src/cli/sub/server/index.ts", "#/cli/sub/server/*": "./src/cli/sub/server/*.ts", diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index bcce141362..7a53209c9c 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -20,6 +20,7 @@ import { import { resolve } from 'pathe'; import { CLI_SHUTDOWN_TIMEOUT_MS, PROMPT_CLEANUP_TIMEOUT_MS } from '#/constant/app'; +import { t } from '#/i18n'; import type { CLIOptions, PromptOutputFormat } from './options'; import { @@ -160,7 +161,7 @@ export async function runPrompt( await harness.ensureConfigFile(); const config = await harness.getConfig(); for (const warning of (await harness.getConfigDiagnostics()).warnings) { - stderr.write(`Warning: ${warning}\n`); + stderr.write(`${t('tui.statusMessages.promptWarning', { warning })}\n`); } const { session, restorePermission, telemetryModel, goalModel } = await resolvePromptSession( @@ -333,7 +334,7 @@ async function resolvePromptSession( goalModel: configuredModel(opts.model, status.model), }; } - stderr.write(`No sessions to continue under "${workDir}"; starting a fresh session.\n`); + stderr.write(`${t('tui.statusMessages.noSessionsToContinue', { workDir })}\n`); } const model = requireConfiguredModel(opts.model, defaultModel); @@ -501,7 +502,9 @@ function runPromptTurn( return; case 'turn.step.retrying': outputWriter.discardAssistant(); - outputWriter.writeRetrying(event); + outputWriter.writeStatus( + `Retrying (${event.nextAttempt}/${event.maxAttempts}) in ${Math.ceil(event.delayMs / 1000)}s — ${event.errorName}`, + ); return; case 'assistant.delta': outputWriter.writeAssistantDelta(event.delta); @@ -613,7 +616,7 @@ interface PromptTurnWriter { argumentsPart: string | undefined, ): void; writeToolResult(toolCallId: string, output: unknown): void; - writeRetrying(event: Extract): void; + writeStatus(message: string): void; flushAssistant(): void; discardAssistant(): void; finish(): void; @@ -622,10 +625,12 @@ interface PromptTurnWriter { class PromptTranscriptWriter implements PromptTurnWriter { private readonly assistantWriter: PromptBlockWriter; private readonly thinkingWriter: PromptBlockWriter; + private readonly stderr: PromptOutput; constructor(stdout: PromptOutput, stderr: PromptOutput) { this.assistantWriter = new PromptBlockWriter(stdout); this.thinkingWriter = new PromptBlockWriter(stderr); + this.stderr = stderr; } writeAssistantDelta(delta: string): void { @@ -650,10 +655,11 @@ class PromptTranscriptWriter implements PromptTurnWriter { writeToolResult(): void {} - // Text `-p` keeps retries silent: only the failed attempt's partial assistant - // text is discarded (handled by the caller). No human-readable retry line is - // emitted, matching the prior behavior. - writeRetrying(): void {} + writeStatus(message: string): void { + this.assistantWriter.finish(); + this.thinkingWriter.finish(); + this.stderr.write(`${message}\n`); + } flushAssistant(): void { this.assistantWriter.finish(); @@ -696,16 +702,10 @@ interface PromptJsonResumeMetaMessage { content: string; } -interface PromptJsonRetryMetaMessage { +interface PromptJsonMetaMessage { role: 'meta'; - type: 'turn.step.retrying'; - failed_attempt: number; - next_attempt: number; - max_attempts: number; - delay_ms: number; - error_name: string; - error_message: string; - status_code?: number; + type: string; + content: string; } function writeResumeHint( @@ -715,7 +715,7 @@ function writeResumeHint( stderr: PromptOutput, ): void { const command = `kimi -r ${sessionId}`; - const content = `To resume this session: ${command}`; + const content = t('tui.statusMessages.shellResumeHint', { sessionId }); if (outputFormat === 'stream-json') { const message: PromptJsonResumeMetaMessage = { role: 'meta', @@ -790,6 +790,11 @@ class PromptJsonWriter implements PromptTurnWriter { }); } + writeStatus(message: string): void { + this.flushAssistant(); + this.writeJsonLine({ role: 'meta', type: 'status', content: message } as PromptJsonMetaMessage); + } + flushAssistant(): void { if (this.assistantText.length === 0 && this.toolCalls.length === 0) return; const message: PromptJsonAssistantMessage = { @@ -806,24 +811,6 @@ class PromptJsonWriter implements PromptTurnWriter { this.toolCalls.length = 0; } - writeRetrying(event: Extract): void { - // Emit a machine-readable meta line so stream-json consumers can observe - // provider retries. The failed attempt's partial assistant text was already - // discarded by the caller, so no half-formed assistant message leaks. - const message: PromptJsonRetryMetaMessage = { - role: 'meta', - type: 'turn.step.retrying', - failed_attempt: event.failedAttempt, - next_attempt: event.nextAttempt, - max_attempts: event.maxAttempts, - delay_ms: event.delayMs, - error_name: event.errorName, - error_message: event.errorMessage, - status_code: event.statusCode, - }; - this.writeJsonLine(message); - } - finish(): void { this.flushAssistant(); } @@ -843,9 +830,7 @@ class PromptJsonWriter implements PromptTurnWriter { return toolCall; } - private writeJsonLine( - message: PromptJsonAssistantMessage | PromptJsonToolMessage | PromptJsonRetryMetaMessage, - ): void { + private writeJsonLine(message: PromptJsonAssistantMessage | PromptJsonToolMessage | PromptJsonMetaMessage): void { this.stdout.write(`${JSON.stringify(message)}\n`); } } @@ -946,7 +931,7 @@ function hasTurnId(event: Event): event is Event & { readonly turnId: number } { function formatTurnEndedFailure(event: Extract): string { if (event.error !== undefined) return `${event.error.code}: ${event.error.message}`; if (event.reason === 'filtered') { - return 'Provider safety policy blocked the response.'; + return t('tui.statusMessages.policyBlocked'); } - return `Prompt turn ended with reason: ${event.reason}`; + return t('tui.statusMessages.promptTurnEnded', { reason: event.reason }); } diff --git a/apps/kimi-code/src/cli/run-shell.ts b/apps/kimi-code/src/cli/run-shell.ts index caeae907ca..d326e68178 100644 --- a/apps/kimi-code/src/cli/run-shell.ts +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -17,6 +17,7 @@ import { } from '@moonshot-ai/kimi-telemetry'; import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from '#/constant/app'; +import { t } from '#/i18n'; import { detectPendingMigration } from '#/migration/index'; import type { TuiConfig } from '#/tui/config'; import { loadTuiConfig, TuiConfigParseError } from '#/tui/config'; @@ -91,7 +92,7 @@ export async function runShell( ignoreMarker: runOptions.migrateOnly, }); if (runOptions.migrateOnly === true && migrationPlan === null) { - process.stdout.write(' Nothing to migrate from ~/.kimi/.\n'); + process.stdout.write(t('tui.statusMessages.shellNothingToMigrate') + '\n'); await harness.close(); return; } @@ -195,10 +196,10 @@ export async function runShell( trackLifecycle('exit', { duration_ms: Date.now() - startedAt }); await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }); const gutter = ' '.repeat(CHROME_GUTTER); - process.stdout.write(`${gutter}Bye!\n`); + process.stdout.write(`${gutter}${t('tui.statusMessages.shellBye')}\n`); const hints: string[] = []; if (sessionId !== '' && hasContent) { - hints.push(`${gutter}To resume this session: kimi -r ${sessionId}`); + hints.push(`${gutter}${t('tui.statusMessages.shellResumeHint', { sessionId })}`); } if (tui.exitOpenUrl !== undefined) { hints.push(`${gutter}open ${toTerminalHyperlink(tui.exitOpenUrl, tui.exitOpenUrl)}`); diff --git a/apps/kimi-code/src/cli/sub/doctor.ts b/apps/kimi-code/src/cli/sub/doctor.ts index 0ccc38d281..cbf10f0678 100644 --- a/apps/kimi-code/src/cli/sub/doctor.ts +++ b/apps/kimi-code/src/cli/sub/doctor.ts @@ -10,6 +10,7 @@ import { import type { Command } from 'commander'; import { z } from 'zod'; +import { t } from '#/i18n'; import { getTuiConfigPath, parseTuiConfig } from '#/tui/config'; interface WritableLike { @@ -81,23 +82,23 @@ export async function handleDoctor(deps: DoctorDeps, options: DoctorOptions): Pr export function registerDoctorCommand(parent: Command, deps?: Partial): void { const doctor = parent .command('doctor') - .description('Validate Kimi Code configuration files.') + .description(t('cli.commandDescriptions.doctor')) .action(async () => { await runDoctorCommand(deps, {}); }); doctor .command('config') - .description('Validate config.toml.') - .argument('[path]', 'Validate this file as config.toml instead of the default path.') + .description(t('cli.commandDescriptions.doctorConfig')) + .argument('[path]', t('cli.optionDescriptions.doctorConfigPath')) .action(async (path: string | undefined) => { await runDoctorCommand(deps, { target: 'config', path }); }); doctor .command('tui') - .description('Validate tui.toml.') - .argument('[path]', 'Validate this file as tui.toml instead of the default path.') + .description(t('cli.commandDescriptions.doctorTui')) + .argument('[path]', t('cli.optionDescriptions.doctorTuiPath')) .action(async (path: string | undefined) => { await runDoctorCommand(deps, { target: 'tui', path }); }); @@ -197,8 +198,8 @@ async function checkTomlFile(deps: ResolvedDoctorDeps, spec: CheckSpec): Promise path: spec.path, status: spec.explicit ? 'ERROR' : 'SKIP', message: spec.explicit - ? 'File does not exist.' - : 'File does not exist; built-in defaults will apply.', + ? t('tui.statusMessages.doctorFileNotExist') + : t('tui.statusMessages.doctorFileNotExistDefaults'), }; } @@ -238,18 +239,18 @@ function resolveInputPath(input: string, cwd: string): string { function formatSuccess(results: readonly CheckResult[]): string { return [ - 'Kimi doctor', + t('tui.statusMessages.doctorTitle'), '', ...formatResults(results), '', - 'All checked config files are valid.', + t('tui.statusMessages.doctorAllValid'), '', ].join('\n'); } function formatFailure(results: readonly CheckResult[], issueCount: number): string { return [ - `Kimi doctor found ${String(issueCount)} ${issueCount === 1 ? 'issue' : 'issues'}.`, + t('tui.statusMessages.doctorFoundIssues', { count: String(issueCount), plural: issueCount === 1 ? '' : 's' }), '', ...formatResults(results), '', @@ -273,8 +274,8 @@ function formatErrorMessage(error: unknown, filePath: string): string { const validationIssues = findValidationIssues(error); if (validationIssues !== undefined) { return [ - `Invalid configuration in ${filePath}.`, - 'Validation issues:', + t('tui.statusMessages.doctorInvalidConfig', { path: filePath }), + t('tui.statusMessages.doctorValidationIssues'), ...validationIssues.map((issue) => ` ${formatIssuePath(issue.path)}: ${issue.message}`), ].join('\n'); } @@ -282,8 +283,8 @@ function formatErrorMessage(error: unknown, filePath: string): string { const zodError = findZodError(error); if (zodError !== undefined) { return [ - `Invalid configuration in ${filePath}.`, - 'Validation issues:', + t('tui.statusMessages.doctorInvalidConfig', { path: filePath }), + t('tui.statusMessages.doctorValidationIssues'), ...zodError.issues.map((issue) => ` ${formatIssuePath(issue.path)}: ${issue.message}`), ].join('\n'); } diff --git a/apps/kimi-code/src/cli/sub/export.ts b/apps/kimi-code/src/cli/sub/export.ts index a93c3e2719..a23979aeaf 100644 --- a/apps/kimi-code/src/cli/sub/export.ts +++ b/apps/kimi-code/src/cli/sub/export.ts @@ -25,6 +25,7 @@ import { import type { Command } from 'commander'; import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from '#/constant/app'; +import { t } from '#/i18n'; import { createCliTelemetryBootstrap, initializeCliTelemetry } from '#/cli/telemetry'; import { detectInstallSource } from '#/cli/update/source'; import { createKimiCodeHostIdentity } from '#/cli/version'; @@ -73,14 +74,14 @@ export async function handleExport( resolvedId = requestedId; } else { if (previousSummary === undefined) { - deps.stderr.write('No previous session found to export.\n'); + deps.stderr.write(t('tui.statusMessages.exportNoSession') + '\n'); deps.exit(1); } resolvedId = previousSummary.id; if (!opts.yes) { const confirmed = await deps.confirmPreviousSession(toPreviousSessionSummary(previousSummary)); if (!confirmed) { - deps.stdout.write('Export cancelled.\n'); + deps.stdout.write(t('tui.statusMessages.exportCancelled') + '\n'); return; } } @@ -107,14 +108,14 @@ export async function handleExport( export function registerExportCommand(parent: Command, deps?: Partial): void { parent .command('export') - .description('Export a session as a ZIP archive.') - .option('-o, --output ', 'Output ZIP path.') - .option('-y, --yes', 'Skip previous-session confirmation.') + .description(t('cli.commandDescriptions.exportCmd')) + .option('-o, --output ', t('cli.optionDescriptions.exportOutput')) + .option('-y, --yes', t('cli.optionDescriptions.exportYes')) .option( '--no-include-global-log', - 'Skip bundling the active global diagnostic log (~/.kimi-code/logs/kimi-code.log, not rotated .1 files). By default the global log is included.', + t('cli.optionDescriptions.exportNoIncludeGlobalLog'), ) - .argument('[sessionId]', 'Session id to export. Defaults to the most recent session.') + .argument('[sessionId]', t('cli.optionDescriptions.exportSessionId')) .action( async ( sessionId: string | undefined, @@ -225,7 +226,7 @@ async function confirmPreviousSession(summary: PreviousSessionSummary): Promise< const rl = createInterface({ input: process.stdin, output: process.stderr }); try { const title = summary.title === undefined ? summary.sessionId : `${summary.title} (${summary.sessionId})`; - const answer = await rl.question(`Export previous session "${title}"? [Y/n] `); + const answer = await rl.question(t('tui.statusMessages.exportConfirmPrompt', { title })); const trimmed = answer.trim().toLowerCase(); return trimmed === '' || trimmed === 'y' || trimmed === 'yes'; } finally { diff --git a/apps/kimi-code/src/cli/sub/login-flow.ts b/apps/kimi-code/src/cli/sub/login-flow.ts index 005dc17295..ab98e6cf2e 100644 --- a/apps/kimi-code/src/cli/sub/login-flow.ts +++ b/apps/kimi-code/src/cli/sub/login-flow.ts @@ -8,6 +8,7 @@ import { createKimiHarness } from '@moonshot-ai/kimi-code-sdk'; import { createKimiCodeHostIdentity } from '#/cli/version'; +import { t } from '#/i18n'; import { openUrl } from '#/utils/open-url'; export async function runLoginFlow(): Promise { @@ -31,12 +32,12 @@ export async function runLoginFlow(): Promise { process.stderr.write( [ '', - `Opening browser for Kimi device login: ${url}`, - `If the browser did not open, paste the URL above and enter code: ${data.userCode}`, + t('tui.statusMessages.loginOpeningBrowser', { url }), + t('tui.statusMessages.loginPasteUrl', { code: data.userCode }), data.expiresIn !== null && data.expiresIn !== undefined - ? `Code expires in ${data.expiresIn}s.` + ? t('tui.statusMessages.loginCodeExpires', { seconds: data.expiresIn }) : undefined, - 'Waiting for authorization to complete...', + t('tui.statusMessages.loginWaiting'), '', ] .filter((line): line is string => line !== undefined) @@ -49,14 +50,14 @@ export async function runLoginFlow(): Promise { } }, }); - process.stderr.write(`Logged in to ${result.providerName}.\n`); + process.stderr.write(t('tui.statusMessages.loginSuccess', { provider: result.providerName }) + '\n'); process.exit(0); } catch (error) { if (controller.signal.aborted) { - process.stderr.write('Login cancelled.\n'); + process.stderr.write(t('tui.statusMessages.loginCancelled') + '\n'); } else { const message = error instanceof Error ? error.message : String(error); - process.stderr.write(`Login failed: ${message}\n`); + process.stderr.write(t('tui.statusMessages.loginFailedMsg', { message }) + '\n'); } process.exit(1); } diff --git a/apps/kimi-code/src/cli/sub/provider.ts b/apps/kimi-code/src/cli/sub/provider.ts index 8ed4546aa6..a0cb5461d7 100644 --- a/apps/kimi-code/src/cli/sub/provider.ts +++ b/apps/kimi-code/src/cli/sub/provider.ts @@ -36,6 +36,7 @@ import { import type { Command } from 'commander'; import { createKimiCodeHostIdentity } from '#/cli/version'; +import { t } from '#/i18n'; interface WritableLike { write(chunk: string): boolean; @@ -77,14 +78,14 @@ export async function handleProviderAdd( const apiKey = resolveApiKey(opts.apiKey, deps.env); if (apiKey === undefined) { deps.stderr.write( - 'Missing API key. Pass --api-key or set KIMI_REGISTRY_API_KEY.\n', + t('tui.statusMessages.providerMissingApiKey') + '\n', ); deps.exit(1); } const trimmedUrl = url.trim(); if (trimmedUrl.length === 0) { - deps.stderr.write('Registry URL is required.\n'); + deps.stderr.write(t('tui.statusMessages.providerUrlRequired') + '\n'); deps.exit(1); } @@ -102,13 +103,13 @@ export async function handleProviderAdd( entries = await fetchCustomRegistry(source); } catch (error) { const suffix = error instanceof CustomRegistryApiError ? ` (HTTP ${String(error.status)})` : ''; - deps.stderr.write(`Failed to fetch registry${suffix}: ${errorMessage(error)}\n`); + deps.stderr.write(t('tui.statusMessages.providerFetchFailed', { suffix, error: errorMessage(error) }) + '\n'); deps.exit(1); } const entryList = Object.values(entries); if (entryList.length === 0) { - deps.stderr.write(`Registry at ${trimmedUrl} contained no usable providers.\n`); + deps.stderr.write(t('tui.statusMessages.providerNoUsable', { url: trimmedUrl }) + '\n'); deps.exit(1); } @@ -155,11 +156,11 @@ export async function handleProviderRemove( await harness.ensureConfigFile(); const config = await harness.getConfig(); if (config.providers[providerId] === undefined) { - deps.stderr.write(`Provider "${providerId}" not found.\n`); + deps.stderr.write(t('tui.statusMessages.providerNotFound', { id: providerId }) + '\n'); deps.exit(1); } await harness.removeProvider(providerId); - deps.stdout.write(`Removed provider "${providerId}".\n`); + deps.stdout.write(t('tui.statusMessages.providerRemoved', { id: providerId }) + '\n'); } export async function handleProviderList( @@ -186,7 +187,7 @@ export async function handleProviderList( const providerIds = Object.keys(config.providers).toSorted(); if (providerIds.length === 0) { - deps.stdout.write('No providers configured.\n'); + deps.stdout.write(t('tui.statusMessages.providerNoneConfigured') + '\n'); return; } @@ -199,7 +200,7 @@ export async function handleProviderList( ); } if (config.defaultModel !== undefined) { - deps.stdout.write(`\nDefault model: ${config.defaultModel}\n`); + deps.stdout.write('\n' + t('tui.statusMessages.providerDefaultModel', { model: config.defaultModel }) + '\n'); } } @@ -219,7 +220,7 @@ export async function handleCatalogList( if (providerId !== undefined) { const entry = catalog[providerId]; if (entry === undefined) { - deps.stderr.write(`Provider "${providerId}" not found in catalog at ${url}.\n`); + deps.stderr.write(t('tui.statusMessages.providerCatalogNotFound', { id: providerId, url }) + '\n'); deps.exit(1); } const models = catalogProviderModels(entry); @@ -230,7 +231,7 @@ export async function handleCatalogList( return; } if (models.length === 0) { - deps.stdout.write(`Provider "${providerId}" lists no usable models in this catalog.\n`); + deps.stdout.write(t('tui.statusMessages.providerCatalogNoModels', { id: providerId }) + '\n'); return; } deps.stdout.write(`${entry.name ?? providerId} (${providerId})\n`); @@ -267,9 +268,9 @@ export async function handleCatalogList( if (entries.length === 0) { if (filter !== undefined) { - deps.stdout.write(`No providers in catalog match "${filter}".\n`); + deps.stdout.write(t('tui.statusMessages.providerCatalogNoMatch', { filter }) + '\n'); } else { - deps.stdout.write('Catalog is empty.\n'); + deps.stdout.write(t('tui.statusMessages.providerCatalogEmpty') + '\n'); } return; } @@ -296,7 +297,7 @@ export async function handleCatalogAdd( const apiKey = resolveApiKey(opts.apiKey, deps.env); if (apiKey === undefined) { deps.stderr.write( - 'Missing API key. Pass --api-key or set KIMI_REGISTRY_API_KEY.\n', + t('tui.statusMessages.providerMissingApiKey') + '\n', ); deps.exit(1); } @@ -306,25 +307,25 @@ export async function handleCatalogAdd( const entry = catalog[providerId]; if (entry === undefined) { - deps.stderr.write(`Provider "${providerId}" not found in catalog at ${url}.\n`); + deps.stderr.write(t('tui.statusMessages.providerCatalogNotFound', { id: providerId, url }) + '\n'); deps.exit(1); } const wire = inferWireType(entry); if (wire === undefined) { - deps.stderr.write(`Provider "${providerId}" has an unsupported wire type in the catalog.\n`); + deps.stderr.write(t('tui.statusMessages.providerCatalogUnsupported', { id: providerId }) + '\n'); deps.exit(1); } const models = catalogProviderModels(entry); if (models.length === 0) { - deps.stderr.write(`Provider "${providerId}" lists no usable models in this catalog.\n`); + deps.stderr.write(t('tui.statusMessages.providerCatalogNoModels', { id: providerId }) + '\n'); deps.exit(1); } if (opts.defaultModel !== undefined && !models.some((m) => m.id === opts.defaultModel)) { deps.stderr.write( - `Model "${opts.defaultModel}" is not in provider "${providerId}". Run "kimi provider catalog list ${providerId}" to see available ids.\n`, + t('tui.statusMessages.providerCatalogModelNotInProvider', { model: opts.defaultModel, id: providerId }) + '\n', ); deps.exit(1); } @@ -389,10 +390,10 @@ export async function handleCatalogAdd( const displayName = entry.name ?? providerId; deps.stdout.write( - `Imported ${displayName} (${providerId}) with ${String(models.length)} model${models.length === 1 ? '' : 's'} from ${url}.\n`, + t('tui.statusMessages.providerImported', { name: displayName, id: providerId, count: String(models.length) }) + '\n', ); if (opts.defaultModel !== undefined) { - deps.stdout.write(`Default model set to ${providerId}/${opts.defaultModel}.\n`); + deps.stdout.write(t('tui.statusMessages.providerDefaultSet', { id: providerId, model: opts.defaultModel }) + '\n'); } } @@ -401,7 +402,7 @@ async function loadCatalogOrExit(deps: ProviderDeps, url: string): Promise): void { const provider = parent .command('provider') - .description('Manage LLM providers non-interactively.'); + .description(t('cli.commandDescriptions.provider')); // Last-resort boundary: handlers report expected failures themselves, but // anything that escapes (e.g. a config write rejected because config.toml @@ -426,8 +427,8 @@ export function registerProviderCommand(parent: Command, deps?: Partial') - .description('Import every provider listed in a custom registry (api.json).') - .option('--api-key ', 'Registry API key. Falls back to KIMI_REGISTRY_API_KEY.') + .description(t('cli.commandDescriptions.providerAdd')) + .option('--api-key ', t('cli.optionDescriptions.providerApiKey')) .action(async (url: string, options: { apiKey?: string }) => { const resolved = resolveDeps(deps); await runAction(resolved, () => handleProviderAdd(resolved, url, { apiKey: options.apiKey })); @@ -435,7 +436,7 @@ export function registerProviderCommand(parent: Command, deps?: Partial') - .description('Remove a provider and every model alias that referenced it.') + .description(t('cli.commandDescriptions.providerRemove')) .action(async (providerId: string) => { const resolved = resolveDeps(deps); await runAction(resolved, () => handleProviderRemove(resolved, providerId)); @@ -443,8 +444,8 @@ export function registerProviderCommand(parent: Command, deps?: Partial { const resolved = resolveDeps(deps); await runAction(resolved, () => handleProviderList(resolved, { json: options.json === true })); @@ -452,14 +453,14 @@ export function registerProviderCommand(parent: Command, deps?: Partial', 'Case-insensitive id/name substring filter.') - .option('--url ', `Override catalog URL. Defaults to ${DEFAULT_CATALOG_URL}.`) - .option('--json', 'Emit the matching catalog slice as JSON.', false) + .description(t('cli.commandDescriptions.providerCatalogList')) + .option('--filter ', t('cli.optionDescriptions.providerCatalogFilter')) + .option('--url ', t('cli.optionDescriptions.providerCatalogUrl', { url: DEFAULT_CATALOG_URL })) + .option('--json', t('cli.optionDescriptions.providerCatalogJson'), false) .action( async ( providerId: string | undefined, @@ -478,10 +479,10 @@ export function registerProviderCommand(parent: Command, deps?: Partial') - .description('Import a known provider from the catalog by id.') - .option('--api-key ', 'API key for the provider. Falls back to KIMI_REGISTRY_API_KEY.') - .option('--default-model ', 'Mark the imported model as default_model after import.') - .option('--url ', `Override catalog URL. Defaults to ${DEFAULT_CATALOG_URL}.`) + .description(t('cli.commandDescriptions.providerCatalogAdd')) + .option('--api-key ', t('cli.optionDescriptions.providerCatalogApiKey')) + .option('--default-model ', t('cli.optionDescriptions.providerCatalogDefaultModel')) + .option('--url ', t('cli.optionDescriptions.providerCatalogUrl', { url: DEFAULT_CATALOG_URL })) .action( async ( providerId: string, diff --git a/apps/kimi-code/src/cli/sub/server/access-urls.ts b/apps/kimi-code/src/cli/sub/server/access-urls.ts index 0c6233fe8d..bec806aa29 100644 --- a/apps/kimi-code/src/cli/sub/server/access-urls.ts +++ b/apps/kimi-code/src/cli/sub/server/access-urls.ts @@ -7,6 +7,7 @@ * user open the link on another device and be authenticated automatically. */ +import { t } from '#/i18n'; import { formatHostForUrl, listNetworkAddresses, type NetworkAddress } from './networks'; /** @@ -67,19 +68,19 @@ export function accessUrlLines( ): AccessUrlLine[] { if (isWildcard(host)) { const lines: AccessUrlLine[] = [ - { label: 'Local: ', url: buildOpenableUrl(`http://localhost:${port}`, token) }, + { label: t('tui.statusMessages.serverAccessLocal'), url: buildOpenableUrl(`http://localhost:${port}`, token) }, ]; const addrs = networkAddresses ?? listNetworkAddresses(); for (const addr of addrs) { lines.push({ - label: 'Network: ', + label: t('tui.statusMessages.serverAccessNetwork'), url: buildOpenableUrl(`http://${formatHostForUrl(addr.address, addr.family)}:${port}`, token), }); } return lines; } if (isLoopbackHost(host)) { - return [{ label: 'Local: ', url: buildOpenableUrl(hostOrigin(host, port), token) }]; + return [{ label: t('tui.statusMessages.serverAccessLocal'), url: buildOpenableUrl(hostOrigin(host, port), token) }]; } - return [{ label: 'URL: ', url: buildOpenableUrl(hostOrigin(host, port), token) }]; + return [{ label: t('tui.statusMessages.serverAccessUrl'), url: buildOpenableUrl(hostOrigin(host, port), token) }]; } diff --git a/apps/kimi-code/src/cli/sub/server/daemon.ts b/apps/kimi-code/src/cli/sub/server/daemon.ts index cc633934c6..5c8b7d68c4 100644 --- a/apps/kimi-code/src/cli/sub/server/daemon.ts +++ b/apps/kimi-code/src/cli/sub/server/daemon.ts @@ -24,6 +24,8 @@ import { dirname, isAbsolute, join, resolve } from 'node:path'; import { DEFAULT_LOCK_DIR, getLiveLock, type LockContents } from '@moonshot-ai/server'; +import { t } from '#/i18n'; + import { DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT, @@ -377,7 +379,7 @@ export async function ensureDaemon(options: EnsureDaemonOptions = {}): Promise { try { await handleKillCommand(DEFAULT_KILL_DEPS); @@ -61,7 +62,7 @@ export function registerKillCommand(server: Command): void { export async function handleKillCommand(deps: KillCommandDeps): Promise { const lock = deps.getLiveLock(); if (!lock) { - deps.stdout.write('No running Kimi server.\n'); + deps.stdout.write(t('tui.statusMessages.serverKillNoRunning') + '\n'); return; } @@ -80,19 +81,19 @@ export async function handleKillCommand(deps: KillCommandDeps): Promise { deps.signalPid(pid, 'SIGTERM'); if (await waitForExit(pid, TERM_GRACE_MS, deps)) { - deps.stdout.write(`Kimi server (pid ${String(pid)}) stopped.\n`); + deps.stdout.write(t('tui.statusMessages.serverKillStopped', { pid: String(pid) }) + '\n'); return; } deps.signalPid(pid, 'SIGKILL'); if (await waitForExit(pid, KILL_GRACE_MS, deps)) { - deps.stdout.write(`Kimi server (pid ${String(pid)}) killed.\n`); + deps.stdout.write(t('tui.statusMessages.serverKillKilled', { pid: String(pid) }) + '\n'); return; } throw new Error( - `Failed to stop Kimi server (pid ${String(pid)}); insufficient permissions?`, + t('tui.statusMessages.serverKillFailedPermissions', { pid: String(pid) }), ); } diff --git a/apps/kimi-code/src/cli/sub/server/lifecycle.ts b/apps/kimi-code/src/cli/sub/server/lifecycle.ts index f7ff072d8e..fc98874924 100644 --- a/apps/kimi-code/src/cli/sub/server/lifecycle.ts +++ b/apps/kimi-code/src/cli/sub/server/lifecycle.ts @@ -20,6 +20,7 @@ import { } from '@moonshot-ai/server'; import { openUrl as defaultOpenUrl } from '#/utils/open-url'; +import { t } from '#/i18n'; import { DEFAULT_LOG_LEVEL, @@ -62,16 +63,16 @@ const DEFAULT_DEPS: LifecycleCommandDeps = { export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps = DEFAULT_DEPS): void { parent .command('install') - .description('Install the Kimi server as an OS-managed service (launchd/systemd/schtasks).') - .option('--port ', `Bind port (default ${DEFAULT_SERVER_PORT})`, String(DEFAULT_SERVER_PORT)) + .description(t('tui.statusMessages.serverInstallDesc')) + .option('--port ', t('tui.statusMessages.serverPortOption', { port: DEFAULT_SERVER_PORT }), String(DEFAULT_SERVER_PORT)) .option( '--log-level ', - `Log level: ${VALID_LOG_LEVELS.join('|')} (default ${DEFAULT_LOG_LEVEL})`, + t('tui.statusMessages.serverLogLevelOption', { levels: VALID_LOG_LEVELS.join('|'), default: DEFAULT_LOG_LEVEL }), DEFAULT_LOG_LEVEL, ) - .option('--force', 'Reinstall and overwrite if already installed', false) - .option('--no-open', 'Do not open the web UI after install.', true) - .option('--json', 'Output JSON', false) + .option('--force', t('tui.statusMessages.serverReinstallOption'), false) + .option('--no-open', t('tui.statusMessages.serverNoOpenOption'), true) + .option('--json', t('tui.statusMessages.serverOutputJson'), false) .action(async (opts: InstallCliOptions) => { await runLifecycle(deps, opts.json === true, async (mgr) => { const args: InstallArgs = { @@ -100,8 +101,8 @@ export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps parent .command('uninstall') - .description('Uninstall the Kimi server service.') - .option('--json', 'Output JSON', false) + .description(t('tui.statusMessages.serverUninstallDesc')) + .option('--json', t('tui.statusMessages.serverOutputJson'), false) .action(async (opts: JsonCliOptions) => { await runLifecycle(deps, opts.json === true, async (mgr) => { const result = await mgr.uninstall(); @@ -111,8 +112,8 @@ export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps parent .command('start') - .description('Start the Kimi server service.') - .option('--json', 'Output JSON', false) + .description(t('tui.statusMessages.serverStartDesc')) + .option('--json', t('tui.statusMessages.serverOutputJson'), false) .action(async (opts: JsonCliOptions) => { await runLifecycle(deps, opts.json === true, async (mgr) => { const result = await mgr.start(); @@ -123,8 +124,8 @@ export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps parent .command('stop') - .description('Stop the Kimi server service.') - .option('--json', 'Output JSON', false) + .description(t('tui.statusMessages.serverStopDesc')) + .option('--json', t('tui.statusMessages.serverOutputJson'), false) .action(async (opts: JsonCliOptions) => { await runLifecycle(deps, opts.json === true, async (mgr) => { const result = await mgr.stop(); @@ -134,8 +135,8 @@ export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps parent .command('restart') - .description('Restart the Kimi server service.') - .option('--json', 'Output JSON', false) + .description(t('tui.statusMessages.serverRestartDesc')) + .option('--json', t('tui.statusMessages.serverOutputJson'), false) .action(async (opts: JsonCliOptions) => { await runLifecycle(deps, opts.json === true, async (mgr) => { const result = await mgr.restart(); @@ -146,8 +147,8 @@ export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps parent .command('status') - .description('Show Kimi server service status and connectivity.') - .option('--json', 'Output JSON', false) + .description(t('tui.statusMessages.serverStatusDesc')) + .option('--json', t('tui.statusMessages.serverOutputJson'), false) .action(async (opts: JsonCliOptions) => { await runLifecycle(deps, opts.json === true, async (mgr) => { const status: ServiceStatus = await mgr.status(); @@ -204,18 +205,18 @@ function formatHuman(result: Record): string { const lines = [`${action}${message}`]; const url = result['url']; - if (typeof url === 'string') lines.push(`URL: ${url}`); + if (typeof url === 'string') lines.push(t('tui.statusMessages.serverStatusUrl', { url })); const running = result['running']; - if (typeof running === 'boolean') lines.push(`Status: ${running ? 'running' : 'not running'}`); + if (typeof running === 'boolean') lines.push(t('tui.statusMessages.serverStatusState', { state: running ? t('tui.statusMessages.serverStatusRunning') : t('tui.statusMessages.serverStatusNotRunning') })); const logPath = result['logPath']; - if (typeof logPath === 'string') lines.push(`Log: ${logPath}`); + if (typeof logPath === 'string') lines.push(t('tui.statusMessages.serverStatusLog', { path: logPath })); const notes = result['notes']; if (Array.isArray(notes)) { for (const note of notes) { - if (typeof note === 'string' && note.length > 0) lines.push(`Note: ${note}`); + if (typeof note === 'string' && note.length > 0) lines.push(t('tui.statusMessages.serverStatusNote', { note })); } } diff --git a/apps/kimi-code/src/cli/sub/server/ps.ts b/apps/kimi-code/src/cli/sub/server/ps.ts index 6e17d71c05..4c681efb3f 100644 --- a/apps/kimi-code/src/cli/sub/server/ps.ts +++ b/apps/kimi-code/src/cli/sub/server/ps.ts @@ -9,6 +9,8 @@ import chalk from 'chalk'; import type { Command } from 'commander'; +import { t } from '#/i18n'; + import { getLiveLock } from '@moonshot-ai/server'; import { getDataDir } from '#/utils/paths'; @@ -39,8 +41,8 @@ const USER_AGENT_MAX_WIDTH = 40; export function registerPsCommand(server: Command): void { server .command('ps') - .description('List clients currently connected to the running Kimi server.') - .option('--json', 'Print the raw connection list as JSON.') + .description(t('cli.commandDescriptions.serverPs')) + .option('--json', t('cli.optionDescriptions.serverPsJson')) .action(async (opts: { json?: boolean }) => { try { await handlePsCommand(opts); @@ -61,7 +63,7 @@ async function handlePsCommand(opts: { json?: boolean }): Promise { const origin = serverOrigin(lockConnectHost(lock), lock.port); if (!(await isServerHealthy(origin, HEALTH_TIMEOUT_MS))) { - throw new Error(`Kimi server at ${origin} is not responding.`); + throw new Error(t('tui.statusMessages.serverPsNotResponding', { origin })); } // The `/api/v1/connections` route is gated by bearer auth (M5.1). Read the @@ -88,16 +90,16 @@ async function fetchConnections(origin: string, token: string): Promise { try { const token = await rotateServerToken(getDataDir()); process.stdout.write( - 'The previous token is now invalid. A running server picks up the new token automatically.\n', + t('tui.statusMessages.serverTokenRotated') + '\n', ); // Token in the middle: indented and set off by blank lines (no color // highlight), so it is easy to spot without dominating the output. - process.stdout.write(`\n ${chalk.bold('New server token:')} ${token}\n\n`); + process.stdout.write('\n ' + chalk.bold(t('tui.statusMessages.serverNewToken', { token })) + '\n\n'); // Re-print the access links with the new token so the user can // reconnect immediately. When a server is running its bind host/port diff --git a/apps/kimi-code/src/cli/sub/server/run.ts b/apps/kimi-code/src/cli/sub/server/run.ts index 19984a4f51..70488623c8 100644 --- a/apps/kimi-code/src/cli/sub/server/run.ts +++ b/apps/kimi-code/src/cli/sub/server/run.ts @@ -19,6 +19,7 @@ import chalk from 'chalk'; import { Option, type Command } from 'commander'; import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app'; +import { t } from '#/i18n'; import { getNativeWebAssetsDir } from '#/native/web-assets'; import { darkColors } from '#/tui/theme/colors'; import { openUrl as defaultOpenUrl } from '#/utils/open-url'; @@ -108,61 +109,61 @@ export function buildRunCommand(cmd: Command, options: { defaultOpen: boolean }) return cmd .option( '--port ', - `Bind port (default ${DEFAULT_SERVER_PORT})`, + t('cli.optionDescriptions.serverRunOptionPort'), String(DEFAULT_SERVER_PORT), ) .option( '--host [host]', - `Bind host. Omit to bind ${DEFAULT_SERVER_HOST} (this machine only); pass --host to bind ${DEFAULT_LAN_HOST} (all interfaces), or --host for a specific host. The bearer token is printed at startup.`, + t('cli.optionDescriptions.serverRunOptionHost'), ) .option( '--allowed-host ', - 'Extra Host header value to allow through the DNS-rebinding check. Repeat or comma-separate; a leading dot matches a domain suffix (e.g. .example.com).', + t('cli.optionDescriptions.serverRunOptionAllowedHost'), ) .option( '--keep-alive', - 'Keep the server running instead of exiting after 60s with no connected clients. Implied automatically by --host / --allowed-host, and always on in --foreground mode.', + t('cli.optionDescriptions.serverRunOptionKeepAlive'), false, ) .option( '--insecure-no-tls', - 'Allow a non-loopback bind without a TLS-terminating reverse proxy. Defaults to true; only relevant for non-loopback binds.', + t('cli.optionDescriptions.serverRunOptionInsecureNoTls'), true, ) .option( '--allow-remote-shutdown', - 'On a non-loopback bind, keep POST /api/v1/shutdown enabled (default: route is disabled → 404).', + t('cli.optionDescriptions.serverRunOptionAllowRemoteShutdown'), false, ) .option( '--allow-remote-terminals', - 'On a non-loopback bind, keep the PTY /api/v1/terminals/* routes enabled (default: disabled → 404). Remote shell is high risk.', + t('cli.optionDescriptions.serverRunOptionAllowRemoteTerminals'), false, ) .option( '--dangerous-bypass-auth', - 'Disable bearer-token auth on every REST and WebSocket route, and advertise it via /api/v1/meta so the web UI connects without a token. Only use on a trusted network or behind your own authenticating proxy.', + t('cli.optionDescriptions.serverRunOptionDangerousBypassAuth'), false, ) .option( '--log-level ', - `Server log level: ${VALID_LOG_LEVELS.join('|')}. Omit to keep logs off.`, + t('cli.optionDescriptions.serverRunOptionLogLevel'), ) .option( '--debug-endpoints', - 'Mount /api/v1/debug/* routes for test introspection. OFF by default; production callers leave this unset.', + t('cli.optionDescriptions.serverRunOptionDebugEndpoints'), false, ) .option( '--foreground', - 'Run the server in the foreground and keep this terminal attached until SIGINT/SIGTERM (do not daemonize).', + t('cli.optionDescriptions.serverRunOptionForeground'), false, ) .option( options.defaultOpen ? '--no-open' : '--open', options.defaultOpen - ? 'Do not open the web UI in the default browser.' - : 'Open the web UI in the default browser once the server is healthy.', + ? t('cli.optionDescriptions.serverRunOptionNoOpen') + : t('cli.optionDescriptions.serverRunOptionOpen'), options.defaultOpen, ) .addOption( @@ -263,7 +264,7 @@ function formatReadyLine( const notice = dangerousBypassAuth ? `${formatDangerNoticeLines().join('\n')}\n` : ''; - return `${notice}Kimi server: ${buildOpenableUrl(origin, token)}\n`; + return `${notice}${t('tui.statusMessages.serverUrl', { url: buildOpenableUrl(origin, token) })}\n`; } /** @@ -275,9 +276,9 @@ function formatDangerNoticeLines(): string[] { const danger = (text: string): string => chalk.hex(darkColors.error)(text); const dangerBold = (text: string): string => chalk.bold.hex(darkColors.error)(text); return [ - ` ${dangerBold('⚠ DANGER: authentication is DISABLED (--dangerous-bypass-auth).')}`, - ` ${danger('Anyone who can reach this port gets full access. Only continue if you understand the risk.')}`, - ` ${danger(`If you are unsure, run `)}${dangerBold('kimi server kill')}${danger(' now to stop this process.')}`, + ` ${dangerBold(t('tui.statusMessages.serverDangerAuthDisabled'))}`, + ` ${danger(t('tui.statusMessages.serverDangerAnyoneAccess'))}`, + ` ${danger(t('tui.statusMessages.serverDangerUnsure'))}${dangerBold(t('tui.statusMessages.serverDangerUnsureCmd'))}${danger(t('tui.statusMessages.serverDangerUnsureSuffix'))}`, ]; } @@ -505,8 +506,8 @@ function formatReadyBanner( const logo = ['▐█▛█▛█▌', '▐█████▌'] as const; const lines: string[] = [ '', - ` ${primary(logo[0])} ${title('Kimi server ready')} ${dim(getVersion())}`, - ` ${primary(logo[1])} ${dim('Local web UI is available from this machine.')}`, + ` ${primary(logo[0])} ${title(t('tui.statusMessages.serverReadyBanner'))} ${dim(getVersion())}`, + ` ${primary(logo[1])} ${dim(t('tui.statusMessages.serverReadyLocalUi'))}`, '', ]; diff --git a/apps/kimi-code/src/cli/sub/server/shared.ts b/apps/kimi-code/src/cli/sub/server/shared.ts index aa2b686ce2..b23dfce49b 100644 --- a/apps/kimi-code/src/cli/sub/server/shared.ts +++ b/apps/kimi-code/src/cli/sub/server/shared.ts @@ -10,6 +10,8 @@ import { join } from 'node:path'; import type { ServerLogLevel } from '@moonshot-ai/server'; +import { t } from '#/i18n'; + export const LOCAL_SERVER_HOST = '127.0.0.1'; export const DEFAULT_LAN_HOST = '0.0.0.0'; export const DEFAULT_SERVER_HOST = LOCAL_SERVER_HOST; @@ -133,7 +135,7 @@ function parseIdleGraceMs(raw: string | undefined): number { if (raw === undefined) return DEFAULT_IDLE_GRACE_MS; const n = Number.parseInt(raw, 10); if (!Number.isFinite(n) || n < 0) { - throw new Error(`error: invalid --idle-grace-ms value: ${raw}`); + throw new Error(t('tui.statusMessages.serverInvalidIdleGrace', { raw })); } return n; } @@ -142,7 +144,7 @@ export function parsePort(raw: string | undefined, label: string, fallback: numb if (raw === undefined) return fallback; const n = Number.parseInt(raw, 10); if (!Number.isFinite(n) || n < 0 || n > 65535) { - throw new Error(`error: invalid ${label} value: ${raw}`); + throw new Error(t('tui.statusMessages.serverInvalidValue', { label, raw })); } return n; } @@ -232,7 +234,7 @@ export async function ensureServerWebReady(origin: string): Promise { } catch (error) { const reason = error instanceof Error ? ` (${error.message})` : ''; throw new Error( - `Server at ${origin} does not serve the Kimi web UI${reason}. Stop the existing server and rerun \`kimi server run\`.`, + t('tui.statusMessages.serverNotServingWebUi', { origin, reason }), { cause: error }, ); } finally { diff --git a/apps/kimi-code/src/cli/sub/upgrade.ts b/apps/kimi-code/src/cli/sub/upgrade.ts index c547106457..3998a603a5 100644 --- a/apps/kimi-code/src/cli/sub/upgrade.ts +++ b/apps/kimi-code/src/cli/sub/upgrade.ts @@ -1,6 +1,7 @@ import { log, type Logger } from '@moonshot-ai/kimi-code-sdk'; import { track as trackTelemetry, type TelemetryProperties } from '@moonshot-ai/kimi-telemetry'; +import { t } from '#/i18n'; import { refreshUpdateCache } from '#/cli/update/refresh'; import { selectUpdateTarget } from '#/cli/update/select'; import { detectInstallSource } from '#/cli/update/source'; @@ -68,7 +69,7 @@ export async function handleUpgrade( currentVersion, error, }); - deps.stderr.write(`error: failed to check for updates: ${reason}\n`); + deps.stderr.write(t('tui.statusMessages.upgradeCheckFailed', { reason }) + '\n'); return 1; } @@ -80,7 +81,7 @@ export async function handleUpgrade( logUpgradeInfo(deps.logger, 'manual upgrade no update', { currentVersion, }); - deps.stdout.write(`Kimi Code is already up to date (${formatDisplayVersion(currentVersion)}).\n`); + deps.stdout.write(t('tui.statusMessages.upgradeAlreadyUpToDate', { version: formatDisplayVersion(currentVersion) }) + '\n'); return 0; } diff --git a/apps/kimi-code/src/cli/sub/vis.ts b/apps/kimi-code/src/cli/sub/vis.ts index 7e6f2e1f25..20b30da2ac 100644 --- a/apps/kimi-code/src/cli/sub/vis.ts +++ b/apps/kimi-code/src/cli/sub/vis.ts @@ -11,6 +11,7 @@ import type { Command } from 'commander'; import { createCliTelemetryBootstrap } from '#/cli/telemetry'; +import { t } from '#/i18n'; import { openUrl } from '#/utils/open-url'; interface WritableLike { @@ -76,7 +77,7 @@ export async function handleVis(deps: VisDeps, opts: VisOptions): Promise }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); - deps.stderr.write(`Failed to start kimi vis: ${msg}\n`); + deps.stderr.write(t('tui.statusMessages.visStartFailed', { message: msg }) + '\n'); return deps.exit(1); } @@ -85,14 +86,14 @@ export async function handleVis(deps: VisDeps, opts: VisOptions): Promise ? server.url : `${server.url}sessions/${encodeURIComponent(opts.sessionId)}`; - deps.stdout.write(`kimi vis is running at ${server.url}\n`); - deps.stdout.write('Press Ctrl-C to stop.\n'); + deps.stdout.write(t('tui.statusMessages.visRunning', { url: server.url }) + '\n'); + deps.stdout.write(t('tui.statusMessages.visStopHint') + '\n'); if (opts.open) { try { await deps.openUrl(target); } catch { - deps.stderr.write(`Could not open a browser; visit ${target} manually.\n`); + deps.stderr.write(t('tui.statusMessages.visBrowserFailed', { url: target }) + '\n'); } } @@ -103,11 +104,11 @@ export async function handleVis(deps: VisDeps, opts: VisOptions): Promise export function registerVisCommand(parent: Command, overrides?: Partial): void { parent .command('vis') - .description('Launch the session visualizer in your browser.') - .option('--port ', 'Port to bind. Default: auto-pick a free port.') - .option('--host ', 'Host to bind. Default: 127.0.0.1.') - .option('--no-open', 'Do not open the browser automatically.') - .argument('[sessionId]', 'Open directly to this session.') + .description(t('cli.commandDescriptions.vis')) + .option('--port ', t('cli.optionDescriptions.visPort')) + .option('--host ', t('cli.optionDescriptions.visHost')) + .option('--no-open', t('cli.optionDescriptions.visNoOpen')) + .argument('[sessionId]', t('cli.optionDescriptions.visSessionId')) .action( async ( sessionId: string | undefined, diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index 5fda96d6ae..218ff5c125 100644 --- a/apps/kimi-code/src/cli/update/preflight.ts +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -7,6 +7,7 @@ import { NATIVE_INSTALL_COMMAND_UNIX, NATIVE_INSTALL_COMMAND_WIN, } from '#/constant/app'; +import { t } from '#/i18n'; import { loadTuiConfig } from '#/tui/config'; import { readUpdateCache } from './cache'; @@ -159,27 +160,26 @@ export function renderManualUpdateMessage( sourceDesc = 'homebrew'; break; case 'native': - sourceDesc = 'native (windows). Auto-update is not supported on this platform.'; + sourceDesc = t('tui.statusMessages.updateUnsupportedPlatform', { platform: 'windows' }); break; case 'unsupported': - sourceDesc = 'unsupported package manager or layout.'; + sourceDesc = t('tui.statusMessages.updateUnsupportedManager'); break; } return ( - `A newer version of ${NPM_PACKAGE_NAME} is available ` + - `(${currentVersion} -> ${target.version}).\n` + - `Detected install source: ${sourceDesc}\n` + - `To update manually, run: ${installCommand}\n` + t('tui.statusMessages.updateNewerAvailable', { name: NPM_PACKAGE_NAME, version: target.version }) + '\n' + + t('tui.statusMessages.updateDetectedSource', { source: sourceDesc }) + '\n' + + t('tui.statusMessages.updateManualCommand', { command: installCommand }) + '\n' ); } export function renderInstallSuccessMessage(target: UpdateTarget): string { - return `Updated ${NPM_PACKAGE_NAME} to ${target.version}. Restart the CLI to use the new version.\n`; + return t('tui.statusMessages.updateUpdated', { name: NPM_PACKAGE_NAME, version: target.version }) + '\n'; } function renderBackgroundInstallSuccessNotice(version: string): string { const displayVersion = version.startsWith('v') ? version : `v${version}`; - return `Kimi Code updated to ${displayVersion}\nChangelog: ${CHANGELOG_URL}\n`; + return t('tui.statusMessages.updateUpdatedWithChangelog', { version: displayVersion, url: CHANGELOG_URL }) + '\n'; } function refreshInBackground(): void { diff --git a/apps/kimi-code/src/cli/update/prompt.ts b/apps/kimi-code/src/cli/update/prompt.ts index 3b48ad0722..6e386b47d2 100644 --- a/apps/kimi-code/src/cli/update/prompt.ts +++ b/apps/kimi-code/src/cli/update/prompt.ts @@ -11,6 +11,7 @@ import { UPDATE_PROMPT_TEXT_DIM, UPDATE_PROMPT_WARNING, } from '#/constant/update'; +import { t } from '#/i18n'; import { type InstallSource, type UpdateTarget } from './types'; @@ -32,8 +33,8 @@ export interface InstallPromptOptions { readonly output?: NodeJS.WriteStream; } -const INSTALL_HINT = 'Install update now'; -const SKIP_HINT = 'Continue with current version'; +const INSTALL_HINT = t('tui.statusMessages.updatePromptInstallNow'); +const SKIP_HINT = t('tui.statusMessages.updatePromptContinue'); export function createInstallPromptChoices(target: UpdateTarget): readonly InstallPromptChoice[] { return [ @@ -68,18 +69,18 @@ function renderInstallPrompt( const targetVersion = chalk.hex(UPDATE_PROMPT_SUCCESS).bold(options.target.version); const sourceLabel = chalk.hex(UPDATE_PROMPT_PRIMARY).bold(options.installSource); const command = chalk.hex(UPDATE_PROMPT_PRIMARY)(options.installCommand); - const changelogText = chalk.hex(UPDATE_PROMPT_PRIMARY).underline(`View changelog: ${CHANGELOG_URL}`); + const changelogText = chalk.hex(UPDATE_PROMPT_PRIMARY).underline(t('tui.statusMessages.updatePromptChangelog', { url: CHANGELOG_URL })); const lines = [ - chalk.hex(UPDATE_PROMPT_PRIMARY).bold('Kimi Code Update Available'), - chalk.hex(UPDATE_PROMPT_MUTED)(`${PRODUCT_NAME} has a newer release ready.`), + chalk.hex(UPDATE_PROMPT_PRIMARY).bold(t('tui.statusMessages.updatePromptTitle')), + chalk.hex(UPDATE_PROMPT_MUTED)(t('tui.statusMessages.updatePromptNewer', { name: PRODUCT_NAME })), `]8;;${CHANGELOG_URL}\\${changelogText}]8;;\\`, '', - `${label('Current')} ${currentVersion}`, - `${label('Target ')} ${targetVersion}`, - `${label('Source ')} ${sourceLabel}`, - `${label('Command')} ${command}`, + `${label(t('tui.statusMessages.updatePromptCurrent'))} ${currentVersion}`, + `${label(t('tui.statusMessages.updatePromptTarget'))} ${targetVersion}`, + `${label(t('tui.statusMessages.updatePromptSource'))} ${sourceLabel}`, + `${label(t('tui.statusMessages.updatePromptCommand'))} ${command}`, '', - chalk.hex(UPDATE_PROMPT_MUTED)('↑↓ choose · Enter confirm · Esc continue'), + chalk.hex(UPDATE_PROMPT_MUTED)(t('tui.statusMessages.updatePromptNavHint')), '', ]; diff --git a/apps/kimi-code/src/i18n/index.ts b/apps/kimi-code/src/i18n/index.ts new file mode 100644 index 0000000000..133e147f3d --- /dev/null +++ b/apps/kimi-code/src/i18n/index.ts @@ -0,0 +1,90 @@ +import en from './locales/en'; +import zh from './locales/zh'; + +export type Locale = 'en' | 'zh'; + +const messages = { en, zh }; + +let currentLocale: Locale = 'en'; + +function detectLocale(): Locale { + const envLang = process.env['KIMI_LANG']; + if (envLang === 'zh' || envLang?.startsWith('zh')) { + return 'zh'; + } + if (envLang === 'en' || envLang?.startsWith('en')) { + return 'en'; + } + const systemLang = process.env['LANG'] || process.env['LC_ALL'] || process.env['LC_MESSAGES']; + if (systemLang?.toLowerCase().startsWith('zh')) { + return 'zh'; + } + return 'en'; +} + +currentLocale = detectLocale(); + +export function setLocale(locale: Locale): void { + if (locale in messages) { + currentLocale = locale; + } +} + +export function getLocale(): Locale { + return currentLocale; +} + +type MessageValue = string | { [key: string]: MessageValue }; + +type Join = K extends string | number + ? P extends string | number + ? `${K}.${P}` + : never + : never; + +type Paths = T extends MessageValue + ? T extends string + ? never + : { + [K in keyof T]-?: K extends string | number + ? Join> | K + : never; + }[keyof T] + : never; + +export type TranslationKey = Paths; + +function resolveMessage( + locale: Locale, + key: string, +): string | undefined { + const parts = key.split('.'); + let current: MessageValue | undefined = messages[locale]; + for (const part of parts) { + if (current === undefined || typeof current === 'string') { + return undefined; + } + current = current[part]; + } + return typeof current === 'string' ? current : undefined; +} + +export function t( + key: TranslationKey | (string & {}), + params?: Record, +): string { + let message = resolveMessage(currentLocale, key); + if (message === undefined) { + message = resolveMessage('en', key); + } + if (message === undefined) { + return key; + } + if (!params) { + return message; + } + return message.replace(/\{\{(\w+)\}\}/g, (_, name) => { + const value = params[name]; + return value !== undefined ? String(value) : `{{${name}}}`; + }); +} diff --git a/apps/kimi-code/src/i18n/locales/en.ts b/apps/kimi-code/src/i18n/locales/en.ts new file mode 100644 index 0000000000..f8af500334 --- /dev/null +++ b/apps/kimi-code/src/i18n/locales/en.ts @@ -0,0 +1,1617 @@ +export default { + meta: { + languageName: 'English', + }, + common: { + yes: 'Yes', + no: 'No', + ok: 'OK', + cancel: 'Cancel', + close: 'Close', + submit: 'Submit', + save: 'Save', + edit: 'Edit', + delete: 'Delete', + loading: 'Loading...', + error: 'Error', + warning: 'Warning', + info: 'Info', + success: 'Success', + optional: 'optional', + required: 'required', + }, + cli: { + program: { + description: 'The Starting Point for Next-Gen Agents', + helpOption: 'Show help.', + usage: '[options] [command]', + documentation: 'Documentation:', + }, + commandDescriptions: { + root: 'Start an interactive chat session.', + new: 'Start a new chat session.', + continue: 'Continue the most recent session.', + session: 'List, search, or switch chat sessions.', + login: 'Authenticate with Kimi Code CLI via the device-code flow.', + logout: 'Log out and clear local credentials.', + upgrade: 'Upgrade Kimi Code to the latest version.', + plugins: 'Manage Kimi Code plugins.', + provider: 'Manage LLM providers non-interactively.', + settings: 'Open the settings dialog.', + config: 'Read or write configuration values.', + web: 'Start the web UI server.', + server: 'Start or manage the headless server.', + exportCmd: 'Export a session as a ZIP archive.', + doctor: 'Validate Kimi Code configuration files.', + doctorConfig: 'Validate config.toml.', + doctorTui: 'Validate tui.toml.', + acp: 'Run an ACP tool or inspector.', + vis: 'Open the visualization server.', + migrate: 'Run data migration, then exit.', + serverRun: 'Start the Kimi server (background daemon; use --foreground to attach).', + serverPs: 'List clients currently connected to the running Kimi server.', + serverKill: 'Stop the running Kimi server (graceful API + forced PID kill).', + serverRotateToken: 'Generate a new persistent server token; the previous token stops working immediately.', + providerAdd: 'Import every provider listed in a custom registry (api.json).', + providerRemove: 'Remove a provider and every model alias that referenced it.', + providerList: 'Show configured providers and their model counts.', + providerCatalog: 'Discover and import providers from the public models.dev catalog.', + providerCatalogList: 'List providers in the catalog, or models when a providerId is given.', + providerCatalogAdd: 'Import a known provider from the catalog by id.', + }, + optionDescriptions: { + session: 'Resume a session. With ID: resume that session. Without ID: interactively pick.', + resume: 'Alias for --session.', + continue: 'Continue the previous session for the working directory.', + yolo: 'Automatically approve all actions.', + auto: 'Start in auto permission mode.', + plan: 'Start in plan mode.', + model: 'LLM model alias to use for this invocation. Defaults to default_model in config.toml.', + prompt: 'Run one prompt non-interactively and print the response.', + outputFormat: 'Output format for prompt mode. Defaults to text.', + skillsDir: 'Load skills from this directory instead of auto-discovered user and project directories. Can be repeated.', + addDir: 'Add an additional workspace directory for this session. Can be repeated.', + yes: 'Automatically confirm prompts.', + autoApprove: 'Automatically approve all actions.', + exportOutput: 'Output ZIP path.', + exportYes: 'Skip previous-session confirmation.', + exportSessionId: 'Session id to export. Defaults to the most recent session.', + exportNoIncludeGlobalLog: 'Skip bundling the active global diagnostic log (~/.kimi-code/logs/kimi-code.log, not rotated .1 files). By default the global log is included.', + acpLogin: 'Run the device-code login flow then exit (entry point for ACP terminal-auth).', + visPort: 'Port to bind. Default: auto-pick a free port.', + visHost: 'Host to bind. Default: 127.0.0.1.', + visNoOpen: 'Do not open the browser automatically.', + visSessionId: 'Open directly to this session.', + doctorConfigPath: 'Validate this file as config.toml instead of the default path.', + doctorTuiPath: 'Validate this file as tui.toml instead of the default path.', + serverPsJson: 'Print the raw connection list as JSON.', + serverRunOptionPort: 'Bind port (default 58627).', + serverRunOptionHost: 'Bind host. Omit for 127.0.0.1 (local); pass --host for 0.0.0.0 (all interfaces), or --host for a specific address. The bearer token is printed at startup.', + serverRunOptionAllowedHost: 'Extra Host header values to allow through the DNS-rebinding check. Repeatable or comma-separated; a leading dot matches a domain suffix (e.g. .example.com).', + serverRunOptionKeepAlive: 'Keep the server running instead of exiting after 60s with no connected clients. Implied by --host / --allowed-host, always on in --foreground.', + serverRunOptionInsecureNoTls: 'Allow a non-loopback bind without a TLS-terminating reverse proxy. Defaults to true; only relevant for non-loopback binds.', + serverRunOptionAllowRemoteShutdown: 'On a non-loopback bind, keep POST /api/v1/shutdown enabled (default: disabled → 404).', + serverRunOptionAllowRemoteTerminals: 'On a non-loopback bind, keep PTY /api/v1/terminals/* routes enabled (default: disabled → 404). Remote shell is high risk.', + serverRunOptionDangerousBypassAuth: 'Disable bearer-token auth on all REST and WebSocket routes, and advertise it via /api/v1/meta. Only use on a trusted network or behind your own authenticating proxy.', + serverRunOptionLogLevel: 'Server log level: fatal|error|warn|info|debug|trace|silent. Omit to keep logs off.', + serverRunOptionDebugEndpoints: 'Mount /api/v1/debug/* routes for test introspection. OFF by default; production callers leave this unset.', + serverRunOptionForeground: 'Run the server in the foreground and keep this terminal attached until SIGINT/SIGTERM (do not daemonize).', + serverRunOptionOpen: 'Open the web UI in the default browser once the server is healthy.', + serverRunOptionNoOpen: 'Do not open the web UI in the default browser.', + providerApiKey: 'Registry API key. Falls back to KIMI_REGISTRY_API_KEY.', + providerListJson: 'Emit the raw providers/models config as JSON.', + providerCatalogFilter: 'Case-insensitive id/name substring filter.', + providerCatalogUrl: 'Override catalog URL. Defaults to {{url}}.', + providerCatalogJson: 'Emit the matching catalog slice as JSON.', + providerCatalogApiKey: 'API key for the provider. Falls back to KIMI_REGISTRY_API_KEY.', + providerCatalogDefaultModel: 'Mark the imported model as default_model after import.', + }, + errors: { + unknownCommand: "unknown command '{{command}}'. See '{{cliName}} --help'.", + promptEmpty: 'Prompt cannot be empty.', + modelEmpty: 'Model cannot be empty.', + outputFormatPromptOnly: 'Output format is only supported in prompt mode.', + cannotCombinePromptAndYolo: 'Cannot combine --prompt with --yolo.', + cannotCombinePromptAndAuto: 'Cannot combine --prompt with --auto.', + cannotCombinePromptAndPlan: 'Cannot combine --prompt with --plan.', + sessionWithoutIdInPromptMode: 'Cannot use --session without an id in prompt mode.', + cannotCombineContinueAndSession: 'Cannot combine --continue, --session.', + cannotCombineYoloAndAuto: 'Cannot combine --yolo with --auto.', + }, + }, + startup: { + operations: { + runPrompt: 'run prompt', + startShell: 'start shell', + runMigration: 'run migration', + runPluginNodeEntry: 'run plugin node entry', + upgrade: 'upgrade', + }, + error: { + failedTo: 'error: failed to {{operation}}: {{message}}', + title: 'error: {{title}}', + messageLabel: 'message:', + seeLog: 'See log: {{path}}', + }, + }, + tui: { + chrome: { + footer: { + context: 'context: {{pct}}', + contextWithTokens: 'context: {{pct}} ({{tokens}}/{{maxTokens}})', + goalBadge: '[goal {{dot}} {{status}} · {{elapsed}} · {{turns}}]', + statusActive: 'active', + statusPaused: 'paused', + statusBlocked: 'blocked', + turn_one: '{{count}} turn', + turn_other: '{{count}} turns', + turnPlural: 'turns', + task_one: '{{count}} task running', + task_other: '{{count}} tasks running', + agent_one: '{{count}} agent running', + agent_other: '{{count}} agents running', + thinking: ' thinking', + thinkingEffort: ' thinking: {{effort}}', + elapsedSeconds: '{{count}}s', + elapsedMinutes: '{{count}}m', + elapsedHours: '{{hours}}h{{minutes}}m', + }, + welcome: { + title: 'Welcome to Kimi Code!', + loggedOutPrompt: 'Run /login or /provider to get started.', + helpPrompt: 'Send /help for help information.', + modelNotSet: 'not set, run /login or /provider', + directory: 'Directory: ', + session: 'Session: ', + model: 'Model: ', + version: 'Version: ', + mcp: 'MCP: ', + }, + deviceCodeBox: { + title: 'Sign in to Kimi Code', + prompt: 'Visit the URL below in your browser to authorize:', + codeLabel: 'Verification code: ', + hint: 'Press Ctrl-C to cancel', + }, + todoPanel: { + header: 'Todo', + collapseHint: 'all {{count}} items · ctrl+t to collapse', + expandHint: '… +{{count}} more{{distribution}} · ctrl+t to expand', + statusDone: 'done', + statusInProgress: 'in progress', + statusPending: 'pending', + }, + activityPane: { + tipPrefix: ' · Tip: {{tip}}', + }, + messages: { + cronMessage: { + missedTitle: 'Missed scheduled reminders', + firedTitle: 'Scheduled reminder fired', + job: 'job {{jobId}}', + oneShot: 'one-shot', + coalesced: '{{count}} fires coalesced', + missed: '{{count}} missed', + finalDelivery: 'final delivery', + }, + }, + hints: { + ctrlDExit: 'Press Ctrl+D again to exit', + ctrlCExit: 'Press Ctrl+C again to exit', + llmNotSet: 'LLM not set, send "/login" to login', + noActiveSession: 'No active session. Send /login to login.', + oauthLoginExpired: 'OAuth login expired. Send /login to login.', + }, + tips: { + ctrlSAddGuidance: 'ctrl-s to add guidance without waiting for the turn to finish', + tasksCheckProgress: '/tasks to check progress and status for background tasks', + initGenerateAgents: '/init: generate AGENTS.md', + tryDance: 'Try /dance for a hidden Easter egg', + pluginsSuperpowers: '/plugins: manage plugins — try the "superpowers" plugin', + pluginsKimiDatasource: + '/plugins: manage plugins — try the "Kimi Datasource" for reliable financial, economic, and academic data', + scheduleTasks: 'ask Kimi to schedule tasks, e.g. "remind me at 5pm"', + sessionsBrowse: '/sessions to browse and resume earlier sessions', + goalMultiStep: '/goal for multi-step work with a clear finish line', + goalNext: '/goal next to queue follow-up work while the current goal keeps running', + webUi: '/web: use the Web UI for a better experience', + mentionFiles: '@: mention files', + runShellCommand: '! to run a shell command', + shiftEnterNewline: 'shift+enter: newline', + ctrlCCancel: 'ctrl+c: cancel', + themeSwitch: '/theme to switch the terminal UI theme', + autoMode: '/auto when you want Kimi to handle approvals and keep going unattended', + yoloMode: '/yolo to skip most approvals for trusted batch work, only use it in repos you trust', + helpCommands: '/help: show commands', + compactContext: '/compact compresses context when it gets long', + ctrlOToolOutput: + 'ctrl-o to hide or reveal tool output switching between a clean chat view and full execution details', + shiftTabPlanMode: 'shift-tab to Plan mode to review the approach before Kimi edits files.', + modelSwitch: '/model: switch model', + }, + }, + slashCommands: { + yolo: 'Toggle YOLO mode: AI auto-approves safe actions, asks for approval on risky ones.', + auto: 'Toggle Auto mode: run all actions automatically, including risky ones.', + permission: 'Select permission mode', + settings: 'Open TUI settings', + plan: 'Toggle plan mode', + swarm: 'Toggle swarm mode or run one task in swarm mode', + discuss: 'Start a roundtable discussion among multiple agents', + model: 'Switch LLM model', + effort: 'Switch thinking effort', + provider: 'Manage AI providers (add / delete / refresh)', + btw: 'Ask a forked side agent a question', + help: 'Show available commands and shortcuts', + new: 'Start a fresh session in the current workspace', + sessions: 'Browse and resume sessions', + tasks: 'Browse background tasks', + mcp: 'Show MCP server status', + plugins: 'Manage plugins', + addDir: 'Add or list an additional workspace directory', + experiments: 'Manage experimental features', + reload: 'Reload session and apply config.toml settings plus tui.toml UI preferences', + reloadTui: 'Reload only tui.toml UI preferences', + compact: 'Compact the conversation context', + goal: 'Start or manage an autonomous goal', + init: 'Analyze the codebase and generate AGENTS.md', + fork: 'Fork the current session', + title: 'Set or show session title', + usage: 'Show session tokens + context window + plan quotas', + status: 'Show current session and runtime status', + feedback: 'Send feedback to make Kimi Code better', + undo: 'Withdraw the last prompt from the transcript', + editor: 'Set the external editor for Ctrl-G', + theme: 'Set the terminal UI theme', + logout: 'Log out of a configured provider', + login: 'Select a platform and authenticate', + exportMd: 'Export current session as a Markdown file', + exportDebugZip: 'Export current session as a debug ZIP archive', + web: 'Open the current session in the Web UI and exit the terminal', + exit: 'Exit the application', + version: 'Show version information', + }, + approvalLabels: { + approve: 'Approve', + approveOnce: 'Approve once', + approveForSession: 'Approve for this session', + reject: 'Reject', + rejectWithFeedback: 'Reject with feedback', + revise: 'Revise', + }, + approvalDescriptions: { + startGoal: 'Start a goal?', + editFile: 'edit {{path}}', + fileOp: '{{operation}} {{path}}', + stopTask: 'stop task: {{description}}', + spawnAgent: 'spawn {{name}}', + invokeSkill: 'invoke skill {{name}}', + fetchUrl: 'fetch {{url}}', + searchQuery: 'search: {{query}}', + updateTodoList: 'update todo list ({{count}} items)', + backgroundTask: '{{status}} task {{taskId}}: {{description}}', + taskStopBrief: 'Stop task {{taskId}}: {{description}}', + goalStartBrief: 'Start goal: {{objective}}', + goalCompletionCriterion: 'Done when: {{criterion}}', + }, + dangerPatterns: { + recursiveDelete: 'recursive delete', + sudo: 'sudo', + pipeToShell: 'pipe to shell', + ddWrite: 'dd write', + mkfs: 'mkfs', + writeToRawDevice: 'write to raw device', + chmod777: 'chmod 777', + forkBomb: 'fork bomb', + }, + dialogs: { + apiKeyInput: { + title: 'Enter API key for {{platformName}}', + emptyHint: 'API key cannot be empty.', + footer: 'Enter to submit · Esc to cancel', + }, + approvalPreview: { + title: ' Preview ', + footerKeys: '↑↓ line · PgUp/PgDn page · g/G top/bot · Q/Esc/Ctrl+E cancel', + }, + choicePicker: { + navHint: '↑↓ navigate · ←→ page · Enter select · Esc cancel', + searchHint: '(type to search)', + searchLabel: 'Search: ', + noMatches: 'No matches', + page: 'Page {{page}}/{{pageCount}}', + }, + compaction: { + compacting: 'Compacting context...', + complete: 'Compaction complete', + cancelled: 'Compaction cancelled', + detailTokens: '({{before}} → {{after}} tokens)', + shortcutHint: '(Ctrl-O to {{action}} compaction summary)', + hide: 'hide', + show: 'show', + tipPrefix: ' · Tip: {{tip}}', + }, + customRegistryImport: { + title: 'Import custom provider registry', + subtitleDefault: 'Paste an api.json URL and its Bearer token.', + subtitleUrlEmpty: 'Registry URL cannot be empty.', + subtitleTokenEmpty: 'Bearer token cannot be empty.', + footerNotLast: 'Tab / ↑↓ to switch · Enter for next field · Esc to cancel', + footerLast: 'Tab / ↑↓ to switch · Enter to submit · Esc to cancel', + urlLabel: 'Registry URL', + tokenLabel: 'Bearer token', + }, + editorSelector: { + title: 'Select external editor', + vsCode: 'VS Code (code --wait)', + vim: 'Vim', + neovim: 'Neovim', + nano: 'Nano', + autoDetect: 'Auto-detect ($VISUAL / $EDITOR)', + }, + feedbackInput: { + title: 'Send feedback to Kimi Code', + subtitleDefault: "Tell us what's working or what's not.", + subtitleEmpty: 'Feedback cannot be empty.', + footer: 'Enter to submit · Esc to cancel', + }, + goalQueueManager: { + title: 'Upcoming goals', + navHint: '↑↓ navigate · Space select · E edit · D delete · Esc cancel', + reorderHint: '↑↓ reorder · Space done · E edit · D delete · Esc cancel', + empty: 'No upcoming goals.', + selected: 'selected', + more: '▼ {{count}} more', + }, + goalQueueEdit: { + title: 'Edit upcoming goal', + subtitle: 'Update the queued objective.', + errorEmpty: 'Goal objective cannot be empty.', + errorTooLong: 'Goal objective cannot exceed {{max}} characters.', + footer: 'Enter submit · Shift-Enter/Ctrl-J newline · Esc cancel', + previous: '… {{count}} previous', + more: '… {{count}} more', + }, + helpPanel: { + title: ' help ', + cancelHint: '· Esc / Enter / q to cancel · ↑↓ scroll', + greeting: 'Sure, Kimi is ready to help! Just send a message to get started.', + keyboardShortcuts: 'Keyboard shortcuts', + slashCommands: 'Slash commands', + showing: ' showing {{from}}-{{to}} of {{total}}', + shortcuts: { + shiftTab: 'Toggle plan mode', + ctrlG: 'Edit in external editor ($VISUAL / $EDITOR)', + ctrlO: 'Toggle tool output / compaction summary expansion', + ctrlT: 'Expand / collapse the todo list (when truncated)', + ctrlS: 'Steer — inject a follow-up during streaming', + shiftEnter: 'Insert newline', + ctrlC: 'Interrupt stream / clear input', + ctrlD: 'Exit (on empty input)', + esc: 'Close dialogs / interrupt streaming', + arrowUpDown: 'Browse input history', + enter: 'Submit', + }, + }, + modelSelector: { + title: 'Select a model', + searchHint: '(type to search)', + searchLabel: 'Search: ', + hintTab: 'Tab toggle provider', + hintNavigate: '↑↓ navigate', + hintBackspace: 'Backspace clear', + hintSelect: 'Enter select', + hintSessionOnly: 'Alt+S session-only', + hintCancel: 'Esc cancel', + noMatches: 'No matches', + count: '{{matches}} / {{total}}', + more: '▼ {{count}} more', + thinking: 'Thinking', + thinkingSwitchable: 'Thinking (←→ to switch)', + thinkingAlwaysOn: 'always-on', + thinkingToggle: 'toggle', + thinkingUnsupported: 'unsupported', + on: 'On', + off: 'Off', + unsupported: 'Unsupported', + }, + tabbedModelSelector: { + allTab: 'All', + }, + themeSelector: { + title: 'Select theme', + auto: 'Auto (match terminal)', + dark: 'Dark', + light: 'Light', + custom: 'Custom: {{name}}', + }, + settingsSelector: { + title: 'Settings', + model: 'Model', + modelDesc: 'Switch the active model and thinking mode.', + permission: 'Permission', + permissionDesc: 'Choose how tool actions are approved.', + theme: 'Theme', + themeDesc: 'Change the terminal UI theme.', + language: 'Language', + languageDesc: 'Change the interface language (restart required).', + editor: 'Editor', + editorDesc: 'Set the external editor command.', + experiments: 'Experiments', + experimentsDesc: 'Turn experimental features on or off.', + upgrade: 'Automatic updates', + upgradeDesc: 'Turn automatic CLI updates on or off.', + usage: 'Usage', + usageDesc: 'Show session tokens, context window, and plan quotas.', + }, + permissionSelector: { + title: 'Select permission mode', + manual: 'Manual', + manualDesc: 'Approve every action yourself.', + auto: 'Auto', + autoDesc: 'Run all actions automatically, including risky ones.', + yolo: 'YOLO', + yoloDesc: 'AI decides which actions need your approval.', + }, + approvalPanel: { + headerForBash: 'Run this command?', + headerForWrite: 'Write this file?', + headerForEdit: 'Apply these edits?', + headerForTaskStop: 'Stop this task?', + headerForExitPlanMode: 'Ready to build with this plan?', + headerForDefault: 'Approve {{toolName}}?', + feedbackHint: 'Type feedback · ↵ submit.', + navHint: '↑/↓ select · {{numeric}} choose · ↵ confirm{{expand}}', + expandHint: ' · ctrl+e preview', + previewHint: 'ctrl+e to preview', + moreLinesHidden_one: '… {{count}} more line hidden ({{previewHint}})', + moreLinesHidden_other: '… {{count}} more lines hidden ({{previewHint}})', + }, + effortSelector: { + title: 'Select thinking effort', + hintSwitch: '←→ switch', + hintSelect: 'Enter select', + hintSessionOnly: 'Alt+S session-only', + hintCancel: 'Esc cancel', + }, + experimentsSelector: { + title: 'Experimental features', + hintSpace: 'Space toggle', + hintEnter: 'Enter apply', + hintPage: 'PgUp/PgDn page', + hintCancel: 'Esc cancel', + hintNavigate: '↑↓ navigate', + hintBackspace: 'Backspace clear', + searchHint: '(type to search)', + searchLabel: 'Search: ', + noMatches: 'No matches', + count: '{{matches}} / {{total}}', + more: '▼ {{count}} more', + applyButton: '[ Apply changes and reload ]', + noChanges: 'no changes', + changeCount_one: '{{count}} change', + changeCount_other: '{{count}} changes', + statusEnabled: 'enabled', + statusDisabled: 'disabled', + modifiedSuffix: ' · modified', + featureId: 'id {{id}}', + sourceConfig: 'config', + sourceDefault: 'default', + lockedBy: 'locked by {{env}}', + lockedByMasterEnv: 'locked by KIMI_CODE_EXPERIMENTAL_FLAG', + }, + sessionPicker: { + titleCwd: 'Sessions', + titleAll: 'All sessions', + loading: 'Loading sessions...', + empty: 'No sessions found.', + scopeHintAll: 'Ctrl+A all', + scopeHintCwd: 'Ctrl+A current cwd', + justNow: 'just now', + minutesAgo: '{{minutes}}m ago', + hoursAgo: '{{hours}}h ago', + daysAgo: '{{days}}d ago', + searchLabel: 'Search: ', + footerShowing: 'Showing {{from}}-{{to}} of {{totalSuffix}}', + footerLoadedMatches: '{{loaded}} loaded / {{total}} matches', + footerSessions: '{{count}} sessions', + footerLoadedSessions: '{{loaded}} loaded / {{total}} sessions', + }, + goalStartPermissionPrompt: { + titleYolo: 'Start a goal in YOLO mode?', + titleManual: 'Start a goal with approvals on?', + notice1: 'Manual mode asks you before Kimi Code runs commands, edits files, or takes other risky actions.', + notice2: 'Manual mode is not suitable for unattended goal work.', + notice3: 'You can go back without losing your command.', + yoloNotice1: 'YOLO mode approves tools and plan changes automatically.', + yoloNotice2: 'YOLO mode can still stop for questions.', + yoloNotice3: 'Switch to Auto if you want questions skipped during goal work.', + optionAutoLabel: 'Switch to Auto and start', + optionAutoDesc: 'Best if you want Kimi Code to keep working while you are away. Tools are approved automatically, and questions are skipped.', + optionYoloLabel: 'Switch to YOLO and start', + optionYoloDesc: 'Tools and plan changes are approved automatically. Kimi Code may still ask you questions.', + optionYoloKeepLabel: 'Keep YOLO and start', + optionYoloKeepDesc: 'Tools and plan changes stay approved automatically. Kimi Code may still ask you questions.', + optionManualLabel: 'Start in Manual', + optionManualDesc: 'Keep approvals on. Kimi Code will ask before risky actions, so the goal may stop and wait for you.', + optionCancelLabel: 'Do not start', + optionCancelDesc: 'Return to the input box with your goal command.', + }, + swarmStartPermissionPrompt: { + title: 'Start a swarm task with approvals on?', + notice1: 'Manual mode asks you before Kimi Code runs commands, edits files, or takes other risky actions.', + notice2: 'Manual mode can block swarm work while agents are running.', + notice3: 'You can go back without losing your command.', + optionAutoLabel: 'Switch to Auto and start', + optionAutoDesc: 'Best for swarm tasks. Tools are approved automatically, and questions are skipped.', + optionYoloLabel: 'Switch to YOLO and start', + optionYoloDesc: 'Tools and plan changes are approved automatically. Kimi Code may still ask you questions.', + optionManualLabel: 'Start in Manual', + optionManualDesc: 'Keep approvals on. Kimi Code may stop and wait for you during the swarm task.', + }, + startPermissionPrompt: { + navHint: '↑↓ navigate · Enter select · Esc cancel', + }, + taskOutputViewer: { + title: ' Task output ', + noOutput: '[no output captured]', + exitCode: 'exit {{code}}', + status: { + running: 'running', + completed: 'completed', + failed: 'failed', + timedOut: 'timed out', + killed: 'killed', + lost: 'lost', + }, + footer: { + line: 'line', + page: 'page', + topBottom: 'top/bot', + cancel: 'cancel', + }, + }, + providerManager: { + title: 'Providers', + headerHint: '↑↓ navigate · D delete · Esc cancel', + addRowLabel: '[ Add New Platform ]', + empty: 'No providers configured.', + deleteConfirmSingle: 'Delete platform "{{label}}"?', + deleteConfirmMultiple: 'Delete platform "{{label}}" and all {{count}} providers?', + page: 'Page {{page}}/{{pageCount}}', + }, + platformSelector: { + title: 'Select a platform', + kimiCode: 'Kimi Code (OAuth)', + }, + localeSelector: { + title: 'Select language / 选择语言', + enLabel: 'English', + enDesc: 'English interface language.', + zhLabel: '中文', + zhDesc: '中文界面语言。', + }, + questionDialog: { + defaultOtherLabel: 'Other', + notAnswered: 'Not answered', + reviewTitle: 'Review your answer before submit', + submitPrompt: 'Ready to submit your answers?', + unansweredWarning: 'Some questions are still unanswered.', + submitTab: 'Submit', + title: ' question', + typeAnswerHint: 'Type your answer, then press Enter to save.', + moreLines: '... {{count}} more lines', + showing: 'showing {{from}}-{{to}} of {{total}}', + questionPrefix: 'Q{{number}}', + hintEdit: { + typeAnswer: 'type answer', + save: '↵ save', + tabSwitch: 'tab switch', + cancel: 'esc cancel', + }, + hintQuestion: { + navigate: '↑↓ select', + choose: '{{range}} / ↵ choose', + toggle: '{{range}} / ↵ toggle', + tabSwitch: '←/→/tab switch', + cancel: 'esc cancel', + }, + hintSubmit: { + navigate: '↑↓ select', + choose: '1/2 choose', + confirm: '↵ confirm', + tabSwitch: '←/→/tab switch', + cancel: 'esc cancel', + }, + }, + pluginsSelector: { + backToInstalled: 'Back to installed plugins', + backToInstalledDesc: 'Return to the local plugin manager.', + removeConfirmTitle: 'Remove {{name}} ({{id}})?', + removeCancelLabel: 'Cancel', + removeCancelDesc: 'Keep this plugin installed.', + removeConfirmLabel: 'Remove plugin', + removeConfirmDesc: 'Remove only the install record; plugin files are left in place.', + installTrustTitle: 'Install third-party plugin {{label}}?', + installTrustExitLabel: 'Exit', + installTrustExitDesc: 'Cancel the installation.', + installTrustTrustLabel: 'Trust and install', + installTrustTrustDesc: 'Install this third-party plugin anyway.', + marketplaceTierOfficial: 'Official plugin', + marketplaceTierCurated: 'Curated plugin', + marketplaceTierUnknown: 'Plugin', + mcpNavHint: ' ↑↓ navigate · Enter/Space enable/disable · Esc cancel', + actionsSection: 'Actions', + tabInstalled: 'Installed', + tabOfficial: 'Official', + tabThirdParty: 'Third-party', + tabCustom: 'Custom', + enterUpdate: 'Enter update', + enterDetails: 'Enter details', + removeConfirmHint: '↑↓ navigate · Enter/Space select · ←/Esc cancel', + installTrustHint: '↑↓ navigate · Enter/Space select · ←/Esc cancel', + installTrustNotice: '⚠️ This is a third-party plugin that Kimi has not reviewed. It can bundle MCP servers, skills, or files that run code and access your workspace. Install it only if you trust the source.', + mcpEnable: 'Enter/Space enable', + mcpDisable: 'Enter/Space disable', + webBridgeDescription: 'Control your real browser from Kimi Code — navigate, click, type, and screenshot', + statusEnabled: 'enabled', + statusDisabled: 'disabled', + panelTitle: 'Plugins', + mcpServersTitle: 'MCP servers · {{name}}', + mcpServersSection: 'MCP servers ({{enabled}}/{{total}} enabled)', + noMcpServers: 'No MCP servers declared.', + noPluginsInstalled: 'No plugins installed.', + countInstalled: '{{count}} installed', + tabHintInstalled: ' Tab switch · Space toggle · D remove · M MCP · {{enterAction}} · I details · R reload · Esc cancel', + tabHintCustom: ' Tab switch · Enter install · Esc cancel', + tabHintMarketplace: ' Tab switch · ↑↓ navigate · Enter open/install · Esc cancel', + loadingMarketplace: 'Loading marketplace…', + marketplaceUnavailable: 'Marketplace unavailable: {{message}}', + useCustomTabHint: 'Use the Custom tab to install from a URL.', + noPluginsFound: 'No plugins found.', + marketplaceCount: '{{installed}} installed · {{available}} available', + marketplaceSource: 'Source: {{source}}', + openInBrowser: 'open in browser', + installFromUrlHint: ' Install from a GitHub URL (or zip URL / local path):', + installingFromMarketplace: 'Installing {{label}} from marketplace…', + pluginId: 'id {{id}}', + skillCount_one: '{{count}} skill', + skillCount_other: '{{count}} skills', + mcpCount: 'MCP {{enabled}}/{{total}}', + diagnosticsAvailable: 'diagnostics available', + pluginState: 'state {{state}}', + mcpServerTransportHint: '{{action}} · {{transport}} · {{target}}', + mcpServerStdioHint: '{{action}} · stdio · {{command}}', + mcpServerCwdSuffix: ' · cwd {{cwd}}', + versionPrefix: 'v{{version}}', + installStatus: 'install', + installStatusVersion: 'install v{{version}}', + updateStatus: 'update {{local}} → {{latest}}', + installedStatus: 'installed', + installedStatusVersion: 'installed · v{{version}}', + }, + tasksBrowser: { + noActiveTasks: 'No active tasks. Tab = show all.', + noBackgroundTasks: 'No background tasks in this session.', + noTaskSelected: 'No task selected.', + statusRunning: 'running', + statusCompleted: 'completed', + statusFailed: 'failed', + statusTimedOut: 'timed out', + statusKilled: 'killed', + statusLost: 'lost', + statusInterrupted: 'interrupted', + statusTotal: 'total', + stopLabel: 'Stop', + taskIdLabel: 'Task ID:', + statusLabel: 'Status:', + descriptionLabel: 'Description:', + agentTypeLabel: 'Agent type:', + toolCallLabel: 'Tool call:', + exitCodeLabel: 'Exit code:', + reasonLabel: 'Reason:', + detailFrameTitle: 'Detail', + previewFrameTitle: 'Preview Output', + commandLabel: 'Command:', + agentIdLabel: 'Agent ID:', + questionsLabel: 'Questions:', + timeLabel: 'Time:', + pidLabel: 'Pid:', + noDescription: '(no description)', + loadingTail: '[loading…]', + noOutputTail: '[no output captured]', + tooSmall: 'Terminal too small (need ≥ {{minWidth}} × {{minHeight}})', + tasksFrameTitle: 'Tasks [{{filter}}]', + headerTitle: ' TASK BROWSER ', + filterAll: 'filter=ALL', + filterActive: 'filter=ACTIVE', + footerSelect: 'select', + footerOutput: 'output', + footerStop: 'stop', + footerRefresh: 'refresh', + footerFilter: 'filter', + footerConfirm: 'confirm', + footerCancel: 'cancel', + timingRunning: 'running ', + timingFinished: 'finished ', + relativeTimeJustNow: 'just now', + relativeTimeMins: '{{minutes}}m ago', + relativeTimeHours: '{{hours}}h ago', + relativeTimeDays: '{{days}}d ago', + }, + undoSelector: { + navHint: '↑↓ navigate · Enter select · Esc cancel', + title: ' Select messages to undo', + noMessages: ' No messages', + }, + queuePane: { + hintCompacting: ' ↑ to edit · will send after compaction', + hintSteer: ' ↑ to edit · ctrl-s to steer immediately', + hintAfterTask: ' ↑ to edit · will send after current task', + }, + btwPanel: { + readyForSideQuestion: 'Ready for a side question...', + waitingForAnswer: 'Waiting for answer...', + title: ' BTW ', + closeHint: 'Esc close ', + scrollHint: 'Esc close · ↑↓ scroll ', + questionPrefix: 'Q: ', + }, + updatePreferenceSelector: { + title: 'Automatic updates', + on: 'On', + off: 'Off', + onDescription: 'Install new versions in the background.', + offDescription: 'Show the install prompt instead.', + }, + }, + statusMessages: { + failedToSyncMcp: 'Failed to sync MCP server status: {{message}}', + turnStoppedFiltered: 'Turn stopped: provider safety policy blocked the response.', + retryingStep: 'Retrying ({{attempt}}/{{maxAttempts}}) in {{delayS}}s — {{errorName}}', + policyBlocked: 'Provider safety policy blocked the response.', + outputFiltered: 'The model output was filtered ({{reason}}).', + maxTokensTruncated: 'Model hit max_tokens — tool call was truncated before it could run.', + maxTokensNoToolCall: 'Model hit max_tokens — no tool call was emitted.', + maxTokensHint: 'If this limit is wrong for your model, set `max_output_size` on the model alias in your kimi-code config.', + interruptedByUser: 'Interrupted by user', + stepMaxSteps: 'reached per-turn step limit (max_steps)', + stepInterrupted: 'step interrupted ({{reason}})', + goalBlocked: 'Goal blocked.', + goalBlockedDetail: 'The next queued goal will start only after this goal is complete.', + warningPrefix: 'Warning: {{message}}', + mcpServerConnected: 'MCP server "{{name}}" connected · {{count}} tools ({{transport}})', + mcpServerFailed: 'MCP server "{{name}}" failed', + mcpServerFailedWithError: 'MCP server "{{name}}" failed: {{error}}', + mcpServerNeedsAuth: 'MCP server "{{name}}" needs OAuth — run /mcp-config login {{name}}', + mcpServerDisabled: 'MCP server "{{name}}" disabled', + mcpServerConnecting: 'MCP server "{{name}}" connecting…', + activatedSkill: 'Activated skill: {{skillName}}', + // goal.ts + resumeGoalInput: 'Resume the active goal.', + startingNow: 'No active goal. Starting this goal now.', + provideObjective: + 'Provide a goal objective, e.g. `/goal Ship feature X`.', + objectiveTooLong: + 'Goal objective is too long (max {{max}} characters). Reference long details by file path.', + provideNextObjective: + 'Provide an upcoming goal objective, e.g. `/goal next Ship feature X`, or use `/goal next manage`.', + failedToInspectGoal: 'Failed to inspect current goal: {{error}}', + failedToLoadUpcomingGoals: 'Failed to load upcoming goals: {{error}}', + failedToUpdateUpcomingGoals: 'Failed to update upcoming goals: {{error}}', + failedToReadUpcomingGoals: 'Failed to read upcoming goals: {{error}}', + queuedGoalRemoveFailed: 'Queued goal started, but could not be removed from the queue: {{error}}', + queuedGoalRestoreFailed: 'Queued goal could not be restored: {{error}}', + queuedGoalCancelFailed: 'Queued goal could not be cancelled: {{error}}', + queuedGoalNoLongerExists: 'Queued goal no longer exists.', + failedToUpdateUpcomingGoal: 'Failed to update upcoming goal: {{error}}', + goalNotStarted: 'Goal not started.', + failedToSetPermission: 'Failed to set permission mode: {{error}}', + // swarm.ts + swarmTaskNotStarted: 'Swarm task not started.', + swarmModeAlreadyOn: 'Swarm mode is already on.', + swarmModeAlreadyOff: 'Swarm mode is already off.', + swarmModeNotEnabled: 'Swarm mode not enabled.', + goalAlreadyActive: + 'A goal is already active. Use `/goal replace ` to replace it, or `/goal status` to inspect it.', + noGoalToPause: 'No goal to pause.', + goalPaused: 'Goal paused. Use `/goal resume` to continue.', + noGoalToResume: 'No goal to resume.', + noGoalToCancel: 'No goal to cancel.', + goalCancelled: 'Goal cancelled.', + noGoalSet: 'No goal set. Start one with `/goal `.', + // auth.ts + loggedIn: 'Logged in.', + loginCancelled: 'Login cancelled.', + loginFailed: 'Login failed.', + authSuccessButConfigFailed: + 'Authentication successful, but failed to refresh config: {{error}}', + alreadyLoggedInRefreshed: 'Already logged in. Model configuration refreshed.', + loginFailedWithError: 'Login failed: {{error}}', + failedToVerifyApiKey: 'Failed to verify API key: {{error}}', + hintUseKimiCodeInstead: + 'Hint: If your API key was obtained from Kimi Code, please select "Kimi Code" instead.', + noModelsForPlatform: 'No models available for this platform.', + setupComplete: 'Setup complete: {{platformName}} · {{modelId}}', + kimiPlatformDisplay: 'Kimi Platform', + kimiPlatformDisplayWithHost: 'Kimi Platform ({{host}})', + savedToLabel: 'saved to', + oauthLoginDescription: 'OAuth login', + nothingToLogout: 'Nothing to logout.', + loggedOutFrom: 'Logged out from {{label}}.', + // commands/config.ts + planCleared: 'Plan cleared', + planModeOn: 'Plan mode: ON', + planWillBeCreatedHere: 'Plan will be created here: {{path}}', + planModeOff: 'Plan mode: OFF', + unknownPlanSubcommand: 'Unknown plan subcommand: {{subcmd}}', + failedToSetPlanMode: 'Failed to set plan mode: {{msg}}', + yoloModeAlreadyOn: 'YOLO mode is already on', + yoloModeOn: 'YOLO mode: ON', + yoloModeOnSub: 'AI auto-approves safe actions, asks for approval on risky ones.', + yoloModeAlreadyOff: 'YOLO mode is already off', + yoloModeOff: 'YOLO mode: OFF', + noModelSelected: 'No model selected. Run /model to select one first.', + unknownTheme: 'Unknown theme: {{theme}}', + unknownModelAlias: 'Unknown model alias: {{alias}}', + unsupportedEffort: 'Unsupported thinking effort "{{arg}}" for {{alias}}. Available: {{segments}}', + switchModelFailed: 'Failed to switch model: {{msg}}', + switchSavedButDefaultFailed: 'Switched to {{name}}, but failed to save default: {{msg}}', + loadExperimentsFailed: 'Failed to load experimental features: {{error}}', + updateExperimentsFailed: 'Failed to update experimental features: {{error}}', + setPermissionFailed: 'Failed to set permission mode: {{msg}}', + noEditorConfigured: 'No editor configured. Set $VISUAL / $EDITOR, or run /editor .', + compactionCancelFailed: 'Failed to cancel compaction: {{message}}', + noActiveSession: 'No active session.', + autoModeAlreadyOn: 'Auto mode is already on', + autoModeOn: 'Auto mode: ON', + autoModeOnSub: 'Run all actions automatically, including risky ones.', + autoModeAlreadyOff: 'Auto mode is already off', + autoModeOff: 'Auto mode: OFF', + // kimi-tui.ts + modelsAdded_one: '{{providerName}} · +{{count}} model.', + modelsAdded_other: '{{providerName}} · +{{count}} models.', + skippedRefreshing: 'Skipped refreshing {{provider}}: {{reason}}', + warningLabel: 'Warning: {{warning}}', + noSessionsToContinue: 'No sessions to continue under "{{workDir}}"; starting a fresh session.', + cannotSendWhileReplaying: 'Cannot send input while session history is replaying.', + noActiveSessionShell: 'No active session for shell command.', + shellCommandFailed: 'Shell command failed: {{message}}', + failedToCancelShell: 'Failed to cancel shell command: {{message}}', + modelNoImageInput: 'Current model does not support image input.', + modelNoVideoInput: 'Current model does not support video input.', + failedToSend: 'Failed to send: {{message}}', + skillFailed: 'Skill "{{skillName}}" failed: {{message}}', + pluginCommandFailed: 'Command "{{pluginId}}:{{commandName}}" failed: {{message}}', + failedToSteer: 'Failed to steer: {{message}}', + resumeOtherWorkDir: 'Current session is in a different working directory.\n To resume, run: {{command}}', + commandCopiedToClipboard: 'Command copied to clipboard', + failedToCopyCommand: 'Failed to copy command to clipboard', + alreadyOnSession: 'Already on this session.', + cannotSwitchWhileStreaming: 'Cannot switch sessions while streaming — press Esc or Ctrl-C first.', + cannotSwitchWhileReplaying: 'Cannot switch sessions while history is replaying.', + failedToResumeSession: 'Failed to resume session {{sessionId}}: {{message}}', + resumedSession: 'Resumed session ({{sessionId}}).', + failedToReplayHistory: 'Failed to replay session history: {{message}}', + cannotStartNewWhileReplaying: 'Cannot start a new session while history is replaying.', + failedToStartNewSession: 'Failed to start a new session: {{message}}', + postCreateSetupFailed: 'Post-create setup failed: {{message}}', + startedNewSession: 'Started a new session ({{sessionId}}).', + approvedForSession: 'Approved for session', + approved: 'Approved', + rejected: 'Rejected', + cancelled: 'Cancelled', + waitingForAuthorization: 'Waiting for authorization…', + working: 'working...', + noShellCommandRunning: 'No shell command running.', + commandStillStarting: 'Command is still starting — try again.', + commandAlreadyFinished: 'Command already finished.', + failedToMoveToBackground: 'Failed to move to background: {{message}}', + movedToBackground: 'Moved to background.', + movedToBackgroundHint: 'Moved to background. /tasks to view.', + noForegroundTaskRunning: 'No foreground task running.', + failedToListTasks: 'Failed to list tasks: {{message}}', + failedToDetachTask: 'Failed to detach {{taskId}}: {{message}}', + taskAlreadyFinished_one: 'Task already finished.', + taskAlreadyFinished_other: 'Tasks already finished.', + movedOneTaskToBackground: 'Moved 1 task to background.', + movedTasksToBackground: 'Moved {{count}} tasks to background.', + movedTasksOfToBackground: 'Moved {{detached}} of {{total}} tasks to background.', + tasksToView: ' /tasks to view.', + failedToApplyStartupFlags: 'Failed to apply startup flags: {{message}}', + compactionCancelled: 'Compaction cancelled', + compactionComplete: 'Compaction complete', + goalSet: 'Goal set', + planSentBackForRevision: 'Plan sent back for revision', + planReviewRejected: 'Plan review rejected', + planReviewCancelled: 'Plan review cancelled', + replayGoalPaused: 'Goal paused', + replayGoalResumed: 'Goal resumed', + replayGoalBlocked: 'Goal blocked', + replayGoalUpdated: 'Goal updated', + replaySessionUnavailable: 'Session history is unavailable for this session.', + replayFailed: 'Failed to replay session history: {{message}}', + replayPlanModeOn: 'Plan mode: ON', + replayPlanModeOff: 'Plan mode: OFF', + replayYoloModeOn: 'YOLO mode: ON', + replayYoloModeOnSub: 'All actions will be approved automatically. Use with caution.', + replayYoloModeOff: 'YOLO mode: OFF', + replayPermissionMode: 'Permission mode: {{mode}}', + replaySkillActivated: 'Activated skill: {{skillName}}', + noModelsConfigured: 'No models configured', + noModelsConfiguredSub: 'Run /login to sign in to Kimi, or /provider to add another provider from a model catalog.', + experimentalUpdated: 'Experimental features updated.', + experimentalUpdatedSessionReloaded: 'Experimental features updated. Session reloaded.', + // commands/session.ts + sessionTitle: 'Session title: {{title}}', + sessionTitleNotSet: 'Session title: (not set) — id: {{sessionId}}', + sessionFailedToSetTitle: 'Failed to set title: {{message}}', + sessionTitleSetTo: 'Session title set to: {{title}}', + sessionFailedToFork: 'Failed to fork session: {{message}}', + sessionForked: 'Session forked ({{forkedId}}). To return to the original session: kimi -r {{originalId}}', + sessionFailedToSwitchToForked: 'Failed to switch to forked session: {{message}}', + sessionExportingMarkdown: 'Exporting session as Markdown…', + sessionNoMessagesToExport: 'No messages to export.', + sessionExportComplete: 'Exported {{count}} messages', + sessionFailedToExport: 'Failed to export session: {{message}}', + sessionExportingDebug: 'Exporting session…', + sessionExportDebugComplete: 'Export complete', + // commands/plugins.ts + pluginsUsageInstall: 'Usage: /plugins install ', + pluginsUsageRemove: 'Usage: /plugins remove ', + pluginsUsageMcp: 'Usage: /plugins mcp enable|disable ', + pluginsUnknownAction: 'Unknown /plugins action: {{action}}. Run /plugins to choose interactively.', + pluginsCommandFailed: '/plugins {{action}} failed: {{error}}', + pluginsFailedToLoad: 'Failed to load plugins: {{error}}', + pluginsFailedToLoadMcp: 'Failed to load plugin MCP servers: {{error}}', + pluginsFailed: '/plugins failed: {{error}}', + pluginsMcpFailed: '/plugins mcp failed: {{error}}', + pluginsInstallCancelled: 'Install cancelled.', + pluginsInstallCancelledLabel: 'Install cancelled: {{label}}.', + pluginsInstallingFrom: 'Installing plugin from {{source}}…', + pluginsInstallFinished: 'Install finished — see details below.', + pluginsInstallFailed: 'Install failed: {{error}}', + pluginsInstallingOrUpdating: 'Installing or updating {{label}} from marketplace...', + pluginsFailedToInstall: 'Failed to install {{label}}: {{error}}', + pluginsMcpEnabled: 'Enabled MCP server {{server}} for {{id}}. Run /reload or /new to apply.', + pluginsMcpDisabled: 'Disabled MCP server {{server}} for {{id}}. Run /reload or /new to apply.', + pluginsEnabled: 'Enabled {{id}}. Run /reload or /new to apply.', + pluginsDisabled: 'Disabled {{id}}. Run /reload or /new to apply.', + pluginsMcpDisabledHint: ' Some MCP servers are disabled; re-enable with /plugins mcp enable {{id}} .', + pluginsInlineMcpDisabled: ' · MCP servers disabled', + pluginsRemoveCancelled: 'Remove cancelled: {{id}}.', + pluginsRemoved: 'Removed {{id}}.', + pluginsReloadHint: 'Run /new or /reload to apply plugin changes.', + pluginsInlineChangeHint: 'run /reload or /new to apply', + pluginsOpeningPage: 'Opening the {{label}} page in your browser…', + pluginsIfNotOpened: 'If it did not open, visit {{url}}', + pluginsReloadResult: 'Reload: +{{added}} -{{removed}}', + pluginsReloadResultErrors: ' ({{count}} errors)', + pluginsDeclaresMcp_one: ' Declares {{count}} MCP server; enabled by default and configurable from /plugins.', + pluginsDeclaresMcp_other: ' Declares {{count}} MCP servers; enabled by default and configurable from /plugins.', + pluginsInstalledDesc: 'Installed {{displayName}}{{version}} {{sourcePhrase}}', + pluginsUpdatedDesc: 'Updated {{displayName}}{{version}} {{sourcePhrase}}', + pluginsMigratedDesc: 'Migrated {{displayName}}: {{prevSource}} → {{source}}{{version}}', + pluginsFromSource: 'from {{source}}', + pluginsViaSource: 'via {{source}}', + // commands/add-dir.ts + addDirNoAdditionalDirs: 'No additional directories configured.', + addDirTitle: 'Add directory to workspace: {{path}}', + addDirHint: '↑↓ navigate · Enter confirm · Esc cancel', + addDirYesSession: 'Yes, for this session', + addDirYesRemember: 'Yes, and remember this directory', + addDirNo: 'No', + addDirDidNotAdd: 'Did not add {{path}} as a working directory.', + addDirListHeader: 'Additional directories:', + addDirSuccessPersist: 'Added workspace directory:\n {{path}}\n Saved to:\n {{configPath}}', + addDirSuccessSession: 'Added workspace directory:\n {{path}}\n For this session only', + // commands/reload.ts + reloadTuiConfigReloaded: 'TUI config reloaded.', + reloadSession: 'Session reloaded.', + reloadNoActiveSession: 'Runtime and TUI config reloaded; no active session.', + // commands/undo.ts + undoCannotWhileStreaming: 'Cannot undo while streaming — press Esc or Ctrl-C first.', + undoUsage: 'Usage: /undo [count], where count is a positive integer.', + undoNothingToUndo: 'Nothing to undo.', + undoFailed: 'Failed to undo: {{message}}', + undoLimit: 'Cannot undo {{requested}}; only {{max}} can be undone in the active context{{reason}}.', + undoLimitAfterCompaction: ' after the last compaction', + undoNothingToUndoAfterCompaction: 'Nothing to undo after the last compaction.', + undoUserMessage: 'User message', + undoUserMessageWithImage: 'User message ({{count}} images)', + undoSkillUnknown: 'Skill: unknown', + // commands/web.ts + openInWebUi: 'Open in Web UI', + openInWebUiHint: 'Open the current session in your browser.', + continueLabel: 'Continue', + continueDescription: 'Exit the terminal and open the session in the browser.', + cancelLabel: 'Cancel', + stayInTui: 'Stay in the terminal UI.', + startingServerAndWebUi: 'Starting Kimi server and opening web UI…', + failedToStartServer: 'Failed to start server: {{error}}', + webOpenUrl: 'open {{url}}', + webToken: 'Token: {{token}}', + // sub/server/lifecycle.ts + serverInstallDesc: 'Install the Kimi server as an OS-managed service (launchd/systemd/schtasks).', + serverPortOption: 'Bind port (default {{port}})', + serverLogLevelOption: 'Log level: {{levels}} (default {{default}})', + serverReinstallOption: 'Reinstall and overwrite if already installed', + serverNoOpenOption: 'Do not open the web UI after install.', + serverOutputJson: 'Output JSON', + serverUninstallDesc: 'Uninstall the Kimi server service.', + serverStartDesc: 'Start the Kimi server service.', + serverStopDesc: 'Stop the Kimi server service.', + serverRestartDesc: 'Restart the Kimi server service.', + serverStatusDesc: 'Show Kimi server service status and connectivity.', + serverStatusUrl: 'URL: {{url}}', + serverStatusState: 'Status: {{state}}', + serverStatusLog: 'Log: {{path}}', + serverStatusNote: 'Note: {{note}}', + serverStatusRunning: 'running', + serverStatusNotRunning: 'not running', + // sub/server/run.ts + serverReadyBanner: 'Kimi server ready', + serverReadyLocalUi: 'Local web UI is available from this machine.', + serverDangerAuthDisabled: '⚠ DANGER: authentication is DISABLED (--dangerous-bypass-auth).', + serverDangerAnyoneAccess: 'Anyone who can reach this port gets full access. Only continue if you understand the risk.', + serverDangerUnsure: 'If you are unsure, run ', + serverDangerUnsureCmd: 'kimi server kill', + serverDangerUnsureSuffix: ' now to stop this process.', + serverUrl: 'Kimi server: {{url}}', + // sub/server/kill.ts + serverKillNoRunning: 'No running Kimi server.', + serverKillStopped: 'Kimi server (pid {{pid}}) stopped.', + serverKillKilled: 'Kimi server (pid {{pid}}) killed.', + serverKillFailedPermissions: 'Failed to stop Kimi server (pid {{pid}}); insufficient permissions?', + // sub/server/daemon.ts + serverDaemonTimeout: 'Kimi server daemon failed to start within {{ms}}ms.', + serverDaemonFailed: 'Kimi server daemon {{reason}} during startup.', + serverDaemonCheckLog: 'Check the log for details: {{path}}', + serverDaemonLogTail: 'Last log lines ({{path}}):', + // sub/server/rotate-token.ts + serverTokenRotated: 'The previous token is now invalid. A running server picks up the new token automatically.', + serverNewToken: 'New server token: {{token}}', + // sub/server/access-urls.ts + serverAccessLocal: 'Local: ', + serverAccessNetwork: 'Network: ', + serverAccessUrl: 'URL: ', + // sub/server/shared.ts + serverInvalidIdleGrace: 'error: invalid --idle-grace-ms value: {{raw}}', + serverInvalidValue: 'error: invalid {{label}} value: {{raw}}', + serverNotServingWebUi: 'Server at {{origin}} does not serve the Kimi web UI{{reason}}. Stop the existing server and rerun `kimi server run`.', + // sub/server/ps.ts + serverPsNotResponding: 'Kimi server at {{origin}} is not responding.', + serverPsFailedHttp: 'Failed to list clients: HTTP {{status}} from {{origin}}.', + serverPsFailedMsg: 'Failed to list clients: {{msg}}', + serverPsTimeout: 'Timed out listing clients from {{origin}}.', + // sub/login-flow.ts + loginOpeningBrowser: 'Opening browser for Kimi device login: {{url}}', + loginPasteUrl: 'If the browser did not open, paste the URL above and enter code: {{code}}', + loginCodeExpires: 'Code expires in {{seconds}}s.', + loginWaiting: 'Waiting for authorization to complete...', + loginSuccess: 'Logged in to {{provider}}.', + loginFailedMsg: 'Login failed: {{message}}', + // sub/provider.ts + providerMissingApiKey: 'Missing API key. Pass --api-key or set KIMI_REGISTRY_API_KEY.', + providerUrlRequired: 'Registry URL is required.', + providerFetchFailed: 'Failed to fetch registry{{suffix}}: {{error}}', + providerNoUsable: 'Registry at {{url}} contained no usable providers.', + providerNotFound: 'Provider "{{id}}" not found.', + providerRemoved: 'Removed provider "{{id}}".', + providerNoneConfigured: 'No providers configured.', + providerDefaultModel: 'Default model: {{model}}', + providerCatalogNotFound: 'Provider "{{id}}" not found in catalog at {{url}}.', + providerCatalogNoModels: 'Provider "{{id}}" lists no usable models in this catalog.', + providerCatalogNoMatch: 'No providers in catalog match "{{filter}}".', + providerCatalogEmpty: 'Catalog is empty.', + providerCatalogUnsupported: 'Provider "{{id}}" has an unsupported wire type in the catalog.', + providerCatalogModelNotInProvider: 'Model "{{model}}" is not in provider "{{id}}". Run "kimi provider catalog list {{id}}" to see available ids.', + providerImported: 'Imported {{name}} ({{id}}) with {{count}} model{{plural}} from {{url}}.', + providerDefaultSet: 'Default model set to {{id}}/{{model}}.', + providerCatalogFetchFailed: 'Failed to fetch catalog from {{url}}{{suffix}}: {{error}}', + // sub/upgrade.ts + upgradeCheckFailed: 'error: failed to check for updates: {{reason}}', + upgradeAlreadyUpToDate: 'Kimi Code is already up to date ({{version}}).', + // sub/vis.ts + visStartFailed: 'Failed to start kimi vis: {{message}}', + visRunning: 'kimi vis is running at {{url}}', + visStopHint: 'Press Ctrl-C to stop.', + visBrowserFailed: 'Could not open a browser; visit {{url}} manually.', + // sub/export.ts + exportNoSession: 'No previous session found to export.', + exportCancelled: 'Export cancelled.', + exportConfirmPrompt: 'Export previous session "{{title}}"? [Y/n] ', + // sub/doctor.ts + doctorFileNotExist: 'File does not exist.', + doctorFileNotExistDefaults: 'File does not exist; built-in defaults will apply.', + doctorTitle: 'Kimi doctor', + doctorAllValid: 'All checked config files are valid.', + doctorFoundIssues: 'Kimi doctor found {{count}} issue{{plural}}.', + doctorInvalidConfig: 'Invalid configuration in {{path}}.', + doctorValidationIssues: 'Validation issues:', + // update/preflight.ts + updateUnsupportedPlatform: 'native ({{platform}}). Auto-update is not supported on this platform.', + updateUnsupportedManager: 'unsupported package manager or layout.', + updateNewerAvailable: 'A newer version of {{name}} is available ({{version}}).', + updateDetectedSource: 'Detected install source: {{source}}', + updateManualCommand: 'To update manually, run: {{command}}', + updateUpdated: 'Updated {{name}} to {{version}}. Restart the CLI to use the new version.', + updateUpdatedWithChangelog: 'Kimi Code updated to {{version}}\nChangelog: {{url}}', + // update/prompt.ts + updatePromptInstallNow: 'Install update now', + updatePromptContinue: 'Continue with current version', + updatePromptChangelog: 'View changelog: {{url}}', + updatePromptTitle: 'Kimi Code Update Available', + updatePromptNewer: '{{name}} has a newer release ready.', + updatePromptCurrent: 'Current', + updatePromptTarget: 'Target ', + updatePromptSource: 'Source ', + updatePromptCommand: 'Command', + updatePromptNavHint: '↑↓ choose · Enter confirm · Esc continue', + // run-prompt.ts + promptWarning: 'Warning: {{warning}}', + promptTurnEnded: 'Prompt turn ended with reason: {{reason}}', + // run-shell.ts + shellNothingToMigrate: ' Nothing to migrate from ~/.kimi/.', + shellBye: 'Bye!', + shellResumeHint: 'To resume this session: kimi -r {{sessionId}}', + // main.ts + mainError: 'error: {{message}}', + }, + messages: { + agentGroup: { + detachHint: 'Press Ctrl+B to run in background', + agentDefault: 'agent', + finishedWithType: '{{count}} {{type}} agents finished', + finished: '{{count}} agents finished', + runningWithBreakdown: 'Running {{count}} agents ({{breakdown}})', + running: 'Running {{count}} agents', + completed: '✓ Completed', + failed: '✗ Failed', + backgrounded: '◐ backgrounded', + waiting: 'Waiting', + runningLabel: 'Running', + starting: 'Starting', + noDescription: '(no description)', + errorPrefix: 'Error: {{error}}', + fallbackWaiting: 'Waiting to start…', + fallbackRunning: 'Still working…', + fallbackStarting: 'Starting…', + tool_one: '{{count}} tool', + tool_other: '{{count}} tools', + elapsedSeconds: '{{count}}s', + elapsedMinutes: '{{minutes}}m {{seconds}}s', + tokensM: '{{count}}M tok', + tokensK: '{{count}}k tok', + tokens: '{{count}} tok', + breakdownDone: '{{count}} done', + breakdownFailed: '{{count}} failed', + breakdownBackgrounded: '{{count}} backgrounded', + breakdownRunning: '{{count}} running', + breakdownWaiting: '{{count}} waiting', + breakdownStarting: '{{count}} starting', + }, + agentSwarmProgress: { + title: 'Agent Swarm', + orchestrating: 'Orchestrating...', + prompting: 'Prompting...', + working: 'Working...', + completed: 'Completed.', + failed: 'Failed.', + aborted: 'Aborted.', + cancelled: 'Cancelled.', + queued: 'Queued...', + rateLimited: 'Rate limited...', + resumed: '(resumed)', + phase: { + pending: 'Queued...', + queued: 'Queued...', + suspended: 'Rate limited...', + running: 'Running', + completed: 'Completed', + failed: 'Failed', + cancelled: 'Aborted', + }, + }, + backgroundAgentStatus: { + detail: ' ({{detail}})', + }, + cronMessage: { + missedTitle: 'Missed scheduled reminders', + firedTitle: 'Scheduled reminder fired', + job: 'job {{jobId}}', + oneShot: 'one-shot', + coalesced: '{{count}} fires coalesced', + missed: '{{count}} missed', + finalDelivery: 'final delivery', + }, + goalMarkers: { + pausedInterruption: "Goal paused due to user's interruption", + pausedByUser: 'Goal paused by the user.', + pausedByAgent: 'Goal paused by the agent.', + pausedWithReason: 'Goal paused: {{reason}}', + pausedWithLowerReason: 'Goal {{reason}}', + pausedGeneric: 'Goal paused', + resumedByUser: 'Goal resumed by the user.', + resumedByAgent: 'Goal resumed by the agent.', + resumedGeneric: 'Goal resumed', + blocked: 'Goal blocked', + }, + goalFormat: { + elapsedSeconds: '{{count}}s', + elapsedMinutes: '{{minutes}}m {{seconds}}s', + elapsedHours: '{{hours}}h {{minutes}}m', + }, + goalPanel: { + goalSet: 'Goal set', + upcomingAdded: 'Upcoming goal added. It will start after the current goal is complete.', + title: ' Goal · {{status}} ', + statusLabel: 'Status', + runningLabel: 'Running', + turnsLabel: 'Turns', + tokensLabel: 'Tokens', + stopLabel: 'Stop', + noStopCondition: 'No stop condition — runs until evaluated complete.', + stopTurns: 'after {{turnBudget}} turns ({{turnsUsed}}/{{turnBudget}})', + stopTokens: 'at {{tokenBudget}} tokens', + stopTime: 'after {{duration}}', + statusActive: 'active', + statusComplete: 'complete', + statusBlocked: 'blocked', + statusPaused: 'paused', + }, + planBox: { + fallback: ' plan ', + titlePrefix: ' plan: ', + }, + readGroup: { + readingFiles: 'Reading {{count}} files…', + readFiles: 'Read {{count}} files', + failed: 'failed', + line_one: '{{count}} line', + line_other: '{{count}} lines', + reading: 'reading…', + }, + toolCall: { + detachHint: 'Press Ctrl+B to run in background', + backgroundLost: 'Background agent lost (session restarted before completion)', + backgroundKilled: 'Background agent killed', + backgroundTimedOut: 'Background agent timed out', + backgroundFailed: 'Background agent failed', + tokenCount: '{{count}} tok', + byteSizeB: '{{count}} B', + byteSizeKB: '{{count}} KB', + byteSizeMB: '{{count}} MB', + elapsedSeconds: '{{count}}s', + elapsedMinutes: '{{minutes}}m {{seconds}}s', + subAgentDefault: 'SubAgent', + subAgentSuffix: 'Agent', + currentPlan: 'Current plan', + approved: 'Approved', + approvedWithOption: 'Approved: {{option}}', + rejected: 'Rejected', + suggestionLabel: '↪ Suggestion', + couldNotCollectInput: 'Could not collect your input', + startedBackgroundQuestion: 'Started background question', + collectedAnswers: 'Collected your answers', + startingBackgroundQuestion: 'Starting background question', + waitingForInput: 'Waiting for your input', + userDismissedQuestion: 'User dismissed the question.', + questionLabel: 'Q', + answerLabel: '→', + truncated: 'Truncated', + ranCommand: 'Ran a command', + runningCommand: 'Running a command', + used: 'Used', + using: 'Using', + toolDefault: 'Tool', + includeIgnored: 'include ignored', + mcpToolLabel: '{{toolName}} · MCP/{{serverName}}', + subagentWithName: 'subagent {{name}} ({{id}})', + subagentNoName: 'subagent ({{id}})', + moreToolCalls_one: '{{count}} more tool call', + moreToolCalls_other: '{{count}} more tool calls', + phaseQueued: 'queued', + phaseStarting: 'starting…', + phaseRunning: 'running', + phaseDone: 'done', + phaseFailed: 'failed', + phaseBackgrounded: 'backgrounded', + toolCount_one: '{{count}} tool', + toolCount_other: '{{count}} tools', + statusCompleted: 'Completed', + statusFailed: 'Failed', + statusRunning: 'Running', + statusBackgrounded: 'Backgrounded', + statusQueued: 'Queued', + statusStarting: 'Starting', + singleSubagentCompleted: 'Completed{{description}}{{stats}}', + argumentsTruncated: 'Tool call arguments truncated by max_tokens — call never executed.', + moreLinesHint: '... ({{remaining}} more lines, {{total}} total, ctrl+o to expand)', + preparingChanges: 'Preparing changes{{target}}... {{size}} · {{elapsed}} elapsed', + preparingChangesTarget: ' for {{filePath}}', + truncatedMarker: '[...truncated]', + agentSwarmLabel: 'Agent swarm: ', + discussionLabel: 'Discussion: ', + discussionSummary: 'Summary:', + completedStatus: '{{count}} completed', + failedStatus: '{{count}} failed', + abortedStatus: '{{count}} aborted', + completedPeriod: 'Completed.', + failedPeriod: 'Failed.', + abortedPeriod: 'Aborted.', + }, + mcpStatusPanel: { + noServers: 'No MCP servers configured. Run /mcp-config to add one.', + servers: 'Servers', + nameLabel: 'Name', + statusLabel: 'Status', + transportLabel: 'Transport', + toolsLabel: 'Tools', + errorLabel: 'error:', + actionLabel: 'action:', + actionLogin: 'run /mcp-config login {{name}}', + configureWith: 'Configure with', + status: { + connected: 'connected', + pending: 'pending', + needsAuth: 'needs auth', + failed: 'failed', + disabled: 'disabled', + }, + disabledToolCount: '—', + toolsAvailable: '{{count}} tools available', + tool_one: '{{count}} tool', + tool_other: '{{count}} tools', + }, + pluginCommand: { + invoked: '▶ Invoked command: ', + }, + pluginsStatusPanel: { + noPlugins: 'No plugins installed.', + installHint: 'Run /plugins to install one.', + enabled: 'enabled', + disabled: 'disabled', + status: 'Status:', + statePrefix: ' | state: ', + trust: 'Trust:', + officialDescription: '(Kimi-built and -maintained)', + curatedDescription: '(Kimi-reviewed, upstream-maintained)', + source: 'Source:', + root: 'Root:', + github: 'GitHub:', + installedSha: 'Installed SHA:', + originalSource: 'Original source:', + installedAt: 'Installed at:', + lastUpdated: 'Last updated:', + manifest: 'Manifest:', + manifestKind: '({{kind}})', + shadowed: 'Shadowed:', + sessionStart: 'Session start:', + skillInstructions: 'Skill instructions:', + skillInstructionsPresent: 'present', + skills: 'Skills ({{count}}):', + mcpServers: 'MCP servers ({{enabled}}/{{total}} enabled):', + mcpHint: 'Enabled by default; disable with /plugins mcp disable {{id}} .', + skillsLabel: 'skills:', + mcpCount: '{{enabled}}/{{total}} mcp', + diagnostics: 'Diagnostics:', + diagnosticsHint: ' | diagnostics: see /plugins info', + keywords: 'Keywords: {{keywords}}', + command: 'command:', + cwd: 'cwd:', + env: 'env:', + url: 'url:', + headers: 'headers:', + display: 'Display:', + by: 'by {{name}}', + }, + shellRun: { + backgrounded: 'Moved to background.', + outputUnavailable: '(output unavailable)', + running: 'Running…', + timing: '+{{extra}} lines ({{elapsed}}s)', + timingNoExtra: '({{elapsed}}s)', + hint: '(ctrl+b to run in background)', + }, + skillActivation: { + activated: '▶ Activated skill: ', + }, + statusPanel: { + title: '>_ {{productName}} (v{{version}})', + titlePrefix: '>_ {{productName}}', + titleVersion: '(v{{version}})', + modelLabel: 'Model', + directoryLabel: 'Directory', + permissionsLabel: 'Permissions', + planModeLabel: 'Plan mode', + sessionLabel: 'Session', + titleLabel: 'Title', + warningLabel: 'Warning', + modelNotSet: 'not set', + modelStatus: '{{model}} (thinking {{effort}})', + planModeOn: 'on', + planModeOff: 'off', + sessionNone: 'none', + contextWindow: 'Context window', + noContextData: 'No context window data available.', + }, + stepSummary: { + thinking_one: 'thinking {{count}} time', + thinking_other: 'thinking {{count}} times', + tool_one: 'call {{count}} tool', + tool_other: 'call {{count}} tools', + }, + swarmMarkers: { + activated: 'Swarm activated', + deactivated: 'Swarm deactivated', + ended: 'Swarm ended', + }, + thinking: { + liveLabel: 'thinking...', + expandHint: '... ({{count}} more lines, ctrl+o to expand)', + }, + usagePanel: { + title: ' Usage ', + sessionUsage: 'Session usage', + noTokenUsage: 'No token usage recorded yet.', + byModel: '{{model}} input {{input}} output {{output}} total {{total}}', + total: 'total', + totalLine: '{{label}} input {{input}} output {{output}} total {{total}}', + contextWindow: 'Context window', + contextBar: '{{pct}}% ({{tokens}} / {{maxTokens}})', + planUsage: 'Plan usage', + noUsageData: 'No usage data available.', + usageRow: '{{label}} {{bar}} {{pct}}% used{{reset}}', + extraUsage: 'Extra Usage', + usedThisMonth: 'Used this month', + monthlyLimit: 'Monthly limit', + unlimited: 'Unlimited', + balance: 'Balance', + }, + // tui/commands/config.ts + configEditorUnchanged: 'Editor unchanged: {{value}}', + configEditorAutoDetect: 'auto-detect', + configEditorSaveFailed: 'Failed to save editor: {{error}}', + configEditorSet: 'Editor set to "{{value}}".', + configEditorAutoSet: 'Editor set to auto-detect ($VISUAL / $EDITOR).', + configModelSwitched: 'Switched to {{name}} with thinking {{effort}}.', + configModelSwitchedSession: 'Switched to {{name}} with thinking {{effort}} for this session only.', + configThinkingSet: 'Thinking set to {{effort}}.', + configThinkingSetSession: 'Thinking set to {{effort}} for this session only.', + configModelSavedDefault: 'Saved {{name}} with thinking {{effort}} as default.', + configModelAlreadyUsing: 'Already using {{name}} with thinking {{effort}}.', + configThemeUnchanged: 'Theme unchanged: "{{theme}}".', + configThemeLoadFailed: 'Theme "{{theme}}" could not be loaded.', + configThemeSaveFailed: 'Failed to save theme: {{error}}', + configThemeSet: 'Theme set to "{{theme}}"{{detail}}.', + configAutoUpdateAlready: 'Automatic updates already {{state}}.', + configAutoUpdateSaveFailed: 'Failed to save automatic update setting: {{error}}', + configAutoUpdateSet: 'Automatic updates {{state}}.', + configAutoUpdateEnabled: 'enabled', + configAutoUpdateDisabled: 'disabled', + configPermissionUnchanged: 'Permission mode unchanged: {{mode}}.', + configPermissionMode: 'Permission mode: {{mode}}', + configLanguageUnchanged: 'Language unchanged: {{locale}}.', + configLanguageSaveFailed: 'Failed to save language preference: {{error}}', + configLanguageSet: 'Language set to {{locale}}. Restart required for full effect.', + configSkippedRefreshing: 'Skipped refreshing {{provider}}: {{reason}}', + configSkippedRefreshingModels: 'Skipped refreshing models: {{error}}', + // tui/commands/dispatch.ts + configInvalidSlashCommand: 'Invalid slash command: /{{name}}', + configVersionDisplay: 'Kimi Code v{{version}}', + configUnknownSlashCommand: 'Unknown slash command: /{{name}}', + // tui/commands/resolve.ts + resolveCannotWhileStreaming: 'Cannot /{{name}} while streaming — press Esc or Ctrl-C first.', + resolveCannotWhileCompacting: 'Cannot /{{name}} while compacting — wait for compaction to finish first.', + // tui/commands/btw.ts + btwStartFailed: 'Failed to start /btw: {{error}}', + // tui/commands/discuss.ts + discussSwarmEnableFailed: 'Failed to enable swarm mode: {{error}}', + // tui/commands/info.ts + infoMcpLoadFailed: 'Failed to load MCP servers: {{error}}', + // tui/commands/session.ts + sessionInitFailed: 'Init failed: {{msg}}', + // tui/commands/swarm.ts + swarmPermissionFailed: 'Failed to set permission mode: {{error}}', + swarmToggleFailed: 'Failed to {{action}} swarm mode: {{error}}', + swarmEnable: 'enable', + swarmDisable: 'disable', + // tui/controllers/btw-panel.ts + btwSendFailed: 'Failed to send /btw prompt: {{error}}', + btwCancelFailed: 'Failed to cancel /btw: {{error}}', + // tui/controllers/editor-keyboard.ts + editorExternalFailed: 'External editor failed: {{msg}}', + // tui/controllers/tasks-browser.ts + tasksLoadFailed: 'Failed to load tasks: {{error}}', + tasksOutputRefreshFailed: 'Output refresh failed: {{message}}', + tasksRefreshFailed: 'Refresh failed: {{error}}', + tasksRefreshing: 'Refreshing…', + tasksStopping: 'Stopping {{taskId}}…', + tasksStopFailed: 'Stop failed: {{message}}', + tasksCannotOpenOutput: 'Cannot open output: {{message}}', + // tui/controllers/clipboard-image-hint.ts + clipboardImageHint: 'Image in clipboard · {{shortcut}} to paste', + // tui/controllers/streaming-ui.ts + streamingTaskComplete: 'Kimi Code task complete', + // tui/kimi-tui.ts + kimiTuiError: 'Error: {{message}}', + kimiTuiApprovalRequired: 'Kimi Code approval required', + kimiTuiNeedsAnswer: 'Kimi Code needs your answer', + // tui/easter-eggs/dance.ts + danceOn: 'Dancing — use {{cmd}} to turn it off.', + danceOff: 'Use {{cmd}} to keep the rainbow on.', + // tui/constant/feedback.ts + feedbackSubmitting: 'Submitting feedback…', + feedbackUploading: 'Uploading attachments, this could take a few minutes…', + feedbackSubmitted: 'Feedback submitted, thank you!', + feedbackCancelled: 'Feedback cancelled.', + feedbackNetworkError: 'Network error, failed to submit feedback.', + feedbackOpeningGithub: 'Opening GitHub Issues as fallback…', + feedbackNotSignedIn: "You're not signed in. Opening GitHub Issues for feedback…", + feedbackSentUploadFailed: 'Feedback sent; attachment upload failed — see feedback-upload.log.', + feedbackHttpFailed: 'Failed to submit feedback (HTTP {{status}}).', + feedbackSession: 'Session: {{sessionId}}', + feedbackId: 'Feedback ID: {{id}}', + feedbackPersistHint: "If this persists, run `/export-debug-zip` and share the file with us for diagnosis. Please don't share it publicly.", + // tui/utils/goal-completion.ts + goalComplete: '✓ Goal complete{{reason}}.', + goalCompleteTurns: '{{count}} turn{{plural}}', + goalCompleteSummary: 'Worked {{turns}} over {{elapsed}}, using {{tokens}} tokens.', + // tui/utils/event-payload.ts + eventFilteredResponse: 'Provider filtered the response before visible output (finishReason={{reason}}{{raw}}).', + // tui/utils/background-task-status.ts + bgTaskAgent: 'agent task', + bgTaskQuestion: 'question task', + bgTaskBash: 'bash task', + bgTaskStarted: '{{subject}} started in background', + bgTaskCompleted: '{{subject}} completed in background', + bgTaskFailed: '{{subject}} failed in background', + bgTaskTimedOut: '{{subject}} timed out', + bgTaskStopped: '{{subject}} stopped', + bgTaskLost: '{{subject}} lost', + bgTaskStoppedReason: 'stopped — {{reason}}', + // tui/utils/background-agent-status.ts + bgAgentStarted: '{{subject}} started in background', + bgAgentCompleted: '{{subject}} completed in background', + bgAgentFailed: '{{subject}} failed in background', + // tui/utils/mcp-server-status.ts + mcpStatusFailed: '{{count}} failed', + mcpStatusNeedsAuth: '{{count}} need auth', + mcpStatusConnecting: '{{count}} connecting', + mcpStatusConnected: '{{count}} connected', + mcpStatusDisabled: '{{count}} disabled', + // tui/components/messages/tool-renderers/goal.ts + goalToolNoGoal: ' No current goal.', + goalToolStatus: 'Goal {{status}}: {{objective}}', + goalToolCouldNotStart: 'Could not start goal', + goalToolStarted: 'Started goal', + goalToolStarting: 'Starting goal', + goalToolCouldNotCheck: 'Could not check goal', + goalToolChecked: 'Checked goal', + goalToolChecking: 'Checking goal', + goalToolCouldNotSetBudget: 'Could not set goal budget', + goalToolSetBudget: 'Set goal budget', + goalToolSettingBudget: 'Setting goal budget', + goalToolCouldNotReport: 'Could not report goal{{suffix}}', + goalToolReported: 'Reported goal {{suffix}}', + goalToolReporting: 'Reporting goal{{suffix}}', + // tui/goal-queue-store.ts + goalQueueObjectiveEmpty: 'Goal objective cannot be empty', + goalQueueObjectiveTooLong: 'Goal objective cannot exceed {{max}} characters', + goalQueueNotFound: 'No queued goal found', + // tui/commands/registry.ts + registryGoalShow: 'Show the current goal', + registryGoalPause: 'Pause the active goal', + registryGoalResume: 'Resume a paused goal', + registryGoalCancel: 'Cancel and remove the current goal', + registryGoalReplace: 'Replace the current goal with a new objective', + registryGoalNext: 'Queue an upcoming goal', + registryGoalManage: 'Manage upcoming goals', + registrySwarmOn: 'Turn swarm mode on', + registrySwarmOff: 'Turn swarm mode off', + registryDiscussRoles: 'Specify participant roles', + registryAddDirShow: 'Show configured additional workspace directories', + }, + }, +} as const; diff --git a/apps/kimi-code/src/i18n/locales/zh.ts b/apps/kimi-code/src/i18n/locales/zh.ts new file mode 100644 index 0000000000..523f0e892b --- /dev/null +++ b/apps/kimi-code/src/i18n/locales/zh.ts @@ -0,0 +1,1612 @@ +export default { + meta: { + languageName: '中文', + }, + common: { + yes: '是', + no: '否', + ok: '确定', + cancel: '取消', + close: '关闭', + submit: '提交', + save: '保存', + edit: '编辑', + delete: '删除', + loading: '加载中...', + error: '错误', + warning: '警告', + info: '信息', + success: '成功', + optional: '可选', + required: '必填', + }, + cli: { + program: { + description: '下一代智能体的起点。', + helpOption: '显示帮助。', + usage: '[选项] [命令]', + documentation: '文档:', + }, + commandDescriptions: { + root: '开始交互式会话。', + new: '开始新的会话。', + continue: '继续最近的会话。', + session: '列出、搜索或切换会话。', + login: '通过设备码流程登录 Kimi Code CLI。', + logout: '登出并清除本地凭据。', + upgrade: '将 Kimi Code 升级到最新版本。', + plugins: '管理 Kimi Code 插件。', + provider: '非交互式管理 LLM 提供商。', + settings: '打开设置对话框。', + config: '读取或写入配置项。', + web: '启动 Web UI 服务。', + server: '启动或管理无头服务。', + exportCmd: '将会话导出为 ZIP 归档。', + doctor: '验证 Kimi Code 配置文件。', + doctorConfig: '验证 config.toml。', + doctorTui: '验证 tui.toml。', + acp: '运行 ACP 工具或检查器。', + vis: '打开可视化服务。', + migrate: '运行数据迁移,然后退出。', + serverRun: '启动 Kimi 服务器(后台守护进程;使用 --foreground 保持前台运行)。', + serverPs: '列出当前连接到 Kimi 服务器的客户端。', + serverKill: '停止运行中的 Kimi 服务器(优雅 API + 强制 PID 终止)。', + serverRotateToken: '生成新的持久化服务器令牌;旧令牌立即失效。', + providerAdd: '导入自定义注册表(api.json)中的所有提供商。', + providerRemove: '移除提供商及所有引用它的模型别名。', + providerList: '显示已配置的提供商及其模型数量。', + providerCatalog: '从公共 models.dev 目录发现并导入提供商。', + providerCatalogList: '列出目录中的提供商,或指定 providerId 时列出其模型。', + providerCatalogAdd: '按 ID 从目录中导入已知提供商。', + }, + optionDescriptions: { + session: '恢复会话。带 ID:恢复该会话;不带 ID:交互式选择。', + resume: '--session 的别名。', + continue: '继续当前工作目录的前一个会话。', + yolo: '自动批准所有操作。', + auto: '以自动权限模式启动。', + plan: '以计划模式启动。', + model: '本次调用使用的 LLM 模型别名。默认使用 config.toml 中的 default_model。', + prompt: '以非交互方式运行一条提示并打印回复。', + outputFormat: '提示模式的输出格式。默认为 text。', + skillsDir: '从指定目录加载 skills,替代自动发现的用户和项目目录。可重复指定。', + addDir: '为本会话添加额外的工作区目录。可重复指定。', + yes: '自动确认提示。', + autoApprove: '自动批准所有操作。', + exportOutput: '输出 ZIP 路径。', + exportYes: '跳过上一个会话的确认。', + exportSessionId: '要导出的会话 ID。默认为最近的会话。', + exportNoIncludeGlobalLog: '跳过打包活动全局诊断日志(~/.kimi-code/logs/kimi-code.log,不包含轮转的 .1 文件)。默认包含全局日志。', + acpLogin: '运行设备码登录流程后退出(ACP 终端认证的入口点)。', + visPort: '绑定的端口。默认自动选择一个空闲端口。', + visHost: '绑定的主机。默认:127.0.0.1。', + visNoOpen: '不自动打开浏览器。', + visSessionId: '直接打开到此会话。', + doctorConfigPath: '将此文件作为 config.toml 进行验证,替代默认路径。', + doctorTuiPath: '将此文件作为 tui.toml 进行验证,替代默认路径。', + serverPsJson: '将原始连接列表以 JSON 格式输出。', + serverRunOptionPort: '绑定的端口(默认:58627)。', + serverRunOptionHost: '绑定的主机。省略时绑定 127.0.0.1(仅本机);传入 --host 绑定 0.0.0.0(所有接口),或 --host 绑定指定地址。启动时会打印令牌。', + serverRunOptionAllowedHost: '允许通过 DNS 反绑检查的额外 Host 头值。可重复或逗号分隔;前导点号匹配域名后缀(例如 .example.com)。', + serverRunOptionKeepAlive: '保持服务器运行,不在无客户端连接 60 秒后退出。由 --host / --allowed-host 隐含启用,--foreground 模式下始终开启。', + serverRunOptionInsecureNoTls: '允许非回环绑定不使用 TLS 反向代理。默认为 true,仅对非回环绑定有效。', + serverRunOptionAllowRemoteShutdown: '在非回环绑定上保持 POST /api/v1/shutdown 启用(默认:禁用 → 404)。', + serverRunOptionAllowRemoteTerminals: '在非回环绑定上保持 PTY /api/v1/terminals/* 路由启用(默认:禁用 → 404)。远程 Shell 具有高风险。', + serverRunOptionDangerousBypassAuth: '禁用所有 REST 和 WebSocket 路由的 Bearer 令牌认证,并通过 /api/v1/meta 广播此状态。仅可在受信任的网络或自有认证代理后使用。', + serverRunOptionLogLevel: '服务器日志级别:fatal|error|warn|info|debug|trace|silent。省略则关闭日志。', + serverRunOptionDebugEndpoints: '挂载 /api/v1/debug/* 路由用于测试自省。默认关闭;生产环境请勿启用。', + serverRunOptionForeground: '在前台运行服务器并保持终端连接,直到 SIGINT/SIGTERM(不守护进程化)。', + serverRunOptionOpen: '服务器就绪后在默认浏览器中打开 Web UI。', + serverRunOptionNoOpen: '不在默认浏览器中打开 Web UI。', + providerApiKey: '注册表 API 密钥。回退到 KIMI_REGISTRY_API_KEY。', + providerListJson: '以 JSON 格式输出原始提供商/模型配置。', + providerCatalogFilter: '不区分大小写的 ID/名称子串过滤器。', + providerCatalogUrl: '覆盖目录 URL。默认为 {{url}}。', + providerCatalogJson: '以 JSON 格式输出匹配的目录片段。', + providerCatalogApiKey: '提供商的 API 密钥。回退到 KIMI_REGISTRY_API_KEY。', + providerCatalogDefaultModel: '将导入的模型标记为默认模型。', + }, + errors: { + unknownCommand: "未知命令 '{{command}}'。参见 '{{cliName}} --help'。", + promptEmpty: '提示不能为空。', + modelEmpty: '模型不能为空。', + outputFormatPromptOnly: '输出格式仅在提示模式下受支持。', + cannotCombinePromptAndYolo: '--prompt 不能与 --yolo 同时使用。', + cannotCombinePromptAndAuto: '--prompt 不能与 --auto 同时使用。', + cannotCombinePromptAndPlan: '--prompt 不能与 --plan 同时使用。', + sessionWithoutIdInPromptMode: '在提示模式下不能不带 ID 使用 --session。', + cannotCombineContinueAndSession: '--continue 不能与 --session 同时使用。', + cannotCombineYoloAndAuto: '--yolo 不能与 --auto 同时使用。', + }, + }, + startup: { + operations: { + runPrompt: '运行提示', + startShell: '启动 shell', + runMigration: '运行迁移', + runPluginNodeEntry: '运行插件节点入口', + upgrade: '升级', + }, + error: { + failedTo: '错误:无法 {{operation}}:{{message}}', + title: '错误:{{title}}', + messageLabel: '消息:', + seeLog: '查看日志:{{path}}', + }, + }, + tui: { + chrome: { + footer: { + context: '上下文:{{pct}}', + contextWithTokens: '上下文:{{pct}}({{tokens}}/{{maxTokens}})', + goalBadge: '[目标 {{dot}} {{status}} · {{elapsed}} · {{turns}}]', + statusActive: '进行中', + statusPaused: '已暂停', + statusBlocked: '已阻塞', + turn_one: '{{count}} 轮', + turn_other: '{{count}} 轮', + turnPlural: '轮', + task_one: '{{count}} 个任务运行中', + task_other: '{{count}} 个任务运行中', + agent_one: '{{count}} 个智能体运行中', + agent_other: '{{count}} 个智能体运行中', + thinking: ' 思考中', + thinkingEffort: ' 思考:{{effort}}', + elapsedSeconds: '{{count}}秒', + elapsedMinutes: '{{count}}分', + elapsedHours: '{{hours}}时{{minutes}}分', + }, + welcome: { + title: '欢迎使用 Kimi Code!', + loggedOutPrompt: '运行 /login 或 /provider 开始使用。', + helpPrompt: '发送 /help 查看帮助信息。', + modelNotSet: '未设置,运行 /login 或 /provider', + directory: '目录:', + session: '会话:', + model: '模型:', + version: '版本:', + mcp: 'MCP:', + }, + deviceCodeBox: { + title: '登录 Kimi Code', + prompt: '在浏览器中访问以下 URL 以授权:', + codeLabel: '验证码:', + hint: '按 Ctrl-C 取消', + }, + todoPanel: { + header: '待办', + collapseHint: '共 {{count}} 项 · ctrl+t 收起', + expandHint: '… +{{count}} 项更多{{distribution}} · ctrl+t 展开', + statusDone: '已完成', + statusInProgress: '进行中', + statusPending: '待处理', + }, + activityPane: { + tipPrefix: ' · 提示:{{tip}}', + }, + messages: { + cronMessage: { + missedTitle: '错过的定时提醒', + firedTitle: '定时提醒已触发', + job: '任务 {{jobId}}', + oneShot: '一次性', + coalesced: '合并了 {{count}} 次触发', + missed: '错过 {{count}} 次', + finalDelivery: '最终投递', + }, + }, + hints: { + ctrlDExit: '再按一次 Ctrl+D 退出', + ctrlCExit: '再按一次 Ctrl+C 退出', + llmNotSet: 'LLM 未设置,发送 "/login" 登录', + noActiveSession: '没有活动会话。发送 /login 登录。', + oauthLoginExpired: 'OAuth 登录已过期。发送 /login 登录。', + }, + tips: { + ctrlSAddGuidance: 'ctrl-s 可在本轮结束前追加指导', + tasksCheckProgress: '/tasks 查看后台任务的进度和状态', + initGenerateAgents: '/init:生成 AGENTS.md', + tryDance: '试试 /dance 解锁隐藏彩蛋', + pluginsSuperpowers: '/plugins:管理插件——试试 "superpowers" 插件', + pluginsKimiDatasource: + '/plugins:管理插件——试试 "Kimi Datasource",获取可靠的财经、经济与学术数据', + scheduleTasks: '让 Kimi 帮你安排任务,例如 "下午 5 点提醒我"', + sessionsBrowse: '/sessions 浏览并恢复之前的会话', + goalMultiStep: '/goal 适合有明确终点的多步骤工作', + goalNext: '/goal next 在当前目标继续运行时排队后续工作', + webUi: '/web:使用 Web UI 获得更好体验', + mentionFiles: '@:引用文件', + runShellCommand: '!:运行 shell 命令', + shiftEnterNewline: 'shift+enter:换行', + ctrlCCancel: 'ctrl+c:取消', + themeSwitch: '/theme 切换终端 UI 主题', + autoMode: '/auto 让 Kimi 自动处理审批并无人值守地继续', + yoloMode: '/yolo 跳过多数审批,适用于你信任的仓库批量操作', + helpCommands: '/help:显示命令', + compactContext: '/compact 在上下文变长时进行压缩', + ctrlOToolOutput: + 'ctrl-o 在简洁聊天视图与完整执行详情之间切换工具输出的显示', + shiftTabPlanMode: 'shift-tab 进入计划模式,在 Kimi 编辑文件前审阅方案', + modelSwitch: '/model:切换模型', + }, + }, + slashCommands: { + yolo: '切换 YOLO 模式:AI 自动批准安全操作,有风险的操作需你确认。', + auto: '切换 Auto 模式:自动运行所有操作,包括有风险的操作。', + permission: '选择权限模式', + settings: '打开 TUI 设置', + plan: '切换计划模式', + swarm: '切换 Swarm 模式或运行一个 swarm 任务', + discuss: '启动多 Agent 圆桌讨论', + model: '切换 LLM 模型', + effort: '切换思考模式', + provider: '管理 AI 提供商(添加/删除/刷新)', + btw: '向分叉的侧代理提问', + help: '显示可用命令和快捷键', + new: '在当前工作区开始新会话', + sessions: '浏览和恢复会话', + tasks: '浏览后台任务', + mcp: '显示 MCP 服务器状态', + plugins: '管理插件', + addDir: '添加或列出额外的工作区目录', + experiments: '管理实验功能', + reload: '重新加载会话并应用配置', + reloadTui: '仅重新加载 tui.toml UI 偏好', + compact: '压缩对话上下文', + goal: '开始或管理自主目标', + init: '分析代码库并生成 AGENTS.md', + fork: '复刻当前会话', + title: '设置或显示会话标题', + usage: '显示会话令牌数、上下文窗口和计划配额', + status: '显示当前会话和运行时状态', + feedback: '发送反馈以帮助改进 Kimi Code', + undo: '从记录中撤回最后一条提示', + editor: '设置外部编辑器(Ctrl-G)', + theme: '设置终端 UI 主题', + logout: '登出已配置的提供商', + login: '选择一个平台并进行身份验证', + exportMd: '将会话导出为 Markdown 文件', + exportDebugZip: '将会话导出为调试 ZIP 归档', + web: '在 Web UI 中打开当前会话并退出终端', + exit: '退出应用程序', + version: '显示版本信息', + }, + approvalLabels: { + approve: '批准', + approveOnce: '仅本次批准', + approveForSession: '本次会话内批准', + reject: '拒绝', + rejectWithFeedback: '拒绝并反馈', + revise: '修改', + }, + approvalDescriptions: { + startGoal: '启动一个目标?', + editFile: '编辑 {{path}}', + fileOp: '{{operation}} {{path}}', + stopTask: '停止任务:{{description}}', + spawnAgent: '生成 {{name}}', + invokeSkill: '调用技能 {{name}}', + fetchUrl: '获取 {{url}}', + searchQuery: '搜索:{{query}}', + updateTodoList: '更新待办列表({{count}} 项)', + backgroundTask: '{{status}} 任务 {{taskId}}:{{description}}', + taskStopBrief: '停止任务 {{taskId}}:{{description}}', + goalStartBrief: '启动目标:{{objective}}', + goalCompletionCriterion: '完成条件:{{criterion}}', + }, + dangerPatterns: { + recursiveDelete: '递归删除', + sudo: 'sudo', + pipeToShell: '管道至 shell', + ddWrite: 'dd 写入', + mkfs: 'mkfs', + writeToRawDevice: '写入裸设备', + chmod777: 'chmod 777', + forkBomb: 'fork 炸弹', + }, + dialogs: { + apiKeyInput: { + title: '为 {{platformName}} 输入 API 密钥', + emptyHint: 'API 密钥不能为空。', + footer: 'Enter 提交 · Esc 取消', + }, + approvalPreview: { + title: ' 预览 ', + footerKeys: '↑↓ 行 · PgUp/PgDn 页 · g/G 顶/底 · Q/Esc/Ctrl+E 取消', + }, + choicePicker: { + navHint: '↑↓ 导航 · ←→ 翻页 · Enter 选择 · Esc 取消', + searchHint: '(输入以搜索)', + searchLabel: '搜索:', + noMatches: '无匹配项', + page: '第 {{page}}/{{pageCount}} 页', + }, + compaction: { + compacting: '正在压缩上下文…', + complete: '压缩完成', + cancelled: '压缩已取消', + detailTokens: '({{before}} → {{after}} tokens)', + shortcutHint: '(Ctrl-O 以{{action}}压缩摘要)', + hide: '隐藏', + show: '显示', + tipPrefix: ' · 提示:{{tip}}', + }, + customRegistryImport: { + title: '导入自定义提供商注册表', + subtitleDefault: '粘贴 api.json URL 及其 Bearer token。', + subtitleUrlEmpty: '注册表 URL 不能为空。', + subtitleTokenEmpty: 'Bearer token 不能为空。', + footerNotLast: 'Tab / ↑↓ 切换 · Enter 下一项 · Esc 取消', + footerLast: 'Tab / ↑↓ 切换 · Enter 提交 · Esc 取消', + urlLabel: '注册表 URL', + tokenLabel: 'Bearer token', + }, + editorSelector: { + title: '选择外部编辑器', + vsCode: 'VS Code (code --wait)', + vim: 'Vim', + neovim: 'Neovim', + nano: 'Nano', + autoDetect: '自动检测($VISUAL / $EDITOR)', + }, + feedbackInput: { + title: '向 Kimi Code 发送反馈', + subtitleDefault: '告诉我们哪些功能好用,哪些不好用。', + subtitleEmpty: '反馈内容不能为空。', + footer: 'Enter 提交 · Esc 取消', + }, + goalQueueManager: { + title: '待启动目标', + navHint: '↑↓ 导航 · Space 选择 · E 编辑 · D 删除 · Esc 取消', + reorderHint: '↑↓ 排序 · Space 完成 · E 编辑 · D 删除 · Esc 取消', + empty: '没有待启动目标。', + selected: '已选择', + more: '▼ 还有 {{count}} 项', + }, + goalQueueEdit: { + title: '编辑待启动目标', + subtitle: '更新队列中的目标。', + errorEmpty: '目标描述不能为空。', + errorTooLong: '目标描述不能超过 {{max}} 个字符。', + footer: 'Enter 提交 · Shift-Enter/Ctrl-J 换行 · Esc 取消', + previous: '… {{count}} 项之前', + more: '… {{count}} 项更多', + }, + helpPanel: { + title: ' 帮助 ', + cancelHint: '· Esc / Enter / q 取消 · ↑↓ 滚动', + greeting: 'Kimi 已准备好为你提供帮助!发送消息即可开始。', + keyboardShortcuts: '键盘快捷键', + slashCommands: '斜杠命令', + showing: ' 显示 {{from}}-{{to}} / 共 {{total}} 项', + shortcuts: { + shiftTab: '切换计划模式', + ctrlG: '在外部编辑器中编辑($VISUAL / $EDITOR)', + ctrlO: '切换工具输出 / 压缩摘要展开', + ctrlT: '展开 / 收起待办列表(截断时)', + ctrlS: '干预——在流式输出中注入后续指令', + shiftEnter: '插入换行', + ctrlC: '中断流式输出 / 清空输入', + ctrlD: '退出(输入为空时)', + esc: '关闭对话框 / 中断流式输出', + arrowUpDown: '浏览输入历史', + enter: '提交', + }, + }, + modelSelector: { + title: '选择模型', + searchHint: '(输入以搜索)', + searchLabel: '搜索:', + hintTab: 'Tab 切换提供商', + hintNavigate: '↑↓ 导航', + hintBackspace: 'Backspace 清除', + hintSelect: 'Enter 选择', + hintSessionOnly: 'Alt+S 仅当前会话', + hintCancel: 'Esc 取消', + noMatches: '无匹配项', + count: '{{matches}} / {{total}}', + more: '▼ 还有 {{count}} 项', + thinking: '思考', + thinkingSwitchable: '思考(←→ 切换)', + thinkingAlwaysOn: '始终开启', + thinkingToggle: '可切换', + thinkingUnsupported: '不支持', + on: '开', + off: '关', + unsupported: '不支持', + }, + tabbedModelSelector: { + allTab: '全部', + }, + themeSelector: { + title: '选择主题', + auto: '自动(跟随终端)', + dark: '深色', + light: '浅色', + custom: '自定义:{{name}}', + }, + settingsSelector: { + title: '设置', + model: '模型', + modelDesc: '切换活动模型和思考模式。', + permission: '权限', + permissionDesc: '选择工具操作的审批方式。', + theme: '主题', + themeDesc: '更改终端 UI 主题。', + language: '语言', + languageDesc: '更改界面语言(需要重启)。', + editor: '编辑器', + editorDesc: '设置外部编辑器命令。', + experiments: '实验功能', + experimentsDesc: '开启或关闭实验功能。', + upgrade: '自动更新', + upgradeDesc: '开启或关闭 CLI 自动更新。', + usage: '用量', + usageDesc: '查看会话令牌数、上下文窗口和计划配额。', + }, + permissionSelector: { + title: '选择权限模式', + manual: '手动', + manualDesc: '自行审批每个操作。', + auto: '自动', + autoDesc: '自动运行所有操作(包括有风险的操作)。', + yolo: 'YOLO', + yoloDesc: 'AI 决定哪些操作需要你的审批。', + }, + localeSelector: { + title: '选择语言 / Select language', + enLabel: 'English', + enDesc: 'English interface language.', + zhLabel: '中文', + zhDesc: '中文界面语言。', + }, + approvalPanel: { + headerForBash: '运行此命令?', + headerForWrite: '写入此文件?', + headerForEdit: '应用这些修改?', + headerForTaskStop: '停止此任务?', + headerForExitPlanMode: '准备按此计划执行?', + headerForDefault: '批准 {{toolName}}?', + feedbackHint: '输入反馈 · ↵ 提交。', + navHint: '↑/↓ 选择 · {{numeric}} 选择 · ↵ 确认{{expand}}', + expandHint: ' · ctrl+e 预览', + previewHint: 'ctrl+e 预览', + moreLinesHidden_one: '… 还有 {{count}} 行隐藏 ({{previewHint}})', + moreLinesHidden_other: '… 还有 {{count}} 行隐藏 ({{previewHint}})', + }, + effortSelector: { + title: '选择思考模式', + hintSwitch: '←→ 切换', + hintSelect: 'Enter 选择', + hintSessionOnly: 'Alt+S 仅当前会话', + hintCancel: 'Esc 取消', + }, + experimentsSelector: { + title: '实验功能', + hintSpace: 'Space 切换', + hintEnter: 'Enter 应用', + hintPage: 'PgUp/PgDn 翻页', + hintCancel: 'Esc 取消', + hintNavigate: '↑↓ 导航', + hintBackspace: 'Backspace 清除', + applyButton: '[ 应用更改并重新加载 ]', + noChanges: '无更改', + changeCount_one: '已修改 {{count}} 项', + changeCount_other: '已修改 {{count}} 项', + statusEnabled: '已启用', + statusDisabled: '已禁用', + modifiedSuffix: ' · 已修改', + searchHint: '(输入以搜索)', + searchLabel: '搜索:', + noMatches: '无匹配项', + count: '{{matches}} / {{total}}', + more: '▼ 还有 {{count}} 项', + featureId: 'ID {{id}}', + sourceConfig: '配置', + sourceDefault: '默认', + lockedBy: '被 {{env}} 锁定', + lockedByMasterEnv: '被 KIMI_CODE_EXPERIMENTAL_FLAG 锁定', + }, + sessionPicker: { + titleCwd: '会话', + titleAll: '所有会话', + loading: '正在加载会话…', + empty: '未找到会话。', + scopeHintAll: 'Ctrl+A 所有会话', + scopeHintCwd: 'Ctrl+A 当前目录', + justNow: '刚刚', + minutesAgo: '{{minutes}}分钟前', + hoursAgo: '{{hours}}小时前', + daysAgo: '{{days}}天前', + searchLabel: '搜索:', + footerShowing: '显示 {{from}}-{{to}},共 {{totalSuffix}}', + footerLoadedMatches: '已加载 {{loaded}} / 匹配 {{total}} 项', + footerSessions: '{{count}} 个会话', + footerLoadedSessions: '已加载 {{loaded}} / 共 {{total}} 个会话', + }, + goalStartPermissionPrompt: { + titleYolo: '以 YOLO 模式启动目标?', + titleManual: '以审批模式启动目标?', + notice1: '手动模式下,Kimi Code 运行命令、编辑文件或执行其他有风险操作前会询问你。', + notice2: '手动模式不适合无人值守的目标工作。', + notice3: '你可以返回而不丢失命令。', + yoloNotice1: 'YOLO 模式会自动审批工具和计划变更。', + yoloNotice2: 'YOLO 模式仍可能因问题而暂停。', + yoloNotice3: '如果希望在目标工作期间跳过问题,请切换到 Auto 模式。', + optionAutoLabel: '切换到 Auto 并启动', + optionAutoDesc: '适合当你希望 Kimi Code 在你离开时继续工作。工具自动审批,问题被跳过。', + optionYoloLabel: '切换到 YOLO 并启动', + optionYoloDesc: '工具和计划变更自动审批。Kimi Code 仍可能向你提问。', + optionYoloKeepLabel: '保持 YOLO 并启动', + optionYoloKeepDesc: '工具和计划变更保持自动审批。Kimi Code 仍可能向你提问。', + optionManualLabel: '以手动模式启动', + optionManualDesc: '保持审批开启。Kimi Code 会在有风险操作前询问,因此目标可能会暂停并等待你。', + optionCancelLabel: '不启动', + optionCancelDesc: '返回输入框,保留你的目标命令。', + }, + swarmStartPermissionPrompt: { + title: '以审批模式启动集群任务?', + notice1: '手动模式下,Kimi Code 运行命令、编辑文件或执行其他有风险操作前会询问你。', + notice2: '手动模式可能会在智能体运行时阻塞集群工作。', + notice3: '你可以返回而不丢失命令。', + optionAutoLabel: '切换到 Auto 并启动', + optionAutoDesc: '最适合集群任务。工具自动审批,问题被跳过。', + optionYoloLabel: '切换到 YOLO 并启动', + optionYoloDesc: '工具和计划变更自动审批。Kimi Code 仍可能向你提问。', + optionManualLabel: '以手动模式启动', + optionManualDesc: '保持审批开启。Kimi Code 在集群任务期间可能会暂停并等待你。', + }, + startPermissionPrompt: { + navHint: '↑↓ 导航 · Enter 选择 · Esc 取消', + }, + taskOutputViewer: { + title: ' 任务输出 ', + noOutput: '[未捕获到输出]', + exitCode: '退出码 {{code}}', + status: { + running: '运行中', + completed: '已完成', + failed: '已失败', + timedOut: '已超时', + killed: '已终止', + lost: '已丢失', + }, + footer: { + line: '行', + page: '页', + topBottom: '顶/底', + cancel: '取消', + }, + }, + providerManager: { + title: '提供商', + headerHint: '↑↓ 导航 · D 删除 · Esc 取消', + addRowLabel: '[ 添加新平台 ]', + empty: '未配置任何提供商。', + deleteConfirmSingle: '删除平台 "{{label}}"?', + deleteConfirmMultiple: '删除平台 "{{label}}" 及其 {{count}} 个提供商?', + page: '第 {{page}}/{{pageCount}} 页', + }, + platformSelector: { + title: '选择平台', + kimiCode: 'Kimi Code(OAuth)', + }, + questionDialog: { + defaultOtherLabel: '其他', + notAnswered: '未回答', + reviewTitle: '提交前请检查你的答案', + submitPrompt: '准备好提交答案了吗?', + unansweredWarning: '还有问题未回答。', + submitTab: '提交', + title: ' 问题 ', + typeAnswerHint: '输入答案,然后按 Enter 保存。', + moreLines: '... 还有 {{count}} 行', + showing: '显示 {{from}}-{{to}} / 共 {{total}} 项', + questionPrefix: 'Q{{number}}', + hintEdit: { + typeAnswer: '输入答案', + save: '↵ 保存', + tabSwitch: 'tab 切换', + cancel: 'esc 取消', + }, + hintQuestion: { + navigate: '↑↓ 选择', + choose: '{{range}} / ↵ 选择', + toggle: '{{range}} / ↵ 切换', + tabSwitch: '←/→/tab 切换', + cancel: 'esc 取消', + }, + hintSubmit: { + navigate: '↑↓ 选择', + choose: '1/2 选择', + confirm: '↵ 确认', + tabSwitch: '←/→/tab 切换', + cancel: 'esc 取消', + }, + }, + pluginsSelector: { + backToInstalled: '返回已安装插件', + backToInstalledDesc: '返回本地插件管理器。', + removeConfirmTitle: '删除 {{name}}({{id}})?', + removeCancelLabel: '取消', + removeCancelDesc: '保留此插件。', + removeConfirmLabel: '删除插件', + removeConfirmDesc: '仅删除安装记录;插件文件保留在原位。', + installTrustTitle: '安装第三方插件 {{label}}?', + installTrustExitLabel: '退出', + installTrustExitDesc: '取消安装。', + installTrustTrustLabel: '信任并安装', + installTrustTrustDesc: '仍然安装此第三方插件。', + marketplaceTierOfficial: '官方插件', + marketplaceTierCurated: '精选插件', + marketplaceTierUnknown: '插件', + mcpNavHint: ' ↑↓ 导航 · Enter/Space 启用/禁用 · Esc 取消', + actionsSection: '操作', + tabInstalled: '已安装', + tabOfficial: '官方', + tabThirdParty: '第三方', + tabCustom: '自定义', + enterUpdate: 'Enter 更新', + enterDetails: 'Enter 详情', + removeConfirmHint: '↑↓ 导航 · Enter/Space 选择 · ←/Esc 取消', + installTrustHint: '↑↓ 导航 · Enter/Space 选择 · ←/Esc 取消', + installTrustNotice: '⚠️ 这是 Kimi 未审核的第三方插件。它可能捆绑 MCP 服务器、技能或可运行代码并访问工作区的文件。仅在信任来源时安装。', + mcpEnable: 'Enter/Space 启用', + mcpDisable: 'Enter/Space 禁用', + webBridgeDescription: '从 Kimi Code 控制你的真实浏览器——导航、点击、输入和截图', + statusEnabled: '已启用', + statusDisabled: '已禁用', + panelTitle: '插件', + mcpServersTitle: 'MCP 服务器 · {{name}}', + mcpServersSection: 'MCP 服务器({{enabled}}/{{total}} 已启用)', + noMcpServers: '未声明 MCP 服务器。', + noPluginsInstalled: '未安装插件。', + countInstalled: '已安装 {{count}} 个', + tabHintInstalled: ' Tab 切换 · Space 开关 · D 删除 · M MCP · {{enterAction}} · I 详情 · R 重载 · Esc 取消', + tabHintCustom: ' Tab 切换 · Enter 安装 · Esc 取消', + tabHintMarketplace: ' Tab 切换 · ↑↓ 导航 · Enter 打开/安装 · Esc 取消', + loadingMarketplace: '正在加载市场…', + marketplaceUnavailable: '市场不可用:{{message}}', + useCustomTabHint: '使用"自定义"标签页从 URL 安装。', + noPluginsFound: '未找到插件。', + marketplaceCount: '已安装 {{installed}} · 可用 {{available}}', + marketplaceSource: '来源:{{source}}', + openInBrowser: '在浏览器中打开', + installFromUrlHint: ' 从 GitHub URL(或 zip URL / 本地路径)安装:', + installingFromMarketplace: '正在从市场安装 {{label}}…', + pluginId: 'ID {{id}}', + skillCount_one: '{{count}} 个技能', + skillCount_other: '{{count}} 个技能', + mcpCount: 'MCP {{enabled}}/{{total}}', + diagnosticsAvailable: '诊断可用', + pluginState: '状态 {{state}}', + mcpServerTransportHint: '{{action}} · {{transport}} · {{target}}', + mcpServerStdioHint: '{{action}} · stdio · {{command}}', + mcpServerCwdSuffix: ' · 工作目录 {{cwd}}', + versionPrefix: 'v{{version}}', + installStatus: '安装', + installStatusVersion: '安装 v{{version}}', + updateStatus: '更新 {{local}} → {{latest}}', + installedStatus: '已安装', + installedStatusVersion: '已安装 · v{{version}}', + }, + tasksBrowser: { + noActiveTasks: '没有活跃任务。Tab = 显示全部。', + noBackgroundTasks: '此会话中没有后台任务。', + noTaskSelected: '未选择任务。', + statusRunning: '运行中', + statusCompleted: '已完成', + statusFailed: '失败', + statusTimedOut: '超时', + statusKilled: '已终止', + statusLost: '已丢失', + statusInterrupted: '已中断', + statusTotal: '总计', + stopLabel: '停止', + taskIdLabel: '任务 ID:', + statusLabel: '状态:', + descriptionLabel: '描述:', + agentTypeLabel: '智能体类型:', + toolCallLabel: '工具调用:', + exitCodeLabel: '退出码:', + reasonLabel: '原因:', + detailFrameTitle: '详情', + previewFrameTitle: '预览输出', + commandLabel: '命令:', + agentIdLabel: '智能体 ID:', + questionsLabel: '问题:', + timeLabel: '时间:', + pidLabel: 'PID:', + noDescription: '(无描述)', + loadingTail: '[加载中…]', + noOutputTail: '[无输出]', + tooSmall: '终端窗口太小(至少需要 {{minWidth}} × {{minHeight}})', + tasksFrameTitle: '任务 [{{filter}}]', + headerTitle: ' 任务浏览器 ', + filterAll: 'filter=全部', + filterActive: 'filter=活跃', + footerSelect: '选择', + footerOutput: '输出', + footerStop: '停止', + footerRefresh: '刷新', + footerFilter: '过滤', + footerConfirm: '确认', + footerCancel: '取消', + timingRunning: '运行了 ', + timingFinished: '结束于 ', + relativeTimeJustNow: '刚刚', + relativeTimeMins: '{{minutes}} 分钟前', + relativeTimeHours: '{{hours}} 小时前', + relativeTimeDays: '{{days}} 天前', + }, + undoSelector: { + navHint: '↑↓ 导航 · Enter 选择 · Esc 取消', + title: ' 选择要撤销的消息', + noMessages: ' 没有消息', + }, + queuePane: { + hintCompacting: ' ↑ 编辑 · 压缩后发送', + hintSteer: ' ↑ 编辑 · ctrl-s 立即引导', + hintAfterTask: ' ↑ 编辑 · 当前任务完成后发送', + }, + btwPanel: { + readyForSideQuestion: '准备进行旁路提问...', + waitingForAnswer: '等待回答...', + title: ' BTW ', + closeHint: 'Esc 关闭 ', + scrollHint: 'Esc 关闭 · ↑↓ 滚动 ', + questionPrefix: 'Q: ', + }, + updatePreferenceSelector: { + title: '自动更新', + on: '开启', + off: '关闭', + onDescription: '在后台安装新版本。', + offDescription: '显示安装提示。', + }, + }, + statusMessages: { + failedToSyncMcp: '同步 MCP 服务器状态失败:{{message}}', + turnStoppedFiltered: '本轮已停止:提供商安全策略拦截了响应。', + retryingStep: '重试中({{attempt}}/{{maxAttempts}}){{delayS}}秒后 — {{errorName}}', + policyBlocked: '提供商安全策略拦截了响应。', + outputFiltered: '模型输出已被过滤({{reason}})。', + maxTokensTruncated: '模型达到 max_tokens — 工具调用在运行前被截断。', + maxTokensNoToolCall: '模型达到 max_tokens — 未发出工具调用。', + maxTokensHint: '如果此限制对你的模型不正确,请在 kimi-code 配置中为模型别名设置 `max_output_size`。', + interruptedByUser: '用户中断', + stepMaxSteps: '达到每轮步骤限制(max_steps)', + stepInterrupted: '步骤中断({{reason}})', + goalBlocked: '目标已阻塞。', + goalBlockedDetail: '下一个排队的任务将在当前目标完成后启动。', + warningPrefix: '警告:{{message}}', + mcpServerConnected: 'MCP 服务器 "{{name}}" 已连接 · {{count}} 个工具({{transport}})', + mcpServerFailed: 'MCP 服务器 "{{name}}" 失败', + mcpServerFailedWithError: 'MCP 服务器 "{{name}}" 失败:{{error}}', + mcpServerNeedsAuth: 'MCP 服务器 "{{name}}" 需要 OAuth — 运行 /mcp-config login {{name}}', + mcpServerDisabled: 'MCP 服务器 "{{name}}" 已禁用', + mcpServerConnecting: 'MCP 服务器 "{{name}}" 正在连接…', + activatedSkill: '已激活技能:{{skillName}}', + // goal.ts + resumeGoalInput: '恢复活动目标。', + startingNow: '没有活动目标,立即开始此目标。', + provideObjective: '请提供目标描述,例如:`/goal 实现功能 X`。', + objectiveTooLong: '目标描述过长(最多 {{max}} 个字符)。请通过文件路径引用详细内容。', + provideNextObjective: + '请提供下一个目标描述,例如:`/goal next 实现功能 X`,或使用 `/goal next manage`。', + failedToInspectGoal: '检查当前目标失败:{{error}}', + failedToLoadUpcomingGoals: '加载待启动目标失败:{{error}}', + failedToUpdateUpcomingGoals: '更新待启动目标失败:{{error}}', + queuedGoalNoLongerExists: '队列中的目标已不存在。', + failedToReadUpcomingGoals: '读取待启动目标失败:{{error}}', + queuedGoalRemoveFailed: '排队目标已启动,但无法从队列中移除:{{error}}', + queuedGoalRestoreFailed: '排队目标无法恢复:{{error}}', + queuedGoalCancelFailed: '排队目标无法取消:{{error}}', + failedToUpdateUpcomingGoal: '更新待启动目标失败:{{error}}', + goalNotStarted: '目标未启动。', + failedToSetPermission: '设置权限模式失败:{{error}}', + goalAlreadyActive: '已有活动目标。请使用 `/goal replace ` 替换它,或使用 `/goal status` 查看状态。', + noGoalToPause: '没有要暂停的目标。', + goalPaused: '目标已暂停。使用 `/goal resume` 继续。', + noGoalToResume: '没有要恢复的目标。', + noGoalToCancel: '没有要取消的目标。', + goalCancelled: '目标已取消。', + noGoalSet: '未设置目标。使用 `/goal ` 开始一个目标。', + // auth.ts + loggedIn: '登录成功。', + loginCancelled: '登录已取消。', + loginFailed: '登录失败。', + authSuccessButConfigFailed: '认证成功,但刷新配置失败:{{error}}', + alreadyLoggedInRefreshed: '已登录。模型配置已刷新。', + loginFailedWithError: '登录失败:{{error}}', + failedToVerifyApiKey: '验证 API 密钥失败:{{error}}', + hintUseKimiCodeInstead: '提示:如果您的 API 密钥来自 Kimi Code,请选择 "Kimi Code"。', + noModelsForPlatform: '此平台没有可用模型。', + setupComplete: '设置完成:{{platformName}} · {{modelId}}', + kimiPlatformDisplay: 'Kimi 平台', + kimiPlatformDisplayWithHost: 'Kimi 平台({{host}})', + savedToLabel: '已保存到', + oauthLoginDescription: 'OAuth 登录', + nothingToLogout: '没有要登出的内容。', + loggedOutFrom: '已从 {{label}} 登出。', + // commands/config.ts + planCleared: '计划已清除', + planModeOn: '计划模式:开', + planWillBeCreatedHere: '计划将在以下位置创建:{{path}}', + planModeOff: '计划模式:关', + unknownPlanSubcommand: '未知的计划子命令:{{subcmd}}', + failedToSetPlanMode: '设置计划模式失败:{{msg}}', + yoloModeAlreadyOn: 'YOLO 模式已开启', + yoloModeOn: 'YOLO 模式:开', + yoloModeOnSub: 'AI 自动批准安全操作,有风险的操作需你确认。', + yoloModeAlreadyOff: 'YOLO 模式已关闭', + yoloModeOff: 'YOLO 模式:关', + noModelSelected: '未选择模型。请运行 /model 选择模型。', + unknownTheme: '未知主题:{{theme}}', + unknownModelAlias: '未知模型别名:{{alias}}', + unsupportedEffort: '{{alias}} 不支持的思考强度 "{{arg}"。可用:{{segments}}', + switchModelFailed: '切换模型失败:{{msg}}', + switchSavedButDefaultFailed: '已切换到 {{name}},但保存默认设置失败:{{msg}}', + loadExperimentsFailed: '加载实验性功能失败:{{error}}', + updateExperimentsFailed: '更新实验性功能失败:{{error}}', + setPermissionFailed: '设置权限模式失败:{{msg}}', + noEditorConfigured: '未配置编辑器。设置 $VISUAL / $EDITOR,或运行 /editor 。', + compactionCancelFailed: '取消压缩失败:{{message}}', + noActiveSession: '没有活动会话。', + autoModeAlreadyOn: 'Auto 模式已开启', + autoModeOn: 'Auto 模式:开', + autoModeOnSub: '自动运行所有操作,包括有风险的操作。', + autoModeAlreadyOff: 'Auto 模式已关闭', + autoModeOff: 'Auto 模式:关', + // kimi-tui.ts + modelsAdded_one: '{{providerName}} · +{{count}} 个模型。', + modelsAdded_other: '{{providerName}} · +{{count}} 个模型。', + skippedRefreshing: '跳过刷新 {{provider}}:{{reason}}', + warningLabel: '警告:{{warning}}', + noSessionsToContinue: '在 "{{workDir}}" 下没有可继续的会话,正在开始新会话。', + cannotSendWhileReplaying: '在回放会话历史时无法发送输入。', + noActiveSessionShell: '没有活动的会话可执行 shell 命令。', + shellCommandFailed: 'Shell 命令失败:{{message}}', + failedToCancelShell: '取消 shell 命令失败:{{message}}', + modelNoImageInput: '当前模型不支持图片输入。', + modelNoVideoInput: '当前模型不支持视频输入。', + failedToSend: '发送失败:{{message}}', + skillFailed: '技能 "{{skillName}}" 失败:{{message}}', + pluginCommandFailed: '命令 "{{pluginId}}:{{commandName}}" 失败:{{message}}', + failedToSteer: '引导失败:{{message}}', + resumeOtherWorkDir: '当前会话位于不同的工作目录中。\n 要恢复,请运行:{{command}}', + commandCopiedToClipboard: '命令已复制到剪贴板', + failedToCopyCommand: '复制命令到剪贴板失败', + alreadyOnSession: '已在此会话上。', + cannotSwitchWhileStreaming: '无法在流式传输时切换会话——请先按 Esc 或 Ctrl-C。', + cannotSwitchWhileReplaying: '无法在回放历史时切换会话。', + failedToResumeSession: '恢复会话 {{sessionId}} 失败:{{message}}', + resumedSession: '已恢复会话({{sessionId}})。', + failedToReplayHistory: '回放会话历史失败:{{message}}', + cannotStartNewWhileReplaying: '无法在回放历史时开始新会话。', + failedToStartNewSession: '开始新会话失败:{{message}}', + postCreateSetupFailed: '创建后设置失败:{{message}}', + startedNewSession: '已开始新会话({{sessionId}})。', + approvedForSession: '已批准当前会话', + approved: '已批准', + rejected: '已拒绝', + cancelled: '已取消', + waitingForAuthorization: '等待授权…', + working: '运行中…', + noShellCommandRunning: '没有 shell 命令在运行。', + commandStillStarting: '命令仍在启动中——请稍后重试。', + commandAlreadyFinished: '命令已完成。', + failedToMoveToBackground: '移至后台失败:{{message}}', + movedToBackground: '已移至后台。', + movedToBackgroundHint: '已移至后台。使用 /tasks 查看。', + noForegroundTaskRunning: '没有前台任务在运行。', + failedToListTasks: '列出任务失败:{{message}}', + failedToDetachTask: '分离任务 {{taskId}} 失败:{{message}}', + taskAlreadyFinished_one: '任务已完成。', + taskAlreadyFinished_other: '任务已完成。', + movedOneTaskToBackground: '已将 1 个任务移至后台。', + movedTasksToBackground: '已将 {{count}} 个任务移至后台。', + movedTasksOfToBackground: '已将 {{total}} 个任务中的 {{detached}} 个移至后台。', + tasksToView: '使用 /tasks 查看。', + failedToApplyStartupFlags: '应用启动标志失败:{{message}}', + compactionCancelled: '压缩已取消', + compactionComplete: '压缩完成', + goalSet: '目标已设置', + planSentBackForRevision: '计划已退回修改', + planReviewRejected: '计划审核未通过', + planReviewCancelled: '计划审核已取消', + replayGoalPaused: '目标已暂停', + replayGoalResumed: '目标已恢复', + replayGoalBlocked: '目标已阻塞', + replayGoalUpdated: '目标已更新', + replaySessionUnavailable: '此会话的会话历史不可用。', + replayFailed: '回放会话历史失败:{{message}}', + replayPlanModeOn: '计划模式:开', + replayPlanModeOff: '计划模式:关', + replayYoloModeOn: 'YOLO 模式:开', + replayYoloModeOnSub: '所有操作将自动批准。请谨慎使用。', + replayYoloModeOff: 'YOLO 模式:关', + replayPermissionMode: '权限模式:{{mode}}', + replaySkillActivated: '已激活技能:{{skillName}}', + noModelsConfigured: '未配置模型', + noModelsConfiguredSub: '运行 /login 登录 Kimi,或 /provider 从模型目录中添加另一个提供商。', + experimentalUpdated: '实验功能已更新。', + experimentalUpdatedSessionReloaded: '实验功能已更新。会话已重新加载。', + // commands/session.ts + sessionTitle: '会话标题:{{title}}', + sessionTitleNotSet: '会话标题:(未设置)— ID:{{sessionId}}', + sessionFailedToSetTitle: '设置标题失败:{{message}}', + sessionTitleSetTo: '会话标题已设置为:{{title}}', + sessionFailedToFork: '复刻会话失败:{{message}}', + sessionForked: '会话已复刻({{forkedId}})。返回原会话:kimi -r {{originalId}}', + sessionFailedToSwitchToForked: '切换到复刻会话失败:{{message}}', + sessionExportingMarkdown: '正在将会话导出为 Markdown…', + sessionNoMessagesToExport: '没有可导出的消息。', + sessionExportComplete: '已导出 {{count}} 条消息', + sessionFailedToExport: '导出会话失败:{{message}}', + sessionExportingDebug: '正在导出会话…', + sessionExportDebugComplete: '导出完成', + // commands/plugins.ts + pluginsUsageInstall: '用法:/plugins install <本地路径或 zip URL>', + pluginsUsageRemove: '用法:/plugins remove ', + pluginsUsageMcp: '用法:/plugins mcp enable|disable ', + pluginsUnknownAction: '未知的 /plugins 操作:{{action}}。运行 /plugins 交互式选择。', + pluginsCommandFailed: '/plugins {{action}} 失败:{{error}}', + pluginsFailedToLoad: '加载插件失败:{{error}}', + pluginsFailedToLoadMcp: '加载插件 MCP 服务器失败:{{error}}', + pluginsFailed: '/plugins 失败:{{error}}', + pluginsMcpFailed: '/plugins mcp 失败:{{error}}', + pluginsInstallCancelled: '安装已取消。', + pluginsInstallCancelledLabel: '安装已取消:{{label}}。', + pluginsInstallingFrom: '正在从 {{source}} 安装插件…', + pluginsInstallFinished: '安装完成——详见下方详情。', + pluginsInstallFailed: '安装失败:{{error}}', + pluginsInstallingOrUpdating: '正在从市场安装或更新 {{label}}…', + pluginsFailedToInstall: '安装 {{label}} 失败:{{error}}', + pluginsMcpEnabled: '已为 {{id}} 启用 MCP 服务器 {{server}}。运行 /reload 或 /new 生效。', + pluginsMcpDisabled: '已为 {{id}} 禁用 MCP 服务器 {{server}}。运行 /reload 或 /new 生效。', + pluginsEnabled: '已启用 {{id}}。运行 /reload 或 /new 生效。', + pluginsDisabled: '已禁用 {{id}}。运行 /reload 或 /new 生效。', + pluginsMcpDisabledHint: ' 部分 MCP 服务器已禁用;使用 /plugins mcp enable {{id}} 重新启用。', + pluginsInlineMcpDisabled: ' · MCP 服务器已禁用', + pluginsRemoveCancelled: '删除已取消:{{id}}。', + pluginsRemoved: '已删除 {{id}}。', + pluginsReloadHint: '运行 /new 或 /reload 以应用插件更改。', + pluginsInlineChangeHint: '运行 /reload 或 /new 生效', + pluginsOpeningPage: '正在浏览器中打开 {{label}} 页面…', + pluginsIfNotOpened: '如果未打开,请访问 {{url}}', + pluginsReloadResult: '重载:+{{added}} -{{removed}}', + pluginsReloadResultErrors: '({{count}} 个错误)', + pluginsDeclaresMcp_one: ' 声明了 {{count}} 个 MCP 服务器;默认启用,可从 /plugins 配置。', + pluginsDeclaresMcp_other: ' 声明了 {{count}} 个 MCP 服务器;默认启用,可从 /plugins 配置。', + pluginsInstalledDesc: '已安装 {{displayName}}{{version}} {{sourcePhrase}}', + pluginsUpdatedDesc: '已更新 {{displayName}}{{version}} {{sourcePhrase}}', + pluginsMigratedDesc: '已迁移 {{displayName}}:{{prevSource}} → {{source}}{{version}}', + pluginsFromSource: '来自 {{source}}', + pluginsViaSource: '通过 {{source}}', + // commands/add-dir.ts + addDirNoAdditionalDirs: '未配置额外目录。', + addDirTitle: '添加目录到工作区:{{path}}', + addDirHint: '↑↓ 导航 · Enter 确认 · Esc 取消', + addDirYesSession: '是,仅本次会话', + addDirYesRemember: '是,并记住此目录', + addDirNo: '否', + addDirDidNotAdd: '未将 {{path}} 添加为工作目录。', + addDirListHeader: '额外目录:', + addDirSuccessPersist: '已添加工作目录:\n {{path}}\n 已保存到:\n {{configPath}}', + addDirSuccessSession: '已添加工作目录:\n {{path}}\n 仅本次会话', + // commands/reload.ts + reloadTuiConfigReloaded: 'TUI 配置已重新加载。', + reloadSession: '会话已重新加载。', + reloadNoActiveSession: '运行时和 TUI 配置已重新加载;无活动会话。', + // commands/undo.ts + undoCannotWhileStreaming: '流式传输时无法撤销——请先按 Esc 或 Ctrl-C。', + undoUsage: '用法:/undo [count],count 为正整数。', + undoNothingToUndo: '没有可撤销的内容。', + undoFailed: '撤销失败:{{message}}', + undoLimit: '无法撤销 {{requested}};当前上下文中仅可撤销 {{max}} 项{{reason}}。', + undoLimitAfterCompaction: '(上次压缩之后)', + undoNothingToUndoAfterCompaction: '上次压缩之后没有可撤销的内容。', + undoUserMessage: '用户消息', + undoUserMessageWithImage: '用户消息({{count}} 张图片)', + undoSkillUnknown: '技能:未知', + // commands/web.ts + openInWebUi: '在 Web UI 中打开', + openInWebUiHint: '在浏览器中打开当前会话。', + continueLabel: '继续', + continueDescription: '退出终端并在浏览器中打开会话。', + cancelLabel: '取消', + stayInTui: '留在终端 UI。', + startingServerAndWebUi: '正在启动 Kimi 服务器并打开 Web UI…', + failedToStartServer: '启动服务器失败:{{error}}', + webOpenUrl: '打开 {{url}}', + webToken: '令牌: {{token}}', + // swarm.ts + swarmTaskNotStarted: '集群任务未启动。', + swarmModeAlreadyOn: 'Swarm 模式已开启。', + swarmModeAlreadyOff: 'Swarm 模式已关闭。', + swarmModeNotEnabled: 'Swarm 模式未启用。', + // sub/server/lifecycle.ts + serverInstallDesc: '将 Kimi 服务器安装为系统服务(launchd/systemd/schtasks)。', + serverPortOption: '绑定端口(默认 {{port}})', + serverLogLevelOption: '日志级别:{{levels}}(默认 {{default}})', + serverReinstallOption: '已安装时重新安装并覆盖', + serverNoOpenOption: '安装后不打开 Web UI。', + serverOutputJson: '以 JSON 格式输出', + serverUninstallDesc: '卸载 Kimi 服务器服务。', + serverStartDesc: '启动 Kimi 服务器服务。', + serverStopDesc: '停止 Kimi 服务器服务。', + serverRestartDesc: '重启 Kimi 服务器服务。', + serverStatusDesc: '显示 Kimi 服务器服务状态和连通性。', + serverStatusUrl: 'URL:{{url}}', + serverStatusState: '状态:{{state}}', + serverStatusLog: '日志:{{path}}', + serverStatusNote: '注意:{{note}}', + serverStatusRunning: '运行中', + serverStatusNotRunning: '未运行', + // sub/server/run.ts + serverReadyBanner: 'Kimi 服务器就绪', + serverReadyLocalUi: '本机可访问本地 Web UI。', + serverDangerAuthDisabled: '⚠ 危险:认证已禁用(--dangerous-bypass-auth)。', + serverDangerAnyoneAccess: '任何能访问此端口的人都将获得完全访问权限。仅在了解风险的情况下继续。', + serverDangerUnsure: '如果不确定,请运行 ', + serverDangerUnsureCmd: 'kimi server kill', + serverDangerUnsureSuffix: ' 立即停止此进程。', + serverUrl: 'Kimi 服务器:{{url}}', + // sub/server/kill.ts + serverKillNoRunning: '没有运行中的 Kimi 服务器。', + serverKillStopped: 'Kimi 服务器(PID {{pid}})已停止。', + serverKillKilled: 'Kimi 服务器(PID {{pid}})已终止。', + serverKillFailedPermissions: '停止 Kimi 服务器(PID {{pid}})失败;权限不足?', + // sub/server/daemon.ts + serverDaemonTimeout: 'Kimi 服务器守护进程在 {{ms}} 毫秒内未能启动。', + serverDaemonFailed: 'Kimi 服务器守护进程在启动期间{{reason}}。', + serverDaemonCheckLog: '查看日志了解详情:{{path}}', + serverDaemonLogTail: '最后几行日志({{path}}):', + // sub/server/rotate-token.ts + serverTokenRotated: '之前的令牌已失效。运行中的服务器会自动使用新令牌。', + serverNewToken: '新的服务器令牌:{{token}}', + // sub/server/access-urls.ts + serverAccessLocal: '本地: ', + serverAccessNetwork: '网络: ', + serverAccessUrl: 'URL: ', + // sub/server/shared.ts + serverInvalidIdleGrace: '错误:无效的 --idle-grace-ms 值:{{raw}}', + serverInvalidValue: '错误:无效的 {{label}} 值:{{raw}}', + serverNotServingWebUi: '{{origin}} 上的服务器未提供 Kimi Web UI{{reason}}。停止现有服务器并重新运行 `kimi server run`。', + // sub/server/ps.ts + serverPsNotResponding: '{{origin}} 上的 Kimi 服务器无响应。', + serverPsFailedHttp: '列出客户端失败:来自 {{origin}} 的 HTTP {{status}}。', + serverPsFailedMsg: '列出客户端失败:{{msg}}', + serverPsTimeout: '从 {{origin}} 列出客户端超时。', + // sub/login-flow.ts + loginOpeningBrowser: '正在打开浏览器进行 Kimi 设备登录:{{url}}', + loginPasteUrl: '如果浏览器未打开,请粘贴上方 URL 并输入验证码:{{code}}', + loginCodeExpires: '验证码将在 {{seconds}} 秒后过期。', + loginWaiting: '等待授权完成…', + loginSuccess: '已登录到 {{provider}}。', + loginFailedMsg: '登录失败:{{message}}', + // sub/provider.ts + providerMissingApiKey: '缺少 API 密钥。传入 --api-key 或设置 KIMI_REGISTRY_API_KEY。', + providerUrlRequired: '注册表 URL 不能为空。', + providerFetchFailed: '获取注册表失败{{suffix}}:{{error}}', + providerNoUsable: '{{url}} 上的注册表不包含可用的提供商。', + providerNotFound: '未找到提供商"{{id}}"。', + providerRemoved: '已移除提供商"{{id}}"。', + providerNoneConfigured: '未配置任何提供商。', + providerDefaultModel: '默认模型:{{model}}', + providerCatalogNotFound: '在 {{url}} 的目录中未找到提供商"{{id}}"。', + providerCatalogNoModels: '提供商"{{id}}"在此目录中没有可用模型。', + providerCatalogNoMatch: '目录中没有匹配"{{filter}}"的提供商。', + providerCatalogEmpty: '目录为空。', + providerCatalogUnsupported: '提供商"{{id}}"在目录中的传输类型不受支持。', + providerCatalogModelNotInProvider: '模型"{{model}}"不在提供商"{{id}}"中。运行"kimi provider catalog list {{id}}"查看可用 ID。', + providerImported: '已从 {{url}} 导入 {{name}}({{id}}),包含 {{count}} 个模型。', + providerDefaultSet: '默认模型已设置为 {{id}}/{{model}}。', + providerCatalogFetchFailed: '从 {{url}} 获取目录失败{{suffix}}:{{error}}', + // sub/upgrade.ts + upgradeCheckFailed: '错误:检查更新失败:{{reason}}', + upgradeAlreadyUpToDate: 'Kimi Code 已是最新版本({{version}})。', + // sub/vis.ts + visStartFailed: '启动 kimi vis 失败:{{message}}', + visRunning: 'kimi vis 正在运行:{{url}}', + visStopHint: '按 Ctrl-C 停止。', + visBrowserFailed: '无法打开浏览器;请手动访问 {{url}}。', + // sub/export.ts + exportNoSession: '没有可导出的上一个会话。', + exportCancelled: '导出已取消。', + exportConfirmPrompt: '导出上一个会话"{{title}}"?[Y/n] ', + // sub/doctor.ts + doctorFileNotExist: '文件不存在。', + doctorFileNotExistDefaults: '文件不存在;将使用内置默认值。', + doctorTitle: 'Kimi 诊断', + doctorAllValid: '所有检查的配置文件均有效。', + doctorFoundIssues: 'Kimi 诊断发现了 {{count}} 个问题。', + doctorInvalidConfig: '{{path}} 中的配置无效。', + doctorValidationIssues: '验证问题:', + // update/preflight.ts + updateUnsupportedPlatform: '原生({{platform}})。此平台不支持自动更新。', + updateUnsupportedManager: '不受支持的包管理器或布局。', + updateNewerAvailable: '{{name}} 有更新版本可用({{version}})。', + updateDetectedSource: '检测到安装来源:{{source}}', + updateManualCommand: '手动更新请运行:{{command}}', + updateUpdated: '已将 {{name}} 更新到 {{version}}。重启 CLI 以使用新版本。', + updateUpdatedWithChangelog: 'Kimi Code 已更新到 {{version}}\n更新日志:{{url}}', + // update/prompt.ts + updatePromptInstallNow: '立即安装更新', + updatePromptContinue: '继续使用当前版本', + updatePromptChangelog: '查看更新日志:{{url}}', + updatePromptTitle: 'Kimi Code 有可用更新', + updatePromptNewer: '{{name}} 有新版本可用。', + updatePromptCurrent: '当前', + updatePromptTarget: '目标 ', + updatePromptSource: '来源 ', + updatePromptCommand: '命令', + updatePromptNavHint: '↑↓ 选择 · Enter 确认 · Esc 继续', + // run-prompt.ts + promptWarning: '警告:{{warning}}', + promptTurnEnded: '提示轮次以原因结束:{{reason}}', + // run-shell.ts + shellNothingToMigrate: ' 没有需要从 ~/.kimi/ 迁移的内容。', + shellBye: '再见!', + shellResumeHint: '恢复此会话:kimi -r {{sessionId}}', + // main.ts + mainError: '错误:{{message}}', + }, + messages: { + agentGroup: { + detachHint: '按 Ctrl+B 在后台运行', + agentDefault: '智能体', + finishedWithType: '{{count}} 个 {{type}} 智能体已完成', + finished: '{{count}} 个智能体已完成', + runningWithBreakdown: '正在运行 {{count}} 个智能体({{breakdown}})', + running: '正在运行 {{count}} 个智能体', + completed: '✓ 已完成', + failed: '✗ 失败', + backgrounded: '◐ 已转入后台', + waiting: '等待中', + runningLabel: '运行中', + starting: '启动中', + noDescription: '(无描述)', + errorPrefix: '错误:{{error}}', + fallbackWaiting: '等待开始…', + fallbackRunning: '仍在运行…', + fallbackStarting: '启动中…', + tool_one: '{{count}} 个工具', + tool_other: '{{count}} 个工具', + elapsedSeconds: '{{count}} 秒', + elapsedMinutes: '{{minutes}} 分 {{seconds}} 秒', + tokensM: '{{count}}M tok', + tokensK: '{{count}}k tok', + tokens: '{{count}} tok', + breakdownDone: '{{count}} 完成', + breakdownFailed: '{{count}} 失败', + breakdownBackgrounded: '{{count}} 已转入后台', + breakdownRunning: '{{count}} 运行中', + breakdownWaiting: '{{count}} 等待中', + breakdownStarting: '{{count}} 启动中', + }, + agentSwarmProgress: { + title: '智能体集群', + orchestrating: '编排中…', + prompting: '提示中…', + working: '运行中…', + completed: '已完成。', + failed: '失败。', + aborted: '已中止。', + cancelled: '已取消。', + queued: '排队中…', + rateLimited: '速率限制中…', + resumed: '(已恢复)', + phase: { + pending: '排队中…', + queued: '排队中…', + suspended: '速率限制中…', + running: '运行中', + completed: '已完成', + failed: '失败', + cancelled: '已中止', + }, + }, + backgroundAgentStatus: { + detail: '({{detail}})', + }, + cronMessage: { + missedTitle: '错过的定时提醒', + firedTitle: '定时提醒已触发', + job: '任务 {{jobId}}', + oneShot: '一次性', + coalesced: '合并了 {{count}} 次触发', + missed: '错过了 {{count}} 次', + finalDelivery: '最终投递', + }, + goalMarkers: { + pausedInterruption: '目标因用户中断而暂停', + pausedByUser: '目标已由用户暂停。', + pausedByAgent: '目标已由智能体暂停。', + pausedWithReason: '目标已暂停:{{reason}}', + pausedWithLowerReason: '目标{{reason}}', + pausedGeneric: '目标已暂停', + resumedByUser: '目标已由用户恢复。', + resumedByAgent: '目标已由智能体恢复。', + resumedGeneric: '目标已恢复', + blocked: '目标已阻塞', + }, + goalFormat: { + elapsedSeconds: '{{count}}秒', + elapsedMinutes: '{{minutes}}分 {{seconds}}秒', + elapsedHours: '{{hours}}时 {{minutes}}分', + }, + goalPanel: { + goalSet: '目标已设置', + upcomingAdded: '待启动目标已添加。当前目标完成后将自动开始。', + title: ' 目标 · {{status}} ', + statusLabel: '状态', + runningLabel: '运行时间', + turnsLabel: '轮数', + tokensLabel: 'Tokens', + stopLabel: '停止条件', + noStopCondition: '无停止条件 — 将持续运行直到判定完成。', + stopTurns: '{{turnBudget}} 轮后({{turnsUsed}}/{{turnBudget}})', + stopTokens: '{{tokenBudget}} tokens 时', + stopTime: '{{duration}} 后', + statusActive: '进行中', + statusComplete: '已完成', + statusBlocked: '已阻塞', + statusPaused: '已暂停', + }, + planBox: { + fallback: ' 计划 ', + titlePrefix: ' 计划:', + }, + readGroup: { + readingFiles: '正在读取 {{count}} 个文件…', + readFiles: '已读取 {{count}} 个文件', + failed: '失败', + line_one: '{{count}} 行', + line_other: '{{count}} 行', + reading: '读取中…', + }, + toolCall: { + detachHint: '按 Ctrl+B 在后台运行', + backgroundLost: '后台智能体已丢失(会话在完成前已重启)', + backgroundKilled: '后台智能体已终止', + backgroundTimedOut: '后台智能体已超时', + backgroundFailed: '后台智能体失败', + tokenCount: '{{count}} tok', + byteSizeB: '{{count}} B', + byteSizeKB: '{{count}} KB', + byteSizeMB: '{{count}} MB', + elapsedSeconds: '{{count}}秒', + elapsedMinutes: '{{minutes}}分 {{seconds}}秒', + subAgentDefault: '子智能体', + subAgentSuffix: '智能体', + currentPlan: '当前计划', + approved: '已批准', + approvedWithOption: '已批准:{{option}}', + rejected: '已拒绝', + suggestionLabel: '↪ 建议', + couldNotCollectInput: '无法收集你的输入', + startedBackgroundQuestion: '已启动后台问题', + collectedAnswers: '已收集你的回答', + startingBackgroundQuestion: '正在启动后台问题', + waitingForInput: '等待你的输入', + userDismissedQuestion: '用户取消了该问题。', + questionLabel: 'Q', + answerLabel: '→', + truncated: '已截断', + ranCommand: '已执行命令', + runningCommand: '正在执行命令', + used: '已使用', + using: '正在使用', + toolDefault: '工具', + includeIgnored: '包含已忽略', + mcpToolLabel: '{{toolName}} · MCP/{{serverName}}', + subagentWithName: '子智能体 {{name}} ({{id}})', + subagentNoName: '子智能体 ({{id}})', + moreToolCalls_one: '{{count}} 个更多工具调用', + moreToolCalls_other: '{{count}} 个更多工具调用', + phaseQueued: '排队中', + phaseStarting: '启动中…', + phaseRunning: '运行中', + phaseDone: '完成', + phaseFailed: '失败', + phaseBackgrounded: '已转入后台', + toolCount_one: '{{count}} 个工具', + toolCount_other: '{{count}} 个工具', + statusCompleted: '已完成', + statusFailed: '失败', + statusRunning: '运行中', + statusBackgrounded: '已转入后台', + statusQueued: '排队中', + statusStarting: '启动中', + singleSubagentCompleted: '已完成{{description}}{{stats}}', + argumentsTruncated: '工具调用参数因 max_tokens 被截断 — 调用未执行。', + moreLinesHint: '... (还有 {{remaining}} 行,共 {{total}} 行,ctrl+o 展开)', + preparingChanges: '正在准备修改{{target}}... {{size}} · 已耗时 {{elapsed}}', + preparingChangesTarget: ':{{filePath}}', + truncatedMarker: '[...已截断]', + agentSwarmLabel: '智能体集群: ', + discussionLabel: '讨论: ', + discussionSummary: '总结:', + completedStatus: '{{count}} 已完成', + failedStatus: '{{count}} 失败', + abortedStatus: '{{count}} 已中止', + completedPeriod: '已完成。', + failedPeriod: '失败。', + abortedPeriod: '已中止。', + }, + mcpStatusPanel: { + noServers: '未配置 MCP 服务器。运行 /mcp-config 添加一个。', + servers: '服务器', + nameLabel: '名称', + statusLabel: '状态', + transportLabel: '传输', + toolsLabel: '工具', + errorLabel: '错误:', + actionLabel: '操作:', + actionLogin: '运行 /mcp-config login {{name}}', + configureWith: '配置方式', + status: { + connected: '已连接', + pending: '待处理', + needsAuth: '需要认证', + failed: '失败', + disabled: '已禁用', + }, + disabledToolCount: '—', + toolsAvailable: '{{count}} 个工具可用', + tool_one: '{{count}} 个工具', + tool_other: '{{count}} 个工具', + }, + pluginCommand: { + invoked: '▶ 已调用命令:', + }, + pluginsStatusPanel: { + noPlugins: '未安装插件。', + installHint: '运行 /plugins 安装一个。', + enabled: '已启用', + disabled: '已禁用', + status: '状态:', + statePrefix: ' | 状态:', + trust: '信任:', + officialDescription: '(Kimi 构建并维护)', + curatedDescription: '(Kimi 审核,上游维护)', + source: '来源:', + root: '根目录:', + github: 'GitHub:', + installedSha: '已安装 SHA:', + originalSource: '原始来源:', + installedAt: '安装时间:', + lastUpdated: '最后更新:', + manifest: '清单:', + manifestKind: '({{kind}})', + shadowed: 'Shadowed:', + sessionStart: '会话启动:', + skillInstructions: '技能说明:', + skillInstructionsPresent: '有', + skills: '技能({{count}}):', + mcpServers: 'MCP 服务器({{enabled}}/{{total}} 已启用):', + mcpHint: '默认已启用;使用 /plugins mcp disable {{id}} 禁用。', + skillsLabel: '技能:', + mcpCount: '{{enabled}}/{{total}} mcp', + diagnostics: '诊断:', + diagnosticsHint: ' | 诊断:参见 /plugins info', + keywords: '关键词:{{keywords}}', + command: '命令:', + cwd: '工作目录:', + env: '环境变量:', + url: 'URL:', + headers: '请求头:', + display: '显示:', + by: '由 {{name}}', + }, + shellRun: { + backgrounded: '已移至后台。', + outputUnavailable: '(输出不可用)', + running: '运行中…', + timing: '+{{extra}} 行({{elapsed}} 秒)', + timingNoExtra: '({{elapsed}} 秒)', + hint: '(ctrl+b 在后台运行)', + }, + skillActivation: { + activated: '▶ 已激活技能:', + }, + statusPanel: { + title: '>_ {{productName}}(v{{version}})', + titlePrefix: '>_ {{productName}}', + titleVersion: '(v{{version}})', + modelLabel: '模型', + directoryLabel: '目录', + permissionsLabel: '权限', + planModeLabel: '计划模式', + sessionLabel: '会话', + titleLabel: '标题', + warningLabel: '警告', + modelNotSet: '未设置', + modelStatus: '{{model}}(思考 {{effort}})', + planModeOn: '开', + planModeOff: '关', + sessionNone: '无', + contextWindow: '上下文窗口', + noContextData: '无上下文窗口数据。', + }, + stepSummary: { + thinking_one: '思考了 {{count}} 次', + thinking_other: '思考了 {{count}} 次', + tool_one: '调用了 {{count}} 个工具', + tool_other: '调用了 {{count}} 个工具', + }, + swarmMarkers: { + activated: '集群已激活', + deactivated: '集群已停用', + ended: '集群已结束', + }, + thinking: { + liveLabel: '思考中…', + expandHint: '…(还有 {{count}} 行,ctrl+o 展开)', + }, + usagePanel: { + title: ' 用量 ', + sessionUsage: '会话用量', + noTokenUsage: '暂无 token 使用记录。', + byModel: '{{model}} 输入 {{input}} 输出 {{output}} 总计 {{total}}', + total: '总计', + totalLine: '{{label}} 输入 {{input}} 输出 {{output}} 总计 {{total}}', + contextWindow: '上下文窗口', + contextBar: '{{pct}}%({{tokens}} / {{maxTokens}})', + planUsage: '套餐用量', + noUsageData: '无可用用量数据。', + usageRow: '{{label}} {{bar}} {{pct}}% 已用{{reset}}', + extraUsage: '额外用量', + usedThisMonth: '本月已用', + monthlyLimit: '月度上限', + unlimited: '无限制', + balance: '余额', + }, + // tui/commands/config.ts + configEditorUnchanged: '编辑器未更改:{{value}}', + configEditorAutoDetect: '自动检测', + configEditorSaveFailed: '保存编辑器失败:{{error}}', + configEditorSet: '编辑器已设置为"{{value}}"。', + configEditorAutoSet: '编辑器已设置为自动检测($VISUAL / $EDITOR)。', + configModelSwitched: '已切换到 {{name}},思考模式 {{effort}}。', + configModelSwitchedSession: '已切换到 {{name}},思考模式 {{effort}}(仅本次会话)。', + configThinkingSet: '思考模式已设置为 {{effort}}。', + configThinkingSetSession: '思考模式已设置为 {{effort}}(仅本次会话)。', + configModelSavedDefault: '已将 {{name}}(思考 {{effort}})保存为默认。', + configModelAlreadyUsing: '已在使用 {{name}},思考模式 {{effort}}。', + configThemeUnchanged: '主题未更改:"{{theme}}"。', + configThemeLoadFailed: '无法加载主题"{{theme}}"。', + configThemeSaveFailed: '保存主题失败:{{error}}', + configThemeSet: '主题已设置为"{{theme}}"{{detail}}。', + configAutoUpdateAlready: '自动更新已{{state}}。', + configAutoUpdateSaveFailed: '保存自动更新设置失败:{{error}}', + configAutoUpdateSet: '自动更新已{{state}}。', + configAutoUpdateEnabled: '开启', + configAutoUpdateDisabled: '关闭', + configPermissionUnchanged: '权限模式未更改:{{mode}}。', + configPermissionMode: '权限模式:{{mode}}', + configLanguageUnchanged: '语言未更改:{{locale}}。', + configLanguageSaveFailed: '保存语言偏好失败:{{error}}', + configLanguageSet: '语言已设置为 {{locale}}。需重启以完全生效。', + configSkippedRefreshing: '跳过刷新 {{provider}}:{{reason}}', + configSkippedRefreshingModels: '跳过刷新模型:{{error}}', + // tui/commands/dispatch.ts + configInvalidSlashCommand: '无效的斜杠命令:/{{name}}', + configVersionDisplay: 'Kimi Code v{{version}}', + configUnknownSlashCommand: '未知的斜杠命令:/{{name}}', + // tui/commands/resolve.ts + resolveCannotWhileStreaming: '流式传输时无法使用 /{{name}}——请先按 Esc 或 Ctrl-C。', + resolveCannotWhileCompacting: '压缩时无法使用 /{{name}}——请等待压缩完成。', + // tui/commands/btw.ts + btwStartFailed: '启动 /btw 失败:{{error}}', + // tui/commands/discuss.ts + discussSwarmEnableFailed: '启用集群模式失败:{{error}}', + // tui/commands/info.ts + infoMcpLoadFailed: '加载 MCP 服务器失败:{{error}}', + // tui/commands/session.ts + sessionInitFailed: '初始化失败:{{msg}}', + // tui/commands/swarm.ts + swarmPermissionFailed: '设置权限模式失败:{{error}}', + swarmToggleFailed: '{{action}}集群模式失败:{{error}}', + swarmEnable: '启用', + swarmDisable: '禁用', + // tui/controllers/btw-panel.ts + btwSendFailed: '发送 /btw 提示失败:{{error}}', + btwCancelFailed: '取消 /btw 失败:{{error}}', + // tui/controllers/editor-keyboard.ts + editorExternalFailed: '外部编辑器失败:{{msg}}', + // tui/controllers/tasks-browser.ts + tasksLoadFailed: '加载任务失败:{{error}}', + tasksOutputRefreshFailed: '输出刷新失败:{{message}}', + tasksRefreshFailed: '刷新失败:{{error}}', + tasksRefreshing: '刷新中…', + tasksStopping: '正在停止 {{taskId}}…', + tasksStopFailed: '停止失败:{{message}}', + tasksCannotOpenOutput: '无法打开输出:{{message}}', + // tui/controllers/clipboard-image-hint.ts + clipboardImageHint: '剪贴板中有图片 · {{shortcut}} 粘贴', + // tui/controllers/streaming-ui.ts + streamingTaskComplete: 'Kimi Code 任务完成', + // tui/kimi-tui.ts + kimiTuiError: '错误:{{message}}', + kimiTuiApprovalRequired: 'Kimi Code 需要你的批准', + kimiTuiNeedsAnswer: 'Kimi Code 需要你的回答', + // tui/easter-eggs/dance.ts + danceOn: '跳舞中——使用 {{cmd}} 关闭。', + danceOff: '使用 {{cmd}} 保持彩虹效果。', + // tui/constant/feedback.ts + feedbackSubmitting: '正在提交反馈…', + feedbackUploading: '正在上传附件,可能需要几分钟…', + feedbackSubmitted: '反馈已提交,谢谢!', + feedbackCancelled: '反馈已取消。', + feedbackNetworkError: '网络错误,提交反馈失败。', + feedbackOpeningGithub: '正在打开 GitHub Issues 作为备选…', + feedbackNotSignedIn: '你尚未登录。正在打开 GitHub Issues 提交反馈…', + feedbackSentUploadFailed: '反馈已发送;附件上传失败——参见 feedback-upload.log。', + feedbackHttpFailed: '提交反馈失败(HTTP {{status}})。', + feedbackSession: '会话:{{sessionId}}', + feedbackId: '反馈 ID:{{id}}', + feedbackPersistHint: '如果持续出现,运行 `/export-debug-zip` 并与我们分享文件以供诊断。请勿公开分享。', + // tui/utils/goal-completion.ts + goalComplete: '✓ 目标完成{{reason}}。', + goalCompleteTurns: '{{count}} 轮', + goalCompleteSummary: '在 {{elapsed}} 内完成了 {{turns}},使用了 {{tokens}} 个 token。', + // tui/utils/event-payload.ts + eventFilteredResponse: '提供商在可见输出之前过滤了响应(finishReason={{reason}}{{raw}})。', + // tui/utils/background-task-status.ts + bgTaskAgent: '智能体任务', + bgTaskQuestion: '问题任务', + bgTaskBash: 'Shell 任务', + bgTaskStarted: '{{subject}} 已在后台启动', + bgTaskCompleted: '{{subject}} 已在后台完成', + bgTaskFailed: '{{subject}} 在后台失败', + bgTaskTimedOut: '{{subject}} 已超时', + bgTaskStopped: '{{subject}} 已停止', + bgTaskLost: '{{subject}} 已丢失', + bgTaskStoppedReason: '已停止——{{reason}}', + // tui/utils/background-agent-status.ts + bgAgentStarted: '{{subject}} 已在后台启动', + bgAgentCompleted: '{{subject}} 已在后台完成', + bgAgentFailed: '{{subject}} 在后台失败', + // tui/utils/mcp-server-status.ts + mcpStatusFailed: '{{count}} 个失败', + mcpStatusNeedsAuth: '{{count}} 个需要认证', + mcpStatusConnecting: '{{count}} 个连接中', + mcpStatusConnected: '{{count}} 个已连接', + mcpStatusDisabled: '{{count}} 个已禁用', + // tui/components/messages/tool-renderers/goal.ts + goalToolNoGoal: ' 无当前目标。', + goalToolStatus: '目标 {{status}}:{{objective}}', + goalToolCouldNotStart: '无法启动目标', + goalToolStarted: '已启动目标', + goalToolStarting: '正在启动目标', + goalToolCouldNotCheck: '无法检查目标', + goalToolChecked: '已检查目标', + goalToolChecking: '正在检查目标', + goalToolCouldNotSetBudget: '无法设置目标预算', + goalToolSetBudget: '已设置目标预算', + goalToolSettingBudget: '正在设置目标预算', + goalToolCouldNotReport: '无法报告目标{{suffix}}', + goalToolReported: '已报告目标{{suffix}}', + goalToolReporting: '正在报告目标{{suffix}}', + // tui/goal-queue-store.ts + goalQueueObjectiveEmpty: '目标描述不能为空', + goalQueueObjectiveTooLong: '目标描述不能超过 {{max}} 个字符', + goalQueueNotFound: '未找到排队的目标', + // tui/commands/registry.ts + registryGoalShow: '显示当前目标', + registryGoalPause: '暂停活动目标', + registryGoalResume: '恢复已暂停的目标', + registryGoalCancel: '取消并移除当前目标', + registryGoalReplace: '用新目标替换当前目标', + registryGoalNext: '排队下一个目标', + registryGoalManage: '管理待启动目标', + registrySwarmOn: '开启集群模式', + registrySwarmOff: '关闭集群模式', + registryDiscussRoles: '指定参与者角色', + registryAddDirShow: '显示已配置的额外工作目录', + }, + }, +} as const; diff --git a/apps/kimi-code/src/main.ts b/apps/kimi-code/src/main.ts index 860c4423d8..c81eff083a 100644 --- a/apps/kimi-code/src/main.ts +++ b/apps/kimi-code/src/main.ts @@ -27,6 +27,7 @@ import { finalizeHeadlessRun } from './cli/headless-exit'; import type { CLIOptions } from './cli/options'; import { OptionConflictError, validateOptions } from './cli/options'; import { runPrompt } from './cli/run-prompt'; +import { t } from '#/i18n'; import { runShell } from './cli/run-shell'; import { formatStartupError } from './cli/startup-error'; import { runPluginNodeEntry } from './cli/sub/plugin-run-node'; @@ -60,7 +61,7 @@ export async function handleMainCommand( validated = validateOptions(opts); } catch (error) { if (error instanceof OptionConflictError) { - process.stderr.write(`error: ${error.message}\n`); + process.stderr.write(t('tui.statusMessages.mainError', { message: error.message }) + '\n'); process.exit(1); } throw error; @@ -182,37 +183,48 @@ export function main(): void { // would then exit 0 nondeterministically. Setting `process.exitCode` // up front makes that drain-exit report failure too. process.exitCode = 1; - const operation = opts.prompt !== undefined ? 'run prompt' : 'start shell'; + const operation = + opts.prompt !== undefined + ? t('startup.operations.runPrompt') + : t('startup.operations.startShell'); await logStartupFailure(operation, error); process.stderr.write( formatStartupError(error, { operation, }), ); - process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`); + process.stderr.write( + `${t('startup.error.seeLog', { path: resolveGlobalLogPath(resolveKimiHome()) })}\n`, + ); process.exit(1); }); }, () => { void handleMigrateCommand(version).catch(async (error: unknown) => { - await logStartupFailure('run migration', error); - process.stderr.write(formatStartupError(error, { operation: 'run migration' })); - process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`); + const operation = t('startup.operations.runMigration'); + await logStartupFailure(operation, error); + process.stderr.write(formatStartupError(error, { operation })); + process.stderr.write( + `${t('startup.error.seeLog', { path: resolveGlobalLogPath(resolveKimiHome()) })}\n`, + ); process.exit(1); }); }, (entry, args) => { void runPluginNodeEntry(entry, args).catch(async (error: unknown) => { - await logStartupFailure('run plugin node entry', error); - process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + await logStartupFailure(t('startup.operations.runPluginNodeEntry'), error); + process.stderr.write(t('tui.statusMessages.mainError', { message: error instanceof Error ? error.message : String(error) }) + '\n'); process.exit(1); }); }, () => { void handleUpgradeCommand(version).catch(async (error: unknown) => { - await logStartupFailure('upgrade', error); - process.stderr.write(formatStartupError(error, { operation: 'upgrade' })); - process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`); + const operation = t('startup.operations.upgrade'); + await logStartupFailure(operation, error); + process.stderr.write(formatStartupError(error, { operation })); + process.stderr.write( + `${t('startup.error.seeLog', { path: resolveGlobalLogPath(resolveKimiHome()) })}\n`, + ); process.exit(1); }); }, diff --git a/apps/kimi-code/src/tui/commands/add-dir.ts b/apps/kimi-code/src/tui/commands/add-dir.ts index 90636cea55..2dc8c47fbc 100644 --- a/apps/kimi-code/src/tui/commands/add-dir.ts +++ b/apps/kimi-code/src/tui/commands/add-dir.ts @@ -1,4 +1,5 @@ -import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { t } from '#/i18n'; +import { getNoActiveSessionMessage } from '../constant/kimi-tui'; import { ChoicePickerComponent } from '../components/dialogs/choice-picker'; import type { SlashCommandHost } from './dispatch'; @@ -11,7 +12,7 @@ export async function handleAddDirCommand(host: SlashCommandHost, args: string): if (input.length === 0 || input.toLowerCase() === 'list') { const additionalDirs = session?.summary?.additionalDirs ?? []; if (additionalDirs.length === 0) { - host.showStatus('No additional directories configured.'); + host.showStatus(t('tui.statusMessages.addDirNoAdditionalDirs')); return; } host.showStatus(formatAdditionalDirsStatus(additionalDirs)); @@ -19,26 +20,26 @@ export async function handleAddDirCommand(host: SlashCommandHost, args: string): } if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } host.mountEditorReplacement( new ChoicePickerComponent({ - title: `Add directory to workspace: ${input}`, - hint: '↑↓ navigate · Enter confirm · Esc cancel', + title: t('tui.statusMessages.addDirTitle', { path: input }), + hint: t('tui.statusMessages.addDirHint'), options: [ { value: 'session', - label: 'Yes, for this session', + label: t('tui.statusMessages.addDirYesSession'), }, { value: 'remember', - label: 'Yes, and remember this directory', + label: t('tui.statusMessages.addDirYesRemember'), }, { value: 'cancel', - label: 'No', + label: t('tui.statusMessages.addDirNo'), }, ], onSelect: (value) => { @@ -46,14 +47,14 @@ export async function handleAddDirCommand(host: SlashCommandHost, args: string): }, onCancel: () => { host.restoreEditor(); - host.showStatus(`Did not add ${input} as a working directory.`); + host.showStatus(t('tui.statusMessages.addDirDidNotAdd', { path: input })); }, }), ); } function formatAdditionalDirsStatus(additionalDirs: readonly string[]): string { - return ['Additional directories:', ...additionalDirs.map((dir) => ` ${dir}`)].join('\n'); + return [t('tui.statusMessages.addDirListHeader'), ...additionalDirs.map((dir) => ` ${dir}`)].join('\n'); } async function handleAddDirChoice( @@ -65,13 +66,13 @@ async function handleAddDirChoice( host.restoreEditor(); if (choice === 'cancel') { - host.showStatus(`Did not add ${path} as a working directory.`); + host.showStatus(t('tui.statusMessages.addDirDidNotAdd', { path })); return; } const session = host.session; if (session === undefined || session.id !== sessionId) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -81,8 +82,8 @@ async function handleAddDirChoice( host.refreshSlashCommandAutocomplete(); host.showStatus( choice === 'remember' - ? `Added workspace directory:\n ${path}\n Saved to:\n ${result.configPath}` - : `Added workspace directory:\n ${path}\n For this session only`, + ? t('tui.statusMessages.addDirSuccessPersist', { path, configPath: result.configPath }) + : t('tui.statusMessages.addDirSuccessSession', { path }), 'success', ); } catch (error) { diff --git a/apps/kimi-code/src/tui/commands/auth.ts b/apps/kimi-code/src/tui/commands/auth.ts index 773638485f..32e4c4f851 100644 --- a/apps/kimi-code/src/tui/commands/auth.ts +++ b/apps/kimi-code/src/tui/commands/auth.ts @@ -12,6 +12,7 @@ import { log } from '@moonshot-ai/kimi-code-sdk'; import type { ChoiceOption } from '../components/dialogs/choice-picker'; import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '../constant/kimi-tui'; +import { t } from '#/i18n'; import { formatErrorMessage } from '../utils/event-payload'; import type { LoginProgressSpinnerHandle } from '../types'; import { @@ -59,13 +60,13 @@ async function handleKimiCodeOAuthLogin(host: SlashCommandHost): Promise { spinner = host.showLoginAuthorizationPrompt(data); }, }); - spinner?.stop({ ok: true, label: 'Logged in.' }); + spinner?.stop({ ok: true, label: t('tui.statusMessages.loggedIn') }); spinner = undefined; try { await host.authFlow.refreshConfigAfterLogin(); } catch (refreshError) { const message = formatErrorMessage(refreshError); - host.showError(`Authentication successful, but failed to refresh config: ${message}`); + host.showError(t('tui.statusMessages.authSuccessButConfigFailed', { error: message })); return; } host.track('login', { @@ -74,13 +75,13 @@ async function handleKimiCodeOAuthLogin(host: SlashCommandHost): Promise { already_logged_in: alreadyLoggedIn, }); if (alreadyLoggedIn) { - host.showStatus('Already logged in. Model configuration refreshed.'); + host.showStatus(t('tui.statusMessages.alreadyLoggedInRefreshed')); } } catch (error) { const cancelled = controller.signal.aborted; spinner?.stop({ ok: false, - label: cancelled ? 'Login cancelled.' : 'Login failed.', + label: cancelled ? t('tui.statusMessages.loginCancelled') : t('tui.statusMessages.loginFailed'), }); spinner = undefined; if (cancelled) return; @@ -91,7 +92,7 @@ async function handleKimiCodeOAuthLogin(host: SlashCommandHost): Promise { error, }); const message = formatErrorMessage(error); - host.showError(`Login failed: ${message}`); + host.showError(t('tui.statusMessages.loginFailedWithError', { error: message })); } finally { if (host.cancelInFlight === cancelLogin) { host.cancelInFlight = undefined; @@ -104,10 +105,12 @@ async function handleOpenPlatformLogin( platform: OpenPlatformDefinition, ): Promise { const consoleHost = platform.consoleUrl?.replace(/^https?:\/\//, '') ?? ''; - const platformName = consoleHost.length > 0 ? `Kimi Platform (${consoleHost})` : 'Kimi Platform'; + const platformName = consoleHost.length > 0 + ? t('tui.statusMessages.kimiPlatformDisplayWithHost', { host: consoleHost }) + : t('tui.statusMessages.kimiPlatformDisplay'); const subtitleLines = [ `${'base_url'.padEnd(12)}${platform.baseUrl}`, - `${'saved to'.padEnd(12)}~/.kimi-code/config.toml`, + `${t('tui.statusMessages.savedToLabel').padEnd(12)}~/.kimi-code/config.toml`, ]; const apiKey = await promptApiKey(host, platformName, subtitleLines); if (apiKey === undefined) return; @@ -125,14 +128,12 @@ async function handleOpenPlatformLogin( } catch (error) { if (controller.signal.aborted) return; const msg = formatErrorMessage(error); - host.showError(`Failed to verify API key: ${msg}`); + host.showError(t('tui.statusMessages.failedToVerifyApiKey', { error: msg })); if ( error instanceof OpenPlatformApiError && error.status === 401 ) { - host.showStatus( - 'Hint: If your API key was obtained from Kimi Code, please select "Kimi Code" instead.', - ); + host.showStatus(t('tui.statusMessages.hintUseKimiCodeInstead')); } return; } finally { @@ -142,7 +143,7 @@ async function handleOpenPlatformLogin( } if (models.length === 0) { - host.showError('No models available for this platform.'); + host.showError(t('tui.statusMessages.noModelsForPlatform')); return; } @@ -176,7 +177,7 @@ async function handleOpenPlatformLogin( await host.authFlow.refreshConfigAfterLogin(); host.track('login', { provider: platform.id, method: 'api_key' }); - host.showStatus(`Setup complete: ${platform.name} · ${selection.model.id}`); + host.showStatus(t('tui.statusMessages.setupComplete', { platformName: platform.name, modelId: selection.model.id })); } export async function handleLogoutCommand(host: SlashCommandHost): Promise { @@ -196,7 +197,7 @@ export async function handleLogoutCommand(host: SlashCommandHost): Promise options.push({ value: DEFAULT_OAUTH_PROVIDER_NAME, label: PRODUCT_NAME, - description: 'OAuth login', + description: t('tui.statusMessages.oauthLoginDescription'), }); } for (const id of apiKeyProviderIds) { @@ -209,7 +210,7 @@ export async function handleLogoutCommand(host: SlashCommandHost): Promise } if (options.length === 0) { - host.showStatus('Nothing to logout.'); + host.showStatus(t('tui.statusMessages.nothingToLogout')); return; } @@ -238,5 +239,5 @@ export async function handleLogoutCommand(host: SlashCommandHost): Promise host.track('logout', { provider: target }); const label = target === DEFAULT_OAUTH_PROVIDER_NAME ? PRODUCT_NAME : target; - host.showStatus(`Logged out from ${label}.`); + host.showStatus(t('tui.statusMessages.loggedOutFrom', { label })); } diff --git a/apps/kimi-code/src/tui/commands/btw.ts b/apps/kimi-code/src/tui/commands/btw.ts index a55ceca0a8..4321551c38 100644 --- a/apps/kimi-code/src/tui/commands/btw.ts +++ b/apps/kimi-code/src/tui/commands/btw.ts @@ -1,4 +1,6 @@ -import { LLM_NOT_SET_MESSAGE } from '../constant/kimi-tui'; +import { t } from '#/i18n'; + +import { getLlmNotSetMessage } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; @@ -6,7 +8,7 @@ export async function handleBtwCommand(host: SlashCommandHost, args: string): Pr const prompt = args.trim(); const session = host.session; if (host.state.appState.model.trim().length === 0 || session === undefined) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return; } host.btwPanelController.closeOrCancel(); @@ -15,6 +17,6 @@ export async function handleBtwCommand(host: SlashCommandHost, args: string): Pr const agentId = await session.startBtw(); host.btwPanelController.open(agentId, prompt); } catch (error) { - host.showError(`Failed to start /btw: ${formatErrorMessage(error)}`); + host.showError(t('tui.messages.btwStartFailed', { error: formatErrorMessage(error) })); } } diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index 9d95974193..35dd215afc 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -7,6 +7,7 @@ import { type ThinkingEffort, } from '@moonshot-ai/kimi-code-sdk'; +import { t, setLocale, getLocale, type Locale } from '#/i18n'; import { EditorSelectorComponent } from '../components/dialogs/editor-selector'; import { EffortSelectorComponent } from '../components/dialogs/effort-selector'; import { @@ -18,11 +19,12 @@ import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model import { PermissionSelectorComponent } from '../components/dialogs/permission-selector'; import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector'; import { ThemeSelectorComponent } from '../components/dialogs/theme-selector'; +import { LocaleSelectorComponent } from '../components/dialogs/locale-selector'; import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector'; import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config'; import type { ThemeName } from '#/tui/theme'; import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme'; -import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { getNoActiveSessionMessage } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; import { thinkingEffortToConfig } from '../utils/thinking-config'; import { showUsage } from './info'; @@ -38,6 +40,7 @@ const MODEL_PICKER_REFRESH_TIMEOUT_MS = 2_000; function currentTuiConfig(host: SlashCommandHost): TuiConfig { return { theme: host.state.appState.theme, + locale: host.state.appState.locale as Locale, editorCommand: host.state.appState.editorCommand, disablePasteBurst: host.state.appState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst, notifications: host.state.appState.notifications, @@ -48,14 +51,14 @@ function currentTuiConfig(host: SlashCommandHost): TuiConfig { export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } const subcmd = args.trim().toLowerCase(); if (subcmd === 'clear') { await session.clearPlan(); - host.showNotice('Plan cleared'); + host.showNotice(t('tui.statusMessages.planCleared')); return; } @@ -64,7 +67,7 @@ export async function handlePlanCommand(host: SlashCommandHost, args: string): P else if (subcmd === 'on') enabled = true; else if (subcmd === 'off') enabled = false; else { - host.showError(`Unknown plan subcommand: ${subcmd}`); + host.showError(t('tui.statusMessages.unknownPlanSubcommand', { subcmd })); return; } @@ -78,22 +81,22 @@ async function applyPlanMode(host: SlashCommandHost, session: Session, enabled: if (enabled) { const plan = await session.getPlan().catch(() => null); host.showNotice( - 'Plan mode: ON', - plan?.path !== undefined ? `Plan will be created here: ${plan.path}` : undefined, + t('tui.statusMessages.planModeOn'), + plan?.path !== undefined ? t('tui.statusMessages.planWillBeCreatedHere', { path: plan.path }) : undefined, ); return; } - host.showNotice('Plan mode: OFF'); + host.showNotice(t('tui.statusMessages.planModeOff')); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to set plan mode: ${msg}`); + host.showError(t('tui.statusMessages.failedToSetPlanMode', { msg })); } } export async function handleYoloCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -102,23 +105,23 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P if (subcmd === 'on') { if (currentMode === 'yolo') { - host.showNotice('YOLO mode is already on'); + host.showNotice(t('tui.statusMessages.yoloModeAlreadyOn')); return; } await session.setPermission('yolo'); host.setAppState({ permissionMode: 'yolo' }); - host.showNotice('YOLO mode: ON', 'AI auto-approves safe actions, asks for approval on risky ones.'); + host.showNotice(t('tui.statusMessages.yoloModeOn'), t('tui.statusMessages.yoloModeOnSub')); return; } if (subcmd === 'off') { if (currentMode !== 'yolo') { - host.showNotice('YOLO mode is already off'); + host.showNotice(t('tui.statusMessages.yoloModeAlreadyOff')); return; } await session.setPermission('manual'); host.setAppState({ permissionMode: 'manual' }); - host.showNotice('YOLO mode: OFF'); + host.showNotice(t('tui.statusMessages.yoloModeOff')); return; } @@ -126,18 +129,18 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P if (currentMode === 'yolo') { await session.setPermission('manual'); host.setAppState({ permissionMode: 'manual' }); - host.showNotice('YOLO mode: OFF'); + host.showNotice(t('tui.statusMessages.yoloModeOff')); } else { await session.setPermission('yolo'); host.setAppState({ permissionMode: 'yolo' }); - host.showNotice('YOLO mode: ON', 'AI auto-approves safe actions, asks for approval on risky ones.'); + host.showNotice(t('tui.statusMessages.yoloModeOn'), t('tui.statusMessages.yoloModeOnSub')); } } export async function handleAutoCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -146,23 +149,23 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P if (subcmd === 'on') { if (currentMode === 'auto') { - host.showNotice('Auto mode is already on'); + host.showNotice(t('tui.statusMessages.autoModeAlreadyOn')); return; } await session.setPermission('auto'); host.setAppState({ permissionMode: 'auto' }); - host.showNotice('Auto mode: ON', 'Run all actions automatically, including risky ones.'); + host.showNotice(t('tui.statusMessages.autoModeOn'), t('tui.statusMessages.autoModeOnSub')); return; } if (subcmd === 'off') { if (currentMode !== 'auto') { - host.showNotice('Auto mode is already off'); + host.showNotice(t('tui.statusMessages.autoModeAlreadyOff')); return; } await session.setPermission('manual'); host.setAppState({ permissionMode: 'manual' }); - host.showNotice('Auto mode: OFF'); + host.showNotice(t('tui.statusMessages.autoModeOff')); return; } @@ -170,18 +173,18 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P if (currentMode === 'auto') { await session.setPermission('manual'); host.setAppState({ permissionMode: 'manual' }); - host.showNotice('Auto mode: OFF'); + host.showNotice(t('tui.statusMessages.autoModeOff')); } else { await session.setPermission('auto'); host.setAppState({ permissionMode: 'auto' }); - host.showNotice('Auto mode: ON', 'Run all actions automatically, including risky ones.'); + host.showNotice(t('tui.statusMessages.autoModeOn'), t('tui.statusMessages.autoModeOnSub')); } } export async function handleCompactCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } const customInstruction = args.trim() || undefined; @@ -206,7 +209,7 @@ export async function handleThemeCommand(host: SlashCommandHost, args: string): if (!isBuiltInTheme(theme)) { const custom = await loadCustomThemeMerged(theme); if (custom === null) { - host.showError(`Unknown theme: ${theme}`); + host.showError(t('tui.statusMessages.unknownTheme', { theme })); return; } } @@ -221,7 +224,7 @@ export async function handleModelCommand(host: SlashCommandHost, args: string): return; } if (host.state.appState.availableModels[alias] === undefined) { - host.showError(`Unknown model alias: ${alias}`); + host.showError(t('tui.statusMessages.unknownModelAlias', { alias })); return; } showModelPicker(host, alias); @@ -231,7 +234,7 @@ export async function handleEffortCommand(host: SlashCommandHost, args: string): const alias = host.state.appState.model; const model = host.state.appState.availableModels[alias]; if (model === undefined) { - host.showError('No model selected. Run /model to select one first.'); + host.showError(t('tui.statusMessages.noModelSelected')); return; } const effective = effectiveModelAlias(model); @@ -243,7 +246,7 @@ export async function handleEffortCommand(host: SlashCommandHost, args: string): } if (!segments.includes(arg)) { host.showError( - `Unsupported thinking effort "${arg}" for ${alias}. Available: ${segments.join(', ')}`, + t('tui.statusMessages.unsupportedEffort', { arg, alias, segments: segments.join(', ') }), ); return; } @@ -305,10 +308,10 @@ async function refreshModelsForPicker(host: SlashCommandHost): Promise { ); if (result === undefined) return; for (const f of result.failed) { - host.showStatus(`Skipped refreshing ${f.provider}: ${f.reason}`, 'warning'); + host.showStatus(t('tui.messages.configSkippedRefreshing', { provider: f.provider, reason: f.reason }), 'warning'); } } catch (error) { - host.showStatus(`Skipped refreshing models: ${formatErrorMessage(error)}`, 'warning'); + host.showStatus(t('tui.messages.configSkippedRefreshingModels', { error: formatErrorMessage(error) }), 'warning'); } } @@ -331,7 +334,7 @@ async function withTimeout(promise: Promise, timeoutMs: number): Promise { const previous = host.state.appState.editorCommand ?? ''; if (value === previous && value.length > 0) { - host.showStatus(`Editor unchanged: ${value.length > 0 ? value : 'auto-detect'}`); + host.showStatus(t('tui.messages.configEditorUnchanged', { value: value.length > 0 ? value : t('tui.messages.configEditorAutoDetect') })); return; } @@ -343,7 +346,7 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise }); } catch (error) { host.showStatus( - `Failed to save editor: ${formatErrorMessage(error)}`, + t('tui.messages.configEditorSaveFailed', { error: formatErrorMessage(error) }), 'error', ); return; @@ -352,8 +355,8 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise host.setAppState({ editorCommand }); host.showStatus( value.length > 0 - ? `Editor set to "${value}".` - : 'Editor set to auto-detect ($VISUAL / $EDITOR).', + ? t('tui.messages.configEditorSet', { value }) + : t('tui.messages.configEditorAutoSet'), ); } @@ -361,8 +364,8 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string = const entries = Object.entries(host.state.appState.availableModels); if (entries.length === 0) { host.showNotice( - 'No models configured', - 'Run /login to sign in to Kimi, or /provider to add another provider from a model catalog.', + t('tui.statusMessages.noModelsConfigured'), + t('tui.statusMessages.noModelsConfiguredSub'), ); return; } @@ -394,7 +397,7 @@ async function performModelSwitch( persist: boolean, ): Promise { if (host.state.appState.streamingPhase !== 'idle') { - host.showError('Cannot switch models while streaming — press Esc or Ctrl-C first.'); + host.showError(t('tui.statusMessages.cannotSwitchWhileStreaming')); return; } @@ -419,7 +422,7 @@ async function performModelSwitch( } } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to switch model: ${msg}`); + host.showError(t('tui.statusMessages.switchModelFailed', { msg })); return; } @@ -443,7 +446,7 @@ async function performModelSwitch( persisted = await persistModelSelection(host, alias, effort); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Switched to ${displayName}, but failed to save default: ${msg}`); + host.showError(t('tui.statusMessages.switchSavedButDefaultFailed', { name: displayName, msg })); return; } } @@ -451,16 +454,16 @@ async function performModelSwitch( let status: string; if (modelChanged) { status = persist - ? `Switched to ${displayName} with thinking ${effort}.` - : `Switched to ${displayName} with thinking ${effort} for this session only.`; + ? t('tui.messages.configModelSwitched', { name: displayName, effort }) + : t('tui.messages.configModelSwitchedSession', { name: displayName, effort }); } else if (effortChanged) { status = persist - ? `Thinking set to ${effort}.` - : `Thinking set to ${effort} for this session only.`; + ? t('tui.messages.configThinkingSet', { effort }) + : t('tui.messages.configThinkingSetSession', { effort }); } else if (persist && persisted) { - status = `Saved ${displayName} with thinking ${effort} as default.`; + status = t('tui.messages.configModelSavedDefault', { name: displayName, effort }); } else { - status = `Already using ${displayName} with thinking ${effort}.`; + status = t('tui.messages.configModelAlreadyUsing', { name: displayName, effort }); } host.showStatus(status, 'success'); } @@ -504,7 +507,7 @@ function showThemePicker(host: SlashCommandHost): void { async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promise { if (theme === host.state.appState.theme) { if (theme === 'auto') host.refreshTerminalThemeTracking(); - host.showStatus(`Theme unchanged: "${theme}".`); + host.showStatus(t('tui.messages.configThemeUnchanged', { theme })); return; } @@ -514,7 +517,7 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi if (!isBuiltInTheme(theme)) { const palette = await loadCustomThemeMerged(theme); if (palette === null) { - host.showStatus(`Theme "${theme}" could not be loaded.`, 'error'); + host.showStatus(t('tui.messages.configThemeLoadFailed', { theme }), 'error'); return; } } @@ -526,7 +529,7 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi }); } catch (error) { host.showStatus( - `Failed to save theme: ${formatErrorMessage(error)}`, + t('tui.messages.configThemeSaveFailed', { error: formatErrorMessage(error) }), 'error', ); return; @@ -539,7 +542,7 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi host.refreshTerminalThemeTracking(); host.track('theme_switch', { theme }); const detail = theme === 'auto' ? ` (tracking terminal; current: ${resolved})` : ''; - host.showStatus(`Theme set to "${theme}"${detail}.`); + host.showStatus(t('tui.messages.configThemeSet', { theme, detail })); } export function showPermissionPicker(host: SlashCommandHost): void { @@ -577,7 +580,7 @@ export async function showExperimentsPanel(host: SlashCommandHost): Promise { if (autoInstall === host.state.appState.upgrade.autoInstall) { - host.showStatus(`Automatic updates already ${autoInstall ? 'enabled' : 'disabled'}.`); + host.showStatus(t('tui.messages.configAutoUpdateAlready', { state: autoInstall ? t('tui.messages.configAutoUpdateEnabled') : t('tui.messages.configAutoUpdateDisabled') })); return; } @@ -667,7 +670,7 @@ export async function applyUpdatePreferenceChoice( }); } catch (error) { host.showStatus( - `Failed to save automatic update setting: ${formatErrorMessage(error)}`, + t('tui.messages.configAutoUpdateSaveFailed', { error: formatErrorMessage(error) }), 'error', ); return; @@ -675,12 +678,12 @@ export async function applyUpdatePreferenceChoice( host.setAppState({ upgrade }); host.track('upgrade_preference_changed', { auto_install: autoInstall }); - host.showStatus(`Automatic updates ${autoInstall ? 'enabled' : 'disabled'}.`); + host.showStatus(t('tui.messages.configAutoUpdateSet', { state: autoInstall ? t('tui.messages.configAutoUpdateEnabled') : t('tui.messages.configAutoUpdateDisabled') })); } async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMode): Promise { if (mode === host.state.appState.permissionMode) { - host.showStatus(`Permission mode unchanged: ${mode}.`); + host.showStatus(t('tui.messages.configPermissionUnchanged', { mode })); return; } @@ -688,12 +691,12 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod await host.requireSession().setPermission(mode); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to set permission mode: ${msg}`); + host.showError(t('tui.statusMessages.setPermissionFailed', { msg })); return; } host.setAppState({ permissionMode: mode }); - host.showNotice(`Permission mode: ${mode}`); + host.showNotice(t('tui.messages.configPermissionMode', { mode })); } export function showSettingsSelector(host: SlashCommandHost): void { @@ -715,9 +718,51 @@ function handleSettingsSelection(host: SlashCommandHost, value: SettingsSelectio case 'model': showModelPicker(host); return; case 'permission': showPermissionPicker(host); return; case 'theme': showThemePicker(host); return; + case 'language': showLocalePicker(host); return; case 'editor': showEditorPicker(host); return; case 'experiments': void showExperimentsPanel(host); return; case 'upgrade': showUpdatePreferencePicker(host); return; case 'usage': void showUsage(host); return; } } + +function showLocalePicker(host: SlashCommandHost): void { + host.mountEditorReplacement( + new LocaleSelectorComponent({ + currentValue: host.state.appState.locale as Locale, + onSelect: (locale) => { + host.restoreEditor(); + void applyLocaleChoice(host, locale); + }, + onCancel: () => { + host.restoreEditor(); + }, + }), + ); +} + +async function applyLocaleChoice(host: SlashCommandHost, locale: Locale): Promise { + if (locale === host.state.appState.locale) { + host.showStatus(t('tui.messages.configLanguageUnchanged', { locale })); + return; + } + + try { + await saveTuiConfig({ + ...currentTuiConfig(host), + locale, + }); + } catch (error) { + host.showStatus( + t('tui.messages.configLanguageSaveFailed', { error: formatErrorMessage(error) }), + 'error', + ); + return; + } + + host.setAppState({ locale }); + setLocale(locale); + host.showNotice( + t('tui.messages.configLanguageSet', { locale }), + ); +} diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index 7508bc06a5..6e82f31c75 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -4,7 +4,8 @@ import type { KimiHarness, Session } from '@moonshot-ai/kimi-code-sdk'; import type { ColorToken, ThemeName } from '#/tui/theme'; -import { LLM_NOT_SET_MESSAGE } from '../constant/kimi-tui'; +import { t } from '#/i18n'; +import { getLlmNotSetMessage } from '../constant/kimi-tui'; import type { AuthFlowController } from '../controllers/auth-flow'; import type { BtwPanelController } from '../controllers/btw-panel'; import type { StreamingUIController } from '../controllers/streaming-ui'; @@ -52,6 +53,7 @@ import { handleTitleCommand, } from './session'; import { handleSwarmCommand } from './swarm'; +import { handleDiscussCommand } from './discuss'; import { handleUndoCommand } from './undo'; import { handleWebCommand } from './web'; @@ -77,6 +79,7 @@ export { showSettingsSelector, } from './config'; export { handleSwarmCommand } from './swarm'; +export { handleDiscussCommand } from './discuss'; export { handleFeedbackCommand, showMcpServers, showStatusReport, showUsage } from './info'; export { handlePluginsCommand } from './plugins'; export { handleReloadCommand, handleReloadTuiCommand } from './reload'; @@ -190,12 +193,12 @@ async function executeSlashCommand(host: SlashCommandHost, input: string): Promi reason: intent.reason, command: intent.commandName, }); - host.showError(`Invalid slash command: /${intent.commandName}`); + host.showError(t('tui.messages.configInvalidSlashCommand', { name: intent.commandName })); return; case 'skill': { const session = host.session; if (host.state.appState.model.trim().length === 0 || session === undefined) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return; } host.track('input_command', { @@ -207,12 +210,12 @@ async function executeSlashCommand(host: SlashCommandHost, input: string): Promi } case 'plugin-command': { if (host.state.appState.model.trim().length === 0) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return; } const session = host.session; if (session === undefined) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return; } host.track('input_command', { command: `${intent.pluginId}:${intent.commandName}` }); @@ -255,7 +258,7 @@ async function handleBuiltInSlashCommand( host.showHelpPanel(); return; case 'version': - host.showStatus(`Kimi Code v${host.state.appState.version}`); + host.showStatus(t('tui.messages.configVersionDisplay', { version: host.state.appState.version })); return; case 'new': await host.createNewSession(); @@ -333,6 +336,9 @@ async function handleBuiltInSlashCommand( case 'swarm': await handleSwarmCommand(host, args); return; + case 'discuss': + await handleDiscussCommand(host, args); + return; case 'compact': await handleCompactCommand(host, args); return; @@ -364,7 +370,7 @@ async function handleBuiltInSlashCommand( await handleWebCommand(host); return; default: - host.showError(`Unknown slash command: /${String(name)}`); + host.showError(t('tui.messages.configUnknownSlashCommand', { name: String(name) })); return; } } diff --git a/apps/kimi-code/src/tui/commands/goal.ts b/apps/kimi-code/src/tui/commands/goal.ts index de790b9060..22e3d82c08 100644 --- a/apps/kimi-code/src/tui/commands/goal.ts +++ b/apps/kimi-code/src/tui/commands/goal.ts @@ -1,4 +1,5 @@ import { ErrorCodes, isKimiError, type PermissionMode } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { GoalStartPermissionPromptComponent, @@ -15,7 +16,7 @@ import { GoalStatusMessageComponent, UpcomingGoalAddedMessageComponent, } from '../components/messages/goal-panel'; -import { LLM_NOT_SET_MESSAGE } from '../constant/kimi-tui'; +import { getLlmNotSetMessage } from '../constant/kimi-tui'; import { appendGoalQueueItem, moveGoalQueueItem, @@ -28,8 +29,6 @@ import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; const MAX_GOAL_OBJECTIVE_LENGTH = 4000; -const RESUME_GOAL_INPUT = 'Resume the active goal.'; -const START_NEXT_GOAL_NOW_MESSAGE = 'No active goal. Starting this goal now.'; type GoalCommandHost = Pick< SlashCommandHost, @@ -108,13 +107,13 @@ export function parseGoalCommand(rawArgs: string): ParsedGoalCommand { return { kind: 'error', severity: 'hint', - message: 'Provide a goal objective, e.g. `/goal Ship feature X`.', + message: t('tui.statusMessages.provideObjective'), }; } if (objective.length > MAX_GOAL_OBJECTIVE_LENGTH) { return { kind: 'error', - message: `Goal objective is too long (max ${MAX_GOAL_OBJECTIVE_LENGTH} characters). Reference long details by file path.`, + message: t('tui.statusMessages.objectiveTooLong', { max: MAX_GOAL_OBJECTIVE_LENGTH }), }; } return { kind: 'create', objective, replace }; @@ -160,14 +159,13 @@ function parseNextGoalCommand(tokens: readonly string[]): ParsedGoalCommand { return { kind: 'error', severity: 'hint', - message: - 'Provide an upcoming goal objective, e.g. `/goal next Ship feature X`, or use `/goal next manage`.', + message: t('tui.statusMessages.provideNextObjective'), }; } if (objective.length > MAX_GOAL_OBJECTIVE_LENGTH) { return { kind: 'error', - message: `Goal objective is too long (max ${MAX_GOAL_OBJECTIVE_LENGTH} characters). Reference long details by file path.`, + message: t('tui.statusMessages.objectiveTooLong', { max: MAX_GOAL_OBJECTIVE_LENGTH }), }; } return { kind: 'next-add', objective }; @@ -183,12 +181,12 @@ async function queueNextGoal( const { goal } = await session.getGoal(); hasCurrentGoal = goal !== null; } catch (error) { - host.showError(`Failed to inspect current goal: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToInspectGoal', { error: formatErrorMessage(error) })); return; } if (!hasCurrentGoal && !isBusy(host)) { - host.showStatus(START_NEXT_GOAL_NOW_MESSAGE); + host.showStatus(t('tui.statusMessages.startingNow')); await createGoal( host, { kind: 'create', objective: parsed.objective, replace: false }, @@ -219,7 +217,7 @@ async function showGoalQueueManager( try { snapshot = await readGoalQueue(host.requireSession()); } catch (error) { - host.showError(`Failed to load upcoming goals: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToLoadUpcomingGoals', { error: formatErrorMessage(error) })); return; } @@ -232,7 +230,7 @@ async function showGoalQueueManager( try { return await handleGoalQueueManagerAction(host, action); } catch (error) { - host.showError(`Failed to update upcoming goals: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToUpdateUpcomingGoals', { error: formatErrorMessage(error) })); return undefined; } }, @@ -276,13 +274,13 @@ async function showGoalQueueEditDialog( try { snapshot = await readGoalQueue(host.requireSession()); } catch (error) { - host.showError(`Failed to load upcoming goals: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToLoadUpcomingGoals', { error: formatErrorMessage(error) })); return; } const goal = snapshot.goals.find((item) => item.id === goalId); if (goal === undefined) { - host.showStatus('Queued goal no longer exists.'); + host.showStatus(t('tui.statusMessages.queuedGoalNoLongerExists')); await showGoalQueueManager(host); return; } @@ -292,7 +290,7 @@ async function showGoalQueueEditDialog( goal, onDone: (result) => { void handleGoalQueueEditResult(host, result).catch((error: unknown) => { - host.showError(`Failed to update upcoming goal: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToUpdateUpcomingGoal', { error: formatErrorMessage(error) })); }); }, }), @@ -324,7 +322,7 @@ export async function createGoal( ): Promise { // A goal must be able to start a model turn; refuse to create one otherwise. if (host.state.appState.model.trim().length === 0 || host.session === undefined) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return false; } @@ -348,7 +346,7 @@ function showGoalStartPermissionPrompt( const commandText = `/goal ${rawArgs.trim()}`; const cancelStart = (): void => { host.restoreInputText(commandText); - host.showStatus('Goal not started.'); + host.showStatus(t('tui.statusMessages.goalNotStarted')); }; host.mountEditorReplacement( new GoalStartPermissionPromptComponent({ @@ -391,7 +389,7 @@ async function setPermissionForGoal(host: GoalCommandHost, mode: PermissionMode) try { await host.requireSession().setPermission(mode); } catch (error) { - host.showError(`Failed to set permission mode: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToSetPermission', { error: formatErrorMessage(error) })); return false; } host.setAppState({ permissionMode: mode }); @@ -411,7 +409,7 @@ async function startGoal( } catch (error) { if (isKimiError(error) && error.code === ErrorCodes.GOAL_ALREADY_EXISTS) { host.showError( - 'A goal is already active. Use `/goal replace ` to replace it, or `/goal status` to inspect it.', + t('tui.statusMessages.goalAlreadyActive'), ); return false; } @@ -438,19 +436,19 @@ async function pauseGoal(host: SlashCommandHost): Promise { if (isStreaming(host)) await session.cancel(); } catch (error) { if (isKimiError(error) && error.code === ErrorCodes.GOAL_NOT_FOUND) { - host.showStatus('No goal to pause.'); + host.showStatus(t('tui.statusMessages.noGoalToPause')); return; } host.showError(formatErrorMessage(error)); return; } host.track('goal_pause'); - host.showStatus('Goal paused. Use `/goal resume` to continue.'); + host.showStatus(t('tui.statusMessages.goalPaused')); } async function resumeGoal(host: SlashCommandHost): Promise { if (host.state.appState.model.trim().length === 0 || host.session === undefined) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return; } @@ -458,14 +456,14 @@ async function resumeGoal(host: SlashCommandHost): Promise { await host.requireSession().resumeGoal(); } catch (error) { if (isKimiError(error) && error.code === ErrorCodes.GOAL_NOT_FOUND) { - host.showStatus('No goal to resume.'); + host.showStatus(t('tui.statusMessages.noGoalToResume')); return; } host.showError(formatErrorMessage(error)); return; } host.track('goal_resume'); - host.sendNormalUserInput(RESUME_GOAL_INPUT); + host.sendNormalUserInput(t('tui.statusMessages.resumeGoalInput')); } async function cancelGoal(host: SlashCommandHost): Promise { @@ -475,21 +473,21 @@ async function cancelGoal(host: SlashCommandHost): Promise { if (isStreaming(host)) await session.cancel(); } catch (error) { if (isKimiError(error) && error.code === ErrorCodes.GOAL_NOT_FOUND) { - host.showStatus('No goal to cancel.'); + host.showStatus(t('tui.statusMessages.noGoalToCancel')); return; } host.showError(formatErrorMessage(error)); return; } host.track('goal_cancel'); - host.showNotice('Goal cancelled.'); + host.showNotice(t('tui.statusMessages.goalCancelled')); } async function showGoalStatus(host: SlashCommandHost): Promise { const { goal } = await host.requireSession().getGoal(); host.track('goal_status', { status: goal?.status ?? 'none' }); if (goal === null) { - host.showStatus('No goal set. Start one with `/goal `.'); + host.showStatus(t('tui.statusMessages.noGoalSet')); return; } host.state.transcriptContainer.addChild( diff --git a/apps/kimi-code/src/tui/commands/info.ts b/apps/kimi-code/src/tui/commands/info.ts index fd5d397f4b..9c0ef1ee6c 100644 --- a/apps/kimi-code/src/tui/commands/info.ts +++ b/apps/kimi-code/src/tui/commands/info.ts @@ -2,6 +2,7 @@ import { release as osRelease, type as osType } from 'node:os'; import type { McpServerInfo, SessionStatus, SessionUsage } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { buildMcpStatusReportLines } from '../components/messages/mcp-status-panel'; import { buildStatusReportLines } from '../components/messages/status-panel'; import { buildUsageReportLines, UsagePanelComponent, type ManagedUsageReport } from '../components/messages/usage-panel'; @@ -169,7 +170,7 @@ export async function showMcpServers(host: SlashCommandHost): Promise { try { servers = await host.requireSession().listMcpServers(); } catch (error) { - host.showError(`Failed to load MCP servers: ${formatErrorMessage(error)}`); + host.showError(t('tui.messages.infoMcpLoadFailed', { error: formatErrorMessage(error) })); return; } diff --git a/apps/kimi-code/src/tui/commands/plugins.ts b/apps/kimi-code/src/tui/commands/plugins.ts index b69f0724a9..7114a61a8c 100644 --- a/apps/kimi-code/src/tui/commands/plugins.ts +++ b/apps/kimi-code/src/tui/commands/plugins.ts @@ -21,6 +21,7 @@ import { import { UsagePanelComponent } from '../components/messages/usage-panel'; import { formatErrorMessage } from '../utils/event-payload'; import { formatPluginSourceLabel, isOfficialPluginSource } from '../utils/plugin-source-label'; +import { t } from '#/i18n'; import { loadPluginMarketplace } from '#/utils/plugin-marketplace'; import { openUrl } from '#/utils/open-url'; import type { SlashCommandHost } from './dispatch'; @@ -63,19 +64,19 @@ export async function handlePluginsCommand(host: SlashCommandHost, rawArgs: stri if (sub === 'install') { const source = rest.join(' ').trim(); if (source.length === 0) { - host.showError('Usage: /plugins install '); + host.showError(t('tui.statusMessages.pluginsUsageInstall')); return; } if (!(await confirmInstallTrust(host, source, isOfficialPluginSource(source)))) { - host.showStatus('Install cancelled.'); + host.showStatus(t('tui.statusMessages.pluginsInstallCancelled')); return; } - const spinner = host.showProgressSpinner(`Installing plugin from ${truncateForStatus(source)}…`); + const spinner = host.showProgressSpinner(t('tui.statusMessages.pluginsInstallingFrom', { source: truncateForStatus(source) })); try { await installPluginFromSource(host, source); - spinner.stop({ ok: true, label: `Install finished — see details below.` }); + spinner.stop({ ok: true, label: t('tui.statusMessages.pluginsInstallFinished') }); } catch (error) { - spinner.stop({ ok: false, label: `Install failed: ${formatErrorMessage(error)}` }); + spinner.stop({ ok: false, label: t('tui.statusMessages.pluginsInstallFailed', { error: formatErrorMessage(error) }) }); throw error; } return; @@ -106,13 +107,12 @@ export async function handlePluginsCommand(host: SlashCommandHost, rawArgs: stri const id = rest[1]; const server = rest[2]; if ((action !== 'enable' && action !== 'disable') || id === undefined || server === undefined) { - host.showError('Usage: /plugins mcp enable|disable '); + host.showError(t('tui.statusMessages.pluginsUsageMcp')); return; } await session.setPluginMcpServerEnabled(id, server, action === 'enable'); - host.showStatus( - `${action === 'enable' ? 'Enabled' : 'Disabled'} MCP server ${server} for ${id}. Run /reload or /new to apply.`, - ); + const mcpKey = action === 'enable' ? 'pluginsMcpEnabled' : 'pluginsMcpDisabled'; + host.showStatus(t(`tui.statusMessages.${mcpKey}`, { server, id })); return; } if (sub === 'enable' || sub === 'disable') { @@ -127,11 +127,11 @@ export async function handlePluginsCommand(host: SlashCommandHost, rawArgs: stri if (sub === 'remove') { const id = rest[0]; if (id === undefined) { - host.showError('Usage: /plugins remove '); + host.showError(t('tui.statusMessages.pluginsUsageRemove')); return; } if (!(await confirmRemovePlugin(host, id))) { - host.showStatus(`Remove cancelled: ${id}.`); + host.showStatus(t('tui.statusMessages.pluginsRemoveCancelled', { id })); return; } await removePlugin(host, id); @@ -146,9 +146,9 @@ export async function handlePluginsCommand(host: SlashCommandHost, rawArgs: stri await renderPluginInfo(host, sub); return; } - host.showError(`Unknown /plugins action: ${sub}. Run /plugins to choose interactively.`); + host.showError(t('tui.statusMessages.pluginsUnknownAction', { action: sub })); } catch (error) { - host.showError(`/plugins ${sub ?? ''} failed: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.pluginsCommandFailed', { action: sub ?? '', error: formatErrorMessage(error) })); } } @@ -160,7 +160,7 @@ async function showPluginsPicker( try { plugins = await host.requireSession().listPlugins(); } catch (error) { - host.showError(`Failed to load plugins: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.pluginsFailedToLoad', { error: formatErrorMessage(error) })); return; } @@ -175,7 +175,7 @@ async function showPluginsPicker( // editor itself, so do not pre-restore here — that would flash the editor // for in-place actions like toggling a plugin. void handlePluginsPanelSelection(host, panel, selection).catch((error: unknown) => { - host.showError(`/plugins failed: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.pluginsFailed', { error: formatErrorMessage(error) })); }); }, onCancel: () => { @@ -227,7 +227,7 @@ async function showPluginMcpPicker( try { info = await host.requireSession().getPluginInfo(id); } catch (error) { - host.showError(`Failed to load plugin MCP servers: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.pluginsFailedToLoadMcp', { error: formatErrorMessage(error) })); return; } @@ -240,7 +240,7 @@ async function showPluginMcpPicker( // Every MCP action re-mounts a picker, so let the handler do the // mounting — pre-restoring the editor here would flash on toggle. void handlePluginMcpSelection(host, selection).catch((error: unknown) => { - host.showError(`/plugins mcp failed: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.pluginsMcpFailed', { error: formatErrorMessage(error) })); }); }, onCancel: () => { @@ -302,7 +302,7 @@ async function installFromPanel( official: boolean, ): Promise { if (!(await confirmInstallTrust(host, label, official))) { - host.showStatus(`Install cancelled: ${label}.`); + host.showStatus(t('tui.statusMessages.pluginsInstallCancelledLabel', { label })); host.restoreEditor(); return; } @@ -312,7 +312,7 @@ async function installFromPanel( if (official) { panel.setInstalling(truncateForStatus(label)); } else { - host.showStatus(`Installing or updating ${label} from marketplace...`); + host.showStatus(t('tui.statusMessages.pluginsInstallingOrUpdating', { label })); } host.state.ui.requestRender(); try { @@ -326,7 +326,7 @@ async function installFromPanel( // instead of being dropped back at the editor. host.mountEditorReplacement(panel); } - host.showError(`Failed to install ${label}: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.pluginsFailedToInstall', { label, error: formatErrorMessage(error) })); return; } // Close the panel after installing so the result status and the @@ -350,12 +350,13 @@ async function applyPluginEnabled( } const mcpHint = enabled && info !== undefined && info.mcpServerCount > info.enabledMcpServerCount - ? ` Some MCP servers are disabled; re-enable with /plugins mcp enable ${id} .` + ? t('tui.statusMessages.pluginsMcpDisabledHint', { id }) : ''; if (showStatus) { - host.showStatus(`${enabled ? 'Enabled' : 'Disabled'} ${id}. Run /reload or /new to apply.${mcpHint}`); + const enabledKey = enabled ? 'pluginsEnabled' : 'pluginsDisabled'; + host.showStatus(t(`tui.statusMessages.${enabledKey}`, { id }) + mcpHint); } - const inlineMcpHint = mcpHint.length > 0 ? ' · MCP servers disabled' : ''; + const inlineMcpHint = mcpHint.length > 0 ? t('tui.statusMessages.pluginsInlineMcpDisabled') : ''; return `${pluginInlineChangeHint()}${inlineMcpHint}`; } @@ -376,7 +377,7 @@ async function handlePluginsPanelSelection( } case 'remove': if (!(await confirmRemovePlugin(host, selection.id))) { - host.showStatus(`Remove cancelled: ${selection.id}.`); + host.showStatus(t('tui.statusMessages.pluginsRemoveCancelled', { id: selection.id })); await showPluginsPicker(host, { initialTab: 'installed', selectedId: selection.id }); return; } @@ -415,8 +416,8 @@ async function handlePluginsPanelSelection( case 'open-url': host.restoreEditor(); openUrl(selection.url); - host.showStatus(`Opening the ${selection.label} page in your browser…`, 'success'); - host.showStatus(`If it did not open, visit ${selection.url}`); + host.showStatus(t('tui.statusMessages.pluginsOpeningPage', { label: selection.label }), 'success'); + host.showStatus(t('tui.statusMessages.pluginsIfNotOpened', { url: selection.url })); return; } } @@ -448,8 +449,8 @@ async function handlePluginMcpSelection( async function removePlugin(host: SlashCommandHost, id: string): Promise { await host.requireSession().removePlugin(id); - host.showStatus(`Removed ${id}.`); - host.showStatus(PLUGIN_RELOAD_HINT, 'warning'); + host.showStatus(t('tui.statusMessages.pluginsRemoved', { id })); + host.showStatus(t('tui.statusMessages.pluginsReloadHint'), 'warning'); } async function renderPluginsList( @@ -490,7 +491,9 @@ async function installPluginFromSource( showPluginInstallResult(host, beforeList, summary); } -const PLUGIN_RELOAD_HINT = 'Run /new or /reload to apply plugin changes.'; +function pluginReloadHint(): string { + return t('tui.statusMessages.pluginsReloadHint'); +} function showPluginInstallResult( host: SlashCommandHost, @@ -498,14 +501,14 @@ function showPluginInstallResult( summary: PluginSummary, ): void { const previous = beforeList.find((entry) => entry.id === summary.id); - const serverWord = summary.mcpServerCount === 1 ? 'server' : 'servers'; + const mcpCount = summary.mcpServerCount; const mcpHint = - summary.mcpServerCount > 0 - ? ` Declares ${summary.mcpServerCount} MCP ${serverWord}; enabled by default and configurable from /plugins.` + mcpCount > 0 + ? t(mcpCount === 1 ? 'tui.statusMessages.pluginsDeclaresMcp_one' : 'tui.statusMessages.pluginsDeclaresMcp_other', { count: mcpCount }) : ''; const action = describeInstallAction(previous, summary); host.showStatus(`${action} (${summary.id}).${mcpHint}`); - host.showStatus(PLUGIN_RELOAD_HINT, 'warning'); + host.showStatus(pluginReloadHint(), 'warning'); } function describeInstallAction( @@ -518,19 +521,35 @@ function describeInstallAction( return ` ${prev} → ${cur ?? '-'}`; }; if (previous === undefined) { - return `Installed ${next.displayName}${versionFromTo(undefined, next.version)} ${sourcePhrase(sourceLabel)}`; + return t('tui.statusMessages.pluginsInstalledDesc', { + displayName: next.displayName, + version: versionFromTo(undefined, next.version), + sourcePhrase: sourcePhrase(sourceLabel), + }); } if (sourceIdentity(previous) !== sourceIdentity(next)) { const prevSourceLabel = formatPluginSourceLabel(previous); - return `Migrated ${next.displayName}: ${prevSourceLabel} → ${sourceLabel}${versionFromTo(previous.version, next.version)}`; + return t('tui.statusMessages.pluginsMigratedDesc', { + displayName: next.displayName, + prevSource: prevSourceLabel, + source: sourceLabel, + version: versionFromTo(previous.version, next.version), + }); } - return `Updated ${next.displayName}${versionFromTo(previous.version, next.version)} ${sourcePhrase(sourceLabel)}`; + return t('tui.statusMessages.pluginsUpdatedDesc', { + displayName: next.displayName, + version: versionFromTo(previous.version, next.version), + sourcePhrase: sourcePhrase(sourceLabel), + }); } // formatPluginSourceLabel already prefixes zip-url hosts with "via", so adding // "from" would read as "from via ". Only prepend "from" otherwise. function sourcePhrase(sourceLabel: string): string { - return sourceLabel.startsWith('via ') ? sourceLabel : `from ${sourceLabel}`; + if (sourceLabel.startsWith('via ')) { + return t('tui.statusMessages.pluginsViaSource', { source: sourceLabel.slice(4) }); + } + return t('tui.statusMessages.pluginsFromSource', { source: sourceLabel }); } function sourceIdentity(plugin: PluginSummary): string { @@ -547,8 +566,8 @@ function truncateForStatus(input: string): string { async function reloadPlugins(host: SlashCommandHost): Promise { const summary = await host.requireSession().reloadPlugins(); - const line = `Reload: +${summary.added.length} -${summary.removed.length}` + - (summary.errors.length > 0 ? ` (${summary.errors.length} errors)` : ''); + const line = t('tui.statusMessages.pluginsReloadResult', { added: summary.added.length, removed: summary.removed.length }) + + (summary.errors.length > 0 ? t('tui.statusMessages.pluginsReloadResultErrors', { count: summary.errors.length }) : ''); host.showStatus(line); } @@ -561,5 +580,5 @@ function resolvePluginInstallSource(source: string, workDir: string): string { } function pluginInlineChangeHint(): string { - return 'run /reload or /new to apply'; + return t('tui.statusMessages.pluginsInlineChangeHint'); } diff --git a/apps/kimi-code/src/tui/commands/prompts.ts b/apps/kimi-code/src/tui/commands/prompts.ts index 5b0fd55339..153a15f1a3 100644 --- a/apps/kimi-code/src/tui/commands/prompts.ts +++ b/apps/kimi-code/src/tui/commands/prompts.ts @@ -12,6 +12,7 @@ import type { OpenPlatformDefinition, } from '@moonshot-ai/kimi-code-oauth'; +import { t } from '#/i18n'; import { ApiKeyInputDialogComponent, type ApiKeyInputResult } from '../components/dialogs/api-key-input-dialog'; import { ChoicePickerComponent, type ChoiceOption } from '../components/dialogs/choice-picker'; import { FeedbackInputDialogComponent, type FeedbackInputDialogResult } from '../components/dialogs/feedback-input-dialog'; @@ -42,7 +43,7 @@ export function promptLogoutProviderSelection( ): Promise { return new Promise((resolve) => { const picker = new ChoicePickerComponent({ - title: 'Select a provider to log out', + title: t('tui.statusMessages.selectProviderToLogout'), options, currentValue, onSelect: (value) => { @@ -75,17 +76,17 @@ export function promptFeedbackInput(host: SlashCommandHost): Promise { return new Promise((resolve) => { const picker = new ChoicePickerComponent({ - title: 'Share diagnostic info to help us investigate?', + title: t('tui.statusMessages.shareDiagnosticInfo'), options: FEEDBACK_ATTACHMENT_OPTIONS, onSelect: (value) => { host.restoreEditor(); @@ -113,7 +114,7 @@ export function promptFeedbackAttachment( export function promptApiKey( host: SlashCommandHost, platformName: string, - subtitleLines: readonly string[] = ['Your key will be saved to ~/.kimi-code/config.toml'], + subtitleLines: readonly string[] = [t('tui.statusMessages.apiKeySavedTo')], ): Promise { return new Promise((resolve) => { const dialog = new ApiKeyInputDialogComponent( @@ -141,13 +142,13 @@ export function promptCatalogProviderSelection(host: SlashCommandHost, catalog: .toSorted((a, b) => a.label.localeCompare(b.label)); if (options.length === 0) { - host.showError('Catalog has no providers with supported wire types.'); + host.showError(t('tui.statusMessages.catalogNoSupportedProviders')); resolve(undefined); return; } const picker = new ChoicePickerComponent({ - title: 'Select a provider', + title: t('tui.statusMessages.selectProviderTitle'), options, searchable: true, onSelect: (value) => { diff --git a/apps/kimi-code/src/tui/commands/provider.ts b/apps/kimi-code/src/tui/commands/provider.ts index eb416a54e6..149d7d2a82 100644 --- a/apps/kimi-code/src/tui/commands/provider.ts +++ b/apps/kimi-code/src/tui/commands/provider.ts @@ -27,6 +27,7 @@ import { } from '../components/dialogs/provider-manager'; import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model-selector'; import { DEFAULT_OAUTH_PROVIDER_NAME } from '../constant/kimi-tui'; +import { t } from '#/i18n'; import { formatErrorMessage } from '../utils/event-payload'; import { thinkingEffortToConfig } from '../utils/thinking-config'; import { @@ -53,12 +54,12 @@ function buildProviderManagerOptions(host: SlashCommandHost): ProviderManagerOpt activeProviderId, onAdd: () => { void handleProviderAdd(host).catch((error: unknown) => { - host.showError(`Add provider failed: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.addProviderFailed', { error: formatErrorMessage(error) })); }); }, onDeleteSource: (providerIds) => { void handleProviderManagerDeleteSource(host, providerIds).catch((error: unknown) => { - host.showError(`Remove provider failed: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.removeProviderFailedGeneric', { error: formatErrorMessage(error) })); }); }, onClose: () => { @@ -76,7 +77,7 @@ async function handleProviderManagerDeleteSource( await handleProviderDelete(host, providerId); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to delete provider ${providerId}: ${msg}`); + host.showError(t('tui.statusMessages.removeProviderFailed', { providerId, error: msg })); } } reopenProviderManager(host); @@ -132,10 +133,10 @@ function promptProviderAddSource( ): Promise<'known' | 'custom' | undefined> { return new Promise((resolve) => { const picker = new ChoicePickerComponent({ - title: 'Add provider', + title: t('tui.statusMessages.addProviderTitle'), options: [ - { value: 'known', label: 'Known third-party provider' }, - { value: 'custom', label: 'Custom registry (api.json)' }, + { value: 'known', label: t('tui.statusMessages.knownThirdPartyProvider') }, + { value: 'custom', label: t('tui.statusMessages.customRegistryOption') }, ], onSelect: (value) => { host.restoreEditor(); @@ -157,18 +158,20 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise { }; host.cancelInFlight = cancel; - const spinner = host.showLoginProgressSpinner(`Fetching catalog from ${DEFAULT_CATALOG_URL}`); + const spinner = host.showLoginProgressSpinner( + t('tui.statusMessages.fetchingCatalog', { url: DEFAULT_CATALOG_URL }), + ); let catalog: Catalog | undefined; try { catalog = await fetchCatalog(DEFAULT_CATALOG_URL, controller.signal); - spinner.stop({ ok: true, label: 'Catalog loaded.' }); + spinner.stop({ ok: true, label: t('tui.statusMessages.catalogLoaded') }); } catch (error) { if (controller.signal.aborted) { - spinner.stop({ ok: false, label: 'Aborted.' }); + spinner.stop({ ok: false, label: t('tui.statusMessages.catalogAborted') }); } else { const hint = error instanceof CatalogFetchError ? ` (HTTP ${error.status})` : ''; - spinner.stop({ ok: false, label: 'Failed to load catalog.' }); - host.showError(`Failed to fetch catalog${hint}: ${formatErrorMessage(error)}`); + spinner.stop({ ok: false, label: t('tui.statusMessages.catalogFailedToLoad') }); + host.showError(t('tui.statusMessages.catalogFetchFailed', { hint, error: formatErrorMessage(error) })); } } finally { if (host.cancelInFlight === cancel) host.cancelInFlight = undefined; @@ -183,7 +186,7 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise { const models = catalogProviderModels(entry); if (models.length === 0) { - host.showError(`Provider "${providerId}" has no usable models in this catalog.`); + host.showError(t('tui.statusMessages.providerNoUsableModels', { providerId })); return; } @@ -192,7 +195,7 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise { const wire = inferWireType(entry); if (wire === undefined) { - host.showError(`Provider "${providerId}" has unsupported wire type.`); + host.showError(t('tui.statusMessages.providerUnsupportedWire', { providerId })); return; } const baseUrl = catalogBaseUrl(entry, wire); @@ -223,7 +226,7 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise { await host.authFlow.refreshConfigAfterLogin(); host.track('connect', { provider: providerId, method: 'catalog' }); - host.showStatus(`Provider added: ${entry.name ?? providerId}`); + host.showStatus(t('tui.statusMessages.providerAdded', { providerName: entry.name ?? providerId })); // Build a merged model dictionary that includes existing models plus the // newly-persisted provider's models, so the tabbed selector shows every @@ -240,7 +243,7 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise { onSelect: ({ alias, thinking }) => { host.restoreEditor(); void setDefaultModel(host, alias, thinking).catch((error: unknown) => { - host.showError(`Set default model failed: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.setDefaultModelFailed', { error: formatErrorMessage(error) })); }); }, onCancel: () => { @@ -261,7 +264,7 @@ async function setDefaultModel( }); await host.authFlow.refreshConfigAfterLogin(); host.track('model_switch', { model: alias }); - host.showStatus(`Default model set to ${alias} with thinking ${effort}.`); + host.showStatus(t('tui.statusMessages.defaultModelSet', { alias, effort })); } async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise { @@ -278,7 +281,7 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise try { entries = await fetchCustomRegistry(source); } catch (error) { - host.showError(`Failed to import registry: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToImportRegistry', { error: formatErrorMessage(error) })); return false; } @@ -296,19 +299,19 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise }); await host.authFlow.refreshConfigAfterLogin(); } catch (error) { - host.showError(`Failed to apply registry: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToApplyRegistry', { error: formatErrorMessage(error) })); return false; } const count = addedProviderIds.length; if (count === 0) { - host.showStatus('Registry contained no providers.'); + host.showStatus(t('tui.statusMessages.registryNoProviders')); return false; } host.showStatus( count === 1 - ? 'Imported 1 provider from registry.' - : `Imported ${String(count)} providers from registry.`, + ? t('tui.statusMessages.importedOneProvider') + : t('tui.statusMessages.importedProviders', { count }), 'success', ); @@ -330,7 +333,7 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise onSelect: ({ alias, thinking }) => { host.restoreEditor(); void setDefaultModel(host, alias, thinking).catch((error: unknown) => { - host.showError(`Set default model failed: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.setDefaultModelFailed', { error: formatErrorMessage(error) })); }); }, onCancel: () => { diff --git a/apps/kimi-code/src/tui/commands/registry.ts b/apps/kimi-code/src/tui/commands/registry.ts index 8e5a163867..a01875caff 100644 --- a/apps/kimi-code/src/tui/commands/registry.ts +++ b/apps/kimi-code/src/tui/commands/registry.ts @@ -4,30 +4,36 @@ import { basename, dirname, join, relative, resolve } from 'pathe'; import type { AutocompleteItem } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; + import { completeLeadingArg, type ArgCompletionSpec } from './complete-args'; import type { KimiSlashCommand, SlashCommandAvailability } from './types'; /** Subcommands offered when autocompleting `/goal <…>`. */ const GOAL_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [ - { value: 'status', description: 'Show the current goal' }, - { value: 'pause', description: 'Pause the active goal' }, - { value: 'resume', description: 'Resume a paused goal' }, - { value: 'cancel', description: 'Cancel and remove the current goal' }, - { value: 'replace', description: 'Replace the current goal with a new objective' }, - { value: 'next', description: 'Queue an upcoming goal' }, + { value: 'status', description: t('tui.messages.registryGoalShow') }, + { value: 'pause', description: t('tui.messages.registryGoalPause') }, + { value: 'resume', description: t('tui.messages.registryGoalResume') }, + { value: 'cancel', description: t('tui.messages.registryGoalCancel') }, + { value: 'replace', description: t('tui.messages.registryGoalReplace') }, + { value: 'next', description: t('tui.messages.registryGoalNext') }, ]; const GOAL_NEXT_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [ - { value: 'manage', description: 'Manage upcoming goals' }, + { value: 'manage', description: t('tui.messages.registryGoalManage') }, ]; const SWARM_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [ - { value: 'on', description: 'Turn swarm mode on' }, - { value: 'off', description: 'Turn swarm mode off' }, + { value: 'on', description: t('tui.messages.registrySwarmOn') }, + { value: 'off', description: t('tui.messages.registrySwarmOff') }, +]; + +const DISCUSS_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [ + { value: 'with', description: t('tui.messages.registryDiscussRoles') }, ]; const ADD_DIR_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [ - { value: 'list', description: 'Show configured additional workspace directories' }, + { value: 'list', description: t('tui.messages.registryAddDirShow') }, ]; /** Argument autocompletion for the `/goal` command (subcommands). */ @@ -132,285 +138,293 @@ function formatDirectoryCompletionValue(argumentPrefix: string, parentInput: str return `${join(parentInput, entryName)}/`; } -export const BUILTIN_SLASH_COMMANDS = [ - { - name: 'yolo', - aliases: ['yes'], - description: 'Toggle YOLO mode: AI auto-approves safe actions, asks for approval on risky ones.', - priority: 101, - availability: 'always', - }, - { - name: 'auto', - aliases: [], - description: 'Toggle Auto mode: run all actions automatically, including risky ones.', - priority: 99, - availability: 'always', - }, - { - name: 'permission', - aliases: [], - description: 'Select permission mode', - priority: 100, - availability: 'always', - }, - { - name: 'settings', - aliases: ['config'], - description: 'Open TUI settings', - priority: 100, - availability: 'always', - }, - { - name: 'plan', - aliases: [], - description: 'Toggle plan mode', - priority: 100, - availability: (args) => (args.trim().toLowerCase() === 'clear' ? 'idle-only' : 'always'), - }, - { - name: 'swarm', - aliases: [], - description: 'Toggle swarm mode or run one task in swarm mode', - priority: 100, - argumentHint: '[on|off] | ', - completeArgs: swarmArgumentCompletions, - availability: 'idle-only', - }, - { - name: 'model', - aliases: [], - description: 'Switch LLM model', - priority: 100, - availability: 'always', - }, - { - name: 'effort', - aliases: ['thinking'], - description: 'Switch thinking effort', - priority: 95, - availability: 'always', - }, - { - name: 'provider', - aliases: ['providers'], - description: 'Manage AI providers (add / delete / refresh)', - priority: 95, - availability: 'always', - }, - { - name: 'btw', - aliases: [], - description: 'Ask a forked side agent a question', - priority: 90, - availability: 'always', - }, - { - name: 'help', - aliases: ['h', '?'], - description: 'Show available commands and shortcuts', - priority: 80, - availability: 'always', - }, - { - name: 'new', - aliases: ['clear'], - description: 'Start a fresh session in the current workspace', - priority: 80, - }, - { - name: 'sessions', - aliases: ['resume'], - description: 'Browse and resume sessions', - priority: 80, - }, - { - name: 'tasks', - aliases: ['task'], - description: 'Browse background tasks', - priority: 80, - availability: 'always', - }, - { - name: 'mcp', - aliases: [], - description: 'Show MCP server status', - priority: 60, - availability: 'always', - }, - { - name: 'plugins', - aliases: [], - description: 'Manage plugins', - priority: 60, - availability: 'always', - }, - { - name: 'add-dir', - aliases: [], - description: 'Add or list an additional workspace directory', - priority: 60, - availability: 'idle-only', - argumentHint: '[list] | ', - completeArgs: addDirArgumentCompletions, - }, - { - name: 'experiments', - aliases: ['experimental'], - description: 'Manage experimental features', - priority: 60, - availability: 'idle-only', - }, - { - name: 'reload', - aliases: [], - description: 'Reload session and apply config.toml settings plus tui.toml UI preferences', - priority: 60, - availability: 'idle-only', - }, - { - name: 'reload-tui', - aliases: [], - description: 'Reload only tui.toml UI preferences', - priority: 60, - availability: 'always', - }, - { - name: 'compact', - aliases: [], - description: 'Compact the conversation context', - priority: 80, - argumentHint: '', - }, - { - name: 'goal', - aliases: [], - description: 'Start or manage an autonomous goal', - priority: 80, - argumentHint: '[status|pause|resume|cancel|replace|next] | ', - completeArgs: goalArgumentCompletions, - // status / pause / cancel are always available; creation, replacement, and - // resume start (or restart) a turn and so are idle-only. - availability: (args) => { - const trimmed = args.trim(); - if (trimmed === 'next' || trimmed.startsWith('next ')) return 'always'; - return trimmed === '' || trimmed === 'status' || trimmed === 'pause' || trimmed === 'cancel' - ? 'always' - : 'idle-only'; - }, - }, - { - name: 'init', - aliases: [], - description: 'Analyze the codebase and generate AGENTS.md', - }, - { - name: 'fork', - aliases: [], - description: 'Fork the current session', - priority: 80, - }, - { - name: 'title', - aliases: ['rename'], - description: 'Set or show session title', - priority: 60, - argumentHint: '', - availability: 'always', - }, - { - name: 'usage', - aliases: [], - description: 'Show session tokens + context window + plan quotas', - priority: 60, - availability: 'always', - }, - { - name: 'status', - aliases: [], - description: 'Show current session and runtime status', - priority: 60, - availability: 'always', - }, - { - name: 'feedback', - aliases: [], - description: 'Send feedback to make Kimi Code better', - priority: 60, - availability: 'always', - }, - { - name: 'undo', - aliases: [], - description: 'Withdraw the last prompt from the transcript', - priority: 80, - availability: 'idle-only', - }, - { - name: 'editor', - aliases: [], - description: 'Set the external editor for Ctrl-G', - priority: 60, - availability: 'always', - }, - { - name: 'theme', - aliases: [], - description: 'Set the terminal UI theme', - priority: 60, - availability: 'always', - }, - { - name: 'logout', - aliases: ['disconnect'], - description: 'Log out of a configured provider', - priority: 40, - }, - { - name: 'login', - aliases: [], - description: 'Select a platform and authenticate', - priority: 40, - }, - { - name: 'export-md', - aliases: ['export'], - description: 'Export current session as a Markdown file', - priority: 40, - }, - { - name: 'export-debug-zip', - aliases: [], - description: 'Export current session as a debug ZIP archive', - priority: 40, - }, - { - name: 'web', - aliases: [], - description: 'Open the current session in the Web UI and exit the terminal', - priority: 40, - availability: 'always', - }, - { - name: 'exit', - aliases: ['quit', 'q'], - description: 'Exit the application', - priority: 20, - }, - { - name: 'version', - aliases: [], - description: 'Show version information', - priority: 20, - availability: 'always', - }, -] as const satisfies readonly KimiSlashCommand[]; +export function getBuiltinSlashCommands(): readonly KimiSlashCommand[] { + return [ + { + name: 'yolo', + aliases: ['yes'], + description: t('tui.slashCommands.yolo'), + priority: 101, + availability: 'always', + }, + { + name: 'auto', + aliases: [], + description: t('tui.slashCommands.auto'), + priority: 99, + availability: 'always', + }, + { + name: 'permission', + aliases: [], + description: t('tui.slashCommands.permission'), + priority: 100, + availability: 'always', + }, + { + name: 'settings', + aliases: ['config'], + description: t('tui.slashCommands.settings'), + priority: 100, + availability: 'always', + }, + { + name: 'plan', + aliases: [], + description: t('tui.slashCommands.plan'), + priority: 100, + availability: (args: string) => (args.trim().toLowerCase() === 'clear' ? 'idle-only' : 'always'), + }, + { + name: 'swarm', + aliases: [], + description: t('tui.slashCommands.swarm'), + priority: 100, + argumentHint: '[on|off] | <task>', + completeArgs: swarmArgumentCompletions, + availability: 'idle-only', + }, + { + name: 'discuss', + aliases: [], + description: t('tui.slashCommands.discuss'), + priority: 90, + argumentHint: '<topic> with <role1>,<role2>,...', + availability: 'idle-only', + }, + { + name: 'model', + aliases: [], + description: t('tui.slashCommands.model'), + priority: 100, + availability: 'always', + }, + { + name: 'effort', + aliases: ['thinking'], + description: t('tui.slashCommands.effort'), + priority: 95, + availability: 'always', + }, + { + name: 'provider', + aliases: ['providers'], + description: t('tui.slashCommands.provider'), + priority: 95, + availability: 'always', + }, + { + name: 'btw', + aliases: [], + description: t('tui.slashCommands.btw'), + priority: 90, + availability: 'always', + }, + { + name: 'help', + aliases: ['h', '?'], + description: t('tui.slashCommands.help'), + priority: 80, + availability: 'always', + }, + { + name: 'new', + aliases: ['clear'], + description: t('tui.slashCommands.new'), + priority: 80, + }, + { + name: 'sessions', + aliases: ['resume'], + description: t('tui.slashCommands.sessions'), + priority: 80, + }, + { + name: 'tasks', + aliases: ['task'], + description: t('tui.slashCommands.tasks'), + priority: 80, + availability: 'always', + }, + { + name: 'mcp', + aliases: [], + description: t('tui.slashCommands.mcp'), + priority: 60, + availability: 'always', + }, + { + name: 'plugins', + aliases: [], + description: t('tui.slashCommands.plugins'), + priority: 60, + availability: 'always', + }, + { + name: 'add-dir', + aliases: [], + description: t('tui.slashCommands.addDir'), + priority: 60, + availability: 'idle-only', + argumentHint: '[list] | <path>', + completeArgs: addDirArgumentCompletions, + }, + { + name: 'experiments', + aliases: ['experimental'], + description: t('tui.slashCommands.experiments'), + priority: 60, + availability: 'idle-only', + }, + { + name: 'reload', + aliases: [], + description: t('tui.slashCommands.reload'), + priority: 60, + availability: 'idle-only', + }, + { + name: 'reload-tui', + aliases: [], + description: t('tui.slashCommands.reloadTui'), + priority: 60, + availability: 'always', + }, + { + name: 'compact', + aliases: [], + description: t('tui.slashCommands.compact'), + priority: 80, + argumentHint: '<instruction>', + }, + { + name: 'goal', + aliases: [], + description: t('tui.slashCommands.goal'), + priority: 80, + argumentHint: '[status|pause|resume|cancel|replace|next] | <objective>', + completeArgs: goalArgumentCompletions, + availability: (args: string) => { + const trimmed = args.trim(); + if (trimmed === 'next' || trimmed.startsWith('next ')) return 'always'; + return trimmed === '' || trimmed === 'status' || trimmed === 'pause' || trimmed === 'cancel' + ? 'always' + : 'idle-only'; + }, + }, + { + name: 'init', + aliases: [], + description: t('tui.slashCommands.init'), + }, + { + name: 'fork', + aliases: [], + description: t('tui.slashCommands.fork'), + priority: 80, + }, + { + name: 'title', + aliases: ['rename'], + description: t('tui.slashCommands.title'), + priority: 60, + argumentHint: '<title>', + availability: 'always', + }, + { + name: 'usage', + aliases: [], + description: t('tui.slashCommands.usage'), + priority: 60, + availability: 'always', + }, + { + name: 'status', + aliases: [], + description: t('tui.slashCommands.status'), + priority: 60, + availability: 'always', + }, + { + name: 'feedback', + aliases: [], + description: t('tui.slashCommands.feedback'), + priority: 60, + availability: 'always', + }, + { + name: 'undo', + aliases: [], + description: t('tui.slashCommands.undo'), + priority: 80, + availability: 'idle-only', + }, + { + name: 'editor', + aliases: [], + description: t('tui.slashCommands.editor'), + priority: 60, + availability: 'always', + }, + { + name: 'theme', + aliases: [], + description: t('tui.slashCommands.theme'), + priority: 60, + availability: 'always', + }, + { + name: 'logout', + aliases: ['disconnect'], + description: t('tui.slashCommands.logout'), + priority: 40, + }, + { + name: 'login', + aliases: [], + description: t('tui.slashCommands.login'), + priority: 40, + }, + { + name: 'export-md', + aliases: ['export'], + description: t('tui.slashCommands.exportMd'), + priority: 40, + }, + { + name: 'export-debug-zip', + aliases: [], + description: t('tui.slashCommands.exportDebugZip'), + priority: 40, + }, + { + name: 'web', + aliases: [], + description: t('tui.slashCommands.web'), + priority: 40, + availability: 'always', + }, + { + name: 'exit', + aliases: ['quit', 'q'], + description: t('tui.slashCommands.exit'), + priority: 20, + }, + { + name: 'version', + aliases: [], + description: t('tui.slashCommands.version'), + priority: 20, + availability: 'always', + }, + ] as const satisfies readonly KimiSlashCommand[]; +} -export type BuiltinSlashCommand = (typeof BUILTIN_SLASH_COMMANDS)[number]; +export type BuiltinSlashCommand = ReturnType<typeof getBuiltinSlashCommands>[number]; export type BuiltinSlashCommandName = BuiltinSlashCommand['name']; export function findBuiltInSlashCommand(commandName: string): BuiltinSlashCommand | undefined { - const commands = BUILTIN_SLASH_COMMANDS as readonly KimiSlashCommand<BuiltinSlashCommandName>[]; + const commands = getBuiltinSlashCommands() as readonly KimiSlashCommand<BuiltinSlashCommandName>[]; return commands.find( (command) => command.name === commandName || command.aliases.includes(commandName), ) as BuiltinSlashCommand | undefined; diff --git a/apps/kimi-code/src/tui/commands/reload.ts b/apps/kimi-code/src/tui/commands/reload.ts index 6f28a16da5..cf4d24788e 100644 --- a/apps/kimi-code/src/tui/commands/reload.ts +++ b/apps/kimi-code/src/tui/commands/reload.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import type { KimiConfig } from '@moonshot-ai/kimi-code-sdk'; import { currentTheme, lightColors } from '#/tui/theme'; @@ -8,7 +9,7 @@ import { setExperimentalFeatures } from './experimental-flags'; export async function handleReloadTuiCommand(host: SlashCommandHost): Promise<void> { const tuiConfig = await loadTuiConfig(); await applyReloadedTuiConfig(host, tuiConfig); - host.showStatus('TUI config reloaded.', 'success'); + host.showStatus(t('tui.statusMessages.reloadTuiConfigReloaded'), 'success'); } export async function handleReloadCommand(host: SlashCommandHost): Promise<void> { @@ -17,7 +18,7 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise<void> if (session !== undefined) { await session.reloadSession({ forcePluginSessionStartReminder: true }); - await host.reloadCurrentSessionView(session, 'Session reloaded.'); + await host.reloadCurrentSessionView(session, t('tui.statusMessages.reloadSession')); } const config = await host.harness.getConfig({ reload: true }); @@ -28,7 +29,7 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise<void> if (session === undefined) { host.showStatus( - 'Runtime and TUI config reloaded; no active session.', + t('tui.statusMessages.reloadNoActiveSession'), 'success', ); } diff --git a/apps/kimi-code/src/tui/commands/resolve.ts b/apps/kimi-code/src/tui/commands/resolve.ts index e67457a94b..e0c4256359 100644 --- a/apps/kimi-code/src/tui/commands/resolve.ts +++ b/apps/kimi-code/src/tui/commands/resolve.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import { findBuiltInSlashCommand, resolveSlashCommandAvailability, @@ -145,7 +146,7 @@ export function slashBusyMessage( reason: SlashCommandBusyReason, ): string { if (reason === 'streaming') { - return `Cannot /${commandName} while streaming — press Esc or Ctrl-C first.`; + return t('tui.messages.resolveCannotWhileStreaming', { name: commandName }); } - return `Cannot /${commandName} while compacting — wait for compaction to finish first.`; + return t('tui.messages.resolveCannotWhileCompacting', { name: commandName }); } diff --git a/apps/kimi-code/src/tui/commands/session.ts b/apps/kimi-code/src/tui/commands/session.ts index 2f0870db0d..ee200044dc 100644 --- a/apps/kimi-code/src/tui/commands/session.ts +++ b/apps/kimi-code/src/tui/commands/session.ts @@ -4,10 +4,11 @@ import { pathToFileURL } from 'node:url'; import type { Session } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { detectInstallSource } from '#/cli/update/source'; import { detectShellEnvironment } from '#/utils/process/shell-env'; import { toTerminalHyperlink } from '#/utils/terminal-hyperlink'; -import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { getLlmNotSetMessage, getNoActiveSessionMessage } from '../constant/kimi-tui'; import { isAbortError } from '../utils/errors'; import { formatErrorMessage } from '../utils/event-payload'; import { buildExportMarkdown } from '../utils/export-markdown'; @@ -23,15 +24,15 @@ export async function handleTitleCommand(host: SlashCommandHost, args: string): const current = host.state.appState.sessionTitle; host.showStatus( current !== null && current.length > 0 - ? `Session title: ${current}` - : `Session title: (not set) — id: ${host.state.appState.sessionId}`, + ? t('tui.statusMessages.sessionTitle', { title: current }) + : t('tui.statusMessages.sessionTitleNotSet', { sessionId: host.state.appState.sessionId }), ); return; } const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -40,17 +41,17 @@ export async function handleTitleCommand(host: SlashCommandHost, args: string): await host.harness.renameSession({ id: session.id, title: newTitle }); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to set title: ${msg}`); + host.showError(t('tui.statusMessages.sessionFailedToSetTitle', { message: msg })); return; } - host.showStatus(`Session title set to: ${newTitle}`); + host.showStatus(t('tui.statusMessages.sessionTitleSetTo', { title: newTitle })); } export async function handleForkCommand(host: SlashCommandHost, args: string): Promise<void> { void args; const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -63,18 +64,18 @@ export async function handleForkCommand(host: SlashCommandHost, args: string): P }); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to fork session: ${msg}`); + host.showError(t('tui.statusMessages.sessionFailedToFork', { message: msg })); return; } try { await host.switchToSession( forked, - `Session forked (${forked.id}). To return to the original session: kimi -r ${session.id}`, + t('tui.statusMessages.sessionForked', { forkedId: forked.id, originalId: session.id }), ); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to switch to forked session: ${msg}`); + host.showError(t('tui.statusMessages.sessionFailedToSwitchToForked', { message: msg })); } } @@ -90,15 +91,15 @@ function forkSourceTitle(host: SlashCommandHost, session: Session): string { export async function handleExportMdCommand(host: SlashCommandHost, args: string): Promise<void> { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } - host.showStatus('Exporting session as Markdown…'); + host.showStatus(t('tui.statusMessages.sessionExportingMarkdown')); try { const context = await session.getContext(); if (context.history.length === 0) { - host.showError('No messages to export.'); + host.showError(t('tui.statusMessages.sessionNoMessagesToExport')); return; } @@ -124,21 +125,21 @@ export async function handleExportMdCommand(host: SlashCommandHost, args: string await writeFile(outputPath, md, 'utf-8'); const linked = toTerminalHyperlink(outputPath, pathToFileURL(outputPath).href); - host.showNotice(`Exported ${String(context.history.length)} messages`, linked); + host.showNotice(t('tui.statusMessages.sessionExportComplete', { count: context.history.length }), linked); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to export session: ${msg}`); + host.showError(t('tui.statusMessages.sessionFailedToExport', { message: msg })); } } export async function handleExportDebugZipCommand(host: SlashCommandHost): Promise<void> { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } - host.showStatus('Exporting session…'); + host.showStatus(t('tui.statusMessages.sessionExportingDebug')); try { const installSource = await detectInstallSource(); const shellEnv = detectShellEnvironment(); @@ -150,17 +151,17 @@ export async function handleExportDebugZipCommand(host: SlashCommandHost): Promi includeGlobalLog: true, }); const linked = toTerminalHyperlink(result.zipPath, pathToFileURL(result.zipPath).href); - host.showNotice('Export complete', linked); + host.showNotice(t('tui.statusMessages.sessionExportDebugComplete'), linked); } catch (error) { const msg = formatErrorMessage(error); - host.showError(`Failed to export session: ${msg}`); + host.showError(t('tui.statusMessages.sessionFailedToExport', { message: msg })); } } export async function handleInitCommand(host: SlashCommandHost): Promise<void> { const session = host.session; if (host.state.appState.model.trim().length === 0 || session === undefined) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return; } @@ -179,7 +180,7 @@ export async function handleInitCommand(host: SlashCommandHost): Promise<void> { return; } const msg = error instanceof Error ? error.message : String(error); - host.failSessionRequest(`Init failed: ${msg}`); + host.failSessionRequest(t('tui.messages.sessionInitFailed', { msg })); } finally { host.deferUserMessages = false; } diff --git a/apps/kimi-code/src/tui/commands/swarm.ts b/apps/kimi-code/src/tui/commands/swarm.ts index 540aa58604..fc97ce04e5 100644 --- a/apps/kimi-code/src/tui/commands/swarm.ts +++ b/apps/kimi-code/src/tui/commands/swarm.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import type { PermissionMode } from '@moonshot-ai/kimi-code-sdk'; import { @@ -8,13 +9,13 @@ import { SwarmModeMarkerComponent, type SwarmModeMarkerState, } from '../components/messages/swarm-markers'; -import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { getLlmNotSetMessage, getNoActiveSessionMessage } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; export async function handleSwarmCommand(host: SlashCommandHost, args: string): Promise<void> { if (host.session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -31,12 +32,12 @@ export async function handleSwarmCommand(host: SlashCommandHost, args: string): } if (host.state.appState.model.trim().length === 0) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); return; } if (host.state.appState.permissionMode === 'manual') { - showSwarmStartPermissionPrompt(host, `/swarm ${prompt}`, 'Swarm task not started.', (choice) => + showSwarmStartPermissionPrompt(host, `/swarm ${prompt}`, t('tui.statusMessages.swarmTaskNotStarted'), (choice) => startSwarmWithPermission(host, prompt, choice), ); return; @@ -81,7 +82,7 @@ async function setPermissionForSwarm(host: SlashCommandHost, mode: PermissionMod try { await host.requireSession().setPermission(mode); } catch (error) { - host.showError(`Failed to set permission mode: ${formatErrorMessage(error)}`); + host.showError(t('tui.messages.swarmPermissionFailed', { error: formatErrorMessage(error) })); return false; } host.setAppState({ permissionMode: mode }); @@ -102,15 +103,15 @@ async function applySwarmMode( commandText: string, ): Promise<void> { if (enabled && host.state.appState.swarmMode) { - host.showStatus('Swarm mode is already on.'); + host.showStatus(t('tui.statusMessages.swarmModeAlreadyOn')); return; } if (!enabled && !host.state.appState.swarmMode) { - host.showStatus('Swarm mode is already off.'); + host.showStatus(t('tui.statusMessages.swarmModeAlreadyOff')); return; } if (enabled && host.state.appState.permissionMode === 'manual') { - showSwarmStartPermissionPrompt(host, commandText, 'Swarm mode not enabled.', async (choice) => { + showSwarmStartPermissionPrompt(host, commandText, t('tui.statusMessages.swarmModeNotEnabled'), async (choice) => { if ((choice === 'auto' || choice === 'yolo') && !(await setPermissionForSwarm(host, choice))) { return; } @@ -132,7 +133,7 @@ async function setSwarmMode( await host.requireSession().setSwarmMode(enabled, trigger); } catch (error) { host.showError( - `Failed to ${enabled ? 'enable' : 'disable'} swarm mode: ${formatErrorMessage(error)}`, + t('tui.messages.swarmToggleFailed', { action: enabled ? t('tui.messages.swarmEnable') : t('tui.messages.swarmDisable'), error: formatErrorMessage(error) }), ); return false; } diff --git a/apps/kimi-code/src/tui/commands/undo.ts b/apps/kimi-code/src/tui/commands/undo.ts index bd427277d4..a00aa19f52 100644 --- a/apps/kimi-code/src/tui/commands/undo.ts +++ b/apps/kimi-code/src/tui/commands/undo.ts @@ -2,6 +2,7 @@ import type { Component } from '@moonshot-ai/pi-tui'; import type { ContextMessage } from '@moonshot-ai/kimi-code-sdk'; import { isKimiError } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { WelcomeComponent } from '../components/chrome/welcome'; import { CompactionComponent } from '../components/dialogs/compaction'; import { @@ -19,7 +20,7 @@ import { PluginCommandComponent } from '../components/messages/plugin-command'; import { ThinkingComponent } from '../components/messages/thinking'; import { ToolCallComponent } from '../components/messages/tool-call'; import { UserMessageComponent } from '../components/messages/user-message'; -import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { getNoActiveSessionMessage } from '../constant/kimi-tui'; import type { TranscriptEntry } from '../types'; import { formatErrorMessage } from '../utils/event-payload'; import { getTranscriptComponentEntry } from '../utils/transcript-component-metadata'; @@ -46,7 +47,7 @@ export async function handleUndoCommand( args: string = '', ): Promise<void> { if (host.state.appState.streamingPhase !== 'idle') { - host.showError('Cannot undo while streaming — press Esc or Ctrl-C first.'); + host.showError(t('tui.statusMessages.undoCannotWhileStreaming')); return; } @@ -58,13 +59,13 @@ export async function handleUndoCommand( const count = parseUndoCount(trimmed); if (count === undefined) { - host.showError('Usage: /undo [count], where count is a positive integer.'); + host.showError(t('tui.statusMessages.undoUsage')); return; } const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -80,14 +81,14 @@ export async function handleUndoCommand( async function undoByCount(host: SlashCommandHost, count: number): Promise<boolean> { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return false; } const entries = host.state.transcriptEntries; const lastUserIndex = findUndoAnchorEntryIndex(entries, count); if (lastUserIndex === undefined) { - showUndoLimitStatus(host, 'Nothing to undo.'); + showUndoLimitStatus(host, t('tui.statusMessages.undoNothingToUndo')); return false; } @@ -100,7 +101,7 @@ async function undoByCount(host: SlashCommandHost, count: number): Promise<boole return false; } const message = formatErrorMessage(error); - host.showError(`Failed to undo: ${message}`); + host.showError(t('tui.statusMessages.undoFailed', { message })); return false; } @@ -126,7 +127,7 @@ async function undoByCount(host: SlashCommandHost, count: number): Promise<boole async function showUndoSelector(host: SlashCommandHost): Promise<void> { if (host.session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } @@ -296,20 +297,20 @@ function formatUndoChoiceLabel( entry.skillName ?? entry.content.replace(/^Activated skill:\s*/, ''), ); const args = singleLine(entry.skillArgs ?? ''); - if (name.length === 0) return 'Skill: unknown'; + if (name.length === 0) return t('tui.statusMessages.undoSkillUnknown'); return args.length > 0 ? `/${name} ${args}` : `/${name}`; } if (entry.kind === 'plugin_command' && entry.pluginCommandData !== undefined) { - return formatPluginCommandSlash(entry.pluginCommandData) ?? 'User message'; + return formatPluginCommandSlash(entry.pluginCommandData) ?? t('tui.statusMessages.undoUserMessage'); } const content = singleLine(entry.content); const imageCount = entry.imageAttachmentIds?.length ?? 0; if (content.length > 0) return content; if (imageCount > 0) { - return `User message (${String(imageCount)} ${imageCount === 1 ? 'image' : 'images'})`; + return t('tui.statusMessages.undoUserMessageWithImage', { count: imageCount }); } - return 'User message'; + return t('tui.statusMessages.undoUserMessage'); } function formatUndoChoiceInput(entry: TranscriptEntry): string { @@ -342,21 +343,15 @@ function formatUndoLimitMessage( requestedCount: number, availability: UndoAvailability, ): string { - const reason = availability.stoppedAtCompaction ? ' after the last compaction' : ''; - const requested = formatPromptCount(requestedCount); - const max = formatPromptCount(availability.maxCount); - return `Cannot undo ${requested}; only ${max} can be undone in the active context${reason}.`; + const reason = availability.stoppedAtCompaction ? t('tui.statusMessages.undoLimitAfterCompaction') : ''; + return t('tui.statusMessages.undoLimit', { requested: String(requestedCount), max: String(availability.maxCount), reason }); } function formatNothingToUndoMessage(availability: UndoAvailability): string { if (availability.stoppedAtCompaction) { - return 'Nothing to undo after the last compaction.'; + return t('tui.statusMessages.undoNothingToUndoAfterCompaction'); } - return 'Nothing to undo.'; -} - -function formatPromptCount(count: number): string { - return `${String(count)} ${count === 1 ? 'prompt' : 'prompts'}`; + return t('tui.statusMessages.undoNothingToUndo'); } function showUndoLimitStatus(host: SlashCommandHost, message: string): void { diff --git a/apps/kimi-code/src/tui/commands/web.ts b/apps/kimi-code/src/tui/commands/web.ts index 366860016e..31086f065e 100644 --- a/apps/kimi-code/src/tui/commands/web.ts +++ b/apps/kimi-code/src/tui/commands/web.ts @@ -2,9 +2,10 @@ import { ensureDaemon } from '#/cli/sub/server/daemon'; import { tryResolveServerToken } from '#/cli/sub/server/shared'; import { openUrl } from '#/utils/open-url'; import { getDataDir } from '#/utils/paths'; +import { t } from '#/i18n'; import { ChoicePickerComponent } from '../components/dialogs/choice-picker'; -import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { getNoActiveSessionMessage } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; @@ -22,26 +23,26 @@ const WEB_CANCEL = 'cancel'; export async function handleWebCommand(host: SlashCommandHost): Promise<void> { const session = host.session; if (session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } const sessionId = session.id; const confirmed = await new Promise<boolean>((resolve) => { const picker = new ChoicePickerComponent({ - title: 'Open current session in the Web UI?', - hint: '↑↓ navigate · Enter select · Esc cancel', + title: t('tui.statusMessages.openInWebUi'), + hint: t('tui.statusMessages.openInWebUiHint'), options: [ { value: WEB_CONFIRM, - label: 'Continue', + label: t('tui.statusMessages.continueLabel'), description: - 'Start the Kimi server (background daemon if needed), open this session in your default browser, and exit the terminal UI.', + t('tui.statusMessages.continueDescription'), }, { value: WEB_CANCEL, - label: 'Cancel', - description: 'Stay in the terminal UI.', + label: t('tui.statusMessages.cancelLabel'), + description: t('tui.statusMessages.stayInTui'), }, ], onSelect: (value) => { @@ -56,12 +57,12 @@ export async function handleWebCommand(host: SlashCommandHost): Promise<void> { host.restoreEditor(); if (!confirmed) return; - host.showStatus('Starting Kimi server and opening web UI…'); + host.showStatus(t('tui.statusMessages.startingServerAndWebUi')); let origin: string; try { ({ origin } = await ensureDaemon({})); } catch (error) { - host.showError(`Failed to start server: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToStartServer', { error: formatErrorMessage(error) })); return; } @@ -72,9 +73,9 @@ export async function handleWebCommand(host: SlashCommandHost): Promise<void> { // file, so we fall back to the plain URL and skip the token line. const token = tryResolveServerToken(getDataDir()); const url = webSessionUrl(origin, sessionId, token); - host.showStatus(`open ${url}`, 'success'); + host.showStatus(t('tui.statusMessages.webOpenUrl', { url }), 'success'); if (token !== undefined) { - host.showStatus(`Token: ${token}`, 'success'); + host.showStatus(t('tui.statusMessages.webToken', { token }), 'success'); } openUrl(url); host.setExitOpenUrl(url); diff --git a/apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts b/apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts index d95d9ff932..cbd716792a 100644 --- a/apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts +++ b/apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts @@ -9,12 +9,13 @@ import { } from '@moonshot-ai/pi-tui'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; export type ApiKeyInputResult = | { readonly kind: 'ok'; readonly value: string } | { readonly kind: 'cancel' }; -const FOOTER = 'Enter to submit · Esc to cancel'; +const FOOTER = t('tui.dialogs.apiKeyInput.footer'); function maskInputLine(raw: string): string { const prefix = '> '; @@ -58,7 +59,7 @@ export class ApiKeyInputDialogComponent extends Container implements Focusable { ) { super(); this.onDone = onDone; - this.title = `Enter API key for ${platformName}`; + this.title = t('tui.dialogs.apiKeyInput.title', { platformName }); this.subtitleLines = subtitleLines; this.input.onSubmit = (value) => { this.submit(value); @@ -96,7 +97,7 @@ export class ApiKeyInputDialogComponent extends Container implements Focusable { const border = (s: string): string => currentTheme.fg('primary', s); const titleStyled = currentTheme.boldFg('textStrong', this.title); - const subtitleSource = this.emptyHinted ? ['API key cannot be empty.'] : this.subtitleLines; + const subtitleSource = this.emptyHinted ? [t('tui.dialogs.apiKeyInput.emptyHint')] : this.subtitleLines; const subtitleLines = subtitleSource.map((line) => truncateToWidth(currentTheme.fg('textDim', line), innerWidth, '…'), ); diff --git a/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts b/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts index e18f5709b3..bd395988c3 100644 --- a/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts +++ b/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts @@ -16,6 +16,7 @@ import { wrapTextWithAnsi, } from '@moonshot-ai/pi-tui'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight'; import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview'; import type { @@ -109,7 +110,7 @@ function renderDisplayBlock( case 'diff': return renderDiffLinesClustered(block.old_text, block.new_text, block.path, { contextLines: 3, - expandKeyHint: 'ctrl+e to preview', + expandKeyHint: t('tui.dialogs.approvalPanel.previewHint'), maxLines: DIFF_SUMMARY_MAX_LINES, }); case 'file_content': { @@ -124,7 +125,12 @@ function renderDisplayBlock( if (remaining > 0) { lines.push( s.dim( - ` … ${String(remaining)} more line${remaining > 1 ? 's' : ''} hidden (ctrl+e to preview)`, + ` ${t( + remaining === 1 + ? 'tui.dialogs.approvalPanel.moreLinesHidden_one' + : 'tui.dialogs.approvalPanel.moreLinesHidden_other', + { count: remaining, previewHint: t('tui.dialogs.approvalPanel.previewHint') }, + )}`, ), ); } @@ -191,17 +197,17 @@ function isDuplicateBriefBlock(block: DisplayBlock, description: string): boolea function headerFor(toolName: string): string { switch (toolName) { case 'Bash': - return 'Run this command?'; + return t('tui.dialogs.approvalPanel.headerForBash'); case 'Write': - return 'Write this file?'; + return t('tui.dialogs.approvalPanel.headerForWrite'); case 'Edit': - return 'Apply these edits?'; + return t('tui.dialogs.approvalPanel.headerForEdit'); case 'TaskStop': - return 'Stop this task?'; + return t('tui.dialogs.approvalPanel.headerForTaskStop'); case 'ExitPlanMode': - return 'Ready to build with this plan?'; + return t('tui.dialogs.approvalPanel.headerForExitPlanMode'); default: - return `Approve ${toolName}?`; + return t('tui.dialogs.approvalPanel.headerForDefault', { toolName }); } } @@ -395,13 +401,13 @@ export class ApprovalPanelComponent extends Container implements Focusable { lines.push(''); if (this.feedbackMode) { - lines.push(indent(dim('Type feedback · ↵ submit.'))); + lines.push(indent(dim(t('tui.dialogs.approvalPanel.feedbackHint')))); } else { - const expandHint = hasPreviewable ? ' · ctrl+e preview' : ''; + const expandHint = hasPreviewable ? t('tui.dialogs.approvalPanel.expandHint') : ''; lines.push( indent( dim( - `↑/↓ select · ${buildNumericHint(data.choices.length)} choose · ↵ confirm${expandHint}`, + t('tui.dialogs.approvalPanel.navHint', { numeric: buildNumericHint(data.choices.length), expand: expandHint }), ), ), ); diff --git a/apps/kimi-code/src/tui/components/dialogs/approval-preview.ts b/apps/kimi-code/src/tui/components/dialogs/approval-preview.ts index 044884792e..96ef55b795 100644 --- a/apps/kimi-code/src/tui/components/dialogs/approval-preview.ts +++ b/apps/kimi-code/src/tui/components/dialogs/approval-preview.ts @@ -31,6 +31,7 @@ import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview'; import type { DiffDisplayBlock, FileContentDisplayBlock } from '#/tui/reverse-rpc/types'; import { currentTheme } from '#/tui/theme'; import { printableChar } from '#/tui/utils/printable-key'; +import { t } from '#/i18n'; const ELLIPSIS = '…'; @@ -150,7 +151,7 @@ export class ApprovalPreviewViewer extends Container implements Focusable { } private renderHeader(width: number): string { - const title = currentTheme.boldFg('primary', ' Preview '); + const title = currentTheme.boldFg('primary', t('tui.dialogs.approvalPreview.title')); return fitExactly(title + this.headerTitle, width); } @@ -190,11 +191,7 @@ export class ApprovalPreviewViewer extends Container implements Focusable { 'textMuted', ` ${String(lineFrom)}-${String(lineTo)} / ${String(total)} (${String(percent)}%) `, ); - const keys = - `${key('↑↓')} ${dim('line')} ` + - `${key('PgUp/PgDn')} ${dim('page')} ` + - `${key('g/G')} ${dim('top/bot')} ` + - `${key('Q/Esc/Ctrl+E')} ${dim('cancel')}`; + const keys = t('tui.dialogs.approvalPreview.footerKeys'); const left = ` ${keys}`; const leftW = visibleWidth(left); const rightW = visibleWidth(position); diff --git a/apps/kimi-code/src/tui/components/dialogs/choice-picker.ts b/apps/kimi-code/src/tui/components/dialogs/choice-picker.ts index b6b74fe5b9..6e794341dd 100644 --- a/apps/kimi-code/src/tui/components/dialogs/choice-picker.ts +++ b/apps/kimi-code/src/tui/components/dialogs/choice-picker.ts @@ -20,6 +20,7 @@ import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols'; import { currentTheme, type ColorToken } from '#/tui/theme'; import { printableChar } from '#/tui/utils/printable-key'; import { SearchableList } from '#/tui/utils/searchable-list'; +import { t } from '#/i18n'; export interface ChoiceOption { /** Value passed to onSelect (e.g. the actual editor command string). */ @@ -135,13 +136,10 @@ export class ChoicePickerComponent extends Container implements Focusable { // Header mirrors the model dialog (see model-selector.ts): border, title // with a "(type to search)" suffix until you type, the hint, a blank, then // the search line. Key vocabulary is lowercase to match every list dialog. - const navParts = ['↑↓ navigate']; - if (view.page.pageCount > 1) navParts.push('←→ page'); - navParts.push('Enter select', 'Esc cancel'); - const hint = this.opts.hint ?? navParts.join(' · '); + const hint = this.opts.hint ?? t('tui.dialogs.choicePicker.navHint'); const titleSuffix = - searchable && view.query.length === 0 ? currentTheme.fg('textMuted', ' (type to search)') : ''; + searchable && view.query.length === 0 ? currentTheme.fg('textMuted', ` (${t('tui.dialogs.choicePicker.searchHint')})`) : ''; const hintLines = hint.split(/\r?\n/); const lines: string[] = [ currentTheme.fg('primary', '─'.repeat(width)), @@ -169,7 +167,7 @@ export class ChoicePickerComponent extends Container implements Focusable { } if (options.length === 0) { - lines.push(currentTheme.fg('textMuted', ' No matches')); + lines.push(currentTheme.fg('textMuted', ` ${t('tui.dialogs.choicePicker.noMatches')}`)); } for (let i = view.page.start; i < view.page.end; i++) { const opt = options[i]!; @@ -197,7 +195,7 @@ export class ChoicePickerComponent extends Container implements Focusable { if (view.page.pageCount > 1) { lines.push( currentTheme.fg('textMuted', - ` Page ${String(view.page.page + 1)}/${String(view.page.pageCount)}`, + ` ${t('tui.dialogs.choicePicker.page', { page: view.page.page + 1, pageCount: view.page.pageCount })}`, ), ); } diff --git a/apps/kimi-code/src/tui/components/dialogs/compaction.ts b/apps/kimi-code/src/tui/components/dialogs/compaction.ts index 9ade9350c7..d5c405b26d 100644 --- a/apps/kimi-code/src/tui/components/dialogs/compaction.ts +++ b/apps/kimi-code/src/tui/components/dialogs/compaction.ts @@ -18,6 +18,7 @@ import type { TUI } from '@moonshot-ai/pi-tui'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; const BLINK_INTERVAL = 500; @@ -148,25 +149,25 @@ export class CompactionComponent extends Container { private buildHeader(): string { if (this.done) { const bullet = currentTheme.fg('success', STATUS_BULLET); - const label = currentTheme.boldFg('success', 'Compaction complete'); + const label = currentTheme.boldFg('success', t('tui.dialogs.compaction.complete')); const detail = this.tokensBefore !== undefined && this.tokensAfter !== undefined - ? currentTheme.dim(` (${String(this.tokensBefore)} → ${String(this.tokensAfter)} tokens)`) + ? currentTheme.dim(` ${t('tui.dialogs.compaction.detailTokens', { before: this.tokensBefore, after: this.tokensAfter })}`) : ''; const shortcutHint = this.summary !== undefined && this.summary.length > 0 - ? currentTheme.dim(` (Ctrl-O to ${this.expanded ? 'hide' : 'show'} compaction summary)`) + ? currentTheme.dim(` ${t('tui.dialogs.compaction.shortcutHint', { action: this.expanded ? t('tui.dialogs.compaction.hide') : t('tui.dialogs.compaction.show') })}`) : ''; return `${bullet}${label}${detail}${shortcutHint}`; } if (this.canceled) { const bullet = currentTheme.fg('warning', STATUS_BULLET); - const label = currentTheme.boldFg('warning', 'Compaction cancelled'); + const label = currentTheme.boldFg('warning', t('tui.dialogs.compaction.cancelled')); return `${bullet}${label}`; } const bullet = this.blinkOn ? currentTheme.fg('text', STATUS_BULLET) : ' '; - const label = currentTheme.boldFg('primary', 'Compacting context...'); - const tip = this.tip ? currentTheme.fg('textDim', ` · Tip: ${this.tip}`) : ''; + const label = currentTheme.boldFg('primary', t('tui.dialogs.compaction.compacting')); + const tip = this.tip ? currentTheme.fg('textDim', t('tui.dialogs.compaction.tipPrefix', { tip: this.tip })) : ''; return `${bullet}${label}${tip}`; } diff --git a/apps/kimi-code/src/tui/components/dialogs/custom-registry-import.ts b/apps/kimi-code/src/tui/components/dialogs/custom-registry-import.ts index aa4aa910b2..a339d1d2cc 100644 --- a/apps/kimi-code/src/tui/components/dialogs/custom-registry-import.ts +++ b/apps/kimi-code/src/tui/components/dialogs/custom-registry-import.ts @@ -20,6 +20,7 @@ import { } from '@moonshot-ai/pi-tui'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; export interface CustomRegistryImportValue { readonly url: string; @@ -30,12 +31,12 @@ export type CustomRegistryImportResult = | { readonly kind: 'ok'; readonly value: CustomRegistryImportValue } | { readonly kind: 'cancel' }; -const TITLE = 'Import custom provider registry'; -const SUBTITLE_DEFAULT = 'Paste an api.json URL and its Bearer token.'; -const SUBTITLE_URL_EMPTY = 'Registry URL cannot be empty.'; -const SUBTITLE_TOKEN_EMPTY = 'Bearer token cannot be empty.'; -const FOOTER_NOT_LAST = 'Tab / ↑↓ to switch · Enter for next field · Esc to cancel'; -const FOOTER_LAST = 'Tab / ↑↓ to switch · Enter to submit · Esc to cancel'; +const TITLE = t('tui.dialogs.customRegistryImport.title'); +const SUBTITLE_DEFAULT = t('tui.dialogs.customRegistryImport.subtitleDefault'); +const SUBTITLE_URL_EMPTY = t('tui.dialogs.customRegistryImport.subtitleUrlEmpty'); +const SUBTITLE_TOKEN_EMPTY = t('tui.dialogs.customRegistryImport.subtitleTokenEmpty'); +const FOOTER_NOT_LAST = t('tui.dialogs.customRegistryImport.footerNotLast'); +const FOOTER_LAST = t('tui.dialogs.customRegistryImport.footerLast'); type FieldId = 'url' | 'token'; @@ -156,8 +157,8 @@ export class CustomRegistryImportDialogComponent extends Container implements Fo this.activeField === 'url' ? FOOTER_NOT_LAST : FOOTER_LAST, ); - const urlLabelText = 'Registry URL'; - const tokenLabelText = 'Bearer token'; + const urlLabelText = t('tui.dialogs.customRegistryImport.urlLabel'); + const tokenLabelText = t('tui.dialogs.customRegistryImport.tokenLabel'); const urlLabelStyled = this.activeField === 'url' ? currentTheme.boldFg('accent', urlLabelText) diff --git a/apps/kimi-code/src/tui/components/dialogs/editor-selector.ts b/apps/kimi-code/src/tui/components/dialogs/editor-selector.ts index 9e98a457bf..1628c1f253 100644 --- a/apps/kimi-code/src/tui/components/dialogs/editor-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/editor-selector.ts @@ -1,11 +1,12 @@ import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; +import { t } from '#/i18n'; const EDITOR_OPTIONS: readonly ChoiceOption[] = [ - { value: 'code --wait', label: 'VS Code (code --wait)' }, - { value: 'vim', label: 'Vim' }, - { value: 'nvim', label: 'Neovim' }, - { value: 'nano', label: 'Nano' }, - { value: '', label: 'Auto-detect ($VISUAL / $EDITOR)' }, + { value: 'code --wait', label: t('tui.dialogs.editorSelector.vsCode') }, + { value: 'vim', label: t('tui.dialogs.editorSelector.vim') }, + { value: 'nvim', label: t('tui.dialogs.editorSelector.neovim') }, + { value: 'nano', label: t('tui.dialogs.editorSelector.nano') }, + { value: '', label: t('tui.dialogs.editorSelector.autoDetect') }, ]; export interface EditorSelectorOptions { @@ -17,11 +18,11 @@ export interface EditorSelectorOptions { export class EditorSelectorComponent extends ChoicePickerComponent { constructor(opts: EditorSelectorOptions) { super({ - title: 'Select external editor', + title: t('tui.dialogs.editorSelector.title'), options: [...EDITOR_OPTIONS], currentValue: opts.currentValue, onSelect: opts.onSelect, onCancel: opts.onCancel, }); } -} +} \ No newline at end of file diff --git a/apps/kimi-code/src/tui/components/dialogs/effort-selector.ts b/apps/kimi-code/src/tui/components/dialogs/effort-selector.ts index 2678899ad9..2ca9b9f35d 100644 --- a/apps/kimi-code/src/tui/components/dialogs/effort-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/effort-selector.ts @@ -8,6 +8,7 @@ import { import type { ThinkingEffort } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; import { effortLabel } from './model-selector'; @@ -68,13 +69,13 @@ export class EffortSelectorComponent extends Container implements Focusable { } override render(width: number): string[] { - const hintParts = ['←→ switch', 'Enter select']; - if (this.opts.onSessionOnlySelect !== undefined) hintParts.push('Alt+S session-only'); - hintParts.push('Esc cancel'); + const hintParts = [t('tui.dialogs.effortSelector.hintSwitch'), t('tui.dialogs.effortSelector.hintSelect')]; + if (this.opts.onSessionOnlySelect !== undefined) hintParts.push(t('tui.dialogs.effortSelector.hintSessionOnly')); + hintParts.push(t('tui.dialogs.effortSelector.hintCancel')); const lines: string[] = [ currentTheme.fg('primary', '─'.repeat(width)), - currentTheme.boldFg('primary', ` ${this.opts.title ?? 'Select thinking effort'}`), + currentTheme.boldFg('primary', ` ${this.opts.title ?? t('tui.dialogs.effortSelector.title')}`), currentTheme.fg('textMuted', ` ${hintParts.join(' · ')}`), '', ]; diff --git a/apps/kimi-code/src/tui/components/dialogs/experiments-selector.ts b/apps/kimi-code/src/tui/components/dialogs/experiments-selector.ts index 1e466f8739..255a47859e 100644 --- a/apps/kimi-code/src/tui/components/dialogs/experiments-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/experiments-selector.ts @@ -10,6 +10,7 @@ import type { ExperimentalFeatureState } from '@moonshot-ai/kimi-code-sdk'; import { SELECT_POINTER } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; import { printableChar } from '#/tui/utils/printable-key'; import { SearchableList } from '#/tui/utils/searchable-list'; @@ -66,25 +67,25 @@ export class ExperimentsSelectorComponent extends Container implements Focusable override render(width: number): string[] { const view = this.list.view(); const titleSuffix = - view.query.length === 0 ? currentTheme.fg('textMuted', ' (type to search)') : ''; - const hintParts = ['↑↓ navigate']; - if (view.page.pageCount > 1) hintParts.push('PgUp/PgDn page'); - hintParts.push('Space toggle', 'Enter apply', 'Esc cancel'); - if (view.query.length > 0) hintParts.push('Backspace clear'); + view.query.length === 0 ? currentTheme.fg('textMuted', ` ${t('tui.dialogs.modelSelector.searchHint')}`) : ''; + const hintParts = [t('tui.dialogs.experimentsSelector.hintNavigate')]; + if (view.page.pageCount > 1) hintParts.push(t('tui.dialogs.experimentsSelector.hintPage')); + hintParts.push(t('tui.dialogs.experimentsSelector.hintSpace'), t('tui.dialogs.experimentsSelector.hintEnter'), t('tui.dialogs.experimentsSelector.hintCancel')); + if (view.query.length > 0) hintParts.push(t('tui.dialogs.experimentsSelector.hintBackspace')); const lines: string[] = [ currentTheme.fg('primary', '─'.repeat(width)), - currentTheme.boldFg('primary', ' Experimental features') + titleSuffix, + currentTheme.boldFg('primary', ` ${t('tui.dialogs.experimentsSelector.title')}`) + titleSuffix, currentTheme.fg('textMuted', ` ${hintParts.join(' · ')}`), '', ]; if (view.query.length > 0) { - lines.push(currentTheme.fg('primary', ` Search: `) + currentTheme.fg('text', view.query)); + lines.push(currentTheme.fg('primary', ` ${t('tui.dialogs.modelSelector.searchLabel')}`) + currentTheme.fg('text', view.query)); } if (view.items.length === 0) { - lines.push(currentTheme.fg('textMuted', ' No matches')); + lines.push(currentTheme.fg('textMuted', ' ' + t('tui.dialogs.modelSelector.noMatches'))); } for (let i = view.page.start; i < view.page.end; i++) { @@ -105,7 +106,7 @@ export class ExperimentsSelectorComponent extends Container implements Focusable lines.push( currentTheme.fg( 'textMuted', - ` ▼ ${String(view.items.length - view.page.end)} more`, + ` ${t('tui.dialogs.experimentsSelector.more', { count: view.items.length - view.page.end })}`, ), ); } @@ -146,9 +147,16 @@ export class ExperimentsSelectorComponent extends Container implements Focusable private renderApplyButton(): string { const changes = this.draftChanges(); const count = changes.length; - const label = '[ Apply changes and reload ]'; + const label = t('tui.dialogs.experimentsSelector.applyButton'); const summary = - count === 0 ? 'no changes' : `${String(count)} ${count === 1 ? 'change' : 'changes'}`; + count === 0 + ? t('tui.dialogs.experimentsSelector.noChanges') + : t( + count === 1 + ? 'tui.dialogs.experimentsSelector.changeCount_one' + : 'tui.dialogs.experimentsSelector.changeCount_other', + { count }, + ); const button = count === 0 ? currentTheme.fg('textDim', label) : currentTheme.boldFg('primary', label); @@ -167,10 +175,10 @@ export class ExperimentsSelectorComponent extends Container implements Focusable const prefix = currentTheme.fg(selected ? 'primary' : 'textDim', ` ${pointer} `); const label = selected ? currentTheme.boldFg('primary', feature.title) : currentTheme.fg('text', feature.title); const enabled = this.effectiveEnabled(feature); - const status = enabled ? 'enabled' : 'disabled'; + const status = enabled ? t('tui.dialogs.experimentsSelector.statusEnabled') : t('tui.dialogs.experimentsSelector.statusDisabled'); const statusText = enabled ? currentTheme.fg('success', status) : currentTheme.fg('textDim', status); const detail = this.isDraftChanged(feature) - ? `${featureDetail(feature)} · modified` + ? `${featureDetail(feature)}${t('tui.dialogs.experimentsSelector.modifiedSuffix')}` : featureDetail(feature); const lines = [ `${prefix}${label} ${statusText}`, @@ -190,22 +198,23 @@ function isLocked(feature: ExperimentalFeatureState): boolean { function featureDetail(feature: ExperimentalFeatureState): string { const source = sourceLabel(feature); + const idPart = t('tui.dialogs.experimentsSelector.featureId', { id: feature.id }); if (feature.source === 'env' || feature.source === 'master-env') { - return `id ${feature.id} · ${source}`; + return `${idPart} · ${source}`; } - return `id ${feature.id} · ${source} · ${feature.env}`; + return `${idPart} · ${source} · ${feature.env}`; } function sourceLabel(feature: ExperimentalFeatureState): string { switch (feature.source) { case 'master-env': - return 'locked by KIMI_CODE_EXPERIMENTAL_FLAG'; + return t('tui.dialogs.experimentsSelector.lockedByMasterEnv'); case 'env': - return `locked by ${feature.env}`; + return t('tui.dialogs.experimentsSelector.lockedBy', { env: feature.env }); case 'config': - return 'config'; + return t('tui.dialogs.experimentsSelector.sourceConfig'); case 'default': - return 'default'; + return t('tui.dialogs.experimentsSelector.sourceDefault'); } } diff --git a/apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts b/apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts index c1f108dd79..bb7a373ca7 100644 --- a/apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts +++ b/apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts @@ -21,15 +21,16 @@ import { type Focusable, } from '@moonshot-ai/pi-tui'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; export type FeedbackInputDialogResult = | { readonly kind: 'ok'; readonly value: string } | { readonly kind: 'cancel' }; -const TITLE = 'Send feedback to Kimi Code'; -const SUBTITLE_DEFAULT = "Tell us what's working or what's not."; -const SUBTITLE_EMPTY = 'Feedback cannot be empty.'; -const FOOTER = 'Enter to submit · Esc to cancel'; +const TITLE = t('tui.dialogs.feedbackInput.title'); +const SUBTITLE_DEFAULT = t('tui.dialogs.feedbackInput.subtitleDefault'); +const SUBTITLE_EMPTY = t('tui.dialogs.feedbackInput.subtitleEmpty'); +const FOOTER = t('tui.dialogs.feedbackInput.footer'); export class FeedbackInputDialogComponent extends Container implements Focusable { focused = false; diff --git a/apps/kimi-code/src/tui/components/dialogs/goal-queue-manager.ts b/apps/kimi-code/src/tui/components/dialogs/goal-queue-manager.ts index b5c2e7ac1b..3e23612829 100644 --- a/apps/kimi-code/src/tui/components/dialogs/goal-queue-manager.ts +++ b/apps/kimi-code/src/tui/components/dialogs/goal-queue-manager.ts @@ -9,6 +9,7 @@ import { } from '@moonshot-ai/pi-tui'; import chalk from 'chalk'; +import { t } from '#/i18n'; import { SELECT_POINTER } from '#/tui/constant/symbols'; import type { GoalQueueMoveDirection, @@ -115,17 +116,17 @@ export class GoalQueueManagerComponent extends Container implements Focusable { override render(width: number): string[] { const view = this.list.view(); const hint = this.movingGoalId === undefined - ? '↑↓ navigate · Space select · E edit · D delete · Esc cancel' - : '↑↓ reorder · Space done · E edit · D delete · Esc cancel'; + ? t('tui.dialogs.goalQueueManager.navHint') + : t('tui.dialogs.goalQueueManager.reorderHint'); const lines: string[] = [ currentTheme.fg('primary', '─'.repeat(width)), - currentTheme.boldFg('primary', ' Upcoming goals'), + currentTheme.boldFg('primary', ` ${t('tui.dialogs.goalQueueManager.title')}`), currentTheme.fg('textMuted', ` ${hint}`), '', ]; if (this.goals.length === 0) { - lines.push(currentTheme.fg('textMuted', ' No upcoming goals.')); + lines.push(currentTheme.fg('textMuted', ` ${t('tui.dialogs.goalQueueManager.empty')}`)); } else { for (let i = view.page.start; i < view.page.end; i++) { const goal = view.items[i]; @@ -136,7 +137,7 @@ export class GoalQueueManagerComponent extends Container implements Focusable { const below = view.items.length - view.page.end; if (below > 0) { lines.push(''); - lines.push(currentTheme.fg('textMuted', ` ▼ ${String(below)} more`)); + lines.push(currentTheme.fg('textMuted', ` ${t('tui.dialogs.goalQueueManager.more', { count: below })}`)); } } @@ -150,7 +151,7 @@ export class GoalQueueManagerComponent extends Container implements Focusable { const pointer = selected ? SELECT_POINTER : ' '; const prefix = currentTheme.fg(selected ? 'primary' : 'textDim', ` ${pointer} `); const labelPrefix = `${String(index + 1)}. `; - const stateLabel = moving ? ' selected' : ''; + const stateLabel = moving ? ` ${t('tui.dialogs.goalQueueManager.selected')}` : ''; const labelWidth = visibleWidth(labelPrefix); const stateWidth = visibleWidth(stateLabel); const objectiveWidth = Math.max(1, width - 5 - labelWidth - stateWidth); @@ -247,21 +248,21 @@ export class GoalQueueEditDialogComponent extends Container implements Focusable const pad = ' '; const border = (s: string): string => currentTheme.fg('primary', s); const title = truncateToWidth( - currentTheme.boldFg('textStrong', 'Edit upcoming goal'), + currentTheme.boldFg('textStrong', t('tui.dialogs.goalQueueEdit.title')), innerWidth, ELLIPSIS, ); const subtitle = truncateToWidth( currentTheme.fg( this.error === undefined ? 'textDim' : 'warning', - this.error ?? 'Update the queued objective.', + this.error ?? t('tui.dialogs.goalQueueEdit.subtitle'), ), innerWidth, ELLIPSIS, ); const inputLines = this.input.render(innerWidth); const footer = truncateToWidth( - currentTheme.fg('textDim', 'Enter submit · Shift-Enter/Ctrl-J newline · Esc cancel'), + currentTheme.fg('textDim', t('tui.dialogs.goalQueueEdit.footer')), innerWidth, ELLIPSIS, ); @@ -291,11 +292,11 @@ export class GoalQueueEditDialogComponent extends Container implements Focusable private submit(value: string): void { const objective = value.trim(); if (objective.length === 0) { - this.error = 'Goal objective cannot be empty.'; + this.error = t('tui.dialogs.goalQueueEdit.errorEmpty'); return; } if (objective.length > MAX_GOAL_OBJECTIVE_LENGTH) { - this.error = `Goal objective cannot exceed ${MAX_GOAL_OBJECTIVE_LENGTH} characters.`; + this.error = t('tui.dialogs.goalQueueEdit.errorTooLong', { max: MAX_GOAL_OBJECTIVE_LENGTH }); return; } this.opts.onDone({ kind: 'save', goalId: this.opts.goal.id, objective }); @@ -390,7 +391,7 @@ class MultilineGoalInput { const rendered: string[] = []; if (range.start > 0) { - rendered.push(padInputLine(` ${ELLIPSIS} ${String(range.start)} previous`, safeWidth)); + rendered.push(padInputLine(` ${t('tui.dialogs.goalQueueEdit.previous', { count: range.start })}`, safeWidth)); } for (let lineIndex = range.start; lineIndex < range.end; lineIndex++) { @@ -405,7 +406,7 @@ class MultilineGoalInput { const remaining = logicalLines.length - range.end; if (remaining > 0) { - rendered.push(padInputLine(` ${ELLIPSIS} ${String(remaining)} more`, safeWidth)); + rendered.push(padInputLine(` ${t('tui.dialogs.goalQueueEdit.more', { count: remaining })}`, safeWidth)); } return rendered; diff --git a/apps/kimi-code/src/tui/components/dialogs/goal-start-permission-prompt.ts b/apps/kimi-code/src/tui/components/dialogs/goal-start-permission-prompt.ts index e60d85ce02..02ac9ef5a8 100644 --- a/apps/kimi-code/src/tui/components/dialogs/goal-start-permission-prompt.ts +++ b/apps/kimi-code/src/tui/components/dialogs/goal-start-permission-prompt.ts @@ -1,3 +1,5 @@ +import { t } from '#/i18n'; + import { StartPermissionPromptComponent, type StartPermissionOption, @@ -11,70 +13,65 @@ export interface GoalStartPermissionPromptOptions { readonly onCancel: () => void; } -export const GOAL_START_MANUAL_OPTIONS: readonly StartPermissionOption[] = [ - { - value: 'auto', - label: 'Switch to Auto and start', - description: - 'Best if you want Kimi Code to keep working while you are away. Tools are approved automatically, and questions are skipped.', - }, - { - value: 'yolo', - label: 'Switch to YOLO and start', - description: - 'Tools and plan changes are approved automatically. Kimi Code may still ask you questions.', - }, - { - value: 'manual', - label: 'Start in Manual', - description: - 'Keep approvals on. Kimi Code will ask before risky actions, so the goal may stop and wait for you.', - }, - { - value: 'cancel', - label: 'Do not start', - description: 'Return to the input box with your goal command.', - }, -]; +export function goalStartManualOptions(): readonly StartPermissionOption[] { + return [ + { + value: 'auto', + label: t('tui.dialogs.goalStartPermissionPrompt.optionAutoLabel'), + description: t('tui.dialogs.goalStartPermissionPrompt.optionAutoDesc'), + }, + { + value: 'yolo', + label: t('tui.dialogs.goalStartPermissionPrompt.optionYoloLabel'), + description: t('tui.dialogs.goalStartPermissionPrompt.optionYoloDesc'), + }, + { + value: 'manual', + label: t('tui.dialogs.goalStartPermissionPrompt.optionManualLabel'), + description: t('tui.dialogs.goalStartPermissionPrompt.optionManualDesc'), + }, + { + value: 'cancel', + label: t('tui.dialogs.goalStartPermissionPrompt.optionCancelLabel'), + description: t('tui.dialogs.goalStartPermissionPrompt.optionCancelDesc'), + }, + ]; +} -export const GOAL_START_YOLO_OPTIONS: readonly StartPermissionOption[] = [ - { - value: 'auto', - label: 'Switch to Auto and start', - description: - 'Best if you want Kimi Code to keep working while you are away. Tools are approved automatically, and questions are skipped.', - }, - { - value: 'yolo', - label: 'Keep YOLO and start', - description: - 'Tools and plan changes stay approved automatically. Kimi Code may still ask you questions.', - }, - { - value: 'cancel', - label: 'Do not start', - description: 'Return to the input box with your goal command.', - }, -]; +export function goalStartYoloOptions(): readonly StartPermissionOption[] { + return [ + { + value: 'auto', + label: t('tui.dialogs.goalStartPermissionPrompt.optionAutoLabel'), + description: t('tui.dialogs.goalStartPermissionPrompt.optionAutoDesc'), + }, + { + value: 'yolo', + label: t('tui.dialogs.goalStartPermissionPrompt.optionYoloKeepLabel'), + description: t('tui.dialogs.goalStartPermissionPrompt.optionYoloKeepDesc'), + }, + { + value: 'cancel', + label: t('tui.dialogs.goalStartPermissionPrompt.optionCancelLabel'), + description: t('tui.dialogs.goalStartPermissionPrompt.optionCancelDesc'), + }, + ]; +} export function goalStartOptions(mode: 'manual' | 'yolo'): readonly StartPermissionOption[] { - return mode === 'yolo' ? GOAL_START_YOLO_OPTIONS : GOAL_START_MANUAL_OPTIONS; + return mode === 'yolo' ? goalStartYoloOptions() : goalStartManualOptions(); } -const MANUAL_OPTIONS = GOAL_START_MANUAL_OPTIONS; - -const YOLO_OPTIONS = GOAL_START_YOLO_OPTIONS; - const MANUAL_NOTICE_LINES = [ - 'Manual mode asks you before Kimi Code runs commands, edits files, or takes other risky actions.', - 'Manual mode is not suitable for unattended goal work.', - 'You can go back without losing your command.', + t('tui.dialogs.goalStartPermissionPrompt.notice1'), + t('tui.dialogs.goalStartPermissionPrompt.notice2'), + t('tui.dialogs.goalStartPermissionPrompt.notice3'), ] as const; const YOLO_NOTICE_LINES = [ - 'YOLO mode approves tools and plan changes automatically.', - 'YOLO mode can still stop for questions.', - 'Switch to Auto if you want questions skipped during goal work.', + t('tui.dialogs.goalStartPermissionPrompt.yoloNotice1'), + t('tui.dialogs.goalStartPermissionPrompt.yoloNotice2'), + t('tui.dialogs.goalStartPermissionPrompt.yoloNotice3'), ] as const; export class GoalStartPermissionPromptComponent extends StartPermissionPromptComponent { @@ -82,10 +79,10 @@ export class GoalStartPermissionPromptComponent extends StartPermissionPromptCom super({ title: opts.mode === 'yolo' - ? 'Start a goal in YOLO mode?' - : 'Start a goal with approvals on?', + ? t('tui.dialogs.goalStartPermissionPrompt.titleYolo') + : t('tui.dialogs.goalStartPermissionPrompt.titleManual'), noticeLines: opts.mode === 'yolo' ? YOLO_NOTICE_LINES : MANUAL_NOTICE_LINES, - options: opts.mode === 'yolo' ? YOLO_OPTIONS : MANUAL_OPTIONS, + options: opts.mode === 'yolo' ? goalStartYoloOptions() : goalStartManualOptions(), onSelect: opts.onSelect, onCancel: opts.onCancel, }); diff --git a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts index 10fd5d5fe8..e376cb77fd 100644 --- a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts +++ b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts @@ -17,6 +17,7 @@ import { truncateToWidth, } from '@moonshot-ai/pi-tui'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; export interface KeyboardShortcut { readonly keys: string; @@ -30,19 +31,21 @@ export interface HelpPanelCommand { } /** Static list — keep in sync with the global editor bindings. */ -export const DEFAULT_KEYBOARD_SHORTCUTS: readonly KeyboardShortcut[] = [ - { keys: 'Shift-Tab', description: 'Toggle plan mode' }, - { keys: 'Ctrl-G', description: 'Edit in external editor ($VISUAL / $EDITOR)' }, - { keys: 'Ctrl-O', description: 'Toggle tool output / compaction summary expansion' }, - { keys: 'Ctrl-T', description: 'Expand / collapse the todo list (when truncated)' }, - { keys: 'Ctrl-S', description: 'Steer — inject a follow-up during streaming' }, - { keys: 'Shift-Enter / Ctrl-J', description: 'Insert newline' }, - { keys: 'Ctrl-C', description: 'Interrupt stream / clear input' }, - { keys: 'Ctrl-D', description: 'Exit (on empty input)' }, - { keys: 'Esc', description: 'Close dialogs / interrupt streaming' }, - { keys: '↑ / ↓', description: 'Browse input history' }, - { keys: 'Enter', description: 'Submit' }, -]; +export function getDefaultKeyboardShortcuts(): readonly KeyboardShortcut[] { + return [ + { keys: 'Shift-Tab', description: t('tui.dialogs.helpPanel.shortcuts.shiftTab') }, + { keys: 'Ctrl-G', description: t('tui.dialogs.helpPanel.shortcuts.ctrlG') }, + { keys: 'Ctrl-O', description: t('tui.dialogs.helpPanel.shortcuts.ctrlO') }, + { keys: 'Ctrl-T', description: t('tui.dialogs.helpPanel.shortcuts.ctrlT') }, + { keys: 'Ctrl-S', description: t('tui.dialogs.helpPanel.shortcuts.ctrlS') }, + { keys: 'Shift-Enter / Ctrl-J', description: t('tui.dialogs.helpPanel.shortcuts.shiftEnter') }, + { keys: 'Ctrl-C', description: t('tui.dialogs.helpPanel.shortcuts.ctrlC') }, + { keys: 'Ctrl-D', description: t('tui.dialogs.helpPanel.shortcuts.ctrlD') }, + { keys: 'Esc', description: t('tui.dialogs.helpPanel.shortcuts.esc') }, + { keys: '↑ / ↓', description: t('tui.dialogs.helpPanel.shortcuts.arrowUpDown') }, + { keys: 'Enter', description: t('tui.dialogs.helpPanel.shortcuts.enter') }, + ]; +} export interface HelpPanelOptions { readonly commands: readonly HelpPanelCommand[]; @@ -97,7 +100,7 @@ export class HelpPanelComponent extends Container implements Focusable { const kbdColor = (text: string) => currentTheme.fg('warning', text); const slashColor = (text: string) => currentTheme.fg('primary', text); - const shortcuts = this.opts.shortcuts ?? DEFAULT_KEYBOARD_SHORTCUTS; + const shortcuts = this.opts.shortcuts ?? getDefaultKeyboardShortcuts(); const kbdWidth = Math.max(8, ...shortcuts.map((s) => s.keys.length)); const sortedCmds = [...this.opts.commands].toSorted(compareSlashCommandsForDisplay); const cmdLabels = sortedCmds.map((c) => { @@ -107,17 +110,18 @@ export class HelpPanelComponent extends Container implements Focusable { const cmdWidth = Math.max(12, ...cmdLabels.map((l) => l.length)); const lines: string[] = [ accent('─'.repeat(width)), - currentTheme.boldFg('primary', ' help ') + muted('· Esc / Enter / q to cancel · ↑↓ scroll'), + currentTheme.boldFg('primary', t('tui.dialogs.helpPanel.title')) + + muted(t('tui.dialogs.helpPanel.cancelHint')), '', // Greeting - ` ${dim('Sure, Kimi is ready to help! Just send a message to get started.')}`, + ` ${dim(t('tui.dialogs.helpPanel.greeting'))}`, '', // Section: keyboard shortcuts - ` ${currentTheme.bold('Keyboard shortcuts')}`, + ` ${currentTheme.bold(t('tui.dialogs.helpPanel.keyboardShortcuts'))}`, ...shortcuts.map((s) => ` ${kbdColor(s.keys.padEnd(kbdWidth))} ${dim(s.description)}`), '', // Section: slash commands - ` ${currentTheme.bold('Slash commands')}`, + ` ${currentTheme.bold(t('tui.dialogs.helpPanel.slashCommands'))}`, ...sortedCmds.map((cmd, i) => { const label = cmdLabels[i] ?? `/${cmd.name}`; return ` ${slashColor(label.padEnd(cmdWidth))} ${dim(cmd.description)}`; @@ -132,9 +136,9 @@ export class HelpPanelComponent extends Container implements Focusable { if (content.length > maxVisible) { this.scrollTop = Math.max(0, Math.min(this.scrollTop, content.length - maxVisible)); const slice = content.slice(this.scrollTop, this.scrollTop + maxVisible); - const scrollInfo = muted( - ` showing ${String(this.scrollTop + 1)}-${String(this.scrollTop + slice.length)} of ${String(content.length)}`, - ); + const from = this.scrollTop + 1; + const to = this.scrollTop + slice.length; + const scrollInfo = muted(t('tui.dialogs.helpPanel.showing', { from, to, total: content.length })); return [lines[0] ?? '', ...slice, scrollInfo, lines.at(-1) ?? ''].map((line) => truncateToWidth(line, width), ); diff --git a/apps/kimi-code/src/tui/components/dialogs/locale-selector.ts b/apps/kimi-code/src/tui/components/dialogs/locale-selector.ts new file mode 100644 index 0000000000..7a2e1d0e19 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/locale-selector.ts @@ -0,0 +1,41 @@ +import type { Locale } from '#/i18n'; +import { t } from '#/i18n'; + +import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; + +const LOCALE_OPTIONS: readonly ChoiceOption[] = [ + { + value: 'en', + label: t('tui.dialogs.localeSelector.enLabel'), + description: t('tui.dialogs.localeSelector.enDesc'), + }, + { + value: 'zh', + label: t('tui.dialogs.localeSelector.zhLabel'), + description: t('tui.dialogs.localeSelector.zhDesc'), + }, +]; + +function isLocaleChoice(value: string): value is Locale { + return value === 'en' || value === 'zh'; +} + +export interface LocaleSelectorOptions { + readonly currentValue: Locale; + readonly onSelect: (locale: Locale) => void; + readonly onCancel: () => void; +} + +export class LocaleSelectorComponent extends ChoicePickerComponent { + constructor(opts: LocaleSelectorOptions) { + super({ + title: t('tui.dialogs.localeSelector.title'), + options: [...LOCALE_OPTIONS], + currentValue: opts.currentValue, + onSelect: (value) => { + if (isLocaleChoice(value)) opts.onSelect(value); + }, + onCancel: opts.onCancel, + }); + } +} \ No newline at end of file diff --git a/apps/kimi-code/src/tui/components/dialogs/model-selector.ts b/apps/kimi-code/src/tui/components/dialogs/model-selector.ts index 82906d1d50..820f7c8b03 100644 --- a/apps/kimi-code/src/tui/components/dialogs/model-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/model-selector.ts @@ -11,11 +11,13 @@ import { import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '#/constant/app'; import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; import { SearchableList } from '#/tui/utils/searchable-list'; import type { ChoiceOption } from './choice-picker'; -type ThinkingAvailability = 'toggle' | 'always-on' | 'unsupported'; +type ThinkingAvailabilityKind = 'toggle' | 'always-on' | 'unsupported'; +type ThinkingAvailability = string; interface ModelChoice { readonly alias: string; @@ -89,13 +91,25 @@ function createModelChoices(models: Record<string, ModelAlias>): readonly ModelC }); } -export function thinkingAvailability(model: ModelAlias): ThinkingAvailability { +export function thinkingAvailabilityKind(model: ModelAlias): ThinkingAvailabilityKind { const caps = model.capabilities ?? []; if (caps.includes('always_thinking')) return 'always-on'; if (caps.includes('thinking') || model.adaptiveThinking === true) return 'toggle'; return 'unsupported'; } +export function thinkingAvailability(model: ModelAlias): ThinkingAvailability { + const kind = thinkingAvailabilityKind(model); + switch (kind) { + case 'always-on': + return t('tui.dialogs.modelSelector.thinkingAlwaysOn'); + case 'toggle': + return t('tui.dialogs.modelSelector.thinkingToggle'); + case 'unsupported': + return t('tui.dialogs.modelSelector.thinkingUnsupported'); + } +} + export function effortsOf(model: ModelAlias): readonly string[] { return model.supportEfforts ?? []; } @@ -108,7 +122,7 @@ export function effortsOf(model: ModelAlias): readonly string[] { */ export function segmentsFor(model: ModelAlias): readonly string[] { const efforts = effortsOf(model); - const availability = thinkingAvailability(model); + const availability = thinkingAvailabilityKind(model); if (efforts.length > 0) { return availability === 'always-on' ? efforts : ['off', ...efforts]; } @@ -128,7 +142,7 @@ export function effortLabel(effort: string): string { * thinking is unsupported. */ function defaultThinkingEffortFor(model: ModelAlias): ThinkingEffort { - if (thinkingAvailability(model) === 'unsupported') return 'off'; + if (thinkingAvailabilityKind(model) === 'unsupported') return 'off'; const efforts = effortsOf(model); if (efforts.length > 0) { return model.defaultEffort ?? efforts[Math.floor(efforts.length / 2)]!; @@ -194,7 +208,7 @@ export class ModelSelectorComponent extends Container implements Focusable { if (def !== undefined && efforts.includes(def)) return def; return efforts[0]!; } - return thinkingAvailability(choice.model) !== 'unsupported' ? 'on' : 'off'; + return thinkingAvailabilityKind(choice.model) !== 'unsupported' ? 'on' : 'off'; } /** Draft coerced onto the model's segment list so rendering/selection never @@ -269,32 +283,32 @@ export class ModelSelectorComponent extends Container implements Focusable { const titleSuffix = searchable && view.query.length === 0 - ? currentTheme.fg('textMuted', ' (type to search)') + ? currentTheme.fg('textMuted', ` ${t('tui.dialogs.modelSelector.searchHint')}`) : ''; // "type to search" already lives in the title suffix, so the hint only // surfaces the backspace shortcut once a query is active. const hintParts: string[] = []; - if (this.opts.providerSwitchHint) hintParts.push('Tab toggle provider'); - hintParts.push('↑↓ navigate'); - if (searchable && view.query.length > 0) hintParts.push('Backspace clear'); - hintParts.push('Enter select'); - if (this.opts.onSessionOnlySelect !== undefined) hintParts.push('Alt+S session-only'); - hintParts.push('Esc cancel'); + if (this.opts.providerSwitchHint) hintParts.push(t('tui.dialogs.modelSelector.hintTab')); + hintParts.push(t('tui.dialogs.modelSelector.hintNavigate')); + if (searchable && view.query.length > 0) hintParts.push(t('tui.dialogs.modelSelector.hintBackspace')); + hintParts.push(t('tui.dialogs.modelSelector.hintSelect')); + if (this.opts.onSessionOnlySelect !== undefined) hintParts.push(t('tui.dialogs.modelSelector.hintSessionOnly')); + hintParts.push(t('tui.dialogs.modelSelector.hintCancel')); const lines: string[] = [ currentTheme.fg('primary', '─'.repeat(width)), - currentTheme.boldFg('primary', ' Select a model') + titleSuffix, + currentTheme.boldFg('primary', ` ${t('tui.dialogs.modelSelector.title')}`) + titleSuffix, currentTheme.fg('textMuted', ' ' + hintParts.join(' · ')), '', ]; if (searchable && view.query.length > 0) { - lines.push(currentTheme.fg('primary', ' Search: ') + currentTheme.fg('text', view.query)); + lines.push(currentTheme.fg('primary', ` ${t('tui.dialogs.modelSelector.searchLabel')}`) + currentTheme.fg('text', view.query)); } if (view.items.length === 0) { - lines.push(currentTheme.fg('textMuted', ' No matches')); + lines.push(currentTheme.fg('textMuted', ' ' + t('tui.dialogs.modelSelector.noMatches'))); } else { // Column width for model names so the provider column lines up. Capped so // the provider + "← current" marker still fit on normal terminal widths. @@ -328,13 +342,16 @@ export class ModelSelectorComponent extends Container implements Focusable { if (view.query.length > 0) { lines.push(''); lines.push( - currentTheme.fg('textMuted', ` ${String(view.items.length)} / ${String(totalCount)}`), + currentTheme.fg( + 'textMuted', + ` ${t('tui.dialogs.modelSelector.count', { matches: view.items.length, total: totalCount })}`, + ), ); } else { const below = view.items.length - view.page.end; if (below > 0) { lines.push(''); - lines.push(currentTheme.fg('textMuted', ` ▼ ${String(below)} more`)); + lines.push(currentTheme.fg('textMuted', ` ${t('tui.dialogs.modelSelector.more', { count: below })}`)); } } @@ -342,7 +359,7 @@ export class ModelSelectorComponent extends Container implements Focusable { const selected = this.selectedChoice(); if (selected !== undefined) { const canSwitch = segmentsFor(selected.model).length > 1; - const thinkingHeader = canSwitch ? ' Thinking (←→ to switch)' : ' Thinking'; + const thinkingHeader = canSwitch ? t('tui.dialogs.modelSelector.thinkingSwitchable') : t('tui.dialogs.modelSelector.thinking'); lines.push(currentTheme.fg('textMuted', thinkingHeader)); lines.push(this.renderThinkingControl(selected)); } @@ -363,17 +380,20 @@ export class ModelSelectorComponent extends Container implements Focusable { // The whole segment is muted, suffix included, so the disabled side reads // as a single greyed-out control rather than a selectable option. const unavailable = (label: string): string => - currentTheme.fg('textMuted', ` ${label} (Unsupported) `); + currentTheme.fg( + 'textMuted', + ` ${label} (${t('tui.dialogs.modelSelector.unsupported')}) `, + ); // Non-effort always-on / unsupported models keep the original On/Off layout // so the control never shifts while moving across legacy models. const efforts = effortsOf(choice.model); - const availability = thinkingAvailability(choice.model); + const availability = thinkingAvailabilityKind(choice.model); if (efforts.length === 0 && availability === 'always-on') { - return ` ${segment('On', true)} ${unavailable('Off')}`; + return ` ${segment(t('tui.dialogs.modelSelector.on'), true)} ${unavailable(t('tui.dialogs.modelSelector.off'))}`; } if (efforts.length === 0 && availability === 'unsupported') { - return ` ${unavailable('On')} ${segment('Off', true)}`; + return ` ${unavailable(t('tui.dialogs.modelSelector.on'))} ${segment(t('tui.dialogs.modelSelector.off'), true)}`; } const segments = segmentsFor(choice.model); diff --git a/apps/kimi-code/src/tui/components/dialogs/permission-selector.ts b/apps/kimi-code/src/tui/components/dialogs/permission-selector.ts index 4b2db673d3..8cf8a21617 100644 --- a/apps/kimi-code/src/tui/components/dialogs/permission-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/permission-selector.ts @@ -1,24 +1,27 @@ import type { PermissionMode } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; -const PERMISSION_OPTIONS: readonly ChoiceOption[] = [ - { - value: 'manual', - label: 'Manual', - description: 'Approve every action yourself.', - }, - { - value: 'auto', - label: 'Auto', - description: 'Run all actions automatically, including risky ones.', - }, - { - value: 'yolo', - label: 'YOLO', - description: 'AI decides which actions need your approval.', - }, -]; +function getPermissionOptions(): readonly ChoiceOption[] { + return [ + { + value: 'manual', + label: t('tui.dialogs.permissionSelector.manual'), + description: t('tui.dialogs.permissionSelector.manualDesc'), + }, + { + value: 'auto', + label: t('tui.dialogs.permissionSelector.auto'), + description: t('tui.dialogs.permissionSelector.autoDesc'), + }, + { + value: 'yolo', + label: t('tui.dialogs.permissionSelector.yolo'), + description: t('tui.dialogs.permissionSelector.yoloDesc'), + }, + ]; +} function isPermissionModeChoice(value: string): value is PermissionMode { return value === 'manual' || value === 'auto' || value === 'yolo'; @@ -33,8 +36,8 @@ export interface PermissionSelectorOptions { export class PermissionSelectorComponent extends ChoicePickerComponent { constructor(opts: PermissionSelectorOptions) { super({ - title: 'Select permission mode', - options: [...PERMISSION_OPTIONS], + title: t('tui.dialogs.permissionSelector.title'), + options: [...getPermissionOptions()], currentValue: opts.currentValue, onSelect: (value) => { if (isPermissionModeChoice(value)) opts.onSelect(value); @@ -42,4 +45,4 @@ export class PermissionSelectorComponent extends ChoicePickerComponent { onCancel: opts.onCancel, }); } -} +} \ No newline at end of file diff --git a/apps/kimi-code/src/tui/components/dialogs/platform-selector.ts b/apps/kimi-code/src/tui/components/dialogs/platform-selector.ts index a332f70af6..a7bc34f001 100644 --- a/apps/kimi-code/src/tui/components/dialogs/platform-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/platform-selector.ts @@ -1,9 +1,10 @@ import { OPEN_PLATFORMS } from '@moonshot-ai/kimi-code-oauth'; +import { t } from '#/i18n'; import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; const PLATFORM_OPTIONS: readonly ChoiceOption[] = [ - { value: 'kimi-code', label: 'Kimi Code (OAuth)' }, + { value: 'kimi-code', label: t('tui.dialogs.platformSelector.kimiCode') }, ...OPEN_PLATFORMS.map((platform) => ({ value: platform.id, label: platform.name })), ]; @@ -15,7 +16,7 @@ export interface PlatformSelectorOptions { export class PlatformSelectorComponent extends ChoicePickerComponent { constructor(opts: PlatformSelectorOptions) { super({ - title: 'Select a platform', + title: t('tui.dialogs.platformSelector.title'), options: [...PLATFORM_OPTIONS], onSelect: opts.onSelect, onCancel: opts.onCancel, diff --git a/apps/kimi-code/src/tui/components/dialogs/plugins-selector.ts b/apps/kimi-code/src/tui/components/dialogs/plugins-selector.ts index 64ec286148..ae45e01b39 100644 --- a/apps/kimi-code/src/tui/components/dialogs/plugins-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/plugins-selector.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import { Container, Input, @@ -16,7 +17,7 @@ import type { ColorPalette } from '#/tui/theme/colors'; import { formatPluginSourceLabel, pluginTrustLabel } from '#/tui/utils/plugin-source-label'; import { printableChar } from '#/tui/utils/printable-key'; import { renderTabStrip } from '#/tui/utils/tab-strip'; -import { computeUpdateStatus, type PluginMarketplaceEntry } from '#/utils/plugin-marketplace'; +import { computeUpdateStatus, type PluginMarketplaceEntry, type PluginUpdateStatus } from '#/utils/plugin-marketplace'; import { ChoicePickerComponent } from './choice-picker'; @@ -32,14 +33,14 @@ const ELLIPSIS = '…'; // Official tab, even when the marketplace catalog is unavailable. Selecting it // opens the install page in the browser rather than installing from a source, // because Web Bridge is a browser extension + daemon, not a plugin package. -const WEB_BRIDGE_URL = 'https://www.kimi.com/features/webbridge#local-agent'; +const WEB_BRIDGE_URL = 'https://www.kimi.com/features/webbridge'; const WEB_BRIDGE_ENTRY: PluginMarketplaceEntry = { id: 'kimi-webbridge', displayName: 'Kimi WebBridge', source: WEB_BRIDGE_URL, tier: 'official', homepage: WEB_BRIDGE_URL, - description: 'Control your real browser from Kimi Code — navigate, click, type, and screenshot', + description: t('tui.dialogs.pluginsSelector.webBridgeDescription'), }; // Only the hardcoded pinned row should open the WebBridge install page. Match @@ -53,7 +54,10 @@ interface PluginsOverviewItem { readonly value: string; readonly kind: 'plugin' | 'action'; readonly label: string; + /** Internal status token used for styling logic (kept in English). */ readonly status?: string; + /** Translated status label shown to the user. */ + readonly statusLabel?: string; readonly description: string; } @@ -129,14 +133,22 @@ export class PluginMcpSelectorComponent extends Container implements Focusable { const actionItems = this.items.filter((item) => item.kind === 'action'); const lines: string[] = [ chalk.hex(colors.primary)('─'.repeat(width)), - chalk.hex(colors.primary).bold(` MCP servers · ${info.displayName}`), - mutedHintLine(' ↑↓ navigate · Enter/Space enable/disable · Esc cancel', colors), + chalk.hex(colors.primary).bold( + ` ${t('tui.dialogs.pluginsSelector.mcpServersTitle', { name: info.displayName })}`, + ), + mutedHintLine(t('tui.dialogs.pluginsSelector.mcpNavHint'), colors), '', - sectionLabel(`MCP servers (${info.enabledMcpServerCount}/${info.mcpServerCount} enabled)`, colors), + sectionLabel( + t('tui.dialogs.pluginsSelector.mcpServersSection', { + enabled: info.enabledMcpServerCount, + total: info.mcpServerCount, + }), + colors, + ), ]; if (serverItems.length === 0) { - lines.push(chalk.hex(colors.textMuted)(' No MCP servers declared.')); + lines.push(chalk.hex(colors.textMuted)(` ${t('tui.dialogs.pluginsSelector.noMcpServers')}`)); } else { for (let i = 0; i < serverItems.length; i++) { lines.push(...this.renderItem(serverItems[i]!, i, width)); @@ -144,7 +156,7 @@ export class PluginMcpSelectorComponent extends Container implements Focusable { } lines.push(''); - lines.push(sectionLabel('Actions', colors)); + lines.push(sectionLabel(t('tui.dialogs.pluginsSelector.actionsSection'), colors)); for (let i = 0; i < actionItems.length; i++) { lines.push(...this.renderItem(actionItems[i]!, serverItems.length + i, width)); } @@ -161,8 +173,8 @@ export class PluginMcpSelectorComponent extends Container implements Focusable { const labelStyle = selected ? chalk.hex(colors.primary).bold : chalk.hex(colors.text); const prefix = chalk.hex(selected ? colors.primary : colors.textDim)(` ${pointer} `); let line = prefix + labelStyle(item.label); - if (item.status !== undefined) { - line += ' ' + statusStyle(item, colors)(item.status); + if (item.status !== undefined && item.statusLabel !== undefined) { + line += ' ' + statusStyle(item, colors)(item.statusLabel); } const serverName = mcpItemServerName(item); if (serverName !== undefined && this.opts.serverHint?.server === serverName) { @@ -190,20 +202,20 @@ export interface PluginRemoveConfirmOptions { export class PluginRemoveConfirmComponent extends ChoicePickerComponent { constructor(opts: PluginRemoveConfirmOptions) { super({ - title: `Remove ${opts.displayName} (${opts.id})?`, - hint: '↑↓ navigate · Enter/Space select · ←/Esc cancel', + title: t('tui.dialogs.pluginsSelector.removeConfirmTitle', { name: opts.displayName, id: opts.id }), + hint: t('tui.dialogs.pluginsSelector.removeConfirmHint'), formatHint: mutedHintLine, options: [ { value: REMOVE_CONFIRM_CANCEL, - label: 'Cancel', - description: 'Keep this plugin installed.', + label: t('tui.dialogs.pluginsSelector.removeCancelLabel'), + description: t('tui.dialogs.pluginsSelector.removeCancelDesc'), }, { value: REMOVE_CONFIRM_REMOVE, - label: 'Remove plugin', + label: t('tui.dialogs.pluginsSelector.removeConfirmLabel'), tone: 'danger', - description: 'Remove only the install record; plugin files are left in place.', + description: t('tui.dialogs.pluginsSelector.removeConfirmDesc'), }, ], onSelect: (value) => { @@ -234,25 +246,22 @@ export interface PluginInstallTrustConfirmOptions { export class PluginInstallTrustConfirmComponent extends ChoicePickerComponent { constructor(opts: PluginInstallTrustConfirmOptions) { super({ - title: `Install third-party plugin ${opts.label}?`, - hint: '↑↓ navigate · Enter/Space select · ←/Esc cancel', + title: t('tui.dialogs.pluginsSelector.installTrustTitle', { label: opts.label }), + hint: t('tui.dialogs.pluginsSelector.installTrustHint'), formatHint: mutedHintLine, - notice: - '⚠️ This is a third-party plugin that Kimi has not reviewed. It can bundle MCP servers, ' + - 'skills, or files that run code and access your workspace. Install it only if you ' + - 'trust the source.', + notice: t('tui.dialogs.pluginsSelector.installTrustNotice'), noticeTone: 'warning', options: [ { value: INSTALL_TRUST_EXIT, - label: 'Exit', - description: 'Cancel the installation.', + label: t('tui.dialogs.pluginsSelector.installTrustExitLabel'), + description: t('tui.dialogs.pluginsSelector.installTrustExitDesc'), }, { value: INSTALL_TRUST_TRUST, - label: 'Trust and install', + label: t('tui.dialogs.pluginsSelector.installTrustTrustLabel'), tone: 'danger', - description: 'Install this third-party plugin anyway.', + description: t('tui.dialogs.pluginsSelector.installTrustTrustDesc'), }, ], onSelect: (value) => { @@ -266,16 +275,29 @@ export class PluginInstallTrustConfirmComponent extends ChoicePickerComponent { } function overviewPluginDescription(plugin: PluginSummary): string { - const state = plugin.state === 'ok' ? '' : ` · state ${plugin.state}`; - const skills = `${plugin.skillCount} skill${plugin.skillCount === 1 ? '' : 's'}`; + const state = + plugin.state === 'ok' + ? '' + : ` · ${t('tui.dialogs.pluginsSelector.pluginState', { state: plugin.state })}`; + const skills = t( + plugin.skillCount === 1 + ? 'tui.dialogs.pluginsSelector.skillCount_one' + : 'tui.dialogs.pluginsSelector.skillCount_other', + { count: plugin.skillCount }, + ); const mcp = plugin.mcpServerCount > 0 - ? ` · MCP ${plugin.enabledMcpServerCount}/${plugin.mcpServerCount}` + ? ` · ${t('tui.dialogs.pluginsSelector.mcpCount', { + enabled: plugin.enabledMcpServerCount, + total: plugin.mcpServerCount, + })}` : ''; - const diagnostics = plugin.hasErrors ? ' · diagnostics available' : ''; + const diagnostics = plugin.hasErrors + ? ` · ${t('tui.dialogs.pluginsSelector.diagnosticsAvailable')}` + : ''; const source = ` · ${formatPluginSourceLabel(plugin)}`; const trust = ` · ${pluginTrustLabel(plugin)}`; - return `id ${plugin.id} · ${skills}${mcp}${source}${trust}${state}${diagnostics}`; + return `${t('tui.dialogs.pluginsSelector.pluginId', { id: plugin.id })} · ${skills}${mcp}${source}${trust}${state}${diagnostics}`; } function pluginStatus(plugin: PluginSummary): string | undefined { @@ -283,6 +305,17 @@ function pluginStatus(plugin: PluginSummary): string | undefined { return plugin.enabled ? 'enabled' : 'disabled'; } +function pluginStatusLabel(status: string): string { + switch (status) { + case 'enabled': + return t('tui.dialogs.pluginsSelector.statusEnabled'); + case 'disabled': + return t('tui.dialogs.pluginsSelector.statusDisabled'); + default: + return status; + } +} + function marketplaceStatusStyle(status: string, colors: ColorPalette): (text: string) => string { // "update …" is a warning (actionable); "installed …" is success; // "install …" is the available action. @@ -348,10 +381,10 @@ type MarketState = | { readonly status: 'loaded'; readonly entries: readonly PluginMarketplaceEntry[]; readonly source: string }; const PLUGINS_PANEL_TABS: readonly { id: PluginsPanelTabId; label: string }[] = [ - { id: 'installed', label: 'Installed' }, - { id: 'official', label: 'Official' }, - { id: 'third-party', label: 'Third-party' }, - { id: 'custom', label: 'Custom' }, + { id: 'installed', label: t('tui.dialogs.pluginsSelector.tabInstalled') }, + { id: 'official', label: t('tui.dialogs.pluginsSelector.tabOfficial') }, + { id: 'third-party', label: t('tui.dialogs.pluginsSelector.tabThirdParty') }, + { id: 'custom', label: t('tui.dialogs.pluginsSelector.tabCustom') }, ]; export class PluginsPanelComponent extends Container implements Focusable { @@ -573,11 +606,11 @@ export class PluginsPanelComponent extends Container implements Focusable { tab === 'installed' ? this.installedHint() : tab === 'custom' - ? ' Tab switch · Enter install · Esc cancel' - : ' Tab switch · ↑↓ navigate · Enter open/install · Esc cancel'; + ? t('tui.dialogs.pluginsSelector.tabHintCustom') + : t('tui.dialogs.pluginsSelector.tabHintMarketplace'); const lines: string[] = [ chalk.hex(colors.primary)('─'.repeat(width)), - chalk.hex(colors.primary).bold(' Plugins'), + chalk.hex(colors.primary).bold(` ${t('tui.dialogs.pluginsSelector.panelTitle')}`), mutedHintLine(hint, colors), '', renderTabStrip({ @@ -602,21 +635,28 @@ export class PluginsPanelComponent extends Container implements Focusable { const { installed } = this.opts; const colors = currentTheme.palette; if (installed.length === 0) { - lines.push(chalk.hex(colors.textMuted)(' No plugins installed.')); + lines.push(chalk.hex(colors.textMuted)(` ${t('tui.dialogs.pluginsSelector.noPluginsInstalled')}`)); } else { for (let i = 0; i < installed.length; i++) { lines.push(...this.renderInstalledRow(installed[i]!, i, width)); } } lines.push(''); - lines.push(mutedHintLine(` ${installed.length} installed`, colors)); + lines.push( + mutedHintLine( + ` ${t('tui.dialogs.pluginsSelector.countInstalled', { count: installed.length })}`, + colors, + ), + ); } private installedHint(): string { const plugin = this.opts.installed[this.selectedIndex]; const hasUpdate = plugin !== undefined && this.installedUpdateStatus(plugin) !== undefined; - const enter = hasUpdate ? 'Enter update' : 'Enter details'; - return ` Tab switch · Space toggle · D remove · M MCP · ${enter} · I details · R reload · Esc cancel`; + const enterAction = hasUpdate + ? t('tui.dialogs.pluginsSelector.enterUpdate') + : t('tui.dialogs.pluginsSelector.enterDetails'); + return t('tui.dialogs.pluginsSelector.tabHintInstalled', { enterAction }); } private installedUpdateStatus( @@ -636,14 +676,18 @@ export class PluginsPanelComponent extends Container implements Focusable { const labelStyle = selected ? chalk.hex(colors.primary).bold : chalk.hex(colors.text); const prefix = chalk.hex(selected ? colors.primary : colors.textDim)(` ${pointer} `); const status = pluginStatus(plugin); + const statusLabel = status === undefined ? undefined : pluginStatusLabel(status); const update = this.installedUpdateStatus(plugin); let line = prefix + labelStyle(plugin.displayName); - if (status !== undefined) { - line += ' ' + statusStyle({ kind: 'plugin', value: '', label: '', description: '', status }, colors)(status); + if (status !== undefined && statusLabel !== undefined) { + line += ' ' + statusStyle({ kind: 'plugin', value: '', label: '', description: '', status }, colors)(statusLabel); } if (update !== undefined) { - const badge = `update ${update.local} → ${update.latest}`; - line += ' ' + marketplaceStatusStyle(badge, colors)(badge); + const badge = t('tui.dialogs.pluginsSelector.updateStatus', { + local: update.local, + latest: update.latest, + }); + line += ' ' + marketplaceStatusStyle(`update ${update.local}`, colors)(badge); } if (this.opts.pluginHint?.id === plugin.id) { line += ' ' + chalk.hex(colors.warning)(this.opts.pluginHint.text); @@ -664,16 +708,22 @@ export class PluginsPanelComponent extends Container implements Focusable { ): void { const colors = currentTheme.palette; if (this.market.status === 'loading' || this.market.status === 'idle') { - lines.push(chalk.hex(colors.textMuted)(' Loading marketplace…')); + lines.push(chalk.hex(colors.textMuted)(` ${t('tui.dialogs.pluginsSelector.loadingMarketplace')}`)); return; } if (this.market.status === 'error') { - lines.push(chalk.hex(colors.warning)(` Marketplace unavailable: ${this.market.message}`)); - lines.push(mutedHintLine(' Use the Custom tab to install from a URL.', colors)); + lines.push( + chalk.hex(colors.warning)( + ` ${t('tui.dialogs.pluginsSelector.marketplaceUnavailable', { message: this.market.message })}`, + ), + ); + lines.push( + mutedHintLine(` ${t('tui.dialogs.pluginsSelector.useCustomTabHint')}`, colors), + ); return; } if (entries.length === 0) { - lines.push(chalk.hex(colors.textMuted)(' No plugins found.')); + lines.push(chalk.hex(colors.textMuted)(` ${t('tui.dialogs.pluginsSelector.noPluginsFound')}`)); } else { for (let i = 0; i < entries.length; i++) { lines.push(...this.renderMarketplaceRow(entries[i]!, i + indexOffset, width)); @@ -682,9 +732,20 @@ export class PluginsPanelComponent extends Container implements Focusable { const installedCount = entries.filter((e) => this.opts.installedIds.has(e.id)).length; lines.push(''); lines.push( - mutedHintLine(` ${installedCount} installed · ${entries.length - installedCount} available`, colors), + mutedHintLine( + ` ${t('tui.dialogs.pluginsSelector.marketplaceCount', { + installed: installedCount, + available: entries.length - installedCount, + })}`, + colors, + ), + ); + lines.push( + mutedHintLine( + ` ${t('tui.dialogs.pluginsSelector.marketplaceSource', { source: this.market.source })}`, + colors, + ), ); - lines.push(mutedHintLine(` Source: ${this.market.source}`, colors)); } private renderOfficial(lines: string[], width: number): void { @@ -706,10 +767,11 @@ export class PluginsPanelComponent extends Container implements Focusable { const labelStyle = selected ? chalk.hex(colors.primary).bold : chalk.hex(colors.text); const prefix = chalk.hex(selected ? colors.primary : colors.textDim)(` ${pointer} `); const status = isPinnedWebBridgeEntry(entry) - ? 'open in browser' + ? 'open-in-browser' : marketplaceEntryStatus(entry, this.installedVersions); + const statusLabel = marketplaceStatusLabel(status); const line = - prefix + labelStyle(entry.displayName) + ' ' + marketplaceStatusStyle(status, colors)(status); + prefix + labelStyle(entry.displayName) + ' ' + marketplaceStatusStyle(status, colors)(statusLabel); const descWidth = Math.max(1, width - 4); const out = [line]; for (const descLine of wrapOverviewDescription(marketplaceEntryDescription(entry), descWidth)) { @@ -720,7 +782,7 @@ export class PluginsPanelComponent extends Container implements Focusable { private renderCustom(lines: string[], width: number): void { const colors = currentTheme.palette; - lines.push(mutedHintLine(' Install from a GitHub URL (or zip URL / local path):', colors)); + lines.push(mutedHintLine(` ${t('tui.dialogs.pluginsSelector.installFromUrlHint')}`, colors)); lines.push(''); lines.push(...renderUrlInputBox(this.customInput, this.focused, width, colors)); } @@ -729,9 +791,11 @@ export class PluginsPanelComponent extends Container implements Focusable { const colors = currentTheme.palette; const lines = [ chalk.hex(colors.primary)('─'.repeat(width)), - chalk.hex(colors.primary).bold(' Plugins'), + chalk.hex(colors.primary).bold(` ${t('tui.dialogs.pluginsSelector.panelTitle')}`), '', - chalk.hex(colors.textMuted)(` Installing ${this.installing} from marketplace…`), + chalk.hex(colors.textMuted)( + ` ${t('tui.dialogs.pluginsSelector.installingFromMarketplace', { label: this.installing ?? '' })}`, + ), '', chalk.hex(colors.primary)('─'.repeat(width)), ]; @@ -740,31 +804,44 @@ export class PluginsPanelComponent extends Container implements Focusable { } function buildMcpItems(info: PluginInfo): PluginsOverviewItem[] { - const items: PluginsOverviewItem[] = info.mcpServers.map((server) => ({ - value: `${MCP_SERVER_PREFIX}${server.name}`, - kind: 'plugin', - label: server.name, - status: server.enabled ? 'enabled' : 'disabled', - description: mcpServerDescription(server), - })); + const items: PluginsOverviewItem[] = info.mcpServers.map((server) => { + const status = server.enabled ? 'enabled' : 'disabled'; + return { + value: `${MCP_SERVER_PREFIX}${server.name}`, + kind: 'plugin', + label: server.name, + status, + statusLabel: t(`tui.dialogs.pluginsSelector.status${status.charAt(0).toUpperCase() + status.slice(1)}`), + description: mcpServerDescription(server), + }; + }); items.push({ value: 'back', kind: 'action', - label: 'Back to installed plugins', - description: 'Return to the local plugin manager.', + label: t('tui.dialogs.pluginsSelector.backToInstalled'), + description: t('tui.dialogs.pluginsSelector.backToInstalledDesc'), }); return items; } function mcpServerDescription(server: PluginMcpServerInfo): string { - const action = server.enabled ? 'Enter/Space disable' : 'Enter/Space enable'; + const action = server.enabled ? t('tui.dialogs.pluginsSelector.mcpDisable') : t('tui.dialogs.pluginsSelector.mcpEnable'); if (server.transport === 'http' || server.transport === 'sse') { - return `${action} · ${server.transport.toUpperCase()} · ${server.url ?? server.runtimeName}`; + return t('tui.dialogs.pluginsSelector.mcpServerTransportHint', { + action, + transport: server.transport.toUpperCase(), + target: server.url ?? server.runtimeName, + }); } const args = server.args !== undefined && server.args.length > 0 ? ` ${server.args.join(' ')}` : ''; const command = `${server.command ?? ''}${args}`.trim(); - const cwd = server.cwd === undefined ? '' : ` · cwd ${server.cwd}`; - return `${action} · stdio · ${command || server.runtimeName}${cwd}`; + const base = t('tui.dialogs.pluginsSelector.mcpServerStdioHint', { + action, + command: command || server.runtimeName, + }); + return server.cwd === undefined + ? base + : `${base}${t('tui.dialogs.pluginsSelector.mcpServerCwdSuffix', { cwd: server.cwd })}`; } function mcpItemServerName(item: PluginsOverviewItem): string | undefined { @@ -775,19 +852,22 @@ function mcpItemServerName(item: PluginsOverviewItem): string | undefined { function marketplaceEntryDescription(entry: PluginMarketplaceEntry): string { const tier = marketplaceTierLabel(entry.tier); const description = entry.description ?? tier; - const version = entry.version !== undefined ? ` · v${entry.version}` : ''; + const version = + entry.version !== undefined + ? ` · ${t('tui.dialogs.pluginsSelector.versionPrefix', { version: entry.version })}` + : ''; const keywords = entry.keywords !== undefined && entry.keywords.length > 0 ? ` · ${entry.keywords.join(', ')}` : ''; const tierSuffix = entry.description !== undefined ? ` · ${tier}` : ''; - return `${description} · id ${entry.id}${version}${tierSuffix}${keywords}`; + return `${description} · ${t('tui.dialogs.pluginsSelector.pluginId', { id: entry.id })}${version}${tierSuffix}${keywords}`; } function marketplaceTierLabel(tier: PluginMarketplaceEntry['tier']): string { - if (tier === 'official') return 'Official plugin'; - if (tier === 'curated') return 'Curated plugin'; - return 'Plugin'; + if (tier === 'official') return t('tui.dialogs.pluginsSelector.marketplaceTierOfficial'); + if (tier === 'curated') return t('tui.dialogs.pluginsSelector.marketplaceTierCurated'); + return t('tui.dialogs.pluginsSelector.marketplaceTierUnknown'); } function installStatus(entry: PluginMarketplaceEntry): string { @@ -803,12 +883,42 @@ function marketplaceEntryStatus( case 'update': return `update ${status.local} → ${status.latest}`; case 'up-to-date': - return status.version === undefined ? 'installed' : `installed · v${status.version}`; + return status.version === undefined ? 'installed' : `installed v${status.version}`; case 'not-installed': return installStatus(entry); } } +function marketplaceStatusLabel(status: string): string { + if (status === 'open-in-browser') { + return t('tui.dialogs.pluginsSelector.openInBrowser'); + } + if (status === 'installed') { + return t('tui.dialogs.pluginsSelector.installedStatus'); + } + if (status.startsWith('install v')) { + const version = status.slice('install v'.length); + return t('tui.dialogs.pluginsSelector.installStatusVersion', { version }); + } + if (status === 'install') { + return t('tui.dialogs.pluginsSelector.installStatus'); + } + if (status.startsWith('installed v')) { + const version = status.slice('installed v'.length); + return t('tui.dialogs.pluginsSelector.installedStatusVersion', { version }); + } + if (status.startsWith('update ')) { + const remainder = status.slice('update '.length); + const arrowIndex = remainder.indexOf(' → '); + if (arrowIndex >= 0) { + const local = remainder.slice(0, arrowIndex); + const latest = remainder.slice(arrowIndex + ' → '.length); + return t('tui.dialogs.pluginsSelector.updateStatus', { local, latest }); + } + } + return status; +} + function sectionLabel(label: string, colors: ColorPalette): string { return chalk.hex(colors.textDim).bold(` ${label}`); } diff --git a/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts b/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts index 7ae0da03d2..c85aa7038e 100644 --- a/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts +++ b/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts @@ -44,6 +44,7 @@ import { type Focusable, } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { DEFAULT_OAUTH_PROVIDER_NAME } from '#/constant/app'; import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; @@ -89,9 +90,7 @@ interface AddRow { type Row = SourceRow | AddRow; -const ADD_ROW_LABEL = '[ Add New Platform ]'; const PAGE_SIZE = 8; -const HEADER_HINT = '↑↓ navigate · D delete · Esc cancel'; // Narrows a `ProviderConfig` blob to a `CustomRegistrySource` payload. // Mirrors `readCustomRegistrySource` in `kimi-tui.ts`. We can't import @@ -205,7 +204,7 @@ function buildRows(opts: ProviderManagerOptions): readonly Row[] { }); } - return [...sources, { kind: 'add', id: '__add__', label: ADD_ROW_LABEL }]; + return [...sources, { kind: 'add', id: '__add__', label: t('tui.dialogs.providerManager.addRowLabel') }]; } export class ProviderManagerComponent extends Container implements Focusable { @@ -325,8 +324,11 @@ export class ProviderManagerComponent extends Container implements Focusable { const ids = selected.providerIds; const prompt = ids.length === 1 - ? `Delete platform "${selected.label}"?` - : `Delete platform "${selected.label}" and all ${String(ids.length)} providers?`; + ? t('tui.dialogs.providerManager.deleteConfirmSingle', { label: selected.label }) + : t('tui.dialogs.providerManager.deleteConfirmMultiple', { + label: selected.label, + count: ids.length, + }); this.confirm = { label: prompt, providerIds: ids, @@ -360,13 +362,13 @@ export class ProviderManagerComponent extends Container implements Focusable { // border under the title. const border = currentTheme.fg('primary', '─'.repeat(width)); lines.push(border); - lines.push(currentTheme.boldFg('primary', ' Providers')); - lines.push(currentTheme.fg('textMuted', ' ' + HEADER_HINT)); + lines.push(currentTheme.boldFg('primary', ` ${t('tui.dialogs.providerManager.title')}`)); + lines.push(currentTheme.fg('textMuted', ` ${t('tui.dialogs.providerManager.headerHint')}`)); lines.push(''); const rows = this.rows; if (rows.length === 0) { - lines.push(currentTheme.fg('textMuted', ' No providers configured.')); + lines.push(currentTheme.fg('textMuted', ` ${t('tui.dialogs.providerManager.empty')}`)); } else { const view = this.page(); for (let i = view.start; i < view.end; i++) { @@ -388,7 +390,7 @@ export class ProviderManagerComponent extends Container implements Focusable { lines.push( currentTheme.fg( 'textMuted', - ` Page ${String(view.page + 1)}/${String(view.pageCount)}`, + ` ${t('tui.dialogs.providerManager.page', { page: view.page + 1, pageCount: view.pageCount })}`, ), ); } diff --git a/apps/kimi-code/src/tui/components/dialogs/question-dialog.ts b/apps/kimi-code/src/tui/components/dialogs/question-dialog.ts index 6764b88d8f..665ab8c7ce 100644 --- a/apps/kimi-code/src/tui/components/dialogs/question-dialog.ts +++ b/apps/kimi-code/src/tui/components/dialogs/question-dialog.ts @@ -5,6 +5,7 @@ * reviews everything before the answers are emitted upstream. */ +import { t } from '#/i18n'; import { Container, Input, @@ -26,12 +27,12 @@ import type { const NUMBER_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; const MAX_BODY_LINES = 12; -const DEFAULT_OTHER_LABEL = 'Other'; -const NOT_ANSWERED_LABEL = 'Not answered'; -const REVIEW_TITLE = 'Review your answer before submit'; -const SUBMIT_PROMPT = 'Ready to submit your answers?'; -const UNANSWERED_WARNING = 'Some questions are still unanswered.'; -const SUBMIT_ACTIONS = ['Submit', 'Cancel'] as const; +const DEFAULT_OTHER_LABEL = t('tui.dialogs.questionDialog.defaultOtherLabel'); +const NOT_ANSWERED_LABEL = t('tui.dialogs.questionDialog.notAnswered'); +const REVIEW_TITLE = t('tui.dialogs.questionDialog.reviewTitle'); +const SUBMIT_PROMPT = t('tui.dialogs.questionDialog.submitPrompt'); +const UNANSWERED_WARNING = t('tui.dialogs.questionDialog.unansweredWarning'); +const SUBMIT_ACTIONS = [t('common.submit'), t('common.cancel')] as const; interface DisplayOption { readonly label: string; @@ -446,13 +447,17 @@ export class QuestionDialogComponent extends Container implements Focusable { const success = (text: string) => currentTheme.fg('success', text); const renderWidth = Math.max(1, width); - const lines: string[] = [accent('─'.repeat(renderWidth)), currentTheme.boldFg('primary', ' question'), '']; + const lines: string[] = [ + accent('─'.repeat(renderWidth)), + currentTheme.boldFg('primary', t('tui.dialogs.questionDialog.title')), + '', + ]; this.pushTabs(lines); lines.push(''); appendWrapped(lines, ' ? ', ' ', question.question, renderWidth, accent); if (this.isEditingOther()) { - lines.push(dim(' Type your answer, then press Enter to save.')); + lines.push(dim(` ${t('tui.dialogs.questionDialog.typeAnswerHint')}`)); } if (question.body !== undefined && question.body.trim().length > 0) { @@ -463,7 +468,13 @@ export class QuestionDialogComponent extends Container implements Focusable { appendWrapped(lines, ' ', ' ', bodyLine, renderWidth, dim); } if (bodyLines.length > visibleBodyLines.length) { - lines.push(dim(` ... ${String(bodyLines.length - visibleBodyLines.length)} more lines`)); + lines.push( + dim( + ` ${t('tui.dialogs.questionDialog.moreLines', { + count: bodyLines.length - visibleBodyLines.length, + })}`, + ), + ); } } @@ -525,7 +536,11 @@ export class QuestionDialogComponent extends Container implements Focusable { if (visibleEnd < options.length || visibleStart > 0) { lines.push( dim( - ` showing ${String(visibleStart + 1)}-${String(visibleEnd)} of ${String(options.length)}`, + ` ${t('tui.dialogs.questionDialog.showing', { + from: visibleStart + 1, + to: visibleEnd, + total: options.length, + })}`, ), ); } @@ -544,7 +559,11 @@ export class QuestionDialogComponent extends Container implements Focusable { const warning = (text: string) => currentTheme.fg('warning', text); const renderWidth = Math.max(1, width); - const lines: string[] = [accent('─'.repeat(renderWidth)), currentTheme.boldFg('primary', ' question'), '']; + const lines: string[] = [ + accent('─'.repeat(renderWidth)), + currentTheme.boldFg('primary', t('tui.dialogs.questionDialog.title')), + '', + ]; this.pushTabs(lines); lines.push(''); lines.push(currentTheme.boldFg('text', ` ${REVIEW_TITLE}`)); @@ -613,13 +632,13 @@ export class QuestionDialogComponent extends Container implements Focusable { const label = question.header !== undefined && question.header.length > 0 ? question.header - : `Q${String(i + 1)}`; + : t('tui.dialogs.questionDialog.questionPrefix', { number: i + 1 }); if (i === this.currentTab) tabs.push(active(` ${label} `)); else if (this.isAnswered(i)) tabs.push(currentTheme.fg('success', `(✓) ${label}`)); else tabs.push(dim(`(○) ${label}`)); } - const submitLabel = 'Submit'; + const submitLabel = t('tui.dialogs.questionDialog.submitTab'); if (this.isSubmitTab()) tabs.push(active(` ${submitLabel} `)); else tabs.push(dim(` ${submitLabel} `)); @@ -629,10 +648,10 @@ export class QuestionDialogComponent extends Container implements Focusable { private buildQuestionHint(dim: (s: string) => string, questionIdx: number): string { if (this.isEditingOther()) { const parts: string[] = [ - 'type answer', - '↵ save', - ...(this.totalTabs() > 1 ? ['tab switch'] : []), - 'esc cancel', + t('tui.dialogs.questionDialog.hintEdit.typeAnswer'), + t('tui.dialogs.questionDialog.hintEdit.save'), + ...(this.totalTabs() > 1 ? [t('tui.dialogs.questionDialog.hintEdit.tabSwitch')] : []), + t('tui.dialogs.questionDialog.hintEdit.cancel'), ]; return dim(` ${parts.join(' ')}`); } @@ -640,21 +659,28 @@ export class QuestionDialogComponent extends Container implements Focusable { const optionCount = Math.min(this.displayOptions(questionIdx).length, NUMBER_KEYS.length); const numberHint = optionCount <= 1 ? '1' : `1-${String(optionCount)}`; const question = this.request.data.questions[questionIdx]; - if (question === undefined) return dim(' esc cancel'); + if (question === undefined) return dim(` ${t('tui.dialogs.questionDialog.hintQuestion.cancel')}`); + const actionKey = question.multi_select + ? 'tui.dialogs.questionDialog.hintQuestion.toggle' + : 'tui.dialogs.questionDialog.hintQuestion.choose'; const parts: string[] = [ - '↑↓ select', - `${numberHint} / ↵ ${question.multi_select ? 'toggle' : 'choose'}`, + t('tui.dialogs.questionDialog.hintQuestion.navigate'), + t(actionKey, { range: numberHint }), ]; - if (this.totalTabs() > 1) parts.push('←/→/tab switch'); - parts.push('esc cancel'); + if (this.totalTabs() > 1) parts.push(t('tui.dialogs.questionDialog.hintQuestion.tabSwitch')); + parts.push(t('tui.dialogs.questionDialog.hintQuestion.cancel')); return dim(` ${parts.join(' ')}`); } private buildSubmitHint(dim: (s: string) => string): string { - const parts: string[] = ['↑↓ select', '1/2 choose', '↵ confirm']; - if (this.totalTabs() > 1) parts.push('←/→/tab switch'); - parts.push('esc cancel'); + const parts: string[] = [ + t('tui.dialogs.questionDialog.hintSubmit.navigate'), + t('tui.dialogs.questionDialog.hintSubmit.choose'), + t('tui.dialogs.questionDialog.hintSubmit.confirm'), + ]; + if (this.totalTabs() > 1) parts.push(t('tui.dialogs.questionDialog.hintSubmit.tabSwitch')); + parts.push(t('tui.dialogs.questionDialog.hintSubmit.cancel')); return dim(` ${parts.join(' ')}`); } diff --git a/apps/kimi-code/src/tui/components/dialogs/session-picker.ts b/apps/kimi-code/src/tui/components/dialogs/session-picker.ts index c8bd9017b5..a1aee8fbd3 100644 --- a/apps/kimi-code/src/tui/components/dialogs/session-picker.ts +++ b/apps/kimi-code/src/tui/components/dialogs/session-picker.ts @@ -13,6 +13,7 @@ import { import { formatSessionLabel } from '#/migration/index'; import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; import { SearchableList } from '#/tui/utils/searchable-list'; export interface SessionRow { @@ -31,13 +32,13 @@ function formatRelativeTime(ts: number): string { // so they use the same millisecond unit as `Date.now()`. if (!Number.isFinite(ts) || ts <= 0) return ''; const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1000); - if (diffSec < 60) return 'just now'; + if (diffSec < 60) return t('tui.dialogs.sessionPicker.justNow'); const minutes = Math.floor(diffSec / 60); - if (minutes < 60) return `${String(minutes)}m ago`; + if (minutes < 60) return t('tui.dialogs.sessionPicker.minutesAgo', { minutes: String(minutes) }); const hours = Math.floor(minutes / 60); - if (hours < 24) return `${String(hours)}h ago`; + if (hours < 24) return t('tui.dialogs.sessionPicker.hoursAgo', { hours: String(hours) }); const days = Math.floor(hours / 24); - return `${String(days)}d ago`; + return t('tui.dialogs.sessionPicker.daysAgo', { days: String(days) }); } function homeAlias(path: string): string { @@ -206,25 +207,25 @@ export class SessionPickerComponent extends Container implements Focusable { // prevents the "Rendered line exceeds terminal width" crash (issue #240). private renderLines(width: number): string[] { const lines: string[] = [currentTheme.fg('primary', '─'.repeat(width))]; - const title = this.scope === 'all' ? 'All sessions' : 'Sessions'; + const title = this.scope === 'all' ? t('tui.dialogs.sessionPicker.titleAll') : t('tui.dialogs.sessionPicker.titleCwd'); const scopeHint = this.onToggleScope === undefined ? undefined : this.scope === 'all' - ? 'Ctrl+A current cwd' - : 'Ctrl+A all'; + ? t('tui.dialogs.sessionPicker.scopeHintCwd') + : t('tui.dialogs.sessionPicker.scopeHintAll'); if (this.loading) { lines.push(currentTheme.boldFg('primary', truncateToWidth(title, width, ELLIPSIS))); lines.push( - currentTheme.fg('textMuted', truncateToWidth('Loading sessions...', width, ELLIPSIS)), + currentTheme.fg('textMuted', truncateToWidth(t('tui.dialogs.sessionPicker.loading'), width, ELLIPSIS)), ); lines.push(currentTheme.fg('primary', '─'.repeat(width))); return lines; } if (this.sessions.length === 0) { - const hintParts = [scopeHint, 'Esc cancel'].filter( + const hintParts = [scopeHint, t('tui.dialogs.modelSelector.hintCancel')].filter( (item): item is string => item !== undefined, ); lines.push(currentTheme.boldFg('primary', truncateToWidth(title, width, ELLIPSIS))); @@ -233,7 +234,7 @@ export class SessionPickerComponent extends Container implements Focusable { ); lines.push(''); lines.push( - currentTheme.fg('textMuted', truncateToWidth('No sessions found.', width, ELLIPSIS)), + currentTheme.fg('textMuted', truncateToWidth(t('tui.dialogs.sessionPicker.empty'), width, ELLIPSIS)), ); lines.push(currentTheme.fg('primary', '─'.repeat(width))); return lines; @@ -241,13 +242,13 @@ export class SessionPickerComponent extends Container implements Focusable { const view = this.list.view(); const titleSuffix = - view.query.length === 0 ? currentTheme.fg('textMuted', ' (type to search)') : ''; + view.query.length === 0 ? currentTheme.fg('textMuted', ' ' + t('tui.dialogs.modelSelector.searchHint')) : ''; const hintParts = [ - ...(view.query.length > 0 ? ['Backspace clear'] : []), - '↑↓ navigate', + ...(view.query.length > 0 ? [t('tui.dialogs.modelSelector.hintBackspace')] : []), + t('tui.dialogs.modelSelector.hintNavigate'), scopeHint, - 'Enter select', - 'Esc cancel', + t('tui.dialogs.modelSelector.hintSelect'), + t('tui.dialogs.modelSelector.hintCancel'), ].filter((item): item is string => item !== undefined); lines.push(currentTheme.boldFg('primary', title) + titleSuffix); @@ -255,12 +256,12 @@ export class SessionPickerComponent extends Container implements Focusable { lines.push(''); if (view.query.length > 0) { - lines.push(currentTheme.fg('primary', 'Search: ') + currentTheme.fg('text', view.query)); + lines.push(currentTheme.fg('primary', t('tui.dialogs.sessionPicker.searchLabel')) + currentTheme.fg('text', view.query)); } const loadedSessions = this.loadedSessions(view.items); if (loadedSessions.length === 0) { - lines.push(currentTheme.fg('textMuted', truncateToWidth('No matches', width, ELLIPSIS))); + lines.push(currentTheme.fg('textMuted', truncateToWidth(t('tui.dialogs.modelSelector.noMatches'), width, ELLIPSIS))); lines.push(currentTheme.fg('primary', '─'.repeat(width))); return lines; } @@ -291,11 +292,11 @@ export class SessionPickerComponent extends Container implements Focusable { lines.push(''); const totalSuffix = view.query.length > 0 - ? `${String(loadedSessions.length)} loaded / ${String(filteredCount)} matches` + ? t("tui.dialogs.sessionPicker.footerLoadedMatches", { loaded: String(loadedSessions.length), total: String(filteredCount) }) : loadedSessions.length === this.sessions.length - ? `${String(loadedSessions.length)} sessions` - : `${String(loadedSessions.length)} loaded / ${String(this.sessions.length)} sessions`; - const footer = `Showing ${String(visibleStart + 1)}-${String(visibleStart + visibleSessions.length)} of ${totalSuffix}`; + ? t("tui.dialogs.sessionPicker.footerSessions", { count: String(loadedSessions.length) }) + : t("tui.dialogs.sessionPicker.footerLoadedSessions", { loaded: String(loadedSessions.length), total: String(this.sessions.length) }); + const footer = t("tui.dialogs.sessionPicker.footerShowing", { from: String(visibleStart + 1), to: String(visibleStart + visibleSessions.length), totalSuffix }); lines.push(currentTheme.fg('textMuted', truncateToWidth(footer, width, ELLIPSIS))); } diff --git a/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts b/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts index 81e4b8d125..7557dd1450 100644 --- a/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts @@ -1,56 +1,66 @@ +import { t } from '#/i18n'; import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; export type SettingsSelection = | 'model' | 'theme' | 'editor' + | 'language' | 'permission' | 'experiments' | 'upgrade' | 'usage'; -const SETTINGS_OPTIONS: readonly ChoiceOption[] = [ - { - value: 'model', - label: 'Model', - description: 'Switch the active model and thinking mode.', - }, - { - value: 'permission', - label: 'Permission', - description: 'Choose how tool actions are approved.', - }, - { - value: 'theme', - label: 'Theme', - description: 'Change the terminal UI theme.', - }, - { - value: 'editor', - label: 'Editor', - description: 'Set the external editor command.', - }, - { - value: 'experiments', - label: 'Experiments', - description: 'Turn experimental features on or off.', - }, - { - value: 'upgrade', - label: 'Automatic updates', - description: 'Turn automatic CLI updates on or off.', - }, - { - value: 'usage', - label: 'Usage', - description: 'Show session tokens, context window, and plan quotas.', - }, -]; +function getSettingsOptions(): readonly ChoiceOption[] { + return [ + { + value: 'model', + label: t('tui.dialogs.settingsSelector.model'), + description: t('tui.dialogs.settingsSelector.modelDesc'), + }, + { + value: 'permission', + label: t('tui.dialogs.settingsSelector.permission'), + description: t('tui.dialogs.settingsSelector.permissionDesc'), + }, + { + value: 'theme', + label: t('tui.dialogs.settingsSelector.theme'), + description: t('tui.dialogs.settingsSelector.themeDesc'), + }, + { + value: 'language', + label: t('tui.dialogs.settingsSelector.language'), + description: t('tui.dialogs.settingsSelector.languageDesc'), + }, + { + value: 'editor', + label: t('tui.dialogs.settingsSelector.editor'), + description: t('tui.dialogs.settingsSelector.editorDesc'), + }, + { + value: 'experiments', + label: t('tui.dialogs.settingsSelector.experiments'), + description: t('tui.dialogs.settingsSelector.experimentsDesc'), + }, + { + value: 'upgrade', + label: t('tui.dialogs.settingsSelector.upgrade'), + description: t('tui.dialogs.settingsSelector.upgradeDesc'), + }, + { + value: 'usage', + label: t('tui.dialogs.settingsSelector.usage'), + description: t('tui.dialogs.settingsSelector.usageDesc'), + }, + ]; +} function isSettingsSelection(value: string): value is SettingsSelection { return ( value === 'model' || value === 'theme' || + value === 'language' || value === 'editor' || value === 'permission' || value === 'experiments' || @@ -67,12 +77,12 @@ export interface SettingsSelectorOptions { export class SettingsSelectorComponent extends ChoicePickerComponent { constructor(opts: SettingsSelectorOptions) { super({ - title: 'Settings', - options: [...SETTINGS_OPTIONS], + title: t('tui.dialogs.settingsSelector.title'), + options: [...getSettingsOptions()], onSelect: (value) => { if (isSettingsSelection(value)) opts.onSelect(value); }, onCancel: opts.onCancel, }); } -} +} \ No newline at end of file diff --git a/apps/kimi-code/src/tui/components/dialogs/start-permission-prompt.ts b/apps/kimi-code/src/tui/components/dialogs/start-permission-prompt.ts index 341ced723b..5582289c9e 100644 --- a/apps/kimi-code/src/tui/components/dialogs/start-permission-prompt.ts +++ b/apps/kimi-code/src/tui/components/dialogs/start-permission-prompt.ts @@ -7,6 +7,7 @@ import { type Focusable, } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { SELECT_POINTER } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; @@ -61,7 +62,7 @@ export class StartPermissionPromptComponent<TChoice extends StartPermissionChoic const lines = [ rule, currentTheme.boldFg('primary', ` ${this.opts.title}`), - currentTheme.fg('textMuted', ' ↑↓ navigate · Enter select · Esc cancel'), + currentTheme.fg('textMuted', ` ${t('tui.dialogs.startPermissionPrompt.navHint')}`), '', ]; diff --git a/apps/kimi-code/src/tui/components/dialogs/swarm-start-permission-prompt.ts b/apps/kimi-code/src/tui/components/dialogs/swarm-start-permission-prompt.ts index 694c0c0e6f..a3d1a34d54 100644 --- a/apps/kimi-code/src/tui/components/dialogs/swarm-start-permission-prompt.ts +++ b/apps/kimi-code/src/tui/components/dialogs/swarm-start-permission-prompt.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import { StartPermissionPromptComponent, type StartPermissionOption, @@ -10,39 +11,38 @@ export interface SwarmStartPermissionPromptOptions { readonly onCancel: () => void; } -const OPTIONS: readonly StartPermissionOption<SwarmStartPermissionChoice>[] = [ - { - value: 'auto', - label: 'Switch to Auto and start', - description: - 'Best for swarm tasks. Tools are approved automatically, and questions are skipped.', - }, - { - value: 'yolo', - label: 'Switch to YOLO and start', - description: - 'Tools and plan changes are approved automatically. Kimi Code may still ask you questions.', - }, - { - value: 'manual', - label: 'Start in Manual', - description: - 'Keep approvals on. Kimi Code may stop and wait for you during the swarm task.', - }, -]; +function swarmOptions(): readonly StartPermissionOption<SwarmStartPermissionChoice>[] { + return [ + { + value: 'auto', + label: t('tui.dialogs.swarmStartPermissionPrompt.optionAutoLabel'), + description: t('tui.dialogs.swarmStartPermissionPrompt.optionAutoDesc'), + }, + { + value: 'yolo', + label: t('tui.dialogs.swarmStartPermissionPrompt.optionYoloLabel'), + description: t('tui.dialogs.swarmStartPermissionPrompt.optionYoloDesc'), + }, + { + value: 'manual', + label: t('tui.dialogs.swarmStartPermissionPrompt.optionManualLabel'), + description: t('tui.dialogs.swarmStartPermissionPrompt.optionManualDesc'), + }, + ]; +} const NOTICE_LINES = [ - 'Manual mode asks you before Kimi Code runs commands, edits files, or takes other risky actions.', - 'Manual mode can block swarm work while agents are running.', - 'You can go back without losing your command.', + t('tui.dialogs.swarmStartPermissionPrompt.notice1'), + t('tui.dialogs.swarmStartPermissionPrompt.notice2'), + t('tui.dialogs.swarmStartPermissionPrompt.notice3'), ] as const; export class SwarmStartPermissionPromptComponent extends StartPermissionPromptComponent<SwarmStartPermissionChoice> { constructor(opts: SwarmStartPermissionPromptOptions) { super({ - title: 'Start a swarm task with approvals on?', + title: t('tui.dialogs.swarmStartPermissionPrompt.title'), noticeLines: NOTICE_LINES, - options: OPTIONS, + options: swarmOptions(), onSelect: opts.onSelect, onCancel: opts.onCancel, }); diff --git a/apps/kimi-code/src/tui/components/dialogs/tabbed-model-selector.ts b/apps/kimi-code/src/tui/components/dialogs/tabbed-model-selector.ts index 9a986b0962..15c96c2e93 100644 --- a/apps/kimi-code/src/tui/components/dialogs/tabbed-model-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/tabbed-model-selector.ts @@ -24,6 +24,7 @@ import { import { currentTheme } from '#/tui/theme'; import { renderTabStrip } from '#/tui/utils/tab-strip'; +import { t } from '#/i18n'; import { ModelSelectorComponent, @@ -33,7 +34,7 @@ import { } from './model-selector'; const ALL_TAB_ID = 'all'; -const ALL_TAB_LABEL = 'All'; +const ALL_TAB_LABEL = t('tui.dialogs.tabbedModelSelector.allTab'); export interface TabbedModelSelectorOptions { readonly models: Record<string, ModelAlias>; diff --git a/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts b/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts index c0f647f67c..5e27b8a2a4 100644 --- a/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts +++ b/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts @@ -20,8 +20,9 @@ import { } from '@moonshot-ai/pi-tui'; import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; -import { printableChar } from '@/tui/utils/printable-key'; +import { printableChar } from '#/tui/utils/printable-key'; const ELLIPSIS = '…'; @@ -32,14 +33,22 @@ export interface TaskOutputViewerProps { readonly onClose: () => void; } -const STATUS_LABEL: Record<BackgroundTaskStatus, string> = { - running: 'running', - completed: 'completed', - failed: 'failed', - timed_out: 'timed out', - killed: 'killed', - lost: 'lost', -}; +function statusLabel(status: BackgroundTaskStatus): string { + switch (status) { + case 'running': + return t('tui.dialogs.taskOutputViewer.status.running'); + case 'completed': + return t('tui.dialogs.taskOutputViewer.status.completed'); + case 'failed': + return t('tui.dialogs.taskOutputViewer.status.failed'); + case 'timed_out': + return t('tui.dialogs.taskOutputViewer.status.timedOut'); + case 'killed': + return t('tui.dialogs.taskOutputViewer.status.killed'); + case 'lost': + return t('tui.dialogs.taskOutputViewer.status.lost'); + } +} function statusColor(status: BackgroundTaskStatus): 'success' | 'textMuted' | 'error' { switch (status) { @@ -104,7 +113,7 @@ export class TaskOutputViewer extends Container implements Focusable { } private splitOutput(output: string): string[] { - return (output.length > 0 ? output : '[no output captured]').split('\n'); + return (output.length > 0 ? output : t('tui.dialogs.taskOutputViewer.noOutput')).split('\n'); } // ── input ────────────────────────────────────────────────────────── @@ -190,14 +199,14 @@ export class TaskOutputViewer extends Container implements Focusable { } private renderHeader(width: number): string { - const title = currentTheme.boldFg('primary', ' Task output '); + const title = currentTheme.boldFg('primary', t('tui.dialogs.taskOutputViewer.title')); const id = currentTheme.boldFg('text', this.props.taskId); const info = this.props.info; const segments: string[] = []; if (info !== undefined) { - segments.push(currentTheme.fg(statusColor(info.status), STATUS_LABEL[info.status])); + segments.push(currentTheme.fg(statusColor(info.status), statusLabel(info.status))); if (info.kind === 'process' && info.exitCode !== null) { - segments.push(currentTheme.fg('textMuted', `exit ${String(info.exitCode)}`)); + segments.push(currentTheme.fg('textMuted', t('tui.dialogs.taskOutputViewer.exitCode', { code: String(info.exitCode) }))); } if (info.description && info.description.length > 0) { segments.push(currentTheme.fg('textMuted', info.description)); @@ -248,10 +257,10 @@ export class TaskOutputViewer extends Container implements Focusable { ` ${String(lineFrom)}-${String(lineTo)} / ${String(total)} (${String(percent)}%) `, ); const keys = - `${key('↑↓')} ${dim('line')} ` + - `${key('PgUp/PgDn/Ctrl+U/D')} ${dim('page')} ` + - `${key('g/G')} ${dim('top/bot')} ` + - `${key('Q/Esc')} ${dim('cancel')}`; + `${key('↑↓')} ${dim(t('tui.dialogs.taskOutputViewer.footer.line'))} ` + + `${key('PgUp/PgDn/Ctrl+U/D')} ${dim(t('tui.dialogs.taskOutputViewer.footer.page'))} ` + + `${key('g/G')} ${dim(t('tui.dialogs.taskOutputViewer.footer.topBottom'))} ` + + `${key('Q/Esc')} ${dim(t('tui.dialogs.taskOutputViewer.footer.cancel'))}`; const left = ` ${keys}`; const leftW = visibleWidth(left); const rightW = visibleWidth(position); diff --git a/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts b/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts index 7902e81e1a..7abd3cabd4 100644 --- a/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts +++ b/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts @@ -13,6 +13,7 @@ * fire the `on*` callbacks back to the controller. */ +import { t } from '#/i18n'; import { Container, Key, @@ -24,9 +25,9 @@ import { } from '@moonshot-ai/pi-tui'; import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk'; -import { SELECT_POINTER } from '@/tui/constant/symbols'; +import { SELECT_POINTER } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; -import { printableChar } from '@/tui/utils/printable-key'; +import { printableChar } from '#/tui/utils/printable-key'; const ELLIPSIS = '…'; @@ -52,12 +53,12 @@ export interface TasksBrowserProps { } const STATUS_LABEL: Record<BackgroundTaskStatus, string> = { - running: 'running', - completed: 'completed', - failed: 'failed', - timed_out: 'timed out', - killed: 'killed', - lost: 'lost', + running: t('tui.dialogs.tasksBrowser.statusRunning'), + completed: t('tui.dialogs.tasksBrowser.statusCompleted'), + failed: t('tui.dialogs.tasksBrowser.statusFailed'), + timed_out: t('tui.dialogs.tasksBrowser.statusTimedOut'), + killed: t('tui.dialogs.tasksBrowser.statusKilled'), + lost: t('tui.dialogs.tasksBrowser.statusLost'), }; /** Auto-cancel the inline stop confirmation after this many ms. */ @@ -99,13 +100,13 @@ function isTerminal(status: BackgroundTaskStatus): boolean { function formatRelativeTime(ts: number | null | undefined): string { if (ts === null || ts === undefined || !Number.isFinite(ts) || ts <= 0) return ''; const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1000); - if (diffSec < 60) return 'just now'; + if (diffSec < 60) return t('tui.dialogs.tasksBrowser.relativeTimeJustNow'); const minutes = Math.floor(diffSec / 60); - if (minutes < 60) return `${String(minutes)}m ago`; + if (minutes < 60) return t('tui.dialogs.tasksBrowser.relativeTimeMins', { minutes }); const hours = Math.floor(minutes / 60); - if (hours < 24) return `${String(hours)}h ago`; + if (hours < 24) return t('tui.dialogs.tasksBrowser.relativeTimeHours', { hours }); const days = Math.floor(hours / 24); - return `${String(days)}d ago`; + return t('tui.dialogs.tasksBrowser.relativeTimeDays', { days }); } function singleLine(text: string): string { @@ -333,10 +334,10 @@ export class TasksBrowserApp extends Container implements Focusable { // ── header / footer ────────────────────────────────────────────────── private renderHeader(width: number): string { - const title = currentTheme.boldFg('primary', ' TASK BROWSER '); + const title = currentTheme.boldFg('primary', t('tui.dialogs.tasksBrowser.headerTitle')); const filterText = currentTheme.fg( 'textMuted', - ` filter=${this.props.filter === 'all' ? 'ALL' : 'ACTIVE'} `, + ` ${this.props.filter === 'all' ? t('tui.dialogs.tasksBrowser.filterAll') : t('tui.dialogs.tasksBrowser.filterActive')} `, ); // Count only the tasks actually listed (background tasks after the // foreground-task filter), so a foreground-only session doesn't read @@ -345,14 +346,14 @@ export class TasksBrowserApp extends Container implements Focusable { const counts = countByStatus(visible); const countSegments: string[] = []; if (counts.running > 0) - countSegments.push(currentTheme.fg('success', ` ${String(counts.running)} running `)); + countSegments.push(currentTheme.fg('success', ` ${String(counts.running)} ${t('tui.dialogs.tasksBrowser.statusRunning')} `)); if (counts.completed > 0) - countSegments.push(currentTheme.fg('textDim', ` ${String(counts.completed)} completed `)); + countSegments.push(currentTheme.fg('textDim', ` ${String(counts.completed)} ${t('tui.dialogs.tasksBrowser.statusCompleted')} `)); if (counts.terminalFailed > 0) countSegments.push( - currentTheme.fg('error', ` ${String(counts.terminalFailed)} interrupted `), + currentTheme.fg('error', ` ${String(counts.terminalFailed)} ${t('tui.dialogs.tasksBrowser.statusInterrupted')} `), ); - const totals = currentTheme.fg('textMuted', ` ${String(visible.length)} total `); + const totals = currentTheme.fg('textMuted', ` ${String(visible.length)} ${t('tui.dialogs.tasksBrowser.statusTotal')} `); const composed = title + filterText + countSegments.join('') + totals; return fitExactly(composed, width); @@ -365,18 +366,18 @@ export class TasksBrowserApp extends Container implements Focusable { if (this.pendingStopTaskId !== undefined) { const warn = (text: string): string => currentTheme.boldFg('warning', text); const line = - ` ${warn('Stop')} ${currentTheme.fg('text', this.pendingStopTaskId)}? ` + - `${key('Y')} ${dim('confirm')} ${key('N')}${dim('/')}${key('esc')} ${dim('cancel')} `; + ` ${warn(t('tui.dialogs.tasksBrowser.stopLabel'))} ${currentTheme.fg('text', this.pendingStopTaskId)}? ` + + `${key('Y')} ${dim(t('tui.dialogs.tasksBrowser.footerConfirm'))} ${key('N')}${dim('/')}${key('esc')} ${dim(t('tui.dialogs.tasksBrowser.footerCancel'))} `; return fitExactly(line, width); } const parts = [ - ` ${key('↑↓')} ${dim('select')}`, - `${key('Enter/O')} ${dim('output')}`, - `${key('S')} ${dim('stop')}`, - `${key('R')} ${dim('refresh')}`, - `${key('Tab')} ${dim('filter')}`, - `${key('Q/Esc')} ${dim('cancel')} `, + ` ${key('↑↓')} ${dim(t('tui.dialogs.tasksBrowser.footerSelect'))}`, + `${key('Enter/O')} ${dim(t('tui.dialogs.tasksBrowser.footerOutput'))}`, + `${key('S')} ${dim(t('tui.dialogs.tasksBrowser.footerStop'))}`, + `${key('R')} ${dim(t('tui.dialogs.tasksBrowser.footerRefresh'))}`, + `${key('Tab')} ${dim(t('tui.dialogs.tasksBrowser.footerFilter'))}`, + `${key('Q/Esc')} ${dim(t('tui.dialogs.tasksBrowser.footerCancel'))} `, ]; const left = parts.join(' '); const flash = this.props.flashMessage; @@ -438,14 +439,14 @@ export class TasksBrowserApp extends Container implements Focusable { // ── left: task list frame ──────────────────────────────────────────── private renderListFrame(width: number, height: number): string[] { - const title = `Tasks [${this.props.filter}]`; + const title = t('tui.dialogs.tasksBrowser.tasksFrameTitle', { filter: this.props.filter }); const innerHeight = Math.max(0, height - 2); if (this.sortedVisible.length === 0) { const empty = this.props.filter === 'active' - ? 'No active tasks. Tab = show all.' - : 'No background tasks in this session.'; + ? t('tui.dialogs.tasksBrowser.noActiveTasks') + : t('tui.dialogs.tasksBrowser.noBackgroundTasks'); const lines: string[] = [currentTheme.fg('textMuted', empty)]; while (lines.length < innerHeight) lines.push(''); return this.renderFrame(title, lines, width, height); @@ -493,7 +494,7 @@ export class TasksBrowserApp extends Container implements Focusable { const description = singleLine(task.description) || (task.kind === 'process' ? singleLine(task.command) : '') || - '(no description)'; + t('tui.dialogs.tasksBrowser.noDescription'); const desc = truncateToWidth(description, descBudget, ELLIPSIS); return fitExactly(`${prefix} ${currentTheme.fg('text', desc)}`, innerWidth); } @@ -530,75 +531,75 @@ export class TasksBrowserApp extends Container implements Focusable { const innerHeight = Math.max(0, height - 2); const task = this.sortedVisible[this.selectedIndex]; if (task === undefined) { - const empty = currentTheme.fg('textMuted', 'Select a task from the list.'); + const empty = currentTheme.fg('textMuted', t('tui.dialogs.tasksBrowser.noTaskSelected')); const lines: string[] = [empty]; while (lines.length < innerHeight) lines.push(''); - return this.renderFrame('Detail', lines, width, height); + return this.renderFrame(t('tui.dialogs.tasksBrowser.detailFrameTitle'), lines, width, height); } const label = (text: string): string => currentTheme.fg('textMuted', text.padEnd(14)); const value = (text: string): string => currentTheme.fg('text', text); const lines: string[] = [ - `${label('Task ID:')}${value(task.taskId)}`, - `${label('Status:')}${currentTheme.fg(statusColor(task.status), STATUS_LABEL[task.status])}`, - `${label('Description:')}${value(singleLine(task.description) || '—')}`, + `${label(t('tui.dialogs.tasksBrowser.taskIdLabel'))}${value(task.taskId)}`, + `${label(t('tui.dialogs.tasksBrowser.statusLabel'))}${currentTheme.fg(statusColor(task.status), STATUS_LABEL[task.status])}`, + `${label(t('tui.dialogs.tasksBrowser.descriptionLabel'))}${value(singleLine(task.description) || '—')}`, ]; if (task.kind === 'process' && task.command && task.command !== task.description) { - lines.push(`${label('Command:')}${value(singleLine(task.command))}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.commandLabel'))}${value(singleLine(task.command))}`); } if (task.kind === 'agent' && task.agentId !== undefined) { - lines.push(`${label('Agent ID:')}${value(task.agentId)}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.agentIdLabel'))}${value(task.agentId)}`); } if (task.kind === 'agent' && task.subagentType !== undefined) { - lines.push(`${label('Agent type:')}${value(task.subagentType)}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.agentTypeLabel'))}${value(task.subagentType)}`); } if (task.kind === 'question') { - lines.push(`${label('Questions:')}${currentTheme.fg('textMuted', String(task.questionCount))}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.questionsLabel'))}${currentTheme.fg('textMuted', String(task.questionCount))}`); if (task.toolCallId !== undefined) { - lines.push(`${label('Tool call:')}${currentTheme.fg('textMuted', task.toolCallId)}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.toolCallLabel'))}${currentTheme.fg('textMuted', task.toolCallId)}`); } } const timing = task.status === 'running' - ? `running ${formatRelativeTime(task.startedAt)}` + ? `${t('tui.dialogs.tasksBrowser.timingRunning')}${formatRelativeTime(task.startedAt)}` : task.endedAt !== null && task.endedAt !== undefined - ? `finished ${formatRelativeTime(task.endedAt)}` + ? `${t('tui.dialogs.tasksBrowser.timingFinished')}${formatRelativeTime(task.endedAt)}` : ''; - if (timing.length > 0) lines.push(`${label('Time:')}${currentTheme.fg('textMuted', timing)}`); + if (timing.length > 0) lines.push(`${label(t('tui.dialogs.tasksBrowser.timeLabel'))}${currentTheme.fg('textMuted', timing)}`); if (task.kind === 'process' && task.pid > 0) { - lines.push(`${label('Pid:')}${currentTheme.fg('textMuted', String(task.pid))}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.pidLabel'))}${currentTheme.fg('textMuted', String(task.pid))}`); } if (task.kind === 'process' && task.exitCode !== null) { - lines.push(`${label('Exit code:')}${currentTheme.fg('textMuted', String(task.exitCode))}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.exitCodeLabel'))}${currentTheme.fg('textMuted', String(task.exitCode))}`); } if (task.stopReason !== undefined && task.stopReason.length > 0) { - lines.push(`${label('Reason:')}${currentTheme.fg('textMuted', task.stopReason)}`); + lines.push(`${label(t('tui.dialogs.tasksBrowser.reasonLabel'))}${currentTheme.fg('textMuted', task.stopReason)}`); } while (lines.length < innerHeight) lines.push(''); - return this.renderFrame('Detail', lines, width, height); + return this.renderFrame(t('tui.dialogs.tasksBrowser.detailFrameTitle'), lines, width, height); } private renderPreviewFrame(width: number, height: number): string[] { const innerHeight = Math.max(0, height - 2); const task = this.sortedVisible[this.selectedIndex]; if (task === undefined) { - const lines: string[] = [currentTheme.fg('textMuted', 'No task selected.')]; + const lines: string[] = [currentTheme.fg('textMuted', t('tui.dialogs.tasksBrowser.noTaskSelected'))]; while (lines.length < innerHeight) lines.push(''); - return this.renderFrame('Preview Output', lines, width, height); + return this.renderFrame(t('tui.dialogs.tasksBrowser.previewFrameTitle'), lines, width, height); } let body: string; - if (this.props.tailLoading) body = '[loading…]'; + if (this.props.tailLoading) body = t('tui.dialogs.tasksBrowser.loadingTail'); else if (this.props.tailOutput === undefined || this.props.tailOutput.length === 0) - body = '[no output captured]'; + body = t('tui.dialogs.tasksBrowser.noOutputTail'); else body = this.props.tailOutput; const rawLines = body.split('\n'); const tailLines = rawLines.slice(-innerHeight); const styled = tailLines.map((line) => currentTheme.fg('textDim', line)); while (styled.length < innerHeight) styled.push(''); - return this.renderFrame('Preview Output', styled, width, height); + return this.renderFrame(t('tui.dialogs.tasksBrowser.previewFrameTitle'), styled, width, height); } // ── too-small fallback ────────────────────────────────────────────── @@ -607,7 +608,7 @@ export class TasksBrowserApp extends Container implements Focusable { const lines: string[] = []; const msg = currentTheme.fg( 'error', - `Terminal too small (need ≥ ${String(MIN_WIDTH)} × ${String(MIN_HEIGHT)})`, + t('tui.dialogs.tasksBrowser.tooSmall', { minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT }), ); lines.push(fitExactly(msg, width)); for (let i = 1; i < rows; i++) lines.push(' '.repeat(width)); diff --git a/apps/kimi-code/src/tui/components/dialogs/theme-selector.ts b/apps/kimi-code/src/tui/components/dialogs/theme-selector.ts index ecd3953fd8..a357c43485 100644 --- a/apps/kimi-code/src/tui/components/dialogs/theme-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/theme-selector.ts @@ -2,12 +2,15 @@ import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; import { listCustomThemesSync } from '#/tui/theme/custom-theme-loader'; import type { ThemeName } from '#/tui/theme/index'; +import { t } from '#/i18n'; -const THEME_OPTIONS: readonly ChoiceOption[] = [ - { value: 'auto', label: 'Auto (match terminal)' }, - { value: 'dark', label: 'Dark' }, - { value: 'light', label: 'Light' }, -]; +function getThemeOptions(): readonly ChoiceOption[] { + return [ + { value: 'auto', label: t('tui.dialogs.themeSelector.auto') }, + { value: 'dark', label: t('tui.dialogs.themeSelector.dark') }, + { value: 'light', label: t('tui.dialogs.themeSelector.light') }, + ]; +} export interface ThemeSelectorOptions { readonly currentValue: ThemeName; @@ -19,11 +22,11 @@ export class ThemeSelectorComponent extends ChoicePickerComponent { constructor(opts: ThemeSelectorOptions) { const customThemes = listCustomThemesSync(); const options: ChoiceOption[] = [ - ...THEME_OPTIONS, - ...customThemes.map((name) => ({ value: name, label: `Custom: ${name}` })), + ...getThemeOptions(), + ...customThemes.map((name) => ({ value: name, label: t('tui.dialogs.themeSelector.custom', { name }) })), ]; super({ - title: 'Select theme', + title: t('tui.dialogs.themeSelector.title'), options, currentValue: opts.currentValue, onSelect: (value) => { @@ -32,4 +35,4 @@ export class ThemeSelectorComponent extends ChoicePickerComponent { onCancel: opts.onCancel, }); } -} +} \ No newline at end of file diff --git a/apps/kimi-code/src/tui/components/dialogs/undo-selector.ts b/apps/kimi-code/src/tui/components/dialogs/undo-selector.ts index 37320f92bd..2cfebf62fb 100644 --- a/apps/kimi-code/src/tui/components/dialogs/undo-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/undo-selector.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import { Container, Key, @@ -66,17 +67,16 @@ export class UndoSelectorComponent extends Container implements Focusable { override render(width: number): string[] { const view = this.list.view(); - const hintParts = ['↑↓ navigate', 'Enter select', 'Esc cancel']; const lines: string[] = [ currentTheme.fg('primary', '─'.repeat(width)), - currentTheme.boldFg('primary', ' Select messages to undo'), - currentTheme.fg('textMuted', ' ' + hintParts.join(' · ')), + currentTheme.boldFg('primary', t('tui.dialogs.undoSelector.title')), + currentTheme.fg('textMuted', ' ' + t('tui.dialogs.undoSelector.navHint')), '', ]; if (view.items.length === 0) { - lines.push(currentTheme.fg('textMuted', ' No messages')); + lines.push(currentTheme.fg('textMuted', t('tui.dialogs.undoSelector.noMessages'))); } else { const visibleCount = Math.min(MAX_VISIBLE_CHOICES, view.items.length); const maxStart = view.items.length - visibleCount; diff --git a/apps/kimi-code/src/tui/components/dialogs/update-preference-selector.ts b/apps/kimi-code/src/tui/components/dialogs/update-preference-selector.ts index 35055e084c..f34e06f08d 100644 --- a/apps/kimi-code/src/tui/components/dialogs/update-preference-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/update-preference-selector.ts @@ -1,15 +1,16 @@ import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; +import { t } from '#/i18n'; const UPDATE_PREFERENCE_OPTIONS: readonly ChoiceOption[] = [ { value: 'on', - label: 'On', - description: 'Install new versions in the background.', + label: t('tui.dialogs.updatePreferenceSelector.on'), + description: t('tui.dialogs.updatePreferenceSelector.onDescription'), }, { value: 'off', - label: 'Off', - description: 'Show the install prompt instead.', + label: t('tui.dialogs.updatePreferenceSelector.off'), + description: t('tui.dialogs.updatePreferenceSelector.offDescription'), }, ]; @@ -22,7 +23,7 @@ export interface UpdatePreferenceSelectorOptions { export class UpdatePreferenceSelectorComponent extends ChoicePickerComponent { constructor(opts: UpdatePreferenceSelectorOptions) { super({ - title: 'Automatic updates', + title: t('tui.dialogs.updatePreferenceSelector.title'), options: [...UPDATE_PREFERENCE_OPTIONS], currentValue: opts.currentValue ? 'on' : 'off', onSelect: (value) => { diff --git a/apps/kimi-code/src/tui/components/messages/agent-group.ts b/apps/kimi-code/src/tui/components/messages/agent-group.ts index 6fd1624d5b..88caf4a38a 100644 --- a/apps/kimi-code/src/tui/components/messages/agent-group.ts +++ b/apps/kimi-code/src/tui/components/messages/agent-group.ts @@ -18,6 +18,7 @@ import type { TUI } from '@moonshot-ai/pi-tui'; import { Container, Spacer, Text } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; @@ -25,8 +26,6 @@ import type { ToolCallComponent, ToolCallSubagentSnapshot } from './tool-call'; const THROTTLE_MS = 200; -const DETACH_HINT_TEXT = 'Press Ctrl+B to run in background'; - interface AgentEntry { readonly toolCallId: string; readonly tc: ToolCallComponent; @@ -134,7 +133,7 @@ export class AgentGroupComponent extends Container { this.appendLines(snap, isLast); }); if (this.shouldShowDetachHint(snapshots)) { - this.bodyContainer.addChild(new Text(currentTheme.dim(DETACH_HINT_TEXT), 2, 0)); + this.bodyContainer.addChild(new Text(currentTheme.dim(t('tui.messages.agentGroup.detachHint')), 2, 0)); } this.lastFlushPhases.clear(); @@ -160,8 +159,11 @@ export class AgentGroupComponent extends Container { const types = new Set(snapshots.map((s) => s.agentName).filter((n) => n !== undefined)); const headerLabel = types.size === 1 - ? `${String(total)} ${[...types][0]} agents finished` - : `${String(total)} agents finished`; + ? t('tui.messages.agentGroup.finishedWithType', { + count: total, + type: [...types][0] ?? t('tui.messages.agentGroup.agentDefault'), + }) + : t('tui.messages.agentGroup.finished', { count: total }); const totalTools = snapshots.reduce((acc, s) => acc + s.toolCount, 0); const totalTokens = snapshots.reduce((acc, s) => acc + s.tokens, 0); const tail = formatHeaderTail({ toolCount: totalTools, tokens: totalTokens, elapsedSeconds }); @@ -169,9 +171,13 @@ export class AgentGroupComponent extends Container { } const parts = formatBreakdownParts(counts); - const headerText = parts.length > 0 - ? `Running ${String(total)} agents (${parts.join(', ')})` - : `Running ${String(total)} agents`; + const headerText = + parts.length > 0 + ? t('tui.messages.agentGroup.runningWithBreakdown', { + count: total, + breakdown: parts.join(', '), + }) + : t('tui.messages.agentGroup.running', { count: total }); const tail = formatHeaderTail({ toolCount: 0, tokens: 0, elapsedSeconds }); return `${bullet}${currentTheme.boldFg('primary', headerText)}${tail}`; } @@ -181,8 +187,8 @@ export class AgentGroupComponent extends Container { // First-level branch line. const branch1 = isLast ? '└─' : '├─'; - const agentType = snap.agentName ?? 'agent'; - const desc = snap.toolCallDescription || '(no description)'; + const agentType = snap.agentName ?? t('tui.messages.agentGroup.agentDefault'); + const desc = snap.toolCallDescription || t('tui.messages.agentGroup.noDescription'); const tail = formatLineTail(snap); const namePart = currentTheme.fg('primary', agentType); const descPart = dim(`· ${desc}`); @@ -194,8 +200,9 @@ export class AgentGroupComponent extends Container { const branch2 = isLast ? ' ' : '│ '; if (snap.phase === 'failed') { // Show one error line; error messages can be long. - const errLine = (snap.errorText ?? 'Failed').split('\n').at(0) ?? 'Failed'; - const errStr = currentTheme.fg('error', `Error: ${errLine}`); + const errLine = (snap.errorText ?? t('tui.messages.agentGroup.failed')).split('\n').at(0) ?? + t('tui.messages.agentGroup.failed'); + const errStr = currentTheme.fg('error', t('tui.messages.agentGroup.errorPrefix', { error: errLine })); this.bodyContainer.addChild(new Text(` ${branch2} ${errStr}`, 0, 0)); return; } @@ -290,17 +297,26 @@ function countPhases(snapshots: readonly ToolCallSubagentSnapshot[]): PhaseCount function formatBreakdownParts(counts: PhaseCounts): string[] { const parts: string[] = []; - if (counts.done > 0) parts.push(`${String(counts.done)} done`); - if (counts.failed > 0) parts.push(`${String(counts.failed)} failed`); - if (counts.backgrounded > 0) parts.push(`${String(counts.backgrounded)} backgrounded`); - if (counts.running > 0) parts.push(`${String(counts.running)} running`); - if (counts.waiting > 0) parts.push(`${String(counts.waiting)} waiting`); - if (counts.starting > 0) parts.push(`${String(counts.starting)} starting`); + if (counts.done > 0) parts.push(t('tui.messages.agentGroup.breakdownDone', { count: counts.done })); + if (counts.failed > 0) parts.push(t('tui.messages.agentGroup.breakdownFailed', { count: counts.failed })); + if (counts.backgrounded > 0) { + parts.push(t('tui.messages.agentGroup.breakdownBackgrounded', { count: counts.backgrounded })); + } + if (counts.running > 0) parts.push(t('tui.messages.agentGroup.breakdownRunning', { count: counts.running })); + if (counts.waiting > 0) parts.push(t('tui.messages.agentGroup.breakdownWaiting', { count: counts.waiting })); + if (counts.starting > 0) parts.push(t('tui.messages.agentGroup.breakdownStarting', { count: counts.starting })); return parts; } function formatStats(snap: ToolCallSubagentSnapshot): string { - const parts = [`${String(snap.toolCount)} tool${snap.toolCount === 1 ? '' : 's'}`]; + const parts = [ + t( + snap.toolCount === 1 + ? 'tui.messages.agentGroup.tool_one' + : 'tui.messages.agentGroup.tool_other', + { count: snap.toolCount }, + ), + ]; if (snap.elapsedSeconds !== undefined) parts.push(formatElapsed(snap.elapsedSeconds)); if (snap.tokens > 0) parts.push(formatTokens(snap.tokens)); return currentTheme.dim(` · ${parts.join(' · ')}`); @@ -310,30 +326,30 @@ function formatLineTail(snap: ToolCallSubagentSnapshot): string { const separator = currentTheme.dim(' · '); switch (snap.phase) { case 'done': - return separator + currentTheme.fg('success', '✓ Completed'); + return separator + currentTheme.fg('success', t('tui.messages.agentGroup.completed')); case 'failed': - return separator + currentTheme.fg('error', '✗ Failed'); + return separator + currentTheme.fg('error', t('tui.messages.agentGroup.failed')); case 'backgrounded': - return separator + currentTheme.dim('◐ backgrounded'); + return separator + currentTheme.dim(t('tui.messages.agentGroup.backgrounded')); case 'queued': - return separator + currentTheme.fg('primary', 'Waiting'); + return separator + currentTheme.fg('primary', t('tui.messages.agentGroup.waiting')); case 'running': - return separator + currentTheme.fg('primary', 'Running'); + return separator + currentTheme.fg('primary', t('tui.messages.agentGroup.runningLabel')); case 'spawning': case undefined: - return separator + currentTheme.fg('primary', 'Starting'); + return separator + currentTheme.fg('primary', t('tui.messages.agentGroup.starting')); } } function fallbackActivityForPhase(phase: ToolCallSubagentSnapshot['phase']): string { switch (phase) { case 'queued': - return 'Waiting to start…'; + return t('tui.messages.agentGroup.fallbackWaiting'); case 'running': - return 'Still working…'; + return t('tui.messages.agentGroup.fallbackRunning'); case 'spawning': case undefined: - return 'Starting…'; + return t('tui.messages.agentGroup.fallbackStarting'); case 'done': case 'failed': case 'backgrounded': @@ -347,7 +363,16 @@ function formatHeaderTail(args: { readonly elapsedSeconds: number | undefined; }): string { const parts: string[] = []; - if (args.toolCount > 0) parts.push(`${String(args.toolCount)} tool${args.toolCount === 1 ? '' : 's'}`); + if (args.toolCount > 0) { + parts.push( + t( + args.toolCount === 1 + ? 'tui.messages.agentGroup.tool_one' + : 'tui.messages.agentGroup.tool_other', + { count: args.toolCount }, + ), + ); + } if (args.tokens > 0) parts.push(formatTokens(args.tokens)); if (args.elapsedSeconds !== undefined) parts.push(formatElapsed(args.elapsedSeconds)); return parts.length > 0 ? currentTheme.dim(` · ${parts.join(' · ')}`) : ''; @@ -364,14 +389,14 @@ function maxElapsedSeconds(snapshots: readonly ToolCallSubagentSnapshot[]): numb } function formatElapsed(seconds: number): string { - if (seconds < 60) return `${String(seconds)}s`; + if (seconds < 60) return t('tui.messages.agentGroup.elapsedSeconds', { count: seconds }); const minutes = Math.floor(seconds / 60); const remainder = seconds % 60; - return `${String(minutes)}m ${String(remainder)}s`; + return t('tui.messages.agentGroup.elapsedMinutes', { minutes, seconds: remainder }); } function formatTokens(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M tok`; - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k tok`; - return `${String(n)} tok`; + if (n >= 1_000_000) return t('tui.messages.agentGroup.tokensM', { count: (n / 1_000_000).toFixed(1) }); + if (n >= 1_000) return t('tui.messages.agentGroup.tokensK', { count: (n / 1_000).toFixed(1) }); + return t('tui.messages.agentGroup.tokens', { count: n }); } diff --git a/apps/kimi-code/src/tui/components/messages/agent-swarm-progress.ts b/apps/kimi-code/src/tui/components/messages/agent-swarm-progress.ts index 75c7266a2f..e7062ce229 100644 --- a/apps/kimi-code/src/tui/components/messages/agent-swarm-progress.ts +++ b/apps/kimi-code/src/tui/components/messages/agent-swarm-progress.ts @@ -1,5 +1,6 @@ import { truncateToWidth, visibleWidth, type Component } from '@moonshot-ai/pi-tui'; import chalk from 'chalk'; +import { t } from '#/i18n'; import { AgentSwarmProgressEstimator, @@ -33,16 +34,6 @@ const AGENT_SWARM_LEFT_INDENT = ' '; const AGENT_SWARM_RIGHT_GAP = 1; const AGENT_SWARM_NON_GRID_LINES = 6; const COMPACT_TERMINAL_MARK_WIDTH = 1; -const ORCHESTRATING_LABEL = 'Orchestrating...'; -const PROMPTING_LABEL = 'Prompting...'; -const WORKING_LABEL = 'Working...'; -const COMPLETED_LABEL = 'Completed.'; -const FAILED_LABEL = 'Failed.'; -const ABORTED_LABEL = 'Aborted.'; -const CANCELLED_LABEL = 'Cancelled.'; -const QUEUED_LABEL = 'Queued...'; -const SUSPENDED_LABEL = 'Rate limited...'; -const RESUMED_ITEM_LABEL = '(resumed)'; const CANCELLED_LABEL_DARKEN_FACTOR = 0.72; const AGENT_SWARM_TITLE_ACCENT_BIAS = 1.3; @@ -174,15 +165,24 @@ export interface AgentSwarmProgressOptions { readonly availableGridHeight?: () => number | undefined; } -const PHASE_LABELS: Record<AgentSwarmPhase, string> = { - pending: QUEUED_LABEL, - queued: QUEUED_LABEL, - suspended: SUSPENDED_LABEL, - running: 'Running', - completed: 'Completed', - failed: 'Failed', - cancelled: ABORTED_LABEL, -}; +function phaseLabel(phase: AgentSwarmPhase): string { + switch (phase) { + case 'pending': + return t('tui.messages.agentSwarmProgress.phase.pending'); + case 'queued': + return t('tui.messages.agentSwarmProgress.phase.queued'); + case 'suspended': + return t('tui.messages.agentSwarmProgress.phase.suspended'); + case 'running': + return t('tui.messages.agentSwarmProgress.phase.running'); + case 'completed': + return t('tui.messages.agentSwarmProgress.phase.completed'); + case 'failed': + return t('tui.messages.agentSwarmProgress.phase.failed'); + case 'cancelled': + return t('tui.messages.agentSwarmProgress.phase.cancelled'); + } +} export class AgentSwarmProgressComponent implements Component { private members: AgentSwarmMember[]; @@ -475,8 +475,8 @@ export class AgentSwarmProgressComponent implements Component { private renderHeader(width: number, _summary: AgentSwarmSummary | undefined): string { if (width <= 3) return chalk.hex(this.colors.primary)('─'.repeat(width)); - - const title = gradientText('Agent Swarm', this.colors.primary, this.colors.accent, AGENT_SWARM_TITLE_ACCENT_BIAS); + + const title = gradientText(t('tui.messages.agentSwarmProgress.title'), this.colors.primary, this.colors.accent, AGENT_SWARM_TITLE_ACCENT_BIAS); const description = this.description.length > 0 ? chalk.hex(this.colors.primary)(' ─ ') + chalk.hex(this.colors.text)(this.description) @@ -535,14 +535,14 @@ export class AgentSwarmProgressComponent implements Component { private renderOrchestratingStatusLine(width: number): string { if (this.itemsStarted) { return truncateToWidth( - renderStatusLabel(ORCHESTRATING_LABEL, this.colors.primary), + renderStatusLabel(t('tui.messages.agentSwarmProgress.orchestrating'), this.colors.primary), width, ); } const promptTemplate = collapseWhitespace(this.promptTemplateText); const label = renderStatusLabel( - promptTemplate.length > 0 ? PROMPTING_LABEL : ORCHESTRATING_LABEL, + promptTemplate.length > 0 ? t('tui.messages.agentSwarmProgress.prompting') : t('tui.messages.agentSwarmProgress.orchestrating'), this.colors.primary, ); if (promptTemplate.length === 0) return truncateToWidth(label, width); @@ -772,7 +772,7 @@ export class AgentSwarmProgressComponent implements Component { member.phase = 'cancelled'; clearMemberState(member, ...CANCELLED_CLEAR_KEYS); if (previousPhase === 'pending' || previousPhase === 'queued' || previousPhase === 'suspended') { - member.cancelledLabelText = CANCELLED_LABEL; + member.cancelledLabelText = t('tui.messages.agentSwarmProgress.cancelled'); member.cancelledLabelColor = cancelledLabelColor(this.colors); member.cancelledMarkColor = this.colors.warning; member.cancelledBarColor = this.colors.warning; @@ -782,7 +782,7 @@ export class AgentSwarmProgressComponent implements Component { member.cancelledMarkColor = this.colors.warning; member.cancelledBarColor = this.colors.warning; } else { - member.cancelledLabelText = ABORTED_LABEL; + member.cancelledLabelText = t('tui.messages.agentSwarmProgress.aborted'); member.cancelledLabelColor = this.colors.warning; member.cancelledMarkColor = this.colors.warning; member.cancelledBarColor = this.colors.warning; @@ -832,7 +832,7 @@ function agentSwarmResumeItemsFromArgs(args: Record<string, unknown>): string[] ) { return []; } - return Object.keys(resumeAgentIds).map(() => RESUMED_ITEM_LABEL); + return Object.keys(resumeAgentIds).map(() => t('tui.messages.agentSwarmProgress.resumed')); } export function agentSwarmPartialItemsCountFromArguments(argumentsText: string): number { @@ -868,7 +868,7 @@ function agentSwarmPartialResumeItemsFromArguments(argumentsText: string): strin if (match === null) return []; return Array.from( { length: countPartialJsonObjectEntries(argumentsText, match.index + match[0].length) }, - () => RESUMED_ITEM_LABEL, + () => t('tui.messages.agentSwarmProgress.resumed'), ); } @@ -1319,11 +1319,11 @@ function totalStatus( function totalStatusLabel(status: TotalStatus): string { const map: Record<TotalStatus, string> = { - working: WORKING_LABEL, - completed: COMPLETED_LABEL, - suspended: SUSPENDED_LABEL, - failed: FAILED_LABEL, - aborted: ABORTED_LABEL, + working: t('tui.messages.agentSwarmProgress.working'), + completed: t('tui.messages.agentSwarmProgress.completed'), + suspended: t('tui.messages.agentSwarmProgress.rateLimited'), + failed: t('tui.messages.agentSwarmProgress.failed'), + aborted: t('tui.messages.agentSwarmProgress.aborted'), }; return map[status]; } @@ -1388,14 +1388,14 @@ function renderCellLabel( if (snapshot.phase === 'cancelled') { return renderCancelledCellLabel(member, width, colors); } - return truncateWithColor(PHASE_LABELS[snapshot.phase], width, phaseColor(snapshot.phase, colors)); + return truncateWithColor(phaseLabel(snapshot.phase), width, phaseColor(snapshot.phase, colors)); } function runningCellLabelText(member: AgentSwarmMember): string { const latestLine = latestNonEmptyLine(member.latestModelText); const itemText = collapseWhitespace(member.itemText); const text = latestLine.length > 0 ? latestLine : itemText; - return text.length > 0 ? text : PHASE_LABELS.running; + return text.length > 0 ? text : phaseLabel('running'); } function renderCancelledCellLabel( @@ -1403,7 +1403,7 @@ function renderCancelledCellLabel( width: number, colors: ColorPalette, ): string { - const labelText = member.cancelledLabelText ?? ABORTED_LABEL; + const labelText = member.cancelledLabelText ?? t('tui.messages.agentSwarmProgress.aborted'); const labelColor = member.cancelledLabelColor ?? colors.warning; const markColor = member.cancelledMarkColor ?? colors.warning; const labelStyle = chalk.hex(labelColor); @@ -1445,7 +1445,7 @@ function renderPendingCell( const id = chalk.hex(colors.primary)(member.id); const prefix = `${id} `; const itemText = collapseWhitespace(member.itemText); - const label = itemText.length > 0 ? itemText : QUEUED_LABEL; + const label = itemText.length > 0 ? itemText : t('tui.messages.agentSwarmProgress.queued'); const labelWidth = Math.max(1, width - visibleWidth(prefix)); return prefix + truncateWithColor(label, labelWidth, colors.textDim); } @@ -1458,7 +1458,7 @@ function renderQueuedCell( const id = chalk.hex(colors.primary)(member.id); const prefix = `${id} `; const labelWidth = Math.max(1, width - visibleWidth(prefix)); - return prefix + truncateWithColor(QUEUED_LABEL, labelWidth, colors.textDim); + return prefix + truncateWithColor(t('tui.messages.agentSwarmProgress.queued'), labelWidth, colors.textDim); } function renderCancelledUnstartedCell( diff --git a/apps/kimi-code/src/tui/components/messages/background-agent-status.ts b/apps/kimi-code/src/tui/components/messages/background-agent-status.ts index 9c1a3d815b..2e3d889a9e 100644 --- a/apps/kimi-code/src/tui/components/messages/background-agent-status.ts +++ b/apps/kimi-code/src/tui/components/messages/background-agent-status.ts @@ -1,5 +1,6 @@ import { Text, truncateToWidth, type Component } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { MESSAGE_INDENT } from '#/tui/constant/rendering'; import { FAILURE_MARK, STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; @@ -27,7 +28,7 @@ export class BackgroundAgentStatusComponent implements Component { const text = currentTheme.fg(tone, this.data.headline) + (this.data.detail !== undefined && this.data.detail.length > 0 - ? currentTheme.fg('textDim', ` (${this.data.detail})`) + ? currentTheme.fg('textDim', t('tui.messages.backgroundAgentStatus.detail', { detail: this.data.detail })) : ''); const textComponent = new Text(text, 0, 0); diff --git a/apps/kimi-code/src/tui/components/messages/cron-message.ts b/apps/kimi-code/src/tui/components/messages/cron-message.ts index 8cb81f5d3a..7176fd3e57 100644 --- a/apps/kimi-code/src/tui/components/messages/cron-message.ts +++ b/apps/kimi-code/src/tui/components/messages/cron-message.ts @@ -1,6 +1,7 @@ import type { Component } from '@moonshot-ai/pi-tui'; import { Spacer, Text, visibleWidth } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; import type { ColorPalette } from '#/tui/theme/colors'; @@ -20,7 +21,9 @@ export class CronMessageComponent implements Component { ) { const missed = data.missedCount !== undefined; this.data = data; - this.title = missed ? 'Missed scheduled reminders' : 'Scheduled reminder fired'; + this.title = missed + ? t('tui.messages.cronMessage.missedTitle') + : t('tui.messages.cronMessage.firedTitle'); this.detail = cronDetail(data); this.prompt = prompt; this.promptText = new Text(currentTheme.fg('text', prompt), 0, 0); @@ -71,14 +74,16 @@ export class CronMessageComponent implements Component { function cronDetail(data: CronTranscriptData): string | undefined { const parts: string[] = []; if (data.cron !== undefined && data.cron.length > 0) parts.push(data.cron); - if (data.jobId !== undefined && data.jobId.length > 0) parts.push(`job ${data.jobId}`); - if (data.recurring === false) parts.push('one-shot'); + if (data.jobId !== undefined && data.jobId.length > 0) { + parts.push(t('tui.messages.cronMessage.job', { jobId: data.jobId })); + } + if (data.recurring === false) parts.push(t('tui.messages.cronMessage.oneShot')); if (data.coalescedCount !== undefined && data.coalescedCount > 1) { - parts.push(`${String(data.coalescedCount)} fires coalesced`); + parts.push(t('tui.messages.cronMessage.coalesced', { count: data.coalescedCount })); } if (data.missedCount !== undefined) { - parts.push(`${String(data.missedCount)} missed`); + parts.push(t('tui.messages.cronMessage.missed', { count: data.missedCount })); } - if (data.stale === true) parts.push('final delivery'); + if (data.stale === true) parts.push(t('tui.messages.cronMessage.finalDelivery')); return parts.length > 0 ? parts.join(' | ') : undefined; } diff --git a/apps/kimi-code/src/tui/components/messages/goal-format.ts b/apps/kimi-code/src/tui/components/messages/goal-format.ts index 2a59334259..0344b990c4 100644 --- a/apps/kimi-code/src/tui/components/messages/goal-format.ts +++ b/apps/kimi-code/src/tui/components/messages/goal-format.ts @@ -1,11 +1,21 @@ +import { t } from '#/i18n'; + export function formatGoalElapsed(ms: number): string { const totalSeconds = Math.round(ms / 1000); - if (totalSeconds < 60) return `${String(totalSeconds)}s`; + if (totalSeconds < 60) return t('tui.messages.goalFormat.elapsedSeconds', { count: totalSeconds }); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; - if (minutes < 60) return `${String(minutes)}m ${seconds.toString().padStart(2, '0')}s`; + if (minutes < 60) { + return t('tui.messages.goalFormat.elapsedMinutes', { + minutes, + seconds: seconds.toString().padStart(2, '0'), + }); + } const hours = Math.floor(minutes / 60); - return `${String(hours)}h ${(minutes % 60).toString().padStart(2, '0')}m`; + return t('tui.messages.goalFormat.elapsedHours', { + hours, + minutes: (minutes % 60).toString().padStart(2, '0'), + }); } export function pluralizeGoalCount(n: number, singular: string, plural?: string): string { diff --git a/apps/kimi-code/src/tui/components/messages/goal-markers.ts b/apps/kimi-code/src/tui/components/messages/goal-markers.ts index f4482941f5..18fdbfccea 100644 --- a/apps/kimi-code/src/tui/components/messages/goal-markers.ts +++ b/apps/kimi-code/src/tui/components/messages/goal-markers.ts @@ -10,6 +10,7 @@ import { truncateToWidth, type Component } from '@moonshot-ai/pi-tui'; import type { GoalChange } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; import type { ColorToken } from '#/tui/theme'; @@ -127,7 +128,7 @@ function markerSpec( return prominentMarker(resumedHeadline(actor), 'primary'); case 'blocked': // The system stopped pursuing the goal; resumable via `/goal resume`. - return { headline: 'Goal blocked', accentToken: 'warning' }; + return { headline: t('tui.messages.goalMarkers.blocked'), accentToken: 'warning' }; default: return null; } @@ -151,18 +152,22 @@ function prominentMarker(headline: string, accentToken: ColorToken) { } function pausedHeadline(reason: string | undefined, actor: GoalMarkerActor | undefined): string { - if (reason === 'Paused after interruption') return "Goal paused due to user's interruption"; - if (actor === 'user') return 'Goal paused by the user.'; - if (reason?.startsWith('Paused ') === true) return `Goal ${lowercaseFirst(reason)}`; - if (reason !== undefined && reason.length > 0) return `Goal paused: ${reason}`; - if (actor === 'model') return 'Goal paused by the agent.'; - return 'Goal paused'; + if (reason === 'Paused after interruption') return t('tui.messages.goalMarkers.pausedInterruption'); + if (actor === 'user') return t('tui.messages.goalMarkers.pausedByUser'); + if (reason?.startsWith('Paused ') === true) { + return t('tui.messages.goalMarkers.pausedWithLowerReason', { reason: lowercaseFirst(reason) }); + } + if (reason !== undefined && reason.length > 0) { + return t('tui.messages.goalMarkers.pausedWithReason', { reason }); + } + if (actor === 'model') return t('tui.messages.goalMarkers.pausedByAgent'); + return t('tui.messages.goalMarkers.pausedGeneric'); } function resumedHeadline(actor: GoalMarkerActor | undefined): string { - if (actor === 'user') return 'Goal resumed by the user.'; - if (actor === 'model') return 'Goal resumed by the agent.'; - return 'Goal resumed'; + if (actor === 'user') return t('tui.messages.goalMarkers.resumedByUser'); + if (actor === 'model') return t('tui.messages.goalMarkers.resumedByAgent'); + return t('tui.messages.goalMarkers.resumedGeneric'); } function lowercaseFirst(text: string): string { diff --git a/apps/kimi-code/src/tui/components/messages/goal-panel.ts b/apps/kimi-code/src/tui/components/messages/goal-panel.ts index d01f580faa..9cfa00fe9e 100644 --- a/apps/kimi-code/src/tui/components/messages/goal-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/goal-panel.ts @@ -22,6 +22,7 @@ import { } from '@moonshot-ai/pi-tui'; import type { GoalSnapshot, GoalStatus } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { MESSAGE_INDENT } from '#/tui/constant/rendering'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; @@ -58,7 +59,7 @@ export class GoalSetMessageComponent implements Component { invalidate(): void {} render(width: number): string[] { - return renderLifecycleLine('Goal set', width); + return renderLifecycleLine(t('tui.messages.goalPanel.goalSet'), width); } } @@ -67,7 +68,7 @@ export class UpcomingGoalAddedMessageComponent implements Component { render(width: number): string[] { return renderLifecycleLine( - 'Upcoming goal added. It will start after the current goal is complete.', + t('tui.messages.goalPanel.upcomingAdded'), width, ); } @@ -125,7 +126,7 @@ export class GoalStatusMessageComponent implements Component { /** Box title, e.g. ` Goal · active `. */ export function goalPanelTitle(goal: GoalSnapshot): string { - return ` Goal · ${goal.status} `; + return t('tui.messages.goalPanel.title', { status: statusLabel(goal.status) }); } export function buildGoalReportLines(goal: GoalSnapshot, wrapWidth: number = WRAP_WIDTH): string[] { @@ -160,21 +161,21 @@ export function buildGoalReportLines(goal: GoalSnapshot, wrapWidth: number = WRA if (showReason) { lines.push( row( - 'Status', - currentTheme.fg(statusColor, goal.status) + + t('tui.messages.goalPanel.statusLabel'), + currentTheme.fg(statusColor, statusLabel(goal.status)) + (reason !== undefined ? muted(` — ${reason}`) : ''), ), ); } - lines.push(row('Running', value(formatGoalElapsed(goal.wallClockMs)))); - lines.push(row('Turns', value(`${goal.turnsUsed}`))); - lines.push(row('Tokens', value(formatTokenCount(goal.tokensUsed)))); + lines.push(row(t('tui.messages.goalPanel.runningLabel'), value(formatGoalElapsed(goal.wallClockMs)))); + lines.push(row(t('tui.messages.goalPanel.turnsLabel'), value(`${goal.turnsUsed}`))); + lines.push(row(t('tui.messages.goalPanel.tokensLabel'), value(formatTokenCount(goal.tokensUsed)))); if (!isComplete) { const stop = formatStopRow(goal); lines.push( stop !== null - ? row('Stop', value(stop)) - : muted('No stop condition — runs until evaluated complete.'), + ? row(t('tui.messages.goalPanel.stopLabel'), value(stop)) + : muted(t('tui.messages.goalPanel.noStopCondition')), ); } return lines; @@ -185,13 +186,26 @@ function formatStopRow(goal: GoalSnapshot): string | null { const { budget } = goal; const parts: string[] = []; if (budget.turnBudget !== null) { - parts.push(`after ${budget.turnBudget} turns (${goal.turnsUsed}/${budget.turnBudget})`); + parts.push( + t('tui.messages.goalPanel.stopTurns', { + turnBudget: budget.turnBudget, + turnsUsed: goal.turnsUsed, + }), + ); } if (budget.tokenBudget !== null) { - parts.push(`at ${formatTokenCount(budget.tokenBudget)} tokens`); + parts.push( + t('tui.messages.goalPanel.stopTokens', { + tokenBudget: formatTokenCount(budget.tokenBudget), + }), + ); } if (budget.wallClockBudgetMs !== null) { - parts.push(`after ${formatGoalElapsed(budget.wallClockBudgetMs)}`); + parts.push( + t('tui.messages.goalPanel.stopTime', { + duration: formatGoalElapsed(budget.wallClockBudgetMs), + }), + ); } return parts.length > 0 ? parts.join(', ') : null; } @@ -209,6 +223,19 @@ function statusToken(status: GoalStatus): ColorToken { } } +function statusLabel(status: GoalStatus): string { + switch (status) { + case 'active': + return t('tui.messages.goalPanel.statusActive'); + case 'complete': + return t('tui.messages.goalPanel.statusComplete'); + case 'blocked': + return t('tui.messages.goalPanel.statusBlocked'); + case 'paused': + return t('tui.messages.goalPanel.statusPaused'); + } +} + /** Word-wrap to `width`, capped at `maxLines` (last line gets an ellipsis when clipped). */ function wrap(text: string, width: number, maxLines: number): string[] { const safeWidth = Math.max(1, width); diff --git a/apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts b/apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts index 416cf6a331..e67dc6095d 100644 --- a/apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts @@ -1,5 +1,6 @@ import type { McpServerInfo } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; export interface McpStatusReportOptions { @@ -14,13 +15,20 @@ const STATUS_PRIORITY: Record<McpServerInfo['status'], number> = { disabled: 4, }; -const STATUS_LABEL: Record<McpServerInfo['status'], string> = { - connected: 'connected', - pending: 'pending', - 'needs-auth': 'needs auth', - failed: 'failed', - disabled: 'disabled', -}; +function statusLabel(status: McpServerInfo['status']): string { + switch (status) { + case 'connected': + return t('tui.messages.mcpStatusPanel.status.connected'); + case 'pending': + return t('tui.messages.mcpStatusPanel.status.pending'); + case 'needs-auth': + return t('tui.messages.mcpStatusPanel.status.needsAuth'); + case 'failed': + return t('tui.messages.mcpStatusPanel.status.failed'); + case 'disabled': + return t('tui.messages.mcpStatusPanel.status.disabled'); + } +} const SUMMARY_ORDER: readonly McpServerInfo['status'][] = [ 'connected', @@ -47,12 +55,17 @@ function statusPainter( } function formatToolCount(server: McpServerInfo): string { - if (server.status === 'disabled') return '—'; - return `${server.toolCount} tool${server.toolCount === 1 ? '' : 's'}`; + if (server.status === 'disabled') return t('tui.messages.mcpStatusPanel.disabledToolCount'); + return t( + server.toolCount === 1 + ? 'tui.messages.mcpStatusPanel.tool_one' + : 'tui.messages.mcpStatusPanel.tool_other', + { count: server.toolCount }, + ); } function formatToolsAvailable(count: number): string { - return `${count} tool${count === 1 ? '' : 's'} available`; + return t('tui.messages.mcpStatusPanel.toolsAvailable', { count }); } /** @@ -84,10 +97,10 @@ function buildSummary(servers: readonly McpServerInfo[]): string { } const parts: string[] = []; for (const status of SUMMARY_ORDER) { - const n = counts[status]; - if (n === undefined || n === 0) continue; - parts.push(`${n} ${STATUS_LABEL[status]}`); - } + const n = counts[status]; + if (n === undefined || n === 0) continue; + parts.push(`${n} ${statusLabel(status)}`); + } parts.push(formatToolsAvailable(toolsAvailable)); return parts.join(' · '); } @@ -99,33 +112,36 @@ export function buildMcpStatusReportLines(options: McpStatusReportOptions): stri const value = (text: string) => currentTheme.fg('text', text); const error = (text: string) => currentTheme.fg('error', text); - const lines: string[] = [accent('Servers')]; + const lines: string[] = [accent(t('tui.messages.mcpStatusPanel.servers'))]; if (servers.length === 0) { - lines.push(muted(' No MCP servers configured. Run /mcp-config to add one.')); + lines.push(muted(` ${t('tui.messages.mcpStatusPanel.noServers')}`)); return lines; } - const nameWidth = Math.max('Name'.length, ...servers.map((server) => server.name.length)); + const nameWidth = Math.max( + t('tui.messages.mcpStatusPanel.nameLabel').length, + ...servers.map((server) => server.name.length), + ); const statusWidth = Math.max( - 'Status'.length, - ...servers.map((server) => STATUS_LABEL[server.status].length), + t('tui.messages.mcpStatusPanel.statusLabel').length, + ...servers.map((server) => statusLabel(server.status).length), ); const transportWidth = Math.max( - 'Transport'.length, + t('tui.messages.mcpStatusPanel.transportLabel').length, ...servers.map((server) => server.transport.length), ); lines.push( - ` ${muted('Name'.padEnd(nameWidth))} ${muted('Status'.padEnd(statusWidth))} ${muted( - 'Transport'.padEnd(transportWidth), - )} ${muted('Tools')}`, + ` ${muted(t('tui.messages.mcpStatusPanel.nameLabel').padEnd(nameWidth))} ${muted( + t('tui.messages.mcpStatusPanel.statusLabel').padEnd(statusWidth), + )} ${muted(t('tui.messages.mcpStatusPanel.transportLabel').padEnd(transportWidth))} ${muted( + t('tui.messages.mcpStatusPanel.toolsLabel'), + )}`, ); for (const server of servers) { - const status = statusPainter( - server.status, - )(STATUS_LABEL[server.status].padEnd(statusWidth)); + const status = statusPainter(server.status)(statusLabel(server.status).padEnd(statusWidth)); lines.push( ` ${value(server.name.padEnd(nameWidth))} ${status} ${muted( server.transport.padEnd(transportWidth), @@ -137,16 +153,20 @@ export function buildMcpStatusReportLines(options: McpStatusReportOptions): stri server.error !== undefined && server.error.trim().length > 0 ) { - lines.push(` ${muted('error:')} ${error(formatErrorLine(server.error))}`); + lines.push(` ${muted(t('tui.messages.mcpStatusPanel.errorLabel'))} ${error(formatErrorLine(server.error))}`); } if (server.status === 'needs-auth') { - lines.push(` ${muted('action:')} ${value(`run /mcp-config login ${server.name}`)}`); + lines.push( + ` ${muted(t('tui.messages.mcpStatusPanel.actionLabel'))} ${value( + t('tui.messages.mcpStatusPanel.actionLogin', { name: server.name }), + )}`, + ); } } lines.push(''); lines.push(` ${value(buildSummary(servers))}`); - lines.push(` ${muted('Configure with')} ${value('/mcp-config')}`); + lines.push(` ${muted(t('tui.messages.mcpStatusPanel.configureWith'))} ${value('/mcp-config')}`); return lines; } diff --git a/apps/kimi-code/src/tui/components/messages/plan-box.ts b/apps/kimi-code/src/tui/components/messages/plan-box.ts index d1eeec03cb..7efba9e9b4 100644 --- a/apps/kimi-code/src/tui/components/messages/plan-box.ts +++ b/apps/kimi-code/src/tui/components/messages/plan-box.ts @@ -11,10 +11,11 @@ import { Markdown, truncateToWidth, visibleWidth, type Component, type MarkdownT import chalk from 'chalk'; import { toTerminalHyperlink } from '#/utils/terminal-hyperlink'; +import { t } from '#/i18n'; const LEFT_MARGIN = 2; // two-space indent matching other tool call children const SIDE_PADDING = 1; // space between the │ and the content on each side -const TITLE_PREFIX = ' plan: '; +const TITLE_PREFIX = t('tui.messages.planBox.titlePrefix'); const TITLE_SUFFIX = ' '; export interface PlanBoxOptions { @@ -95,9 +96,9 @@ export class PlanBoxComponent implements Component { } private buildTitle(horzLen: number): string { - const fallback = ' plan '; + const fallback = t('tui.messages.planBox.fallback'); const statusSuffix = this.buildStatusSuffix(); - const fallbackWithStatus = ` plan${statusSuffix} `; + const fallbackWithStatus = `${fallback.trimEnd()}${statusSuffix} `; const budget = Math.max(0, horzLen - 1); const fallbackTitle = truncateToWidth( visibleWidth(fallbackWithStatus) <= budget ? fallbackWithStatus : fallback, diff --git a/apps/kimi-code/src/tui/components/messages/plugin-command.ts b/apps/kimi-code/src/tui/components/messages/plugin-command.ts index afbc2b4447..25d171802b 100644 --- a/apps/kimi-code/src/tui/components/messages/plugin-command.ts +++ b/apps/kimi-code/src/tui/components/messages/plugin-command.ts @@ -13,6 +13,7 @@ import { Container, Text, Spacer } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; const ARGS_PREVIEW_MAX = 200; @@ -29,7 +30,7 @@ export class PluginCommandComponent extends Container { this.args = args; this.addChild(new Spacer(1)); const head = - currentTheme.boldFg('primary', '▶ Invoked command: ') + + currentTheme.boldFg('primary', t('tui.messages.pluginCommand.invoked')) + currentTheme.boldFg('roleUser', this.label); this.headText = new Text(head, 0, 0); this.addChild(this.headText); @@ -44,7 +45,7 @@ export class PluginCommandComponent extends Container { override invalidate(): void { const head = - currentTheme.boldFg('primary', '▶ Invoked command: ') + + currentTheme.boldFg('primary', t('tui.messages.pluginCommand.invoked')) + currentTheme.boldFg('roleUser', this.label); this.headText.setText(head); if (this.previewText !== undefined && this.args !== undefined) { diff --git a/apps/kimi-code/src/tui/components/messages/plugins-status-panel.ts b/apps/kimi-code/src/tui/components/messages/plugins-status-panel.ts index 4654ee82df..e781ec3a32 100644 --- a/apps/kimi-code/src/tui/components/messages/plugins-status-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/plugins-status-panel.ts @@ -1,5 +1,6 @@ import type { PluginInfo, PluginSummary } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; import { CURATED_BADGE, @@ -22,9 +23,9 @@ export function buildPluginsListLines(input: PluginsListPanelInput): readonly st const warning = (text: string) => currentTheme.fg('warning', text); if (input.plugins.length === 0) { return [ - muted('No plugins installed.'), + muted(t('tui.messages.pluginsStatusPanel.noPlugins')), '', - value('Run /plugins to install one.'), + value(t('tui.messages.pluginsStatusPanel.installHint')), ]; } const renderTrustBadge = (label: PluginTrustLabel): string => { @@ -34,10 +35,14 @@ export function buildPluginsListLines(input: PluginsListPanelInput): readonly st }; const lines: string[] = []; for (const plugin of input.plugins) { - const enabled = plugin.enabled ? success('enabled') : muted('disabled'); + const enabled = plugin.enabled + ? success(t('tui.messages.pluginsStatusPanel.enabled')) + : muted(t('tui.messages.pluginsStatusPanel.disabled')); const state = plugin.state === 'ok' ? '' : ` [${plugin.state}]`; const version = plugin.version ?? '-'; - const diagnostics = plugin.hasErrors ? warning(' | diagnostics: see /plugins info') : ''; + const diagnostics = plugin.hasErrors + ? warning(t('tui.messages.pluginsStatusPanel.diagnosticsHint')) + : ''; const sourceTag = muted(`[${formatPluginSourceLabel(plugin)}]`); const trustBadge = ` ${renderTrustBadge(pluginTrustLabel(plugin))}`; lines.push( @@ -45,9 +50,14 @@ export function buildPluginsListLines(input: PluginsListPanelInput): readonly st ); const mcp = plugin.mcpServerCount > 0 - ? ` | ${plugin.enabledMcpServerCount}/${plugin.mcpServerCount} mcp` + ? ` | ${t('tui.messages.pluginsStatusPanel.mcpCount', { + enabled: plugin.enabledMcpServerCount, + total: plugin.mcpServerCount, + })}` : ''; - lines.push(` ${muted('skills:')} ${value(String(plugin.skillCount))}${muted(mcp)}${diagnostics}`); + lines.push( + ` ${muted(t('tui.messages.pluginsStatusPanel.skillsLabel'))} ${value(String(plugin.skillCount))}${muted(mcp)}${diagnostics}`, + ); } return lines; } @@ -65,72 +75,119 @@ export function buildPluginsInfoLines(input: PluginsInfoPanelInput): readonly st const warning = (text: string) => currentTheme.fg('warning', text); const error = (text: string) => currentTheme.fg('error', text); const primary = (text: string) => currentTheme.fg('primary', text); - const status = info.enabled ? success('enabled') : muted('disabled'); + const status = info.enabled + ? success(t('tui.messages.pluginsStatusPanel.enabled')) + : muted(t('tui.messages.pluginsStatusPanel.disabled')); const trustLine = (() => { const label = pluginTrustLabel(info); if (label === 'official') { - return `${muted('Trust:')} ${success(OFFICIAL_BADGE)} ${muted('(Kimi-built and -maintained)')}`; + return `${muted(t('tui.messages.pluginsStatusPanel.trust'))} ${success(OFFICIAL_BADGE)} ${muted(t('tui.messages.pluginsStatusPanel.officialDescription'))}`; } if (label === 'curated') { - return `${muted('Trust:')} ${primary(CURATED_BADGE)} ${muted('(Kimi-reviewed, upstream-maintained)')}`; + return `${muted(t('tui.messages.pluginsStatusPanel.trust'))} ${primary(CURATED_BADGE)} ${muted(t('tui.messages.pluginsStatusPanel.curatedDescription'))}`; } - return `${muted('Trust:')} ${muted(THIRD_PARTY_BADGE)}`; + return `${muted(t('tui.messages.pluginsStatusPanel.trust'))} ${muted(THIRD_PARTY_BADGE)}`; })(); const lines: string[] = [ `${value(info.displayName)} (${muted(info.id)}) ${muted(info.version ?? '')}`.trim(), - `${muted('Status:')} ${status} | ${muted('state:')} ${stateText(info.state)}`, + `${muted(t('tui.messages.pluginsStatusPanel.status'))} ${status}${muted(t('tui.messages.pluginsStatusPanel.statePrefix'))}${stateText(info.state)}`, trustLine, - `${muted('Source:')} ${value(info.source)}`, - `${muted('Root:')} ${value(info.root)}`, + `${muted(t('tui.messages.pluginsStatusPanel.source'))} ${value(info.source)}`, + `${muted(t('tui.messages.pluginsStatusPanel.root'))} ${value(info.root)}`, ]; if (info.source === 'github' && info.github !== undefined) { const refLabel = `${info.github.ref.kind}:${info.github.ref.value}`; - lines.push(`${muted('GitHub:')} ${value(`${info.github.owner}/${info.github.repo}`)} ${muted(`@${refLabel}`)}`); + lines.push( + `${muted(t('tui.messages.pluginsStatusPanel.github'))} ${value(`${info.github.owner}/${info.github.repo}`)} ${muted(`@${refLabel}`)}`, + ); if (info.github.installedSha !== undefined) { - lines.push(`${muted('Installed SHA:')} ${value(info.github.installedSha)}`); + lines.push( + `${muted(t('tui.messages.pluginsStatusPanel.installedSha'))} ${value(info.github.installedSha)}`, + ); } } - if (info.originalSource !== undefined) lines.push(`${muted('Original source:')} ${value(info.originalSource)}`); - lines.push(`${muted('Installed at:')} ${value(info.installedAt)}`); + if (info.originalSource !== undefined) { + lines.push( + `${muted(t('tui.messages.pluginsStatusPanel.originalSource'))} ${value(info.originalSource)}`, + ); + } + lines.push(`${muted(t('tui.messages.pluginsStatusPanel.installedAt'))} ${value(info.installedAt)}`); if (info.updatedAt !== undefined && info.updatedAt !== info.installedAt) { - lines.push(`${muted('Last updated:')} ${value(info.updatedAt)}`); + lines.push(`${muted(t('tui.messages.pluginsStatusPanel.lastUpdated'))} ${value(info.updatedAt)}`); } if (info.manifestPath !== undefined) { - const kindSuffix = info.manifestKind !== undefined ? ` ${muted(`(${info.manifestKind})`)}` : ''; - lines.push(`${muted('Manifest:')} ${value(info.manifestPath)}${kindSuffix}`); + const kindSuffix = + info.manifestKind !== undefined + ? ` ${muted(t('tui.messages.pluginsStatusPanel.manifestKind', { kind: info.manifestKind }))}` + : ''; + lines.push(`${muted(t('tui.messages.pluginsStatusPanel.manifest'))} ${value(info.manifestPath)}${kindSuffix}`); } if (info.shadowedManifestPath !== undefined) { - lines.push(`${muted('Shadowed:')} ${value(info.shadowedManifestPath)}`); + lines.push(`${muted(t('tui.messages.pluginsStatusPanel.shadowed'))} ${value(info.shadowedManifestPath)}`); } const sessionStartSkill = info.manifest?.sessionStart?.skill; if (sessionStartSkill !== undefined) { - lines.push(`${muted('Session start:')} ${value(sessionStartSkill)}`); + lines.push( + `${muted(t('tui.messages.pluginsStatusPanel.sessionStart'))} ${value(sessionStartSkill)}`, + ); } if (info.manifest?.skillInstructions !== undefined) { - lines.push(`${muted('Skill instructions:')} ${value('present')}`); + lines.push( + `${muted(t('tui.messages.pluginsStatusPanel.skillInstructions'))} ${value(t('tui.messages.pluginsStatusPanel.skillInstructionsPresent'))}`, + ); } lines.push(''); - lines.push(value(`Skills (${info.manifest?.skills?.length ?? 0}):`)); + lines.push( + value( + t('tui.messages.pluginsStatusPanel.skills', { + count: info.manifest?.skills?.length ?? 0, + }), + ), + ); for (const dir of info.manifest?.skills ?? []) lines.push(` ${muted('-')} ${value(dir)}`); if (info.mcpServers.length > 0) { lines.push(''); - lines.push(value(`MCP servers (${info.enabledMcpServerCount}/${info.mcpServerCount} enabled):`)); - lines.push(muted(` Enabled by default; disable with /plugins mcp disable ${info.id} <server>.`)); + lines.push( + value( + t('tui.messages.pluginsStatusPanel.mcpServers', { + enabled: info.enabledMcpServerCount, + total: info.mcpServerCount, + }), + ), + ); + lines.push( + muted( + ` ${t('tui.messages.pluginsStatusPanel.mcpHint', { id: info.id })}`, + ), + ); for (const server of info.mcpServers) { - const enabled = server.enabled ? success('enabled') : muted('disabled'); + const enabled = server.enabled + ? success(t('tui.messages.pluginsStatusPanel.enabled')) + : muted(t('tui.messages.pluginsStatusPanel.disabled')); lines.push(` ${muted('-')} ${value(server.name)} ${enabled} ${muted(`(${server.runtimeName})`)}`); if (server.transport === 'stdio') { - const args = server.args !== undefined && server.args.length > 0 ? ` ${server.args.join(' ')}` : ''; - lines.push(` ${muted('command:')} ${value(`${server.command ?? ''}${args}`.trim())}`); - if (server.cwd !== undefined) lines.push(` ${muted('cwd:')} ${value(server.cwd)}`); + const args = + server.args !== undefined && server.args.length > 0 ? ` ${server.args.join(' ')}` : ''; + lines.push( + ` ${muted(t('tui.messages.pluginsStatusPanel.command'))} ${value(`${server.command ?? ''}${args}`.trim())}`, + ); + if (server.cwd !== undefined) { + lines.push(` ${muted(t('tui.messages.pluginsStatusPanel.cwd'))} ${value(server.cwd)}`); + } if (server.envKeys !== undefined && server.envKeys.length > 0) { - lines.push(` ${muted('env:')} ${value(server.envKeys.join(', '))}`); + lines.push( + ` ${muted(t('tui.messages.pluginsStatusPanel.env'))} ${value(server.envKeys.join(', '))}`, + ); } } else { - lines.push(` ${muted('url:')} ${value(server.url ?? '')}`); + lines.push( + ` ${muted(t('tui.messages.pluginsStatusPanel.url'))} ${value(server.url ?? '')}`, + ); if (server.headerKeys !== undefined && server.headerKeys.length > 0) { - lines.push(` ${muted('headers:')} ${value(server.headerKeys.join(', '))}`); + lines.push( + ` ${muted(t('tui.messages.pluginsStatusPanel.headers'))} ${value(server.headerKeys.join(', '))}`, + ); } } } @@ -139,20 +196,32 @@ export function buildPluginsInfoLines(input: PluginsInfoPanelInput): readonly st const iface = info.manifest?.interface; if (iface !== undefined) { lines.push(''); - lines.push(value('Display:')); - if (iface.shortDescription !== undefined) lines.push(` ${muted('-')} ${value(iface.shortDescription)}`); - if (iface.developerName !== undefined) lines.push(` ${muted('-')} ${value(`by ${iface.developerName}`)}`); + lines.push(value(t('tui.messages.pluginsStatusPanel.display'))); + if (iface.shortDescription !== undefined) { + lines.push(` ${muted('-')} ${value(iface.shortDescription)}`); + } + if (iface.developerName !== undefined) { + lines.push( + ` ${muted('-')} ${value(t('tui.messages.pluginsStatusPanel.by', { name: iface.developerName }))}`, + ); + } if (iface.websiteURL !== undefined) lines.push(` ${muted('-')} ${value(iface.websiteURL)}`); } if (info.manifest?.keywords !== undefined && info.manifest.keywords.length > 0) { lines.push(''); - lines.push(muted(`Keywords: ${info.manifest.keywords.join(', ')}`)); + lines.push( + muted( + t('tui.messages.pluginsStatusPanel.keywords', { + keywords: info.manifest.keywords.join(', '), + }), + ), + ); } if (info.diagnostics.length > 0) { lines.push(''); - lines.push(value('Diagnostics:')); + lines.push(value(t('tui.messages.pluginsStatusPanel.diagnostics'))); for (const d of info.diagnostics) { const paint = d.severity === 'error' ? error : d.severity === 'warn' ? warning : muted; lines.push(` ${paint(`[${d.severity}]`)} ${value(d.message)}`); diff --git a/apps/kimi-code/src/tui/components/messages/read-group.ts b/apps/kimi-code/src/tui/components/messages/read-group.ts index 141562e4c0..b0795ed47a 100644 --- a/apps/kimi-code/src/tui/components/messages/read-group.ts +++ b/apps/kimi-code/src/tui/components/messages/read-group.ts @@ -25,6 +25,7 @@ import { Container, Spacer, Text } from '@moonshot-ai/pi-tui'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; +import { t } from '#/i18n'; import type { ToolCallComponent, ToolCallReadSnapshot } from './tool-call'; @@ -135,21 +136,30 @@ export class ReadGroupComponent extends Container { if (pending > 0) { const bullet = currentTheme.fg('text', STATUS_BULLET); - const label = currentTheme.boldFg('primary', `Reading ${String(total)} files…`); + const label = currentTheme.boldFg('primary', t('tui.messages.readGroup.readingFiles', { count: total })); return `${bullet}${label}`; } // All reads have finished, either successfully or with failures. if (failed === total) { const bullet = currentTheme.fg('error', '✗ '); - const label = currentTheme.boldFg('error', `Read ${String(total)} files`); - return `${bullet}${label}${currentTheme.fg('error', ' · failed')}`; + const label = currentTheme.boldFg('error', t('tui.messages.readGroup.readFiles', { count: total })); + return `${bullet}${label}${currentTheme.fg('error', ` · ${t('tui.messages.readGroup.failed')}`)}`; } const bullet = currentTheme.fg('success', STATUS_BULLET); - const label = currentTheme.boldFg('primary', `Read ${String(total)} files`); - const linesPart = dim(` · ${String(totalLines)} ${totalLines === 1 ? 'line' : 'lines'}`); - const failPart = failed > 0 ? currentTheme.fg('error', ` · ${String(failed)} failed`) : ''; + const label = currentTheme.boldFg('primary', t('tui.messages.readGroup.readFiles', { count: total })); + const linesPart = dim( + ` · ${t( + totalLines === 1 + ? 'tui.messages.readGroup.line_one' + : 'tui.messages.readGroup.line_other', + { count: totalLines }, + )}`, + ); + const failPart = failed > 0 + ? currentTheme.fg('error', ` · ${String(failed)} ${t('tui.messages.readGroup.failed')}`) + : ''; return `${bullet}${label}${linesPart}${failPart}`; } @@ -161,11 +171,16 @@ export class ReadGroupComponent extends Container { let tail: string; if (snap.phase === 'pending') { - tail = dim(' · reading…'); + tail = dim(` · ${t('tui.messages.readGroup.reading')}`); } else if (snap.phase === 'failed') { - tail = currentTheme.fg('error', ' · failed'); + tail = currentTheme.fg('error', ` · ${t('tui.messages.readGroup.failed')}`); } else { - tail = dim(` · ${String(snap.lines)} ${snap.lines === 1 ? 'line' : 'lines'}`); + tail = dim( + ` · ${t( + snap.lines === 1 ? 'tui.messages.readGroup.line_one' : 'tui.messages.readGroup.line_other', + { count: snap.lines }, + )}`, + ); } return ` ${branch} ${pathPart}${tail}`; } diff --git a/apps/kimi-code/src/tui/components/messages/shell-run.ts b/apps/kimi-code/src/tui/components/messages/shell-run.ts index ca99f2e763..1df33b9740 100644 --- a/apps/kimi-code/src/tui/components/messages/shell-run.ts +++ b/apps/kimi-code/src/tui/components/messages/shell-run.ts @@ -1,5 +1,6 @@ import { Container, Text } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; import { formatBashOutputForDisplay, sanitizeShellOutput } from '#/tui/utils/shell-output'; @@ -103,7 +104,7 @@ export class ShellRunComponent extends Container { private renderText(): string { try { if (this.backgrounded) { - return ` ${currentTheme.fg('textDim', 'Moved to background.')}`; + return ` ${currentTheme.fg('textDim', t('tui.messages.shellRun.backgrounded'))}`; } if (!this.running) { return formatBashOutputForDisplay(this.finalStdout, this.finalStderr, this.finalIsError) @@ -117,18 +118,21 @@ export class ShellRunComponent extends Container { let body: string; let extra = 0; if (trimmed.length === 0) { - body = ` ${dim('Running…')}`; + body = ` ${dim(t('tui.messages.shellRun.running'))}`; } else { const lines = trimmed.split('\n'); const tail = lines.slice(-RUNNING_TAIL_LINES); extra = Math.max(0, lines.length - RUNNING_TAIL_LINES); body = tail.map((line) => ` ${dim(line)}`).join('\n'); } - const timing = ` ${dim(`${extra > 0 ? `+${extra} lines ` : ''}(${elapsed}s)`)}`; - const hint = ` ${dim('(ctrl+b to run in background)')}`; + const timing = + extra > 0 + ? ` ${dim(t('tui.messages.shellRun.timing', { extra, elapsed }))}` + : ` ${dim(t('tui.messages.shellRun.timingNoExtra', { elapsed }))}`; + const hint = ` ${dim(t('tui.messages.shellRun.hint'))}`; return `${body}\n${timing}\n${hint}`; } catch { - return ' (output unavailable)'; + return ` ${t('tui.messages.shellRun.outputUnavailable')}`; } } } diff --git a/apps/kimi-code/src/tui/components/messages/skill-activation.ts b/apps/kimi-code/src/tui/components/messages/skill-activation.ts index f977d21d92..876e648be9 100644 --- a/apps/kimi-code/src/tui/components/messages/skill-activation.ts +++ b/apps/kimi-code/src/tui/components/messages/skill-activation.ts @@ -14,6 +14,7 @@ import { Container, Text, Spacer } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; import type { SkillActivationTrigger } from '#/tui/types'; @@ -35,7 +36,7 @@ export class SkillActivationComponent extends Container { this.args = args; this.addChild(new Spacer(1)); const head = - currentTheme.boldFg('primary', '▶ Activated skill: ') + + currentTheme.boldFg('primary', t('tui.messages.skillActivation.activated')) + currentTheme.boldFg('roleUser', name); this.headText = new Text(head, 0, 0); this.addChild(this.headText); @@ -50,7 +51,7 @@ export class SkillActivationComponent extends Container { override invalidate(): void { const head = - currentTheme.boldFg('primary', '▶ Activated skill: ') + + currentTheme.boldFg('primary', t('tui.messages.skillActivation.activated')) + currentTheme.boldFg('roleUser', this.name); this.headText.setText(head); if (this.previewText !== undefined && this.args !== undefined) { diff --git a/apps/kimi-code/src/tui/components/messages/status-panel.ts b/apps/kimi-code/src/tui/components/messages/status-panel.ts index 1d788d53b5..82ad20cbdc 100644 --- a/apps/kimi-code/src/tui/components/messages/status-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/status-panel.ts @@ -13,6 +13,7 @@ import { type ThinkingEffort, } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { PRODUCT_NAME } from '#/constant/app'; import { currentTheme } from '#/tui/theme'; import { @@ -63,10 +64,13 @@ function displayModelName(alias: string, models: Record<string, ModelAlias>): st function formatModelStatus(options: StatusReportOptions): string { const model = options.status?.model ?? options.model; - if (model.trim().length === 0) return 'not set'; + if (model.trim().length === 0) return t('tui.messages.statusPanel.modelNotSet'); const effort = options.status?.thinkingEffort ?? options.thinkingEffort; - return `${displayModelName(model, options.availableModels)} (thinking ${effort})`; + return t('tui.messages.statusPanel.modelStatus', { + model: displayModelName(model, options.availableModels), + effort, + }); } function addFieldRows( @@ -105,39 +109,57 @@ export function buildStatusReportLines(options: StatusReportOptions): string[] { const permission = options.status?.permission ?? options.permissionMode; const planMode = options.status?.planMode ?? options.planMode; - const sessionId = options.sessionId.trim().length > 0 ? options.sessionId : 'none'; + const sessionId = + options.sessionId.trim().length > 0 + ? options.sessionId + : t('tui.messages.statusPanel.sessionNone'); const rows: FieldRow[] = [ - { label: 'Model', value: formatModelStatus(options) }, - { label: 'Directory', value: options.workDir }, - { label: 'Permissions', value: permission }, - { label: 'Plan mode', value: planMode ? 'on' : 'off' }, - { label: 'Session', value: sessionId }, + { label: t('tui.messages.statusPanel.modelLabel'), value: formatModelStatus(options) }, + { label: t('tui.messages.statusPanel.directoryLabel'), value: options.workDir }, + { label: t('tui.messages.statusPanel.permissionsLabel'), value: permission }, + { + label: t('tui.messages.statusPanel.planModeLabel'), + value: planMode + ? t('tui.messages.statusPanel.planModeOn') + : t('tui.messages.statusPanel.planModeOff'), + }, + { label: t('tui.messages.statusPanel.sessionLabel'), value: sessionId }, ]; const title = options.sessionTitle?.trim(); - if (title !== undefined && title.length > 0) rows.push({ label: 'Title', value: title }); + if (title !== undefined && title.length > 0) { + rows.push({ label: t('tui.messages.statusPanel.titleLabel'), value: title }); + } if (options.statusError !== undefined) { - rows.push({ label: 'Warning', value: options.statusError, severity: 'error' }); + rows.push({ + label: t('tui.messages.statusPanel.warningLabel'), + value: options.statusError, + severity: 'error', + }); } const lines: string[] = [ - `${accent(`>_ ${PRODUCT_NAME}`)} ${muted(`(v${options.version})`)}`, + `${accent(t('tui.messages.statusPanel.titlePrefix', { productName: PRODUCT_NAME }))} ${muted(t('tui.messages.statusPanel.titleVersion', { version: options.version }))}`, '', ]; addFieldRows(lines, rows, muted, value, errorStyle); const { ratio, tokens, maxTokens } = contextValues(options); lines.push(''); - lines.push(accent('Context window')); + lines.push(accent(t('tui.messages.statusPanel.contextWindow'))); if (maxTokens > 0) { const safeRatio = safeUsageRatio(ratio); const bar = renderProgressBar(safeRatio, 20); const barColoured = currentTheme.fg(severityToken(ratioSeverity(safeRatio)), bar); + const pct = (safeRatio * 100).toFixed(1); lines.push( - ` ${barColoured} ${value(`${(safeRatio * 100).toFixed(1)}%`.padStart(6, ' '))} ` + - muted(`(${formatTokenCount(tokens)} / ${formatTokenCount(maxTokens)})`), + ` ${barColoured} ${t('tui.messages.usagePanel.contextBar', { + pct: value(`${pct}`.padStart(5, ' ')), + tokens: muted(formatTokenCount(tokens)), + maxTokens: muted(formatTokenCount(maxTokens)), + })}`, ); } else { - lines.push(` ${muted('No context window data available.')}`); + lines.push(` ${muted(t('tui.messages.statusPanel.noContextData'))}`); } const managedSection = buildManagedUsageReportLines({ diff --git a/apps/kimi-code/src/tui/components/messages/step-summary.ts b/apps/kimi-code/src/tui/components/messages/step-summary.ts index 325ae6eb24..edd278950f 100644 --- a/apps/kimi-code/src/tui/components/messages/step-summary.ts +++ b/apps/kimi-code/src/tui/components/messages/step-summary.ts @@ -1,5 +1,6 @@ import type { Component } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; /** @@ -24,8 +25,26 @@ export class StepSummaryComponent implements Component { render(_width: number): string[] { const parts: string[] = []; - if (this.thinking > 0) parts.push(`thinking ${this.thinking} times`); - if (this.tool > 0) parts.push(`call ${this.tool} tools`); + if (this.thinking > 0) { + parts.push( + t( + this.thinking === 1 + ? 'tui.messages.stepSummary.thinking_one' + : 'tui.messages.stepSummary.thinking_other', + { count: this.thinking }, + ), + ); + } + if (this.tool > 0) { + parts.push( + t( + this.tool === 1 + ? 'tui.messages.stepSummary.tool_one' + : 'tui.messages.stepSummary.tool_other', + { count: this.tool }, + ), + ); + } if (parts.length === 0) return []; return [currentTheme.dim(`\u2026 ${parts.join(', ')}`)]; } diff --git a/apps/kimi-code/src/tui/components/messages/swarm-markers.ts b/apps/kimi-code/src/tui/components/messages/swarm-markers.ts index 4fee9629fe..ca6c6a49cf 100644 --- a/apps/kimi-code/src/tui/components/messages/swarm-markers.ts +++ b/apps/kimi-code/src/tui/components/messages/swarm-markers.ts @@ -1,5 +1,6 @@ import { truncateToWidth, type Component } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; @@ -24,10 +25,10 @@ export class SwarmModeMarkerComponent implements Component { function swarmMarkerLabel(state: SwarmModeMarkerState): string { switch (state) { case 'active': - return 'Swarm activated'; + return t('tui.messages.swarmMarkers.activated'); case 'inactive': - return 'Swarm deactivated'; + return t('tui.messages.swarmMarkers.deactivated'); case 'ended': - return 'Swarm ended'; + return t('tui.messages.swarmMarkers.ended'); } } diff --git a/apps/kimi-code/src/tui/components/messages/thinking.ts b/apps/kimi-code/src/tui/components/messages/thinking.ts index 23a038c707..c496af64ab 100644 --- a/apps/kimi-code/src/tui/components/messages/thinking.ts +++ b/apps/kimi-code/src/tui/components/messages/thinking.ts @@ -14,6 +14,7 @@ import { THINKING_PREVIEW_LINES, } from '#/tui/constant/rendering'; import { STATUS_BULLET } from '#/tui/constant/symbols'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; import { isRenderCacheEnabled } from '#/tui/utils/render-cache'; @@ -111,7 +112,7 @@ export class ThinkingComponent implements Component { ); rendered = [ '', - spinner + currentTheme.fg('textDim', 'thinking...'), + spinner + currentTheme.fg('textDim', t('tui.messages.thinking.liveLabel')), ...visibleLines.map((line) => MESSAGE_INDENT + line), ]; } else { @@ -127,7 +128,7 @@ export class ThinkingComponent implements Component { // Leading blank + first PREVIEW_LINES content lines + hint line. const truncated = lines.slice(0, 1 + THINKING_PREVIEW_LINES); const remaining = contentLines.length - THINKING_PREVIEW_LINES; - const hint = `... (${String(remaining)} more lines, ctrl+o to expand)`; + const hint = t('tui.messages.thinking.expandHint', { count: remaining }); const indentWidth = Math.min(MESSAGE_INDENT.length, Math.max(0, width)); const hintWidth = Math.max(0, width - indentWidth); truncated.push( diff --git a/apps/kimi-code/src/tui/components/messages/tool-call.ts b/apps/kimi-code/src/tui/components/messages/tool-call.ts index a25c88e643..669b34e741 100644 --- a/apps/kimi-code/src/tui/components/messages/tool-call.ts +++ b/apps/kimi-code/src/tui/components/messages/tool-call.ts @@ -28,6 +28,7 @@ import type { TokenUsage } from '@moonshot-ai/kimi-code-sdk'; import { appendStreamingArgsPreview } from '#/tui/utils/event-payload'; import { decodeMcpToolName } from '#/tui/utils/mcp-tool-name'; import { isRenderCacheEnabled } from '#/tui/utils/render-cache'; +import { t } from '#/i18n'; import { agentSwarmResultSummaryFromOutput } from './agent-swarm-progress'; import { PlanBoxComponent } from './plan-box'; @@ -49,7 +50,7 @@ const MAX_LIVE_OUTPUT_CHARS = 50_000; /** Delay before a long-running foreground Bash/Agent card advertises Ctrl+B. */ const DETACH_HINT_DELAY_MS = 10_000; -const DETACH_HINT_TEXT = 'Press Ctrl+B to run in background'; +const DETACH_HINT_TEXT = t('tui.messages.toolCall.detachHint'); type SubagentTextKind = 'thinking' | 'text'; type SubagentPhase = 'queued' | 'spawning' | 'running' | 'done' | 'failed' | 'backgrounded'; @@ -118,13 +119,13 @@ function backgroundFailureMessage( ): string | undefined { switch (status) { case 'lost': - return 'Background agent lost (session restarted before completion)'; + return t('tui.messages.toolCall.backgroundLost'); case 'killed': - return 'Background agent killed'; + return t('tui.messages.toolCall.backgroundKilled'); case 'timed_out': - return 'Background agent timed out'; + return t('tui.messages.toolCall.backgroundTimedOut'); case 'failed': - return 'Background agent failed'; + return t('tui.messages.toolCall.backgroundFailed'); case 'completed': case undefined: return undefined; @@ -138,7 +139,7 @@ function str(v: unknown): string { function formatSubagentContextTokens(contextTokens: number | undefined): string | undefined { if (contextTokens === undefined || contextTokens <= 0) return undefined; const formatted = contextTokens >= 1000 ? `${(contextTokens / 1000).toFixed(1)}k` : String(contextTokens); - return `${formatted} tok`; + return t('tui.messages.toolCall.tokenCount', { count: formatted }); } function usageInputTotal(usage: TokenUsage): number { @@ -154,20 +155,20 @@ function formatSubagentTokens(usage: TokenUsage | undefined): string | undefined const total = usageTotal(usage); if (total <= 0) return undefined; const formatted = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : String(total); - return `${formatted} tok`; + return t('tui.messages.toolCall.tokenCount', { count: formatted }); } function formatByteSize(bytes: number): string { - if (bytes < 1024) return `${String(bytes)} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / 1024 / 1024).toFixed(1)} MB`; + if (bytes < 1024) return t('tui.messages.toolCall.byteSizeB', { count: bytes }); + if (bytes < 1024 * 1024) return t('tui.messages.toolCall.byteSizeKB', { count: (bytes / 1024).toFixed(1) }); + return t('tui.messages.toolCall.byteSizeMB', { count: (bytes / 1024 / 1024).toFixed(1) }); } function formatElapsed(seconds: number): string { - if (seconds < 60) return `${String(seconds)}s`; + if (seconds < 60) return t('tui.messages.toolCall.elapsedSeconds', { count: seconds }); const minutes = Math.floor(seconds / 60); const remainder = seconds % 60; - return `${String(minutes)}m ${String(remainder)}s`; + return t('tui.messages.toolCall.elapsedMinutes', { minutes, seconds: remainder }); } function extractApprovedPlan(output: string): string { @@ -378,7 +379,7 @@ function makeWorkspaceRelativePath(filePath: string, workspaceDir: string | unde ) { return filePath; } - return relativePath; + return relativePath.replaceAll('\\', '/'); } function formatKeyArgument( @@ -424,7 +425,7 @@ function extractKeyArgument( summary += ` · ${makeWorkspaceRelativePath(path, workspaceDir)}`; } if (args['include_ignored'] === true) { - summary += ' · include ignored'; + summary += ` · ${t('tui.messages.toolCall.includeIgnored')}`; } return truncateArgValue('pattern', summary); } @@ -444,14 +445,14 @@ function extractKeyArgument( function formatSubagentLabel(agentName: string | undefined): string { const raw = agentName?.trim(); - if (raw === undefined || raw.length === 0) return 'SubAgent'; + if (raw === undefined || raw.length === 0) return t('tui.messages.toolCall.subAgentDefault'); const label = raw .split(/[-_\s]+/) .filter((part) => part.length > 0) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); if (/\bagent$/i.test(label)) return label; - return `${label} Agent`; + return `${label} ${t('tui.messages.toolCall.subAgentSuffix')}`; } function tailNonEmptyLines(text: string, maxLines: number): string[] { @@ -758,7 +759,7 @@ export class ToolCallComponent extends Container { if (this.result !== undefined || text.length === 0) return; this.liveOutput += text; if (this.liveOutput.length > MAX_LIVE_OUTPUT_CHARS) { - this.liveOutput = `[...truncated]\n${this.liveOutput.slice( + this.liveOutput = `${t('tui.messages.toolCall.truncatedMarker')}\n${this.liveOutput.slice( this.liveOutput.length - MAX_LIVE_OUTPUT_CHARS, )}`; } @@ -1328,12 +1329,13 @@ export class ToolCallComponent extends Container { delta.argumentsPart, ); const parsed = parseArgsPreview(nextArgsText); + const fallbackName = t('tui.messages.toolCall.toolDefault'); this.ongoingSubCalls.set(delta.id, { - name: delta.name ?? existing?.name ?? 'Tool', + name: delta.name ?? existing?.name ?? fallbackName, args: parsed, streamingArguments: nextArgsText, }); - this.upsertSubToolActivity(delta.id, delta.name ?? existing?.name ?? 'Tool', parsed, 'ongoing'); + this.upsertSubToolActivity(delta.id, delta.name ?? existing?.name ?? fallbackName, parsed, 'ongoing'); if ( this.subagentPhase === undefined || this.subagentPhase === 'queued' || @@ -1352,12 +1354,13 @@ export class ToolCallComponent extends Container { const activity = this.subToolActivities.get(id); const ongoing = this.ongoingSubCalls.get(id); if (activity === undefined && ongoing === undefined) return; - const name = activity?.name ?? ongoing?.name ?? 'Tool'; + const fallbackName = t('tui.messages.toolCall.toolDefault'); + const name = activity?.name ?? ongoing?.name ?? fallbackName; const args = activity?.args ?? ongoing?.args ?? {}; const existingOutput = activity?.output ?? ''; let output = existingOutput + text; if (output.length > MAX_LIVE_OUTPUT_CHARS) { - output = `[...truncated]\n${output.slice(output.length - MAX_LIVE_OUTPUT_CHARS)}`; + output = `${t('tui.messages.toolCall.truncatedMarker')}\n${output.slice(output.length - MAX_LIVE_OUTPUT_CHARS)}`; } this.upsertSubToolActivity(id, name, args, activity?.phase ?? 'ongoing', output); this.rebuildContent(); @@ -1414,7 +1417,7 @@ export class ToolCallComponent extends Container { } if (toolCall.name === 'ExitPlanMode') { - const label = currentTheme.boldFg('primary', 'Current plan'); + const label = currentTheme.boldFg('primary', t('tui.messages.toolCall.currentPlan')); if (!isFinished || result === undefined || result.is_error === true) { return label; } @@ -1422,8 +1425,8 @@ export class ToolCallComponent extends Container { if (outcome.kind === 'approved') { const chipText = outcome.chosen !== undefined && outcome.chosen.length > 0 - ? `Approved: ${outcome.chosen}` - : 'Approved'; + ? t('tui.messages.toolCall.approvedWithOption', { option: outcome.chosen }) + : t('tui.messages.toolCall.approved'); return `${label}${currentTheme.fg('success', ` · ${chipText}`)}`; } return label; @@ -1433,13 +1436,13 @@ export class ToolCallComponent extends Container { const isBackgroundAsk = toolCall.args['background'] === true; const label = isFinished ? isError - ? 'Could not collect your input' + ? t('tui.messages.toolCall.couldNotCollectInput') : isBackgroundAsk - ? 'Started background question' - : 'Collected your answers' + ? t('tui.messages.toolCall.startedBackgroundQuestion') + : t('tui.messages.toolCall.collectedAnswers') : isBackgroundAsk - ? 'Starting background question' - : 'Waiting for your input'; + ? t('tui.messages.toolCall.startingBackgroundQuestion') + : t('tui.messages.toolCall.waitingForInput'); const tone = isError ? 'error' : 'primary'; return `${bullet}${currentTheme.boldFg(tone, label)}`; } @@ -1450,9 +1453,11 @@ export class ToolCallComponent extends Container { // would duplicate the body. Wording mirrors the other label-only headers // (e.g. AskUserQuestion): the whole label takes the tone colour. if (isTruncated) { - return `${bullet}${currentTheme.fg('error', 'Truncated')} ${currentTheme.boldFg('primary', 'Bash')}`; + return `${bullet}${currentTheme.fg('error', t('tui.messages.toolCall.truncated'))} ${currentTheme.boldFg('primary', 'Bash')}`; } - const label = isFinished ? 'Ran a command' : 'Running a command'; + const label = isFinished + ? t('tui.messages.toolCall.ranCommand') + : t('tui.messages.toolCall.runningCommand'); const tone = isError ? 'error' : 'primary'; const chipStr = isFinished && result !== undefined ? this.buildHeaderChip(result) : ''; return `${bullet}${currentTheme.boldFg(tone, label)}${chipStr}`; @@ -1470,7 +1475,11 @@ export class ToolCallComponent extends Container { return this.buildSingleSubagentHeader(); } - const verb = isFinished ? 'Used' : isTruncated ? 'Truncated' : 'Using'; + const verb = isFinished + ? t('tui.messages.toolCall.used') + : isTruncated + ? t('tui.messages.toolCall.truncated') + : t('tui.messages.toolCall.using'); const keyArg = extractKeyArgument(toolCall.name, toolCall.args, this.workspaceDir); const decoded = decodeMcpToolName(toolCall.name); const verbStyled = isTruncated @@ -1587,15 +1596,26 @@ export class ToolCallComponent extends Container { const phaseChip = this.formatPhaseChip(); const headerLabel = this.subagentAgentName !== undefined - ? `subagent ${this.subagentAgentName} (${this.formatAgentId()})` - : `subagent (${this.formatAgentId()})`; + ? t('tui.messages.toolCall.subagentWithName', { + name: this.subagentAgentName, + id: this.formatAgentId(), + }) + : t('tui.messages.toolCall.subagentNoName', { id: this.formatAgentId() }); this.addChild(new Text(` ${currentTheme.dim(`↳ ${headerLabel}`)}${phaseChip}`, 0, 0)); if (this.hiddenSubCallCount > 0) { - const suffix = this.hiddenSubCallCount > 1 ? 's' : ''; this.addChild( new Text( - currentTheme.italic(currentTheme.dim(` ${String(this.hiddenSubCallCount)} more tool call${suffix} ...`)), + currentTheme.italic( + currentTheme.dim( + ` ${t( + this.hiddenSubCallCount === 1 + ? 'tui.messages.toolCall.moreToolCalls_one' + : 'tui.messages.toolCall.moreToolCalls_other', + { count: this.hiddenSubCallCount }, + )} ...`, + ), + ), 0, 0, ), @@ -1609,7 +1629,7 @@ export class ToolCallComponent extends Container { const keyArg = extractKeyArgument(sub.name, sub.args, this.workspaceDir); const nameCol = currentTheme.fg('primary', sub.name); const argCol = keyArg ? currentTheme.dim(` (${keyArg})`) : ''; - this.addChild(new Text(` ${mark} Used ${nameCol}${argCol}`, 0, 0)); + this.addChild(new Text(` ${mark} ${t('tui.messages.toolCall.used')} ${nameCol}${argCol}`, 0, 0)); } for (const [id, call] of this.ongoingSubCalls) { @@ -1617,7 +1637,9 @@ export class ToolCallComponent extends Container { const nameCol = currentTheme.fg('primary', call.name); const argCol = keyArg ? currentTheme.dim(` (${keyArg})`) : ''; void id; - this.addChild(new Text(` ${currentTheme.dim('…')} Using ${nameCol}${argCol}`, 0, 0)); + this.addChild( + new Text(` ${currentTheme.dim('…')} ${t('tui.messages.toolCall.using')} ${nameCol}${argCol}`, 0, 0), + ); } if (this.subagentText.length > 0) { @@ -1658,18 +1680,27 @@ export class ToolCallComponent extends Container { const parts: string[] = []; switch (this.subagentPhase) { case 'queued': - parts.push('○ queued'); + parts.push(`○ ${t('tui.messages.toolCall.phaseQueued')}`); break; case 'spawning': - parts.push('↻ starting…'); + parts.push(`↻ ${t('tui.messages.toolCall.phaseStarting')}`); break; case 'running': - parts.push('↻ running'); + parts.push(`↻ ${t('tui.messages.toolCall.phaseRunning')}`); break; case 'done': { - parts.push(currentTheme.fg('success', '✓ done')); + parts.push(currentTheme.fg('success', `✓ ${t('tui.messages.toolCall.phaseDone')}`)); const toolCount = this.finishedSubCalls.length + this.hiddenSubCallCount; - if (toolCount > 0) parts.push(`${String(toolCount)} tool${toolCount > 1 ? 's' : ''}`); + if (toolCount > 0) { + parts.push( + t( + toolCount === 1 + ? 'tui.messages.toolCall.toolCount_one' + : 'tui.messages.toolCall.toolCount_other', + { count: toolCount }, + ), + ); + } const tokens = formatSubagentContextTokens(this.subagentContextTokens) ?? formatSubagentTokens(this.subagentUsage); @@ -1677,10 +1708,10 @@ export class ToolCallComponent extends Container { break; } case 'failed': - parts.push(currentTheme.fg('error', '✗ failed')); + parts.push(currentTheme.fg('error', `✗ ${t('tui.messages.toolCall.phaseFailed')}`)); break; case 'backgrounded': - parts.push('◐ backgrounded'); + parts.push(`◐ ${t('tui.messages.toolCall.phaseBackgrounded')}`); break; } return parts.length > 0 ? currentTheme.dim(` · ${parts.join(' · ')}`) : ''; @@ -1740,7 +1771,7 @@ export class ToolCallComponent extends Container { const descriptionText = descriptionPlain.length > 0 ? currentTheme.dim(descriptionPlain) : ''; const statsText = this.formatSingleSubagentStatsText(); if (isDone) { - return `${marker}${currentTheme.boldFg('success', labelText)} ${currentTheme.fg('success', `Completed${descriptionPlain}${statsText}`)}`; + return `${marker}${currentTheme.boldFg('success', labelText)} ${currentTheme.fg('success', t('tui.messages.toolCall.singleSubagentCompleted', { description: descriptionPlain, stats: statsText }))}`; } const stats = currentTheme.dim(statsText); return `${marker}${label} ${status}${descriptionText}${stats}`; @@ -1749,24 +1780,30 @@ export class ToolCallComponent extends Container { private formatSingleSubagentStatus(phase: SubagentPhase | undefined): string { switch (phase) { case 'done': - return currentTheme.fg('success', 'Completed'); + return currentTheme.fg('success', t('tui.messages.toolCall.statusCompleted')); case 'failed': - return currentTheme.fg('error', 'Failed'); + return currentTheme.fg('error', t('tui.messages.toolCall.statusFailed')); case 'running': - return currentTheme.fg('primary', 'Running'); + return currentTheme.fg('primary', t('tui.messages.toolCall.statusRunning')); case 'backgrounded': - return 'Backgrounded'; + return t('tui.messages.toolCall.statusBackgrounded'); case 'queued': - return currentTheme.fg('primary', 'Queued'); + return currentTheme.fg('primary', t('tui.messages.toolCall.statusQueued')); case 'spawning': case undefined: - return currentTheme.fg('primary', 'Starting'); + return currentTheme.fg('primary', t('tui.messages.toolCall.statusStarting')); } } private formatSingleSubagentStatsText(): string { + const toolCount = this.subToolActivities.size; const parts = [ - `${String(this.subToolActivities.size)} tool${this.subToolActivities.size === 1 ? '' : 's'}`, + t( + toolCount === 1 + ? 'tui.messages.toolCall.toolCount_one' + : 'tui.messages.toolCall.toolCount_other', + { count: toolCount }, + ), ]; const elapsed = this.getSubagentElapsedSeconds(); if (elapsed !== undefined) parts.push(formatElapsed(elapsed)); @@ -1860,12 +1897,20 @@ export class ToolCallComponent extends Container { private buildSingleSubagentSummaryLine(): string { const toolCount = this.subToolActivities.size; - const countLabel = `${String(toolCount)} tool${toolCount === 1 ? '' : 's'}`; + const countLabel = t( + toolCount === 1 + ? 'tui.messages.toolCall.toolCount_one' + : 'tui.messages.toolCall.toolCount_other', + { count: toolCount }, + ); const current = this.getCurrentSubToolActivity(); if (current === undefined) { return currentTheme.dim(` · ${countLabel}`); } - const verb = current.phase === 'ongoing' ? 'Using' : 'Used'; + const verb = + current.phase === 'ongoing' + ? t('tui.messages.toolCall.using') + : t('tui.messages.toolCall.used'); const keyArg = extractKeyArgument(current.name, current.args, this.workspaceDir); const nameCol = currentTheme.fg('primary', current.name); const argCol = keyArg ? currentTheme.dim(` (${keyArg})`) : ''; @@ -1924,7 +1969,7 @@ export class ToolCallComponent extends Container { if (this.result === undefined && this.toolCall.truncated === true) { this.addChild( new Text( - currentTheme.dim('Tool call arguments truncated by max_tokens — call never executed.'), + currentTheme.dim(t('tui.messages.toolCall.argumentsTruncated')), 2, 0, ), @@ -1964,7 +2009,10 @@ export class ToolCallComponent extends Container { this.addChild( new Text( currentTheme.dim( - `... (${String(remaining)} more lines, ${String(allLines.length)} total, ctrl+o to expand)`, + t('tui.messages.toolCall.moreLinesHint', { + remaining, + total: allLines.length, + }), ), 2, 0, @@ -2050,10 +2098,15 @@ export class ToolCallComponent extends Container { const startedAtMs = this.toolCall.streamingStartedAtMs; const elapsedSeconds = startedAtMs === undefined ? 0 : Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000)); - const target = filePath.length > 0 ? ` for ${filePath}` : ''; - const progress = `Preparing changes${target}... ${formatByteSize(bytes)} · ${formatElapsed( - elapsedSeconds, - )} elapsed`; + const target = + filePath.length > 0 + ? t('tui.messages.toolCall.preparingChangesTarget', { filePath }) + : ''; + const progress = t('tui.messages.toolCall.preparingChanges', { + target, + size: formatByteSize(bytes), + elapsed: formatElapsed(elapsedSeconds), + }); this.addChild(new Text(currentTheme.dim(progress), 2, 0)); return; } @@ -2112,7 +2165,7 @@ export class ToolCallComponent extends Container { if (!isExitPlanModeOutcomeOutput(result.output)) return undefined; const outcome = interpretExitPlanModeOutcome(result.output); if (outcome.kind !== 'rejected') return undefined; - return { label: 'Rejected', colorHex: currentTheme.color('error') }; + return { label: t('tui.messages.toolCall.rejected'), colorHex: currentTheme.color('error') }; } private buildContent(): void { @@ -2124,6 +2177,11 @@ export class ToolCallComponent extends Container { return; } + if (this.toolCall.name === 'SwarmDiscussion') { + this.buildDiscussionResultSummary(result); + return; + } + if (!result.output) return; if (this.isSingleSubagentView()) { @@ -2150,7 +2208,7 @@ export class ToolCallComponent extends Container { const trimmed = outcome.feedback.trim(); if (trimmed.length > 0) { const labelTone = (text: string) => currentTheme.boldFg('warning', text); - this.addChild(new Text(labelTone('↪ Suggestion'), 2, 0)); + this.addChild(new Text(labelTone(t('tui.messages.toolCall.suggestionLabel')), 2, 0)); for (const line of trimmed.split('\n')) { this.addChild(new Text(line, 4, 0)); } @@ -2188,6 +2246,43 @@ export class ToolCallComponent extends Container { } } + private buildDiscussionResultSummary(result: ToolResultBlockData): void { + const dim = (s: string): string => currentTheme.fg('textDim', s); + const accent = (s: string): string => currentTheme.fg('primary', s); + + // Parse <discussion_result> XML + const summaryMatch = result.output.match(/<summary>([^<]+)<\/summary>/); + const transcriptMatch = result.output.match(/<transcript>([\s\S]*?)<\/transcript>/); + const summaryTextMatch = result.output.match(/<final_summary>([\s\S]*?)<\/final_summary>/); + + const speechCount = result.is_error === true ? 0 : (transcriptMatch?.[1]?.split('\n\n').filter(l => l.trim().startsWith('[')).length ?? 0); + + const segments: string[] = []; + if (speechCount > 0) { + segments.push(`${String(speechCount)} speeches`); + } + + if (result.is_error === true) { + segments.push(`${FAILURE_MARK.trimEnd()} ${t('tui.messages.toolCall.failedPeriod')}`); + } else { + segments.push(`${SUCCESS_MARK.trimEnd()} ${t('tui.messages.toolCall.completedPeriod')}`); + } + + this.addChild(new Text( + `${dim(t('tui.messages.toolCall.discussionLabel'))}${segments.join(dim(' · '))}`, + 2, 0, + )); + + const summaryText = summaryTextMatch?.[1]; + if (summaryText != null && summaryText.trim().length > 0) { + this.addChild(new Text('', 2, 0)); + this.addChild(new Text(accent(t('tui.messages.toolCall.discussionSummary')), 2, 0)); + for (const line of summaryText.trim().split('\n')) { + this.addChild(new Text(line, 4, 0)); + } + } + } + private buildAgentSwarmResultSummary(result: ToolResultBlockData): void { const summary = agentSwarmResultSummaryFromOutput(result.output); const dim = (s: string): string => currentTheme.fg('textDim', s); @@ -2195,33 +2290,42 @@ export class ToolCallComponent extends Container { if (summary.completed > 0) { segments.push( - currentTheme.fg('success', `${SUCCESS_MARK.trimEnd()} ${String(summary.completed)} completed`), + currentTheme.fg( + 'success', + `${SUCCESS_MARK.trimEnd()} ${t('tui.messages.toolCall.completedStatus', { count: summary.completed })}`, + ), ); } if (summary.failed > 0) { segments.push( - currentTheme.fg('error', `${FAILURE_MARK.trimEnd()} ${String(summary.failed)} failed`), + currentTheme.fg( + 'error', + `${FAILURE_MARK.trimEnd()} ${t('tui.messages.toolCall.failedStatus', { count: summary.failed })}`, + ), ); } if (summary.aborted > 0) { segments.push( - currentTheme.fg('warning', `${ABORTED_MARK} ${String(summary.aborted)} aborted`), + currentTheme.fg( + 'warning', + `${ABORTED_MARK} ${t('tui.messages.toolCall.abortedStatus', { count: summary.aborted })}`, + ), ); } if (segments.length > 0) { - this.addChild(new Text(`${dim('Agent swarm: ')}${segments.join(dim(' · '))}`, 2, 0)); + this.addChild(new Text(`${dim(t('tui.messages.toolCall.agentSwarmLabel'))}${segments.join(dim(' · '))}`, 2, 0)); return; } const isAborted = result.is_error === true && /\b(?:aborted|cancelled)\b/i.test(result.output); const colorToken = isAborted ? 'warning' : result.is_error === true ? 'error' : 'success'; const label = isAborted - ? `${ABORTED_MARK} Aborted.` + ? `${ABORTED_MARK} ${t('tui.messages.toolCall.abortedPeriod')}` : result.is_error === true - ? `${FAILURE_MARK.trimEnd()} Failed.` - : `${SUCCESS_MARK.trimEnd()} Completed.`; - this.addChild(new Text(`${dim('Agent swarm: ')}${currentTheme.fg(colorToken, label)}`, 2, 0)); + ? `${FAILURE_MARK.trimEnd()} ${t('tui.messages.toolCall.failedPeriod')}` + : `${SUCCESS_MARK.trimEnd()} ${t('tui.messages.toolCall.completedPeriod')}`; + this.addChild(new Text(`${dim(t('tui.messages.toolCall.agentSwarmLabel'))}${currentTheme.fg(colorToken, label)}`, 2, 0)); } /** @@ -2248,7 +2352,7 @@ export class ToolCallComponent extends Container { if (!hasAnswers) { const noteText = - typeof note === 'string' && note.length > 0 ? note : 'User dismissed the question.'; + typeof note === 'string' && note.length > 0 ? note : t('tui.messages.toolCall.userDismissedQuestion'); this.addChild(new Text(currentTheme.dim(` ${noteText}`), 0, 0)); return true; } @@ -2277,13 +2381,18 @@ function computeLatestActivity( if (ongoing.size > 0) { const lastOngoing = [...ongoing.values()].at(-1); if (lastOngoing !== undefined) { - return formatActivityLine('Using', lastOngoing.name, lastOngoing.args, workspaceDir); + return formatActivityLine( + translateActivityVerb('Using'), + lastOngoing.name, + lastOngoing.args, + workspaceDir, + ); } } if (finished.length > 0) { const last = finished.at(-1); if (last !== undefined) { - return formatActivityLine('Used', last.name, last.args, workspaceDir); + return formatActivityLine(translateActivityVerb('Used'), last.name, last.args, workspaceDir); } } if (text.length > 0) { @@ -2297,9 +2406,9 @@ function computeLatestActivity( } function formatTokens(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M tok`; - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k tok`; - return `${String(n)} tok`; + if (n >= 1_000_000) return t('tui.messages.toolCall.tokenCount', { count: (n / 1_000_000).toFixed(1) }); + if (n >= 1_000) return t('tui.messages.toolCall.tokenCount', { count: (n / 1_000).toFixed(1) }); + return t('tui.messages.toolCall.tokenCount', { count: n }); } function formatActivityLine( @@ -2311,3 +2420,9 @@ function formatActivityLine( const keyArg = extractKeyArgument(toolName, args, workspaceDir); return keyArg ? `${verb} ${toolName} (${keyArg})` : `${verb} ${toolName}`; } + +function translateActivityVerb(verb: 'Using' | 'Used'): string { + return verb === 'Using' + ? t('tui.messages.toolCall.using') + : t('tui.messages.toolCall.used'); +} diff --git a/apps/kimi-code/src/tui/components/messages/tool-renderers/goal.ts b/apps/kimi-code/src/tui/components/messages/tool-renderers/goal.ts index 1b38fd2782..af403a7fab 100644 --- a/apps/kimi-code/src/tui/components/messages/tool-renderers/goal.ts +++ b/apps/kimi-code/src/tui/components/messages/tool-renderers/goal.ts @@ -1,5 +1,6 @@ import { Text } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { STATUS_BULLET } from '#/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; @@ -100,10 +101,10 @@ function renderGoalSnapshot( const muted = (s: string) => currentTheme.dimFg('textDim', s); const value = (s: string) => currentTheme.fg('text', s); - if (goal === null) return [new Text(muted(' No current goal.'), 0, 0)]; + if (goal === null) return [new Text(muted(t('tui.messages.goalToolNoGoal')), 0, 0)]; const lines = [ - ` ${value(`Goal ${goal.status}: ${truncateOneLine(goal.objective, 96)}`)}`, + ` ${value(t('tui.messages.goalToolStatus', { status: goal.status, objective: truncateOneLine(goal.objective, 96) }))}`, ` ${muted(formatGoalStats(goal))}`, ]; if (goal.terminalReason !== undefined && goal.terminalReason.length > 0) { @@ -121,23 +122,23 @@ function goalToolLabel( const finished = result !== undefined; switch (toolName) { case 'CreateGoal': - return failed ? 'Could not start goal' : finished ? 'Started goal' : 'Starting goal'; + return failed ? t('tui.messages.goalToolCouldNotStart') : finished ? t('tui.messages.goalToolStarted') : t('tui.messages.goalToolStarting'); case 'GetGoal': - return failed ? 'Could not check goal' : finished ? 'Checked goal' : 'Checking goal'; + return failed ? t('tui.messages.goalToolCouldNotCheck') : finished ? t('tui.messages.goalToolChecked') : t('tui.messages.goalToolChecking'); case 'SetGoalBudget': return failed - ? 'Could not set goal budget' + ? t('tui.messages.goalToolCouldNotSetBudget') : finished - ? 'Set goal budget' - : 'Setting goal budget'; + ? t('tui.messages.goalToolSetBudget') + : t('tui.messages.goalToolSettingBudget'); case 'UpdateGoal': { const status = stringArg(args, 'status'); const suffix = status ?? 'status'; return failed - ? `Could not report goal ${suffix}` + ? t('tui.messages.goalToolCouldNotReport', { suffix }) : finished - ? `Reported goal ${suffix}` - : `Reporting goal ${suffix}`; + ? t('tui.messages.goalToolReported', { suffix }) + : t('tui.messages.goalToolReporting', { suffix }); } } } diff --git a/apps/kimi-code/src/tui/components/messages/usage-panel.ts b/apps/kimi-code/src/tui/components/messages/usage-panel.ts index 195860bc39..6a7eb7a5e0 100644 --- a/apps/kimi-code/src/tui/components/messages/usage-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/usage-panel.ts @@ -14,6 +14,7 @@ import { renderProgressBar, safeUsageRatio, } from '#/utils/usage/usage-format'; +import { t } from '#/i18n'; import { currentTheme } from '#/tui/theme'; import type { ColorToken } from '#/tui/theme'; @@ -83,7 +84,7 @@ function buildSessionUsageSection( const byModel = (usage as { readonly byModel?: Record<string, TokenUsage> } | undefined) ?.byModel; const entries = Object.entries(byModel ?? {}); - if (entries.length === 0) return [muted(' No token usage recorded yet.')]; + if (entries.length === 0) return [muted(` ${t('tui.messages.usagePanel.noTokenUsage')}`)]; const lines: string[] = []; let totalInput = 0; @@ -94,16 +95,22 @@ function buildSessionUsageSection( totalInput += input; totalOutput += output; lines.push( - ` ${muted(model)} input ${value(formatTokenCount(input))} output ${value( - formatTokenCount(output), - )} total ${value(formatTokenCount(input + output))}`, + ` ${t('tui.messages.usagePanel.byModel', { + model: muted(model), + input: value(formatTokenCount(input)), + output: value(formatTokenCount(output)), + total: value(formatTokenCount(input + output)), + })}`, ); } if (entries.length > 1) { lines.push( - ` ${muted('total')} input ${value(formatTokenCount(totalInput))} output ${value( - formatTokenCount(totalOutput), - )} total ${value(formatTokenCount(totalInput + totalOutput))}`, + ` ${t('tui.messages.usagePanel.totalLine', { + label: muted(t('tui.messages.usagePanel.total')), + input: value(formatTokenCount(totalInput)), + output: value(formatTokenCount(totalOutput)), + total: value(formatTokenCount(totalInput + totalOutput)), + })}`, ); } return lines; @@ -117,11 +124,11 @@ function buildManagedUsageSection( muted: Colorize, errorStyle: Colorize, ): string[] { - if (error !== undefined) return [accent('Plan usage'), errorStyle(` ${error}`)]; + if (error !== undefined) return [accent(t('tui.messages.usagePanel.planUsage')), errorStyle(` ${error}`)]; if (usage === undefined) return []; const { summary, limits } = usage; if (summary === null && limits.length === 0) { - return [accent('Plan usage'), muted(' No usage data available.')]; + return [accent(t('tui.messages.usagePanel.planUsage')), muted(` ${t('tui.messages.usagePanel.noUsageData')}`)]; } const rows: ManagedUsageRow[] = []; @@ -130,17 +137,35 @@ function buildManagedUsageSection( const usedRatio = (r: ManagedUsageRow): number => r.limit > 0 ? Math.max(0, Math.min(r.used / r.limit, 1)) : 0; const labelWidth = Math.max(10, ...rows.map((r) => r.label.length)); - const pctWidth = Math.max(...rows.map((r) => `${Math.round(usedRatio(r) * 100)}% used`.length)); + const pctWidth = Math.max( + ...rows.map((r) => { + const pctStr = t('tui.messages.usagePanel.usageRow', { + label: '', + bar: '', + pct: Math.round(usedRatio(r) * 100), + reset: '', + }).trimStart(); + return pctStr.length; + }), + ); - const out: string[] = [accent('Plan usage')]; + const out: string[] = [accent(t('tui.messages.usagePanel.planUsage'))]; for (const row of rows) { const ratioUsed = usedRatio(row); const bar = renderProgressBar(ratioUsed, 20); - const pct = `${Math.round(ratioUsed * 100)}% used`; + const pct = Math.round(ratioUsed * 100); const barColoured = currentTheme.fg(severityColor(ratioSeverity(ratioUsed)), bar); const label = row.label.padEnd(labelWidth, ' '); + const pctStr = t('tui.messages.usagePanel.usageRow', { + label: '', + bar: '', + pct, + reset: '', + }).trimStart(); const resetStr = row.resetHint ? ` ${muted(row.resetHint)}` : ''; - out.push(` ${muted(label)} ${barColoured} ${value(pct.padEnd(pctWidth, ' '))}${resetStr}`); + out.push( + ` ${muted(label)} ${barColoured} ${value(pctStr.padEnd(pctWidth, ' '))}${resetStr}`, + ); } return out; } @@ -198,13 +223,17 @@ export function buildExtraUsageSection( const bar = renderProgressBar(ratio, 20); barLine = ` ${currentTheme.fg(severityColor(ratioSeverity(ratio)), bar)}`; const limit = formatCurrencyParts(extraUsage.monthlyChargeLimitCents, extraUsage.currency); - rows.push({ label: 'Used this month', ...used }); - rows.push({ label: 'Monthly limit', ...limit }); - rows.push({ label: 'Balance', ...balance }); + rows.push({ label: t('tui.messages.usagePanel.usedThisMonth'), ...used }); + rows.push({ label: t('tui.messages.usagePanel.monthlyLimit'), ...limit }); + rows.push({ label: t('tui.messages.usagePanel.balance'), ...balance }); } else { - rows.push({ label: 'Used this month', ...used }); - rows.push({ label: 'Monthly limit', symbol: '', number: 'Unlimited' }); - rows.push({ label: 'Balance', ...balance }); + rows.push({ label: t('tui.messages.usagePanel.usedThisMonth'), ...used }); + rows.push({ + label: t('tui.messages.usagePanel.monthlyLimit'), + symbol: '', + number: t('tui.messages.usagePanel.unlimited'), + }); + rows.push({ label: t('tui.messages.usagePanel.balance'), ...balance }); } // `Used this month` is the longest label; size the column to the widest label @@ -223,7 +252,7 @@ export function buildExtraUsageSection( return ` ${muted(label.padEnd(labelWidth, ' '))} ${value(cell)}`; }; - const lines: string[] = [accent('Extra Usage')]; + const lines: string[] = [accent(t('tui.messages.usagePanel.extraUsage'))]; if (barLine !== null) lines.push(barLine); for (const r of rows) lines.push(row(r.label, r.symbol, r.number)); @@ -253,7 +282,7 @@ export function buildUsageReportLines(options: UsageReportOptions): string[] { const errorStyle = (text: string) => currentTheme.fg('error', text); const lines: string[] = [ - accent('Session usage'), + accent(t('tui.messages.usagePanel.sessionUsage')), ...buildSessionUsageSection( options.sessionUsage, options.sessionUsageError, @@ -266,17 +295,16 @@ export function buildUsageReportLines(options: UsageReportOptions): string[] { if (options.maxContextTokens > 0) { const ratio = safeUsageRatio(options.contextUsage); const bar = renderProgressBar(ratio, 20); - const pct = `${(ratio * 100).toFixed(1)}%`; + const pct = (ratio * 100).toFixed(1); const barColoured = currentTheme.fg(severityColor(ratioSeverity(ratio)), bar); lines.push(''); - lines.push(accent('Context window')); + lines.push(accent(t('tui.messages.usagePanel.contextWindow'))); lines.push( - ` ${barColoured} ${value(pct.padStart(6, ' '))} ` + - muted( - `(${formatTokenCount(options.contextTokens)} / ${formatTokenCount( - options.maxContextTokens, - )})`, - ), + ` ${barColoured} ${t('tui.messages.usagePanel.contextBar', { + pct: value(`${pct}`.padStart(5, ' ')), + tokens: muted(formatTokenCount(options.contextTokens)), + maxTokens: muted(formatTokenCount(options.maxContextTokens)), + })}`, ); } @@ -310,7 +338,7 @@ export class UsagePanelComponent implements Component { constructor( private readonly buildLines: () => readonly string[], private readonly borderToken: ColorToken, - private readonly title: string = ' Usage ', + private readonly title: string = t('tui.messages.usagePanel.title'), ) { this.lines = buildLines(); } diff --git a/apps/kimi-code/src/tui/components/panes/btw-panel.ts b/apps/kimi-code/src/tui/components/panes/btw-panel.ts index f32aa9321d..b270eaf0e2 100644 --- a/apps/kimi-code/src/tui/components/panes/btw-panel.ts +++ b/apps/kimi-code/src/tui/components/panes/btw-panel.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import type { Component, MarkdownTheme } from '@moonshot-ai/pi-tui'; import { Markdown, @@ -120,10 +121,10 @@ export class BtwPanelComponent implements Component { private renderTopBorder(width: number, truncated: boolean): string { const paint = (s: string): string => chalk.hex(currentTheme.palette.border)(s); const hint = truncated && this.options.canUseScrollKeys() - ? 'Esc close · ↑↓ scroll ' - : 'Esc close '; + ? t('tui.dialogs.btwPanel.scrollHint') + : t('tui.dialogs.btwPanel.closeHint'); const title = - chalk.hex(currentTheme.palette.accent).bold(' BTW ') + + chalk.hex(currentTheme.palette.accent).bold(t('tui.dialogs.btwPanel.title')) + paint('─ ') + chalk.hex(currentTheme.palette.textMuted)(hint); const innerWidth = Math.max(1, width - 2); @@ -140,7 +141,7 @@ export class BtwPanelComponent implements Component { lines.push(...this.renderTurn(turn, width)); } if (this.turns.length === 0) { - lines.push(chalk.hex(currentTheme.palette.textDim)('Ready for a side question...')); + lines.push(chalk.hex(currentTheme.palette.textDim)(t('tui.dialogs.btwPanel.readyForSideQuestion'))); } lines.push(...this.renderTransientNotices(width)); return this.fitBodyLines(lines); @@ -190,7 +191,9 @@ export class BtwPanelComponent implements Component { } private renderTurn(turn: BtwTurn, width: number): string[] { - const prompt = chalk.hex(currentTheme.palette.accent)(`Q: ${turn.prompt}`); + const prompt = chalk.hex(currentTheme.palette.accent)( + `${t('tui.dialogs.btwPanel.questionPrefix')}${turn.prompt}`, + ); const lines = [...new Text(prompt, 0, 0).render(width)]; const answer = turn.answer.trim(); const thinking = turn.thinking.trim(); @@ -206,7 +209,7 @@ export class BtwPanelComponent implements Component { : thinkingLines; lines.push(...visibleThinking); } else if (turn.error === undefined) { - lines.push(chalk.hex(currentTheme.palette.textDim)('Waiting for answer...')); + lines.push(chalk.hex(currentTheme.palette.textDim)(t('tui.dialogs.btwPanel.waitingForAnswer'))); } if (turn.error !== undefined) { const error = chalk.hex(currentTheme.palette.error)(turn.error); diff --git a/apps/kimi-code/src/tui/components/panes/queue-pane.ts b/apps/kimi-code/src/tui/components/panes/queue-pane.ts index 1a2b26d078..c491e66cf4 100644 --- a/apps/kimi-code/src/tui/components/panes/queue-pane.ts +++ b/apps/kimi-code/src/tui/components/panes/queue-pane.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import { Container, truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui'; import { SELECT_POINTER } from '../../constant/symbols'; @@ -28,10 +29,10 @@ export class QueuePaneComponent extends Container { const canSteer = options.canSteerImmediately && hasSteerable; this.hint = options.isCompacting && !options.isStreaming - ? ' ↑ to edit · will send after compaction' + ? t('tui.dialogs.queuePane.hintCompacting') : canSteer - ? ' ↑ to edit · ctrl-s to steer immediately' - : ' ↑ to edit · will send after current task'; + ? t('tui.dialogs.queuePane.hintSteer') + : t('tui.dialogs.queuePane.hintAfterTask'); } } diff --git a/apps/kimi-code/src/tui/config.ts b/apps/kimi-code/src/tui/config.ts index cd3329b552..364887b8ff 100644 --- a/apps/kimi-code/src/tui/config.ts +++ b/apps/kimi-code/src/tui/config.ts @@ -14,6 +14,8 @@ import { z } from 'zod'; import { getDataDir } from '#/utils/paths'; +import type { Locale } from '#/i18n'; + export const INVALID_TUI_CONFIG_MESSAGE = 'Invalid TUI config in ~/.kimi-code/tui.toml; using defaults.'; @@ -33,6 +35,7 @@ export const UpgradePreferencesSchema = z.object({ export const TuiConfigFileSchema = z.object({ theme: TuiThemeSchema.optional(), disable_paste_burst: z.boolean().optional(), + locale: z.enum(['en', 'zh']).optional(), editor: z .object({ command: z.string().optional(), @@ -54,6 +57,7 @@ export const TuiConfigFileSchema = z.object({ export const TuiConfigSchema = z.object({ theme: TuiThemeSchema, disablePasteBurst: z.boolean(), + locale: z.enum(['en', 'zh']), editorCommand: z.string().nullable(), notifications: NotificationsConfigSchema, upgrade: UpgradePreferencesSchema, @@ -76,6 +80,7 @@ export const DEFAULT_UPGRADE_PREFERENCES: UpgradePreferences = { export const DEFAULT_TUI_CONFIG: TuiConfig = TuiConfigSchema.parse({ theme: 'auto', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: DEFAULT_NOTIFICATIONS_CONFIG, upgrade: DEFAULT_UPGRADE_PREFERENCES, @@ -136,6 +141,7 @@ export function normalizeTuiConfig(config: TuiConfigFileShape): TuiConfig { return TuiConfigSchema.parse({ theme: config.theme ?? DEFAULT_TUI_CONFIG.theme, disablePasteBurst: config.disable_paste_burst ?? DEFAULT_TUI_CONFIG.disablePasteBurst, + locale: config.locale ?? DEFAULT_TUI_CONFIG.locale, editorCommand: command === undefined || command.length === 0 ? null : command, notifications: { enabled: config.notifications?.enabled ?? DEFAULT_NOTIFICATIONS_CONFIG.enabled, @@ -155,6 +161,7 @@ export function renderTuiConfig(config: TuiConfig): string { theme = "${escapeTomlBasicString(config.theme)}" # "auto" | "dark" | "light" | custom theme name disable_paste_burst = ${String(config.disablePasteBurst)} # true disables non-bracketed paste-burst fallback +locale = "${config.locale}" # "en" | "zh" [editor] command = "${escapeTomlBasicString(config.editorCommand ?? '')}" # Empty uses $VISUAL / $EDITOR diff --git a/apps/kimi-code/src/tui/constant/feedback.ts b/apps/kimi-code/src/tui/constant/feedback.ts index d72a62def0..d88d5ecd91 100644 --- a/apps/kimi-code/src/tui/constant/feedback.ts +++ b/apps/kimi-code/src/tui/constant/feedback.ts @@ -7,6 +7,8 @@ * visual contract. */ +import { t } from '#/i18n'; + import { FEEDBACK_VERSION_PREFIX } from '#/constant/app'; export { @@ -15,33 +17,31 @@ export { FEEDBACK_VERSION_PREFIX, } from '#/constant/app'; -export const FEEDBACK_STATUS_SUBMITTING = 'Submitting feedback…'; -export const FEEDBACK_STATUS_UPLOADING = 'Uploading attachments, this could take a few minutes…'; -export const FEEDBACK_STATUS_SUCCESS = 'Feedback submitted, thank you!'; -export const FEEDBACK_STATUS_CANCELLED = 'Feedback cancelled.'; -export const FEEDBACK_STATUS_NETWORK_ERROR = 'Network error, failed to submit feedback.'; -export const FEEDBACK_STATUS_FALLBACK = 'Opening GitHub Issues as fallback…'; -export const FEEDBACK_STATUS_NOT_SIGNED_IN = - "You're not signed in. Opening GitHub Issues for feedback…"; -export const FEEDBACK_STATUS_UPLOAD_FAILED = - 'Feedback sent; attachment upload failed — see feedback-upload.log.'; +export const FEEDBACK_STATUS_SUBMITTING = t('tui.messages.feedbackSubmitting'); +export const FEEDBACK_STATUS_UPLOADING = t('tui.messages.feedbackUploading'); +export const FEEDBACK_STATUS_SUCCESS = t('tui.messages.feedbackSubmitted'); +export const FEEDBACK_STATUS_CANCELLED = t('tui.messages.feedbackCancelled'); +export const FEEDBACK_STATUS_NETWORK_ERROR = t('tui.messages.feedbackNetworkError'); +export const FEEDBACK_STATUS_FALLBACK = t('tui.messages.feedbackOpeningGithub'); +export const FEEDBACK_STATUS_NOT_SIGNED_IN = t('tui.messages.feedbackNotSignedIn'); +export const FEEDBACK_STATUS_UPLOAD_FAILED = t('tui.messages.feedbackSentUploadFailed'); export function feedbackHttpErrorMessage(status: number): string { - return `Failed to submit feedback (HTTP ${String(status)}).`; + return t('tui.messages.feedbackHttpFailed', { status: String(status) }); } export function feedbackSessionLine(sessionId: string): string { - return `Session: ${sessionId}`; + return t('tui.messages.feedbackSession', { sessionId }); } export function feedbackIdLine(feedbackId: number): string { - return `Feedback ID: ${String(feedbackId)}`; + return t('tui.messages.feedbackId', { id: String(feedbackId) }); } // Hint shown beneath session-level error messages in the TUI to point users // at the `/export-debug-zip` workflow so they can share diagnostics with us. export function errorReportHintLine(): string { - return "If this persists, run `/export-debug-zip` and share the file with us for diagnosis. Please don't share it publicly."; + return t('tui.messages.feedbackPersistHint'); } export function withFeedbackVersionPrefix(version: string): string { diff --git a/apps/kimi-code/src/tui/constant/kimi-tui.ts b/apps/kimi-code/src/tui/constant/kimi-tui.ts index 8c8f9807b4..8a42a380cd 100644 --- a/apps/kimi-code/src/tui/constant/kimi-tui.ts +++ b/apps/kimi-code/src/tui/constant/kimi-tui.ts @@ -1,13 +1,24 @@ import { DEFAULT_OAUTH_PROVIDER_NAME } from '#/constant/app'; +import { t } from '#/i18n'; export { DEFAULT_OAUTH_PROVIDER_NAME, OAUTH_LOGIN_REQUIRED_CODE, PRODUCT_NAME } from '#/constant/app'; -export const LLM_NOT_SET_MESSAGE = 'LLM not set, send "/login" to login'; -export const NO_ACTIVE_SESSION_MESSAGE = 'No active session. Send /login to login.'; -export const CTRL_D_HINT = 'Press Ctrl+D again to exit'; -export const CTRL_C_HINT = 'Press Ctrl+C again to exit'; +export function getLlmNotSetMessage(): string { + return t('tui.chrome.hints.llmNotSet'); +} +export function getNoActiveSessionMessage(): string { + return t('tui.chrome.hints.noActiveSession'); +} +export function getCtrlDHint(): string { + return t('tui.chrome.hints.ctrlDExit'); +} +export function getCtrlCHint(): string { + return t('tui.chrome.hints.ctrlCExit'); +} export const MAIN_AGENT_ID = 'main'; -export const OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE = 'OAuth login expired. Send /login to login.'; +export function getOauthLoginRequiredStartupNotice(): string { + return t('tui.chrome.hints.oauthLoginExpired'); +} export const EXIT_CONFIRM_WINDOW_MS = 1500; // Time window for treating two consecutive Esc presses as a double-Esc, which // opens the undo selector. Kept short (double-click feel) so two deliberate diff --git a/apps/kimi-code/src/tui/controllers/auth-flow.ts b/apps/kimi-code/src/tui/controllers/auth-flow.ts index ae70c0cb8f..0bef6ebd95 100644 --- a/apps/kimi-code/src/tui/controllers/auth-flow.ts +++ b/apps/kimi-code/src/tui/controllers/auth-flow.ts @@ -1,7 +1,7 @@ import type { CreateSessionOptions, KimiHarness, Session } from '@moonshot-ai/kimi-code-sdk'; import type { SkillListSession } from '../commands'; -import { OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE } from '../constant/kimi-tui'; +import { getOauthLoginRequiredStartupNotice } from '../constant/kimi-tui'; import { refreshAllProviderModels, type RefreshProviderScope, @@ -58,7 +58,7 @@ export class AuthFlowController { contextUsage: 0, sessionTitle: null, }); - this.host.appendStartupNotice(OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE); + this.host.appendStartupNotice(getOauthLoginRequiredStartupNotice()); this.host.setStartupReady(); } diff --git a/apps/kimi-code/src/tui/controllers/btw-panel.ts b/apps/kimi-code/src/tui/controllers/btw-panel.ts index c29c764605..177342891a 100644 --- a/apps/kimi-code/src/tui/controllers/btw-panel.ts +++ b/apps/kimi-code/src/tui/controllers/btw-panel.ts @@ -1,3 +1,4 @@ +import { t } from '#/i18n'; import { Spacer } from '@moonshot-ai/pi-tui'; import type { Event, @@ -6,7 +7,7 @@ import type { TurnEndedEvent, } from '@moonshot-ai/kimi-code-sdk'; -import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { getNoActiveSessionMessage } from '../constant/kimi-tui'; import { BtwPanelComponent } from '../components/panes/btw-panel'; import { formatErrorMessage } from '../utils/event-payload'; import { formatHookResultPlain } from '../utils/hook-result-format'; @@ -168,12 +169,12 @@ export class BtwPanelController { private promptAgent(agentId: string, prompt: string, panel: BtwPanelComponent): void { const session = this.host.session; if (session === undefined) { - panel.markFailed(NO_ACTIVE_SESSION_MESSAGE); + panel.markFailed(getNoActiveSessionMessage()); this.host.state.ui.requestRender(); return; } void this.withInteractiveAgent(agentId, () => session.prompt(prompt)).catch((error: unknown) => { - panel.markFailed(`Failed to send /btw prompt: ${formatErrorMessage(error)}`); + panel.markFailed(t('tui.messages.btwSendFailed', { error: formatErrorMessage(error) })); this.host.state.ui.requestRender(); }); } @@ -182,7 +183,7 @@ export class BtwPanelController { const session = this.host.session; if (session === undefined) return; await this.withInteractiveAgent(agentId, () => session.cancel()).catch((error: unknown) => { - this.host.showError(`Failed to cancel /btw: ${formatErrorMessage(error)}`); + this.host.showError(t('tui.messages.btwCancelFailed', { error: formatErrorMessage(error) })); }); } diff --git a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts index 48dd1f8b83..812288e9e0 100644 --- a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts +++ b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts @@ -1,5 +1,6 @@ import type { TUI } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { clipboardHasImage } from '#/utils/clipboard/clipboard-has-image'; import { FOCUS_DEBOUNCE_MS, HINT_DISPLAY_MS } from '../constant/clipboard-image-hint'; @@ -153,7 +154,7 @@ export class ClipboardImageHintController { // Same image we already notified about — stay quiet until it changes. if (!this.armed) return; - const hintText = `Image in clipboard · ${getPasteImageShortcut()} to paste`; + const hintText = t('tui.messages.clipboardImageHint', { shortcut: getPasteImageShortcut() }); this.clearClearHintTimer(); this.lastHintText = hintText; this.armed = false; diff --git a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts index 383694c073..6113891f10 100644 --- a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts +++ b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts @@ -1,17 +1,18 @@ import type { KimiHarness, Session } from '@moonshot-ai/kimi-code-sdk'; import { compressImageForModel, persistOriginalImage, sessionMediaOriginalsDir } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { ClipboardMediaError, readClipboardMedia } from '#/utils/clipboard/clipboard-image'; import { parseImageMeta } from '#/utils/image/image-mime'; import { editInExternalEditor, resolveEditorCommand } from '#/utils/process/external-editor'; import { - CTRL_C_HINT, - CTRL_D_HINT, DOUBLE_ESC_WINDOW_MS, EXIT_CONFIRM_WINDOW_MS, - LLM_NOT_SET_MESSAGE, - NO_ACTIVE_SESSION_MESSAGE, + getCtrlCHint, + getCtrlDHint, + getLlmNotSetMessage, + getNoActiveSessionMessage, } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { ImageAttachmentStore } from '../utils/image-attachment-store'; @@ -159,7 +160,7 @@ export class EditorKeyboardController { if (editor.getText().length > 0) { editor.setText(''); } - this.armPendingExit('ctrl-c', CTRL_C_HINT); + this.armPendingExit('ctrl-c', getCtrlCHint()); }; editor.onCtrlD = () => { @@ -168,7 +169,7 @@ export class EditorKeyboardController { void host.stop(); return; } - this.armPendingExit('ctrl-d', CTRL_D_HINT); + this.armPendingExit('ctrl-d', getCtrlDHint()); }; editor.onEscape = () => { @@ -203,7 +204,7 @@ export class EditorKeyboardController { editor.onShiftTab = () => { if (host.session === undefined) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); + host.showError(getNoActiveSessionMessage()); return; } const next = !host.state.appState.planMode; @@ -263,7 +264,7 @@ export class EditorKeyboardController { if (!editorIsBash) editor.setText(''); const session = host.session; if (host.state.appState.model.trim().length === 0 || session === undefined) { - host.showError(LLM_NOT_SET_MESSAGE); + host.showError(getLlmNotSetMessage()); } else { host.steerMessage(session, parts); } @@ -378,7 +379,7 @@ export class EditorKeyboardController { if (session === undefined) return; void session.cancelCompaction().catch((error: unknown) => { const message = formatErrorMessage(error); - this.host.showError(`Failed to cancel compaction: ${message}`); + this.host.showError(t('tui.statusMessages.compactionCancelFailed', { message })); }); } @@ -466,7 +467,7 @@ export class EditorKeyboardController { if (state.externalEditorRunning) return; const cmd = resolveEditorCommand(state.appState.editorCommand); if (cmd === undefined) { - this.host.showError('No editor configured. Set $VISUAL / $EDITOR, or run /editor <command>.'); + this.host.showError(t('tui.statusMessages.noEditorConfigured')); return; } this.host.setExternalEditorRunning(true); @@ -482,7 +483,7 @@ export class EditorKeyboardController { } } catch (error) { const msg = formatErrorMessage(error); - this.host.showError(`External editor failed: ${msg}`); + this.host.showError(t('tui.messages.editorExternalFailed', { msg })); } finally { if (typeof process.stdin.pause === 'function') { process.stdin.pause(); diff --git a/apps/kimi-code/src/tui/controllers/session-event-handler.ts b/apps/kimi-code/src/tui/controllers/session-event-handler.ts index 2052c38c04..d179edd33a 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -27,6 +27,7 @@ import type { TurnStartedEvent, TurnStepCompletedEvent, TurnStepInterruptedEvent, + TurnStepRetryingEvent, TurnStepStartedEvent, WarningEvent, } from '@moonshot-ai/kimi-code-sdk'; @@ -39,8 +40,8 @@ import { type SwarmModeMarkerState, } from '../components/messages/swarm-markers'; import { + getOauthLoginRequiredStartupNotice, OAUTH_LOGIN_REQUIRED_CODE, - OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE, } from '../constant/kimi-tui'; import { buildGoalCompletionMessage } from '../utils/goal-completion'; import { @@ -71,6 +72,7 @@ import { currentTheme } from '#/tui/theme'; import type { ColorToken } from '#/tui/theme'; import { errorReportHintLine } from '../constant/feedback'; import { formatStepDebugTiming } from '#/utils/usage/debug-timing'; +import { t } from '#/i18n'; import { nextTranscriptId } from '../utils/transcript-id'; import type { BtwPanelController } from './btw-panel'; import type { StreamingUIController } from './streaming-ui'; @@ -205,7 +207,7 @@ export class SessionEventHandler { } catch (error) { if (host.session !== session || host.aborted) return; const message = error instanceof Error ? error.message : String(error); - host.showError(`Failed to sync MCP server status: ${message}`); + host.showError(t('tui.statusMessages.failedToSyncMcp', { message })); return; } if (host.session !== session || host.state.appState.sessionId !== session.id) return; @@ -245,7 +247,7 @@ export class SessionEventHandler { case 'turn.step.started': this.handleStepBegin(event); break; case 'turn.step.interrupted': this.handleStepInterrupted(event); break; case 'turn.step.completed': this.handleStepCompleted(event); break; - case 'turn.step.retrying': break; + case 'turn.step.retrying': this.handleStepRetrying(event); break; case 'tool.progress': this.handleToolProgress(event); break; case 'shell.output': this.host.handleShellOutput(event); break; case 'shell.started': this.host.handleShellStarted(event); break; @@ -334,7 +336,7 @@ export class SessionEventHandler { this.markActiveAgentSwarmsCancelled(); } if (event.reason === 'filtered') { - this.host.showStatus('Turn stopped: provider safety policy blocked the response.', 'error'); + this.host.showStatus(t('tui.statusMessages.turnStoppedFiltered'), 'error'); } const todos = this.host.state.todoPanel.getTodos(); if (todos.length > 0 && todos.every((t) => t.status === 'done')) { @@ -348,6 +350,20 @@ export class SessionEventHandler { this.scheduleQueuedGoalPromotion(); } + private handleStepRetrying(event: TurnStepRetryingEvent): void { + this.host.streamingUI.flushNow(); + this.host.streamingUI.finalizeLiveTextBuffers('waiting'); + const delayS = Math.ceil(event.delayMs / 1000); + this.host.showStatus( + t('tui.statusMessages.retryingStep', { attempt: String(event.nextAttempt), maxAttempts: String(event.maxAttempts), delayS: String(delayS), errorName: event.errorName }), + 'warning', + ); + this.host.setAppState({ + streamingPhase: 'waiting', + streamingStartTime: Date.now(), + }); + } + private handleStepBegin(event: TurnStepStartedEvent): void { this.host.streamingUI.flushNow(); this.host.streamingUI.setStep(event.step); @@ -370,8 +386,8 @@ export class SessionEventHandler { if (event.providerFinishReason === 'filtered') { this.host.showNotice( - 'Provider safety policy blocked the response.', - `The model output was filtered (${event.rawFinishReason ?? 'content_filter'}).`, + t('tui.statusMessages.policyBlocked'), + t('tui.statusMessages.outputFiltered', { reason: event.rawFinishReason ?? 'content_filter' }), ); return; } @@ -385,10 +401,10 @@ export class SessionEventHandler { const title = truncatedCount > 0 - ? 'Model hit max_tokens — tool call was truncated before it could run.' - : 'Model hit max_tokens — no tool call was emitted.'; + ? t('tui.statusMessages.maxTokensTruncated') + : t('tui.statusMessages.maxTokensNoToolCall'); const detail = this.isAnthropicSessionActive() - ? 'If this limit is wrong for your model, set `max_output_size` on the model alias in your kimi-code config.' + ? t('tui.statusMessages.maxTokensHint') : undefined; this.host.showNotice(title, detail); } @@ -426,13 +442,13 @@ export class SessionEventHandler { if (reason === 'error') return; if (reason === 'aborted' || reason === undefined || reason === '') { this.markActiveAgentSwarmsCancelled(); - this.host.showStatus('Interrupted by user', 'error'); + this.host.showStatus(t('tui.statusMessages.interruptedByUser'), 'error'); return; } this.host.showError( reason === 'max_steps' - ? 'reached per-turn step limit (max_steps)' - : `step interrupted (${reason})`, + ? t('tui.statusMessages.stepMaxSteps') + : t('tui.statusMessages.stepInterrupted', { reason }), ); } @@ -515,7 +531,7 @@ export class SessionEventHandler { turnId, }; streamingUI.registerToolCall(toolCall); - if (event.name === 'AgentSwarm') { + if (event.name === 'AgentSwarm' || event.name === 'SwarmDiscussion') { this.subAgentEventHandler.handleAgentSwarmToolCallStarted(event.toolCallId, toolCall.args); } this.host.patchLivePane({ @@ -532,7 +548,7 @@ export class SessionEventHandler { const preview = streamingUI.getStreamingToolCallPreview(event.toolCallId); if ( preview !== undefined && - (preview.name === 'AgentSwarm' || this.subAgentEventHandler.hasAgentSwarmProgress(event.toolCallId)) + (preview.name === 'AgentSwarm' || preview.name === 'SwarmDiscussion' || this.subAgentEventHandler.hasAgentSwarmProgress(event.toolCallId)) ) { this.subAgentEventHandler.handleAgentSwarmToolCallDelta(event.toolCallId, preview.args, { streamingArguments: preview.argumentsText, @@ -752,7 +768,7 @@ export class SessionEventHandler { try { queue = await readGoalQueue(session); } catch (error) { - host.showError(`Failed to read upcoming goals: ${formatErrorMessage(error)}`); + host.showError(t('tui.statusMessages.failedToReadUpcomingGoals', { error: formatErrorMessage(error) })); return false; } if (host.session !== session || host.aborted) return true; @@ -776,7 +792,7 @@ export class SessionEventHandler { await removeGoalQueueItem(session, { goalId: next.id }); } catch (error) { host.showError( - `Queued goal started, but could not be removed from the queue: ${formatErrorMessage(error)}`, + t('tui.statusMessages.queuedGoalRemoveFailed', { error: formatErrorMessage(error) }), ); await this.cancelStartedQueuedGoal(session); return false; @@ -802,7 +818,7 @@ export class SessionEventHandler { try { await restoreGoalQueueItem(session, goal); } catch (error) { - this.host.showError(`Queued goal could not be restored: ${formatErrorMessage(error)}`); + this.host.showError(t('tui.statusMessages.queuedGoalRestoreFailed', { error: formatErrorMessage(error) })); } await this.cancelStartedQueuedGoal(session); } @@ -811,7 +827,7 @@ export class SessionEventHandler { try { await session.cancelGoal(); } catch (error) { - this.host.showError(`Queued goal could not be cancelled: ${formatErrorMessage(error)}`); + this.host.showError(t('tui.statusMessages.queuedGoalCancelFailed', { error: formatErrorMessage(error) })); } } @@ -830,8 +846,8 @@ export class SessionEventHandler { if (!hasQueuedGoal || host.session !== session || host.aborted) return; host.showNotice( - 'Goal blocked.', - 'The next queued goal will start only after this goal is complete.', + t('tui.statusMessages.goalBlocked'), + t('tui.statusMessages.goalBlockedDetail'), ); } @@ -848,7 +864,7 @@ export class SessionEventHandler { this.host.streamingUI.resetToolUi(); this.host.streamingUI.finalizeLiveTextBuffers('idle'); if (event.code === OAUTH_LOGIN_REQUIRED_CODE) { - this.host.showError(OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE); + this.host.showError(getOauthLoginRequiredStartupNotice()); return; } this.host.showError(formatErrorPayload(event)); @@ -859,7 +875,7 @@ export class SessionEventHandler { } private handleSessionWarning(event: WarningEvent): void { - this.host.showStatus(`Warning: ${event.message}`, 'warning'); + this.host.showStatus(t('tui.statusMessages.warningPrefix', { message: event.message }), 'warning'); } private renderMcpServerStatus(server: McpServerStatusSnapshot): void { @@ -872,25 +888,26 @@ export class SessionEventHandler { switch (server.status) { case 'connected': { - const toolStr = `${server.toolCount} tool${server.toolCount === 1 ? '' : 's'}`; - const message = `MCP server "${server.name}" connected · ${toolStr} (${server.transport})`; + const message = t('tui.statusMessages.mcpServerConnected', { name: server.name, count: server.toolCount, transport: server.transport }); this.finalizeMcpServerStatusRow(server.name, message, 'success'); return; } case 'failed': { - const message = `MCP server "${server.name}" failed${server.error !== undefined ? `: ${server.error}` : ''}`; + const message = server.error !== undefined + ? t('tui.statusMessages.mcpServerFailedWithError', { name: server.name, error: server.error }) + : t('tui.statusMessages.mcpServerFailed', { name: server.name }); this.finalizeMcpServerStatusRow(server.name, message, 'error'); return; } case 'needs-auth': { - const message = `MCP server "${server.name}" needs OAuth — run /mcp-config login ${server.name}`; + const message = t('tui.statusMessages.mcpServerNeedsAuth', { name: server.name }); this.finalizeMcpServerStatusRow(server.name, message, 'warning'); return; } case 'disabled': this.finalizeMcpServerStatusRow( server.name, - `MCP server "${server.name}" disabled`, + t('tui.statusMessages.mcpServerDisabled', { name: server.name }), 'textMuted', ); return; @@ -902,7 +919,7 @@ export class SessionEventHandler { private showMcpServerStatusSpinner(name: string): void { const { state } = this.host; - const label = `MCP server "${name}" connecting…`; + const label = t('tui.statusMessages.mcpServerConnecting', { name }); const existing = this.mcpServerStatusSpinners.get(name); if (existing !== undefined) { existing.setLabel(label); @@ -944,7 +961,7 @@ export class SessionEventHandler { kind: 'skill_activation', turnId: undefined, renderMode: 'plain', - content: `Activated skill: ${event.skillName}`, + content: t('tui.statusMessages.activatedSkill', { skillName: event.skillName }), skillActivationId: event.activationId, skillName: event.skillName, skillArgs: event.skillArgs, diff --git a/apps/kimi-code/src/tui/controllers/session-replay.ts b/apps/kimi-code/src/tui/controllers/session-replay.ts index 00ee5a64b5..c1016f8d27 100644 --- a/apps/kimi-code/src/tui/controllers/session-replay.ts +++ b/apps/kimi-code/src/tui/controllers/session-replay.ts @@ -9,6 +9,7 @@ import type { ToolCall, } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { ToolCallComponent } from '../components/messages/tool-call'; import { currentTheme } from '../theme'; import type { TodoItem } from '../components/chrome/todo-panel'; @@ -86,7 +87,7 @@ export class SessionReplayRenderer { try { const main = session.getResumeState()?.agents['main']; if (main === undefined) { - this.host.showError('Session history is unavailable for this session.'); + this.host.showError(t('tui.statusMessages.replaySessionUnavailable')); return false; } @@ -97,7 +98,7 @@ export class SessionReplayRenderer { return true; } catch (error) { const message = formatErrorMessage(error); - this.host.showError(`Failed to replay session history: ${message}`); + this.host.showError(t('tui.statusMessages.replayFailed', { message })); return false; } finally { this.host.setAppState({ isReplaying: false }); @@ -215,7 +216,7 @@ export class SessionReplayRenderer { } context.suppressNextPlanModeOffNotice = false; this.host.appendTranscriptEntry( - replayEntry(context, 'status', `Plan mode: ${record.enabled ? 'ON' : 'OFF'}`, 'notice'), + replayEntry(context, 'status', record.enabled ? t('tui.statusMessages.replayPlanModeOn') : t('tui.statusMessages.replayPlanModeOff'), 'notice'), ); return; case 'permission_updated': @@ -421,7 +422,7 @@ export class SessionReplayRenderer { context.skillActivationIds.add(skill.activationId); sessionEventHandler.renderedSkillActivationIds.add(skill.activationId); this.host.appendTranscriptEntry({ - ...replayEntry(context, 'skill_activation', `Activated skill: ${skill.skillName}`, 'plain'), + ...replayEntry(context, 'skill_activation', t('tui.statusMessages.replaySkillActivated', { skillName: skill.skillName }), 'plain'), skillActivationId: skill.activationId, skillName: skill.skillName, skillArgs: skill.skillArgs, @@ -460,7 +461,7 @@ export class SessionReplayRenderer { if (record.result === undefined) return; if (record.result === 'cancelled') { this.host.appendTranscriptEntry({ - ...replayEntry(context, 'status', 'Compaction cancelled', 'plain'), + ...replayEntry(context, 'status', t('tui.statusMessages.compactionCancelled'), 'plain'), compactionData: { result: 'cancelled', instruction: record.instruction, @@ -470,7 +471,7 @@ export class SessionReplayRenderer { } this.host.appendTranscriptEntry({ - ...replayEntry(context, 'status', 'Compaction complete', 'plain'), + ...replayEntry(context, 'status', t('tui.statusMessages.compactionComplete'), 'plain'), compactionData: { summary: record.result.summary, tokensBefore: record.result.tokensBefore, @@ -486,7 +487,7 @@ export class SessionReplayRenderer { switch (change.kind) { case 'created': this.host.appendTranscriptEntry({ - ...replayEntry(context, 'goal', 'Goal set', 'plain'), + ...replayEntry(context, 'goal', t('tui.statusMessages.goalSet'), 'plain'), goalData: { kind: 'created' }, }); return; @@ -568,8 +569,8 @@ export class SessionReplayRenderer { private renderPermissionUpdate(context: ReplayRenderContext, mode: PermissionMode): void { if (mode === 'yolo') { this.host.appendTranscriptEntry( - replayEntry(context, 'status', 'YOLO mode: ON', 'notice', { - detail: 'All actions will be approved automatically. Use with caution.', + replayEntry(context, 'status', t('tui.statusMessages.replayYoloModeOn'), 'notice', { + detail: t('tui.statusMessages.replayYoloModeOnSub'), }), ); return; @@ -578,7 +579,7 @@ export class SessionReplayRenderer { replayEntry( context, 'status', - mode === 'manual' ? 'YOLO mode: OFF' : `Permission mode: ${mode}`, + mode === 'manual' ? t('tui.statusMessages.replayYoloModeOff') : t('tui.statusMessages.replayPermissionMode', { mode }), 'notice', ), ); @@ -597,13 +598,13 @@ export class SessionReplayRenderer { const parts: string[] = []; switch (result.decision) { case 'approved': - parts.push(result.scope === 'session' ? 'Approved for session' : 'Approved'); + parts.push(result.scope === 'session' ? t('tui.statusMessages.approvedForSession') : t('tui.statusMessages.approved')); break; case 'rejected': - parts.push('Rejected'); + parts.push(t('tui.statusMessages.rejected')); break; case 'cancelled': - parts.push('Cancelled'); + parts.push(t('tui.statusMessages.cancelled')); break; } parts.push(`: ${record.action}`); @@ -628,10 +629,10 @@ export class SessionReplayRenderer { switch (result.decision) { case 'rejected': content = - result.selectedLabel === 'Revise' ? 'Plan sent back for revision' : 'Plan review rejected'; + result.selectedLabel === 'Revise' ? t('tui.statusMessages.planSentBackForRevision') : t('tui.statusMessages.planReviewRejected'); break; case 'cancelled': - content = 'Plan review cancelled'; + content = t('tui.statusMessages.planReviewCancelled'); break; } const detail = @@ -726,14 +727,14 @@ function isResumeNormalizationGoalPause(change: GoalReplayLifecycleChange): bool function goalLifecycleReplayContent(change: GoalReplayLifecycleChange): string { switch (change.status) { case 'paused': - return 'Goal paused'; + return t('tui.statusMessages.replayGoalPaused'); case 'active': - return 'Goal resumed'; + return t('tui.statusMessages.replayGoalResumed'); case 'blocked': - return 'Goal blocked'; + return t('tui.statusMessages.replayGoalBlocked'); case 'complete': case undefined: - return 'Goal updated'; + return t('tui.statusMessages.replayGoalUpdated'); } } diff --git a/apps/kimi-code/src/tui/controllers/streaming-ui.ts b/apps/kimi-code/src/tui/controllers/streaming-ui.ts index ae5eac7681..0d80bcc133 100644 --- a/apps/kimi-code/src/tui/controllers/streaming-ui.ts +++ b/apps/kimi-code/src/tui/controllers/streaming-ui.ts @@ -1,5 +1,6 @@ import type { Session } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import { AgentGroupComponent } from '../components/messages/agent-group'; import { AssistantMessageComponent } from '../components/messages/assistant-message'; import { currentWorkingTip } from '../components/chrome/working-tips'; @@ -577,7 +578,7 @@ export class StreamingUIController { this.host.setAppState({ streamingPhase: 'idle' }); this.host.resetLivePane(); notifyTerminalOnce(state, `turn-complete:${completedTurnKey}`, { - title: 'Kimi Code task complete', + title: t('tui.messages.streamingTaskComplete'), body: state.appState.sessionTitle ?? undefined, }); } diff --git a/apps/kimi-code/src/tui/controllers/tasks-browser.ts b/apps/kimi-code/src/tui/controllers/tasks-browser.ts index 6994b13b86..45858120a7 100644 --- a/apps/kimi-code/src/tui/controllers/tasks-browser.ts +++ b/apps/kimi-code/src/tui/controllers/tasks-browser.ts @@ -1,6 +1,7 @@ import type { BackgroundTaskInfo, Session } from '@moonshot-ai/kimi-code-sdk'; import type { Component, ProcessTerminal, TUI } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import { TaskOutputViewer } from '../components/dialogs/task-output-viewer'; import { TasksBrowserApp, type TasksFilter } from '../components/dialogs/tasks-browser'; import type { Theme } from '#/tui/theme'; @@ -52,7 +53,7 @@ export class TasksBrowserController { const session = this.host.session; if (session === undefined) { - this.host.showError('No active session.'); + this.host.showError(t('tui.statusMessages.noActiveSession')); return; } @@ -61,7 +62,9 @@ export class TasksBrowserController { tasks = await session.listBackgroundTasks({ activeOnly: false }); } catch (error) { this.host.showError( - `Failed to load tasks: ${error instanceof Error ? error.message : String(error)}`, + t('tui.messages.tasksLoadFailed', { + error: error instanceof Error ? error.message : String(error), + }), ); return; } @@ -151,7 +154,7 @@ export class TasksBrowserController { } catch (error) { if (!opts.silent) { const message = error instanceof Error ? error.message : String(error); - this.flash(`Output refresh failed: ${message}`); + this.flash(t('tui.messages.tasksOutputRefreshFailed', { message })); } return; } @@ -208,7 +211,9 @@ export class TasksBrowserController { } catch (error) { if (!opts.silent) { this.flash( - `Refresh failed: ${error instanceof Error ? error.message : String(error)}`, + t('tui.messages.tasksRefreshFailed', { + error: error instanceof Error ? error.message : String(error), + }), ); } return; @@ -287,7 +292,7 @@ export class TasksBrowserController { } private handleRefresh(): void { - this.flash('Refreshing…', 600); + this.flash(t('tui.messages.tasksRefreshing'), 600); void this.refresh(); } @@ -297,17 +302,17 @@ export class TasksBrowserController { const session = this.host.session; if (session === undefined) { - this.flash('No active session.'); + this.flash(t('tui.statusMessages.noActiveSession')); return; } - this.flash(`Stopping ${taskId}…`, 1500); + this.flash(t('tui.messages.tasksStopping', { taskId }), 1500); try { await session.stopBackgroundTask(taskId, { reason: 'User initiated stop' }); await this.refresh({ silent: true }); } catch (error) { const message = error instanceof Error ? error.message : String(error); - this.flash(`Stop failed: ${message}`); + this.flash(t('tui.messages.tasksStopFailed', { message })); } } @@ -319,7 +324,7 @@ export class TasksBrowserController { const session = this.host.session; if (session === undefined) { - this.flash('No active session.'); + this.flash(t('tui.statusMessages.noActiveSession')); return; } @@ -328,7 +333,7 @@ export class TasksBrowserController { output = await session.getBackgroundTaskOutput(taskId); } catch (error) { const message = error instanceof Error ? error.message : String(error); - this.flash(`Cannot open output: ${message}`); + this.flash(t('tui.messages.tasksCannotOpenOutput', { message })); return; } const current = state.tasksBrowser; diff --git a/apps/kimi-code/src/tui/easter-eggs/dance.ts b/apps/kimi-code/src/tui/easter-eggs/dance.ts index 15e3608f83..ccba41cfae 100644 --- a/apps/kimi-code/src/tui/easter-eggs/dance.ts +++ b/apps/kimi-code/src/tui/easter-eggs/dance.ts @@ -12,6 +12,7 @@ import chalk from 'chalk'; import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui'; +import { t } from '#/i18n'; import type { SlashCommandHost } from '../commands/dispatch'; import type { ParsedSlashInput } from '../commands/types'; import { currentTheme } from '../theme'; @@ -239,10 +240,10 @@ export function tryHandleDanceCommand(host: SlashCommandHost, parsed: ParsedSlas currentDanceController.stop(); } else if (sub === 'on') { currentDanceController.start({ hold: true }); - host.showStatus(`Dancing — use ${cmd('/dance off')} to turn it off.`); + host.showStatus(t('tui.messages.danceOn', { cmd: cmd('/dance off') })); } else { currentDanceController.start({ hold: false }); - host.showStatus(`Use ${cmd('/dance on')} to keep the rainbow on.`); + host.showStatus(t('tui.messages.danceOff', { cmd: cmd('/dance on') })); } return true; } diff --git a/apps/kimi-code/src/tui/goal-queue-store.ts b/apps/kimi-code/src/tui/goal-queue-store.ts index 0b98eda676..4ae3013ac5 100644 --- a/apps/kimi-code/src/tui/goal-queue-store.ts +++ b/apps/kimi-code/src/tui/goal-queue-store.ts @@ -1,11 +1,13 @@ import { randomUUID } from 'node:crypto'; -import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { ErrorCodes, KimiError, } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; + const GOAL_QUEUE_FILE = 'upcoming-goals.json'; const GOAL_QUEUE_VERSION = 1; const MAX_GOAL_OBJECTIVE_LENGTH = 4000; @@ -170,7 +172,12 @@ async function readQueueFile(session: GoalQueueSession): Promise<GoalQueueFile> async function writeQueueFile(session: GoalQueueSession, file: GoalQueueFile): Promise<void> { const filePath = goalQueuePath(session); await mkdir(dirname(filePath), { recursive: true }); - await writeFile(filePath, `${JSON.stringify(file, null, 2)}\n`, 'utf-8'); + // Atomic write: write to a temp file in the same directory, then rename. + // This prevents partial-write corruption if the process crashes mid-write + // or if another process reads the file concurrently. + const tmpPath = `${filePath}.${process.pid}.tmp`; + await writeFile(tmpPath, `${JSON.stringify(file, null, 2)}\n`, 'utf-8'); + await rename(tmpPath, filePath); } async function withQueueMutationLock<T>( @@ -205,12 +212,12 @@ function toSnapshot(file: GoalQueueFile): GoalQueueSnapshot { function normalizeObjective(value: string): string { const objective = value.trim(); if (objective.length === 0) { - throw new KimiError(ErrorCodes.GOAL_OBJECTIVE_EMPTY, 'Goal objective cannot be empty'); + throw new KimiError(ErrorCodes.GOAL_OBJECTIVE_EMPTY, t('tui.messages.goalQueueObjectiveEmpty')); } if (objective.length > MAX_GOAL_OBJECTIVE_LENGTH) { throw new KimiError( ErrorCodes.GOAL_OBJECTIVE_TOO_LONG, - `Goal objective cannot exceed ${MAX_GOAL_OBJECTIVE_LENGTH} characters`, + t('tui.messages.goalQueueObjectiveTooLong', { max: MAX_GOAL_OBJECTIVE_LENGTH }), ); } return objective; @@ -219,7 +226,7 @@ function normalizeObjective(value: string): string { function findGoalIndex(file: GoalQueueFile, goalId: string): number { const index = file.goals.findIndex((goal) => goal.id === goalId); if (index === -1) { - throw new KimiError(ErrorCodes.GOAL_NOT_FOUND, 'No queued goal found'); + throw new KimiError(ErrorCodes.GOAL_NOT_FOUND, t('tui.messages.goalQueueNotFound')); } return index; } diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 62d1b2370b..122cb7e3d5 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -23,6 +23,7 @@ import { import { resolve } from 'pathe'; import type { CLIOptions } from '#/cli/options'; +import { t, setLocale } from '#/i18n'; import { MigrationScreenComponent, type MigrationScreenResult } from '#/migration/index'; import { copyTextToClipboard } from '#/utils/clipboard/clipboard-text'; import { appendInputHistory, loadInputHistory } from '#/utils/history/input-history'; @@ -35,7 +36,7 @@ import { restoreTerminalModes } from '#/utils/terminal-restore'; import { BannerProvider } from './banner/banner-provider'; import { readBannerDisplayState, writeBannerDisplayState } from './banner/state'; import { - BUILTIN_SLASH_COMMANDS, + getBuiltinSlashCommands, buildPluginSlashCommands, buildSkillSlashCommands, isExperimentalFlagEnabled, @@ -90,9 +91,9 @@ import { ActivityPaneComponent, type ActivityPaneMode } from './components/panes import { QueuePaneComponent } from './components/panes/queue-pane'; import type { TuiConfig } from './config'; import { - LLM_NOT_SET_MESSAGE, + getLlmNotSetMessage, MAIN_AGENT_ID, - NO_ACTIVE_SESSION_MESSAGE, + getNoActiveSessionMessage, PRODUCT_NAME, } from './constant/kimi-tui'; import { CHROME_GUTTER } from './constant/rendering'; @@ -219,6 +220,7 @@ function createInitialAppState(input: KimiTUIStartupInput): AppState { streamingStartTime: 0, theme: input.tuiConfig.theme, version: input.version, + locale: input.tuiConfig.locale, editorCommand: input.tuiConfig.editorCommand, disablePasteBurst: input.tuiConfig.disablePasteBurst, notifications: input.tuiConfig.notifications, @@ -317,6 +319,8 @@ export class KimiTUI { constructor(harness: KimiHarness, startupInput: KimiTUIStartupInput) { this.harness = harness; + // Apply persisted language preference from tui.toml + setLocale(startupInput.tuiConfig.locale as 'en' | 'zh'); const tuiOptions: KimiTUIOptions = { initialAppState: createInitialAppState(startupInput), startup: { @@ -370,7 +374,7 @@ export class KimiTUI { // ========================================================================= private getSlashCommands(): readonly KimiSlashCommand[] { - const builtins = sortSlashCommands(BUILTIN_SLASH_COMMANDS).filter((command) => + const builtins = sortSlashCommands(getBuiltinSlashCommands()).filter((command) => isExperimentalFlagEnabled(command.experimentalFlag), ); return [...builtins, ...this.skillCommands, ...this.pluginCommands]; @@ -611,10 +615,11 @@ export class KimiTUI { const result = await this.authFlow.refreshProviderModels(); for (const c of result.changed) { if (c.added <= 0) continue; - this.showStatus(`${c.providerName} · +${String(c.added)} model${c.added > 1 ? 's' : ''}.`); + const modelsAddedKey = c.added === 1 ? 'tui.statusMessages.modelsAdded_one' : 'tui.statusMessages.modelsAdded_other'; + this.showStatus(t(modelsAddedKey, { providerName: c.providerName, count: c.added })); } for (const f of result.failed) { - this.showStatus(`Skipped refreshing ${f.provider}: ${f.reason}`, 'warning'); + this.showStatus(t('tui.statusMessages.skippedRefreshing', { provider: f.provider, reason: f.reason }), 'warning'); } } catch { // Best-effort: startup must not crash on background refresh failures. @@ -637,7 +642,7 @@ export class KimiTUI { } const resumeState = this.session?.getResumeState(); if (resumeState?.warning !== undefined) { - this.showStatus(`Warning: ${resumeState.warning}`, 'warning'); + this.showStatus(t('tui.statusMessages.warningLabel', { warning: resumeState.warning }), 'warning'); } if (this.session !== undefined) { this.sessionEventHandler.startSubscription(); @@ -657,7 +662,7 @@ export class KimiTUI { if (this.session !== session) return; for (const warning of warnings) { const severity = warning.severity === 'error' ? 'error' : 'warning'; - this.showStatus(`Warning: ${warning.message}`, severity); + this.showStatus(t('tui.statusMessages.warningLabel', { warning: warning.message }), severity); } } catch { // Best-effort: startup must not block on warning retrieval. @@ -737,7 +742,7 @@ export class KimiTUI { session = await this.harness.createSession(createSessionOptions); this.startupNotice = combineStartupNotice( this.startupNotice, - `No sessions to continue under "${workDir}"; starting a fresh session.`, + t('tui.statusMessages.noSessionsToContinue', { workDir }), ); } } @@ -931,7 +936,7 @@ export class KimiTUI { } if (text.trim().length === 0) return; if (this.state.appState.isReplaying) { - this.showError('Cannot send input while session history is replaying.'); + this.showError(t('tui.statusMessages.cannotSendWhileReplaying')); return; } // Shell commands are stored with a leading `!` so ↑ recall can tell them @@ -957,7 +962,7 @@ export class KimiTUI { private runShellCommandFromInput(command: string): void { const session = this.session; if (session === undefined) { - this.showError('No active session for shell command.'); + this.showError(t('tui.statusMessages.noActiveSessionShell')); return; } // Echo the command locally (bash-input) with a `$` prompt. The agent also @@ -1000,7 +1005,7 @@ export class KimiTUI { (error: unknown) => { const message = formatErrorMessage(error); this.finishShellOutput(commandId, '', message, true); - this.showError(`Shell command failed: ${message}`); + this.showError(t('tui.statusMessages.shellCommandFailed', { message: formatErrorMessage(error) })); }, ); } @@ -1024,7 +1029,7 @@ export class KimiTUI { if (session === undefined) return; for (const commandId of this.shellOutputStreams.keys()) { void session.cancelShellCommand(commandId).catch((error: unknown) => { - this.showError(`Failed to cancel shell command: ${formatErrorMessage(error)}`); + this.showError(t('tui.statusMessages.failedToCancelShell', { message: formatErrorMessage(error) })); }); } } @@ -1072,14 +1077,14 @@ export class KimiTUI { sendNormalUserInput(text: string): void { if (this.btwPanelController.sendUserInput(text)) return; if (this.state.appState.model.trim().length === 0) { - this.showError(LLM_NOT_SET_MESSAGE); + this.showError(getLlmNotSetMessage()); return; } const extraction = extractMediaAttachments(text, this.imageStore); if (!this.validateMediaCapabilities(extraction)) return; const session = this.session; if (session === undefined) { - this.showError(LLM_NOT_SET_MESSAGE); + this.showError(getLlmNotSetMessage()); return; } if (extraction.hasMedia) { @@ -1103,14 +1108,14 @@ export class KimiTUI { extraction.imageAttachmentIds.length > 0 && !this.supportsCurrentModelCapability('image_in') ) { - this.showError('Current model does not support image input.'); + this.showError(t('tui.statusMessages.modelNoImageInput')); return false; } if ( extraction.videoAttachmentIds.length > 0 && !this.supportsCurrentModelCapability('video_in') ) { - this.showError('Current model does not support video input.'); + this.showError(t('tui.statusMessages.modelNoVideoInput')); return false; } return true; @@ -1238,7 +1243,7 @@ export class KimiTUI { const sdkInput = options?.parts ?? input; void session.prompt(sdkInput).catch((error: unknown) => { const message = formatErrorMessage(error); - this.failSessionRequest(`Failed to send: ${message}`); + this.failSessionRequest(t('tui.statusMessages.failedToSend', { message })); }); } @@ -1246,7 +1251,7 @@ export class KimiTUI { this.beginSessionRequest(); void session.activateSkill(skillName, skillArgs).catch((error: unknown) => { const message = formatErrorMessage(error); - this.failSessionRequest(`Skill "${skillName}" failed: ${message}`); + this.failSessionRequest(t('tui.statusMessages.skillFailed', { skillName, message })); }); } @@ -1259,7 +1264,7 @@ export class KimiTUI { this.beginSessionRequest(); void session.activatePluginCommand(pluginId, commandName, args).catch((error: unknown) => { const message = formatErrorMessage(error); - this.failSessionRequest(`Command "${pluginId}:${commandName}" failed: ${message}`); + this.failSessionRequest(t('tui.statusMessages.pluginCommandFailed', { pluginId, commandName, message })); }); } @@ -1301,7 +1306,7 @@ export class KimiTUI { void session.steer(input.join('\n\n')).catch((error: unknown) => { const message = formatErrorMessage(error); - this.showError(`Failed to steer: ${message}`); + this.showError(t('tui.statusMessages.failedToSteer', { message })); }); } @@ -1410,7 +1415,7 @@ export class KimiTUI { requireSession(): Session { if (this.session === undefined) { - throw new Error(NO_ACTIVE_SESSION_MESSAGE); + throw new Error(getNoActiveSessionMessage()); } return this.session; } @@ -1418,7 +1423,7 @@ export class KimiTUI { private async createSessionFromCurrentState(): Promise<Session> { const model = this.state.appState.model.trim(); if (model.length === 0) { - throw new Error(LLM_NOT_SET_MESSAGE); + throw new Error(getLlmNotSetMessage()); } const options: MutableCreateSessionOptions = { workDir: this.state.appState.workDir, @@ -1586,26 +1591,26 @@ export class KimiTUI { private async showResumeOtherWorkDirHint(session: SessionRow): Promise<void> { this.hideSessionPicker(); const command = `cd ${quoteShellArg(session.work_dir)} && kimi --resume ${quoteShellArg(session.id)}`; - const message = `Current session is in a different working directory.\n To resume, run: ${command}`; + const message = t('tui.statusMessages.resumeOtherWorkDir', { command }); try { await copyTextToClipboard(command); - this.showStatus(`${message}\n Command copied to clipboard`, 'warning'); + this.showStatus(`${message}\n ${t('tui.statusMessages.commandCopiedToClipboard')}`, 'warning'); } catch { - this.showStatus(`${message}\n Failed to copy command to clipboard`, 'warning'); + this.showStatus(`${message}\n ${t('tui.statusMessages.failedToCopyCommand')}`, 'warning'); } } private async resumeSession(targetSessionId: string): Promise<boolean> { if (targetSessionId === this.state.appState.sessionId) { - this.showStatus('Already on this session.'); + this.showStatus(t('tui.statusMessages.alreadyOnSession')); return true; } if (this.state.appState.streamingPhase !== 'idle') { - this.showError('Cannot switch sessions while streaming — press Esc or Ctrl-C first.'); + this.showError(t('tui.statusMessages.cannotSwitchWhileStreaming')); return false; } if (this.state.appState.isReplaying) { - this.showError('Cannot switch sessions while history is replaying.'); + this.showError(t('tui.statusMessages.cannotSwitchWhileReplaying')); return false; } @@ -1614,11 +1619,11 @@ export class KimiTUI { session = await this.harness.resumeSession({ id: targetSessionId }); } catch (error) { const msg = formatErrorMessage(error); - this.showError(`Failed to resume session ${targetSessionId}: ${msg}`); + this.showError(t('tui.statusMessages.failedToResumeSession', { sessionId: targetSessionId, message: msg })); return false; } - await this.switchToSession(session, `Resumed session (${session.id}).`); + await this.switchToSession(session, t('tui.statusMessages.resumedSession', { sessionId: session.id })); return true; } @@ -1638,13 +1643,13 @@ export class KimiTUI { await this.sessionReplay.hydrateFromReplay(session); } catch (error) { const msg = formatErrorMessage(error); - this.showError(`Failed to replay session history: ${msg}`); + this.showError(t('tui.statusMessages.failedToReplayHistory', { message: msg })); } finally { this.sessionEventHandler.startSubscription(); } const resumeState = session.getResumeState(); if (resumeState?.warning !== undefined) { - this.showStatus(`Warning: ${resumeState.warning}`, 'warning'); + this.showStatus(t('tui.statusMessages.warningLabel', { warning: resumeState.warning }), 'warning'); } this.showStatus(statusMessage); void this.showSessionWarnings(session); @@ -1674,7 +1679,7 @@ export class KimiTUI { this.sessionEventHandler.startSubscription(); const resumeState = session.getResumeState(); if (resumeState?.warning !== undefined) { - this.showStatus(`Warning: ${resumeState.warning}`, 'warning'); + this.showStatus(t('tui.statusMessages.warningLabel', { warning: resumeState.warning }), 'warning'); } this.showStatus(statusMessage); void this.showSessionWarnings(session); @@ -1682,7 +1687,7 @@ export class KimiTUI { async createNewSession(): Promise<void> { if (this.state.appState.isReplaying) { - this.showError('Cannot start a new session while history is replaying.'); + this.showError(t('tui.statusMessages.cannotStartNewWhileReplaying')); return; } @@ -1691,7 +1696,7 @@ export class KimiTUI { session = await this.createSessionFromCurrentState(); } catch (error) { const msg = formatErrorMessage(error); - this.showError(`Failed to start a new session: ${msg}`); + this.showError(t('tui.statusMessages.failedToStartNewSession', { message: msg })); return; } @@ -1704,7 +1709,7 @@ export class KimiTUI { } catch (error) { this.sessionEventHandler.startSubscription(); const msg = formatErrorMessage(error); - this.showError(`Post-create setup failed: ${msg}`); + this.showError(t('tui.statusMessages.postCreateSetupFailed', { message: msg })); return; } try { @@ -1715,7 +1720,7 @@ export class KimiTUI { } this.sessionEventHandler.startSubscription(); this.clearTranscriptAndRedraw(); - this.showStatus(`Started a new session (${session.id}).`); + this.showStatus(t('tui.statusMessages.startedNewSession', { sessionId: session.id })); void this.showSessionWarnings(session); void this.showConfigWarningsIfAny(); } @@ -1850,13 +1855,13 @@ export class KimiTUI { const parts: string[] = []; switch (response.decision) { case 'approved': - parts.push(response.scope === 'session' ? 'Approved for session' : 'Approved'); + parts.push(response.scope === 'session' ? t('tui.statusMessages.approvedForSession') : t('tui.statusMessages.approved')); break; case 'rejected': - parts.push('Rejected'); + parts.push(t('tui.statusMessages.rejected')); break; case 'cancelled': - parts.push('Cancelled'); + parts.push(t('tui.statusMessages.cancelled')); break; } parts.push(`: ${request.action}`); @@ -2156,7 +2161,7 @@ export class KimiTUI { } showError(message: string): void { - this.showStatus(`Error: ${message}`, 'error'); + this.showStatus(t('tui.messages.kimiTuiError', { message }), 'error'); } showLoginProgressSpinner(label: string): LoginProgressSpinnerHandle { @@ -2187,14 +2192,14 @@ export class KimiTUI { openUrl(auth.verificationUriComplete); this.state.transcriptContainer.addChild( new DeviceCodeBoxComponent({ - title: 'Sign in to Kimi Code', + title: t('tui.chrome.deviceCodeBox.title'), url: auth.verificationUriComplete, code: auth.userCode, - hint: 'Press Ctrl-C to cancel', + hint: t('tui.chrome.deviceCodeBox.hint'), }), ); this.state.ui.requestRender(); - return this.showLoginProgressSpinner('Waiting for authorization…'); + return this.showLoginProgressSpinner(t('tui.statusMessages.waitingForAuthorization')); } // ========================================================================= @@ -2262,7 +2267,7 @@ export class KimiTUI { break; } case 'composing': { - const spinner = this.ensureActivitySpinner('braille', 'working...', (s) => + const spinner = this.ensureActivitySpinner('braille', t('tui.statusMessages.working'), (s) => currentTheme.fg('primary', s), ); this.syncAgentSwarmActivitySpinner(undefined); @@ -2379,12 +2384,12 @@ export class KimiTUI { // Only one `!` command runs at a time (input is queued while busy). const next = this.shellOutputStreams.entries().next(); if (next.done) { - this.showDetachHint('No shell command running.'); + this.showDetachHint(t('tui.statusMessages.noShellCommandRunning')); return; } const [commandId, stream] = next.value; if (stream.taskId === undefined) { - this.showDetachHint('Command is still starting — try again.'); + this.showDetachHint(t('tui.statusMessages.commandStillStarting')); return; } const session = this.session; @@ -2392,23 +2397,23 @@ export class KimiTUI { try { const info = await session.detachBackgroundTask(stream.taskId); if (info === undefined) { - this.showDetachHint('Command already finished.'); + this.showDetachHint(t('tui.statusMessages.commandAlreadyFinished')); return; } } catch (error) { - this.showError(`Failed to move to background: ${formatErrorMessage(error)}`); + this.showError(t('tui.statusMessages.failedToMoveToBackground', { message: formatErrorMessage(error) })); return; } // Finalize the card as backgrounded and drop the stream so the eventual // runShellCommand resolution (which carries background metadata) is a no-op // instead of overwriting this view. stream.component.finishBackgrounded(); - stream.entry.content = 'Moved to background.'; + stream.entry.content = t('tui.statusMessages.movedToBackground'); this.shellOutputStreams.delete(commandId); // The backgrounded command's notification turn (started by agent-core via // appendSystemReminderAndNotify) owns the streaming phase and drains the // queue when it completes, so we intentionally leave both untouched here. - this.showDetachHint('Moved to background. /tasks to view.'); + this.showDetachHint(t('tui.statusMessages.movedToBackgroundHint')); } async detachCurrentForegroundTask(): Promise<void> { @@ -2420,7 +2425,7 @@ export class KimiTUI { const session = this.session; if (session === undefined) { - this.showError(NO_ACTIVE_SESSION_MESSAGE); + this.showError(getNoActiveSessionMessage()); return; } @@ -2430,13 +2435,13 @@ export class KimiTUI { // and therefore included. We filter to `detached === false` ourselves. tasks = await session.listBackgroundTasks(); } catch (error) { - this.showError(`Failed to list tasks: ${formatErrorMessage(error)}`); + this.showError(t('tui.statusMessages.failedToListTasks', { message: formatErrorMessage(error) })); return; } const targets = pickForegroundTasks(tasks); if (targets.length === 0) { - this.showDetachHint('No foreground task running.'); + this.showDetachHint(t('tui.statusMessages.noForegroundTaskRunning')); return; } @@ -2448,20 +2453,22 @@ export class KimiTUI { if (info === undefined) alreadyFinished++; else detached++; } catch (error) { - this.showError(`Failed to detach ${target.taskId}: ${formatErrorMessage(error)}`); + this.showError(t('tui.statusMessages.failedToDetachTask', { taskId: target.taskId, message: formatErrorMessage(error) })); } } let hint: string; if (detached === 0 && alreadyFinished > 0) { - hint = alreadyFinished === 1 ? 'Task already finished.' : 'Tasks already finished.'; + const key = alreadyFinished === 1 ? 'tui.statusMessages.taskAlreadyFinished_one' : 'tui.statusMessages.taskAlreadyFinished_other'; + hint = t(key); } else if (detached === targets.length) { - hint = - detached === 1 ? 'Moved 1 task to background.' : `Moved ${detached} tasks to background.`; + hint = detached === 1 + ? t('tui.statusMessages.movedOneTaskToBackground') + : t('tui.statusMessages.movedTasksToBackground', { count: detached }); } else { - hint = `Moved ${detached} of ${targets.length} tasks to background.`; + hint = t('tui.statusMessages.movedTasksOfToBackground', { detached, total: targets.length }); } - if (detached > 0) hint = `${hint} /tasks to view.`; + if (detached > 0) hint = `${hint}${t('tui.statusMessages.tasksToView')}`; this.showDetachHint(hint); } @@ -2784,7 +2791,7 @@ export class KimiTUI { onSelect: (session: SessionRow) => { void this.handleSessionPickerSelect(session, options.applyStartupModes === true).catch( (error) => { - this.showError(`Failed to apply startup flags: ${formatErrorMessage(error)}`); + this.showError(t('tui.statusMessages.failedToApplyStartupFlags', { message: formatErrorMessage(error) })); }, ); }, @@ -2820,7 +2827,7 @@ export class KimiTUI { private showApprovalPanel(payload: ApprovalPanelData): void { this.patchLivePane({ pendingApproval: { data: payload } }); notifyTerminalOnce(this.state, `approval:${payload.id}`, { - title: 'Kimi Code approval required', + title: t('tui.messages.kimiTuiApprovalRequired'), body: payload.tool_name, }); const panel = new ApprovalPanelComponent( @@ -2887,7 +2894,7 @@ export class KimiTUI { private showQuestionDialog(payload: QuestionPanelData): void { this.patchLivePane({ pendingQuestion: { data: payload } }); notifyTerminalOnce(this.state, `question:${payload.id}`, { - title: 'Kimi Code needs your answer', + title: t('tui.messages.kimiTuiNeedsAnswer'), body: payload.questions[0]?.question, }); const dialog = new QuestionDialogComponent( diff --git a/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts b/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts index 8112534a07..63e9594092 100644 --- a/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts +++ b/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts @@ -1,20 +1,25 @@ import type { ApprovalRequest, ApprovalResponse, ToolInputDisplay } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; import type { ApprovalPanelResponse } from '#/tui/components/dialogs/approval-panel'; import { goalStartOptions } from '#/tui/components/dialogs/goal-start-permission-prompt'; import type { ApprovalPanelChoice, ApprovalPanelData, DisplayBlock } from '#/tui/reverse-rpc/types'; -const DEFAULT_APPROVAL_CHOICES: ApprovalPanelChoice[] = [ - { label: 'Approve once', response: 'approved' }, - { label: 'Approve for this session', response: 'approved_for_session' }, - { label: 'Reject', response: 'rejected' }, - { label: 'Reject with feedback', response: 'rejected', requires_feedback: true }, -]; +function getDefaultApprovalChoices(): ApprovalPanelChoice[] { + return [ + { label: t('tui.approvalLabels.approveOnce'), response: 'approved' }, + { label: t('tui.approvalLabels.approveForSession'), response: 'approved_for_session' }, + { label: t('tui.approvalLabels.reject'), response: 'rejected' }, + { label: t('tui.approvalLabels.rejectWithFeedback'), response: 'rejected', requires_feedback: true }, + ]; +} -const PLAN_REJECT_CHOICES: ApprovalPanelChoice[] = [ - { label: 'Reject', response: 'rejected', selected_label: 'Reject' }, - { label: 'Revise', response: 'rejected', selected_label: 'Revise', requires_feedback: true }, -]; +function getPlanRejectChoices(): ApprovalPanelChoice[] { + return [ + { label: t('tui.approvalLabels.reject'), response: 'rejected', selected_label: t('tui.approvalLabels.reject') }, + { label: t('tui.approvalLabels.revise'), response: 'rejected', selected_label: t('tui.approvalLabels.revise'), requires_feedback: true }, + ]; +} export function adaptApprovalRequest(event: ApprovalRequest): ApprovalPanelData { const resolved = resolveDisplay(event.toolName, event.display, event.action); @@ -178,7 +183,7 @@ function describeApproval(display: ToolInputDisplay, action: string): string { case 'plan_review': return ''; case 'goal_start': - return 'Start a goal?'; + return t('tui.approvalDescriptions.startGoal'); case 'generic': if (typeof display.detail === 'string' && display.detail.length > 0) { return display.detail; @@ -187,39 +192,48 @@ function describeApproval(display: ToolInputDisplay, action: string): string { case 'command': return display.description ?? display.command ?? action; case 'diff': - return `edit ${display.path ?? ''}`.trim(); + return t('tui.approvalDescriptions.editFile', { path: display.path ?? '' }).trim(); case 'file_io': - return `${display.operation ?? 'file'} ${display.path ?? ''}`.trim(); + return t('tui.approvalDescriptions.fileOp', { + operation: display.operation ?? 'file', + path: display.path ?? '', + }).trim(); case 'task_stop': - return `stop task: ${display.task_description ?? display.task_id ?? ''}`.trim(); + return t('tui.approvalDescriptions.stopTask', { + description: display.task_description ?? display.task_id ?? '', + }).trim(); case 'agent_call': - return `spawn ${display.agent_name ?? 'agent'}`; + return t('tui.approvalDescriptions.spawnAgent', { name: display.agent_name ?? 'agent' }); case 'skill_call': - return `invoke skill ${display.skill_name ?? ''}`.trim(); + return t('tui.approvalDescriptions.invokeSkill', { name: display.skill_name ?? '' }).trim(); case 'url_fetch': - return `fetch ${display.url ?? ''}`.trim(); + return t('tui.approvalDescriptions.fetchUrl', { url: display.url ?? '' }).trim(); case 'search': - return `search: ${display.query ?? ''}`.trim(); + return t('tui.approvalDescriptions.searchQuery', { query: display.query ?? '' }).trim(); case 'todo_list': - return `update todo list (${String(display.items?.length ?? 0)} items)`; + return t('tui.approvalDescriptions.updateTodoList', { + count: String(display.items?.length ?? 0), + }); case 'background_task': - return `${display.status ?? 'background'} task ${display.task_id ?? ''}: ${ - display.description ?? '' - }`.trim(); + return t('tui.approvalDescriptions.backgroundTask', { + status: display.status ?? 'background', + taskId: display.task_id ?? '', + description: display.description ?? '', + }).trim(); default: return action; } } const DANGER_PATTERNS: Array<{ pattern: RegExp; label: string }> = [ - { pattern: /\brm\s+(-[a-zA-Z]*[rRfF][a-zA-Z]*|--recursive|--force)/i, label: 'recursive delete' }, - { pattern: /\bsudo\b/i, label: 'sudo' }, - { pattern: /\b(curl|wget)\b[^|]*\|\s*(sh|bash|zsh)\b/i, label: 'pipe to shell' }, - { pattern: /\bdd\b[^|]*\bof=/i, label: 'dd write' }, - { pattern: /\bmkfs\b/i, label: 'mkfs' }, - { pattern: />\s*\/dev\/(sd|nvme|disk|hd)/i, label: 'write to raw device' }, - { pattern: /\bchmod\s+-R?\s*777\b/i, label: 'chmod 777' }, - { pattern: /:\(\)\s*\{\s*:\|:&\s*\}/i, label: 'fork bomb' }, + { pattern: /\brm\s+(-[a-zA-Z]*[rRfF][a-zA-Z]*|--recursive|--force)/i, label: t('tui.dangerPatterns.recursiveDelete') }, + { pattern: /\bsudo\b/i, label: t('tui.dangerPatterns.sudo') }, + { pattern: /\b(curl|wget)\b[^|]*\|\s*(sh|bash|zsh)\b/i, label: t('tui.dangerPatterns.pipeToShell') }, + { pattern: /\bdd\b[^|]*\bof=/i, label: t('tui.dangerPatterns.ddWrite') }, + { pattern: /\bmkfs\b/i, label: t('tui.dangerPatterns.mkfs') }, + { pattern: />\s*\/dev\/(sd|nvme|disk|hd)/i, label: t('tui.dangerPatterns.writeToRawDevice') }, + { pattern: /\bchmod\s+-R?\s*777\b/i, label: t('tui.dangerPatterns.chmod777') }, + { pattern: /:\(\)\s*\{\s*:\|:&\s*\}/i, label: t('tui.dangerPatterns.forkBomb') }, ]; function detectDanger(command: string): string | undefined { @@ -318,15 +332,22 @@ function adaptDisplay(display: ToolInputDisplay): DisplayBlock[] { return [ { type: 'brief', - text: `Stop task ${display.task_id ?? ''}: ${display.task_description ?? ''}`, + text: t('tui.approvalDescriptions.taskStopBrief', { + taskId: display.task_id ?? '', + description: display.task_description ?? '', + }), }, ]; case 'plan_review': return []; case 'goal_start': { - const lines = [`Start goal: ${display.objective}`]; + const lines = [t('tui.approvalDescriptions.goalStartBrief', { objective: display.objective })]; if (typeof display.completionCriterion === 'string' && display.completionCriterion.length > 0) { - lines.push(`Done when: ${display.completionCriterion}`); + lines.push( + t('tui.approvalDescriptions.goalCompletionCriterion', { + criterion: display.completionCriterion, + }), + ); } return [{ type: 'brief', text: lines.join('\n') }]; } @@ -349,7 +370,7 @@ function adaptChoices(toolName: string, display: ToolInputDisplay): ApprovalPane return adaptGoalStartChoices(display); } - return DEFAULT_APPROVAL_CHOICES.map((choice) => cloneChoice(choice)); + return getDefaultApprovalChoices().map((choice) => cloneChoice(choice)); } function adaptGoalStartChoices( @@ -383,8 +404,8 @@ function adaptPlanReviewChoices(display: ToolInputDisplay): ApprovalPanelChoice[ response: 'approved' as const, selected_label: option.label, })) - : [{ label: 'Approve', response: 'approved' as const, selected_label: 'Approve' }]; - return [...optionChoices, ...PLAN_REJECT_CHOICES].map((choice) => cloneChoice(choice)); + : [{ label: t('tui.approvalLabels.approve'), response: 'approved' as const, selected_label: t('tui.approvalLabels.approve') }]; + return [...optionChoices, ...getPlanRejectChoices()].map((choice) => cloneChoice(choice)); } function cloneChoice(choice: ApprovalPanelChoice): ApprovalPanelChoice { diff --git a/apps/kimi-code/src/tui/types.ts b/apps/kimi-code/src/tui/types.ts index 6dcdccdd1a..540cbb022e 100644 --- a/apps/kimi-code/src/tui/types.ts +++ b/apps/kimi-code/src/tui/types.ts @@ -45,6 +45,7 @@ export interface AppState { isReplaying: boolean; streamingPhase: 'idle' | 'waiting' | 'thinking' | 'composing' | 'shell'; streamingStartTime: number; + locale: string; theme: ThemeName; version: string; editorCommand: string | null; diff --git a/apps/kimi-code/src/tui/utils/background-agent-status.ts b/apps/kimi-code/src/tui/utils/background-agent-status.ts index a54257a971..b728e9d4eb 100644 --- a/apps/kimi-code/src/tui/utils/background-agent-status.ts +++ b/apps/kimi-code/src/tui/utils/background-agent-status.ts @@ -4,6 +4,8 @@ import type { BackgroundAgentStatusPhase, } from '#/tui/types'; +import { t } from '#/i18n'; + const MAX_BACKGROUND_FIELD_LENGTH = 240; function normalizeBackgroundField(value: string | undefined): string | undefined { @@ -23,10 +25,10 @@ export function formatBackgroundAgentTranscript( const subject = normalizedAgentName !== undefined ? `${normalizedAgentName} agent` : 'agent'; const headline = phase === 'started' - ? `${subject} started in background` + ? t('tui.messages.bgAgentStarted', { subject }) : phase === 'completed' - ? `${subject} completed in background` - : `${subject} failed in background`; + ? t('tui.messages.bgAgentCompleted', { subject }) + : t('tui.messages.bgAgentFailed', { subject }); const tail = phase === 'failed' ? normalizeBackgroundField(extras?.error) : undefined; const detailParts = [normalizeBackgroundField(meta.description), tail].filter( (part): part is string => part !== undefined, diff --git a/apps/kimi-code/src/tui/utils/background-task-status.ts b/apps/kimi-code/src/tui/utils/background-task-status.ts index 10f579ad9f..931bcf37dc 100644 --- a/apps/kimi-code/src/tui/utils/background-task-status.ts +++ b/apps/kimi-code/src/tui/utils/background-task-status.ts @@ -13,6 +13,8 @@ import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi import type { BackgroundAgentStatusData, BackgroundAgentStatusPhase } from '@/tui/types'; +import { t } from '#/i18n'; + const MAX_DETAIL_LENGTH = 240; function truncate(value: string | undefined): string | undefined { @@ -40,26 +42,26 @@ function phaseFromStatus(status: BackgroundTaskStatus): BackgroundAgentStatusPha } function subjectFor(info: BackgroundTaskInfo): string { - if (info.kind === 'agent') return 'agent task'; - if (info.kind === 'question') return 'question task'; - return 'bash task'; + if (info.kind === 'agent') return t('tui.messages.bgTaskAgent'); + if (info.kind === 'question') return t('tui.messages.bgTaskQuestion'); + return t('tui.messages.bgTaskBash'); } function headlineFor(info: BackgroundTaskInfo): string { const subject = subjectFor(info); switch (info.status) { case 'running': - return `${subject} started in background`; + return t('tui.messages.bgTaskStarted', { subject }); case 'completed': - return `${subject} completed in background`; + return t('tui.messages.bgTaskCompleted', { subject }); case 'failed': - return `${subject} failed in background`; + return t('tui.messages.bgTaskFailed', { subject }); case 'timed_out': - return `${subject} timed out`; + return t('tui.messages.bgTaskTimedOut', { subject }); case 'killed': - return `${subject} stopped`; + return t('tui.messages.bgTaskStopped', { subject }); case 'lost': - return `${subject} lost`; + return t('tui.messages.bgTaskLost', { subject }); } } @@ -75,7 +77,7 @@ function detailFor(info: BackgroundTaskInfo): string | undefined { } if (info.status === 'killed') { const reason = truncate(info.stopReason); - parts.push(reason !== undefined ? `stopped — ${reason}` : 'stopped'); + parts.push(reason !== undefined ? t('tui.messages.bgTaskStoppedReason', { reason }) : t('tui.messages.bgTaskStopped', { subject: '' })); } if (info.status === 'failed') { const reason = truncate(info.stopReason); diff --git a/apps/kimi-code/src/tui/utils/event-payload.ts b/apps/kimi-code/src/tui/utils/event-payload.ts index 2508996ea4..258c5f5665 100644 --- a/apps/kimi-code/src/tui/utils/event-payload.ts +++ b/apps/kimi-code/src/tui/utils/event-payload.ts @@ -7,6 +7,7 @@ import { STREAMING_ARGS_FIELD_RE, STREAMING_ARGS_PREVIEW_MAX_CHARS, } from '#/tui/constant/streaming'; +import { t } from '#/i18n'; export function appendStreamingArgsPreview( current: string | undefined, @@ -120,7 +121,7 @@ function formatProviderFilteredMessage( const normalizedFinishReason = finishReason ?? 'filtered'; const raw = rawFinishReason === undefined ? '' : `, rawFinishReason=${rawFinishReason}`; - return `Provider filtered the response before visible output (finishReason=${normalizedFinishReason}${raw}).`; + return t('tui.messages.eventFilteredResponse', { reason: normalizedFinishReason, raw }); } function stringDetail( diff --git a/apps/kimi-code/src/tui/utils/goal-completion.ts b/apps/kimi-code/src/tui/utils/goal-completion.ts index f4c802b4a8..221e24b629 100644 --- a/apps/kimi-code/src/tui/utils/goal-completion.ts +++ b/apps/kimi-code/src/tui/utils/goal-completion.ts @@ -1,5 +1,7 @@ import type { GoalSnapshot } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; + interface GoalCompletionStats { readonly terminalReason?: string | undefined; readonly turnsUsed: number; @@ -17,9 +19,9 @@ export function buildGoalCompletionMessage(goal: GoalSnapshot): string { } export function buildGoalCompletionMessageFromStats(goal: GoalCompletionStats): string { - const head = `✓ Goal complete${goal.terminalReason ? ` — ${goal.terminalReason}` : ''}.`; - const turns = `${goal.turnsUsed} turn${goal.turnsUsed === 1 ? '' : 's'}`; - const stats = `Worked ${turns} over ${formatElapsed(goal.wallClockMs)}, using ${formatTokens(goal.tokensUsed)} tokens.`; + const head = t('tui.messages.goalComplete', { reason: goal.terminalReason ? ` — ${goal.terminalReason}` : '' }); + const turns = t('tui.messages.goalCompleteTurns', { count: goal.turnsUsed, plural: goal.turnsUsed === 1 ? '' : 's' }); + const stats = t('tui.messages.goalCompleteSummary', { turns, elapsed: formatElapsed(goal.wallClockMs), tokens: formatTokens(goal.tokensUsed) }); return `${head}\n${stats}`; } diff --git a/apps/kimi-code/src/tui/utils/mcp-server-status.ts b/apps/kimi-code/src/tui/utils/mcp-server-status.ts index 53e4694885..3463b9c0fb 100644 --- a/apps/kimi-code/src/tui/utils/mcp-server-status.ts +++ b/apps/kimi-code/src/tui/utils/mcp-server-status.ts @@ -1,5 +1,7 @@ import type { McpServerInfo, McpServerStatusEvent } from '@moonshot-ai/kimi-code-sdk'; +import { t } from '#/i18n'; + export type McpServerStatusSnapshot = McpServerInfo | McpServerStatusEvent['server']; export const MCP_STARTUP_STATUS_ROW_LIMIT = 4; @@ -57,11 +59,11 @@ export function formatMcpStartupStatusSummary( } const parts: string[] = []; - if (failed > 0) parts.push(`${failed} failed`); - if (needsAuth > 0) parts.push(`${needsAuth} need auth`); - if (connecting > 0) parts.push(`${connecting} connecting`); - if (connected > 0) parts.push(`${connected} connected`); - if (disabled > 0) parts.push(`${disabled} disabled`); + if (failed > 0) parts.push(t('tui.messages.mcpStatusFailed', { count: failed })); + if (needsAuth > 0) parts.push(t('tui.messages.mcpStatusNeedsAuth', { count: needsAuth })); + if (connecting > 0) parts.push(t('tui.messages.mcpStatusConnecting', { count: connecting })); + if (connected > 0) parts.push(t('tui.messages.mcpStatusConnected', { count: connected })); + if (disabled > 0) parts.push(t('tui.messages.mcpStatusDisabled', { count: disabled })); return parts.join(', '); } diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index 4bc79289b2..d93d11ceb2 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -162,6 +162,7 @@ function tuiConfig(overrides: Partial<TuiConfig> = {}): TuiConfig { return { theme: 'auto', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, diff --git a/apps/kimi-code/test/i18n/index.test.ts b/apps/kimi-code/test/i18n/index.test.ts new file mode 100644 index 0000000000..101a9bb66c --- /dev/null +++ b/apps/kimi-code/test/i18n/index.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { t, setLocale, getLocale } from '#/i18n'; + +describe('i18n', () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + setLocale('en'); + }); + + afterEach(() => { + setLocale('en'); + }); + + describe('t()', () => { + it('returns the English string for a known key', () => { + expect(t('common.ok')).toBe('OK'); + }); + + it('returns the Chinese string when locale is zh', () => { + setLocale('zh'); + expect(t('common.ok')).toBe('确定'); + }); + + it('returns the key itself when key does not exist in any locale', () => { + expect(t('nonexistent.key.here')).toBe('nonexistent.key.here'); + }); + + it('interpolates {{param}} placeholders', () => { + setLocale('en'); + expect(t('tui.statusMessages.shellCommandFailed', { message: 'ENOENT' })).toContain('ENOENT'); + }); + + it('handles empty params object', () => { + setLocale('en'); + expect(t('common.ok', {})).toBe('OK'); + }); + + it('handles multiple params', () => { + setLocale('en'); + const result = t('tui.statusMessages.unsupportedEffort', { + arg: 'high', + alias: 'gpt-4', + segments: 'low, medium', + }); + expect(result).toContain('high'); + expect(result).toContain('gpt-4'); + expect(result).toContain('low, medium'); + }); + + it('keeps placeholder when param is missing', () => { + setLocale('en'); + const result = t('tui.statusMessages.shellCommandFailed', {}); + expect(result).toContain('{{message}}'); + }); + }); + + describe('setLocale() / getLocale()', () => { + it('getLocale returns the current locale', () => { + setLocale('zh'); + expect(getLocale()).toBe('zh'); + setLocale('en'); + expect(getLocale()).toBe('en'); + }); + + it('setLocale ignores invalid locale values', () => { + setLocale('en'); + setLocale('fr' as any); + expect(getLocale()).toBe('en'); + }); + }); +}); \ No newline at end of file diff --git a/apps/kimi-code/test/tui/activity-pane.test.ts b/apps/kimi-code/test/tui/activity-pane.test.ts index 0bcae749a5..247496d103 100644 --- a/apps/kimi-code/test/tui/activity-pane.test.ts +++ b/apps/kimi-code/test/tui/activity-pane.test.ts @@ -30,6 +30,7 @@ function makeStartupInput(): KimiTUIStartupInput { tuiConfig: { theme: 'dark', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, diff --git a/apps/kimi-code/test/tui/commands/registry.test.ts b/apps/kimi-code/test/tui/commands/registry.test.ts index bc4c5894fc..1bf245541f 100644 --- a/apps/kimi-code/test/tui/commands/registry.test.ts +++ b/apps/kimi-code/test/tui/commands/registry.test.ts @@ -1,5 +1,5 @@ import { - BUILTIN_SLASH_COMMANDS, + getBuiltinSlashCommands, findBuiltInSlashCommand, parseSlashInput, resolveSlashCommandAvailability, @@ -143,7 +143,7 @@ describe('built-in slash command registry', () => { }); it('contains the expected command names once', () => { - const names = BUILTIN_SLASH_COMMANDS.map((command) => command.name); + const names = getBuiltinSlashCommands().map((command) => command.name); expect(new Set(names).size).toBe(names.length); expect(names).toEqual( diff --git a/apps/kimi-code/test/tui/components/chrome/footer.test.ts b/apps/kimi-code/test/tui/components/chrome/footer.test.ts index 2fe6f3e52e..9476f65330 100644 --- a/apps/kimi-code/test/tui/components/chrome/footer.test.ts +++ b/apps/kimi-code/test/tui/components/chrome/footer.test.ts @@ -48,6 +48,7 @@ const appState: AppState = { isReplaying: false, streamingPhase: 'idle', streamingStartTime: 0, + locale: 'en', planMode: false, inputMode: 'prompt', swarmMode: false, diff --git a/apps/kimi-code/test/tui/components/chrome/welcome.test.ts b/apps/kimi-code/test/tui/components/chrome/welcome.test.ts index bc1b754fb6..8defadfcd1 100644 --- a/apps/kimi-code/test/tui/components/chrome/welcome.test.ts +++ b/apps/kimi-code/test/tui/components/chrome/welcome.test.ts @@ -25,6 +25,7 @@ const appState: AppState = { isReplaying: false, streamingPhase: 'idle', streamingStartTime: 0, + locale: 'en', planMode: false, inputMode: 'prompt', swarmMode: false, diff --git a/apps/kimi-code/test/tui/config.test.ts b/apps/kimi-code/test/tui/config.test.ts index c4c5035cdb..aeae5045fa 100644 --- a/apps/kimi-code/test/tui/config.test.ts +++ b/apps/kimi-code/test/tui/config.test.ts @@ -60,6 +60,7 @@ auto_install = false expect(config).toEqual({ theme: 'light', disablePasteBurst: false, + locale: 'en', editorCommand: 'code --wait', notifications: { enabled: false, condition: 'always' }, upgrade: { autoInstall: false }, @@ -84,6 +85,7 @@ command = " " expect(config).toEqual({ theme: 'auto', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, @@ -116,6 +118,7 @@ command = " " { theme: 'light', disablePasteBurst: false, + locale: 'en', editorCommand: 'vim', notifications: { enabled: false, condition: 'always' }, upgrade: { autoInstall: false }, @@ -126,6 +129,7 @@ command = " " expect(await loadTuiConfig(filePath)).toEqual({ theme: 'light', disablePasteBurst: false, + locale: 'en', editorCommand: 'vim', notifications: { enabled: false, condition: 'always' }, upgrade: { autoInstall: false }, @@ -138,6 +142,7 @@ command = " " { theme, disablePasteBurst: DEFAULT_TUI_CONFIG.disablePasteBurst, + locale: DEFAULT_TUI_CONFIG.locale, editorCommand: null, notifications: DEFAULT_TUI_CONFIG.notifications, upgrade: DEFAULT_TUI_CONFIG.upgrade, diff --git a/apps/kimi-code/test/tui/create-tui-state.test.ts b/apps/kimi-code/test/tui/create-tui-state.test.ts index 0899cf0702..24369c73a7 100644 --- a/apps/kimi-code/test/tui/create-tui-state.test.ts +++ b/apps/kimi-code/test/tui/create-tui-state.test.ts @@ -24,6 +24,7 @@ function fakeInitialAppState(): AppState { streamingStartTime: 0, theme: 'dark', version: '0.0.0-test', + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index a014a45860..4af2228dc4 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -129,6 +129,7 @@ function makeStartupInput(): KimiTUIStartupInput { tuiConfig: { theme: 'dark', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, @@ -1221,7 +1222,7 @@ command = "vim" driver.handleUserInput('/undo 10'); await vi.waitFor(() => { expect(stripSgr(renderTranscript(driver))).toContain( - 'Cannot undo 10 prompts; only 1 prompt can be undone in the active context.', + 'Cannot undo 10; only 1 can be undone in the active context.', ); }); diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index 79a36d70f2..3ace97d2c9 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -89,6 +89,7 @@ function makeStartupInput( tuiConfig: { theme: 'dark', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, diff --git a/apps/kimi-code/test/tui/message-replay.test.ts b/apps/kimi-code/test/tui/message-replay.test.ts index 5e4e11670f..f9535950f0 100644 --- a/apps/kimi-code/test/tui/message-replay.test.ts +++ b/apps/kimi-code/test/tui/message-replay.test.ts @@ -53,6 +53,7 @@ function makeStartupInput(): KimiTUIStartupInput { tuiConfig: { theme: 'dark', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, diff --git a/apps/kimi-code/test/tui/signal-handlers.test.ts b/apps/kimi-code/test/tui/signal-handlers.test.ts index 92a91e0633..1380690b1c 100644 --- a/apps/kimi-code/test/tui/signal-handlers.test.ts +++ b/apps/kimi-code/test/tui/signal-handlers.test.ts @@ -26,6 +26,7 @@ function makeStartupInput(): KimiTUIStartupInput { tuiConfig: { theme: 'dark', disablePasteBurst: false, + locale: 'en', editorCommand: null, notifications: { enabled: true, condition: 'unfocused' }, upgrade: { autoInstall: true }, diff --git a/apps/kimi-web/src/components/ServerAuthDialog.vue b/apps/kimi-web/src/components/ServerAuthDialog.vue index 44c008649b..bce6a965d8 100644 --- a/apps/kimi-web/src/components/ServerAuthDialog.vue +++ b/apps/kimi-web/src/components/ServerAuthDialog.vue @@ -6,10 +6,12 @@ the unified v2 dialog look. --> <script setup lang="ts"> import { nextTick, onMounted, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; import { setCredential } from '../api/daemon/serverAuth'; import Button from './ui/Button.vue'; import Input from './ui/Input.vue'; +const { t } = useI18n(); const credential = ref(''); const inputRef = ref<InstanceType<typeof Input> | null>(null); const submitting = ref(false); @@ -39,10 +41,9 @@ function onKeydown(e: KeyboardEvent): void { <div class="server-auth-overlay" role="dialog" aria-modal="true" aria-labelledby="server-auth-title"> <div class="server-auth-card"> <div class="server-auth-head"> - <h1 id="server-auth-title" class="server-auth-title">Server token required</h1> + <h1 id="server-auth-title" class="server-auth-title">{{ t('app.serverAuthTitle') }}</h1> <p class="server-auth-hint"> - This server is protected. Enter the bearer token printed when the server - started (or the password set via <code>KIMI_CODE_PASSWORD</code>). + {{ t('app.serverAuthHint') }} </p> </div> <div class="server-auth-body"> @@ -51,7 +52,7 @@ function onKeydown(e: KeyboardEvent): void { v-model="credential" type="password" autocomplete="current-password" - placeholder="Token" + :placeholder="t('app.serverAuthPlaceholder')" :disabled="submitting" @keydown="onKeydown" /> @@ -63,7 +64,7 @@ function onKeydown(e: KeyboardEvent): void { :loading="submitting" @click="submit" > - {{ submitting ? 'Connecting…' : 'Connect' }} + {{ submitting ? t('app.connecting') : t('app.serverAuthConnect') }} </Button> </div> </div> diff --git a/apps/kimi-web/src/components/chat/GoalStrip.vue b/apps/kimi-web/src/components/chat/GoalStrip.vue index 11dab6b78c..259a35655b 100644 --- a/apps/kimi-web/src/components/chat/GoalStrip.vue +++ b/apps/kimi-web/src/components/chat/GoalStrip.vue @@ -27,15 +27,6 @@ const tokenPct = computed(() => { return Math.max(0, Math.min(100, Math.round((props.goal.tokensUsed / budget) * 100))); }); -function goalStatusLabel(status: AppGoal['status']): string { - switch (status) { - case 'active': return t('status.goalStatusActive'); - case 'paused': return t('status.goalStatusPaused'); - case 'blocked': return t('status.goalStatusBlocked'); - case 'complete': return t('status.goalStatusComplete'); - } -} - function formatMs(ms: number): string { const sec = Math.max(0, Math.round(ms / 1000)); const min = Math.floor(sec / 60); @@ -51,13 +42,13 @@ function formatMs(ms: number): string { <Card class="goal-strip" :class="{ expanded }"> <template #head> <button class="goal-row" type="button" @click="expanded = !expanded"> - <span class="goal-kicker">{{ t('status.goalLabel') }}</span> + <span class="goal-kicker">{{ t('app.goalKicker') }}</span> <span class="goal-objective" :class="{ 'expanded-hidden': expanded }">{{ goal.objective }}</span> <Badge :variant="goal.status === 'active' ? 'success' : goal.status === 'blocked' ? 'danger' : goal.status === 'paused' ? 'warning' : 'neutral'" size="sm" class="goal-status" - >{{ goalStatusLabel(goal.status) }}</Badge> + >{{ goal.status }}</Badge> <span class="goal-progress" aria-hidden="true"> <span class="goal-progress-fill" :style="{ width: `${tokenPct}%` }"></span> </span> @@ -68,50 +59,39 @@ function formatMs(ms: number): string { <template v-if="expanded" #default> <div class="goal-full">{{ goal.objective }}</div> <div v-if="goal.completionCriterion" class="goal-criterion"> - <span>Done when</span> + <span>{{ t('app.goalDoneWhen') }}</span> <p>{{ goal.completionCriterion }}</p> </div> + <div class="goal-stats"> + <Badge variant="neutral" size="sm">{{ t('app.goalTurns', { count: goal.turnsUsed }) }}</Badge> + <Badge variant="neutral" size="sm">{{ t('app.goalTokens', { count: goal.tokensUsed.toLocaleString() }) }}</Badge> + <Badge variant="neutral" size="sm">{{ formatMs(goal.wallClockMs) }}</Badge> + <Badge v-if="goal.budget.tokenBudget !== null" variant="neutral" size="sm">{{ t('app.goalTokenBudget', { pct: tokenPct }) }}</Badge> + </div> </template> <template v-if="expanded" #foot> - <div class="goal-footer"> - <div class="goal-meta"> - <span>{{ goal.turnsUsed }} turns</span> - <span>{{ goal.tokensUsed.toLocaleString() }} tokens</span> - <span>{{ formatMs(goal.wallClockMs) }}</span> - <span v-if="goal.budget.tokenBudget !== null">{{ tokenPct }}% token budget</span> - </div> - <div class="goal-actions"> - <Button - v-if="goal.status === 'active'" - size="sm" - variant="secondary" - class="goal-action" - @click.stop="emit('controlGoal', 'pause')" - > - <Icon name="pause" size="md" /> - <span>{{ t('status.goalPause') }}</span> - </Button> - <Button - v-if="goal.status === 'paused' || goal.status === 'blocked'" - size="sm" - variant="primary" - class="goal-action" - @click.stop="emit('controlGoal', 'resume')" - > - <Icon name="play" size="md" /> - <span>{{ t('status.goalResume') }}</span> - </Button> - <Button - size="sm" - variant="danger-soft" - class="goal-action" - @click.stop="emit('controlGoal', 'cancel')" - > - <Icon name="close" size="md" /> - <span>{{ t('status.goalCancel') }}</span> - </Button> - </div> + <div class="goal-actions"> + <Button + v-if="goal.status !== 'paused'" + size="sm" + variant="secondary" + class="goal-action" + @click.stop="emit('controlGoal', 'pause')" + >{{ t('status.goalPause') }}</Button> + <Button + v-if="goal.status === 'paused'" + size="sm" + variant="primary" + class="goal-action" + @click.stop="emit('controlGoal', 'resume')" + >{{ t('status.goalResume') }}</Button> + <Button + size="sm" + variant="danger-soft" + class="goal-action" + @click.stop="emit('controlGoal', 'cancel')" + >{{ t('status.goalCancel') }}</Button> </div> </template> </Card> @@ -119,21 +99,8 @@ function formatMs(ms: number): string { <style scoped> .goal-strip { - --composer-send-size: 32px; - --composer-send-inset: var(--space-2); margin: var(--space-2) var(--space-4) 0; } -.goal-strip.ui-card { - border-radius: calc((var(--composer-send-size) / 2) + var(--composer-send-inset)); -} -.goal-strip :deep(.ui-card__foot) { - padding: var(--composer-send-inset); -} -.goal-strip :deep(.ui-card__head), -.goal-strip :deep(.ui-card__body), -.goal-strip :deep(.ui-card__foot) { - padding-left: calc((var(--composer-send-inset) + var(--composer-send-size)) / 2); -} /* When collapsed the body/foot slots are not rendered; collapse the (always- rendered) Card body and drop the head border so the strip is a single row. */ .goal-strip:not(.expanded) :deep(.ui-card__body) { display: none; } @@ -149,14 +116,13 @@ function formatMs(ms: number): string { background: transparent; color: var(--color-text); font: var(--text-base)/var(--leading-normal) var(--font-ui); - text-align: left; cursor: pointer; } .goal-kicker { flex: none; color: var(--color-success); - font: var(--text-base)/var(--leading-normal) var(--font-ui); - font-weight: var(--weight-semibold); + font: var(--weight-bold) var(--text-xs) var(--font-mono); + text-transform: uppercase; } .goal-objective { min-width: 0; @@ -166,7 +132,6 @@ function formatMs(ms: number): string { white-space: nowrap; color: var(--color-text); font-size: var(--text-base); - text-align: left; } .goal-objective.expanded-hidden { visibility: hidden; @@ -218,43 +183,25 @@ function formatMs(ms: number): string { font: var(--text-xs)/var(--leading-normal) var(--font-ui); text-transform: none; } -.goal-footer { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - width: 100%; - min-width: 0; -} -.goal-meta { - min-width: 0; +.goal-stats { display: flex; flex-wrap: wrap; gap: var(--space-2); + margin-top: var(--space-3); color: var(--color-text-muted); - font: 12px/var(--leading-normal) var(--font-ui); - font-weight: 450; - font-variant-numeric: tabular-nums; + font: var(--text-xs) var(--font-mono); } .goal-actions { display: flex; gap: var(--space-2); - justify-content: flex-end; - flex: none; + width: 100%; } .goal-action { - flex: none; + flex: 1; min-width: 0; - height: var(--composer-send-size); - border-radius: calc(var(--composer-send-size) / 2); - padding-inline: var(--space-4); -} -.goal-action :deep(.ui-button__content) { - gap: var(--space-1); } @media (max-width: 640px) { .goal-strip { - --composer-send-size: 36px; margin: var(--space-2) var(--space-3) 0; } .goal-progress { diff --git a/apps/kimi-web/src/components/ui/CommandBar.vue b/apps/kimi-web/src/components/ui/CommandBar.vue index 427381c735..f20f6a24b6 100644 --- a/apps/kimi-web/src/components/ui/CommandBar.vue +++ b/apps/kimi-web/src/components/ui/CommandBar.vue @@ -1,9 +1,11 @@ <!-- apps/kimi-web/src/components/ui/CommandBar.vue --> <!-- Design-system §03 Command Bar: primary action + mono command + copy. --> <script setup lang="ts"> +import { useI18n } from 'vue-i18n'; import IconButton from './IconButton.vue'; import Icon from './Icon.vue'; +const { t } = useI18n(); const props = defineProps<{ command: string }>(); async function copy() { @@ -20,7 +22,7 @@ async function copy() { <span class="ui-cmdbar__action"><slot /></span> <span class="ui-cmdbar__cmd"> <code class="ui-cmdbar__text">{{ command }}</code> - <IconButton size="sm" label="Copy" @click="copy"> + <IconButton size="sm" :label="t('app.copy')" @click="copy"> <Icon name="copy" size="md" /> </IconButton> </span> diff --git a/apps/kimi-web/src/components/ui/Dialog.vue b/apps/kimi-web/src/components/ui/Dialog.vue index a11153bce7..cab235219a 100644 --- a/apps/kimi-web/src/components/ui/Dialog.vue +++ b/apps/kimi-web/src/components/ui/Dialog.vue @@ -4,10 +4,13 @@ Includes focus trap, Esc-to-close, and optional overlay-click-to-close. --> <script setup lang="ts"> import { nextTick, onBeforeUnmount, ref, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; import { openDialogCount } from '../../composables/dialogStack'; import IconButton from './IconButton.vue'; import Icon from './Icon.vue'; +const { t } = useI18n(); + const props = withDefaults(defineProps<{ open: boolean; title?: string; @@ -149,7 +152,7 @@ onBeforeUnmount(() => { <div v-if="description" class="ui-dialog__desc">{{ description }}</div> </div> </slot> - <IconButton class="ui-dialog__close" size="sm" label="Close" @click="close"> + <IconButton class="ui-dialog__close" size="sm" :label="t('app.close')" @click="close"> <Icon name="close" size="md" /> </IconButton> </div> diff --git a/apps/kimi-web/src/components/ui/Sheet.vue b/apps/kimi-web/src/components/ui/Sheet.vue index f12f186dba..64c901d243 100644 --- a/apps/kimi-web/src/components/ui/Sheet.vue +++ b/apps/kimi-web/src/components/ui/Sheet.vue @@ -2,9 +2,11 @@ <!-- Design-system §03 Sheet / BottomSheet: mobile bottom panel (≤640px dialogs anchor here). Top radius xl + drag handle + xl shadow. --> <script setup lang="ts"> +import { useI18n } from 'vue-i18n'; import IconButton from './IconButton.vue'; import Icon from './Icon.vue'; +const { t } = useI18n(); defineProps<{ open: boolean; title?: string }>(); const emit = defineEmits<{ 'update:open': [value: boolean]; close: [] }>(); @@ -22,7 +24,7 @@ function close() { <div class="ui-sheet__handle" aria-hidden="true" /> <div v-if="title" class="ui-sheet__head"> <span class="ui-sheet__title">{{ title }}</span> - <IconButton size="sm" label="Close" @click="close"> + <IconButton size="sm" :label="t('app.close')" @click="close"> <Icon name="close" size="md" /> </IconButton> </div> diff --git a/apps/kimi-web/src/i18n/locales/en/app.ts b/apps/kimi-web/src/i18n/locales/en/app.ts index 77ac5140bd..fe21d60802 100644 --- a/apps/kimi-web/src/i18n/locales/en/app.ts +++ b/apps/kimi-web/src/i18n/locales/en/app.ts @@ -6,4 +6,15 @@ export default { authPageLogin: 'Sign in', connecting: 'Connecting…', internalBuildBanner: 'Internal testing only', + serverAuthTitle: 'Server token required', + serverAuthHint: 'This server is protected. Enter the bearer token printed when the server started (or the password set via KIMI_CODE_PASSWORD).', + serverAuthPlaceholder: 'Token', + serverAuthConnect: 'Connect', + close: 'Close', + copy: 'Copy', + goalKicker: 'Goal', + goalDoneWhen: 'Done when', + goalTurns: '{{count}} turns', + goalTokens: '{{count}} tokens', + goalTokenBudget: '{{pct}}% token budget', } as const; diff --git a/apps/kimi-web/src/i18n/locales/zh/app.ts b/apps/kimi-web/src/i18n/locales/zh/app.ts index 19e8edd068..40321f602d 100644 --- a/apps/kimi-web/src/i18n/locales/zh/app.ts +++ b/apps/kimi-web/src/i18n/locales/zh/app.ts @@ -6,4 +6,15 @@ export default { authPageLogin: '登录', connecting: '连接中…', internalBuildBanner: '仅供内部测试', + serverAuthTitle: '需要服务器令牌', + serverAuthHint: '此服务器受保护。请输入服务器启动时打印的 Bearer 令牌(或通过 KIMI_CODE_PASSWORD 设置的密码)。', + serverAuthPlaceholder: '令牌', + serverAuthConnect: '连接', + close: '关闭', + copy: '复制', + goalKicker: '目标', + goalDoneWhen: '完成条件', + goalTurns: '{{count}} 轮', + goalTokens: '{{count}} tokens', + goalTokenBudget: '{{pct}}% token 预算', } as const; diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index f35266ad53..a998d27700 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -66,6 +66,7 @@ const config = withMermaid(defineConfig({ { text: 'Model Context Protocol', link: '/zh/customization/mcp' }, { text: 'Agent Skills', link: '/zh/customization/skills' }, { text: 'Plugins', link: '/zh/customization/plugins' }, + { text: '数据源', link: '/zh/customization/datasource' }, { text: 'Agent 与子 Agent', link: '/zh/customization/agents' }, { text: 'Hooks', link: '/zh/customization/hooks' }, { text: '自定义主题', link: '/zh/customization/themes' }, @@ -143,6 +144,7 @@ const config = withMermaid(defineConfig({ { text: 'Model Context Protocol', link: '/en/customization/mcp' }, { text: 'Agent Skills', link: '/en/customization/skills' }, { text: 'Plugins', link: '/en/customization/plugins' }, + { text: 'Datasource', link: '/en/customization/datasource' }, { text: 'Agents and Subagents', link: '/en/customization/agents' }, { text: 'Hooks', link: '/en/customization/hooks' }, { text: 'Custom Themes', link: '/en/customization/themes' }, From 3ff39f89554d726eace631ec7b00d7761f06023e Mon Sep 17 00:00:00 2001 From: kimi <dev@kimi.local> Date: Sun, 12 Jul 2026 08:17:56 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(i18n):=20address=20code=20review=20?= =?UTF-8?q?=E2=80=94=20restore=20structured=20retry=20JSON,=20add=20missin?= =?UTF-8?q?g=20locale=20keys,=20fix=20GoalStrip=20blocked=20state,=20fix?= =?UTF-8?q?=20vue-i18n=20placeholder=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore writeRetrying with structured JSON meta for stream-json consumers; t() only for text output - Add missing i18n keys for prompts.ts and provider.ts (selectProviderToLogout, feedback*, catalog*, addProvider*, etc.) - Fix GoalStrip.vue: blocked goals now show Resume instead of Pause (paused || blocked → resume, active → pause) - Fix vue-i18n placeholder syntax: {{count}} → {count}, {{pct}} → {pct} --- apps/kimi-code/src/cli/goal-prompt.ts | 2 +- apps/kimi-code/src/cli/run-prompt.ts | 31 +++++++----- apps/kimi-code/src/cli/run-shell.ts | 2 +- apps/kimi-code/src/cli/sub/acp.ts | 4 +- apps/kimi-code/src/cli/sub/plugin-run-node.ts | 5 +- apps/kimi-code/src/cli/sub/server/run.ts | 6 +-- apps/kimi-code/src/i18n/locales/en.ts | 49 +++++++++++++++++++ apps/kimi-code/src/i18n/locales/zh.ts | 49 +++++++++++++++++++ .../src/components/chat/GoalStrip.vue | 4 +- apps/kimi-web/src/i18n/locales/en/app.ts | 6 +-- apps/kimi-web/src/i18n/locales/zh/app.ts | 6 +-- 11 files changed, 137 insertions(+), 27 deletions(-) diff --git a/apps/kimi-code/src/cli/goal-prompt.ts b/apps/kimi-code/src/cli/goal-prompt.ts index 57f223ad44..844f2c0a1b 100644 --- a/apps/kimi-code/src/cli/goal-prompt.ts +++ b/apps/kimi-code/src/cli/goal-prompt.ts @@ -96,7 +96,7 @@ export function goalSummaryJson(goal: GoalSnapshot | null): GoalSummary { } export function formatGoalSummaryText(goal: GoalSnapshot | null): string { - if (goal === null) return 'Goal: no goal found.'; + if (goal === null) return t('tui.statusMessages.goalNoGoalFound'); const parts = [`Goal [${goal.status}]`]; if (goal.terminalReason !== undefined) parts.push(goal.terminalReason); return `${parts.join(': ')} (turns: ${goal.turnsUsed}, tokens: ${goal.tokensUsed})`; diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index 7a53209c9c..dddeb65731 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -272,7 +272,7 @@ async function resolvePromptSession( const sessions = await harness.listSessions({ sessionId: opts.session, workDir }); const target = sessions[0]; if (target === undefined) { - throw new Error(`Session "${opts.session}" not found.`); + throw new Error(t('tui.statusMessages.sessionNotFound', { sessionId: opts.session ?? '' })); } if (resolve(target.workDir) !== resolve(workDir)) { stderr.write( @@ -379,7 +379,7 @@ function requireConfiguredModel(...models: readonly (string | undefined)[]): str const model = configuredModel(...models); if (model === undefined) { throw new Error( - 'No model configured. Run `kimi` and use /login to sign in, then retry; or set default_model in config.toml.', + t('tui.statusMessages.noModelPrompt'), ); } return model; @@ -502,9 +502,7 @@ function runPromptTurn( return; case 'turn.step.retrying': outputWriter.discardAssistant(); - outputWriter.writeStatus( - `Retrying (${event.nextAttempt}/${event.maxAttempts}) in ${Math.ceil(event.delayMs / 1000)}s — ${event.errorName}`, - ); + outputWriter.writeRetrying(event); return; case 'assistant.delta': outputWriter.writeAssistantDelta(event.delta); @@ -616,7 +614,7 @@ interface PromptTurnWriter { argumentsPart: string | undefined, ): void; writeToolResult(toolCallId: string, output: unknown): void; - writeStatus(message: string): void; + writeRetrying(event: Extract<Event, { type: 'turn.step.retrying' }>): void; flushAssistant(): void; discardAssistant(): void; finish(): void; @@ -625,12 +623,10 @@ interface PromptTurnWriter { class PromptTranscriptWriter implements PromptTurnWriter { private readonly assistantWriter: PromptBlockWriter; private readonly thinkingWriter: PromptBlockWriter; - private readonly stderr: PromptOutput; constructor(stdout: PromptOutput, stderr: PromptOutput) { this.assistantWriter = new PromptBlockWriter(stdout); this.thinkingWriter = new PromptBlockWriter(stderr); - this.stderr = stderr; } writeAssistantDelta(delta: string): void { @@ -790,9 +786,22 @@ class PromptJsonWriter implements PromptTurnWriter { }); } - writeStatus(message: string): void { - this.flushAssistant(); - this.writeJsonLine({ role: 'meta', type: 'status', content: message } as PromptJsonMetaMessage); + writeRetrying(event: Extract<Event, { type: 'turn.step.retrying' }>): void { + // Emit a machine-readable meta line so stream-json consumers can observe + // provider retries. The failed attempt's partial assistant text was already + // discarded by the caller, so no half-formed assistant message leaks. + const message: PromptJsonRetryMetaMessage = { + role: 'meta', + type: 'turn.step.retrying', + failed_attempt: event.failedAttempt, + next_attempt: event.nextAttempt, + max_attempts: event.maxAttempts, + delay_ms: event.delayMs, + error_name: event.errorName, + error_message: event.errorMessage, + status_code: event.statusCode, + }; + this.writeJsonLine(message); } flushAssistant(): void { diff --git a/apps/kimi-code/src/cli/run-shell.ts b/apps/kimi-code/src/cli/run-shell.ts index d326e68178..8321c14560 100644 --- a/apps/kimi-code/src/cli/run-shell.ts +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -202,7 +202,7 @@ export async function runShell( hints.push(`${gutter}${t('tui.statusMessages.shellResumeHint', { sessionId })}`); } if (tui.exitOpenUrl !== undefined) { - hints.push(`${gutter}open ${toTerminalHyperlink(tui.exitOpenUrl, tui.exitOpenUrl)}`); + hints.push(`${gutter}${t('tui.statusMessages.webOpenUrl', { url: toTerminalHyperlink(tui.exitOpenUrl, tui.exitOpenUrl) })}`); } if (hints.length > 0) { process.stderr.write(`\n${hints.join('\n')}\n`); diff --git a/apps/kimi-code/src/cli/sub/acp.ts b/apps/kimi-code/src/cli/sub/acp.ts index a98991464f..831b8507f1 100644 --- a/apps/kimi-code/src/cli/sub/acp.ts +++ b/apps/kimi-code/src/cli/sub/acp.ts @@ -31,6 +31,7 @@ import { createKimiHarness, type Session, type SkillSummary } from '@moonshot-ai import { KIMI_CODE_HOME_ENV } from '#/constant/app'; import { createKimiCodeHostIdentity, getVersion } from '#/cli/version'; +import { t } from '#/i18n'; import { buildSkillSlashCommands } from '#/tui/commands/skills'; import { runLoginFlow } from './login-flow'; @@ -119,7 +120,8 @@ export function registerAcpCommand(parent: Command): void { }); process.exit(0); } catch (err) { - process.stderr.write(`acp server: fatal error: ${String(err)}\n`); + process.stderr.write(t('tui.statusMessages.acpFatalError', { error: String(err) }) + ' +'); process.exit(1); } }); diff --git a/apps/kimi-code/src/cli/sub/plugin-run-node.ts b/apps/kimi-code/src/cli/sub/plugin-run-node.ts index 3926a04dc1..6baadfb25d 100644 --- a/apps/kimi-code/src/cli/sub/plugin-run-node.ts +++ b/apps/kimi-code/src/cli/sub/plugin-run-node.ts @@ -1,11 +1,12 @@ import { realpath } from 'node:fs/promises'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; +import { t } from '#/i18n'; export async function runPluginNodeEntry(entry: string, args: readonly string[]): Promise<void> { const pluginRoot = process.env['KIMI_PLUGIN_ROOT']; if (pluginRoot === undefined || pluginRoot.trim().length === 0) { - throw new Error('KIMI_PLUGIN_ROOT is required to run a plugin node entry.'); + throw new Error(t('tui.statusMessages.pluginRootRequired')); } const [rootReal, entryReal] = await Promise.all([ @@ -13,7 +14,7 @@ export async function runPluginNodeEntry(entry: string, args: readonly string[]) realpath(entry), ]); if (!isWithin(entryReal, rootReal)) { - throw new Error(`Plugin node entry must be inside KIMI_PLUGIN_ROOT: ${entry}`); + throw new Error(t('tui.statusMessages.pluginEntryOutsideRoot', { entry })); } process.argv = [process.argv[0] ?? process.execPath, entryReal, ...args]; diff --git a/apps/kimi-code/src/cli/sub/server/run.ts b/apps/kimi-code/src/cli/sub/server/run.ts index 70488623c8..3f50ce380e 100644 --- a/apps/kimi-code/src/cli/sub/server/run.ts +++ b/apps/kimi-code/src/cli/sub/server/run.ts @@ -528,7 +528,7 @@ function formatReadyBanner( } // On a loopback bind there is no network URL; show how to enable one. if (isLoopbackHost(host)) { - lines.push(` ${label('Network: ')}${muted('off')}${dim(' use --host to enable')}`); + lines.push(` ${label(t('tui.statusMessages.serverAccessNetwork'))}${muted(t('tui.statusMessages.serverNetworkOff'))}${dim(' ' + t('tui.statusMessages.serverNetworkUseHost'))}`); } if (opts.token !== undefined) { // Set the token off with surrounding whitespace rather than color, so it is @@ -539,8 +539,8 @@ function formatReadyBanner( } // Auxiliary controls last. - lines.push(` ${label('Logs: ')}${muted('off')}${dim(' use --log-level info to enable')}`); - lines.push(` ${label('Stop: ')}${muted('kimi server kill')}`); + lines.push(` ${label('Logs: ')}${muted(t('tui.statusMessages.serverLogsOff'))}${dim(' ' + t('tui.statusMessages.serverLogsUseLevel'))}`); + lines.push(` ${label('Stop: ')}${muted(t('tui.statusMessages.serverStopCmd'))}`); lines.push(''); return lines.join('\n'); } diff --git a/apps/kimi-code/src/i18n/locales/en.ts b/apps/kimi-code/src/i18n/locales/en.ts index f8af500334..2ba4dd6862 100644 --- a/apps/kimi-code/src/i18n/locales/en.ts +++ b/apps/kimi-code/src/i18n/locales/en.ts @@ -1155,8 +1155,57 @@ export default { shellNothingToMigrate: ' Nothing to migrate from ~/.kimi/.', shellBye: 'Bye!', shellResumeHint: 'To resume this session: kimi -r {{sessionId}}', + // goal-prompt.ts + goalNoGoalFound: 'Goal: no goal found.', + // run-prompt.ts + sessionNotFound: 'Session "{{sessionId}}" not found.', + noModelPrompt: 'No model configured. Run `kimi` and use /login to sign in, then retry; or set default_model in config.toml.', + // sub/server/run.ts + serverNetworkOff: 'off', + serverNetworkUseHost: 'use --host to enable', + serverLogsOff: 'off', + serverLogsUseLevel: 'use --log-level info to enable', + serverStopCmd: 'kimi server kill', + // sub/plugin-run-node.ts + pluginRootRequired: 'KIMI_PLUGIN_ROOT is required to run a plugin node entry.', + pluginEntryOutsideRoot: 'Plugin node entry must be inside KIMI_PLUGIN_ROOT: {{entry}}', + // sub/acp.ts + acpFatalError: 'acp server: fatal error: {{error}}', // main.ts mainError: 'error: {{message}}', + // commands/prompts.ts & provider.ts + selectProviderToLogout: 'Select a provider to log out', + feedbackNoAttachment: 'No attachment', + feedbackNoAttachmentDesc: 'Only send the feedback message.', + feedbackLogsOnly: 'Logs', + feedbackLogsOnlyDesc: 'Include recent logs.', + feedbackLogsAndCodebase: 'Logs + codebase', + feedbackLogsAndCodebaseDesc: 'Include recent logs and info about the active codebase.', + shareDiagnosticInfo: 'Share diagnostic info', + apiKeySavedTo: 'API key saved for {{provider}}.', + catalogNoSupportedProviders: 'No supported providers in the catalog.', + selectProviderTitle: 'Select a provider', + addProviderFailed: 'Failed to add provider: {error}', + removeProviderFailedGeneric: 'Failed to remove provider: {error}', + removeProviderFailed: 'Failed to remove provider {providerId}: {error}', + addProviderTitle: 'Add a provider', + knownThirdPartyProvider: 'Known third-party provider', + customRegistryOption: 'Custom registry', + fetchingCatalog: 'Fetching catalog from {url}...', + catalogLoaded: 'Catalog loaded.', + catalogAborted: 'Aborted.', + catalogFailedToLoad: 'Failed to load catalog.', + catalogFetchFailed: 'Failed to fetch catalog{hint}: {error}', + providerNoUsableModels: 'Provider "{providerId}" has no usable models in this catalog.', + providerUnsupportedWire: 'Provider "{providerId}" has an unsupported wire type in the catalog.', + providerAdded: 'Added {providerName} ({providerId}).', + setDefaultModelFailed: 'Failed to set default model: {error}', + defaultModelSet: 'Default model set to {alias} ({effort}).', + failedToImportRegistry: 'Failed to import registry: {error}', + failedToApplyRegistry: 'Failed to apply registry: {error}', + registryNoProviders: 'No providers in registry.', + importedOneProvider: 'Imported 1 provider.', + importedProviders: 'Imported {count} providers.', }, messages: { agentGroup: { diff --git a/apps/kimi-code/src/i18n/locales/zh.ts b/apps/kimi-code/src/i18n/locales/zh.ts index 523f0e892b..ca148401f0 100644 --- a/apps/kimi-code/src/i18n/locales/zh.ts +++ b/apps/kimi-code/src/i18n/locales/zh.ts @@ -1150,8 +1150,57 @@ export default { shellNothingToMigrate: ' 没有需要从 ~/.kimi/ 迁移的内容。', shellBye: '再见!', shellResumeHint: '恢复此会话:kimi -r {{sessionId}}', + // goal-prompt.ts + goalNoGoalFound: '目标:未找到目标。', + // run-prompt.ts + sessionNotFound: '会话 "{{sessionId}}" 未找到。', + noModelPrompt: '未配置模型。请运行 `kimi` 并使用 /login 登录后重试;或在 config.toml 中设置 default_model。', + // sub/server/run.ts + serverNetworkOff: '关闭', + serverNetworkUseHost: '使用 --host 开启', + serverLogsOff: '关闭', + serverLogsUseLevel: '使用 --log-level info 开启', + serverStopCmd: 'kimi server kill', + // sub/plugin-run-node.ts + pluginRootRequired: '运行插件节点入口需要设置 KIMI_PLUGIN_ROOT。', + pluginEntryOutsideRoot: '插件节点入口必须在 KIMI_PLUGIN_ROOT 内:{{entry}}', + // sub/acp.ts + acpFatalError: 'acp 服务器:致命错误:{{error}}', // main.ts mainError: '错误:{{message}}', + // commands/prompts.ts & provider.ts + selectProviderToLogout: '选择要退出的提供商', + feedbackNoAttachment: '无附件', + feedbackNoAttachmentDesc: '仅发送反馈消息。', + feedbackLogsOnly: '日志', + feedbackLogsOnlyDesc: '包含最近的日志。', + feedbackLogsAndCodebase: '日志 + 代码库', + feedbackLogsAndCodebaseDesc: '包含最近的日志和活跃代码库信息。', + shareDiagnosticInfo: '共享诊断信息', + apiKeySavedTo: 'API 密钥已保存至 {provider}。', + catalogNoSupportedProviders: '目录中没有受支持的提供商。', + selectProviderTitle: '选择提供商', + addProviderFailed: '添加提供商失败: {error}', + removeProviderFailedGeneric: '移除提供商失败: {error}', + removeProviderFailed: '移除提供商 {providerId} 失败: {error}', + addProviderTitle: '添加提供商', + knownThirdPartyProvider: '已知第三方提供商', + customRegistryOption: '自定义注册表', + fetchingCatalog: '正在从 {url} 获取目录...', + catalogLoaded: '目录已加载。', + catalogAborted: '已取消。', + catalogFailedToLoad: '加载目录失败。', + catalogFetchFailed: '获取目录失败{hint}: {error}', + providerNoUsableModels: '提供商 "{providerId}" 在此目录中没有可用模型。', + providerUnsupportedWire: '提供商 "{providerId}" 的传输类型不受支持。', + providerAdded: '已添加 {providerName} ({providerId})。', + setDefaultModelFailed: '设置默认模型失败: {error}', + defaultModelSet: '默认模型已设置为 {alias} ({effort})。', + failedToImportRegistry: '导入注册表失败: {error}', + failedToApplyRegistry: '应用注册表失败: {error}', + registryNoProviders: '注册表中无提供商。', + importedOneProvider: '已导入 1 个提供商。', + importedProviders: '已导入 {count} 个提供商。', }, messages: { agentGroup: { diff --git a/apps/kimi-web/src/components/chat/GoalStrip.vue b/apps/kimi-web/src/components/chat/GoalStrip.vue index 259a35655b..45adf14146 100644 --- a/apps/kimi-web/src/components/chat/GoalStrip.vue +++ b/apps/kimi-web/src/components/chat/GoalStrip.vue @@ -73,14 +73,14 @@ function formatMs(ms: number): string { <template v-if="expanded" #foot> <div class="goal-actions"> <Button - v-if="goal.status !== 'paused'" + v-if="goal.status === 'active'" size="sm" variant="secondary" class="goal-action" @click.stop="emit('controlGoal', 'pause')" >{{ t('status.goalPause') }}</Button> <Button - v-if="goal.status === 'paused'" + v-if="goal.status === 'paused' || goal.status === 'blocked'" size="sm" variant="primary" class="goal-action" diff --git a/apps/kimi-web/src/i18n/locales/en/app.ts b/apps/kimi-web/src/i18n/locales/en/app.ts index fe21d60802..908df5f179 100644 --- a/apps/kimi-web/src/i18n/locales/en/app.ts +++ b/apps/kimi-web/src/i18n/locales/en/app.ts @@ -14,7 +14,7 @@ export default { copy: 'Copy', goalKicker: 'Goal', goalDoneWhen: 'Done when', - goalTurns: '{{count}} turns', - goalTokens: '{{count}} tokens', - goalTokenBudget: '{{pct}}% token budget', + goalTurns: '{count} turns', + goalTokens: '{count} tokens', + goalTokenBudget: '{pct}% token budget', } as const; diff --git a/apps/kimi-web/src/i18n/locales/zh/app.ts b/apps/kimi-web/src/i18n/locales/zh/app.ts index 40321f602d..4b319bac18 100644 --- a/apps/kimi-web/src/i18n/locales/zh/app.ts +++ b/apps/kimi-web/src/i18n/locales/zh/app.ts @@ -14,7 +14,7 @@ export default { copy: '复制', goalKicker: '目标', goalDoneWhen: '完成条件', - goalTurns: '{{count}} 轮', - goalTokens: '{{count}} tokens', - goalTokenBudget: '{{pct}}% token 预算', + goalTurns: '{count} 轮', + goalTokens: '{count} tokens', + goalTokenBudget: '{pct}% token 预算', } as const;