From b894565a85a595d6c361da107399bb029f9cbb02 Mon Sep 17 00:00:00 2001 From: Suzuku <280906548+ZilaiWang@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:53:30 +0800 Subject: [PATCH] add bilingual Chinese Studio interface --- README.md | 4 ++ apps/studio/e2e/studio.spec.ts | 13 ++++ apps/studio/src/components/CommandPalette.tsx | 26 +++---- apps/studio/src/components/SemanticDiff.tsx | 6 +- apps/studio/src/components/Shell.tsx | 32 ++++++--- apps/studio/src/components/ui.tsx | 21 ++++-- apps/studio/src/i18n.test.ts | 12 ++++ apps/studio/src/i18n.tsx | 71 +++++++++++++++++++ apps/studio/src/main.tsx | 3 +- apps/studio/src/navigation.ts | 20 +++--- apps/studio/src/pages/CandidatesPage.tsx | 42 +++++------ apps/studio/src/pages/ConceptsPage.tsx | 22 +++--- apps/studio/src/pages/DraftsPage.tsx | 12 ++-- apps/studio/src/pages/ExchangePage.tsx | 28 ++++---- apps/studio/src/pages/RelationsPage.tsx | 24 ++++--- apps/studio/src/pages/ReleasesPage.tsx | 18 ++--- apps/studio/src/pages/ReviewPage.tsx | 10 +-- apps/studio/src/pages/RuntimePage.tsx | 4 +- apps/studio/src/pages/SourcesPage.tsx | 29 ++++---- apps/studio/src/styles.css | 4 +- docs/studio.md | 5 ++ .../studio_assets/assets/index-BJzfA-mX.css | 1 - .../studio_assets/assets/index-BL_jwQtI.css | 1 + .../studio_assets/assets/index-CG7Z-BF3.js | 55 ++++++++++++++ .../studio_assets/assets/index-CogvhzVj.js | 55 -------------- src/termkit/studio_assets/index.html | 4 +- 26 files changed, 335 insertions(+), 187 deletions(-) create mode 100644 apps/studio/src/i18n.test.ts create mode 100644 apps/studio/src/i18n.tsx delete mode 100644 src/termkit/studio_assets/assets/index-BJzfA-mX.css create mode 100644 src/termkit/studio_assets/assets/index-BL_jwQtI.css create mode 100644 src/termkit/studio_assets/assets/index-CG7Z-BF3.js delete mode 100644 src/termkit/studio_assets/assets/index-CogvhzVj.js diff --git a/README.md b/README.md index 932c729..1485c54 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,10 @@ uv run termkit resolve \ quiet and keyboard-friendly; machine assistance belongs behind Forge and reconciliation capabilities, not at the center of the product. +Studio opens in Simplified Chinese by default. Use the language switch in the +top bar to move between 中文 and English; the preference is stored locally in +the browser and never changes canonical terminology data. + ### Review semantic changes ![TermKit Studio draft inbox](.github/screenshots/studio-drafts-v2.png) diff --git a/apps/studio/e2e/studio.spec.ts b/apps/studio/e2e/studio.spec.ts index be41941..2a09f20 100644 --- a/apps/studio/e2e/studio.spec.ts +++ b/apps/studio/e2e/studio.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from "@playwright/test"; test("navigates the curation workflow and command palette", async ({ page }) => { + await page.addInitScript(() => localStorage.setItem("termkit.studio.locale", "en")); await page.goto("/studio/#/concepts"); await expect(page.getByText("Canonical source valid")).toBeVisible(); @@ -22,6 +23,7 @@ test("navigates the curation workflow and command palette", async ({ page }) => }); test("Concepts 2.0 keeps its visual hierarchy", async ({ page }) => { + await page.addInitScript(() => localStorage.setItem("termkit.studio.locale", "en")); await page.goto("/studio/#/concepts"); await expect(page.getByRole("tab", { name: "Overview" })).toBeVisible(); @@ -33,3 +35,14 @@ test("Concepts 2.0 keeps its visual hierarchy", async ({ page }) => { ], }); }); + +test("defaults to Chinese and can switch back to English", async ({ page }) => { + await page.goto("/studio/#/concepts"); + await expect(page.getByRole("tab", { name: "概览" })).toBeVisible(); + await expect(page.getByText("规范源有效")).toBeVisible(); + + await page.getByRole("button", { name: "切换为英文" }).click(); + await expect(page.getByRole("tab", { name: "Overview" })).toBeVisible(); + await page.reload(); + await expect(page.getByRole("tab", { name: "Overview" })).toBeVisible(); +}); diff --git a/apps/studio/src/components/CommandPalette.tsx b/apps/studio/src/components/CommandPalette.tsx index 2ad343f..fc267b9 100644 --- a/apps/studio/src/components/CommandPalette.tsx +++ b/apps/studio/src/components/CommandPalette.tsx @@ -5,6 +5,7 @@ import { Command, Search, X } from "lucide-react"; import { navigate, navigation, type PageId } from "../navigation"; import { api } from "../api/client"; +import { useLocale } from "../i18n"; interface CommandPaletteProps { open: boolean; @@ -12,6 +13,7 @@ interface CommandPaletteProps { } export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { + const { locale, tr } = useLocale(); const [query, setQuery] = useState(""); useEffect(() => { if (!open) setQuery(""); @@ -28,7 +30,7 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { runtime: "compile resolve check termdb", releases: "validate build pack changelog" }; - return `open ${item.label} ${item.shortcut} ${aliases[item.id] ?? ""}`.toLowerCase().includes(query.toLowerCase()); + return `open ${item.label.en} ${item.label.zh} ${item.shortcut} ${aliases[item.id] ?? ""}`.toLowerCase().includes(query.toLowerCase()); }), [query]); const matchingSources = sources.data?.filter((item) => `${item.source.title} ${item.source.source_id}`.toLowerCase().includes(query.toLowerCase())).slice(0, 5) ?? []; const matchingDrafts = drafts.data?.filter((item) => `${item.reason} ${item.changeset_id} ${item.status}`.toLowerCase().includes(query.toLowerCase())).slice(0, 5) ?? []; @@ -44,39 +46,39 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { - Command palette + {tr("Command palette", "命令面板")}
setQuery(event.target.value)} - placeholder="Search commands…" + placeholder={tr("Search commands…", "搜索命令、概念或来源…")} /> - +
-
Open workspace
+
{tr("Open workspace", "打开工作区")}
{commands.map((item) => { const Icon = item.icon; return ( ); })} - {query.length > 1 && concepts.data?.items.length ? <>
Concepts
{concepts.data.items.map((item) => )} : null} - {query.length > 1 && matchingSources.length ? <>
Sources
{matchingSources.map((item) => )} : null} - {query.length > 1 && matchingDrafts.length ? <>
ChangeSets
{matchingDrafts.map((item) => )} : null} - {query.length > 1 && matchingReviews.length ? <>
Review tasks
{matchingReviews.map((item) => )} : null} - {commands.length === 0 && !concepts.data?.items.length && !matchingSources.length && !matchingDrafts.length && !matchingReviews.length &&
No matching object or command
} + {query.length > 1 && concepts.data?.items.length ? <>
{tr("Concepts", "概念")}
{concepts.data.items.map((item) => )} : null} + {query.length > 1 && matchingSources.length ? <>
{tr("Sources", "来源")}
{matchingSources.map((item) => )} : null} + {query.length > 1 && matchingDrafts.length ? <>
{tr("ChangeSets", "变更集")}
{matchingDrafts.map((item) => )} : null} + {query.length > 1 && matchingReviews.length ? <>
{tr("Review tasks", "审核任务")}
{matchingReviews.map((item) => )} : null} + {commands.length === 0 && !concepts.data?.items.length && !matchingSources.length && !matchingDrafts.length && !matchingReviews.length &&
{tr("No matching object or command", "没有匹配的对象或命令")}
}
diff --git a/apps/studio/src/components/SemanticDiff.tsx b/apps/studio/src/components/SemanticDiff.tsx index 361ef7d..2788f69 100644 --- a/apps/studio/src/components/SemanticDiff.tsx +++ b/apps/studio/src/components/SemanticDiff.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; import type { ChangeOperation } from "../api/types"; +import { useLocale } from "../i18n"; function display(value: unknown): string { if (Array.isArray(value)) return value.join(" · ") || "None"; @@ -15,6 +16,7 @@ export function SemanticDiff({ operations }: { operations: ChangeOperation[] }) } function SemanticOperation({ operation }: { operation: ChangeOperation }) { + const { tr } = useLocale(); const [raw, setRaw] = useState(false); const before = operation.before ?? {}; const after = operation.after ?? {}; @@ -22,8 +24,8 @@ function SemanticOperation({ operation }: { operation: ChangeOperation }) { .filter((field) => JSON.stringify(before[field]) !== JSON.stringify(after[field])); return
{operation.subject_kind.replaceAll("_", " ")}{operation.kind.replaceAll("_", " ")}
{operation.subject_id}
-
{fields.length ? fields.map((field) =>
{field.replaceAll("_", " ")}{display(before[field])}{display(after[field])}
) :

No field-level change.

}
- +
{fields.length ? fields.map((field) =>
{field.replaceAll("_", " ")}{display(before[field])}{display(after[field])}
) :

{tr("No field-level change.", "没有字段级变更。")}

}
+ {raw &&
{JSON.stringify(operation.before, null, 2)}
{JSON.stringify(operation.after, null, 2)}
}
; } diff --git a/apps/studio/src/components/Shell.tsx b/apps/studio/src/components/Shell.tsx index 4b29f6d..5eeb6b8 100644 --- a/apps/studio/src/components/Shell.tsx +++ b/apps/studio/src/components/Shell.tsx @@ -1,8 +1,9 @@ import type { ReactNode } from "react"; -import { Command, Search } from "lucide-react"; +import { Command, Languages, Search } from "lucide-react"; import type { WorkspaceSummary } from "../api/types"; import { navigate, navigation, productIcon, type PageId } from "../navigation"; +import { useLocale } from "../i18n"; import { StatusIndicator } from "./ui"; interface ShellProps { @@ -13,9 +14,15 @@ interface ShellProps { } export function Shell({ activePage, workspace, onOpenCommand, children }: ShellProps) { + const { locale, setLocale, tr } = useLocale(); const ProductIcon = productIcon; - const coordinate = workspace?.coordinate ?? "Opening workspace"; + const coordinate = workspace?.coordinate ?? tr("Opening workspace", "正在打开工作区"); const groups = ["CURATE", "KNOWLEDGE", "DELIVER"] as const; + const groupLabels = { + CURATE: tr("CURATE", "编审"), + KNOWLEDGE: tr("KNOWLEDGE", "知识"), + DELIVER: tr("DELIVER", "交付"), + }; return (
@@ -30,26 +37,29 @@ export function Shell({ activePage, workspace, onOpenCommand, children }: ShellP main
+ -