From 4681135423ba1931e1a24637060306eca746bb25 Mon Sep 17 00:00:00 2001 From: Yaowei Zheng Date: Wed, 29 Jul 2026 18:03:41 +0800 Subject: [PATCH] chore: make non-i18n code English-only (#117) Co-authored-by: Claude Opus 5 (1M context) --- packages/cli/test/render.test.ts | 6 ++- packages/core/test/describe-image.test.ts | 14 +++--- packages/core/test/markers.test.ts | 32 +++++++------ packages/core/test/session-title.test.ts | 28 +++++------ packages/landing/src/lib/blog.ts | 5 +- packages/landing/src/sections/hero.tsx | 2 +- packages/landing/test/blog.test.ts | 6 +-- packages/server/test/models.test.ts | 46 +++++++++---------- packages/server/test/title-generator.test.ts | 28 +++++------ .../web/src/components/layout/sidebar.tsx | 2 +- .../features/agents/agent-settings-page.tsx | 2 +- .../src/features/chat/approval-buttons.tsx | 2 +- packages/web/src/features/chat/draft-view.tsx | 4 +- .../web/src/features/chat/tool-call-card.tsx | 2 +- .../web/src/features/skills/skills-page.tsx | 26 +++++------ packages/web/src/lib/omni/task-stats.ts | 8 ++-- packages/web/src/lib/strings-en.ts | 2 +- packages/web/test/skill-slash.test.ts | 2 +- packages/web/test/task-stats.test.ts | 2 +- 19 files changed, 113 insertions(+), 106 deletions(-) diff --git a/packages/cli/test/render.test.ts b/packages/cli/test/render.test.ts index c78c8c7..2824169 100644 --- a/packages/cli/test/render.test.ts +++ b/packages/cli/test/render.test.ts @@ -147,12 +147,14 @@ describe("StreamRenderer", () => { partialToolCall({ eventType: "delta", name: "", - arguments: '"description":"列出当前目录的文件"}', + arguments: '"description":"List files in the current directory"}', toolCallId: "c9", }), ); r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "c9" })); - expect(stripAnsi(text())).toBe("[tool-c9] exec_command <- 列出当前目录的文件 ($ ls -la)\n"); + expect(stripAnsi(text())).toBe( + "[tool-c9] exec_command <- List files in the current directory ($ ls -la)\n", + ); }); it("streams the command live when the schema has no description argument", () => { diff --git a/packages/core/test/describe-image.test.ts b/packages/core/test/describe-image.test.ts index 01fdcc2..f6d4028 100644 --- a/packages/core/test/describe-image.test.ts +++ b/packages/core/test/describe-image.test.ts @@ -129,10 +129,10 @@ describe("describe_image (the text-only-model variant of read_image)", () => { it("image + custom prompt is sent to the vision model in a single shot, its text comes back, the result carries no images", async () => { await writeFile(path.join(tmp, "a.png"), PNG_1X1); - const { llm, calls } = fakeLLM("图里是一只企鹅。"); + const { llm, calls } = fakeLLM("The image shows a penguin."); const describer: VisionDescriberService = { modelId: "vis-1", createLLM: () => llm }; const { messages, result, text } = await run( - { source: "a.png", prompt: "图里是什么动物?" }, + { source: "a.png", prompt: "What animal is in the image?" }, tmp, describer, ); @@ -143,17 +143,17 @@ describe("describe_image (the text-only-model variant of read_image)", () => { (m) => m.payload as { type: string; text?: string; image_url?: string }, ); expect(payloads[0]!.type).toBe("text"); - expect(payloads[0]!.text).toBe("图里是什么动物?"); + expect(payloads[0]!.text).toBe("What animal is in the image?"); expect(payloads[1]!.type).toBe("image_url"); expect(payloads[1]!.image_url).toBe(`data:image/png;base64,${PNG_1X1.toString("base64")}`); expect(text).toContain("described by vis-1"); - expect(text).toContain("图里是一只企鹅。"); + expect(text).toContain("The image shows a penguin."); // Streaming forward: the header line and description deltas are emitted as separate chunks (not buffered as a whole), and the complete text is not forwarded again. const outputs = messages.map((m) => (m.payload as { output?: string }).output ?? ""); expect(outputs.length).toBeGreaterThanOrEqual(3); // header + >=2 description delta chunks expect(outputs[0]).toContain("described by vis-1"); - expect(outputs.slice(1).join("")).toBe("图里是一只企鹅。"); + expect(outputs.slice(1).join("")).toBe("The image shows a penguin."); // Text-based description: the result carries no images (images never enter session history). expect(result?.images).toBeUndefined(); expect(result?.stopReason).toBeUndefined(); // defaults to completed @@ -200,7 +200,7 @@ describe("describe_image (the text-only-model variant of read_image)", () => { it("Environment assembles the describe_image entry with the delegated-description implementation; definition matches the config", async () => { await writeFile(path.join(tmp, "a.png"), PNG_1X1); - const { llm } = fakeLLM("代读结果"); + const { llm } = fakeLLM("delegated description result"); const env = new Environment({ workspaceDir: tmp, toolConfig: { @@ -235,7 +235,7 @@ describe("describe_image (the text-only-model variant of read_image)", () => { }; expect(complete.type).toBe("tool_call_output"); expect(complete.stop_reason).toBe("completed"); - expect(complete.output).toContain("代读结果"); + expect(complete.output).toContain("delegated description result"); expect(complete.images).toBeUndefined(); }); }); diff --git a/packages/core/test/markers.test.ts b/packages/core/test/markers.test.ts index 6ea9506..1487ce2 100644 --- a/packages/core/test/markers.test.ts +++ b/packages/core/test/markers.test.ts @@ -79,19 +79,19 @@ describe("stripConversationMarkers (whole-message title cleaning)", () => { // The skill-invocation block that wraps a first user message must not reach the title. expect( stripConversationMarkers( - "[use_skills]\nskills: penguin-sdk, web-design\n[/use_skills]\n做一个 RAG 应用", + "[use_skills]\nskills: penguin-sdk, web-design\n[/use_skills]\nBuild a RAG app", ), - ).toBe("做一个 RAG 应用"); + ).toBe("Build a RAG app"); // Handoff and scheduled-task markers are stripped too; ordinary bracketed text stays. - expect(stripConversationMarkers("[handoff_from]data_analyst[/handoff_from]继续分析")).toBe( - "继续分析", - ); + expect( + stripConversationMarkers("[handoff_from]data_analyst[/handoff_from]continue the analysis"), + ).toBe("continue the analysis"); // The /model switch origin block (the new session's first message) must not leak into the title either. expect( stripConversationMarkers( - "[model_switch_from]\nsession: session-01\ntrace: /t/x_001.jsonl\n[/model_switch_from]\n继续这个任务", + "[model_switch_from]\nsession: session-01\ntrace: /t/x_001.jsonl\n[/model_switch_from]\ncontinue this task", ), - ).toBe("继续这个任务"); + ).toBe("continue this task"); expect(stripConversationMarkers("render a
element")).toBe("render a
element"); expect(stripConversationMarkers("check the [config] section")).toBe( "check the [config] section", @@ -100,14 +100,18 @@ describe("stripConversationMarkers (whole-message title cleaning)", () => { it("the old angle-bracket marker form is still stripped (material from old Traces)", () => { expect( - stripConversationMarkers("\nskills: web-design\n\n做一个落地页"), - ).toBe("做一个落地页"); - expect(stripConversationMarkers("data_analyst继续分析")).toBe( - "继续分析", - ); + stripConversationMarkers( + "\nskills: web-design\n\nBuild a landing page", + ), + ).toBe("Build a landing page"); expect( - stripConversationMarkers("session: s1继续这个任务"), - ).toBe("继续这个任务"); + stripConversationMarkers("data_analystcontinue the analysis"), + ).toBe("continue the analysis"); + expect( + stripConversationMarkers( + "session: s1continue this task", + ), + ).toBe("continue this task"); }); }); diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index 6d52f19..f4e3d66 100644 --- a/packages/core/test/session-title.test.ts +++ b/packages/core/test/session-title.test.ts @@ -64,18 +64,18 @@ describe("session-title", () => { const result = await generateTitleWithLLM( fakeLLM( [ - thinkingMessage("想一下"), // thinking does not count - assistantText("「Tailwind 主题配置」。"), + thinkingMessage("thinking"), // thinking does not count + assistantText("「Tailwind theme setup」。"), tokenUsage(emptyTokenCounts(), { cache_read: 1, cache_write: 2, output: 3, total: 6 }), ], { status: "completed" }, seen, ), - { userText: "解释 @theme", assistantText: "好的……" }, + { userText: "explain @theme", assistantText: "sure thing." }, ); - expect(result.title).toBe("Tailwind 主题配置"); + expect(result.title).toBe("Tailwind theme setup"); expect(result.usage).toEqual({ cache_read: 1, cache_write: 2, output: 3, total: 6 }); - expect(seen[0]).toBe(buildTitlePrompt("解释 @theme", "好的……")); + expect(seen[0]).toBe(buildTitlePrompt("explain @theme", "sure thing.")); expect(seen[0]).toContain("SAME language"); }); @@ -105,23 +105,23 @@ describe("session-title", () => { it("still generates with empty assistant material (tool-only turn): uses only the user request, prompt omits the assistant section", async () => { const seen: string[] = []; const result = await generateTitleWithLLM( - fakeLLM([assistantText("配置 Tailwind 主题")], { status: "completed" }, seen), - { userText: "帮我配置 @theme", assistantText: "" }, + fakeLLM([assistantText("Configure the Tailwind theme")], { status: "completed" }, seen), + { userText: "help me configure @theme", assistantText: "" }, ); - expect(result.title).toBe("配置 Tailwind 主题"); - expect(seen[0]).toBe(buildTitlePrompt("帮我配置 @theme", "")); + expect(result.title).toBe("Configure the Tailwind theme"); + expect(seen[0]).toBe(buildTitlePrompt("help me configure @theme", "")); expect(seen[0]).not.toContain("[Assistant]"); }); it("sanitizeTitle: strips quotes/punctuation to a fixed point, collapses whitespace, truncates overlong input, returns null for empty", () => { - expect(sanitizeTitle("“ 构建配置 说明 。”")).toBe("构建配置 说明"); - expect(sanitizeTitle("『标题』!")).toBe("标题"); + expect(sanitizeTitle("“ Build config notes 。”")).toBe("Build config notes"); + expect(sanitizeTitle("『Title』!")).toBe("Title"); expect(sanitizeTitle(" \n ")).toBeNull(); expect(sanitizeTitle("x".repeat(50))).toHaveLength(30); // A leaked [use_skills] block is stripped from the model output. - expect(sanitizeTitle("[use_skills]\nskills: web-design\n[/use_skills]\n构建落地页")).toBe( - "构建落地页", - ); + expect( + sanitizeTitle("[use_skills]\nskills: web-design\n[/use_skills]\nBuild a landing page"), + ).toBe("Build a landing page"); }); it("Session.generateTitle: sends via createBareLLM; returns null when no factory is provided", async () => { diff --git a/packages/landing/src/lib/blog.ts b/packages/landing/src/lib/blog.ts index 34a7e09..d3919d7 100644 --- a/packages/landing/src/lib/blog.ts +++ b/packages/landing/src/lib/blog.ts @@ -36,7 +36,7 @@ export function parseAuthors(raw: string | undefined): string[] { return authors.length > 0 ? authors : [DEFAULT_AUTHOR]; } -/** Join authors for display: 顿号 in Chinese, comma otherwise. */ +/** Join authors for display: an ideographic comma in Chinese, an ASCII comma otherwise. */ export function formatAuthors(authors: string[], locale: Locale): string { return authors.join(locale === "zh" ? "、" : ", "); } @@ -82,7 +82,8 @@ export function comparePosts( } /** - * Format a YYYY-MM-DD post date for display ("July 20, 2026" / "2026年7月20日"). + * Format a YYYY-MM-DD post date for display (e.g. "July 20, 2026" in English, the + * year-month-day form in Chinese). * Parsed as UTC and formatted in UTC so the calendar day never shifts with the * viewer's timezone; unexpected input falls back to the raw string. */ diff --git a/packages/landing/src/sections/hero.tsx b/packages/landing/src/sections/hero.tsx index cf2b911..8421319 100644 --- a/packages/landing/src/sections/hero.tsx +++ b/packages/landing/src/sections/hero.tsx @@ -1,7 +1,7 @@ /** * Hero: enlarged logo + product name, the slogan inside the pill badge, then the * one-line headline whose rotating word crossfades through a gaussian blur - * (Desktop <-> Server / 桌面 <-> 服务器), three keywords, the install one-liner + * (Desktop <-> Server, localized per dictionary), three keywords, the install one-liner * and stats. The rotating word is a stacked inline-grid so line width never jumps. */ import { Fragment, useEffect, useState } from "react"; diff --git a/packages/landing/test/blog.test.ts b/packages/landing/test/blog.test.ts index 403d6e1..bc7f8e2 100644 --- a/packages/landing/test/blog.test.ts +++ b/packages/landing/test/blog.test.ts @@ -36,7 +36,7 @@ describe("formatPostDate", () => { expect(formatPostDate("2026-07-20", "en")).toBe("July 20, 2026"); }); - it("formats zh dates as 年月日", () => { + it("formats zh dates as year-month-day", () => { expect(formatPostDate("2026-06-18", "zh")).toBe("2026年6月18日"); expect(formatPostDate("2026-07-20", "zh")).toBe("2026年7月20日"); }); @@ -68,9 +68,9 @@ describe("parseAuthors / formatAuthors", () => { expect(parseAuthors(" ")).toEqual([DEFAULT_AUTHOR]); }); - it("joins with a comma in English and 顿号 in Chinese", () => { + it("joins with a comma in English and an ideographic comma in Chinese", () => { expect(formatAuthors(["A", "B"], "en")).toBe("A, B"); - expect(formatAuthors(["甲", "乙"], "zh")).toBe("甲、乙"); + expect(formatAuthors(["A", "B"], "zh")).toBe("A、B"); expect(formatAuthors(["A"], "en")).toBe("A"); }); }); diff --git a/packages/server/test/models.test.ts b/packages/server/test/models.test.ts index 2b1d1c5..73e2031 100644 --- a/packages/server/test/models.test.ts +++ b/packages/server/test/models.test.ts @@ -45,7 +45,7 @@ describe("models preset & catalog enrichment", () => { const { cookie } = await provisionUser(t.app, "alice"); api = apiClient(t.app, cookie); const created = (await ( - await api.post("/api/projects", { projectId: "alice-preset", name: "预置项目" }) + await api.post("/api/projects", { projectId: "alice-preset", name: "Preset project" }) ).json()) as ProjectCreateResponse; projectId = created.project.projectId; }); @@ -53,7 +53,7 @@ describe("models preset & catalog enrichment", () => { await t.cleanup(); }); - it("credential 内联单文件:PUT 的 apiKey 落 .project_config.toml(0600),GET 只回掩码", async () => { + it("credentials are inlined in one file: the apiKey from PUT lands in .project_config.toml (0600), GET returns only a mask", async () => { const put = await api.put(url(), { defaultModel: { provider: "custom", modelId: "m-inline" }, models: [ @@ -93,7 +93,7 @@ describe("models preset & catalog enrichment", () => { await expect(readFile(path.join(projectDir, "project_config.toml"), "utf8")).rejects.toThrow(); }); - it("新建 Project 即预置全部内置模型(provider 与 model_id 分列 + 目录信息)", async () => { + it("a new Project is preset with every built-in model (provider and model_id as separate fields + catalog info)", async () => { const res = await api.get(url()); expect(res.status).toBe(200); const body = (await res.json()) as ModelsResponse; @@ -125,7 +125,7 @@ describe("models preset & catalog enrichment", () => { expect(mimo.credential?.apiKeyMasked).toBeUndefined(); }); - it("PUT 自定义模型:持久化 vision 标注;openai 协议给 OPENAI_API_KEY 兜底;provider 必填", async () => { + it("PUT a custom model: the vision flag persists; the openai protocol falls back to OPENAI_API_KEY; provider is required", async () => { const put = await api.put(url(), { defaultModel: { provider: "custom", modelId: "my-model" }, models: [ @@ -167,7 +167,7 @@ describe("models preset & catalog enrichment", () => { expect(noProvider.status).toBe(400); }); - it("PUT maxTokens:落盘为 max_tokens 并经 GET 回读;整表省略即清除;0/负数/非数字 400", async () => { + it("PUT maxTokens: persisted as max_tokens and read back through GET; omitting it table-wide clears it; 0 / negative / non-numeric 400", async () => { const put = await api.put(url(), { models: [ { provider: "custom", modelId: "local-qwen", clientType: "openai", maxTokens: 8000 }, @@ -204,7 +204,7 @@ describe("models preset & catalog enrichment", () => { } }); - it("同名 model_id 可在不同 provider 下并存(成对键,互不覆盖)", async () => { + it("the same model_id can coexist under different providers (paired keys, neither overwrites the other)", async () => { const put = await api.put(url(), { models: [ { provider: "moonshot", modelId: "kimi-k2.6", apiKey: "sk-official-aaaa1111" }, @@ -231,7 +231,7 @@ describe("models preset & catalog enrichment", () => { }); }); -describe("default_project 预置", () => { +describe("default_project presets", () => { let t: TestApp; let prevKey: string | undefined; @@ -247,7 +247,7 @@ describe("default_project 预置", () => { await t.cleanup(); }); - it("种子 admin 纳管 default_project 时补齐预置模型与默认模型(此前无 .project_config.toml)", async () => { + it("when the seeded admin adopts default_project, the preset models and the default model are filled in (no prior .project_config.toml)", async () => { // The default_project shared by admin seeding and the CLI (the dir already exists, so writeInitialConfig is skipped). t = await createTestApp(); const { cookie } = await loginAdmin(t.app); @@ -269,7 +269,7 @@ describe("default_project 预置", () => { expect(session.modelId).toBe("deepseek-v4-pro"); }); - it("已配置过模型的 default_project 原样保留(不覆盖 CLI 既有配置)", async () => { + it("a default_project that already has models configured is left untouched (existing CLI config is not overwritten)", async () => { // First have the "CLI" write a config with a single custom model, then admin seeding adopts it. t = await createTestApp({ beforeSeed: async (root) => { @@ -290,7 +290,7 @@ describe("default_project 预置", () => { }); }); -describe("模型引用改键与连通性测试", () => { +describe("model-reference rekeying and the connectivity test", () => { let t: TestApp; let api: ReturnType; let projectId: string; @@ -302,7 +302,7 @@ describe("模型引用改键与连通性测试", () => { const { cookie } = await provisionUser(t.app, "carol"); api = apiClient(t.app, cookie); const created = (await ( - await api.post("/api/projects", { projectId: "carol-rename", name: "改名项目" }) + await api.post("/api/projects", { projectId: "carol-rename", name: "Rename project" }) ).json()) as ProjectCreateResponse; projectId = created.project.projectId; }); @@ -310,7 +310,7 @@ describe("模型引用改键与连通性测试", () => { await t.cleanup(); }); - it("renamedFrom 迁移 credential 与配置,默认/视觉模型指针跟随改键", async () => { + it("renamedFrom migrates the credential and the config; the default / vision model pointers follow the rekey", async () => { await api.put(url(), { defaultModel: { provider: "custom", modelId: "old-id" }, visionModel: { provider: "custom", modelId: "old-id" }, @@ -347,7 +347,7 @@ describe("模型引用改键与连通性测试", () => { expect(again.defaultModel).toEqual({ provider: "custom", modelId: "new-id" }); }); - it("分组变更也是改键:credential 随迁;envKey 按 client 解析、不随分组(PRN-021)", async () => { + it("a group change is a rekey too: the credential migrates with it; envKey resolves by client, not by group (PRN-021)", async () => { // Move the preset DeepSeek model to another group (changing provider is a key change; the paired renamedFrom migrates it). const put = await api.put(url(), { models: [ @@ -384,28 +384,28 @@ describe("模型引用改键与连通性测试", () => { expect(moved.envKey).toBe("DEEPSEEK_API_KEY"); }); - it("展示名可编辑:与内置目录一致时不落盘;provider 恒作为条目字段落盘", async () => { + it("the display name is editable: not persisted when it matches the built-in catalog; provider is always persisted as an entry field", async () => { // Preset model: saved as-is → the display name falls back to the built-in catalog, and display_name isn't written to the config file. await api.put(url(), { models: [ { provider: "openai", modelId: "gpt-5.5", displayName: "GPT-5.5" }, - { provider: "openai", modelId: "gpt-5.4", displayName: "我的 GPT" }, + { provider: "openai", modelId: "gpt-5.4", displayName: "My GPT" }, ], }); const body = (await (await api.get(url())).json()) as ModelsResponse; expect(pick(body, "openai", "gpt-5.5").displayName).toBe("GPT-5.5"); // Edited one: the display name takes effect per the user's setting. - expect(pick(body, "openai", "gpt-5.4").displayName).toBe("我的 GPT"); + expect(pick(body, "openai", "gpt-5.4").displayName).toBe("My GPT"); // Clean on disk: unchanged preset models don't write display_name; provider is stored as a separate column, no concatenated string. const toml = await readFile(path.join(t.root, projectId, ".project_config.toml"), "utf8"); expect(toml).not.toContain('display_name = "GPT-5.5"'); - expect(toml).toContain('display_name = "我的 GPT"'); + expect(toml).toContain('display_name = "My GPT"'); expect(toml).toContain('provider = "openai"'); expect(toml).not.toContain("openai/gpt-5.5"); }); - it("renamedFrom 非法值 400;不带 renamedFrom 改键等于删旧建新(credential 不迁移)", async () => { + it("an invalid renamedFrom is 400; rekeying without renamedFrom equals delete-old-then-create-new (the credential does not migrate)", async () => { await api.put(url(), { models: [{ provider: "custom", modelId: "m-a", apiKey: "sk-secret-abcd1234" }], }); @@ -426,7 +426,7 @@ describe("模型引用改键与连通性测试", () => { expect(body.models[0]!.credential).toBeUndefined(); }); - it("连通性测试发的是条目的上游 model_id(引用成对随请求体)", async () => { + it("the connectivity test sends the entry's upstream model_id (the reference pair travels with the request body)", async () => { // Local openai-compatible endpoint: records the model field from the request body, then always rejects with 401 (never hits the network). const seenModels: string[] = []; const server = createServer((req, res) => { @@ -472,7 +472,7 @@ describe("模型引用改键与连通性测试", () => { } }); - it("连通性测试请求体不含 tools 与 tool_choice(空工具列表整体省略,vLLM 等严格端点不再 400)", async () => { + it("the connectivity-test request body carries no tools or tool_choice (an empty tool list is omitted entirely, so strict endpoints such as vLLM no longer 400)", async () => { // The probe runs with an empty tool list. The wire body must omit `tools` entirely — // `tools: []` is rejected by strict OpenAI-compatible servers (vLLM: "tools must not be an // empty array") — and must never carry `tool_choice`. @@ -512,7 +512,7 @@ describe("模型引用改键与连通性测试", () => { } }); - it("连通性测试:已保存的模型与**尚未保存**的自定义模型都可测(LLM 层不抛异常,一律收敛)", async () => { + it("connectivity test: both a saved model and a **not-yet-saved** custom model can be tested (the LLM layer throws nothing, every outcome converges)", async () => { await api.put(url(), { models: [{ provider: "openai", modelId: "gpt-5.5", apiKey: "sk-invalid-key-for-test" }], }); @@ -536,7 +536,7 @@ describe("模型引用改键与连通性测试", () => { expect(typeof unsavedBody.message).toBe("string"); }, 40_000); - it("连通性测试:模型完全没有 credential 时收敛为 ok:false,而非 500", async () => { + it("connectivity test: a model with no credential at all converges to ok:false instead of 500", async () => { // A model using the OpenAI protocol: the provider SDK throws at **client construction** because // the key is missing — if that construction were outside the try it would bubble up as a 500. Clear the env-var key so there's nowhere to get one (no real network request). const prev = process.env.OPENAI_API_KEY; @@ -555,7 +555,7 @@ describe("模型引用改键与连通性测试", () => { } }); - it("连通性测试:clearApiKey 时不回落已存 key(照当前草稿测)", async () => { + it("connectivity test: clearApiKey does not fall back to the stored key (the current draft is what gets tested)", async () => { // A key is already saved, but the test request carries clearApiKey — the server must **not** use // the saved key. Clear the env var so "don't use the saved key" == no credential at all, so construction synchronously throws missing-credential (no network request, deterministic). const prev = process.env.OPENAI_API_KEY; diff --git a/packages/server/test/title-generator.test.ts b/packages/server/test/title-generator.test.ts index 97ddece..fb39a54 100644 --- a/packages/server/test/title-generator.test.ts +++ b/packages/server/test/title-generator.test.ts @@ -100,21 +100,21 @@ describe("title-generator", () => { CTX, fakeSession( { - title: "Tailwind 主题配置", + title: "Tailwind theme setup", usage: { cache_read: 1, cache_write: 2, output: 3, total: 6 }, }, calls, ), - { fallbackText: "解释一下 @theme" }, + { fallbackText: "explain @theme" }, ); await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null); - expect(sessions.findById(ROW.sessionId)?.title).toBe("Tailwind 主题配置"); + expect(sessions.findById(ROW.sessionId)?.title).toBe("Tailwind theme setup"); // No material override passed: generateTitle is called with no argument, and the core Session gathers its own material. expect(calls.args[0]).toBeUndefined(); expect( serverEvents(events).some( - (e) => e.type === "session_title" && e.title === "Tailwind 主题配置", + (e) => e.type === "session_title" && e.title === "Tailwind theme setup", ), ).toBe(true); // usage is converted into token_usage and handed to the recorder (metered normally, same as a real call). @@ -125,14 +125,14 @@ describe("title-generator", () => { it("material override (sub-session scenario) is passed to generateTitle verbatim", async () => { const calls = { count: 0, args: [] as unknown[] }; const gen = makeGenerator(); - const material = { userText: "子会话 prompt", assistantText: "子会话回答" }; - gen.maybeGenerate(CTX, fakeSession({ title: "子标题", usage: null }, calls), { - fallbackText: "子会话 prompt", + const material = { userText: "sub-session prompt", assistantText: "sub-session reply" }; + gen.maybeGenerate(CTX, fakeSession({ title: "Sub title", usage: null }, calls), { + fallbackText: "sub-session prompt", material, }); await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null); expect(calls.args[0]).toEqual({ material }); - expect(sessions.findById(ROW.sessionId)?.title).toBe("子标题"); + expect(sessions.findById(ROW.sessionId)?.title).toBe("Sub title"); }); it("an existing title is never regenerated (no one-shot request issued)", async () => { @@ -155,11 +155,11 @@ describe("title-generator", () => { { title: null, usage: { cache_read: 0, cache_write: 0, output: 1, total: 1 } }, calls, ), - { fallbackText: "配置 Tailwind 主题\n第二行" }, + { fallbackText: "Configure the Tailwind theme\nsecond line" }, ); // Fallback = the material's first non-empty line, sanitized and truncated, guaranteeing a title is always produced. await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null); - expect(sessions.findById(ROW.sessionId)?.title).toBe("配置 Tailwind 主题"); + expect(sessions.findById(ROW.sessionId)?.title).toBe("Configure the Tailwind theme"); // The one-off request's usage is still recorded normally. await waitFor(() => recorded.length > 0); }); @@ -168,21 +168,21 @@ describe("title-generator", () => { const calls = { count: 0, args: [] as unknown[] }; const gen = makeGenerator(); gen.maybeGenerate(CTX, fakeSession({ title: null, usage: null }, calls), { - fallbackText: "[use_skills]\nskills: web-design\n[/use_skills]\n做一个落地页", + fallbackText: "[use_skills]\nskills: web-design\n[/use_skills]\nBuild a landing page", }); await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null); // Not "[use_skills]" — the marker block is removed before the first line is taken. - expect(sessions.findById(ROW.sessionId)?.title).toBe("做一个落地页"); + expect(sessions.findById(ROW.sessionId)?.title).toBe("Build a landing page"); }); it("fallback also strips the legacy angle-bracket block (material from old Traces)", async () => { const calls = { count: 0, args: [] as unknown[] }; const gen = makeGenerator(); gen.maybeGenerate(CTX, fakeSession({ title: null, usage: null }, calls), { - fallbackText: "\nskills: web-design\n\n做一个落地页", + fallbackText: "\nskills: web-design\n\nBuild a landing page", }); await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null); - expect(sessions.findById(ROW.sessionId)?.title).toBe("做一个落地页"); + expect(sessions.findById(ROW.sessionId)?.title).toBe("Build a landing page"); }); it("LLM returns null and the fallback material is blank → the title stays NULL (retryable next time)", async () => { diff --git a/packages/web/src/components/layout/sidebar.tsx b/packages/web/src/components/layout/sidebar.tsx index be5a754..7a3c6fd 100644 --- a/packages/web/src/components/layout/sidebar.tsx +++ b/packages/web/src/components/layout/sidebar.tsx @@ -1236,7 +1236,7 @@ export function Sidebar({ * pinned indicator. The header row carries the `group/header` scope so the reveal only * reacts to its own row, not to the session rows' plain `group` scope. * The accessible name stays STATIC and aria-pressed alone carries the state (the toggle - * pattern the grouping-mode buttons use) — a name that swaps 置顶/取消置顶 alongside + * pattern the grouping-mode buttons use) — a name that swaps Pin/Unpin alongside * aria-pressed reads as "Unpin group, pressed", saying the state twice in conflicting * ways. The title tooltip may still swap: it is presentation for pointer users and does * not feed the accessible name while aria-label is present. diff --git a/packages/web/src/features/agents/agent-settings-page.tsx b/packages/web/src/features/agents/agent-settings-page.tsx index 0503313..041faa2 100644 --- a/packages/web/src/features/agents/agent-settings-page.tsx +++ b/packages/web/src/features/agents/agent-settings-page.tsx @@ -44,7 +44,7 @@ type TabKey = "overview" | "prompt" | "runtime" | "tools" | "vault" | "schedules * The "" (not-overridden / inherit) row is filtered out per review — the menus offer only * concrete values in dictionary order, the user picks explicitly. An unset stored value * simply matches no row, so the OptionMenu trigger falls back to its placeholder - * ((缺省)/(default), the same convention as the tools-table permission menu); nothing is + * ("(default)", the same convention as the tools-table permission menu); nothing is * ever written silently, and the reset link next to each menu rewinds a local pick back to "". */ export function optionRows( diff --git a/packages/web/src/features/chat/approval-buttons.tsx b/packages/web/src/features/chat/approval-buttons.tsx index 5269a8a..3a93531 100644 --- a/packages/web/src/features/chat/approval-buttons.tsx +++ b/packages/web/src/features/chat/approval-buttons.tsx @@ -24,7 +24,7 @@ export function ApprovalButtons({ }; // Text at every breakpoint (per review): action buttons the user presses must read as words — - // "允许/Allow" and "拒绝/Deny" — never as bare glyphs; iconic shorthand is reserved for + // "Allow" and "Deny" — never as bare glyphs; iconic shorthand is reserved for // passive indicators. The buttons live on their own row under the (wrapping) argument // preview, so the words cost no width the pending card doesn't already have. return ( diff --git a/packages/web/src/features/chat/draft-view.tsx b/packages/web/src/features/chat/draft-view.tsx index 62be940..6790618 100644 --- a/packages/web/src/features/chat/draft-view.tsx +++ b/packages/web/src/features/chat/draft-view.tsx @@ -683,8 +683,8 @@ const versionBadgeClass = "ml-1.5 inline-block rounded-full bg-[var(--accent-bg)] px-1.5 align-super text-[10px] font-medium leading-4 text-[var(--accent-fg)] transition-opacity duration-150 hover:opacity-80"; /** - * Quiet version line under the brand subtitle: `vX.Y.Z · 最近更新日期 7 月 26 日` / - * `… · Last updated Jul 26`. The product name is not repeated here — the brand wordmark + * Quiet version line under the brand subtitle: `vX.Y.Z · Last updated Jul 26` + * (localized per dictionary). The product name is not repeated here — the brand wordmark * sits directly above, and the sidebar's version footer is bare `vX.Y.Z` too. The date is * the running version's release * date, stamped into core's BUILD_DATE at build time — displayed as-is, no network; diff --git a/packages/web/src/features/chat/tool-call-card.tsx b/packages/web/src/features/chat/tool-call-card.tsx index ea2cb4e..f35dce7 100644 --- a/packages/web/src/features/chat/tool-call-card.tsx +++ b/packages/web/src/features/chat/tool-call-card.tsx @@ -205,7 +205,7 @@ export function ToolCallCard({ item, ctx }: { item: ToolCallItem; ctx: StreamRen : failed ? "failed" : "done"; - // Decision wording ("Approved · manual" / "已拒绝 · 手动" …): carried ONLY by the left status + // Decision wording ("Approved · manual", "Denied · manual", …): carried ONLY by the left status // icon's title/aria-label — per review the row shows no visible decision text at any // breakpoint; the icon is the single source of truth for how the call was decided. const decisionText = item.decision diff --git a/packages/web/src/features/skills/skills-page.tsx b/packages/web/src/features/skills/skills-page.tsx index 76c7e9b..786a79b 100644 --- a/packages/web/src/features/skills/skills-page.tsx +++ b/packages/web/src/features/skills/skills-page.tsx @@ -21,16 +21,16 @@ * Agent's v_old → v_new and warns the overwriting reinstall drops local * edits), then reinstalls the current library copy on every outdated Agent * (install-again-is-update semantics), with a single success toast; the - * manage-installs Modal marks outdated rows with an accent "更新"/"Update" - * button doing the same per Agent (through the same confirm); + * manage-installs Modal marks outdated rows with an accent "Update" button + * doing the same per Agent (through the same confirm); * - Paper plane "quick invoke": enters /chat/new draft mode with default_agent, * pre-selects the skill, and pre-fills the invocation text per UI language - * (zh "使用 X 技能" / en "use the X skill", overwriting any existing draft body); + * ("use the X skill" in the active dictionary, overwriting any existing draft body); * - Download "manage installs": a Modal listing every Agent in the current - * Project — not-installed shows "安装"/"Install", installed shows - * "已安装"/"Installed" (hover switches to "卸载"/"Uninstall", click to - * uninstall); any member can operate it; optimistic update, a top-level - * toast on success for install/uninstall, rollback plus a toast on failure. + * Project — not-installed shows "Install", installed shows "Installed" + * (hover switches to "Uninstall", click to uninstall); any member can + * operate it; optimistic update, a top-level toast on success for + * install/uninstall, rollback plus a toast on failure. */ import { useEffect, useState } from "react"; import { useNavigate } from "react-router"; @@ -550,11 +550,11 @@ function SkillCard({ /** * One Agent row in the "manage installs" Modal: not-installed shows - * "安装"/"Install"; installed shows "已安装"/"Installed", switching to - * "卸载"/"Uninstall" on hover (same button, click to uninstall); an installed - * copy older than the library additionally shows an accent "更新"/"Update" - * button (reinstall = update). Install and uninstall go through optimistic - * updates (toggleInstall), rolling back on failure. + * "Install"; installed shows "Installed", switching to "Uninstall" on hover + * (same button, click to uninstall); an installed copy older than the library + * additionally shows an accent "Update" button (reinstall = update). Install + * and uninstall go through optimistic updates (toggleInstall), rolling back on + * failure. */ function InstallRow({ agentId, @@ -589,7 +589,7 @@ function InstallRow({ )} {installed ? ( - // group: on hover the button's copy switches "已安装"/"Installed" → "卸载"/"Uninstall" (the same button carries the uninstall action). + // group: on hover the button's copy switches "Installed" → "Uninstall" (the same button carries the uninstall action).