fix: refine i18n evidence quality gates

This commit is contained in:
coso
2026-05-27 11:24:11 +08:00
parent 518eced3ba
commit cc3e0bb82d
11 changed files with 371 additions and 11 deletions
+1
View File
@@ -142,6 +142,7 @@
- legacy DOM Patch 只允许作为迁移期兜底,不允许成为新功能或新文案的本地化事实源;确需临时例外时,必须写入对应路线图或执行计划并说明退出条件
- 动态用户可见文案使用 i18next interpolation / plural / context;日期、数字、相对时间、列表和排序优先复用 `src/i18n/format.ts`
- 协议 facts / JSON schema / stable enum(例如 `type/status/failureCategory/reasonCode`)不得随 locale 翻译;只翻译 presentation 层,避免测试、join key 或跨模块判断被本地化文案污染
- i18n 资源、loader 或 bundle 策略变更后,质量任务选择器的 `recommendedCommands` 会提示刷新 `translation-pr-pack.json` 与 `bundle-strategy-report.json`;这类 evidence 用于证明翻译 PR 可审阅、bundle 体积与 chunk 策略没有漂移
- 优先补现有 `*.test.tsx` 的关键文案、状态与交互断言
- 如果目标区域已有 snapshot / 结构化快照机制,沿用现有机制
- 不要因为“只是 UI”就跳过回归
@@ -0,0 +1,145 @@
{
"bundleGroups": [
{
"fileCount": 40,
"keyCount": 17520,
"localeRawBytes": {
"en-US": 302154,
"ja-JP": 338662,
"ko-KR": 324064,
"zh-CN": 296675,
"zh-TW": 297075
},
"namespace": "agent",
"partNames": [
"agent",
"agentExperts",
"agentHome",
"agentInputbar",
"agentMessageList",
"agentRuntime",
"agentSkills",
"agentTeamWorkspace"
],
"role": "inline",
"sourceLocaleKeyCount": 3504,
"sourceLocaleRawBytes": 296675,
"totalRawBytes": 1558630
},
{
"fileCount": 5,
"keyCount": 12670,
"localeRawBytes": {
"en-US": 235012,
"ja-JP": 263141,
"ko-KR": 252246,
"zh-CN": 226601,
"zh-TW": 227590
},
"namespace": "settings",
"partNames": [
"settings"
],
"role": "inline",
"sourceLocaleKeyCount": 2534,
"sourceLocaleRawBytes": 226601,
"totalRawBytes": 1204590
},
{
"fileCount": 5,
"keyCount": 4405,
"localeRawBytes": {
"en-US": 79219,
"ja-JP": 88994,
"ko-KR": 85060,
"zh-CN": 77041,
"zh-TW": 77279
},
"namespace": "workspace",
"partNames": [
"workspace"
],
"role": "inline",
"sourceLocaleKeyCount": 881,
"sourceLocaleRawBytes": 77041,
"totalRawBytes": 407593
},
{
"fileCount": 5,
"keyCount": 1305,
"localeRawBytes": {
"en-US": 22159,
"ja-JP": 27493,
"ko-KR": 25698,
"zh-CN": 21312,
"zh-TW": 21401
},
"namespace": "common",
"partNames": [
"common"
],
"role": "inline",
"sourceLocaleKeyCount": 261,
"sourceLocaleRawBytes": 21312,
"totalRawBytes": 118063
},
{
"fileCount": 5,
"keyCount": 1065,
"localeRawBytes": {
"en-US": 19163,
"ja-JP": 22560,
"ko-KR": 20848,
"zh-CN": 18767,
"zh-TW": 18914
},
"namespace": "errors",
"partNames": [
"errors"
],
"role": "inline",
"sourceLocaleKeyCount": 213,
"sourceLocaleRawBytes": 18767,
"totalRawBytes": 100252
},
{
"fileCount": 5,
"keyCount": 780,
"localeRawBytes": {
"en-US": 11836,
"ja-JP": 13493,
"ko-KR": 12759,
"zh-CN": 11541,
"zh-TW": 11565
},
"namespace": "navigation",
"partNames": [
"navigation"
],
"role": "inline",
"sourceLocaleKeyCount": 156,
"sourceLocaleRawBytes": 11541,
"totalRawBytes": 61194
}
],
"coreNamespaces": [
"common",
"navigation",
"settings",
"workspace",
"agent",
"errors"
],
"localeCount": 5,
"resourcesDir": "/Users/coso/Documents/dev/ai/aiclientproxy/lime/src/i18n/resources",
"schemaVersion": "lime.i18n.bundleStrategyReport.v1",
"sourceLocale": "zh-CN",
"summary": {
"coreGroupCount": 6,
"inlineGroupCount": 6,
"lazyGroupCount": 0,
"sourceLocaleFileCount": 13,
"sourceLocaleKeyCount": 7549,
"totalRawBytes": 3450322
}
}
@@ -901,3 +901,29 @@ benchmark 摘要:
- `npm run verify:gui-smoke -- --reuse-running` 通过。
- `npm run i18n:patch-retirement-gate:json -- --output "docs/roadmap/i18n/evidence/patch-retirement-gate-report.json" --patch-report ".lime/i18n/patch-metrics-report.json" --legacy-report ".lime/governance/legacy-surface-report.json"` 通过。
## 2026-05-27:P3 bundle 体积与 chunk 策略 evidence 落盘
本轮继续完成:
- `scripts/i18n-bundle-report.ts` 新增 `--output <path>`,使 bundle footprint / chunk strategy 报告可以直接落成版本化 evidence,而不是依赖 stdout 复制。
- 新增 `docs/roadmap/i18n/evidence/bundle-strategy-report.json`;当前报告覆盖 5 个 locale、13 个 source locale 文件、7549 个 source key,总 raw bytes 为 3450322。
- 当前 core namespace 仍全部 inline;最大 inline group 是 `agent`,由 `agent / agentExperts / agentHome / agentInputbar / agentMessageList / agentRuntime / agentSkills / agentTeamWorkspace` 8 个资源分片组成,source locale 为 3504 key、296675 bytes。
- `settings` 是第二大 inline group,source locale 为 2534 key、226601 bytes;后续新增非启动路径 namespace 时应继续默认走 lazy chunk 候选,避免桌面首屏被非核心资源拖慢。
验证:
- `npm run i18n:bundle-report:json -- --output "docs/roadmap/i18n/evidence/bundle-strategy-report.json"` 通过。
## 2026-05-27:P3 bundle strategy evidence 接入质量选择器
本轮继续完成:
- `scripts/quality-task-planner.mjs` 将 i18n 资源、`loadNamespace.ts`、`bundledNamespaceParts.ts` 与 `scripts/i18n-bundle-report.ts` 纳入 bundle strategy 推荐命令范围。
- 资源改动现在会同时推荐刷新 `translation-pr-pack.json` 与 `bundle-strategy-report.json`;bundle loader / 报告脚本改动会推荐刷新 `bundle-strategy-report.json`。
- `docs/aiprompts/quality-workflow.md` 同步记录该口径,确保 P3 bundle 体积与 chunk 策略 evidence 不停留在一次性手工报告,而是进入质量任务选择器的治理闭环。
验证:
- `npm test -- "scripts/quality-task-planner.test.ts" "scripts/i18n-bundle-report.test.ts"` 通过。
- `npm run verify:tasks -- --format json` 能在当前 i18n bundle 报告改动下输出 `npm run i18n:bundle-report:json -- --output docs/roadmap/i18n/evidence/bundle-strategy-report.json` 推荐命令。
+29
View File
@@ -134,4 +134,33 @@ describe("i18n bundle report", () => {
}),
);
});
it("CLI 应支持把 bundle evidence 写入指定文件", () => {
const root = createTempDir();
writeResource(root, "zh-CN", "common", {
"common.save": "保存",
});
writeResource(root, "en-US", "common", {
"common.save": "Save",
});
const outputPath = path.join(root, "reports", "bundle-report.json");
const exitCode = runCli([
"--format",
"json",
"--output",
outputPath,
"--resources-dir",
path.join(root, "resources"),
"--source-locale",
"zh-CN",
]);
expect(exitCode).toBe(0);
expect(JSON.parse(fs.readFileSync(outputPath, "utf8"))).toEqual(
expect.objectContaining({
schemaVersion: "lime.i18n.bundleStrategyReport.v1",
}),
);
});
});
+18 -2
View File
@@ -47,6 +47,7 @@ export interface I18nBundleReport {
interface CliOptions {
format: I18nBundleReportFormat;
output?: string;
resourcesDir: string;
sourceLocale: string;
}
@@ -301,6 +302,12 @@ function parseCliArgs(argv: string[]): CliOptions {
throw new Error(`Unknown or missing format value: ${next}`);
}
if (arg === "--output" && next) {
options.output = next;
index += 1;
continue;
}
if (arg === "--resources-dir" && next) {
options.resourcesDir = next;
index += 1;
@@ -315,7 +322,7 @@ function parseCliArgs(argv: string[]): CliOptions {
if (arg === "--help" || arg === "-h") {
console.log(
"Usage: npm run i18n:bundle-report -- [--format text|json] [--resources-dir <dir>] [--source-locale <locale>]",
"Usage: npm run i18n:bundle-report -- [--format text|json] [--output <path>] [--resources-dir <dir>] [--source-locale <locale>]",
);
process.exit(0);
}
@@ -329,7 +336,16 @@ function parseCliArgs(argv: string[]): CliOptions {
export function runCli(argv = process.argv.slice(2)): number {
const options = parseCliArgs(argv);
const report = analyzeI18nBundleReport(options);
process.stdout.write(formatI18nBundleReport(report, options.format));
const content = formatI18nBundleReport(report, options.format);
if (options.output) {
const outputPath = path.resolve(options.output);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, content, "utf8");
} else {
process.stdout.write(content);
}
return 0;
}
+24 -1
View File
@@ -62,6 +62,18 @@ const I18N_TRANSLATION_REVIEW_PACK_RECOMMENDED_COMMANDS = [
"npm run i18n:translation-pr-pack:json -- --output docs/roadmap/i18n/evidence/translation-pr-pack.json",
];
const I18N_BUNDLE_STRATEGY_RECOMMENDED_COMMANDS = [
"npm run i18n:bundle-report:json -- --output docs/roadmap/i18n/evidence/bundle-strategy-report.json",
];
const I18N_BUNDLE_STRATEGY_FILES = new Set([
"docs/roadmap/i18n/evidence/bundle-strategy-report.json",
"scripts/i18n-bundle-report.test.ts",
"scripts/i18n-bundle-report.ts",
"src/i18n/bundledNamespaceParts.ts",
"src/i18n/loadNamespace.ts",
]);
const I18N_PATCH_RETIREMENT_RECOMMENDED_COMMANDS = [
"npm run i18n:patch-retirement-gate -- --check",
];
@@ -335,6 +347,13 @@ function isI18nTranslationReviewPackChange(file) {
);
}
function isI18nBundleStrategyChange(file) {
return (
file.startsWith("src/i18n/resources/") ||
I18N_BUNDLE_STRATEGY_FILES.has(file)
);
}
function isI18nPatchRetirementChange(file) {
return I18N_PATCH_RETIREMENT_FILES.has(file);
}
@@ -433,7 +452,11 @@ function collectRecommendedCommands(changedFiles, { docsOnly = false } = {}) {
commands.push(...I18N_TRANSLATION_REVIEW_PACK_RECOMMENDED_COMMANDS);
}
return commands;
if (changedFiles.some(isI18nBundleStrategyChange)) {
commands.push(...I18N_BUNDLE_STRATEGY_RECOMMENDED_COMMANDS);
}
return Array.from(new Set(commands));
}
function isIntegrityChange(file) {
+29
View File
@@ -110,6 +110,7 @@ describe("quality-task-planner", () => {
expect(tasks.docsOnly).toBe(false);
expect(tasks.recommendedCommands).toEqual([
"npm run i18n:translation-pr-pack:json -- --output docs/roadmap/i18n/evidence/translation-pr-pack.json",
"npm run i18n:bundle-report:json -- --output docs/roadmap/i18n/evidence/bundle-strategy-report.json",
]);
});
@@ -136,6 +137,34 @@ describe("quality-task-planner", () => {
]);
});
it("bundle strategy 脚本改动应触发 i18n 校验并推荐刷新 bundle evidence", () => {
const tasks = detectTasks(["scripts/i18n-bundle-report.ts"]);
expect(tasks.i18n).toBe(true);
expect(tasks.i18nHardcoded).toBe(false);
expect(tasks.i18nUnused).toBe(true);
expect(tasks.frontend).toBe(false);
expect(tasks.guiSmoke).toBe(false);
expect(tasks.docsOnly).toBe(false);
expect(tasks.recommendedCommands).toEqual([
"npm run i18n:bundle-report:json -- --output docs/roadmap/i18n/evidence/bundle-strategy-report.json",
]);
});
it("bundle loader 边界改动应推荐刷新 bundle evidence", () => {
const tasks = detectTasks(["src/i18n/loadNamespace.ts"]);
expect(tasks.i18n).toBe(true);
expect(tasks.i18nHardcoded).toBe(false);
expect(tasks.i18nUnused).toBe(true);
expect(tasks.frontend).toBe(true);
expect(tasks.guiSmoke).toBe(false);
expect(tasks.docsOnly).toBe(false);
expect(tasks.recommendedCommands).toEqual([
"npm run i18n:bundle-report:json -- --output docs/roadmap/i18n/evidence/bundle-strategy-report.json",
]);
});
it("前端源码改动应触发硬编码文案扫描", () => {
const tasks = detectTasks(["src/components/settings/LanguagePicker.tsx"]);
@@ -51,4 +51,15 @@ describe("agentRuntimeErrorPresentation", () => {
"当前 AI 服务商余额或额度不足,请在服务商后台充值或开通额度,或切换到其他可用模型后重试。",
});
});
it("普通错误里包含 402 字符串时不应误判为额度不足", () => {
expect(
resolveAgentRuntimeErrorPresentation(
"requestKey 2026042402 failed: 模型通道暂时不可用",
),
).toEqual({
displayMessage: "requestKey 2026042402 failed: 模型通道暂时不可用",
toastMessage: "响应错误: requestKey 2026042402 failed: 模型通道暂时不可用",
});
});
});
@@ -19,7 +19,10 @@ function normalizeRuntimeErrorMessage(errorMessage: string): string {
return normalized || DEFAULT_RUNTIME_ERROR_MESSAGE;
}
function looksLikeHttpStatus(message: string, status: "401" | "403"): boolean {
function looksLikeHttpStatus(
message: string,
status: "401" | "402" | "403",
): boolean {
return new RegExp(`(^|\\D)${status}(\\D|$)`).test(message);
}
@@ -75,7 +78,7 @@ function isLikelyProviderAuthError(message: string): boolean {
function isLikelyProviderQuotaError(message: string): boolean {
return (
message.includes("402") ||
looksLikeHttpStatus(message, "402") ||
includesAny(message, [
"payment required",
"insufficient balance",
@@ -287,4 +287,81 @@ describe("WorkspaceHarnessDialogs", () => {
expect(guide?.textContent).toContain("File changes need review");
expect(guide?.textContent).toContain("Review changes");
});
it("正在写入阶段的文件名不应被普通文件读取事件覆盖", () => {
renderDialog({
harnessState: createHarnessState({
activeFileWrites: [
{
id: "write-code",
path: "/tmp/workspace/src/real-change.ts",
displayName: "real-change.ts",
phase: "streaming",
status: "streaming",
source: "artifact_snapshot",
updatedAt: new Date("2026-05-26T10:00:00.000Z"),
},
],
recentFileEvents: [
{
id: "event-read",
toolCallId: "tool-read",
path: "/tmp/workspace/README.md",
displayName: "README.md",
kind: "document",
action: "read",
sourceToolName: "read_file",
timestamp: new Date("2026-05-26T10:01:00.000Z"),
clickable: true,
},
],
}),
});
const guide = document.body.querySelector(
'[data-testid="code-workbench-guide"]',
) as HTMLElement | null;
expect(guide?.getAttribute("data-stage")).toBe("writing");
expect(guide?.textContent).toContain("real-change.ts");
expect(guide?.textContent).not.toContain("README.md");
});
it("没有真实文件快照时不应在导轨显示快照可回滚", async () => {
await changeLimeLocale("zh-CN");
renderDialog({
threadRead: {
thread_id: "session-code",
file_checkpoint_summary: {
count: 0,
latest_checkpoint: null,
},
},
harnessState: createHarnessState({
recentFileEvents: [
{
id: "event-code-edit",
toolCallId: "tool-code-edit",
path: "/tmp/workspace/src/main.ts",
displayName: "main.ts",
kind: "code",
action: "edit",
sourceToolName: "edit_file",
timestamp: new Date("2026-05-26T10:01:00.000Z"),
preview: "Update main entry",
clickable: true,
},
],
}),
});
const guide = document.body.querySelector(
'[data-testid="code-workbench-guide"]',
) as HTMLElement | null;
expect(guide?.getAttribute("data-stage")).toBe("review");
expect(guide?.textContent).toContain("文件变更待处理");
expect(guide?.textContent).not.toContain("快照可回滚");
});
});
@@ -57,11 +57,13 @@ function resolveCodeWorkbenchGuideMetrics(
harnessState: HarnessPanelBaseProps["harnessState"],
) {
const fileChangePaths = new Set<string>();
let latestFileName: string | null = null;
for (const activeWrite of harnessState.activeFileWrites) {
const path = activeWrite.path.trim();
if (path) {
fileChangePaths.add(path);
latestFileName ||= activeWrite.displayName || path;
}
}
@@ -69,16 +71,14 @@ function resolveCodeWorkbenchGuideMetrics(
const path = event.path.trim();
if (path && isReviewableCodeFileAction(event.action)) {
fileChangePaths.add(path);
latestFileName ||= event.displayName || path;
}
}
return {
pendingFileChangeCount: fileChangePaths.size,
totalFileChangeCount: fileChangePaths.size,
latestFileName:
harnessState.activeFileWrites[0]?.displayName ||
harnessState.recentFileEvents[0]?.displayName ||
null,
latestFileName,
};
}
@@ -181,7 +181,7 @@ export function GeneralWorkbenchHarnessDialogSection({
isCodeRuntime:
panelBaseProps.diagnosticRuntimeContext?.executionStrategy ===
"code_orchestrated",
hasFileCheckpoints: Boolean(diagnosticSessionId),
hasFileCheckpoints: (fileCheckpointSummary?.count ?? 0) > 0,
});
const openFileCheckpoints = diagnosticSessionId
? () => setFileCheckpointDialogOpen(true)
@@ -298,7 +298,7 @@ export function GeneralWorkbenchDialogSection({
executionRuntime?.execution_strategy === "code_orchestrated" ||
panelBaseProps.diagnosticRuntimeContext?.executionStrategy ===
"code_orchestrated",
hasFileCheckpoints: Boolean(diagnosticSessionId),
hasFileCheckpoints: (fileCheckpointSummary?.count ?? 0) > 0,
});
const openFileCheckpoints = diagnosticSessionId
? () => setFileCheckpointDialogOpen(true)