From 9d69bfe822dfafafc5d4324aa1a142672456de78 Mon Sep 17 00:00:00 2001 From: omartelo Date: Sat, 10 Oct 2026 00:11:30 -0300 Subject: [PATCH 1/2] feat(prompt): add Simplified Chinese prompt catalog --- internal/prompt/prompt.go | 6 +- internal/prompt/prompt_test.go | 3 +- internal/prompt/zh_cn.go | 122 +++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 internal/prompt/zh_cn.go diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index be2693a5..0159882b 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -20,10 +20,11 @@ const ( English Lang = "en" PortugueseBR Lang = "pt-BR" Spanish Lang = "es" + ChineseCN Lang = "zh-CN" ) // Langs is every language a catalog exists for, English first as the default. -var Langs = []Lang{English, PortugueseBR, Spanish} +var Langs = []Lang{English, PortugueseBR, Spanish, ChineseCN} // EnvVar is the variable lich exports into every session PTY with the prompt // language at spawn time. Text composed outside the lich process (the lich CLI, @@ -47,6 +48,7 @@ var catalogs = map[Lang]*Catalog{ English: &en, PortugueseBR: &ptBR, Spanish: &es, + ChineseCN: &zhCN, } // For is the catalog of a language. A Lang that came through Parse always has @@ -76,6 +78,8 @@ func (c *Catalog) Pick(n int, p Plural) string { // isOneEnglish, isOnePortuguese and isOneSpanish are the CLDR "one" rules for // integers: Portuguese counts zero as singular, English and Spanish do not. +// Chinese has no plural: isOneChinese is never true, so Pick always returns Other. func isOneEnglish(n int) bool { return n == 1 } func isOnePortuguese(n int) bool { return n == 0 || n == 1 } func isOneSpanish(n int) bool { return n == 1 } +func isOneChinese(int) bool { return false } diff --git a/internal/prompt/prompt_test.go b/internal/prompt/prompt_test.go index b8c0679f..f7c1400c 100644 --- a/internal/prompt/prompt_test.go +++ b/internal/prompt/prompt_test.go @@ -70,7 +70,7 @@ func verbs(format string) []string { func TestParse(t *testing.T) { for tag, want := range map[string]Lang{ - "en": English, "pt-BR": PortugueseBR, "": English, "fr": English, "pt-br": English, + "en": English, "pt-BR": PortugueseBR, "": English, "fr": English, "pt-br": English, "zh-CN": ChineseCN, } { if got := Parse(tag); got != want { t.Errorf("Parse(%q) = %q, want %q", tag, got, want) @@ -96,6 +96,7 @@ func TestPickFollowsEachLanguagesPluralRule(t *testing.T) { }{ {English, 0, "other"}, {English, 1, "one"}, {English, 2, "other"}, {PortugueseBR, 0, "one"}, {PortugueseBR, 1, "one"}, {PortugueseBR, 2, "other"}, + {ChineseCN, 0, "other"}, {ChineseCN, 1, "other"}, {ChineseCN, 2, "other"}, } for _, tc := range cases { if got := For(tc.lang).Pick(tc.n, forms); got != tc.want { diff --git a/internal/prompt/zh_cn.go b/internal/prompt/zh_cn.go new file mode 100644 index 00000000..1afa74c3 --- /dev/null +++ b/internal/prompt/zh_cn.go @@ -0,0 +1,122 @@ +package prompt + +var zhCN = Catalog{ + isOne: isOneChinese, + + BriefingIntro: "你正运行在 lich 中。lich 并排运行多个编码智能体会话,并且可以在这个会话旁再打开更多会话:" + + "每个会话都是一张卡片,用户可以随时查看,也可以在任务进行到一半时接管;" + + "当工作需要独立的检出时,卡片会运行在各自的 git worktree 中。", + BriefingCards: "在这里,你自己的 Agent 工具会打开它们:general-purpose 子智能体会作为其中一张卡片运行," + + "除非你要求 isolation 'worktree',否则就在当前检出中后台运行,它的报告会自动回到你这里。" + + "请用它来分派工作。只有子智能体做不到的事才自己打开会话:另一种智能体、" + + "用户指定的分支,或者需要比当前会话存活更久的工作。", + BriefingNative: "这个会话本身就是由另一个会话打开的子智能体卡片,在这里你自己的 Agent 工具会在本会话内部运行子智能体," + + "而不是作为卡片运行。请用它来分派工作,不要新开 lich 会话:" + + "你的最终消息就是打开你的那个会话正在等待的报告。", + BriefingSessions: "当工作需要分派时(同时进行多个任务,每个分支或检出一个),应当打开的是这些会话," + + "而不是你自己的宿主工具运行的子智能体:子智能体没有检出,没有卡片," + + "用户也无法引导或恢复它。", + BriefingCommandTools: "你工具列表里的 lich 工具可以打开一个会话并把任务交给它。", + BriefingCommandCLI: "用 `lich open --worktree BRANCH --prompt '任务内容'` 打开一个会话," + + "这条命令会打开会话并同时把任务交给它。", + + RelayMessage: "[lich] %[1]s,并非来自你自己的提示词。\n\n%[2]s\n\n%[3]s", + OriginCLI: "由 lich 命令行转发的消息", + OriginSession: "来自会话 %q 的消息", + ReplyCommand: " \"$LICH_BIN\" reply %s \"<你的回答>\"", + ReplyRouteCLI: "有了答案后,运行以下命令发回:\n%s", + ReplyRouteTools: "有了答案后,用 lich 工具 `%[1]s`(工单 %[2]s)发回," + + "或运行:\n%[3]s", + ReplyOnlyWayBack: "这张工单是唯一的回路:提问的一方正阻塞在它上面,不会读取任何其他内容。" + + "不要通过给其他会话发消息来回答,用其他任何方式发送的答案都会丢失。" + + "回答请写成简明的报告,说明做了什么、在哪里做的、还剩什么,不要贴完整记录:" + + "发送方要为读到的每个字节付费,而细节本来就在你的提交和文件里。", + WorkerTask: "[lich] 来自会话 %[1]q 的任务,该会话把本会话作为它的子智能体打开。" + + "你完成后的最终消息会作为报告发回给它。\n\n%[2]s", + PickTicketNudge: "[lich] 你的这一轮结束时没有发送任何答案,因此 %[1]d 个请求已作为未回答退回给了各自的发送方:\n" + + "%[2]s\n会话之外没有任何办法判断那一轮对应其中哪一个,所以无法替你回答其中任何一个。" + + "你仍在处理的那一个依然可以回答,并且此后的每个答案都必须指明它的工单:" + + "lich reply \"\"。", + NudgeResultsReady: Plural{ + One: "你发出的任务 %[2]s 的结果已就绪", + Other: "你发出的 %[1]d 个任务的结果已就绪(%[2]s)", + }, + NudgeRouteCLI: "运行:\n \"$LICH_BIN\" wait", + NudgeRouteTools: "不带工单调用 lich 工具 `%s`,或运行:\n \"$LICH_BIN\" wait", + NudgeNotice: "[lich] %[1]s。要一次性取回全部结果,请%[2]s", + + SubagentReport: "[lich] 会话 %[1]q%[2]s 已完成你交给它的任务(工单 %[3]s)。" + + "它的报告:\n\n%[4]s", + SubagentReportBranch: ",分支 %s", + BlockedNotice: "[lich] 会话 %q 是你打开的子智能体,正在它的卡片中等待权限确认。" + + "它的任务仍然保持打开:请打开该卡片来回答。", + ReportSummary: Plural{ + One: "lich 会话 %s 已完成", + Other: "lich 会话 %s 已完成", + }, + BlockedSummary: "lich 会话 %q 正在等待权限确认", + MergeNotice: "[lich] 拉取请求 #%[1]d %[2]q(分支 %[3]s)已通过 lich 合并到 %[4]s。" + + "这是一条通知,不是任务:请更新你所记录的这项工作的状态,不要做其他任何事。", + MergeSummary: "拉取请求 #%[1]d 已合并到 %[2]s", + LateNotice: "[lich] 原定于 %[1]s 发送,延迟 %[2]s 送达。\n\n", + ResumePrompt: "[lich] 你的用量上限已重置。请继续之前进行的任务。", + + CopyNotice: "[lich] %s 的副本;原文件无法从本会话访问,修改只会保留在副本中。", + + MCPInstructions: `lich 在同一个窗口中并排运行编码智能体会话;这些工具就是本会话与其他会话协作的方式。 + +并行委派:对于可以与你并行进行的工作,在它自己的 git worktree 中打开一个工作会话(open_session 并指定 worktree),让它拥有自己的检出,然后用 send_to_session 把任务交给它。先检查 list_worktrees:已经检出的分支会被直接打开,而不是新建。工作会话是显示在用户屏幕上的完整会话:可见、可引导,工作完成后由你负责关闭(close_session)。这正是它与你自己的宿主工具所运行的子智能体的区别,也是用户说要把工作分派到各个分支或 worktree 时所要求的:子智能体没有自己的检出,也没有用户可以打开、阅读或在任务中途接管的卡片。子智能体只适合用来读取信息然后丢弃,不适合用来运行别人要求亲眼看到的实现。例外是 lich-plugin 把 general-purpose 子智能体变成这类卡片的 Claude Code 会话:在那里你的 Agent 工具会在后台打开工作会话,它的报告会自动回来,所以用它来分派,而把在这里打开会话留给子智能体做不到的事。 + +不要轮询结果。超过等待时间的发送会返回一个工单,你继续做自己的工作;结果就绪时,会有一条简短的 [lich] 通知出现在你的提示处,用 wait_for_answer(不带工单)一次性取回全部。该通知只会在你的两轮之间送达,所以在你自己的长时间一轮运行期间,请在结果会改变你下一步行动的节点(继续委派之前、长时间验证之后、最终汇总之前)用 wait_for_answer 加 no_wait 查看:它会返回已就绪的内容以及谁还欠一个结果,而不会等待。每个决策点只看一次:没有就绪的就继续做自己的事。循环反复调用就是轮询,会耗费这套设计本来要节省的 token。 + +在本会话内部运行的子智能体或工作流步骤不是它自己的智能体,lich 无法区分它们:它以 private 发送和打开,然后等待它自己的工单。[lich] 通知和不带工单的 wait_for_answer 属于本会话自己的智能体。 + +当一条带工单的 [lich] 消息出现在你的提示处时,你就是工作者:完成任务,然后用 %s 回答,内容是简明的报告(做了什么、在哪里、还剩什么),不要贴完整记录。`, + HandOverFailed: "任务没有送达:%[1]v。会话已打开,等上述问题处理完后," + + "请用 %[2]s 把任务交给它。", + SendToSessionPrivate: "带 private 的 send_to_session", + OutcomeUnread: "会话 %[1]q 一直没有接手任务:任务已键入该提示处,但没有任何东西读取它," + + "说明该终端被别的东西占用了,比如提供商仍在启动,或屏幕上有它自己的提问" + + "(Claude Code 第一次在某个目录运行时会询问是否信任该目录)。" + + "没有任何内容排队,也没有任何回答。请告诉用户打开 %[1]q 卡片并处理上面的内容;" + + "之后需要重新发送任务。", + OutcomeUndelivered: "任务没有送达会话 %[1]q:因为该终端不在提示处,任务被扣留了" + + "(worktree 的设置脚本仍在运行,或提供商始终没有启动),而会话已经结束," + + "或这种状态持续了太久。不再有任何内容排队,也没有任何回答。" + + "请告诉用户打开 %[1]q 卡片查看那里发生了什么;之后需要重新发送任务。", + OutcomeUnanswered: "会话 %[1]q 结束了这一轮,但没有通过 lich 回答。" + + "它产生的内容都在该会话中,请告诉用户打开 %[1]q 卡片阅读。" + + "如果它仍在后台工作,它的答案在一小时内仍可能送达这张工单。", + OutcomeStopped: "会话 %q 在回答之前被关闭,因此任务已停止,不会再有答案。", + OutcomeExpired: "会话 %[1]q 没有在任务保持打开的一小时内回答,因此任务已过期,不会再有答案。" + + "请打开 %[1]q 卡片查看它的进展。", + StillWorkingPrivate: "%[1]s 仍在工作。这个任务是私有的:不会有通知宣告它的结果," + + "不带工单的 wait_for_answer 也永远不会返回它。请用工单 %[2]q 调用 wait_for_answer " + + "来等待它,等待你自己的工单不算轮询。", + StillWorkingOpen: "%[1]s 仍在工作。这个任务是开放的:如果该会话还没到提示处,任务会被扣留," + + "等它就绪后再送入。结果就绪时,会有一条简短的通知出现在你自己的提示处," + + "所以请继续做自己的事,无需轮询。用 wait_for_answer 取回" + + "(工单 %[2]q,或不带工单一次性取回全部)。", + StillWorkingPrivateCLI: "%[1]s 仍在工作。这个任务是私有的:发送会话的提示处不会键入任何通知," + + "不带工单的 `lich wait` 也不会返回它。" + + "请用以下命令等待它:\n lich wait %[2]s\n", + StillWorkingOpenCLI: "%[1]s 仍在工作。这个任务是开放的:该会话尚未就绪时收到的消息会被扣留到就绪为止," + + "结果就绪时会在发送会话的提示处键入一条通知。" + + "如果想改为等待它:\n" + + " lich wait %[2]s\n", + CollectedNothing: "没有可取回的内容:没有等待中的结果,你也没有打开的任务。", + CollectedAnswer: "来自 %[1]q 的答案(工单 %[2]s):\n%[3]s", + CollectedStillWorking: "仍在工作:%s。它们的结果会自行出现在你的提示处。", + OpenedProject: "项目 %q", + OpenedWorktree: "%[1]s,位于 worktree %[2]s", + OpenedConfined: " 它在受限环境中运行:一个空的主目录,只包含它的智能体自己的状态," + + "整台机器只读,只能写入它自己的检出。", + OpenedSession: "已打开会话 %[1]q(%[2]s),位于 %[3]s。%[4]s\n" + + "它可以用 %[1]q 和 %[5]q 称呼。它的智能体可能仍在启动(新的 " + + "worktree 会先运行项目的设置脚本),所以你发给它的任务会被扣留," + + "等智能体就绪后再送入,不会丢失。\n", + ClosedRemoved: "已关闭 %[1]q,并移除了它的 worktree %[2]s。\n", + ClosedKept: "已关闭 %[1]q。它的 worktree %[2]s 仍然保留,会话已被搁置:" + + "再次在该分支上打开会话,会接续它之前的对话。\n", + ClosedPlain: "已关闭 %q。\n", +} From a15440b1aa61c856e71f6f3f76a78bda79a7adc2 Mon Sep 17 00:00:00 2001 From: omartelo Date: Sat, 10 Oct 2026 00:11:30 -0300 Subject: [PATCH 2/2] feat(i18n): add Simplified Chinese interface language --- CHANGELOG.md | 5 + docs/hooks/README.md | 2 +- frontend/src/components/common/Trans.test.tsx | 8 + frontend/src/lib/i18n/i18n.test.ts | 20 +- frontend/src/lib/i18n/i18n.ts | 6 +- frontend/src/lib/i18n/lib-messages.test.ts | 9 + frontend/src/lib/i18n/locales/zh-cn/common.ts | 43 ++ frontend/src/lib/i18n/locales/zh-cn/diff.ts | 88 ++++ frontend/src/lib/i18n/locales/zh-cn/dock.ts | 37 ++ frontend/src/lib/i18n/locales/zh-cn/env.ts | 58 +++ frontend/src/lib/i18n/locales/zh-cn/footer.ts | 19 + frontend/src/lib/i18n/locales/zh-cn/git.ts | 40 ++ .../src/lib/i18n/locales/zh-cn/hotkeys.ts | 60 +++ frontend/src/lib/i18n/locales/zh-cn/index.ts | 39 ++ .../src/lib/i18n/locales/zh-cn/palette.ts | 32 ++ .../src/lib/i18n/locales/zh-cn/prompts.ts | 32 ++ frontend/src/lib/i18n/locales/zh-cn/pulls.ts | 317 ++++++++++++++ .../src/lib/i18n/locales/zh-cn/session.ts | 60 +++ .../src/lib/i18n/locales/zh-cn/settings.ts | 404 ++++++++++++++++++ frontend/src/lib/i18n/locales/zh-cn/shell.ts | 180 ++++++++ .../src/lib/i18n/locales/zh-cn/sidebar.ts | 271 ++++++++++++ frontend/src/lib/i18n/locales/zh-cn/tabs.ts | 38 ++ .../src/lib/i18n/locales/zh-cn/terminal.ts | 47 ++ frontend/src/lib/i18n/locales/zh-cn/update.ts | 18 + frontend/src/lib/prompt-language-store.ts | 2 +- 25 files changed, 1830 insertions(+), 5 deletions(-) create mode 100644 frontend/src/lib/i18n/locales/zh-cn/common.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/diff.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/dock.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/env.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/footer.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/git.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/hotkeys.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/index.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/palette.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/prompts.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/pulls.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/session.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/settings.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/shell.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/sidebar.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/tabs.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/terminal.ts create mode 100644 frontend/src/lib/i18n/locales/zh-cn/update.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 909ff2a4..5e0769f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 selection or a file. `lich sessions --json` and `whoami --json` now also carry each session's `path` and `projectPath`, and `send --project` now takes a directory, as `lich --help` already said it did. +- **Simplified Chinese (简体中文) joins the language settings.** Both Settings › + Appearance pickers, the *Interface language* and the *Prompt language*, now + offer Simplified Chinese next to English, Brazilian Portuguese and + Spanish. A browser set to any Chinese variant picks it by default for the + interface. - **Two language settings, English and Brazilian Portuguese.** Settings › Appearance now has an *Interface language* for lich's own screens and a diff --git a/docs/hooks/README.md b/docs/hooks/README.md index cd5700e3..54c55226 100644 --- a/docs/hooks/README.md +++ b/docs/hooks/README.md @@ -90,7 +90,7 @@ release before it did. `LICH_PROMPT_LANG` is the seventh, set on every session with a transport: the language the user picked for the text lich hands agents (Settings, "Prompt -language"), as a BCP 47 tag: `en`, `pt-BR` or `es` today. Anything that composes text +language"), as a BCP 47 tag: `en`, `pt-BR`, `es` or `zh-CN` today. Anything that composes text an agent reads from inside the session (the `lich` CLI, its MCP server, a hook) writes it in this language. An absent or unknown tag means English, so a plugin older or newer than the lich it runs under keeps working. It is read at spawn: diff --git a/frontend/src/components/common/Trans.test.tsx b/frontend/src/components/common/Trans.test.tsx index fffb5300..398ab943 100644 --- a/frontend/src/components/common/Trans.test.tsx +++ b/frontend/src/components/common/Trans.test.tsx @@ -1,6 +1,7 @@ import { renderToStaticMarkup } from "react-dom/server" import { afterEach, describe, expect, it, vi } from "vitest" import { resetLocale, setLocale } from "@/lib/i18n/i18n" +import { zhCN } from "@/lib/i18n/locales/zh-cn" import { Trans } from "./Trans" afterEach(() => { @@ -29,4 +30,11 @@ describe("Trans", () => { const html = renderToStaticMarkup() expect(html).toBe("2 archivos") }) + + it("renders Simplified Chinese", () => { + vi.stubGlobal("localStorage", { setItem: () => {}, getItem: () => null }) + setLocale("zh-CN") + const html = renderToStaticMarkup() + expect(html).toBe(zhCN.common.count.file.other.replace("{count}", "2")) + }) }) diff --git a/frontend/src/lib/i18n/i18n.test.ts b/frontend/src/lib/i18n/i18n.test.ts index ded80b88..c57b84eb 100644 --- a/frontend/src/lib/i18n/i18n.test.ts +++ b/frontend/src/lib/i18n/i18n.test.ts @@ -12,6 +12,7 @@ import { import { en } from "./locales/en" import { es } from "./locales/es" import { ptBR } from "./locales/pt-br" +import { zhCN } from "./locales/zh-cn" function stubStorage(initial: Record = {}) { const data = new Map(Object.entries(initial)) @@ -102,6 +103,23 @@ describe("t", () => { expect(t("common.count.file", { count: 2 })).toBe("2 arquivos") }) + it("renders Simplified Chinese with its single plural form", () => { + stubStorage() + setLocale("zh-CN") + const forms = zhCN.common.count.file + expect(forms.one).toBe(forms.other) + expect(t("common.count.file", { count: 1 })).toBe(forms.other.replace("{count}", "1")) + expect(t("common.count.file", { count: 0 })).toBe(forms.other.replace("{count}", "0")) + expect(t("common.action.cancel")).toBe(zhCN.common.action.cancel) + expect(t("common.action.cancel")).not.toBe("Cancel") + }) + + it("matches any Chinese browser language to zh-CN", () => { + expect(matchLocale("zh-CN")).toBe("zh-CN") + expect(matchLocale("zh")).toBe("zh-CN") + expect(matchLocale("zh-Hans-CN")).toBe("zh-CN") + }) + it("rejects misuse at compile time", () => { // @ts-expect-error a key that does not exist expect(() => t("sidebar.nothing.here")).toThrow() @@ -135,7 +153,7 @@ const placeholders = (text: string) => [...text.matchAll(/\{(\w+)\}/g)].map((m) // translation that renames, drops or invents a placeholder. describe("every locale against English", () => { const source = leaves(en as unknown as Tree) - const locales = { "pt-BR": ptBR, es } + const locales = { "pt-BR": ptBR, es, "zh-CN": zhCN } it("covers every locale the picker offers", () => { expect(Object.keys(locales)).toEqual(LOCALES.filter((locale) => locale !== "en")) diff --git a/frontend/src/lib/i18n/i18n.ts b/frontend/src/lib/i18n/i18n.ts index 0d03fc54..aac83fe0 100644 --- a/frontend/src/lib/i18n/i18n.ts +++ b/frontend/src/lib/i18n/i18n.ts @@ -4,6 +4,7 @@ import type { At, ParamsArg, PluralForms, Shape } from "./catalog" import { en, type MessageKey, type Messages } from "./locales/en" import { es } from "./locales/es" import { ptBR } from "./locales/pt-br" +import { zhCN } from "./locales/zh-cn" // The interface language. It is a page preference, not a workspace setting: // it changes nothing the backend does, so it lives in localStorage like the @@ -11,7 +12,7 @@ import { ptBR } from "./locales/pt-br" // setting on purpose (prompt-language-store.ts, internal/prompt): people often // run the interface in one language and talk to their agents in another. -export const LOCALES = ["en", "pt-BR", "es"] as const +export const LOCALES = ["en", "pt-BR", "es", "zh-CN"] as const export type Locale = (typeof LOCALES)[number] /** Each language named in itself, which is how someone who cannot read the @@ -20,9 +21,10 @@ export const LOCALE_NAMES: Record = { en: "English", "pt-BR": "Português (Brasil)", es: "Español", + "zh-CN": "简体中文", } -const CATALOGS: Record> = { en, "pt-BR": ptBR, es } +const CATALOGS: Record> = { en, "pt-BR": ptBR, es, "zh-CN": zhCN } export const UI_LANGUAGE_PREF = "lich.appearance.language" diff --git a/frontend/src/lib/i18n/lib-messages.test.ts b/frontend/src/lib/i18n/lib-messages.test.ts index 5257c21b..759672ef 100644 --- a/frontend/src/lib/i18n/lib-messages.test.ts +++ b/frontend/src/lib/i18n/lib-messages.test.ts @@ -63,4 +63,13 @@ describe("messages built in plain modules follow the language", () => { setLocale("pt-BR") expect(formatAge(5 * 60_000)).toBe("5 min") }) + + it("formats units and ages for Simplified Chinese", () => { + setLocale("zh-CN") + expect(COST_MISS_REASON["mixed-models"]).toMatch(/[\u4e00-\u9fff]/) + expect(formatAge(5 * 60_000)).toMatch(/^5\s?(分|min)/) + const now = new Date(2026, 0, 5, 12, 0, 0) + const at = Math.floor(now.getTime() / 1000) + 45 * 60 + expect(timeUntil(at, now)).toContain("45") + }) }) diff --git a/frontend/src/lib/i18n/locales/zh-cn/common.ts b/frontend/src/lib/i18n/locales/zh-cn/common.ts new file mode 100644 index 00000000..0272457e --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/common.ts @@ -0,0 +1,43 @@ +import type { Shape } from "../../catalog" +import type { common as en } from "../en/common" + +export const common = { + count: { + file: { one: "{count} 个文件", other: "{count} 个文件" }, + commit: { one: "{count} 个提交", other: "{count} 个提交" }, + }, + time: { + justNow: "刚刚", + }, + action: { + cancel: "取消", + save: "保存", + create: "创建", + }, + checkAgainButton: { + check: "重新检查", + checking: "正在检查…", + }, + errorBoundary: { + stoppedRendering: "{label} 已停止渲染", + reload: "重新加载窗口", + retry: "重试", + }, + stepper: { + default: "默认", + }, + toolMissing: { + notInstalled: "未安装 {label}", + install: "安装 {bin}", + }, + pickerDialog: { + navigate: "导航", + filter: "筛选", + close: "关闭", + }, + ui: { + dialog: { + close: "关闭", + }, + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/diff.ts b/frontend/src/lib/i18n/locales/zh-cn/diff.ts new file mode 100644 index 00000000..b2a3b7de --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/diff.ts @@ -0,0 +1,88 @@ +import type { Shape } from "../../catalog" +import type { diff as en } from "../en/diff" + +export const diff = { + binaryPreview: { + before: "之前", + after: "之后", + added: "已添加", + deleted: "已删除", + binary: "二进制文件", + binaryNoPreview: "二进制文件 · 此文件类型无法预览", + loading: "正在加载…", + missing: "不在此修订中", + pdfSide: "选择显示 PDF 的哪一侧", + }, + commentBatch: { + openSession: "打开一个会话,将这些评论发送给它", + removeComment: "移除评论", + notePlaceholder: "关于整个更改的备注…", + noteLabel: "关于整个更改的备注", + count: { one: "{count} 条评论", other: "{count} 条评论" }, + clear: "清空", + send: "发送", + }, + diffBulk: { + collapseAll: "折叠所有文件", + expandAll: "展开所有文件", + showWhitespace: "显示空白字符更改", + hideWhitespace: "隐藏空白字符更改", + }, + discardDialog: { + title: "放弃更改", + description: "要还原 {path} 的所有未提交更改吗?此操作无法撤销。", + descriptionAdded: "要还原 {path} 的所有未提交更改吗?该文件将从磁盘中删除。此操作无法撤销。", + confirm: "放弃更改", + }, + injectMenu: { + injectFile: "注入文件", + injectLines: "注入行", + injectLinesRange: "注入行 {lines}", + sessionComment: "给会话的评论…", + sessionCommentRange: "给会话的评论 {lines}…", + reviewComment: "对拉取请求的评论…", + reviewCommentRange: "对拉取请求的评论 {lines}…", + revertSelected: "还原所选行", + revert: { one: "还原 {count} 行更改", other: "还原 {count} 行更改" }, + }, + fileDiff: { + readFailed: "无法读取 {path}", + whitespaceOnly: "仅空白字符", + onlyWhitespaceChanged: "仅空白字符发生了更改", + addAsContext: "将文件添加为上下文", + discard: "放弃更改", + viewed: "已查看", + }, + reviewPanel: { + lastTurnHint: "上一轮运行期间磁盘上发生的更改,包括格式化工具、编辑器或你自己所做的编辑。", + discardFailed: "放弃更改失败:{error}", + reverted: "已还原 {name} 中的 +{added} -{deleted}", + undo: "撤销", + undoFailed: "无法撤销对 {name} 的还原", + revertFailed: "无法还原 {name} 中的行", + said: "说", + sourceLabel: "选择要显示的更改", + workingTree: "工作区", + lastTurn: "上一轮", + ended: "{age}前结束", + turnReadFailed: "无法读取上一轮", + notRepository: "不是 git 仓库", + loading: "正在加载…", + emptyTitle: "此时间窗口内没有任何更改。", + emptyBody: "上一轮开始到结束期间,磁盘上没有文件发生变化。", + unrecordedTitle: "没有记录上一轮。", + unrecordedBody: "一轮在此结束后,这里会显示内容。", + lostTitle: "这一轮的记录已丢失。", + lostBody: "一轮在此结束,但 lich 无法为它保存检出的快照。下一轮会照常记录。", + noChanges: "没有未提交的更改", + squeezed: "并排视图需要更宽的面板", + }, + reviewSlots: { + forSession: "给会话 · {lines}", + onPullRequest: "对拉取请求 · {lines}", + addToBatch: "添加到批次", + addToReview: "添加到审查", + sessionPlaceholder: "这里应该改什么?它会连同其他内容一起发送给会话。", + reviewPlaceholder: "留下评论。提交审查时一并发送。", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/dock.ts b/frontend/src/lib/i18n/locales/zh-cn/dock.ts new file mode 100644 index 00000000..3d551b5c --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/dock.ts @@ -0,0 +1,37 @@ +import type { Shape } from "../../catalog" +import type { dock as en } from "../en/dock" + +export const dock = { + rightDock: { + fileBrowser: "文件浏览器", + reviewChanges: "审查更改", + tabCode: "代码", + tabReview: "审查", + exitFullScreen: "退出全屏", + fullScreen: "全屏", + closePanel: "关闭面板", + fileTree: "文件树", + reviewPanel: "审查面板", + resizePanel: "调整面板大小", + }, + searchResults: { + prompt: "搜索此检出中每个文件的文本", + searching: "正在搜索…", + noMatch: "没有文件包含“{query}”", + }, + filesPanel: { + searchPlaceholder: "在文件中搜索", + filterPlaceholder: "按名称筛选", + searchLabel: "在文件中搜索", + filterLabel: "按名称筛选文件", + matchesLabel: "输入框匹配的内容", + byName: "名称", + byText: "文本", + readFailed: "无法读取此文件夹", + loading: "正在加载…", + noMatch: "没有匹配的文件", + empty: "这里没有文件", + backToTree: "返回文件树", + readOnly: "只读", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/env.ts b/frontend/src/lib/i18n/locales/zh-cn/env.ts new file mode 100644 index 00000000..99e87d7f --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/env.ts @@ -0,0 +1,58 @@ +import type { Shape } from "../../catalog" +import type { env as en } from "../en/env" + +export const env = { + providerSummary: { + installed: "已安装", + installedCustomPath: "已安装 · 自定义路径", + signedOut: "已登出", + windowPercent: "{length} 窗口的 {percent}%", + percentUsed: "已用 {percent}%", + }, + quota: { + tokenLogin: "令牌登录", + weekShort: "周", + }, + paths: { + cwdUnknown: "工作目录未知 · 位于 {host} 内", + }, + sandbox: { + windowsReason: "lich 在 Windows 上没有沙箱后端", + windowsAdvice: "无需安装任何东西,每个会话都直接在本机上运行。", + macReason: "sandbox-exec 不可用", + macAdvice: + "macOS 自带 /usr/bin/sandbox-exec,所以缺少可用的 sandbox-exec 意味着系统已损坏,lich 无法修复。每个会话都直接在本机上运行。", + linuxReason: "未安装 bubblewrap", + linuxAdvice: "请安装 bubblewrap 并重新打开 lich。在此之前,每个会话都直接在本机上运行。", + confinedMeans: + "一个只包含智能体自身状态的空主目录,本机只读,只能写入其检出目录内部。网络保持开启。", + }, + vcsTools: { + gitWithout: "没有它,分支、差异和工作树都将为空。", + ghWithout: "没有它,无法使用拉取请求、检查和 PR 检出。", + }, + binary: { + parkedProject: "{name} 覆盖已关闭", + parkedProjectFallback: "项目", + parkedGlobal: "全局覆盖已关闭", + executable: "可执行", + noSuchFile: "文件不存在", + notOnPath: "不在 $PATH 中", + notExecutable: "不可执行", + homeNotExpanded: "~ 不会被展开", + relativePath: "相对路径", + detailHomeShortcut: + "lich 直接启动该二进制文件,因此 ~ 按字面处理而不会被展开。请使用完整路径。", + detailRelative: + "相对路径会按每个会话自己的工作目录解析,因此每个会话指向的二进制文件各不相同。请使用完整路径。", + detailBroken: "问题解决之前,会话无法启动。你也可以关闭或清除覆盖设置,回退到下一层的配置。", + }, + commitIdentity: { + noneLead: "此检出中没有 git 身份。", + noneNote: "在设置 user.email 之前,提交将被拒绝。", + landAsNamed: "提交将以 {name} 的身份记录", + landAs: "提交将以以下身份记录", + setLocal: ",在此仓库中设置,覆盖你的全局配置。", + setGlobal: ",来自 git user.email,而非此账户。", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/footer.ts b/frontend/src/lib/i18n/locales/zh-cn/footer.ts new file mode 100644 index 00000000..c75dcd5a --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/footer.ts @@ -0,0 +1,19 @@ +import type { Shape } from "../../catalog" +import type { footer as en } from "../en/footer" + +export const footer = { + item: { + attach: { label: "附加文件", example: "附加文件" }, + files: { label: "文件资源管理器", example: "文件" }, + changes: { label: "更改" }, + pr: { label: "拉取请求" }, + checkout: { label: "分支" }, + path: { label: "工作目录" }, + model: { label: "模型", example: "模型" }, + context: { label: "上下文窗口" }, + plan: { label: "套餐用量" }, + cost: { label: "费用" }, + handsOn: { label: "实际操作时间" }, + clock: { label: "日期和时间" }, + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/git.ts b/frontend/src/lib/i18n/locales/zh-cn/git.ts new file mode 100644 index 00000000..819a0c58 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/git.ts @@ -0,0 +1,40 @@ +import type { Shape } from "../../catalog" +import type { git as en } from "../en/git" + +export const git = { + baseStatus: { + behind: { + one: "落后 {base} {count} 个提交", + other: "落后 {base} {count} 个提交", + }, + conflict: { one: "{count} 个文件有冲突", other: "{count} 个文件有冲突" }, + }, + fileSearch: { + cutMatches: "{count}+ 处匹配", + summary: "{matches}({files})", + matches: { one: "{count} 处匹配", other: "{count} 处匹配" }, + cutNote: "仅显示前 {count} 个匹配行。请缩小搜索范围。", + tooLarge: { + one: "{count} 个超过 1 MB 的文件未被搜索。", + other: "{count} 个超过 1 MB 的文件未被搜索。", + }, + }, + fileTree: { + hidden: "已隐藏:{names}。", + cut: "此文件夹中的文件数超出了文件树能列出的数量。", + }, + lastTurn: { + saidPreviousTurn: "来自上一轮", + noTurnWindow: "{name} 既不报告一轮的开始也不报告其结束,因此没有可划定的时间窗口。", + }, + carry: { + reused: "{name} 已存在,因此按现状检出,未带入未提交的工作。", + failed: "无法带入未提交的工作:{error}", + }, + codemirror: { + expand: { one: "展开 {count} 行未更改内容", other: "展开 {count} 行未更改内容" }, + showLines: "显示第 {from}–{to} 行", + revertTitle: "还原此更改", + revert: "还原", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/hotkeys.ts b/frontend/src/lib/i18n/locales/zh-cn/hotkeys.ts new file mode 100644 index 00000000..07379f9e --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/hotkeys.ts @@ -0,0 +1,60 @@ +import type { Shape } from "../../catalog" +import type { hotkeys as en } from "../en/hotkeys" + +export const hotkeys = { + group: { + sessions: "会话", + view: "视图", + app: "应用", + }, + action: { + commandPalette: "命令面板", + newSession: "新建会话", + newWorktree: "新建工作树会话", + renameSession: "重命名当前会话", + closeSession: "关闭当前会话", + togglePin: "固定或取消固定当前会话", + openTerminal: "在会话目录中打开终端", + delegate: "委派给另一个会话", + nextSession: "下一个会话", + prevSession: "上一个会话", + nextWaitingSession: "下一个等待输入的会话", + focusTerminal: "聚焦会话终端", + nextProject: "下一个项目", + prevProject: "上一个项目", + toggleSidebar: "切换会话侧边栏", + toggleDock: "切换右侧停靠栏", + searchInFiles: "在文件中搜索", + splitBeside: "在此会话旁显示另一个会话", + otherPane: "聚焦下一个窗格", + settings: "设置", + pulls: "拉取请求", + shortcuts: "键盘快捷键", + }, + terminalCost: { + a: "Ctrl+{key} 是 shell 的移到行首操作,会话将不再收到它。", + c: "Ctrl+{key} 是 shell 的中断操作,会话将不再收到它。", + d: "Ctrl+{key} 是 shell 的输入结束操作,会话将不再收到它。", + e: "Ctrl+{key} 是 shell 的移到行尾操作,会话将不再收到它。", + k: "Ctrl+{key} 是 shell 的删除到行尾操作,会话将不再收到它。", + l: "Ctrl+{key} 是 shell 的清屏操作,会话将不再收到它。", + q: "Ctrl+{key} 是 shell 的恢复输出操作,会话将不再收到它。", + r: "Ctrl+{key} 是 shell 的历史搜索,会话将不再收到它。", + s: "Ctrl+{key} 是 shell 的暂停输出操作,会话将不再收到它。", + u: "Ctrl+{key} 是 shell 的删除到行首操作,会话将不再收到它。", + w: "Ctrl+{key} 是 shell 的删除单词操作,会话将不再收到它。", + z: "Ctrl+{key} 是 shell 的挂起操作,会话将不再收到它。", + f: "Ctrl+{key} 是会话终端自带的搜索,会话将不再收到它。", + v: "Ctrl+{key} 是 lich 发送给智能体的图片粘贴操作,会话将不再收到它。", + Backspace: "Ctrl+{key} 是 lich 发送给智能体的删除单词操作,会话将不再收到它。", + }, + unassigned: "未分配", + shortcuts: { + terminalTitle: "终端", + passthroughTitle: "传递给智能体", + searchOutput: "搜索会话的输出", + attachImage: "从剪贴板附加图片", + newline: "插入换行而不发送", + eraseWord: "删除前一个单词", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/index.ts b/frontend/src/lib/i18n/locales/zh-cn/index.ts new file mode 100644 index 00000000..85431d2d --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/index.ts @@ -0,0 +1,39 @@ +import type { Shape } from "../../catalog" +import type { Messages } from "../en" +import { common } from "./common" +import { diff } from "./diff" +import { dock } from "./dock" +import { env } from "./env" +import { footer } from "./footer" +import { git } from "./git" +import { hotkeys } from "./hotkeys" +import { palette } from "./palette" +import { prompts } from "./prompts" +import { pulls } from "./pulls" +import { session } from "./session" +import { settings } from "./settings" +import { shell } from "./shell" +import { sidebar } from "./sidebar" +import { tabs } from "./tabs" +import { terminal } from "./terminal" +import { update } from "./update" + +export const zhCN = { + common, + diff, + dock, + env, + footer, + git, + hotkeys, + palette, + prompts, + pulls, + session, + settings, + shell, + sidebar, + tabs, + terminal, + update, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/palette.ts b/frontend/src/lib/i18n/locales/zh-cn/palette.ts new file mode 100644 index 00000000..9ab0c052 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/palette.ts @@ -0,0 +1,32 @@ +import type { Shape } from "../../catalog" +import type { palette as en } from "../en/palette" + +export const palette = { + commandPalette: { + title: "命令面板", + placeholder: "跳转到会话、项目或某条消息,或输入 > 执行操作…", + searchLabel: "搜索会话和项目", + resultsLabel: "结果", + filterLabel: "筛选结果", + forgetFailed: "无法忘记 {label}:{error}", + resumeFailed: "无法恢复 {label}:{error}", + actionUnavailable: "{label} 在此处不可用", + nothingClosed: "还没有关闭的内容", + nothingClosedHint: "关闭的会话会保留在这里,包括它的分支、智能体和对话,直到它的工作树被移除。", + noMatches: "没有与 {query} 匹配的结果", + noMatchesInTab: "在{tab}中没有与 {query} 匹配的结果", + shownOfTotal: "{shown} / {total}", + tab: { + All: "全部", + Sessions: "会话", + Projects: "项目", + Messages: "消息", + History: "历史", + }, + checkoutGone: "检出已不存在", + matches: { one: "{count} 处匹配", other: "{count} 处匹配" }, + sessions: { one: "{count} 个会话", other: "{count} 个会话" }, + relocate: "重新定位", + reopen: "重新打开", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/prompts.ts b/frontend/src/lib/i18n/locales/zh-cn/prompts.ts new file mode 100644 index 00000000..150aede7 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/prompts.ts @@ -0,0 +1,32 @@ +import type { Shape } from "../../catalog" +import type { prompts as en } from "../en/prompts" + +export const prompts = { + pullRequest: { + conflicts: "拉取请求 #{number}({branch})与 {base} 存在合并冲突。请解决这些冲突。", + checksUnnamed: { + one: "拉取请求 #{number}({branch})的 CI 失败了。有 {count} 项检查未通过,请找出是哪些并修复。", + other: + "拉取请求 #{number}({branch})的 CI 失败了。有 {count} 项检查未通过,请找出是哪些并修复。", + }, + checksListed: "拉取请求 #{number}({branch})的 CI 失败了。请修复以下检查:\n\n{checks}", + checksOmitted: { + one: "\n\n…另有 {count} 项失败的检查未列出。", + other: "\n\n…另有 {count} 项失败的检查未列出。", + }, + create: + "分支 {branch} 还没有拉取请求。请用 `gh pr create` 创建一个:如果分支尚未推送到远程,先推送;根据提交和与基础分支的差异撰写标题和正文;如果仓库有拉取请求模板,请遵循该模板。", + }, + delegate: { + session: "委派给“{target}”会话:", + newWorktree: "委派给一个新的工作树会话:", + newWorktreeWithCommand: + "委派给一个新的工作树会话(运行 `lich open --worktree --prompt `):", + }, + reviewComments: { + header: "审查评论:", + }, + issue: { + head: "GitHub issue #{number},{title}\n{url}", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/pulls.ts b/frontend/src/lib/i18n/locales/zh-cn/pulls.ts new file mode 100644 index 00000000..4f14b900 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/pulls.ts @@ -0,0 +1,317 @@ +import type { Shape } from "../../catalog" +import type { pulls as en } from "../en/pulls" + +export const pulls = { + prHandoff: { + resolveConflicts: "解决冲突", + fixChecks: "修复 CI 错误", + }, + byline: { + someone: "某人", + }, + commentBox: { + sending: "正在发送…", + }, + verdict: { + approved: "已批准", + changesRequested: "请求了更改", + dismissed: "评审已驳回", + commented: "已评论", + requested: "已请求评审", + }, + mergeMessageDialog: { + squashAndMerge: "压缩并合并", + createMergeCommit: "创建合并提交", + description: "编辑提交消息,然后合并。", + subject: "提交消息", + body: "详细描述", + optional: "可选", + merging: "正在合并…", + merge: "合并", + }, + mergeGate: { + changesRequested: "评审者请求了更改", + reviewRequired: "GitHub 要求先完成评审,但尚未有人评审", + merged: "拉取请求已合并", + closed: "拉取请求已关闭", + draft: "拉取请求仍是草稿", + conflicts: "与 {base} 存在冲突", + behind: "基础分支已更新,请先更新此分支", + pattern: { + match: "{target}必须匹配 {pattern}", + notMatch: "{target}不得匹配 {pattern}", + startWith: "{target}必须以 {pattern} 开头", + notStartWith: "{target}不得以 {pattern} 开头", + endWith: "{target}必须以 {pattern} 结尾", + notEndWith: "{target}不得以 {pattern} 结尾", + contain: "{target}必须包含 {pattern}", + notContain: "{target}不得包含 {pattern}", + }, + target: { + message: "消息", + authorEmail: "作者邮箱", + committerEmail: "提交者邮箱", + }, + ruleNote: "规则集约束着 {base}:对每个提交,{clauses}。", + }, + commitAuthors: { + committed: "{who} 于 {date} 提交", + committedOn: "提交于 {date}", + noAccount: "未关联 GitHub 账号", + committedByNoAccount: "由未关联 GitHub 账号的地址提交", + strays: { + one: "有 {count} 个提交归在 {logins} 名下,而不是 @{author}。", + other: "有 {count} 个提交归在 {logins} 名下,而不是 @{author}。", + }, + orphans: { + one: "有 {count} 个提交未关联 GitHub 账号。", + other: "有 {count} 个提交未关联 GitHub 账号。", + }, + orphansWithEmails: { + one: "有 {count} 个提交未关联 GitHub 账号:{emails}。", + other: "有 {count} 个提交未关联 GitHub 账号:{emails}。", + }, + andMore: "{shown} 等另外 {hidden} 项", + }, + pullsChecks: { + none: "没有检查结果。", + }, + pullsCommits: { + none: "没有提交。", + }, + pullsConflicts: { + loading: "正在判断哪些文件存在冲突…", + failed: "无法判断哪些文件存在冲突:{error}", + stale: "目前没有文件冲突,GitHub 可能尚未同步。", + count: { + one: "{count} 个冲突文件", + other: "{count} 个冲突文件", + }, + showFewer: "收起", + more: "另有 {hidden} 个", + }, + pullsEmptyState: { + openFailed: "无法打开拉取请求:{error}", + noneFor: "{branch} 没有打开的拉取请求。", + none: "没有打开的拉取请求。", + openingSession: "正在打开会话…", + createWithAgent: "让智能体创建", + opening: "正在打开…", + openOnGitHub: "在 GitHub 上打开", + }, + submitReviewDialog: { + verdict: { + approve: { + title: "批准", + action: "批准", + description: "批准此拉取请求。GitHub 不允许批准自己的拉取请求。", + }, + comment: { + title: "评论", + action: "发送评审", + description: "只发送评论,不给出结论。", + }, + request_changes: { + title: "请求更改", + action: "请求更改", + description: "要求完成下面的更改后才能合入。", + }, + }, + summaryPlaceholder: "摘要(可选)", + summaryAlone: "没有行评论,这次只发送摘要。", + lineComments: { + one: "随附 {count} 条行评论。", + other: "随附 {count} 条行评论。", + }, + }, + pullsConversation: { + commentFailed: "评论失败:{error}", + loading: "正在读取对话…", + empty: "还没有任何发言。在这里发表的评论属于拉取请求本身;针对某一行的评论会出现在差异中。", + resolved: { + one: "{count} 个已解决的讨论串", + other: "{count} 个已解决的讨论串", + }, + commentOn: "评论此拉取请求", + comment: "评论", + commentPlaceholder: "说点关于整个拉取请求的内容", + dismissFailed: "驳回失败:{error}", + dismiss: "驳回", + why: "此评审不再适用的原因", + dismissReview: "驳回评审", + whyPlaceholder: "评审写下之后有什么变化", + }, + pullsOverview: { + saveFailed: "无法保存描述:{error}", + openedBy: "由 {author} 打开", + description: "描述", + edit: "编辑", + noDescription: "没有描述。", + descriptionPlaceholder: "描述此拉取请求", + saving: "正在保存…", + candidatesFailed: "无法读取可评审的人员:{error}", + requestFailed: "无法向 {login} 请求评审:{error}", + withdrawFailed: "无法撤回对 {login} 的评审请求:{error}", + reviewers: "评审者", + nobodyYet: "暂无", + requestReview: "请求评审", + candidatesLoading: "正在读取可评审的人员…", + nobodyElse: "没有其他人可以评审", + }, + pullsStats: { + checksFailing: "{total} 项检查中有 {failed} 项失败", + checksRunning: "{total} 项检查中有 {pending} 项正在运行", + checksPassed: { + one: "{count} 项检查通过", + other: "{count} 项检查通过", + }, + merged: "已合并", + closed: "已关闭", + draft: "草稿", + open: "打开", + approved: "已批准", + changesRequested: "已请求更改", + reviewRequired: "需要评审", + threadsUnresolved: "已请求更改 · {total} 个讨论串中有 {open} 个未解决", + threadsResolved: "已请求更改 · 所有讨论串均已解决", + conflictsWith: "与 {base} 存在冲突", + mergeable: "可合并", + checkingMergeability: "正在检查是否可合并…", + }, + pullsList: { + filter: { + all: "全部", + ready: "就绪", + drafts: "草稿", + failing: "失败", + }, + sort: { + updated: "最近更新", + oldest: "最早更新", + failing: "失败的优先", + number: "编号最大", + }, + qualifierWords: "关键词会匹配编号、标题、作者和分支。", + show: "显示拉取请求列表", + hide: "隐藏拉取请求列表", + filterPlaceholder: "筛选,例如 is:merged", + filterLabel: "筛选拉取请求", + filterGroup: "拉取请求筛选", + state: { + open: "打开", + closed: "已关闭", + merged: "已合并", + all: "全部", + }, + cappedHint: "只加载了最近更新的 {limit} 个,筛选只在这些中搜索。", + capped: "前 {limit} 个", + none: { + open: "没有打开的拉取请求。", + closed: "没有已关闭的拉取请求。", + merged: "没有已合并的拉取请求。", + all: "没有拉取请求。", + }, + noMatch: "没有符合该筛选条件的内容。", + review: { + approved: "已批准", + changesRequested: "已请求更改", + }, + draft: "草稿", + fork: "分叉", + checkedOut: "已检出", + }, + pullsFiles: { + loadFailed: "无法加载差异:{error}", + none: "没有文件更改", + resizeTree: "调整文件树大小", + hideTree: "隐藏文件树", + showTree: "显示文件树", + viewed: "已查看 {viewed}/{total}", + }, + reviewThread: { + replyFailed: "回复失败:{error}", + resolveFailed: "标记为已解决失败:{error}", + reopenFailed: "重新打开失败:{error}", + line: "第 {line} 行", + resolved: "已解决", + outdated: "· 已过时", + comments: { + one: "{count} 条评论", + other: "{count} 条评论", + }, + reopen: "重新打开", + resolve: "标记为已解决", + reply: "回复", + replyPlaceholder: "回复此讨论串", + pending: "待处理,提交评审时发送", + edit: "编辑此评论", + discard: "丢弃此评论", + }, + pulls: { + alreadyRemoved: "{name} 已被移除", + pinnedSession: "该工作树有已固定的会话,请先取消固定。", + notCreatedByLich: "此工作树不是 lich 创建的,请在侧边栏中移除。", + dirty: "该工作树有未提交的更改,请在侧边栏中移除。", + removed: "已移除 {name}", + removeFailed: "移除工作树失败:{error}", + merged: "已将 #{number} 合并到 {base}", + announceFailed: "无法通知 {name} 中的会话:{error}", + removeWorktree: "移除工作树", + openSessionFailed: "无法打开会话:{error}", + handOffFailed: "无法交给该会话:{error}", + goToSession: "前往会话", + openInSession: "在会话中打开", + forkBlocked: "头部分支位于不允许维护者编辑的分叉上,无法把提交推回去", + noRepository: "没有仓库", + loadFailed: "无法加载拉取请求:{error}", + runningRemoval: { + title: "会话仍在运行", + description: + "{labels} 正在 {name} 中执行一轮。移除工作树会让它当场停止,进行中的这一轮将丢失。", + removeAnyway: "仍然移除", + }, + noSessionToHandOff: "此检出上没有可以交给的会话", + headNotOnBranch: "HEAD 不在任何分支上", + }, + pullRequestView: { + rebaseAndMerge: "变基并合并", + failingChecks: { + one: "有 {count} 项检查失败,GitHub 仍会合并", + other: "有 {count} 项检查失败,GitHub 仍会合并", + }, + mergeFailed: "合并失败:{error}", + approvedToast: "已批准 #{number}", + reviewSent: "已在 #{number} 上发送评审", + reviewSentWithComments: { + one: "已在 #{number} 上发送评审,含 {count} 条评论", + other: "已在 #{number} 上发送评审,含 {count} 条评论", + }, + reviewFailed: "评审失败:{error}", + opening: "正在打开…", + openingSession: "正在打开会话…", + approving: "正在批准…", + approved: "已批准", + approve: "批准", + submitReview: "提交评审", + comment: "评论", + requestChanges: "请求更改", + discardPending: { + one: "丢弃 {count} 条待处理评论", + other: "丢弃 {count} 条待处理评论", + }, + merging: "正在合并…", + merge: "合并", + methodBypass: "{method},绕过规则", + editMessage: "{method},编辑消息…", + editMessageBypass: "{method},编辑消息…(绕过规则)", + refresh: "刷新", + open: "打开", + tab: { + overview: "概览", + commits: "提交", + files: "文件更改", + conversation: "对话", + checks: "检查", + }, + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/session.ts b/frontend/src/lib/i18n/locales/zh-cn/session.ts new file mode 100644 index 00000000..747f59e5 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/session.ts @@ -0,0 +1,60 @@ +import type { Shape } from "../../catalog" +import type { session as en } from "../en/session" + +export const session = { + schedule: { + choice15Minutes: "15 分钟", + choice1Hour: "1 小时", + choice4Hours: "4 小时", + choiceTomorrow: "明天", + countdown: "{amount}后", + today: "今天 {clock}", + weekdayAt: "{weekday} {clock}", + }, + limitLine: { + sessionWindow: "会话上限", + weeklyWindow: "每周上限", + usageWindow: "用量上限", + }, + filter: { + phase: { + waiting: "等待中", + running: "运行中", + unread: "未读", + idle: "空闲", + }, + phaseJoiner: "或", + noMatch: "没有会话。当前会话保持显示。", + noMatchQuery: "没有与“{query}”匹配的会话。当前会话保持显示。", + noMatchPhases: "没有{phases}的会话。当前会话保持显示。", + noMatchPhasesQuery: "没有与“{query}”匹配的{phases}会话。当前会话保持显示。", + }, + cost: { + mixedModels: "此对话切换过模型,因此 lich 无法计价。", + unpricedModel: "此模型暂无价格,lich 需要联网获取。", + }, + handsOn: { + detailTurn: + "此会话被实际操作的时长,包括输入、汇报或运行一轮的时间。超过 15 分钟的间隔计为离开。", + detailTool: + "此会话被实际操作的时长,包括输入或汇报工具调用的时间。超过 15 分钟的间隔计为离开。", + }, + spawnGate: { + checkoutGone: "此会话的工作树已不存在,因此会话已关闭。请重新创建工作树以继续。", + conversationGone: "之前的对话已不可用,正在启动新会话。", + }, + fork: { + unavailable: "{name} 不支持分叉,只能恢复。", + }, + palette: { + untitledConversation: "未命名对话", + groupSessions: "会话", + groupProjects: "项目", + groupMessages: "消息", + groupOpen: "已打开", + groupClosed: "已关闭", + groupClosedSessions: "已关闭的会话", + groupOutsideLich: "lich 之外", + indexing: { one: "正在索引 {count} 个会话", other: "正在索引 {count} 个会话" }, + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/settings.ts b/frontend/src/lib/i18n/locales/zh-cn/settings.ts new file mode 100644 index 00000000..db8b2fc8 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/settings.ts @@ -0,0 +1,404 @@ +import type { Shape } from "../../catalog" +import type { settings as en } from "../en/settings" + +export const settings = { + language: { + uiTitle: "界面语言", + uiDescription: "lich 自身界面所用的语言。", + promptTitle: "提示词语言", + promptDescription: + "lich 交给智能体的文本所用的语言:简报、转发的消息和通知。此后启动的会话在所有场景下都使用该语言;已打开的会话仅在转发的消息中使用。", + uiSearchWords: "语言 区域 翻译 英语 葡萄牙语 简体中文", + promptSearchWords: "语言 区域 翻译 智能体 简报 转发 英语 葡萄牙语 简体中文", + promptSaveFailed: "无法保存提示词语言:{error}", + }, + settings: { + search: "搜索设置", + resultCount: { one: "{count} 项设置", other: "{count} 项设置" }, + noMatch: "没有与 {query} 匹配的内容。搜索只匹配每项设置的名称,不匹配其值。", + section: { + appearance: "外观", + notifications: "通知", + hotkeys: "快捷键", + providers: "提供商", + sandbox: "沙箱", + versionControl: "版本控制", + updates: "更新", + help: "帮助", + }, + }, + appearanceSettings: { + importPickTitle: "导入主题", + templatePickTitle: "保存主题模板", + imported: "已导入主题:{name}", + importFailed: "主题导入失败:{error}", + installed: { + one: "已从 {pack} v{version} 安装 {count} 个主题", + other: "已从 {pack} v{version} 安装 {count} 个主题", + }, + installFailed: "主题安装失败:{error}", + upToDate: "{pack} 已是 v{version}", + updated: "已将 {pack} 更新到 v{version}", + updateFailed: "主题更新失败:{error}", + removed: "已移除主题:{name}", + removeFailed: "主题移除失败:{error}", + templateSaved: "已将主题模板保存到 {path}", + templateFailed: "主题模板生成失败:{error}", + zoomTitle: "缩放", + zoomOut: "缩小", + zoomIn: "放大", + resetZoom: "重置缩放", + resetTextSize: "重置终端文字大小", + textSizeTitle: "终端文字大小", + textSmaller: "减小终端文字", + textLarger: "增大终端文字", + replaceThemesTitle: "替换已导入的主题?", + replaceThemesDescription: + "该仓库包含已安装的主题:{themes}。仍要安装吗?这将永久删除之前的版本。", + replaceThemes: "替换主题", + removeThemeTitle: "移除已导入的主题?", + removeThemeDescription: "删除 {name}?这将从 lich 中移除已导入的主题文件。", + removeTheme: "移除主题", + replaceThemeTitle: "替换已导入的主题?", + replaceThemeDescription: "已存在 id 为 {id} 的主题。仍要导入吗?这将永久删除之前的主题。", + replaceTheme: "替换主题", + }, + themePicker: { + title: "主题", + searchWords: "颜色 深色 浅色 终端 调色板", + description: "为界面和终端着色。", + choose: "选择主题", + system: "系统", + followsOs: "跟随操作系统", + inUse: "使用中", + bundled: "内置", + count: { one: "{count} 个主题", other: "{count} 个主题" }, + import: "导入", + updateNamed: "更新 {name}", + updateHint: "检查仓库中是否有更新的版本", + removeNamed: "移除 {name}", + }, + brokenThemeList: { + label: "无法加载的主题", + heading: { one: "{count} 个主题无法加载", other: "{count} 个主题无法加载" }, + removeNamed: "移除 {name}", + remove: "移除", + }, + importThemeDialog: { + title: "导入主题", + description: "从 git 仓库安装可保留主题的版本信息,也可以选择单个主题文件。", + repositoryUrl: "仓库 URL", + install: "安装", + installing: "正在安装…", + repositoryHint: "包含 {file} 和一个或多个主题文件的仓库。更新时,lich 比较的是其清单中的版本。", + or: "或", + chooseFile: "选择文件…", + fileHint: "单个主题文件。没有版本,也没有更新。", + downloadTemplate: "下载模板", + }, + fontSetting: { + title: "终端字体", + searchWords: "字体 等宽", + sample: "the quick brown fox 0O1lI", + search: "搜索字体", + empty: "没有匹配的字体。", + }, + footerSettings: { + title: "页脚", + searchWords: "状态栏 布局 排列", + loading: "正在加载已保存的选择…", + saving: "正在保存…", + description: "在两侧之间拖动项目,或拖出以隐藏。对所有项目生效。", + restoreDefault: "恢复默认", + saveFailed: "无法保存页脚:{error}", + ceilingTitle: "花费上限", + ceilingSearchWords: "费用 预算 美元 usd", + ceilingDescription: + "会话的 API 费用接近此金额时发出警告:80% 为琥珀色,95% 为红色。留空表示不设上限。", + ceilingPlaceholder: "无上限", + ceilingLabel: "会话花费上限(美元)", + }, + footerLayoutEditor: { + zone: { available: "可用", left: "左侧", right: "右侧" }, + allPlaced: "所有项目都已在页脚中", + dragHere: "将项目拖到此处", + moveNamed: "移动 {name}", + previewLabel: "页脚预览", + previewHint: "预览 · 示例值 · 滚动查看所有项目", + previewScrollLabel: "可滚动的页脚预览", + previewEmpty: "未选择页脚项目", + }, + diffLayoutSetting: { + title: "差异布局", + searchWords: "并排 拆分 统一 评审", + description: + "审查标签页和拉取请求中变更的显示方式。面板过窄而放不下两列时,并排视图会显示为统一视图。", + layout: { unified: "统一", split: "并排" }, + }, + notificationsSettings: { + needsInputTitle: "会话需要输入时通知我", + needsInputDescription: + "当会话被你阻塞(例如权限提示或提问),且你当前不在 lich 窗口中时,发出桌面通知。你在 lich 中时不会有变化:由卡片上的提醒铃和提示消息来告知。", + needsInputLabel: "会话需要输入时发出桌面通知", + finishedTitle: "会话完成工作时通知我", + finishedDescription: + "当会话结束一轮、没有需要询问你的内容,且你当前不在 lich 窗口中时,发出桌面通知。与上面的设置相互独立:即使你不想被每个提示打扰,也值得知道自己离开时的长时间运行已经结束。不会附带提示消息:在 lich 中,卡片及其项目标签页已经会告知。", + finishedLabel: "会话完成工作时发出桌面通知", + }, + hotkeysSettings: { + alsoBound: "同时绑定到 {actions}", + pressKeys: "请按下按键…", + unassignNamed: "取消 {name} 的快捷键", + unassignHint: "不为此操作绑定快捷键", + resetNamed: "重置 {name} 的快捷键", + resetHint: "恢复默认快捷键", + terminalNote: + "这是 lich 自有的固定快捷键:它会覆盖浏览器的加速键,而浏览器只响应原本的组合键,不响应重新绑定后的按键。", + passthroughNote: + "lich 会在这些按键送往智能体终端的途中改写它们,因此它们是固定的,不可重新绑定。", + }, + providersPane: { + detecting: "正在检测提供商…", + heading: "提供商", + addProvider: "添加提供商", + moreDetected: "另检测到 {count} 个", + defaultTitle: "默认提供商", + defaultSearchWords: "智能体 新建会话", + defaultDescription: + "隐式的会话操作所启动的提供商:新建会话快捷键、空项目中的“新建会话”按钮,以及新创建的工作树。", + allProjects: "所有项目", + defaultForAll: "所有项目的默认提供商", + defaultForProject: "{name} 的默认提供商", + clear: "清除", + clearFallsBack: "清除后回退到 {name}", + following: "跟随所有项目:{name}", + openSettings: "打开 {name} 的设置", + defaultHere: "此处默认", + customPath: "自定义路径", + skipWorktrees: "工作树", + skipEverywhere: "无提示", + enableNamed: "启用 {name}", + }, + providersSettings: { + defaultDescription: + "隐式的会话操作所使用的提供商,除非你所在的项目在 设置 › 提供商 中另行指定。", + defaultLabel: "全局默认提供商", + detected: "已在 PATH 中检测到", + }, + providerDetail: { + back: "所有提供商", + enabled: "已启用", + rightNowTitle: "当前状态", + rightNowSearchWords: "打开的会话 文档", + openSessions: { one: "{count} 个会话已打开", other: "{count} 个会话已打开" }, + documentation: "文档", + }, + providerToggleRow: { + notFound: "在 PATH 中未找到", + }, + providerDocsLink: { + howToInstall: "安装方法", + }, + providerBinary: { + title: "可执行文件", + searchWords: "路径 可执行 命令 binary", + description: "会话启动所用的可执行文件。", + descriptionProject: "{name} 中的会话启动所用的可执行文件。", + pickTitle: "选择 {provider} 的可执行文件", + pickerFailed: "无法打开文件选择器", + projectOnly: "仅 {name}", + allProjects: "所有项目", + layersHint: "来源:启用的第一层优先生效", + done: "完成", + useLayer: "使用为{layer}设置的 {provider} 可执行文件", + layerPath: "{layer}的 {provider} 可执行文件", + pickLayer: "选择{layer}的 {provider} 可执行文件", + browse: "浏览", + useDifferent: "使用其他可执行文件", + sourceProjectNamed: "为 {name} 设置", + sourceProject: "为此项目设置", + sourceGlobal: "为所有项目设置", + sourcePath: "来自 $PATH", + }, + providerBinSettings: { + skipTitle: "跳过权限提示", + skipSearchWords: "yolo 危险 批准 确认", + skipDescription: + "{provider} 可以在不询问的情况下运行到什么程度:它会不经确认地编辑文件、运行命令和安装内容,lich 使用 {flag} 启动它。", + skipLabel: "{provider} 可以在不询问的情况下运行到什么程度", + skip: { + never: { + label: "从不", + consequence: "在每个检出中,每次编辑和每条命令都会等待你确认。", + }, + worktrees: { + label: "仅工作树", + consequence: "项目目录中的会话仍会询问。", + }, + everywhere: { + label: "所有位置", + consequence: "包括你工作的目录树。不会再询问任何内容。", + }, + }, + confirmTitle: "跳过权限提示:{level}", + confirmDescription: + "{provider} 将不经确认地编辑文件、运行命令和安装内容,并使用 {flag} 启动。{consequence}", + confirm: "跳过提示", + }, + planUsageSetting: { + title: "套餐用量", + searchWords: "配额 订阅 上限 用量", + description: + "你的订阅在每个时间窗口内已用掉多少,从你的账户读取,lich 打开期间每 5 分钟刷新一次。", + signedOut: "已登出。", + signedOutRun: "已登出。运行 {command} 以读取套餐用量。", + readFailed: "无法读取套餐用量。提供商的用量端点没有文档,可能已经变更。", + used: "已用", + resets: "重置于", + }, + restoreSetting: { + title: "恢复的会话", + searchWords: "恢复 继续 对话 新建 重启 询问", + description: "在 lich 重启之前运行过 {provider} 的卡片,在你第一次打开时的行为。", + label: "已恢复的 {provider} 卡片的行为", + choice: { + ask: { label: "询问", consequence: "每张已恢复的卡片在打开前都会询问。" }, + resume: { + label: "继续", + consequence: "已恢复的卡片在打开时继续其对话。如需空白会话,请新建会话。", + }, + fresh: { + label: "重新开始", + consequence: "已恢复的卡片以空白状态打开,不会接续旧的对话。", + }, + }, + }, + subagentCardsSetting: { + title: "子智能体作为 lich 会话", + searchWords: "智能体 子智能体 卡片 工作树 委派 后台", + description: + "通用子智能体会打开自己的卡片和工作树,而不是隐藏在 {provider} 内部运行。对更改后打开的会话生效。", + label: "将 {provider} 子智能体作为 lich 会话运行", + }, + ultracodeSetting: { + title: "Ultracode", + searchWords: "工作流 编排 多智能体 强度", + description: + "每个 {provider} 会话启动时都开启 ultracode,在其运行的任意强度下编排多智能体工作流。会话重启或恢复后仍保持开启。", + label: "以 ultracode 启动 {provider} 会话", + }, + sandboxSettings: { + rung: { off: "关闭", ask: "询问", worktrees: "工作树", everywhere: "所有位置" }, + exposes: { + off: "每个会话都在本机上运行。", + ask: "你取消勾选其选项的会话会在本机上运行。", + worktrees: "项目目录中的会话在本机上运行。", + }, + summary: "空的主目录,本机只读,仅在检出目录中可写。网络保持开启。", + confinedTitle: "哪些会话在受限环境中运行", + confinedSearchWords: "隔离 bwrap seatbelt", + confinedHint: + "“询问”会在“新建会话”菜单和“新建工作树”对话框中提出这个问题。在无人可以回答的地方打开的会话(由其他会话或通过 MCP 工具打开)会受限运行。终端永远不受限;沙箱是为无人值守的智能体准备的。", + carryTitle: "受限会话可以带入什么", + sshTitle: "SSH 代理", + sshSearchWords: "推送 密钥", + sshDescription: + "git push 可在其中使用。会话运行期间,使用你的代理中的每个身份,对任意主机签名。", + sshLoaded: "已加载:{keys}", + sshNone: "未加载任何密钥。", + ghTitle: "GitHub 令牌", + ghSearchWords: "gh 凭据", + ghDescription: "gh 可在其中使用。智能体可以从其环境中读出该令牌,并在此仓库之外使用它。", + ghAccount: "以 {login} 身份。", + ghActiveAccount: "以 gh 的活动账户身份。", + rungLabel: "哪些 {provider} 会话在受限环境中运行", + confirmTitle: "减少受限的 {provider} 会话", + confirmDescription: "{rung}:{exposes}", + confirm: "不再限制", + canConfine: "此机器可以限制会话", + cannotConfine: "此机器无法限制会话", + statusDetail: ":{detail}", + }, + vcsToolsSetting: { + title: "命令行工具", + searchWords: "git gh 安装", + description: "lich 通过 git 和 GitHub CLI 工作。此界面上的一切操作都经由它们执行。", + notOnPath: "不在 $PATH 中", + install: "安装", + }, + versionControlSettings: { + account: "GitHub 账户", + accountFor: "{name} 的 GitHub 账户", + accountSearchWords: "登录 gh 切换", + openProject: "打开一个项目以配置其版本控制。", + accountDescription: + "lich 为此项目以哪个账户运行 gh:用于拉取请求、检查和 PR 检出。gh 每个主机只保留一个活动账户,因此只有其他账户可见的仓库会显示为未找到。企业主机上的账户会连同主机一起列出。", + activeAccount: "gh 的活动账户", + selectAccount: "选择账户", + listFailed: "无法列出 GitHub 账户:{error}", + noAccounts: "gh 尚未登录任何账户。请在终端中运行 `gh auth login`。", + }, + updatesSettings: { + applicationTitle: "应用程序", + applicationSearchWords: "版本 升级", + applicationDescription: "lich {version},启动时以及每小时检查一次更新。", + checkForUpdates: "检查更新", + available: "lich {version} 可用,请按提示操作。", + upToDate: "你使用的已是最新版本。", + checkFailed: "检查失败,是否已联网?", + whatsNewTitle: "新增内容", + whatsNewSearchWords: "更新日志 发行说明", + patchNotesFor: "v{version} 的更新说明。", + noPatchNotes: "此构建没有更新说明。", + viewPatchNotes: "查看更新说明", + }, + pluginSetting: { + title: "lich 插件", + searchWords: "钩子 安装", + description: "在支持运行它的提供商 CLI 中提供会话状态、标题、git 刷新和恢复。", + installing: "正在为 {name} 安装 lich 插件…", + installed: "插件已安装。重启会话以生效。", + installFailed: "安装失败", + updating: "正在为 {name} 更新 lich 插件…", + updated: "插件已更新。重启会话以生效。", + updateFailed: "更新失败", + checked: "已检查。", + checkFailed: "检查失败,是否已联网?", + cliMissing: "未安装 CLI", + notInstalled: "未安装插件", + unsupported: "v{version},此 lich 不支持", + install: "安装", + updateTo: "更新到 v{version}", + installVersion: "安装 v{version}", + codexHint: "Codex:在 Codex 会话中运行 /hooks 以信任该插件的钩子。", + crushHint: + "Crush:上报其会话 id 并刷新 git 状态。它没有回合结束事件,因此其卡片不显示状态,并保留自己的名称。", + ompHint: + "oh-my-pi:上报其状态、名称和 git 变更。它没有可观测的批准事件,因此等待你授权的会话显示旋转图标,而不是提醒铃。", + antigravityHint: + "Antigravity:上报其状态、名称和 git 变更。它没有可观测的批准事件,因此等待你授权的会话显示旋转图标,而不是提醒铃。", + cursorHint: + "Cursor CLI:运行安装在 Claude Code 中的插件,因此它上报该版本所上报的内容,并随之更新。lich 在此只添加自己的工具。与上面两者一样,它不会触发批准事件,因此等待你授权的会话显示旋转图标,而不是提醒铃。", + checkForUpdates: "检查更新", + }, + helpSettings: { + bugTitle: "报告问题", + bugSearchWords: "issue github 缺陷 bug", + bugDescription: + "在浏览器中打开问题表单,并已填好版本和平台。lich 不会替你提交问题,需要你自己填写。在终端中运行 `lich rage`,可将日志、版本以及 lich 在此机器上发现的信息打包成一个归档文件以便附上。", + openBugReport: "打开问题报告", + logTitle: "日志文件", + logSearchWords: "调试 日志", + logDescription: + "日志包含绝对路径、项目和分支名称以及你的 gh 登录名,但绝不包含会话令牌。附到任何地方之前请先阅读。", + openingLogFolder: "正在打开日志文件夹…", + logFolderOpened: "已打开日志文件夹。", + openLogFolderFailed: "无法打开日志文件夹", + openLogFolder: "打开日志文件夹", + noLogFile: "仅输出到 stderr,没有日志文件。", + aboutTitle: "关于", + aboutSearchWords: "版本 许可证", + aboutVersion: "lich {version},{platform}。", + repository: "仓库", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/shell.ts b/frontend/src/lib/i18n/locales/zh-cn/shell.ts new file mode 100644 index 00000000..5bed149e --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/shell.ts @@ -0,0 +1,180 @@ +import type { Shape } from "../../catalog" +import type { shell as en } from "../en/shell" + +export const shell = { + agentPluginGate: { + updateAvailable: "lich 插件 {version} 可用", + update: "更新", + later: "稍后", + installVersion: "安装 v{version}", + updating: "正在更新 lich 插件…", + updated: "插件已更新。重启会话以应用。", + updateFailed: "更新失败", + installed: "插件已安装。重启会话以应用。", + codexTrust: "Codex:在 Codex 会话中运行 /hooks 以信任该插件的钩子。", + installFailed: "安装失败:{error}", + title: "启用智能体集成", + description: + "lich 插件会向应用报告会话的状态和标题,刷新其 git 状态,并让已恢复的会话继续之前的对话。请将它安装到你使用的 CLI 中。", + needsHooks: "需要通过 /hooks 信任", + dontAskAgain: "不再询问", + notNow: "暂不", + installing: "正在安装…", + install: "安装", + }, + appUpdateGate: { + available: "lich {version} 可用", + updateAndInstall: "更新并安装", + later: "稍后", + updated: "lich 已更新到 {version}", + retry: "重试", + restart: "重启", + restartFailed: "重启失败:{error}", + install: "安装", + viewRelease: "查看发布说明", + }, + confirmDialog: { + cancel: "取消", + }, + emptySessions: { + title: "没有打开的会话", + description: "打开一个会话,开始在此项目中工作。", + noAgent: "在 PATH 中未找到智能体,因此这里会打开终端。请在设置 › 提供商中配置。", + newTerminal: "新建终端", + newSession: "新建会话", + }, + fileTree: { + openInEditor: "在编辑器中打开", + expandAll: "全部展开", + collapseAll: "全部折叠", + }, + footerBar: { + attachFailed: "无法附加文件", + attachFile: "附加文件", + browseCode: "浏览代码", + reviewChanges: "审查更改", + viewPullRequest: "查看拉取请求", + pullRequest: "PR #{number}", + leftFooter: "左侧页脚", + rightFooter: "右侧页脚", + }, + footerSession: { + contextWindow: "上下文窗口", + contextTokens: "{tokens} / {window} 个 token", + costUnavailable: "费用不可用", + sessionCost: "会话费用", + costOfBudget: "预算 {budget} 中的 {cost}", + costNote: "此会话各对话的 API 费用。订阅收费可能不同。", + handsOnTime: "亲自操作时长", + handsOnFailed: "无法读取亲自操作时长。", + }, + gitMissingGate: { + title: "未安装 git", + description: + "lich 通过 git 读取每个分支、差异和工作树。没有 git 时会话仍可运行,但版本控制相关界面会保持为空。", + notNow: "暂不", + installGit: "安装 git", + }, + home: { + title: "没有打开的项目", + description: "打开一个文件夹,开始终端会话。", + openProject: "打开项目", + }, + notificationsOptIn: { + title: "会话需要你时通知你?", + description: + "刚才有会话在你离开时请求了你的输入。发生这种情况时,lich 可以弹出桌面通知,注明会话及其项目,这样你就可以启动任务后离开窗口。你正在查看的会话不会发送通知。你可以随时在设置 › 通知中更改。", + noThanks: "不用了", + notifyMe: "通知我", + }, + paneSeams: { + resizeColumns: "调整列宽", + resizeRows: "调整行高", + }, + patchNotesDialog: { + title: "lich {version} 的新功能", + viewChangelog: "查看完整更新日志", + gotIt: "知道了", + }, + planQuota: { + planUsage: "套餐用量", + locked: "已锁定", + }, + providerSetupDialog: { + chooseTitle: "选择你的智能体", + noneTitle: "未找到编码智能体", + chooseDescription: + "lich 在你的机器上找到了这些。打开你使用的,其余的不会出现在新建会话菜单中。", + noneDescription: "lich 运行你机器上已安装的智能体,但一个也没找到。请安装一个,然后再检查。", + changeLater: "你可以随时在设置 › 提供商中更改。", + pointToBinary: "已经安装了?在设置 › 提供商中让 lich 指向它的可执行文件。", + continue: "继续", + }, + quotaGauge: { + aheadOfPace: "超前于进度", + aheadTooltip: "消耗速度超前于此窗口的进度。", + locked: "已锁定", + }, + resumeSessionDialog: { + title: "恢复之前的会话?", + description: + "{label} 留下了一段对话({id})。选择恢复可从中断处继续,或选择新建以开始空白对话。", + alwaysFor: "对 {provider} 始终如此", + changeInSettings: "· 可在设置 › 提供商中更改", + thisProvider: "此提供商", + startNew: "新建", + resume: "恢复", + }, + shortcutsOverlay: { + title: "键盘快捷键", + rebind: "在设置 › 快捷键中重新绑定", + close: "关闭", + }, + uncleanExitGate: { + title: "lich 上次运行意外结束", + description: "你的会话已恢复,但它们正在运行的内容已随之停止。", + openLogFolder: "打开日志文件夹", + opening: "正在打开日志文件夹…", + opened: "已打开日志文件夹。", + openFailed: "无法打开日志文件夹", + dismiss: "忽略", + }, + updateProgressToast: { + downloading: "正在下载 lich {version}", + installing: "正在安装 lich {version}…", + reopens: "lich 会自行关闭并重新打开。", + }, + app: { + stage: "舞台", + reloadStage: "重新加载舞台", + screen: "此界面", + panel: "此面板", + }, + projects: { + relocated: "{name} 现在打开 {path}", + relocateFailed: "重新定位失败:{error}", + noLongerAvailable: "{label} 已无法恢复", + bringBackFailed: "无法恢复 {label}", + closed: "已关闭 {label}", + undo: "撤销", + entrypointSet: "已设置入口命令", + entrypointSetDescription: "将在此终端下次启动时运行。", + entrypointCleared: "已清除入口命令", + entrypointClearedDescription: "此终端将再次启动普通 shell。", + entrypointFailed: "无法保存入口命令:{error}", + scheduled: "已定时 {when}", + scheduledNow: "已立即定时", + scheduleCleared: "已清除定时", + scheduleFailed: "无法设置定时:{error}", + }, + projectEvents: { + unlabeledSession: "一个会话", + scheduleLost: "定时提示词随 {label} 一同丢失", + scheduleDue: "{when} 到期:{prompt}", + relayAnswered: "{target} 已在其自己的会话中回复", + relayDetail: "它结束时没有通过 lich 回复,请打开它查看答案。", + open: "打开", + needsInput: "{label} 需要你的输入", + finished: "{label} 已完成工作", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/sidebar.ts b/frontend/src/lib/i18n/locales/zh-cn/sidebar.ts new file mode 100644 index 00000000..a65312b7 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/sidebar.ts @@ -0,0 +1,271 @@ +import type { Shape } from "../../catalog" +import type { sidebar as en } from "../en/sidebar" + +export const sidebar = { + cardColor: { + menu: "颜色", + current: "{color}(当前)", + color: { + theme: "主题色", + red: "红色", + orange: "橙色", + amber: "琥珀色", + green: "绿色", + teal: "青色", + blue: "蓝色", + violet: "紫罗兰色", + pink: "粉色", + }, + }, + entrypointDialog: { + title: "入口命令", + description: "每次启动此终端时运行。退出后会回到 shell。", + command: "命令", + hint: "在 {cwd} 中运行,不会加载你的 shell rc 文件,因此别名不能作为命令。留空则启动普通 shell。", + }, + newFolderDialog: { + title: "新建文件夹", + description: { + one: "将此会话归入你自己命名的文件夹。会话保持打开,其检出不会改变。", + other: "将这 {count} 个会话归入你自己命名的文件夹。会话保持打开,它们的检出不会改变。", + }, + name: "名称", + placeholder: "设计系统", + exists: "{name} 已存在,因此会归入其中。", + moveHere: "移到此处", + }, + sessionCard: { + schedule: "定时发送提示词…", + scheduledIn: "已定时 {when}…", + scheduledDue: "已定时…", + handOffFailed: "无法交给该会话:{error}", + openCheckoutFailed: "无法打开检出:{error}", + commandCopied: "已复制:{command}", + copyCommandFailed: "无法复制命令:{error}", + openFolderFailed: "无法打开文件夹:{error}", + sandboxed: "沙箱中", + sandboxedDrift: "沙箱中,设置已变更", + unsandboxedDrift: "不在沙箱中,设置已变更", + viewPullRequest: "查看拉取请求 #{number}", + askForPullRequest: "让智能体创建拉取请求", + pullRequestShort: "PR", + pinNamed: "置顶 {name}", + unpinNamed: "取消置顶 {name}", + closeNamed: "关闭 {name}", + rename: "重命名", + pin: "置顶", + unpin: "取消置顶", + moveToFolder: "移到文件夹", + takeOutOf: "从 {folder} 中移出", + newFolder: "新建文件夹…", + showBeside: "并排显示", + stopShowing: "停止显示", + showBesideDelegates: { + one: "并排显示其 {count} 个被委派会话", + other: "并排显示其 {count} 个被委派会话", + }, + delegate: "委派给会话…", + copySendCommand: "复制发送命令", + openIn: "打开方式", + terminal: "终端", + editor: "编辑器", + fileManager: "文件管理器", + pullRequest: "拉取请求", + closeSession: "关闭会话", + }, + sessionEntrypointItem: { + label: "入口命令…", + shellOnly: "入口命令仅适用于终端会话。", + }, + sessionForkItem: { + label: "分叉到工作树…", + }, + settingsCard: { + label: "设置", + close: "关闭设置", + }, + pullRequestCard: { + label: "拉取请求", + close: "关闭拉取请求", + open: "打开", + }, + sessionSidebar: { + newSession: "新建会话", + newSessionButton: "新建会话", + filter: "筛选会话", + collapse: "折叠侧边栏", + resize: "调整侧边栏大小", + pullRequests: "拉取请求", + closePullRequests: "关闭拉取请求", + delegatesSkipped: { + one: "{count} 个被委派会话无法加入分屏", + other: "{count} 个被委派会话无法加入分屏", + }, + issueHandoffFailed: "无法将该议题交给会话:{error}", + forkPruned: "{name} 已没有可分叉的对话:{provider} 已将其清理。", + moveTitle: "将 {name} 移到此分屏?", + showBesideTitle: "在此会话旁显示 {name}?", + moveEndsGroup: + "它是 {group} 中仅有的两个会话之一,因此它离开后该分组会结束。无论如何都不会关闭任何会话。", + moveLeavesSplit: + "它已在 {group} 中,而一个会话同一时间只能在一个分屏中,因此它会离开原分屏。无论如何都不会关闭任何会话。", + moveConfirm: "移动", + }, + sidebarRail: { + expand: "展开侧边栏", + newSession: "新建会话", + }, + sessionTargetPicker: { + title: "委派给会话", + placeholder: "搜索要委派的会话…", + search: "搜索要委派的会话", + results: "此会话可委派的会话", + pick: "选择", + noMatch: "没有匹配 {query} 的会话", + newWorktree: "新建工作树会话…", + }, + sessionPhaseChips: { + group: "按状态筛选", + all: "全部", + phase: { + waiting: "等待中", + running: "运行中", + unread: "未读", + idle: "空闲", + }, + }, + sessionStatusRung: { + commandLine: "命令行", + waitingOnYou: "等待你操作", + compacting: "正在压缩…", + handoffHeld: "有内容正在等待此提示词", + inbox: { one: "{count} 个结果已就绪", other: "{count} 个结果已就绪" }, + todo: "已完成 {progress}", + todoProgress: "{done}/{total}", + scheduled: "已定时 {when}", + waitingForPrompt: "等待提示词", + origin: "来自 {origin}", + }, + sessionGroup: { + pinned: "已置顶", + }, + sessionGroupHeader: { + expand: "展开 {name}", + collapse: "折叠 {name}", + waiting: "此处有会话在等待你操作", + unread: "此处有未读的回合", + groupName: "分组名称", + newSessionIn: "在 {name} 中新建会话", + optionsFor: "{name} 的选项", + renameFolder: "重命名文件夹", + renameGroup: "重命名分组", + ungroup: "取消分组", + moveToFolder: "将分组移到文件夹", + newFolder: "新建文件夹…", + }, + sessionLaunchMenuItems: { + terminal: "终端", + newTerminal: "新建终端", + run: "运行", + goToRunCard: "前往运行卡片", + runNotOnWindows: "运行脚本是 sh 脚本,不会在 Windows 上运行。", + runConfined: "受限运行", + openSession: "打开会话", + back: "返回", + worktree: "工作树", + }, + folderLaunchMenuItems: { + newSessionIn: "在 {folder} 中新建会话", + newSessionInOn: "在 {folder} 中新建会话,位于 {checkout}", + }, + schedulePromptDialog: { + title: "定时发送提示词", + description: "到时间后会输入到 {label},效果与你亲自输入相同。", + placeholder: "执行发布检查清单", + scheduledFor: "定时于 {when}", + cancel: "取消定时", + }, + worktreeDialog: { + title: "新建工作树", + forkTitle: "将 {label} 分叉到新工作树", + description: "选择基础分支,并可选填新工作树的名称。", + carriesConversation: "带上对话", + carriesHistory: "新会话会基于 {label} 的历史副本打开,并从那里继续。原卡片不受影响。", + name: "工作树名称", + namePlaceholder: "分支名、议题(#128)或用文字描述的任务", + opensSelected: "打开所选工作树", + lookingUpIssue: "正在查找 #{number}…", + branch: "分支:{branch}", + autoBranch: "<自动生成>", + baseBranch: "基础分支", + loadingBranches: "正在加载分支…", + searchPlaceholder: "搜索分支…", + searchLabel: "搜索基础分支", + thisSession: "此会话", + thisCheckout: "此检出", + workingTree: "{branch} · 工作区", + sessionUncommitted: { + one: "同一提交,另加此会话尚未提交的 {count} 个文件。", + other: "同一提交,另加此会话尚未提交的 {count} 个文件。", + }, + checkoutUncommitted: { + one: "同一提交,另加此检出尚未提交的 {count} 个文件。", + other: "同一提交,另加此检出尚未提交的 {count} 个文件。", + }, + worktrees: "工作树", + localBranches: "本地分支", + remoteBranches: "远程分支", + noMatch: "没有匹配 {filter} 的分支", + noBranches: "未找到分支", + creating: "正在创建…", + open: "打开工作树", + fork: "分叉", + create: "创建工作树", + }, + worktreeCloseDialogs: { + runningTitle: "会话仍在运行", + runningDescription: "{label} 正在执行一个回合。关闭会让进程就地停止,进行中的回合将丢失。", + closeAnyway: "仍然关闭", + closeTitle: "关闭工作树会话", + adoptedDescription: + "位于 {path} 的工作树不是 lich 创建的。保留它,还是删除该检出?删除会移除该目录,但保留其分支。", + createdDescription: "保留还是删除位于 {path} 的工作树?删除会移除检出,但保留其分支。", + keep: "保留工作树", + remove: "删除工作树", + dirtyTitle: "工作树有未提交的更改", + dirtyDescription: + "位于 {path} 的工作树包含未提交的更改。删除后这些更改将被永久丢弃。分支会保留。", + discardAndRemove: "丢弃更改并删除", + }, + worktreeScriptRows: { + setupEditorLabel: "设置脚本,保存到 {file}", + runEditorLabel: "运行命令,保存到 {file}", + discard: "丢弃", + detected: "检测到 {detected}。在新工作树中运行 {command}?", + use: "使用", + edit: "编辑", + noRunCommand: "没有运行命令,因此该检出保留的端口保持未使用。", + set: "设置", + }, + worktreeSandboxRow: { + runConfined: "受限运行", + }, + useWorktreeClose: { + removeFailed: "删除工作树失败:{error}", + }, + sessionTooltip: { + waitingOn: "正在等待 {peer}", + answering: "正在回复 {peer}", + commandLine: "命令行", + ticket: "工单 {ticket}", + scheduledFor: "定时于 {when}", + sandboxed: "沙箱中", + notSandboxed: "不在沙箱中", + confinedMeans: "主目录为空,机器只读,仅可写入此检出。", + unconfinedMeans: "此会话直接运行在本机上。", + settingMoved: "在沙箱设置变更前打开;重新打开会话以应用。", + setAtOpen: "会话打开时确定;重新打开以更改。", + notMounted: "未挂载(符号链接):{paths}", + morePaths: "另外 {count} 项", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/tabs.ts b/frontend/src/lib/i18n/locales/zh-cn/tabs.ts new file mode 100644 index 00000000..32001077 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/tabs.ts @@ -0,0 +1,38 @@ +import type { Shape } from "../../catalog" +import type { tabs as en } from "../en/tabs" + +export const tabs = { + homeTab: { + home: "主页", + }, + notificationsButton: { + title: "通知", + titlePending: "通知,{count} 条待处理", + caughtUp: "你已处理完所有通知", + dismiss: "忽略 {name}", + }, + openProjectMenu: { + open: "打开项目", + recent: "最近的项目", + relocate: "重新定位", + openFolder: "打开文件夹…", + moreClosed: { + one: "还有 {count} 个已关闭的项目,请在命令面板中搜索", + other: "还有 {count} 个已关闭的项目,请在命令面板中搜索", + }, + }, + projectTab: { + close: "关闭 {name}", + }, + projectTabs: { + pullRequests: "拉取请求", + settings: "设置", + closeTitle: "会话仍在运行", + closeBody: { + one: "关闭 {name} 会中断一个正在进行一轮的会话。重新打开项目时可选择从每个会话中断处恢复,进行中的那一轮会丢失。", + other: + "关闭 {name} 会中断 {count} 个正在进行一轮的会话。重新打开项目时可选择从每个会话中断处恢复,进行中的那一轮会丢失。", + }, + closeAnyway: "仍然关闭", + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/terminal.ts b/frontend/src/lib/i18n/locales/zh-cn/terminal.ts new file mode 100644 index 00000000..11603587 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/terminal.ts @@ -0,0 +1,47 @@ +import type { Shape } from "../../catalog" +import type { terminal as en } from "../en/terminal" + +export const terminal = { + dropHint: { + attachTo: "附加到 {label}", + confined: "位于检出目录之外时,会以副本形式传入", + pasted: "其路径会粘贴到提示符处", + }, + exitBanner: { + restart: "重新启动", + close: "关闭", + }, + searchBar: { + placeholder: "查找", + label: "搜索终端", + previous: "上一个匹配项", + next: "下一个匹配项", + close: "关闭搜索", + }, + drop: { + notAttached: "未附加:{files}", + }, + host: { + stopShowing: "不再显示 {name}", + paneName: "{name} 窗格", + rememberFailed: "无法记住该选择:{error}", + }, + view: { + restartFailed: "会话重新启动失败:{error}", + startFailed: "会话启动失败:{error}", + }, + sessionExit: { + ended: "会话已结束", + endedWithCode: "会话已结束,退出码 {code}", + }, + dropFiles: { + folderConfined: "无法移交沙箱会话检出目录之外的文件夹,请拖入文件", + folderMissing: "在此会话或你的主目录下找不到该文件夹,请拖入文件", + }, + copyToast: { + copied: { + one: "已复制 {count} 个字符到剪贴板", + other: "已复制 {count} 个字符到剪贴板", + }, + }, +} satisfies Shape diff --git a/frontend/src/lib/i18n/locales/zh-cn/update.ts b/frontend/src/lib/i18n/locales/zh-cn/update.ts new file mode 100644 index 00000000..f13e493e --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh-cn/update.ts @@ -0,0 +1,18 @@ +import type { Shape } from "../../catalog" +import type { update as en } from "../en/update" + +export const update = { + plugin: { + incompatibleInstall: + "{installs} 中的 lich 插件不受此 lich 支持。请安装 v{version},即此 lich 支持的版本。", + incompatibleUpdate: + "{installs} 中的 lich 插件不受此 lich 支持。请更新 lich,或确认已联网以查找此 lich 支持的版本。", + }, + progress: { + updateFailed: "更新失败:{error}", + installFailed: "安装失败:{error}", + downloadFailed: "下载失败:{error}", + downloadFailedAt: "下载在 {percent}% 时失败:{error}", + bytesOf: "{received} / {total}", + }, +} satisfies Shape diff --git a/frontend/src/lib/prompt-language-store.ts b/frontend/src/lib/prompt-language-store.ts index 7f5ea0f1..56101ec9 100644 --- a/frontend/src/lib/prompt-language-store.ts +++ b/frontend/src/lib/prompt-language-store.ts @@ -7,7 +7,7 @@ import { Store } from "@/lib/rpc" // every message, a spawn reads it for the briefing and LICH_PROMPT_LANG. // This store is the page's copy, loaded once and written through. // -// The tags are the interface's own (en, pt-BR): both sides ship the same set. +// The tags are the interface's own (en, pt-BR, zh-CN): both sides ship the same set. // SETTING_KEY and GLOBAL_SCOPE mirror the Go side's promptLanguageKey and its // global (project-less) scope.