Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tui-chinese-language-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add Simplified Chinese UI translations and a language selector. Run /language or open Settings to switch between English and Chinese.
1 change: 1 addition & 0 deletions apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"imports": {
"#/tui/theme": "./src/tui/theme/index.ts",
"#/tui/commands": "./src/tui/commands/index.ts",
"#/tui/i18n": "./src/tui/i18n/index.ts",
"#/cli/sub/web": "./src/cli/sub/web/index.ts",
"#/cli/sub/web/*": "./src/cli/sub/web/*.ts",
"#/generated/vis-web-asset": [
Expand Down
57 changes: 57 additions & 0 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {

import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
import { EffortSelectorComponent } from '../components/dialogs/effort-selector';
import { LanguageSelectorComponent } from '../components/dialogs/language-selector';
import {
ExperimentsSelectorComponent,
type ExperimentalFeatureDraftChange,
Expand All @@ -20,6 +21,8 @@ import { SettingsSelectorComponent, type SettingsSelection } from '../components
import { ThemeSelectorComponent } from '../components/dialogs/theme-selector';
import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector';
import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config';
import { isLanguage, type Language } from '#/tui/i18n';
import { t } from '#/tui/i18n';
import type { ThemeName } from '#/tui/theme';
import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme';
import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
Expand Down Expand Up @@ -53,6 +56,7 @@ function hasConversationHistory(host: SlashCommandHost): boolean {
function currentTuiConfig(host: SlashCommandHost): TuiConfig {
return {
theme: host.state.appState.theme,
language: host.state.appState.language,
editorCommand: host.state.appState.editorCommand,
disablePasteBurst: host.state.appState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
notifications: host.state.appState.notifications,
Expand Down Expand Up @@ -236,6 +240,19 @@ export async function handleThemeCommand(host: SlashCommandHost, args: string):
await applyThemeChoice(host, theme);
}

export async function handleLanguageCommand(host: SlashCommandHost, args: string): Promise<void> {
const language = args.trim();
if (language.length === 0) {
showLanguagePicker(host);
return;
}
if (!isLanguage(language)) {
host.showError(`Unknown language: ${language}`);
return;
}
await applyLanguageChoice(host, language);
}

export async function handleModelCommand(host: SlashCommandHost, args: string): Promise<void> {
const alias = args.trim();
await refreshModelsForPicker(host);
Expand Down Expand Up @@ -609,6 +626,45 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi
host.showStatus(`Theme set to "${theme}"${detail}.`);
}

function showLanguagePicker(host: SlashCommandHost): void {
host.mountEditorReplacement(
new LanguageSelectorComponent({
currentValue: host.state.appState.language,
onSelect: (value) => {
host.restoreEditor();
void applyLanguageChoice(host, value);
},
onCancel: () => {
host.restoreEditor();
},
}),
);
}

async function applyLanguageChoice(host: SlashCommandHost, language: Language): Promise<void> {
if (language === host.state.appState.language) {
host.showStatus(`Language already set to "${language}".`);
return;
}

try {
await saveTuiConfig({
...currentTuiConfig(host),
language,
});
} catch (error) {
host.showStatus(
`Failed to save language: ${formatErrorMessage(error)}`,
'error',
);
return;
}

host.applyLanguage(language);
host.track('language_switch', { language });
host.showStatus(`Language set to "${language}".`);
}

export function showPermissionPicker(host: SlashCommandHost): void {
host.mountEditorReplacement(
new PermissionSelectorComponent({
Expand Down Expand Up @@ -782,6 +838,7 @@ function handleSettingsSelection(host: SlashCommandHost, value: SettingsSelectio
case 'model': showModelPicker(host); return;
case 'permission': showPermissionPicker(host); return;
case 'theme': showThemePicker(host); return;
case 'language': showLanguagePicker(host); return;
case 'editor': showEditorPicker(host); return;
case 'experiments': void showExperimentsPanel(host); return;
case 'upgrade': showUpdatePreferencePicker(host); return;
Expand Down
9 changes: 9 additions & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DeviceAuthorization } from '@moonshot-ai/kimi-code-oauth';
import type { KimiHarness, Session } from '@moonshot-ai/kimi-code-sdk';

import type { ColorToken, ThemeName } from '#/tui/theme';
import type { Language } from '#/tui/i18n';

import { LLM_NOT_SET_MESSAGE } from '../constant/kimi-tui';
import type { AuthFlowController } from '../controllers/auth-flow';
Expand All @@ -27,6 +28,7 @@ import {
handleCompactCommand,
handleEditorCommand,
handleEffortCommand,
handleLanguageCommand,
handleModelCommand,
handlePlanCommand,
handleThemeCommand,
Expand Down Expand Up @@ -69,6 +71,7 @@ export {
handleCompactCommand,
handleEditorCommand,
handleEffortCommand,
handleLanguageCommand,
handleModelCommand,
handlePlanCommand,
handleThemeCommand,
Expand Down Expand Up @@ -134,6 +137,9 @@ export interface SlashCommandHost {
applyTheme(theme: ThemeName, resolved?: ResolvedTheme): Promise<void>;
refreshTerminalThemeTracking(): void;

// Language
applyLanguage(language: Language): void;

// Dispatch
stop(exitCode?: number): Promise<void>;
setExitOpenUrl(url: string): void;
Expand Down Expand Up @@ -300,6 +306,9 @@ async function handleBuiltInSlashCommand(
case 'theme':
await handleThemeCommand(host, args);
return;
case 'language':
await handleLanguageCommand(host, args);
return;
case 'model':
await handleModelCommand(host, args);
return;
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { handleCopyCommand } from './copy';
export {
handleCompactCommand,
handleEditorCommand,
handleLanguageCommand,
handleModelCommand,
handlePlanCommand,
handleThemeCommand,
Expand Down
7 changes: 7 additions & 0 deletions apps/kimi-code/src/tui/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,13 @@ export const BUILTIN_SLASH_COMMANDS = [
priority: 60,
availability: 'always',
},
{
name: 'language',
aliases: ['lang'],
description: 'Set the TUI display language',
priority: 60,
availability: 'always',
},
{
name: 'logout',
aliases: ['disconnect'],
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/commands/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function applyReloadedTuiConfig(
notifications: config.notifications,
upgrade: config.upgrade,
});
host.applyLanguage(config.language);
host.state.editor.setDisablePasteBurst(config.disablePasteBurst);
}

Expand Down
5 changes: 3 additions & 2 deletions apps/kimi-code/src/tui/components/chrome/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
usagePercent,
usagePercentFromRatio,
} from '#/utils/usage/usage-format';
import { t } from '#/tui/i18n';

const MAX_CWD_SEGMENTS = 3;
const GOAL_TIMER_INTERVAL_MS = 1_000;
Expand Down Expand Up @@ -167,9 +168,9 @@ function shortenCwd(path: string): string {
function formatContextStatus(usage: number, tokens?: number, maxTokens?: number): string {
if (maxTokens !== undefined && maxTokens > 0 && tokens !== undefined) {
const pct = String(usagePercent(tokens, maxTokens));
return `context: ${pct}% (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
return `${t('footer.context', { pct })} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
}
return `context: ${String(usagePercentFromRatio(usage))}%`;
return `${t('footer.context', { pct: String(usagePercentFromRatio(usage)) })}`;
}

export function formatFooterGitBadge(status: GitStatus, colors: ColorPalette): string {
Expand Down
27 changes: 14 additions & 13 deletions apps/kimi-code/src/tui/components/chrome/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import chalk from 'chalk';
import { effectiveModelAlias } from '@moonshot-ai/kimi-code-sdk';

import { isRainbowDancing, renderDanceWelcomeHeader } from '#/tui/easter-eggs/dance';
import { t } from '#/tui/i18n';
import type { AppState } from '#/tui/types';
import { currentTheme } from '#/tui/theme';

Expand All @@ -30,14 +31,14 @@ export class WelcomeComponent implements Component {
const effectiveActiveModel = activeModel === undefined ? undefined : effectiveModelAlias(activeModel);

if (safeWidth < 24) {
const title = chalk.bold.hex(currentTheme.palette.primary)('Welcome to Kimi Code!');
const title = chalk.bold.hex(currentTheme.palette.primary)(t('welcome.title'));
const prompt = isLoggedOut
? chalk.hex(currentTheme.palette.warning)('Run /login or /provider to get started.')
: chalk.hex(currentTheme.palette.textDim)('Send /help for help information.');
? chalk.hex(currentTheme.palette.warning)(t('welcome.loggedOutHint'))
: chalk.hex(currentTheme.palette.textDim)(t('welcome.loggedInHint'));
const model = isLoggedOut
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
? chalk.hex(currentTheme.palette.warning)(t('welcome.notSet'))
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);
return ['', title, prompt, `Model: ${model}`].map((line) =>
return ['', title, prompt, `${t('welcome.model')} ${model}`].map((line) =>
truncateToWidth(line, safeWidth, '…'),
);
}
Expand All @@ -52,14 +53,14 @@ export class WelcomeComponent implements Component {
const textWidth = Math.max(4, innerWidth - logoWidth - gap.length);

const rightRow0 = truncateToWidth(
chalk.bold.hex(currentTheme.palette.primary)('Welcome to Kimi Code!'),
chalk.bold.hex(currentTheme.palette.primary)(t('welcome.title')),
textWidth,
'…',
);
const dim = chalk.hex(currentTheme.palette.textDim);
const labelStyle = chalk.bold.hex(currentTheme.palette.textDim);
const rightRow1 = truncateToWidth(
dim(isLoggedOut ? 'Run /login or /provider to get started.' : 'Send /help for help information.'),
dim(isLoggedOut ? t('welcome.loggedOutHint') : t('welcome.loggedInHint')),
textWidth,
'…',
);
Expand All @@ -73,18 +74,18 @@ export class WelcomeComponent implements Component {
}

const modelValue = isLoggedOut
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
? chalk.hex(currentTheme.palette.warning)(t('welcome.notSet'))
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);

const infoLines = [
labelStyle('Directory: ') + this.state.workDir,
labelStyle('Session: ') + this.state.sessionId,
labelStyle('Model: ') + modelValue,
labelStyle('Version: ') + this.state.version,
labelStyle(t('welcome.directory')) + ' ' + this.state.workDir,
labelStyle(t('welcome.session')) + ' ' + this.state.sessionId,
labelStyle(t('welcome.model')) + ' ' + modelValue,
labelStyle(t('welcome.version')) + ' ' + this.state.version,
];

if (this.state.mcpServersSummary) {
infoLines.push(labelStyle('MCP: ') + this.state.mcpServersSummary);
infoLines.push(labelStyle(t('welcome.mcp')) + ' ' + this.state.mcpServersSummary);
}

const contentLines: string[] = [...renderedHeaderLines, '', ...infoLines];
Expand Down
31 changes: 31 additions & 0 deletions apps/kimi-code/src/tui/components/dialogs/language-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ChoicePickerComponent, type ChoiceOption } from './choice-picker';

import type { Language } from '#/tui/i18n';
import { t } from '#/tui/i18n';

function languageOptions(): readonly ChoiceOption[] {
return [
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文' },
];
}

export interface LanguageSelectorOptions {
readonly currentValue: Language;
readonly onSelect: (language: Language) => void;
readonly onCancel: () => void;
}

export class LanguageSelectorComponent extends ChoicePickerComponent {
constructor(opts: LanguageSelectorOptions) {
super({
title: t('language.label'),
options: [...languageOptions()],
currentValue: opts.currentValue,
onSelect: (value) => {
opts.onSelect(value as Language);
},
onCancel: opts.onCancel,
});
}
}
Loading