diff --git a/extensions/vscode/__mocks__/vscode.js b/extensions/vscode/__mocks__/vscode.js
new file mode 100644
index 0000000..2d64845
--- /dev/null
+++ b/extensions/vscode/__mocks__/vscode.js
@@ -0,0 +1,19 @@
+module.exports = {
+ env: { language: 'en' },
+ Uri: { file: (p) => ({ fsPath: p, scheme: 'file' }), joinPath: (...args) => args.join('/') },
+ workspace: { workspaceFolders: [{ uri: { fsPath: '/mock' } }], openTextDocument: () => Promise.resolve({ lineCount: 10, lineAt: () => ({ text: '' }) }) },
+ window: { showErrorMessage: () => {}, showWarningMessage: () => Promise.resolve(undefined), createOutputChannel: () => ({ appendLine: () => {}, dispose: () => {} }), createWebviewPanel: () => ({ webview: { html: '', onDidReceiveMessage: () => ({ dispose: () => {} }), asWebviewUri: (u) => u }, onDidDispose: () => ({ dispose: () => {} }), reveal: () => {}, dispose: () => {} }) },
+ commands: { registerCommand: () => ({ dispose: () => {} }), executeCommand: () => Promise.resolve() },
+ extensions: { getExtension: () => null },
+ Disposable: { from: (...ds) => ({ dispose: () => ds.forEach((d) => d.dispose()) }) },
+ CommentMode: { Preview: 1 },
+ CommentThreadCollapsibleState: { Expanded: 0 },
+ MarkdownString: function (v) { this.value = v; this.isTrusted = false; },
+ Range: function (a, b, c, d) { this.start = { line: a, character: b }; this.end = { line: c, character: d }; },
+ WorkspaceEdit: function () { this.replace = () => {}; this.delete = () => {}; },
+ ThemeIcon: function () {},
+ ViewColumn: { One: 1 },
+ EventEmitter: function () { this.event = () => {}; this.fire = () => {}; },
+ CommentController: function () { this.createCommentThread = () => ({ canReply: false, label: '', collapsibleState: 0, contextValue: '', comments: [], dispose: () => {} }); },
+ WebviewViewProvider: function () {},
+};
\ No newline at end of file
diff --git a/extensions/vscode/open-code-review-vscode-0.1.0.vsix b/extensions/vscode/open-code-review-vscode-0.1.0.vsix
index 5f008a1..428b077 100644
Binary files a/extensions/vscode/open-code-review-vscode-0.1.0.vsix and b/extensions/vscode/open-code-review-vscode-0.1.0.vsix differ
diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json
index 9005778..6d8ebd5 100644
--- a/extensions/vscode/package.json
+++ b/extensions/vscode/package.json
@@ -1,7 +1,7 @@
{
"name": "open-code-review-vscode",
"displayName": "Open Code Review",
- "description": "AI 代码审查 —— 基于 open-code-review CLI",
+ "description": "%ocr.description%",
"version": "0.1.0",
"publisher": "open-code-review",
"license": "Apache-2.0",
@@ -25,7 +25,7 @@
"activitybar": [
{
"id": "ocr-container",
- "title": "Open Code Review",
+ "title": "%ocr.activitybar.title%",
"icon": "resources/icon.svg"
}
]
@@ -35,34 +35,34 @@
{
"id": "ocr.sidebar",
"type": "webview",
- "name": "Code Review"
+ "name": "%ocr.sidebar.name%"
}
]
},
"commands": [
{
"command": "ocr.review.start",
- "title": "OCR: 开始代码审查"
+ "title": "%ocr.review.start%"
},
{
"command": "ocr.review.cancel",
- "title": "OCR: 取消审查"
+ "title": "%ocr.review.cancel%"
},
{
"command": "ocr.config.open",
- "title": "OCR: 打开配置"
+ "title": "%ocr.config.open%"
},
{
"command": "ocr.comment.apply",
- "title": "应用"
+ "title": "%ocr.comment.apply%"
},
{
"command": "ocr.comment.discard",
- "title": "忽略"
+ "title": "%ocr.comment.discard%"
},
{
"command": "ocr.comment.falsePositive",
- "title": "误报"
+ "title": "%ocr.comment.falsePositive%"
}
],
"menus": {
diff --git a/extensions/vscode/package.nls.json b/extensions/vscode/package.nls.json
new file mode 100644
index 0000000..94c0761
--- /dev/null
+++ b/extensions/vscode/package.nls.json
@@ -0,0 +1,11 @@
+{
+ "ocr.activitybar.title": "Open Code Review",
+ "ocr.sidebar.name": "Code Review",
+ "ocr.review.start": "OCR: Start Code Review",
+ "ocr.review.cancel": "OCR: Cancel Review",
+ "ocr.config.open": "OCR: Open Configuration",
+ "ocr.comment.apply": "Apply",
+ "ocr.comment.discard": "Discard",
+ "ocr.comment.falsePositive": "False Positive",
+ "ocr.description": "AI Code Review — powered by open-code-review CLI"
+}
\ No newline at end of file
diff --git a/extensions/vscode/package.nls.zh-cn.json b/extensions/vscode/package.nls.zh-cn.json
new file mode 100644
index 0000000..33766f7
--- /dev/null
+++ b/extensions/vscode/package.nls.zh-cn.json
@@ -0,0 +1,11 @@
+{
+ "ocr.activitybar.title": "Open Code Review",
+ "ocr.sidebar.name": "Code Review",
+ "ocr.review.start": "OCR: 开始代码审查",
+ "ocr.review.cancel": "OCR: 取消审查",
+ "ocr.config.open": "OCR: 打开配置",
+ "ocr.comment.apply": "应用",
+ "ocr.comment.discard": "忽略",
+ "ocr.comment.falsePositive": "误报",
+ "ocr.description": "AI 代码审查 —— 基于 open-code-review CLI"
+}
\ No newline at end of file
diff --git a/extensions/vscode/src/extension/providers/CommentProvider.ts b/extensions/vscode/src/extension/providers/CommentProvider.ts
index bea8f30..02d1dba 100644
--- a/extensions/vscode/src/extension/providers/CommentProvider.ts
+++ b/extensions/vscode/src/extension/providers/CommentProvider.ts
@@ -1,3 +1,4 @@
+import { t, resolveLocale, SupportedLocale } from '../../shared/i18n';
import * as vscode from 'vscode';
import { ReviewComment, CommentStatus, CommentSyncState } from '../../shared/types';
import { COMMENT_CONTROLLER_ID } from '../../shared/constants';
@@ -13,8 +14,11 @@ export class CommentProvider {
private offsets = new LineOffsetTracker();
private syncListeners: Array<(s: CommentSyncState[]) => void> = [];
+ private locale: SupportedLocale;
+
constructor(private extensionUri: vscode.Uri) {
- this.controller = vscode.comments.createCommentController(COMMENT_CONTROLLER_ID, 'Open Code Review');
+ this.locale = resolveLocale(vscode.env.language);
+ this.controller = vscode.comments.createCommentController(COMMENT_CONTROLLER_ID, t(this.locale, 'ext.commentController'));
}
onSync(fn: (s: CommentSyncState[]) => void): void {
@@ -58,10 +62,10 @@ export class CommentProvider {
const body = this.renderBody(c, i, 'pending');
const thread = this.controller.createCommentThread(doc.uri, range, [{
body, mode: vscode.CommentMode.Preview,
- author: { name: '⏳ [未处理]' },
+ author: { name: t(this.locale, 'ext.comment.pending') },
}]);
thread.canReply = false;
- thread.label = `Code Review (${i + 1} / ${this.comments.length})`;
+ thread.label = `${t(this.locale, 'ext.comment.threadLabel')} (${i + 1} / ${this.comments.length})`;
// 有代码建议 → 'pending'(显示应用+忽略);无建议 → 'pendingNoSuggestion'(仅忽略)
thread.contextValue = this.hasSuggestion(c) ? 'pending' : 'pendingNoSuggestion';
thread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
@@ -82,7 +86,7 @@ export class CommentProvider {
if (this.hasSuggestion(c)) {
md += `\n***\n\`\`\`diff\n${c.suggestionCode}\n\`\`\``;
} else {
- md += `\n***\n_💡 无代码建议,请手动处理_`;
+ md += `\n***\n${t(this.locale, 'ext.comment.noSuggestion')}`;
}
const s = new vscode.MarkdownString(md);
s.isTrusted = true;
@@ -100,7 +104,7 @@ export class CommentProvider {
const start = Math.max(0, this.offsets.adjusted(c.path, c.startLine) - 1);
const end = Math.min(doc.lineCount - 1, this.offsets.adjusted(c.path, c.endLine) - 1);
if (end < start) {
- vscode.window.showErrorMessage('应用失败:代码位置已失效,请刷新后重试。');
+ vscode.window.showErrorMessage(t(this.locale, 'ext.comment.applyFailedStale'));
return;
}
const range = new vscode.Range(start, 0, end, doc.lineAt(end).text.length);
@@ -113,7 +117,7 @@ export class CommentProvider {
else edit.delete(uri, range);
const ok = await vscode.workspace.applyEdit(edit);
if (!ok) {
- vscode.window.showErrorMessage('应用失败:无法修改文件,请检查文件是否被占用或处于只读状态。');
+ vscode.window.showErrorMessage(t(this.locale, 'ext.comment.applyFailedLocked'));
return;
}
await doc.save();
@@ -129,7 +133,12 @@ export class CommentProvider {
this.status.set(index, status);
const thread = this.threads.get(index);
if (thread) {
- const label = { applied: '✅ [已应用]', discarded: '✅ [已忽略]', falsePositive: '✅ [已误报]', pending: '⏳ [未处理]' }[status];
+ const label = {
+ applied: t(this.locale, 'ext.comment.statusApplied'),
+ discarded: t(this.locale, 'ext.comment.statusDiscarded'),
+ falsePositive: t(this.locale, 'ext.comment.statusFalsePositive'),
+ pending: t(this.locale, 'ext.comment.pending'),
+ }[status];
thread.comments = [{ ...thread.comments[0], author: { name: label }, body: this.renderBody(this.comments[index], index, status) }] as any;
thread.contextValue = status;
thread.collapsibleState = vscode.CommentThreadCollapsibleState.Collapsed;
@@ -141,7 +150,7 @@ export class CommentProvider {
const thread = this.threads.get(index);
if (!thread) {
const c = this.comments[index];
- if (c) vscode.window.showWarningMessage(`无法定位到 ${c.path}:该路径不是可打开的文件。`);
+ if (c) vscode.window.showWarningMessage(`${t(this.locale, 'ext.comment.jumpFailed')}${c.path}${t(this.locale, 'ext.comment.jumpNotAFile')}`);
return;
}
await vscode.window.showTextDocument(thread.uri, { selection: thread.range, preview: false });
diff --git a/extensions/vscode/src/extension/providers/ConfigPanelProvider.ts b/extensions/vscode/src/extension/providers/ConfigPanelProvider.ts
index 2be41d8..03dc13d 100644
--- a/extensions/vscode/src/extension/providers/ConfigPanelProvider.ts
+++ b/extensions/vscode/src/extension/providers/ConfigPanelProvider.ts
@@ -1,3 +1,4 @@
+import { resolveLocale, t, toHtmlLang } from '../../shared/i18n';
import * as vscode from 'vscode';
import { ConfigPanelFocus, isConfigReady } from '../../shared/configUtils';
import { ConfigPanelHostToWebview, WebviewToHost } from '../../shared/messages';
@@ -30,7 +31,7 @@ export class ConfigPanelProvider implements vscode.Disposable {
this.panel = vscode.window.createWebviewPanel(
PANEL_VIEW_TYPE,
- '模型配置',
+ t(resolveLocale(vscode.env.language), 'ext.configPanelTitle'),
vscode.ViewColumn.One,
{ enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [this.extensionUri] },
);
@@ -80,12 +81,14 @@ export class ConfigPanelProvider implements vscode.Disposable {
const config = this.config.read();
const cached = this.cli.getCachedEnvironment();
const skipEnvCheck = focus?.step === 2 || isConfigReady(config);
+ const locale = resolveLocale(vscode.env.language);
this.post({
type: 'configPanelInit',
config,
focus: focus ?? null,
env: cached,
skipEnvCheck,
+ locale,
});
break;
}
@@ -106,12 +109,13 @@ export class ConfigPanelProvider implements vscode.Disposable {
break;
}
case 'deleteCustomProvider': {
+ const locale = resolveLocale(vscode.env.language);
const confirmed = await vscode.window.showWarningMessage(
- `确定删除自定义 Provider「${msg.name}」?`,
+ t(locale, 'ext.deleteProviderConfirm').replace('{name}', msg.name),
{ modal: true },
- '删除',
+ t(locale, 'ext.deleteProviderConfirmBtn'),
);
- if (confirmed !== '删除') break;
+ if (confirmed !== t(locale, 'ext.deleteProviderConfirmBtn')) break;
this.notifyConfig(this.config.deleteCustomProvider(msg.name));
break;
}
@@ -144,8 +148,10 @@ export class ConfigPanelProvider implements vscode.Disposable {
private html(webview: vscode.Webview): string {
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'out', 'configPanel.js'));
const nonce = String(Date.now());
+ const resolved = resolveLocale(vscode.env.language);
+ const lang = toHtmlLang(resolved);
return `
-
+
diff --git a/extensions/vscode/src/extension/providers/SidebarProvider.ts b/extensions/vscode/src/extension/providers/SidebarProvider.ts
index f17de1d..f63ff58 100644
--- a/extensions/vscode/src/extension/providers/SidebarProvider.ts
+++ b/extensions/vscode/src/extension/providers/SidebarProvider.ts
@@ -1,3 +1,4 @@
+import { resolveLocale, toHtmlLang } from '../../shared/i18n';
import * as vscode from 'vscode';
import { ConfigPanelFocus } from '../../shared/configUtils';
import { HostToWebview, WebviewToHost } from '../../shared/messages';
@@ -48,7 +49,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
case 'ready': {
const config = this.config.read();
const gitState = await this.git.getState('workspace');
- this.post({ type: 'init', config, gitState });
+ const locale = resolveLocale(vscode.env.language);
+ this.post({ type: 'init', config, gitState, locale });
break;
}
case 'getGitState': {
@@ -108,8 +110,10 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
private html(webview: vscode.Webview): string {
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'out', 'webview.js'));
const nonce = String(Date.now());
+ const resolved = resolveLocale(vscode.env.language);
+ const lang = toHtmlLang(resolved);
return `
-
+
diff --git a/extensions/vscode/src/extension/services/CliService.ts b/extensions/vscode/src/extension/services/CliService.ts
index 291385f..8de2994 100644
--- a/extensions/vscode/src/extension/services/CliService.ts
+++ b/extensions/vscode/src/extension/services/CliService.ts
@@ -1,3 +1,5 @@
+import { t, resolveLocale } from '../../shared/i18n';
+import * as vscode from 'vscode';
import { spawn } from 'child_process';
import { CliResult, CliRunOptions, EnvCheckResult, LogLine } from '../../shared/types';
import { buildReviewArgs, extractCliError, parseCliResult, parseLogLine } from './cliParse';
@@ -90,7 +92,8 @@ export class CliService {
proc.on('error', (err) => { onLog({ text: String(err), level: 'error' }); resolve(false); });
proc.on('close', (code) => {
emitLines('', 'info', true);
- onLog({ text: code === 0 ? '✓ 安装完成' : `✗ 安装失败 (exit ${code})`, level: code === 0 ? 'info' : 'error' });
+ const locale = resolveLocale(vscode.env.language);
+ onLog({ text: code === 0 ? t(locale, 'ext.cli.installOk') : `${t(locale, 'ext.cli.installFail')}${code})`, level: code === 0 ? 'info' : 'error' });
if (code === 0) this.invalidateEnvironmentCache();
resolve(code === 0);
});
diff --git a/extensions/vscode/src/extension/services/GitService.ts b/extensions/vscode/src/extension/services/GitService.ts
index 2751035..a3b29f0 100644
--- a/extensions/vscode/src/extension/services/GitService.ts
+++ b/extensions/vscode/src/extension/services/GitService.ts
@@ -1,3 +1,4 @@
+import { t, resolveLocale } from '../../shared/i18n';
import * as vscode from 'vscode';
import { execFile } from 'child_process';
import { GitState, CommitInfo, FileChange, ReviewMode } from '../../shared/types';
@@ -167,7 +168,7 @@ export class GitService {
if (opts.mode === 'workspace') {
left = api.toGitUri(fileUri, opts.status === 'added' ? emptyRef : 'HEAD');
right = opts.status === 'deleted' ? api.toGitUri(fileUri, emptyRef) : fileUri;
- label = '工作区 ↔ HEAD';
+ label = t(resolveLocale(vscode.env.language), 'ext.git.workspaceVsHead');
} else if (opts.mode === 'commit' && opts.commit) {
left = api.toGitUri(fileUri, opts.status === 'added' ? emptyRef : `${opts.commit}^`);
right = opts.status === 'deleted' ? api.toGitUri(fileUri, emptyRef) : api.toGitUri(fileUri, opts.commit);
@@ -211,11 +212,13 @@ function runGit(cwd: string, args: string[]): Promise {
function formatRelative(date?: Date): string {
if (!date) return '';
+ const locale = resolveLocale(vscode.env.language);
const diff = Date.now() - date.getTime();
const h = Math.floor(diff / 3.6e6);
- if (h < 1) return '刚刚';
- if (h < 24) return `${h} 小时前`;
+ if (h < 1) return t(locale, 'ext.git.justNow');
+ if (h === 1) return t(locale, 'ext.git.hourAgo');
+ if (h < 24) return t(locale, 'ext.git.hoursAgo').replace('{h}', String(h));
const d = Math.floor(h / 24);
- if (d === 1) return '昨天';
- return `${d} 天前`;
+ if (d === 1) return t(locale, 'ext.git.yesterday');
+ return t(locale, 'ext.git.daysAgo').replace('{d}', String(d));
}
diff --git a/extensions/vscode/src/extension/services/shellEnv.ts b/extensions/vscode/src/extension/services/shellEnv.ts
index 5bef761..7fb4b58 100644
--- a/extensions/vscode/src/extension/services/shellEnv.ts
+++ b/extensions/vscode/src/extension/services/shellEnv.ts
@@ -57,7 +57,7 @@ export function resolveBin(name: string): string {
let resolved = name;
try {
const shell = process.env.SHELL || '/bin/zsh';
- if (!/^[a-zA-Z0-9._\/-]+$/.test(name)) return name;
+ if (!/^[a-zA-Z0-9._/-]+$/.test(name)) return name;
const res = spawnSync(shell, ['-ilc', `command -v '${name.replace(/'/g, "'\\''")}'`], {
encoding: 'utf8',
timeout: 5000,
diff --git a/extensions/vscode/src/shared/i18n.ts b/extensions/vscode/src/shared/i18n.ts
new file mode 100644
index 0000000..346af78
--- /dev/null
+++ b/extensions/vscode/src/shared/i18n.ts
@@ -0,0 +1,332 @@
+/**
+ * Supported display locales for the extension UI.
+ * Add new entries here and in the `messages` dictionary below to extend.
+ *
+ * - `en` — English (default, fallback for all unrecognized locales)
+ * - `zh-cn` — Simplified Chinese (matches VS Code `zh-cn` / `zh-CN`)
+ */
+export type SupportedLocale = 'en' | 'zh-cn';
+
+const messages: Record> = {
+ en: {
+ // ── IdleView ──
+ 'view.idle.configFirst': 'Configure model first',
+ 'view.idle.reviewing': 'Reviewing…',
+ 'view.idle.selectBranch': 'Select comparison branch',
+ 'view.idle.selectCommit': 'Select a commit',
+ 'view.idle.noFiles': 'No files to review',
+ 'view.idle.reviewAll': 'Review all changes',
+ 'view.idle.workspace': 'Workspace',
+ 'view.idle.branch': 'Branch Compare',
+ 'view.idle.commit': 'Single Commit',
+ 'view.idle.baseRef': 'Base ref',
+ 'view.idle.targetRef': 'Target ref',
+ 'view.idle.chooseBranch': 'Choose branch',
+ 'view.idle.commitHistory': 'Commit history',
+ 'view.idle.customPrompt': 'Custom review prompt (optional)',
+ 'view.idle.manageCustom': 'Manage custom providers',
+ 'view.idle.modelConfig': 'Model config',
+
+ // ── RunningView ──
+ 'view.running.reviewLog': 'Review log',
+ 'view.running.cancel': 'Cancel',
+
+ // ── DoneView ──
+ 'view.done.comments': 'comments',
+ 'view.done.files': 'files',
+ 'view.done.processLog': 'Process log',
+
+ // ── EmptyView ──
+ 'view.empty.noIssues': 'No issues found · Passed',
+ 'view.empty.processLog': 'Process log',
+
+ // ── CancelledView ──
+ 'view.cancelled.title': 'Review cancelled',
+
+ // ── FailedView ──
+ 'view.failed.title': 'Review failed.',
+ 'view.failed.checkConfig': 'Please check model configuration and retry.',
+ 'view.failed.checkApiKey': 'Please check API key and network connection.',
+ 'view.failed.retry': 'Retry',
+
+ // ── ConfigView ──
+ 'view.config.title': 'Model Configuration',
+ 'view.config.desc': 'Connect an LLM provider to start code review',
+ 'view.config.close': 'Close',
+ 'view.config.step1': 'Environment Setup',
+ 'view.config.step2': 'Provider Config',
+ 'view.config.checking': 'ocr checking…',
+ 'view.config.notInstalled': 'ocr not installed',
+ 'view.config.official': 'Official Provider',
+ 'view.config.custom': 'Custom Provider',
+ 'view.config.currentUse': 'Currently using',
+ 'view.config.notConfigured': 'No provider configured',
+ 'view.config.officialLabel': 'Official',
+ 'view.config.customLabel': 'Custom',
+ 'view.config.legacyLabel': 'Legacy',
+ 'view.config.model': 'Model',
+ 'view.config.customModel': 'Enter custom model…',
+ 'view.config.apiKey': 'API Key',
+ 'view.config.apiKeyEnvHint': 'Also available via env var',
+ 'view.config.apiKeySaved': 'Saved (leave blank to keep)',
+ 'view.config.testing': 'Testing connection…',
+ 'view.config.testOk': '✓ Connected',
+ 'view.config.testFail': '✗ Connection failed',
+ 'view.config.previous': 'Previous',
+ 'view.config.testFailDetail': '✗ Connection failed: {message}',
+ 'view.config.test': 'Test Connection',
+ 'view.config.save': 'Save',
+ 'view.config.continueProvider': 'Continue to Provider Config',
+ 'view.config.providerName': 'Provider Name',
+ 'view.config.protocol': 'Protocol',
+ 'view.config.baseUrl': 'Base URL',
+ 'view.config.modelList': 'Model list',
+ 'view.config.modelListPlaceholder': 'Comma-separated, e.g. model-a, model-b',
+ 'view.config.authHeader': 'Auth Header',
+ 'view.config.authHeaderHint': 'Optional x-api-key or authorization for Anthropic protocol',
+ 'view.config.authHeaderDefault': 'Default (Authorization)',
+ 'view.config.backToList': '← Back to list',
+ 'view.config.optional': '(optional)',
+ 'view.config.ocrVersionTooltip': 'Open Code Review CLI Version',
+
+ // ── EnvSetupGuide ──
+ 'view.env.installing': 'Installing ocr CLI…',
+ 'view.env.checking': 'Checking, please wait…',
+ 'view.env.ready': 'Environment is ready. Continue to Provider Config.',
+ 'view.env.stepLead': 'Complete each step in order. Move to the next after each passes.',
+ 'view.env.nodeHint': 'Node.js not detected. Visit nodejs.org to install the LTS version, then restart VS Code.',
+ 'view.env.npmHint': 'npm not detected. npm is usually bundled with Node.js — verify your Node installation.',
+ 'view.env.ocrHint': 'Install open-code-review globally in your terminal, or click "One-Click Install" below.',
+ 'view.env.oneClickInstall': 'One-Click Install',
+ 'view.env.redetect': 'Re-detect',
+ 'view.env.checkingStatus': 'Checking',
+ 'view.env.readyStatus': 'Ready',
+ 'view.env.notReady': 'Not ready',
+ 'view.env.pass': 'Pass',
+ 'view.env.fail': 'Fail',
+ 'view.env.waitPrev': 'Waiting for previous',
+ 'view.env.copy': 'Copy',
+ 'view.env.copiedToast': 'Copied ✓',
+
+ // ── CustomProviderManager ──
+ 'cmp.custom.title': 'Custom Providers',
+ 'cmp.custom.desc': 'Manage self-hosted LLM gateways and compatible endpoints. Switch the active review model.',
+ 'cmp.custom.add': 'Add',
+ 'cmp.custom.empty': 'No custom providers',
+ 'cmp.custom.addFirst': 'Add custom provider',
+ 'cmp.custom.currentUse': 'Currently using',
+ 'cmp.custom.model': 'Model',
+ 'cmp.custom.edit': 'Edit',
+ 'cmp.custom.setCurrent': 'Set as current',
+ 'cmp.custom.delete': 'Delete',
+
+ // ── FileList ──
+ 'cmp.fileList.pending': 'Pending files',
+ 'cmp.fileList.noChanges': 'No changed files',
+ 'cmp.fileList.viewDiff': 'Click to view diff',
+
+ // ── LogViewer ──
+ 'cmp.log.waiting': 'Waiting for output',
+
+ // ── CommentCard ──
+ 'cmp.comment.view': 'View',
+ 'cmp.comment.discard': 'Discard',
+
+ // ── PasswordInput ──
+ 'cmp.password.hideSecret': 'Hide secret',
+ 'cmp.password.showSecret': 'Show secret',
+
+ // ── Select ──
+ 'cmp.select.placeholder': 'Select',
+
+ // ── Extension ──
+ 'ext.commentController': 'Open Code Review',
+ 'ext.configPanelTitle': 'Model Configuration',
+ 'ext.config.legacyDisplayName': 'Legacy LLM Endpoint',
+ 'ext.comment.threadLabel': 'Code Review',
+ 'ext.comment.pending': '⏳ [Pending]',
+ 'ext.comment.noSuggestion': '_💡 No code suggestion, please handle manually_',
+ 'ext.comment.applyFailedStale': 'Apply failed: code location is stale, please refresh and retry.',
+ 'ext.comment.applyFailedLocked': 'Apply failed: cannot modify file, check if it is read-only or locked.',
+ 'ext.comment.statusApplied': '✅ [Applied]',
+ 'ext.comment.statusDiscarded': '✅ [Discarded]',
+ 'ext.comment.statusFalsePositive': '✅ [False Positive]',
+ 'ext.comment.jumpFailed': 'Cannot locate ',
+ 'ext.comment.jumpNotAFile': ': is not an openable file.',
+ 'ext.deleteProviderConfirm': 'Delete custom provider "{name}"?',
+ 'ext.deleteProviderConfirmBtn': 'Delete',
+ 'ext.git.justNow': 'just now',
+ 'ext.git.hoursAgo': '{h} hours ago',
+ 'ext.git.hourAgo': '1 hour ago',
+ 'ext.git.yesterday': 'yesterday',
+ 'ext.git.daysAgo': '{d} days ago',
+ 'ext.git.workspaceVsHead': 'Workspace ↔ HEAD',
+ 'ext.cli.installOk': '✓ Install complete',
+ 'ext.cli.installFail': '✗ Install failed (exit ',
+ },
+
+ 'zh-cn': {
+ 'view.idle.configFirst': '请先配置模型',
+ 'view.idle.reviewing': '审查中…',
+ 'view.idle.selectBranch': '请选择对比分支',
+ 'view.idle.selectCommit': '请选择提交',
+ 'view.idle.noFiles': '无可审查文件',
+ 'view.idle.reviewAll': '审查所有变更',
+ 'view.idle.workspace': '工作区',
+ 'view.idle.branch': '分支对比',
+ 'view.idle.commit': '单次提交',
+ 'view.idle.baseRef': '基础引用',
+ 'view.idle.targetRef': '目标引用',
+ 'view.idle.chooseBranch': '选择分支',
+ 'view.idle.commitHistory': '提交历史',
+ 'view.idle.customPrompt': '自定义审查提示词(可选)',
+ 'view.idle.manageCustom': '管理自定义 Provider',
+ 'view.idle.modelConfig': '模型配置',
+
+ 'view.running.reviewLog': '审查日志',
+ 'view.running.cancel': '取消',
+
+ 'view.done.comments': '条评论',
+ 'view.done.files': '个文件',
+ 'view.done.processLog': '过程日志',
+
+ 'view.empty.noIssues': '未发现问题 · 已通过',
+ 'view.empty.processLog': '过程日志',
+
+ 'view.cancelled.title': '审查已取消',
+
+ 'view.failed.title': '审查失败。',
+ 'view.failed.checkConfig': '请检查模型配置后重试。',
+ 'view.failed.checkApiKey': '请检查 API Key 和网络连接。',
+ 'view.failed.retry': '重试',
+
+ 'view.config.title': '模型配置',
+ 'view.config.desc': '连接 LLM Provider 以开始代码审查',
+ 'view.config.close': '关闭',
+ 'view.config.step1': '环境检测',
+ 'view.config.step2': 'Provider 配置',
+ 'view.config.checking': 'ocr 检测中…',
+ 'view.config.notInstalled': 'ocr 未安装',
+ 'view.config.official': '官方 Provider',
+ 'view.config.custom': '自定义 Provider',
+ 'view.config.currentUse': '当前使用',
+ 'view.config.notConfigured': '尚未配置 Provider',
+ 'view.config.officialLabel': '官方',
+ 'view.config.customLabel': '自定义',
+ 'view.config.legacyLabel': 'Legacy',
+ 'view.config.model': '模型',
+ 'view.config.customModel': '输入自定义模型…',
+ 'view.config.apiKey': 'API 密钥',
+ 'view.config.apiKeyEnvHint': '也可通过环境变量',
+ 'view.config.apiKeySaved': '已保存(留空保持不变)',
+ 'view.config.testing': '正在测试连接…',
+ 'view.config.testOk': '✓ 连接成功',
+ 'view.config.testFail': '✗ 连接失败',
+ 'view.config.testFailDetail': '✗ 连接失败:{message}',
+ 'view.config.previous': '上一步',
+ 'view.config.test': '测试连接',
+ 'view.config.save': '保存',
+ 'view.config.continueProvider': '继续配置 Provider',
+ 'view.config.providerName': 'Provider 名称',
+ 'view.config.protocol': '协议',
+ 'view.config.baseUrl': 'Base URL',
+ 'view.config.modelList': '模型列表',
+ 'view.config.modelListPlaceholder': '逗号分隔,如 model-a, model-b',
+ 'view.config.authHeader': 'Auth Header',
+ 'view.config.authHeaderHint': 'Anthropic 协议下可选 x-api-key 或 authorization',
+ 'view.config.authHeaderDefault': '默认 (Authorization)',
+ 'view.config.backToList': '← 返回列表',
+ 'view.config.optional': '(可选)',
+ 'view.config.ocrVersionTooltip': 'Open Code Review CLI 版本',
+
+ 'view.env.installing': '正在安装 ocr CLI…',
+ 'view.env.checking': '正在检测,请稍候…',
+ 'view.env.ready': '环境已就绪,可继续配置 Provider。',
+ 'view.env.stepLead': '按顺序完成环境准备,通过一项后再进行下一项。',
+ 'view.env.nodeHint': '未检测到 Node.js。请前往 nodejs.org 安装 LTS 版本,完成后重启 VS Code。',
+ 'view.env.npmHint': '未检测到 npm。npm 通常随 Node 一起安装,请确认 Node 安装完整。',
+ 'view.env.ocrHint': '在终端全局安装 open-code-review,或点击下方「一键安装」。',
+ 'view.env.oneClickInstall': '一键安装',
+ 'view.env.redetect': '重新检测',
+ 'view.env.checkingStatus': '检测中',
+ 'view.env.readyStatus': '就绪',
+ 'view.env.notReady': '未就绪',
+ 'view.env.pass': '通过',
+ 'view.env.fail': '未通过',
+ 'view.env.waitPrev': '等待上一步',
+ 'view.env.copy': '复制',
+ 'view.env.copiedToast': '已复制到剪贴板 ✓',
+
+ 'cmp.custom.title': '自定义 Provider',
+ 'cmp.custom.desc': '管理自建 LLM 网关与兼容端点,可切换为当前审查模型。',
+ 'cmp.custom.add': '添加',
+ 'cmp.custom.empty': '暂无自定义 Provider',
+ 'cmp.custom.addFirst': '添加自定义 Provider',
+ 'cmp.custom.currentUse': '当前使用',
+ 'cmp.custom.model': '模型',
+ 'cmp.custom.edit': '编辑',
+ 'cmp.custom.setCurrent': '设为当前',
+ 'cmp.custom.delete': '删除',
+
+ 'cmp.fileList.pending': '待审查文件',
+ 'cmp.fileList.noChanges': '无变更文件',
+ 'cmp.fileList.viewDiff': '点击查看 diff',
+
+ 'cmp.log.waiting': '等待输出',
+
+ 'cmp.comment.view': '查看',
+ 'cmp.comment.discard': '忽略',
+
+ 'cmp.password.hideSecret': '隐藏密钥',
+ 'cmp.password.showSecret': '显示密钥',
+
+ 'cmp.select.placeholder': '请选择',
+
+ 'ext.commentController': 'Open Code Review',
+ 'ext.configPanelTitle': '模型配置',
+ 'ext.config.legacyDisplayName': 'Legacy LLM 端点',
+ 'ext.comment.threadLabel': 'Code Review',
+ 'ext.comment.pending': '⏳ [未处理]',
+ 'ext.comment.noSuggestion': '_💡 无代码建议,请手动处理_',
+ 'ext.comment.applyFailedStale': '应用失败:代码位置已失效,请刷新后重试。',
+ 'ext.comment.applyFailedLocked': '应用失败:无法修改文件,请检查文件是否被占用或处于只读状态。',
+ 'ext.comment.statusApplied': '✅ [已应用]',
+ 'ext.comment.statusDiscarded': '✅ [已忽略]',
+ 'ext.comment.statusFalsePositive': '✅ [已误报]',
+ 'ext.comment.jumpFailed': '无法定位到 ',
+ 'ext.comment.jumpNotAFile': ':该路径不是可打开的文件。',
+ 'ext.deleteProviderConfirm': '确定删除自定义 Provider「{name}」?',
+ 'ext.deleteProviderConfirmBtn': '删除',
+ 'ext.git.justNow': '刚刚',
+ 'ext.git.hoursAgo': '{h} 小时前',
+ 'ext.git.hourAgo': '1 小时前',
+ 'ext.git.yesterday': '昨天',
+ 'ext.git.daysAgo': '{d} 天前',
+ 'ext.git.workspaceVsHead': '工作区 ↔ HEAD',
+ 'ext.cli.installOk': '✓ 安装完成',
+ 'ext.cli.installFail': '✗ 安装失败 (exit ',
+ },
+};
+
+export function t(locale: SupportedLocale, key: string): string {
+ return messages[locale]?.[key] ?? messages.en[key] ?? key;
+}
+
+/**
+ * Resolve a VS Code locale string to a {@link SupportedLocale}.
+ * Only `zh-cn` (case-insensitive) maps to Simplified Chinese;
+ * other Chinese variants like `zh-tw` / `zh-hk` fall back to English
+ * until their translations are added.
+ */
+export function resolveLocale(raw: string): SupportedLocale {
+ if (raw.toLowerCase() === 'zh-cn') return 'zh-cn';
+ return 'en';
+}
+
+/**
+ * Convert a {@link SupportedLocale} to the BCP 47 HTML `lang` attribute value.
+ * `zh-cn` → `zh-CN`, others stay as-is.
+ */
+export function toHtmlLang(locale: SupportedLocale): string {
+ return locale === 'zh-cn' ? 'zh-CN' : locale;
+}
diff --git a/extensions/vscode/src/shared/messages.ts b/extensions/vscode/src/shared/messages.ts
index a868c48..059d72d 100644
--- a/extensions/vscode/src/shared/messages.ts
+++ b/extensions/vscode/src/shared/messages.ts
@@ -3,6 +3,7 @@ import {
OcrConfig, ReviewMode, ReviewState,
} from './types';
import { ConfigPanelFocus } from './configUtils';
+import { SupportedLocale } from './i18n';
export type WebviewToHost =
| { type: 'ready' }
@@ -28,7 +29,7 @@ export type WebviewToHost =
| { type: 'commentAction'; index: number; action: 'apply' | 'discard' | 'falsePositive' };
export type HostToWebview =
- | { type: 'init'; config: OcrConfig | null; gitState: GitState }
+ | { type: 'init'; config: OcrConfig | null; gitState: GitState; locale: SupportedLocale }
| { type: 'gitState'; gitState: GitState }
| { type: 'modeFiles'; mode: ReviewMode; files: FileChange[] }
| { type: 'logLine'; line: LogLine }
@@ -38,7 +39,7 @@ export type HostToWebview =
| { type: 'commentSync'; comments: CommentSyncState[] };
export type ConfigPanelHostToWebview =
- | { type: 'configPanelInit'; config: OcrConfig | null; focus?: ConfigPanelFocus | null; env?: EnvCheckResult | null; skipEnvCheck?: boolean }
+ | { type: 'configPanelInit'; config: OcrConfig | null; focus?: ConfigPanelFocus | null; env?: EnvCheckResult | null; skipEnvCheck?: boolean; locale: SupportedLocale }
| { type: 'configPanelFocus'; focus?: ConfigPanelFocus | null }
| { type: 'config'; config: OcrConfig | null }
| { type: 'connectionResult'; ok: boolean; message?: string }
diff --git a/extensions/vscode/src/webview/App.tsx b/extensions/vscode/src/webview/App.tsx
index 3236268..caebc28 100644
--- a/extensions/vscode/src/webview/App.tsx
+++ b/extensions/vscode/src/webview/App.tsx
@@ -1,3 +1,4 @@
+import { I18nContext, resolveLocale } from './I18nProvider';
import { useEffect, useReducer } from 'preact/hooks';
import { reducer, initialState } from './store';
import { bridge } from './bridge';
@@ -38,32 +39,34 @@ export function App() {
};
return (
-
-
-
bridge.post({ type: 'openConfigPanel' })}
- onOpenCustomProviders={() => bridge.post({
- type: 'openConfigPanel',
- focus: { step: 2, tab: 'custom', customView: 'list' },
- })}
- running={state.view === 'running'} />
+
+
+
+
bridge.post({ type: 'openConfigPanel' })}
+ onOpenCustomProviders={() => bridge.post({
+ type: 'openConfigPanel',
+ focus: { step: 2, tab: 'custom', customView: 'list' },
+ })}
+ running={state.view === 'running'} />
- {state.view !== 'idle' && (
-
- {state.view === 'running' && bridge.post({ type: 'cancelReview' })} />}
- {state.view === 'done' && state.session.result && (
- bridge.post({ type: 'jumpToComment', index: i })}
- onAction={(i, action) => bridge.post({ type: 'commentAction', index: i, action })} />
- )}
- {state.view === 'empty' && }
- {state.view === 'cancelled' && }
- {state.view === 'failed' && start({ mode: 'workspace' })} />}
-
- )}
+ {state.view !== 'idle' && (
+
+ {state.view === 'running' && bridge.post({ type: 'cancelReview' })} />}
+ {state.view === 'done' && state.session.result && (
+ bridge.post({ type: 'jumpToComment', index: i })}
+ onAction={(i, action) => bridge.post({ type: 'commentAction', index: i, action })} />
+ )}
+ {state.view === 'empty' && }
+ {state.view === 'cancelled' && }
+ {state.view === 'failed' && start({ mode: 'workspace' })} />}
+
+ )}
+
-
+
);
}
diff --git a/extensions/vscode/src/webview/ConfigPanelApp.tsx b/extensions/vscode/src/webview/ConfigPanelApp.tsx
index 9721c15..1439a12 100644
--- a/extensions/vscode/src/webview/ConfigPanelApp.tsx
+++ b/extensions/vscode/src/webview/ConfigPanelApp.tsx
@@ -1,3 +1,4 @@
+import { I18nContext, resolveLocale } from './I18nProvider';
import { useEffect, useReducer } from 'preact/hooks';
import { bridge } from './bridge';
import { ConfigView } from './views/ConfigView';
@@ -38,6 +39,7 @@ export function ConfigPanelApp() {
}, [state.errorHint]);
return (
+
{state.copyHint &&
{state.copyHint}
}
{state.errorHint &&
{state.errorHint}
}
@@ -63,5 +65,6 @@ export function ConfigPanelApp() {
onClose={() => bridge.post({ type: 'closeConfigPanel' })}
/>
+
);
}
diff --git a/extensions/vscode/src/webview/I18nProvider.tsx b/extensions/vscode/src/webview/I18nProvider.tsx
new file mode 100644
index 0000000..f19dc27
--- /dev/null
+++ b/extensions/vscode/src/webview/I18nProvider.tsx
@@ -0,0 +1,13 @@
+import { createContext } from 'preact';
+import { useContext } from 'preact/hooks';
+import { t, resolveLocale, SupportedLocale } from '../shared/i18n';
+
+export const I18nContext = createContext
('en');
+
+export function useT(): (key: string) => string {
+ const locale = useContext(I18nContext);
+ return (key: string) => t(locale, key);
+}
+
+export { resolveLocale, t };
+export type { SupportedLocale };
diff --git a/extensions/vscode/src/webview/components/CommentCard.tsx b/extensions/vscode/src/webview/components/CommentCard.tsx
index 59a5150..2e6b9d2 100644
--- a/extensions/vscode/src/webview/components/CommentCard.tsx
+++ b/extensions/vscode/src/webview/components/CommentCard.tsx
@@ -1,3 +1,4 @@
+import { useT } from '../I18nProvider';
import { ReviewComment, CommentStatus } from '../../shared/types';
interface Props {
@@ -10,6 +11,7 @@ interface Props {
}
export function CommentCard({ comment, index, status, canJump, onOpen, onAction }: Props) {
+ const t = useT();
return (
);
diff --git a/extensions/vscode/src/webview/components/CustomProviderManager.tsx b/extensions/vscode/src/webview/components/CustomProviderManager.tsx
index ef1317a..f7e2a28 100644
--- a/extensions/vscode/src/webview/components/CustomProviderManager.tsx
+++ b/extensions/vscode/src/webview/components/CustomProviderManager.tsx
@@ -1,5 +1,6 @@
import { listCustomProviderNames } from '../../shared/configUtils';
import { OcrConfig, ProviderEntry } from '../../shared/types';
+import { useT } from '../I18nProvider';
interface Props {
config: OcrConfig | null;
@@ -16,6 +17,7 @@ function formatModels(entry: ProviderEntry): string {
}
export function CustomProviderManager({ config, onAdd, onEdit, onActivate, onDelete }: Props) {
+ const t = useT();
const names = listCustomProviderNames(config);
const activeProvider = config?.provider ?? '';
@@ -23,16 +25,16 @@ export function CustomProviderManager({ config, onAdd, onEdit, onActivate, onDel
{names.length === 0 ? (
-
暂无自定义 Provider
-
+
{t('cmp.custom.empty')}
+
) : (
@@ -45,21 +47,21 @@ export function CustomProviderManager({ config, onAdd, onEdit, onActivate, onDel
{name}
- {isActive && 当前使用}
+ {isActive && {t('cmp.custom.currentUse')}}
{entry.protocol || '—'}
·
{entry.url || '—'}
-
模型:{formatModels(entry)}
+
{t('cmp.custom.model')}: {formatModels(entry)}
-
+
{!isActive && (
-
+
)}
-
+
);
diff --git a/extensions/vscode/src/webview/components/EnvSetupGuide.tsx b/extensions/vscode/src/webview/components/EnvSetupGuide.tsx
index 72af1f5..3d05893 100644
--- a/extensions/vscode/src/webview/components/EnvSetupGuide.tsx
+++ b/extensions/vscode/src/webview/components/EnvSetupGuide.tsx
@@ -1,6 +1,7 @@
import { EnvCheckResult, LogLine } from '../../shared/types';
import { CliStatus } from '../configStore';
import { LogViewer } from './LogViewer';
+import { useT } from '../I18nProvider';
export const OCR_INSTALL_CMD = 'npm install -g @alibaba-group/open-code-review';
@@ -40,12 +41,13 @@ export function EnvSetupGuide({
layout, cliStatus, envCheck, skipEnvCheck = false, installing, installLogs,
onInstall, onCheckEnv, onCopy, onNext,
}: Props) {
+ const t = useT();
const checking = cliStatus === 'checking' || cliStatus === 'unknown';
if (installing) {
return (
-
+
);
@@ -54,7 +56,7 @@ export function EnvSetupGuide({
if (checking) {
return (
-
+
);
@@ -66,7 +68,7 @@ export function EnvSetupGuide({
@@ -76,10 +78,10 @@ export function EnvSetupGuide({
if (cliStatus === 'installed' && skipEnvCheck) {
return (
-
环境已就绪,可继续配置 Provider。
+
{t('view.env.ready')}
@@ -92,7 +94,7 @@ export function EnvSetupGuide({
return (
-
按顺序完成环境准备,通过一项后再进行下一项。
+
{t('view.env.stepLead')}
@@ -124,9 +126,9 @@ export function EnvSetupGuide({
@@ -144,6 +146,7 @@ function EnvCheckingBanner({ label }: { label: string }) {
}
function EnvChecklist({ checking, env }: { checking?: boolean; env?: EnvCheckResult }) {
+ const t = useT();
return (
{CHECK_ITEMS.map(({ key, label }, i) => {
@@ -155,9 +158,9 @@ function EnvChecklist({ checking, env }: { checking?: boolean; env?: EnvCheckRes
{label}
- {checking && '检测中'}
- {!checking && ok && (item?.version ?? '就绪')}
- {!checking && env && !ok && '未就绪'}
+ {checking && t('view.env.checkingStatus')}
+ {!checking && ok && (item?.version ?? t('view.env.readyStatus'))}
+ {!checking && env && !ok && t('view.env.notReady')}
);
@@ -177,6 +180,7 @@ function EnvTimelineItem({
onCopy?: (text: string) => void;
last?: boolean;
}) {
+ const t = useT();
const showDetail = state === 'fail' || state === 'ok';
return (
@@ -188,10 +192,10 @@ function EnvTimelineItem({
{title}
- {state === 'ok' && (version ?? '通过')}
- {state === 'fail' && '未通过'}
- {state === 'pending' && '等待上一步'}
- {state === 'checking' && '检测中'}
+ {state === 'ok' && (version ?? t('view.env.pass'))}
+ {state === 'fail' && t('view.env.fail')}
+ {state === 'pending' && t('view.env.waitPrev')}
+ {state === 'checking' && t('view.env.checkingStatus')}
{showDetail && (
@@ -200,7 +204,7 @@ function EnvTimelineItem({
{command}
{onCopy && (
-
+
)}
diff --git a/extensions/vscode/src/webview/components/FileList.tsx b/extensions/vscode/src/webview/components/FileList.tsx
index 7c6c68e..7f592f9 100644
--- a/extensions/vscode/src/webview/components/FileList.tsx
+++ b/extensions/vscode/src/webview/components/FileList.tsx
@@ -1,3 +1,4 @@
+import { useT } from '../I18nProvider';
import { FileChange } from '../../shared/types';
const BADGE: Record = {
@@ -7,9 +8,10 @@ const BADGE: Record = {
interface Props { files: FileChange[]; loading?: boolean; onOpenFile?: (file: FileChange) => void; }
export function FileList({ files, loading, onOpenFile }: Props) {
+ const t = useT();
return (
-
待审查文件 {loading ? '' : `(${files.length})`}
+
{t('cmp.fileList.pending')} {loading ? '' : `(${files.length})`}
{loading ? (
{[68, 52, 60].map((w, i) => (
@@ -19,11 +21,11 @@ export function FileList({ files, loading, onOpenFile }: Props) {
))}
) : files.length === 0 ? (
-
无变更文件
+
{t('cmp.fileList.noChanges')}
) : (