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

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
- {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
+
-