From ff32ecf5c2f431011a76831878428ce095b2a2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=83=E6=99=BA=E9=B9=8F?= Date: Sun, 26 Jul 2026 15:44:26 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFi18n=20=E5=9B=BD=E9=99=85=E5=8C=96?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E6=8E=A5=E5=85=A5=20+=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=8E=20UI=20=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 i18n 系统,所有用户可见字符串替换为 t() 调用,支持中/EN/日三语 - 插件激活时读取 ai.outputLanguage 配置初始化语言,onDidChangeConfiguration 监听变更自动切换 - ReviewPanel 与 SetupViewProvider 注册 onLanguageChange 回调,语言切换时全量重渲染 - package.json description:"AI 输出语言" -> "插件语言" - 修复 repairJsonEscapes 无差别解引号导致 JSON 解析失败 - 设置页规则名称空值时输入框红框 + 错误提示 - 规则导入流程添加 withProgress 加载提示 --- _AI_USAGE_LOG.md | 8 +- package.json | 2 +- src/activation/commands.ts | 47 ++++----- src/adapters/pmd.ts | 3 +- src/adapters/sql-lint.ts | 3 +- src/ai/engine.ts | 24 ++--- src/ai/providers/openai-compatible.ts | 3 +- src/extension.ts | 15 ++- src/panel/webview.ts | 76 +++++++++------ src/utils/report.ts | 29 +++--- src/views/setupView.js | 16 ++- src/views/setupView.ts | 134 +++++++++++++++----------- 12 files changed, 220 insertions(+), 140 deletions(-) diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 9317966..9991483 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -1,4 +1,4 @@ -# AI Usage Log +# AI Usage Log | 日期时间 | 范式步骤 | 修改摘要 | 中间产物 | 涉及文件 | 使用模型 | |---------|---------|---------|---------|---------|---------| @@ -109,3 +109,9 @@ | 2026-07-25 23:08 | ① 用户提出 → ⑤ 编码实现 | 预览编辑表单 UI 优化:id 字段添加标签说明;每个字段标签后加括号注释(如 severity(严重级别)) | 无 | src/rules/import-preview.ts | deepseek-v4-flash | | 2026-07-25 23:45 | ① 用户提出 → ⑤ 编码实现 | 预览编辑表单布局调整:移除顶部多余 id 大标题;id 标签与值同行 + 切换按钮同行右对齐;id 样式简化为纯文本 | id 位置迭代:一行 → 上下 → 一行 + 按钮同行 | src/rules/import-preview.ts | deepseek-v4-flash | | 2026-07-25 23:50 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 去除代码审查报告画面底部的设置按钮(消息类型、HTML 按钮、消息处理 handler 全部移除) | 无 | src/panel/webview.ts | deepseek-v4-flash-free | +| 2026-07-26 00:43 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | i18n 国际化:复用 ai.outputLanguage 控制 UI 语言;src/i18n/messages.ts 单文件注册 ~100 条文案(Language 类型、t/setLanguage/onLanguageChange/getLanguage);配置变更 → onDidChangeConfiguration → 自动切换;webview 全量重渲染 + 语言下拉即时生效;commands.ts/report.ts/setupView.ts/webview.ts/import-preview.ts + 适配器/AI引擎/converter 错误消息全部替换为 t();提示词与 package.json 保持不变 | 提示词翻译 vs 不翻译(最终不翻译);方案 A 单文件 vs 方案 B 多 JSON(最终选 A);语言切换同步 setLanguage vs 纯配置驱动(最终选配置驱动单一入口);import-preview.ts 尝试用 esc() 导致编译错误 → 改为直接传入 sourceFileName | docs/superpowers/specs/2026-07-26-i18n-design.md src/i18n/messages.ts src/test/messages.test.ts src/extension.ts src/activation/commands.ts src/panel/webview.ts src/views/setupView.ts src/views/import-preview.ts src/utils/report.ts src/adapters/pmd.ts src/adapters/sql-lint.ts src/ai/engine.ts src/ai/providers/openai-compatible.ts src/rules/import-service.ts src/rules/converters/excel-converter.ts src/rules/converters/docx-converter.ts src/rules/converters/pptx-converter.ts | deepseek-v4-pro | +| 2026-07-26 13:00 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 语言设置标签修正:节标题"输出语言"→"语言"、字段提示"AI 审查结果输出语言"→"更改插件的显示语言"、package.json description"AI 输出语言"→"插件语言"(三语同步更新) | 无 | src/i18n/messages.ts package.json | deepseek-v4-flash | +| 2026-07-26 13:27 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | i18n 补全:report 面板(injectedCount/injectedRules/issuesCount/itemsCount)和 import-preview(全部文本标签/统计/校验消息)共 26 条新键三语,替换 webview.ts 4 处 + import-preview.ts 全部硬编码中文 | 无 | src/i18n/messages.ts src/panel/webview.ts src/rules/import-preview.ts | deepseek-v4-flash | +| 2026-07-26 14:16 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | AI 转换器输出语言修复:5 个 converter(md/txt/excel/docx/pptx)和 import-service.ts fallback prompt 去掉字段描述中的(中文)硬编码,追加 `\n输出语言:${getAIOutputLanguage()}` | 无 | src/rules/converters/md-converter.ts src/rules/converters/txt-converter.ts src/rules/converters/excel-converter.ts src/rules/converters/docx-converter.ts src/rules/converters/pptx-converter.ts src/rules/import-service.ts | deepseek-v4-flash | +| 2026-07-26 14:54 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 规则名称空值时输入框红框+错误提示文字 | 无 | src/i18n/messages.ts src/views/setupView.ts src/views/setupView.js | deepseek-v4-flash-free | +| 2026-07-26 15:26 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | repairJsonEscapes 去掉 replace 无差别解引号,防止 AI 正确转义的 \" 被破坏导致 JSON 解析失败 | 无 | src/ai/engine.ts | deepseek-v4-flash-free | diff --git a/package.json b/package.json index e9dca13..0631d9c 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "en", "ja" ], - "description": "AI 输出语言" + "description": "插件语言" }, "vscode-code-reviewer.linters.javascript": { "type": "string", diff --git a/src/activation/commands.ts b/src/activation/commands.ts index 9570222..fea12eb 100644 --- a/src/activation/commands.ts +++ b/src/activation/commands.ts @@ -7,6 +7,7 @@ import { mergeResults, MergedReport } from '../merger/merger'; import { reportToMarkdown } from '../utils/report'; import { getApiKey } from '../config'; import { ReviewPanel } from '../panel/webview'; +import { t } from '../i18n/messages'; let currentReport: MergedReport | null = null; @@ -19,7 +20,7 @@ export function registerCommands( vscode.commands.registerCommand('codeReviewer.review', async () => { const editor = vscode.window.activeTextEditor; if (!editor) { - vscode.window.showWarningMessage('请先打开一个文件'); + vscode.window.showWarningMessage(t('review.noEditor')); return; } @@ -29,15 +30,15 @@ export function registerCommands( await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, - title: '正在审查...', + title: t('review.running'), cancellable: false, }, async (progress) => { - progress.report({ message: '运行静态分析...' }); + progress.report({ message: t('review.staticAnalysis') }); const startTime = Date.now(); const staticResult = await orchestrator.runStaticAnalysis(document, workingDir); - progress.report({ message: '运行 AI 审查...' }); + progress.report({ message: t('review.aiReview') }); const allRules = loadActiveRules(workspaceRoot); const filterResult = filterAndSummarize(allRules, document); @@ -76,14 +77,14 @@ export function registerCommands( const selection = editor.selection; if (selection.isEmpty) { - vscode.window.showWarningMessage('请先选中要审查的代码'); + vscode.window.showWarningMessage(t('review.noSelection')); return; } const code = editor.document.getText(selection); const apiKey = await getApiKey(context); if (!apiKey) { - vscode.window.showWarningMessage('请先在设置面板中配置 API Key'); + vscode.window.showWarningMessage(t('review.needApiKey')); return; } @@ -92,12 +93,12 @@ export function registerCommands( await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, - title: '审查选中代码...', + title: t('review.reviewingSelection'), cancellable: false, }, async () => { const aiResult = await runAIReview(context, code, [], customRules); vscode.window.showInformationMessage( - `选中代码审查完成: ${aiResult.customRuleResults.length + aiResult.findings.length} 个问题` + t('review.selectionComplete', { 0: String(aiResult.customRuleResults.length + aiResult.findings.length) }) ); }); }) @@ -116,18 +117,18 @@ export function registerCommands( context.subscriptions.push( vscode.commands.registerCommand('codeReviewer.exportReport', async () => { if (!currentReport) { - vscode.window.showWarningMessage('请先运行完整审查生成报告'); + vscode.window.showWarningMessage(t('export.needRunFirst')); return; } const markdown = reportToMarkdown(currentReport); const pick = await vscode.window.showQuickPick([ - { label: '📋 复制到剪贴板', description: '将报告内容以 Markdown 格式复制到剪贴板' }, - { label: '📄 下载 Markdown 文件', description: '将报告保存为 .md 文件' }, - ], { placeHolder: '选择导出方式' }); + { label: t('export.copyToClipboard'), description: t('export.copyDescription') }, + { label: t('export.downloadMarkdown'), description: t('export.saveDescription') }, + ], { placeHolder: t('export.selectMethod') }); if (!pick) { return; } - if (pick.label.startsWith('📋')) { + if (pick.label === t('export.copyToClipboard')) { await vscode.env.clipboard.writeText(markdown); - vscode.window.showInformationMessage('报告已复制到剪贴板'); + vscode.window.showInformationMessage(t('export.copied')); } else { const fileName = currentReport.filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, '') ?? 'review-report'; const defaultUri = vscode.workspace.workspaceFolders?.[0] @@ -136,11 +137,11 @@ export function registerCommands( const uri = await vscode.window.showSaveDialog({ defaultUri, filters: { 'Markdown': ['md'] }, - title: '保存审查报告', + title: t('export.saveDialogTitle'), }); if (!uri) { return; } await vscode.workspace.fs.writeFile(uri, Buffer.from(markdown, 'utf-8')); - vscode.window.showInformationMessage(`报告已保存到 ${uri.fsPath}`); + vscode.window.showInformationMessage(t('export.saved', { 0: uri.fsPath })); } }) ); @@ -149,22 +150,22 @@ export function registerCommands( vscode.commands.registerCommand('codeReviewer.addCustomRule', () => { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!workspaceRoot) { - vscode.window.showWarningMessage('请先打开工作区'); + vscode.window.showWarningMessage(t('setup.noWorkspace')); return; } - vscode.window.showInformationMessage('请在设置面板中管理自定义规则'); + vscode.window.showInformationMessage(t('setup.manageRulesHint')); }) ); context.subscriptions.push( vscode.commands.registerCommand('codeReviewer.fixIssue', () => { - vscode.window.showInformationMessage('单条修复功能开发中'); + vscode.window.showInformationMessage(t('review.fixNotAvailable')); }) ); context.subscriptions.push( vscode.commands.registerCommand('codeReviewer.fixAll', () => { - vscode.window.showInformationMessage('批量修复功能开发中'); + vscode.window.showInformationMessage(t('review.fixAllNotAvailable')); }) ); @@ -174,10 +175,10 @@ export function registerCommands( await vscode.commands.executeCommand('workbench.view.extension.code-reviewer'); } catch { const action = await vscode.window.showErrorMessage( - '无法打开设置面板', - '打开设置 (JSON)' + t('setup.openSetupFail'), + t('setup.openSettingsJson') ); - if (action === '打开设置 (JSON)') { + if (action === t('setup.openSettingsJson')) { await vscode.commands.executeCommand('workbench.action.openSettingsJson'); } } diff --git a/src/adapters/pmd.ts b/src/adapters/pmd.ts index 7014fa6..455bbd5 100644 --- a/src/adapters/pmd.ts +++ b/src/adapters/pmd.ts @@ -4,6 +4,7 @@ import { existsSync } from 'fs'; import { execSync, spawn } from 'child_process'; import type { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types'; import { getPMDRulesetPath } from '../config'; +import { t } from '../i18n/messages'; export class PmdAdapter implements LinterAdapter { id = 'pmd'; @@ -57,7 +58,7 @@ export class PmdAdapter implements LinterAdapter { } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes('ENOENT') || message.includes('java not found') || message.includes('Cannot find')) { - return { diagnostics: [], status: 'tool-unavailable', errorMessage: 'Java 11+ 未安装或不在 PATH 中' }; + return { diagnostics: [], status: 'tool-unavailable', errorMessage: t('adapter.javaNotInstalled') }; } return { diagnostics: [], status: 'execution-failed', errorMessage: message }; } diff --git a/src/adapters/sql-lint.ts b/src/adapters/sql-lint.ts index 796707e..b07bc0a 100644 --- a/src/adapters/sql-lint.ts +++ b/src/adapters/sql-lint.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { spawn } from 'child_process'; import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter'; +import { t } from '../i18n/messages'; const DIALECT_MAP: Record = { sql: 'ansi', @@ -95,7 +96,7 @@ export class SqlLintAdapter implements LinterAdapter { return { diagnostics: [], status: 'tool-unavailable', - errorMessage: 'sqlfluff 未安装,请执行 pip install sqlfluff', + errorMessage: t('adapter.sqlfluffNotInstalled'), }; } return { diff --git a/src/ai/engine.ts b/src/ai/engine.ts index 03f6192..e3c9cf2 100644 --- a/src/ai/engine.ts +++ b/src/ai/engine.ts @@ -9,6 +9,7 @@ import type { TranslatedDiagnostic, AIFinding, } from './schema'; +import { t } from '../i18n/messages'; function buildCustomRulePrompt(rules: CustomRule[]): string { return rules.map(r => @@ -27,22 +28,21 @@ function addLineNumbers(code: string): string { } function repairJsonEscapes(str: string): string { - let s = str.replace(/\\"\\n/g, '\\n').replace(/\\"/g, '"'); let inString = false; let out = ''; - for (let i = 0; i < s.length; i++) { - const ch = s[i]; + for (let i = 0; i < str.length; i++) { + const ch = str[i]; if (ch === '\\') { out += ch; - if (i + 1 < s.length) { out += s[++i]; } + if (i + 1 < str.length) { out += str[++i]; } } else if (ch === '"') { if (!inString) { inString = true; out += ch; } else { let j = i + 1; - while (j < s.length && s[j] === ' ') { j++; } - if (j < s.length && ':,\]}'.includes(s[j])) { + while (j < str.length && str[j] === ' ') { j++; } + if (j < str.length && ':,\]}'.includes(str[j])) { inString = false; out += ch; } else { @@ -109,7 +109,7 @@ export async function runAIReview( translatedDiagnostics: [], findings: [], degraded: true, - error: '未配置 API Key', + error: t('adapter.noApiKey'), }; } @@ -125,7 +125,7 @@ export async function runAIReview( translatedDiagnostics: [], findings: [], degraded: true, - error: `创建 Provider 失败: ${err instanceof Error ? err.message : String(err)}`, + error: t('adapter.createProviderFail', { 0: err instanceof Error ? err.message : String(err) }), }; } @@ -166,10 +166,10 @@ export async function runAIReview( ruleId: `custom:${r.ruleId}`, })); } catch (e) { - errors.push(`自定义规则响应解析失败: ${e instanceof Error ? e.message : String(e)}`); + errors.push(t('adapter.customRuleParseFail', { 0: e instanceof Error ? e.message : String(e) })); } } else { - errors.push(`自定义规则请求失败: ${resultA.reason}`); + errors.push(t('adapter.customRuleRequestFail', { 0: resultA.reason })); } let translatedDiagnostics: TranslatedDiagnostic[] = []; @@ -183,10 +183,10 @@ export async function runAIReview( translatedDiagnostics = parsed.translatedDiagnostics ?? []; findings = parsed.findings ?? []; } catch (e) { - errors.push(`AI 审查响应解析失败: ${e instanceof Error ? e.message : String(e)}`); + errors.push(t('adapter.aiReviewParseFail', { 0: e instanceof Error ? e.message : String(e) })); } } else { - errors.push(`AI 审查请求失败: ${resultB.reason}`); + errors.push(t('adapter.aiReviewRequestFail', { 0: resultB.reason })); } const degraded = errors.length > 0; diff --git a/src/ai/providers/openai-compatible.ts b/src/ai/providers/openai-compatible.ts index 384b835..035ebd2 100644 --- a/src/ai/providers/openai-compatible.ts +++ b/src/ai/providers/openai-compatible.ts @@ -1,4 +1,5 @@ import { AIProvider, ChatOptions } from './base'; +import { t } from '../../i18n/messages'; export class OpenAICompatibleProvider extends AIProvider { id: string; @@ -40,7 +41,7 @@ export class OpenAICompatibleProvider extends AIProvider { if (!response.ok) { const errorText = await response.text(); if (response.status === 401) { - throw new Error('API Key 无效,请重新设置'); + throw new Error(t('adapter.invalidApiKey')); } throw new Error(`API 请求失败 (${response.status}): ${errorText}`); } diff --git a/src/extension.ts b/src/extension.ts index f8bf1d9..30dfb63 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,11 +2,15 @@ import * as vscode from 'vscode'; import { Orchestrator } from './orchestrator/orchestrator'; import { registerCommands } from './activation/commands'; import { SetupViewProvider } from './views/setupView'; +import { setLanguage, t, type Language } from './i18n/messages'; +import { getAIOutputLanguage } from './config'; let orchestrator: Orchestrator; export function activate(context: vscode.ExtensionContext) { - console.log('净码特工 · Code Purifier 已激活'); + const lang = getAIOutputLanguage() as Language; + setLanguage(lang); + console.log(t('extension.activated')); orchestrator = new Orchestrator(); @@ -35,6 +39,15 @@ export function activate(context: vscode.ExtensionContext) { debounceTimers.set(key, timer); }) ); + + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('vscode-code-reviewer.ai.outputLanguage')) { + const newLang = getAIOutputLanguage() as Language; + setLanguage(newLang); + } + }) + ); } export function deactivate() { diff --git a/src/panel/webview.ts b/src/panel/webview.ts index 622a1dc..4c290a9 100644 --- a/src/panel/webview.ts +++ b/src/panel/webview.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { MergedReport } from '../merger/merger'; +import { t, onLanguageChange, getLanguage } from '../i18n/messages'; interface PanelMessage { type: 'navigate' | 'rerun' | 'export' | 'fix' | 'fixAll'; @@ -19,10 +20,16 @@ function svgIcon(): string { const SVG_HEADER_ICON = svgIcon(); const BADGE_CLASS: Record = { linter: 'badge-linter', custom: 'badge-custom', ai: 'badge-ai' }; -const BADGE_LABEL: Record = { linter: 'Linter', custom: '自定义', ai: 'AI' }; function badgeHtml(source: string): string { - return `${BADGE_LABEL[source] || source}`; + let label: string; + switch (source) { + case 'linter': label = t('report.sourceLinter'); break; + case 'custom': label = t('report.sourceCustom'); break; + case 'ai': label = t('report.sourceAI'); break; + default: label = source; + } + return `${label}`; } function severityClass(severity: string): string { @@ -38,6 +45,7 @@ export class ReviewPanel { public static currentPanel: ReviewPanel | undefined; private readonly panel: vscode.WebviewPanel; private disposables: vscode.Disposable[] = []; + private currentReport: MergedReport | null = null; private constructor( private readonly extensionUri: vscode.Uri, @@ -45,7 +53,7 @@ export class ReviewPanel { ) { this.panel = vscode.window.createWebviewPanel( 'codeReviewer.reviewPanel', - '净码特工 · 代码审查报告', + t('report.panelTitle'), column, { enableScripts: true, @@ -61,6 +69,15 @@ export class ReviewPanel { null, this.disposables ); + + this.disposables.push( + onLanguageChange(() => { + this.panel.title = t('report.panelTitle'); + if (this.currentReport) { + this.panel.webview.html = this.buildHtml(this.currentReport); + } + }) + ); } static createOrShow(extensionUri: vscode.Uri, column?: vscode.ViewColumn): ReviewPanel { @@ -74,6 +91,7 @@ export class ReviewPanel { } update(report: MergedReport): void { + this.currentReport = report; this.panel.webview.html = this.buildHtml(report); } @@ -96,13 +114,13 @@ export class ReviewPanel { const totalInfos = linterInfos + customInfos + aiInfos; const errorBox = report.errors.length > 0 - ? `
✖ 执行错误
${report.errors.map(e => `
${esc(e)}
`).join('')}
` + ? `
✖ ${t('report.executionErrors')}
${report.errors.map(e => `
${esc(e)}
`).join('')}
` : ''; const banner = report.errors.length > 0 - ? '' + ? `` : report.degraded - ? '' + ? `` : ''; const fixableLinterSet = new Set(report.fixableLinterIndices); @@ -116,11 +134,11 @@ export class ReviewPanel { return pts.join(' '); }; - const linterToolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : '静态分析'; + const linterToolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : t('report.sourceLinter'); const customFilterInfo = report.customRuleFilterInfo; const customFilterLabel = customFilterInfo - ? `(注入 ${customFilterInfo.injected}/${customFilterInfo.totalActive} 条)` + ? t('report.injectedCount', { 0: customFilterInfo.injected, 1: customFilterInfo.totalActive }) : ''; return ` @@ -128,7 +146,7 @@ export class ReviewPanel { -代码审查报告 +${t('report.title')}