From 7684bd09aa8e40a99b3f8d9b331cb49e8d7b0ebe Mon Sep 17 00:00:00 2001 From: BillChenIDY <31582542+BillChenIDY@users.noreply.github.com> Date: Wed, 5 Aug 2026 04:51:32 +0800 Subject: [PATCH] feat(i18n): add Simplified Chinese locale (#2243) Co-authored-by: Carlos Florencio --- apps/backend/internal/i18n/i18n.go | 14 +- apps/backend/internal/i18n/i18n_test.go | 45 +- apps/backend/internal/i18n/locales/zh-cn.json | 36 + .../system/updates/notify_dispatch_test.go | 11 +- apps/backend/internal/webapp/shell_test.go | 3 + apps/web/app/settings/executor/new/page.tsx | 3 +- .../components/github/issue-watch-dialog.tsx | 5 +- .../settings/executor-profile-dialog.tsx | 6 +- .../profile-edit/inline-secret-select.tsx | 3 +- .../components/settings/sprites-settings.tsx | 12 +- .../settings/voice-mode-settings.tsx | 12 +- .../settings/workflow-sync-dialog.tsx | 10 +- .../chat/messages/turn-group-message.test.tsx | 4 +- apps/web/e2e/fixtures/backend.ts | 45 +- .../e2e/tests/i18n/language-switch.spec.ts | 45 +- .../tests/i18n/mobile-language-switch.spec.ts | 35 + apps/web/lib/e2e/backend-process.test.ts | 36 + apps/web/lib/i18n/boot.test.ts | 6 +- apps/web/lib/i18n/formats.test.ts | 24 + apps/web/lib/i18n/index.test.ts | 34 +- apps/web/lib/i18n/index.ts | 35 +- apps/web/scripts/check-i18n-keys.mjs | 28 +- apps/web/scripts/check-i18n-keys.test.ts | 178 +++++ apps/web/scripts/lib/i18n-catalogs.mjs | 151 +++++ apps/web/src/locales/en/chat.json | 4 +- apps/web/src/locales/en/executors.json | 6 +- apps/web/src/locales/en/github.json | 4 +- apps/web/src/locales/en/settings.json | 4 +- apps/web/src/locales/en/workflows.json | 2 +- apps/web/src/locales/pseudo/chat.json | 4 +- apps/web/src/locales/pseudo/executors.json | 6 +- apps/web/src/locales/pseudo/github.json | 4 +- apps/web/src/locales/pseudo/settings.json | 4 +- apps/web/src/locales/pseudo/workflows.json | 2 +- apps/web/src/locales/zh-cn/agents.json | 198 ++++++ apps/web/src/locales/zh-cn/automations.json | 155 +++++ apps/web/src/locales/zh-cn/azuredevops.json | 156 +++++ apps/web/src/locales/zh-cn/chat.json | 40 ++ apps/web/src/locales/zh-cn/common.json | 91 +++ apps/web/src/locales/zh-cn/configChat.json | 21 + apps/web/src/locales/zh-cn/executors.json | 172 +++++ apps/web/src/locales/zh-cn/github.json | 371 +++++++++++ apps/web/src/locales/zh-cn/gitlab.json | 151 +++++ apps/web/src/locales/zh-cn/jira.json | 149 +++++ apps/web/src/locales/zh-cn/linear.json | 143 ++++ apps/web/src/locales/zh-cn/sentry.json | 154 +++++ apps/web/src/locales/zh-cn/settings.json | 619 ++++++++++++++++++ apps/web/src/locales/zh-cn/sidebar.json | 76 +++ apps/web/src/locales/zh-cn/slack.json | 39 ++ apps/web/src/locales/zh-cn/system.json | 453 +++++++++++++ apps/web/src/locales/zh-cn/workflows.json | 242 +++++++ apps/web/src/locales/zh-cn/workspaces.json | 106 +++ docs/i18n.md | 56 +- docs/plans/i18n-zh-cn/plan.md | 155 +++++ .../task-01-frontend-locale-catalogs.md | 71 ++ .../task-02-backend-locale-negotiation.md | 62 ++ .../i18n-zh-cn/task-03-catalog-parity-gate.md | 68 ++ .../i18n-zh-cn/task-04-chinese-locale-e2e.md | 81 +++ .../task-05-localization-documentation.md | 73 +++ .../task-06-repository-verification.md | 113 ++++ docs/public/feature-status.md | 1 + docs/specs/platform/i18n.md | 100 +-- 62 files changed, 4808 insertions(+), 129 deletions(-) create mode 100644 apps/backend/internal/i18n/locales/zh-cn.json create mode 100644 apps/web/e2e/tests/i18n/mobile-language-switch.spec.ts create mode 100644 apps/web/lib/e2e/backend-process.test.ts create mode 100644 apps/web/scripts/check-i18n-keys.test.ts create mode 100644 apps/web/scripts/lib/i18n-catalogs.mjs create mode 100644 apps/web/src/locales/zh-cn/agents.json create mode 100644 apps/web/src/locales/zh-cn/automations.json create mode 100644 apps/web/src/locales/zh-cn/azuredevops.json create mode 100644 apps/web/src/locales/zh-cn/chat.json create mode 100644 apps/web/src/locales/zh-cn/common.json create mode 100644 apps/web/src/locales/zh-cn/configChat.json create mode 100644 apps/web/src/locales/zh-cn/executors.json create mode 100644 apps/web/src/locales/zh-cn/github.json create mode 100644 apps/web/src/locales/zh-cn/gitlab.json create mode 100644 apps/web/src/locales/zh-cn/jira.json create mode 100644 apps/web/src/locales/zh-cn/linear.json create mode 100644 apps/web/src/locales/zh-cn/sentry.json create mode 100644 apps/web/src/locales/zh-cn/settings.json create mode 100644 apps/web/src/locales/zh-cn/sidebar.json create mode 100644 apps/web/src/locales/zh-cn/slack.json create mode 100644 apps/web/src/locales/zh-cn/system.json create mode 100644 apps/web/src/locales/zh-cn/workflows.json create mode 100644 apps/web/src/locales/zh-cn/workspaces.json create mode 100644 docs/plans/i18n-zh-cn/plan.md create mode 100644 docs/plans/i18n-zh-cn/task-01-frontend-locale-catalogs.md create mode 100644 docs/plans/i18n-zh-cn/task-02-backend-locale-negotiation.md create mode 100644 docs/plans/i18n-zh-cn/task-03-catalog-parity-gate.md create mode 100644 docs/plans/i18n-zh-cn/task-04-chinese-locale-e2e.md create mode 100644 docs/plans/i18n-zh-cn/task-05-localization-documentation.md create mode 100644 docs/plans/i18n-zh-cn/task-06-repository-verification.md diff --git a/apps/backend/internal/i18n/i18n.go b/apps/backend/internal/i18n/i18n.go index c04cd91af8..009c1e2039 100644 --- a/apps/backend/internal/i18n/i18n.go +++ b/apps/backend/internal/i18n/i18n.go @@ -44,6 +44,7 @@ const LocaleCookie = "kandev_locale" // with SUPPORTED_LOCALES in apps/web/lib/i18n/index.ts. var supportedLocales = map[string]bool{ "en": true, + "zh-cn": true, "pseudo": true, } @@ -77,17 +78,22 @@ func load() { } // Supported reports whether locale has a committed catalog. -func Supported(locale string) bool { return supportedLocales[locale] } +func Supported(locale string) bool { return supportedLocales[canonicalLocale(locale)] } // Normalize returns locale when it is supported, otherwise DefaultLocale. Used // for both `` and message lookup so they can never disagree. func Normalize(locale string) string { - if supportedLocales[locale] { - return locale + canonical := canonicalLocale(locale) + if supportedLocales[canonical] { + return canonical } return DefaultLocale } +func canonicalLocale(locale string) string { + return strings.ToLower(strings.TrimSpace(locale)) +} + // FromRequest resolves the active locale: the kandev_locale cookie first (the // SPA's explicit user choice), then the browser's Accept-Language, then the // default. Unknown values coerce to DefaultLocale rather than erroring. @@ -96,7 +102,7 @@ func FromRequest(r *http.Request) string { return DefaultLocale } if cookie, err := r.Cookie(LocaleCookie); err == nil && Supported(cookie.Value) { - return cookie.Value + return Normalize(cookie.Value) } for _, tag := range parseAcceptLanguage(r.Header.Get("Accept-Language")) { if Supported(tag) { diff --git a/apps/backend/internal/i18n/i18n_test.go b/apps/backend/internal/i18n/i18n_test.go index a0a033e480..129e95b513 100644 --- a/apps/backend/internal/i18n/i18n_test.go +++ b/apps/backend/internal/i18n/i18n_test.go @@ -11,6 +11,8 @@ func TestNormalize(t *testing.T) { t.Parallel() for _, tc := range []struct{ in, want string }{ {"en", "en"}, + {"zh-cn", "zh-cn"}, + {"zh-CN", "zh-cn"}, {"pseudo", "pseudo"}, {"", "en"}, {"fr", "en"}, @@ -33,6 +35,9 @@ func TestTranslatesAndFallsBack(t *testing.T) { if pseudo := T("pseudo", "webapp.shellUnavailable"); pseudo == en { t.Fatalf("pseudo message should differ from en, both %q", en) } + if chinese := T("zh-cn", "webapp.shellUnavailable"); chinese == en { + t.Fatalf("zh-cn message should differ from en, both %q", en) + } // An unknown locale falls back to en rather than erroring or blanking. if got := T("klingon", "webapp.shellUnavailable"); got != en { t.Fatalf("unknown locale = %q, want en fallback %q", got, en) @@ -111,11 +116,22 @@ func TestTfPseudoLocaleKeepsPlaceholderValues(t *testing.T) { func TestCatalogsHaveMatchingKeys(t *testing.T) { t.Parallel() - // Every en key must exist in pseudo, otherwise the QA locale silently shows - // English and stops proving that a string was externalized. - for _, key := range Keys() { - if T("pseudo", key) == key { - t.Fatalf("pseudo catalog is missing key %q", key) + load() + source := catalogs[DefaultLocale] + for _, locale := range []string{"pseudo", "zh-cn"} { + translated := catalogs[locale] + if len(translated) != len(source) { + t.Fatalf("%s catalog has %d keys, want %d", locale, len(translated), len(source)) + } + for key := range source { + if _, ok := translated[key]; !ok { + t.Fatalf("%s catalog is missing key %q", locale, key) + } + } + for key := range translated { + if _, ok := source[key]; !ok { + t.Fatalf("%s catalog has extra key %q", locale, key) + } } } } @@ -133,6 +149,16 @@ func TestFromRequest(t *testing.T) { } }) + t.Run("zh-cn cookie wins over accept-language", func(t *testing.T) { + t.Parallel() + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.AddCookie(&http.Cookie{Name: LocaleCookie, Value: "zh-cn"}) + r.Header.Set("Accept-Language", "en") + if got := FromRequest(r); got != "zh-cn" { + t.Fatalf("got %q, want zh-cn", got) + } + }) + t.Run("invalid cookie ignored", func(t *testing.T) { t.Parallel() r := httptest.NewRequest(http.MethodGet, "/", nil) @@ -151,6 +177,15 @@ func TestFromRequest(t *testing.T) { } }) + t.Run("accept-language canonicalizes zh-CN and honors q-value", func(t *testing.T) { + t.Parallel() + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("Accept-Language", "en;q=0.4, zh-CN;q=0.9") + if got := FromRequest(r); got != "zh-cn" { + t.Fatalf("got %q, want zh-cn", got) + } + }) + t.Run("region subtag matches base locale", func(t *testing.T) { t.Parallel() r := httptest.NewRequest(http.MethodGet, "/", nil) diff --git a/apps/backend/internal/i18n/locales/zh-cn.json b/apps/backend/internal/i18n/locales/zh-cn.json new file mode 100644 index 0000000000..84949b67e8 --- /dev/null +++ b/apps/backend/internal/i18n/locales/zh-cn.json @@ -0,0 +1,36 @@ +{ + "share.brandTag": "分享的任务", + "share.builtWith": "基于", + "share.ctaTryKandev": "在 GitHub 上试用 kandev", + "share.emptyMessage": "空", + "share.gistDescription": "kandev 分享:{{title}}", + "share.gistDescriptionUntitled": "kandev 任务分享", + "share.messageCount_one": "{{count}} 条消息", + "share.messageCount_other": "{{count}} 条消息", + "share.metaAgent": "代理", + "share.metaCompleted": "已完成", + "share.metaExecutor": "执行器", + "share.metaModel": "模型", + "share.metaStarted": "开始时间", + "share.metaWorkflowStep": "工作流步骤", + "share.noMessages": "暂无消息。", + "share.noMetadata": "无元数据", + "share.openRenderedView": "**[打开渲染视图 →]({{url}})**,查看正确排版的对话。", + "share.openShareHTML": "**打开 {{file}}** 查看渲染后的对话。", + "share.pageTitle": "{{title}} — kandev 分享", + "share.pitch": "由 **[kandev]({{url}})** 分享——开源的智能代理开发环境。", + "share.rawExport": "原始导出:", + "share.redacted": "已脱敏:", + "share.redactedBeforePublish": "发布前已脱敏:", + "share.roleAssistant": "助手", + "share.roleSystem": "系统", + "share.roleUser": "用户", + "share.roleYou": "你", + "share.sessionDetails": "会话详情", + "share.toolOutput": "工具输出", + "share.toolOutputTruncated": "工具输出(已截断)", + "share.untitledTask": "未命名任务", + "webapp.assetReadFailed": "无法读取网页资源。", + "webapp.devServerUnavailable": "Vite 开发服务器不可用。", + "webapp.shellUnavailable": "Kandev Web 应用不可用。" +} diff --git a/apps/backend/internal/system/updates/notify_dispatch_test.go b/apps/backend/internal/system/updates/notify_dispatch_test.go index da50716201..df68973974 100644 --- a/apps/backend/internal/system/updates/notify_dispatch_test.go +++ b/apps/backend/internal/system/updates/notify_dispatch_test.go @@ -149,8 +149,16 @@ func TestService_ReplayCachedUpdateSerializesWithChannelSelection(t *testing.T) t.Cleanup(releaseReplay) svc.SetNotifier(notifier) nightlyFetchEntered := make(chan struct{}) - svc.SetNightlyFetcher(func(context.Context) (string, string, error) { + nightlyFetchRelease := make(chan struct{}) + releaseNightlyFetch := sync.OnceFunc(func() { close(nightlyFetchRelease) }) + t.Cleanup(releaseNightlyFetch) + svc.SetNightlyFetcher(func(ctx context.Context) (string, string, error) { close(nightlyFetchEntered) + select { + case <-nightlyFetchRelease: + case <-ctx.Done(): + return "", "", ctx.Err() + } return "v1.2.5-nightly.shaabcdef123456", "https://example.test/nightly", nil }) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) @@ -199,6 +207,7 @@ func TestService_ReplayCachedUpdateSerializesWithChannelSelection(t *testing.T) case <-ctx.Done(): t.Fatal("channel selection did not resolve Nightly") } + releaseNightlyFetch() select { case err := <-selectDone: if err != nil { diff --git a/apps/backend/internal/webapp/shell_test.go b/apps/backend/internal/webapp/shell_test.go index 4346907416..8baeb62bbf 100644 --- a/apps/backend/internal/webapp/shell_test.go +++ b/apps/backend/internal/webapp/shell_test.go @@ -110,6 +110,7 @@ func TestRenderShellSetsHTMLLangFromLocale(t *testing.T) { wantLang string }{ {name: "default when empty", locale: "", wantLang: `lang="en"`}, + {name: "supported simplified chinese", locale: "zh-cn", wantLang: `lang="zh-cn"`}, {name: "supported pseudo", locale: "pseudo", wantLang: `lang="pseudo"`}, {name: "unknown coerces to en", locale: "klingon", wantLang: `lang="en"`}, } @@ -160,6 +161,8 @@ func TestNormalizeLocale(t *testing.T) { for _, tc := range []struct{ in, want string }{ {"en", "en"}, + {"zh-cn", "zh-cn"}, + {"zh-CN", "zh-cn"}, {"pseudo", "pseudo"}, {"", "en"}, {"fr", "en"}, diff --git a/apps/web/app/settings/executor/new/page.tsx b/apps/web/app/settings/executor/new/page.tsx index fbe36734c2..cc5177cebc 100644 --- a/apps/web/app/settings/executor/new/page.tsx +++ b/apps/web/app/settings/executor/new/page.tsx @@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next"; const EXECUTOR_TYPES = ["local_docker", "remote_docker"] as const; type ExecutorType = (typeof EXECUTOR_TYPES)[number]; +const REMOTE_DOCKER_SCHEMES = "tcp://, ssh://"; export default function ExecutorCreatePage() { const { t } = useTranslation(); @@ -169,7 +170,7 @@ function ExecutorFormCard({ />

{isRemoteDocker - ? t("executors:theRemoteDockerHostUrlTcp") + ? t("executors:theRemoteDockerHostUrlTcp", { schemes: REMOTE_DOCKER_SCHEMES }) : t("executors:repositoriesWillBeMountedAsVolumes")}

diff --git a/apps/web/components/github/issue-watch-dialog.tsx b/apps/web/components/github/issue-watch-dialog.tsx index 4201a91133..946cfdc8af 100644 --- a/apps/web/components/github/issue-watch-dialog.tsx +++ b/apps/web/components/github/issue-watch-dialog.tsx @@ -75,6 +75,7 @@ type FormState = { }; const DEFAULT_QUERY = "type:issue state:open"; +const ISSUE_LABEL_EXAMPLES = "bug, enhancement, priority:high"; function makeDefaultForm(workspaceId: string): FormState { return { @@ -236,7 +237,9 @@ function IssueFilterFields({ setForm((prev) => ({ ...prev, labels: e.target.value }))} - placeholder={t("github:eGBugEnhancementPriorityHigh")} + placeholder={t("github:eGBugEnhancementPriorityHigh", { + examples: ISSUE_LABEL_EXAMPLES, + })} />

{t("github:onlyMatchIssuesWithTheseLabels")} diff --git a/apps/web/components/settings/executor-profile-dialog.tsx b/apps/web/components/settings/executor-profile-dialog.tsx index 9c61d36a98..1fe1c43b0e 100644 --- a/apps/web/components/settings/executor-profile-dialog.tsx +++ b/apps/web/components/settings/executor-profile-dialog.tsx @@ -16,6 +16,8 @@ import { createExecutorProfile } from "@/lib/api/domains/settings-api"; import type { ExecutorProfile } from "@/lib/types/http"; import { useTranslation } from "react-i18next"; +const PROFILE_NAME_EXAMPLE = "Production, Development"; + type ExecutorProfileDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; @@ -73,7 +75,9 @@ export function ExecutorProfileDialog({ id="profile-name" value={name} onChange={(e) => setName(e.target.value)} - placeholder={t("executors:eGProductionDevelopment")} + placeholder={t("executors:eGProductionDevelopment", { + example: PROFILE_NAME_EXAMPLE, + })} autoFocus /> diff --git a/apps/web/components/settings/profile-edit/inline-secret-select.tsx b/apps/web/components/settings/profile-edit/inline-secret-select.tsx index b501cdeba4..c6a213b2cf 100644 --- a/apps/web/components/settings/profile-edit/inline-secret-select.tsx +++ b/apps/web/components/settings/profile-edit/inline-secret-select.tsx @@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next"; const NONE_VALUE = "__none__"; const CREATE_VALUE = "__create__"; +const SECRET_NAME_EXAMPLE = "my-api-token"; type InlineSecretSelectProps = { secretId: string | null; @@ -113,7 +114,7 @@ function InlineCreateForm({ setName(e.target.value)} - placeholder={t("executors:eGMyApiToken")} + placeholder={t("executors:eGMyApiToken", { example: SECRET_NAME_EXAMPLE })} className="h-8 text-sm" /> diff --git a/apps/web/components/settings/sprites-settings.tsx b/apps/web/components/settings/sprites-settings.tsx index 0a6fcd4f35..87c2b46e28 100644 --- a/apps/web/components/settings/sprites-settings.tsx +++ b/apps/web/components/settings/sprites-settings.tsx @@ -24,6 +24,8 @@ import { import type { SpritesInstance, SpritesTestResult, SpritesTestStep } from "@/lib/types/http-sprites"; import { Trans, useTranslation } from "react-i18next"; +const SPRITES_TOKEN_ENV_VAR = "SPRITES_API_TOKEN"; + export function SpritesConnectionCard({ secretId }: { secretId?: string }) { const { t } = useTranslation(); const { status } = useSprites(secretId); @@ -79,9 +81,13 @@ export function SpritesConnectionCard({ secretId }: { secretId?: string }) {

) : (

- - Configure a SPRITES_API_TOKEN environment variable - in the executor profile, referencing a secret with your Sprites.dev API token. + + Configure a {SPRITES_TOKEN_ENV_VAR} environment + variable in the executor profile, referencing a secret with your Sprites.dev API + token.

)} diff --git a/apps/web/components/settings/voice-mode-settings.tsx b/apps/web/components/settings/voice-mode-settings.tsx index fa6b758bd7..2badbfad22 100644 --- a/apps/web/components/settings/voice-mode-settings.tsx +++ b/apps/web/components/settings/voice-mode-settings.tsx @@ -38,6 +38,11 @@ import type { import { useSettingsSaveContributor } from "./settings-save-provider"; import { SettingsCard } from "./settings-card"; +const VOICE_SECURE_SCHEME = "HTTPS"; +const VOICE_LOCALHOST_HOST = "localhost"; +const VOICE_INSECURE_SCHEME = "HTTP"; +const VOICE_LOCALHOST_URL = "http://localhost"; + // Single source of truth for the language options. Web Speech reads `lang`, // Whisper engines treat it as a hint. "auto" defers to the browser locale. // `value` is the BCP-47 tag handed to the engine, so it is never translated; @@ -506,7 +511,12 @@ function AvailabilityBanner({ caps }: { caps: VoiceCapabilities }) {

{t("settings:voiceUnavailableTitle")}

{insecure - ? t("settings:voiceUnavailableInsecure") + ? t("settings:voiceUnavailableInsecure", { + secureScheme: VOICE_SECURE_SCHEME, + localhostHost: VOICE_LOCALHOST_HOST, + insecureScheme: VOICE_INSECURE_SCHEME, + localhostUrl: VOICE_LOCALHOST_URL, + }) : t("settings:voiceUnavailableUnsupported")}

diff --git a/apps/web/components/settings/workflow-sync-dialog.tsx b/apps/web/components/settings/workflow-sync-dialog.tsx index fd97d92007..2cfcb509b4 100644 --- a/apps/web/components/settings/workflow-sync-dialog.tsx +++ b/apps/web/components/settings/workflow-sync-dialog.tsx @@ -19,6 +19,9 @@ import type { WorkflowSyncFormState, } from "@/hooks/domains/settings/use-workflow-sync"; +const WORKFLOW_EXPORT_FORMAT = "kandev_workflow"; +const WORKFLOW_EXPORT_EXTENSIONS = ".yml/.yaml/.json"; + type RepoUrlFieldProps = { url: string; invalid: boolean; @@ -157,7 +160,12 @@ export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDia onChange={sync.setUrlInput} /> -

{t("workflows:syncDirectoryHelp")}

+

+ {t("workflows:syncDirectoryHelp", { + format: WORKFLOW_EXPORT_FORMAT, + extensions: WORKFLOW_EXPORT_EXTENSIONS, + })} +

{hasConfig && ( diff --git a/apps/web/components/task/chat/messages/turn-group-message.test.tsx b/apps/web/components/task/chat/messages/turn-group-message.test.tsx index 76e98ee25d..a0cf87bbe1 100644 --- a/apps/web/components/task/chat/messages/turn-group-message.test.tsx +++ b/apps/web/components/task/chat/messages/turn-group-message.test.tsx @@ -187,9 +187,9 @@ describe("TurnGroupMessage collapsed label", () => { } it("pluralizes with the count it displays", () => { - expect(renderLabel(2)).toContain("tool calls"); + expect(renderLabel(2)).toContain("2 tool calls"); const single = renderLabel(1); - expect(single).toContain("tool call"); + expect(single).toContain("1 tool call"); expect(single).not.toContain("tool calls"); }); diff --git a/apps/web/e2e/fixtures/backend.ts b/apps/web/e2e/fixtures/backend.ts index 1b043847ed..ce57182ef1 100644 --- a/apps/web/e2e/fixtures/backend.ts +++ b/apps/web/e2e/fixtures/backend.ts @@ -1,5 +1,5 @@ import { test as base } from "@playwright/test"; -import { type ChildProcess, spawn } from "node:child_process"; +import { type ChildProcess, execFile, spawn } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -150,14 +150,34 @@ async function waitForPortFree(port: number, timeoutMs = 10_000): Promise // port is still held and waitForHealth will surface the error. } +type WindowsTreeKiller = (pid: number, done: (error?: Error) => void) => void; +type ProcessAliveProbe = (pid: number) => boolean; + +const taskkillProcessTree: WindowsTreeKiller = (pid, done) => { + execFile("taskkill", ["/PID", String(pid), "/T", "/F"], (error) => done(error ?? undefined)); +}; + +const isProcessAlive: ProcessAliveProbe = (pid) => { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +}; + /** - * Kills an entire process group. Used for the backend process which is spawned - * with `detached: true` so it becomes a process group leader. Sending signals - * to the negative PID targets all processes in that group (backend + agentctl). - * The 7s grace period gives agentctl time to cascade cleanup to agent process groups. + * Kills the backend and every child process it owns. POSIX uses the detached + * process group; Windows needs taskkill because negative-PID signals are not + * supported there. */ -function killProcessGroup(proc: ChildProcess): Promise { - return new Promise((resolve) => { +export function killProcessGroup( + proc: ChildProcess, + platform: NodeJS.Platform = process.platform, + killWindowsTree: WindowsTreeKiller = taskkillProcessTree, + processIsAlive: ProcessAliveProbe = isProcessAlive, +): Promise { + return new Promise((resolve, reject) => { if (!proc.pid) { resolve(); return; @@ -165,6 +185,17 @@ function killProcessGroup(proc: ChildProcess): Promise { const pid = proc.pid; + if (platform === "win32") { + killWindowsTree(pid, (error) => { + if (error && processIsAlive(pid)) { + reject(error); + return; + } + resolve(); + }); + return; + } + try { process.kill(-pid, "SIGTERM"); } catch { diff --git a/apps/web/e2e/tests/i18n/language-switch.spec.ts b/apps/web/e2e/tests/i18n/language-switch.spec.ts index ab222c1dc6..9e89124839 100644 --- a/apps/web/e2e/tests/i18n/language-switch.spec.ts +++ b/apps/web/e2e/tests/i18n/language-switch.spec.ts @@ -34,6 +34,41 @@ test.describe("i18n language switcher", () => { }); }); + test("switching to Simplified Chinese persists through reload and can restore English", async ({ + testPage, + prCapture, + }) => { + await testPage.goto(APPEARANCE_URL); + + const select = testPage.getByLabel("Display language"); + await expect(select).toBeVisible({ timeout: 10_000 }); + await select.click(); + await testPage.getByRole("listbox").getByRole("option", { name: "简体中文" }).click(); + + await expect(testPage.locator("html")).toHaveAttribute("lang", "zh-cn", { timeout: 10_000 }); + await expect(testPage.getByLabel("显示语言")).toBeVisible({ timeout: 10_000 }); + await expect + .poll(async () => { + const cookies = await testPage.context().cookies(); + return cookies.find((cookie) => cookie.name === "kandev_locale")?.value; + }) + .toBe("zh-cn"); + + await prCapture.screenshot("simplified-chinese-locale-desktop", { + caption: "Settings > General > Appearance with Simplified Chinese active", + }); + + await testPage.reload(); + await expect(testPage.locator("html")).toHaveAttribute("lang", "zh-cn", { timeout: 10_000 }); + await expect(testPage.getByLabel("显示语言")).toBeVisible({ timeout: 10_000 }); + + const selectAfter = testPage.getByLabel("显示语言"); + await selectAfter.click(); + await testPage.getByRole("listbox").getByRole("option", { name: "English" }).click(); + await expect(testPage.locator("html")).toHaveAttribute("lang", "en", { timeout: 10_000 }); + await expect(testPage.getByLabel("Display language")).toBeVisible({ timeout: 10_000 }); + }); + test("switching to pseudo re-renders accented copy and survives reload", async ({ testPage, prCapture, @@ -43,7 +78,10 @@ test.describe("i18n language switcher", () => { const select = testPage.getByLabel("Display language"); await expect(select).toBeVisible({ timeout: 10_000 }); await select.click(); - await testPage.getByRole("option", { name: /Pseudo/ }).click(); + await testPage + .getByRole("listbox") + .getByRole("option", { name: /Pseudo/ }) + .click(); // Locale activates client-side: flips and chrome is accented. await expect(testPage.locator("html")).toHaveAttribute("lang", "pseudo", { timeout: 10_000 }); @@ -62,7 +100,10 @@ test.describe("i18n language switcher", () => { // Switch back so the persisted cookie does not leak into later tests. const selectAfter = testPage.getByLabel(/Display language|Ďĩśƥĺàŷ ĺàńĝũàĝē/); await selectAfter.click(); - await testPage.getByRole("option", { name: /English|Ēńĝĺĩśĥ/ }).click(); + await testPage + .getByRole("listbox") + .getByRole("option", { name: /English|Ēńĝĺĩśĥ/ }) + .click(); await expect(testPage.locator("html")).toHaveAttribute("lang", "en", { timeout: 10_000 }); }); }); diff --git a/apps/web/e2e/tests/i18n/mobile-language-switch.spec.ts b/apps/web/e2e/tests/i18n/mobile-language-switch.spec.ts new file mode 100644 index 0000000000..a2672eaa77 --- /dev/null +++ b/apps/web/e2e/tests/i18n/mobile-language-switch.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "../../fixtures/test-base"; + +const APPEARANCE_URL = "/settings/general/appearance"; + +test.describe("mobile i18n language switcher", () => { + test("switches to Chinese, persists through reload, and restores English", async ({ + testPage, + }) => { + await testPage.goto(APPEARANCE_URL); + + const select = testPage.getByLabel("Display language"); + await expect(select).toBeVisible({ timeout: 10_000 }); + await select.tap(); + await testPage.getByRole("listbox").getByRole("option", { name: "简体中文" }).tap(); + + await expect(testPage.locator("html")).toHaveAttribute("lang", "zh-cn", { timeout: 10_000 }); + await expect(testPage.getByLabel("显示语言")).toBeVisible({ timeout: 10_000 }); + await expect + .poll(async () => { + const cookies = await testPage.context().cookies(); + return cookies.find((cookie) => cookie.name === "kandev_locale")?.value; + }) + .toBe("zh-cn"); + + await testPage.reload(); + await expect(testPage.locator("html")).toHaveAttribute("lang", "zh-cn", { timeout: 10_000 }); + await expect(testPage.getByLabel("显示语言")).toBeVisible({ timeout: 10_000 }); + + const selectAfter = testPage.getByLabel("显示语言"); + await selectAfter.tap(); + await testPage.getByRole("listbox").getByRole("option", { name: "English" }).tap(); + await expect(testPage.locator("html")).toHaveAttribute("lang", "en", { timeout: 10_000 }); + await expect(testPage.getByLabel("Display language")).toBeVisible({ timeout: 10_000 }); + }); +}); diff --git a/apps/web/lib/e2e/backend-process.test.ts b/apps/web/lib/e2e/backend-process.test.ts new file mode 100644 index 0000000000..25b5b635a9 --- /dev/null +++ b/apps/web/lib/e2e/backend-process.test.ts @@ -0,0 +1,36 @@ +import type { ChildProcess } from "node:child_process"; +import { describe, expect, test, vi } from "vitest"; +import { killProcessGroup } from "../../e2e/fixtures/backend"; + +function childProcessWithPid(pid: number): ChildProcess { + return { pid } as ChildProcess; +} + +describe("killProcessGroup", () => { + test("uses taskkill for the full process tree on Windows", async () => { + const killWindowsTree = vi.fn((_pid: number, done: (error?: Error) => void) => done()); + + await killProcessGroup(childProcessWithPid(42), "win32", killWindowsTree); + + expect(killWindowsTree).toHaveBeenCalledOnce(); + expect(killWindowsTree).toHaveBeenCalledWith(42, expect.any(Function)); + }); + + test("reports a taskkill failure while the process is still alive", async () => { + const failure = new Error("taskkill failed"); + const killWindowsTree = (_pid: number, done: (error?: Error) => void) => done(failure); + + await expect( + killProcessGroup(childProcessWithPid(42), "win32", killWindowsTree, () => true), + ).rejects.toBe(failure); + }); + + test("accepts a taskkill race when the process has already exited", async () => { + const killWindowsTree = (_pid: number, done: (error?: Error) => void) => + done(new Error("not found")); + + await expect( + killProcessGroup(childProcessWithPid(42), "win32", killWindowsTree, () => false), + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/web/lib/i18n/boot.test.ts b/apps/web/lib/i18n/boot.test.ts index d5b9936545..77c3034de9 100644 --- a/apps/web/lib/i18n/boot.test.ts +++ b/apps/web/lib/i18n/boot.test.ts @@ -18,12 +18,12 @@ describe("resolveInitialLocale", () => { it("prefers the boot payload locale", () => { document.cookie = `${LOCALE_COOKIE}=en; path=/`; - expect(resolveInitialLocale(payloadWithLocale("pseudo"))).toBe("pseudo"); + expect(resolveInitialLocale(payloadWithLocale("zh-cn"))).toBe("zh-cn"); }); it("falls back to the cookie when the payload has no locale", () => { - document.cookie = `${LOCALE_COOKIE}=pseudo; path=/`; - expect(resolveInitialLocale(payloadWithLocale())).toBe("pseudo"); + document.cookie = `${LOCALE_COOKIE}=zh-cn; path=/`; + expect(resolveInitialLocale(payloadWithLocale())).toBe("zh-cn"); }); it("defaults to en when neither payload nor cookie is present", () => { diff --git a/apps/web/lib/i18n/formats.test.ts b/apps/web/lib/i18n/formats.test.ts index 160012246a..7f32741ff9 100644 --- a/apps/web/lib/i18n/formats.test.ts +++ b/apps/web/lib/i18n/formats.test.ts @@ -42,4 +42,28 @@ describe("locale-aware Intl wrappers", () => { expect(formatDate("2026-07-27T00:00:00Z", { year: "numeric" })).toBe("2026"); await activateLocale("en"); }); + + it("passes zh-cn to number and date formatters", async () => { + await activateLocale("zh-cn"); + const numberOptions: Intl.NumberFormatOptions = { + style: "currency", + currency: "CNY", + currencyDisplay: "name", + }; + const dateOptions: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "long", + day: "numeric", + timeZone: "UTC", + }; + const date = "2026-07-27T00:00:00Z"; + + expect(formatNumber(1234.5, numberOptions)).toBe( + new Intl.NumberFormat("zh-cn", numberOptions).format(1234.5), + ); + expect(formatDate(date, dateOptions)).toBe( + new Intl.DateTimeFormat("zh-cn", dateOptions).format(new Date(date)), + ); + await activateLocale("en"); + }); }); diff --git a/apps/web/lib/i18n/index.test.ts b/apps/web/lib/i18n/index.test.ts index 638f53aa0a..59e5738369 100644 --- a/apps/web/lib/i18n/index.test.ts +++ b/apps/web/lib/i18n/index.test.ts @@ -11,6 +11,9 @@ import { } from "./index"; import { LOCALE_COOKIE, readLocaleCookie } from "./cookie"; +const ZH_CN_LOCALE = "zh-cn"; +const DISPLAY_LANGUAGE_KEY = "settings:displayLanguage"; + afterEach(async () => { document.cookie = `${LOCALE_COOKIE}=; path=/; max-age=0`; vi.clearAllMocks(); @@ -21,25 +24,31 @@ afterEach(async () => { describe("locale predicates", () => { it("recognizes supported locales", () => { expect(isSupportedLocale("en")).toBe(true); + expect(isSupportedLocale(ZH_CN_LOCALE)).toBe(true); + expect(isSupportedLocale("zh-CN")).toBe(true); + expect(isSupportedLocale(" ZH-CN ")).toBe(true); expect(isSupportedLocale("pseudo")).toBe(true); expect(isSupportedLocale("fr")).toBe(false); expect(isSupportedLocale(42)).toBe(false); }); it("normalizes unknown values to the default", () => { + expect(normalizeLocale(ZH_CN_LOCALE)).toBe(ZH_CN_LOCALE); + expect(normalizeLocale("zh-CN")).toBe(ZH_CN_LOCALE); + expect(normalizeLocale(" ZH-CN ")).toBe(ZH_CN_LOCALE); expect(normalizeLocale("pseudo")).toBe("pseudo"); expect(normalizeLocale("nope")).toBe(DEFAULT_LOCALE); expect(normalizeLocale(undefined)).toBe(DEFAULT_LOCALE); }); - it("exposes en as the default and lists both locales", () => { + it("exposes en as the default and lists every shipped locale", () => { expect(DEFAULT_LOCALE).toBe("en"); - expect([...SUPPORTED_LOCALES]).toEqual(["en", "pseudo"]); + expect([...SUPPORTED_LOCALES]).toEqual(["en", "zh-cn", "pseudo"]); }); it("hides the pseudo locale in production builds only", () => { - expect(selectableLocales(false)).toEqual(["en", "pseudo"]); - expect(selectableLocales(true)).toEqual(["en"]); + expect(selectableLocales(false)).toEqual(["en", "zh-cn", "pseudo"]); + expect(selectableLocales(true)).toEqual(["en", "zh-cn"]); }); }); @@ -61,11 +70,22 @@ describe("activateLocale", () => { it("resolves real catalog messages for the active locale", async () => { await activateLocale("en"); - expect(i18n.t("settings:displayLanguage")).toBe("Display language"); + expect(i18n.t(DISPLAY_LANGUAGE_KEY)).toBe("Display language"); // The pseudo catalog is generated from `en`, so the same key is accented — // this is the completeness oracle the QA locale exists for. await activateLocale("pseudo"); - expect(i18n.t("settings:displayLanguage")).not.toBe("Display language"); - expect(i18n.t("settings:displayLanguage")).toMatch(/[À-ɏ]/); + expect(i18n.t(DISPLAY_LANGUAGE_KEY)).not.toBe("Display language"); + expect(i18n.t(DISPLAY_LANGUAGE_KEY)).toMatch(/[À-ɏ]/); + }); + + it("activates Simplified Chinese and resolves its real catalog", async () => { + const result = await activateLocale(ZH_CN_LOCALE); + expect(result).toBe(ZH_CN_LOCALE); + expect(i18n.language).toBe(ZH_CN_LOCALE); + expect(document.documentElement.lang).toBe(ZH_CN_LOCALE); + expect(readLocaleCookie()).toBe(ZH_CN_LOCALE); + expect(i18n.hasResourceBundle(ZH_CN_LOCALE, "settings")).toBe(true); + expect(i18n.getResource(ZH_CN_LOCALE, "settings", "displayLanguage")).toBe("显示语言"); + expect(i18n.t(DISPLAY_LANGUAGE_KEY)).toBe("显示语言"); }); }); diff --git a/apps/web/lib/i18n/index.ts b/apps/web/lib/i18n/index.ts index 3b08380f74..7a0de334b2 100644 --- a/apps/web/lib/i18n/index.ts +++ b/apps/web/lib/i18n/index.ts @@ -18,28 +18,42 @@ import { writeLocaleCookie } from "./cookie"; */ export const DEFAULT_LOCALE = "en"; export const DEFAULT_NAMESPACE = "common"; -export const SUPPORTED_LOCALES = ["en", "pseudo"] as const; +export const SUPPORTED_LOCALES = ["en", "zh-cn", "pseudo"] as const; export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number]; /** Human-readable labels for the language switcher. */ export const LOCALE_LABELS: Record = { en: "English", + "zh-cn": "简体中文", pseudo: "Pseudo (QA)", }; -export function isSupportedLocale(value: unknown): value is SupportedLocale { - return typeof value === "string" && (SUPPORTED_LOCALES as readonly string[]).includes(value); -} - -/** Coerce any value to a supported locale, defaulting to `en`. */ -export function normalizeLocale(value: unknown): SupportedLocale { - return isSupportedLocale(value) ? value : DEFAULT_LOCALE; +/** Canonicalize locale input the same way the Go shell does (trim + lower). */ +function canonicalLocale(value: string): string { + return value.trim().toLowerCase(); } /** - * Locales offered in the language switcher. The `pseudo` QA locale is hidden in - * production builds. + * Whether `value` names a shipped locale. Accepts case variants such as + * `zh-CN`; prefer `normalizeLocale` when you need the canonical id to pass on. */ +export function isSupportedLocale(value: unknown): boolean { + if (typeof value !== "string") return false; + return (SUPPORTED_LOCALES as readonly string[]).includes(canonicalLocale(value)); +} + +/** + * Coerce any value to a supported canonical locale, defaulting to `en`. + * Case-insensitive so a hand-edited `zh-CN` cookie matches the backend. + */ +export function normalizeLocale(value: unknown): SupportedLocale { + if (typeof value !== "string") return DEFAULT_LOCALE; + const canonical = canonicalLocale(value); + return (SUPPORTED_LOCALES as readonly string[]).includes(canonical) + ? (canonical as SupportedLocale) + : DEFAULT_LOCALE; +} + /** * Locales offered by the switcher. The pseudo QA locale is hidden in production. * @@ -87,6 +101,7 @@ function ensureInitialized(locale: SupportedLocale) { if (initialized) return; void i18next.use(initReactI18next).init({ lng: locale, + lowerCaseLng: true, fallbackLng: DEFAULT_LOCALE, defaultNS: DEFAULT_NAMESPACE, ns: knownNamespaces(), diff --git a/apps/web/scripts/check-i18n-keys.mjs b/apps/web/scripts/check-i18n-keys.mjs index 5813985408..b31d9f9ca3 100644 --- a/apps/web/scripts/check-i18n-keys.mjs +++ b/apps/web/scripts/check-i18n-keys.mjs @@ -16,18 +16,21 @@ import fs from "node:fs"; import path from "node:path"; +import { + discoverRealLocales, + formatParityIssue, + readLocaleNamespaces, + realLocaleParityIssues, +} from "./lib/i18n-catalogs.mjs"; + const ROOT = path.resolve(import.meta.dirname, ".."); const LOCALES = path.join(ROOT, "src", "locales"); const STRICT_ORPHANS = process.argv.includes("--strict-orphans"); function readCatalog(locale) { - const dir = path.join(LOCALES, locale); const out = new Map(); // "ns:key" -> value - if (!fs.existsSync(dir)) return out; - for (const file of fs.readdirSync(dir).filter((f) => f.endsWith(".json"))) { - const ns = file.replace(/\.json$/, ""); - const entries = JSON.parse(fs.readFileSync(path.join(dir, file), "utf8")); - for (const [key, value] of Object.entries(entries)) out.set(`${ns}:${key}`, value); + for (const [namespace, messages] of readLocaleNamespaces(LOCALES, locale)) { + for (const [key, value] of messages) out.set(`${namespace}:${key}`, value); } return out; } @@ -101,6 +104,11 @@ const enKeys = new Set(en.keys()); const pseudoKeys = new Set(pseudo.keys()); const pseudoMissing = [...enKeys].filter((k) => !pseudoKeys.has(k)); const pseudoExtra = [...pseudoKeys].filter((k) => !enKeys.has(k)); +const sourceNamespaces = readLocaleNamespaces(LOCALES, "en"); +const realLocales = discoverRealLocales(LOCALES); +const realLocaleIssues = realLocales.flatMap((locale) => + realLocaleParityIssues(sourceNamespaces, readLocaleNamespaces(LOCALES, locale), locale), +); let failed = false; @@ -122,6 +130,12 @@ if (pseudoMissing.length || pseudoExtra.length) { ); } +if (realLocaleIssues.length) { + failed = true; + console.error(`\n✖ ${realLocaleIssues.length} real-locale catalog parity issue(s):`); + for (const issue of realLocaleIssues) console.error(` ${formatParityIssue(issue)}`); +} + if (orphans.length) { const label = STRICT_ORPHANS ? "✖" : "⚠"; console[STRICT_ORPHANS ? "error" : "warn"]( @@ -135,7 +149,7 @@ if (orphans.length) { if (!failed) { console.log( `✓ i18n keys OK — ${used.size} key(s) referenced, ${en.size} en entr(ies), ` + - `${orphans.length} orphan(s), pseudo in sync.`, + `${orphans.length} orphan(s), pseudo and ${realLocales.join(", ") || "no real locales"} in sync.`, ); } process.exit(failed ? 1 : 0); diff --git a/apps/web/scripts/check-i18n-keys.test.ts b/apps/web/scripts/check-i18n-keys.test.ts new file mode 100644 index 0000000000..ec0a4b64ec --- /dev/null +++ b/apps/web/scripts/check-i18n-keys.test.ts @@ -0,0 +1,178 @@ +import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +import { describe, expect, it } from "vitest"; + +type Namespace = Record; +type Locale = Record; +type Fixture = Record; + +const ZH_CN = "zh-cn"; + +const SCRIPT = path.resolve(import.meta.dirname, "check-i18n-keys.mjs"); +const CATALOG_HELPER = path.resolve(import.meta.dirname, "lib", "i18n-catalogs.mjs"); + +function runFixture(fixture: Fixture) { + const root = mkdtempSync(path.join(tmpdir(), "kandev-i18n-keys-")); + try { + const scriptsDir = path.join(root, "scripts"); + mkdirSync(scriptsDir, { recursive: true }); + const script = path.join(scriptsDir, "check-i18n-keys.mjs"); + copyFileSync(SCRIPT, script); + const libDir = path.join(scriptsDir, "lib"); + mkdirSync(libDir, { recursive: true }); + copyFileSync(CATALOG_HELPER, path.join(libDir, "i18n-catalogs.mjs")); + + for (const [locale, namespaces] of Object.entries(fixture)) { + const localeDir = path.join(root, "src", "locales", locale); + mkdirSync(localeDir, { recursive: true }); + for (const [namespace, messages] of Object.entries(namespaces)) { + writeFileSync(path.join(localeDir, `${namespace}.json`), JSON.stringify(messages)); + } + } + + return spawnSync(process.execPath, [script], { encoding: "utf8" }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +const completeFixture = (): Fixture => ({ + en: { common: { first: "First", second: "Second" }, settings: { third: "Third" } }, + pseudo: { common: { first: "Ƒîřśţ", second: "Śēćøńđ" }, settings: { third: "Ţĥîřđ" } }, + [ZH_CN]: { common: { first: "第一", second: "第二" }, settings: { third: "第三" } }, +}); + +describe("real locale catalog parity", () => { + it("accepts a complete real locale", () => { + const result = runFixture(completeFixture()); + expect(result.status).toBe(0); + }); + + it("accepts an identical translated value for a technical literal", () => { + const fixture = completeFixture(); + fixture.en.common.first = "Kandev"; + fixture[ZH_CN].common.first = "Kandev"; + const result = runFixture(fixture); + + expect(result.status).toBe(0); + }); + + it("rejects a missing namespace", () => { + const fixture = completeFixture(); + delete fixture[ZH_CN].settings; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("settings"); + expect(result.stderr).toContain("missing namespace"); + }); + + it("rejects an extra namespace", () => { + const fixture = completeFixture(); + fixture[ZH_CN].extra = { surprise: "意外" }; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("extra"); + expect(result.stderr).toContain("extra namespace"); + }); + + it("rejects a missing key", () => { + const fixture = completeFixture(); + delete fixture[ZH_CN].common.second; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("missing key: second"); + }); + + it("rejects an extra key", () => { + const fixture = completeFixture(); + fixture[ZH_CN].common.surprise = "意外"; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("extra key: surprise"); + }); + + it("rejects a translation that drops an interpolation placeholder", () => { + const fixture = completeFixture(); + fixture.en.common.first = "Open {{url}} as {{user}}"; + fixture[ZH_CN].common.first = "以 {{user}} 身份打开"; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("interpolation placeholder mismatch: first"); + }); + + it("rejects a translation that drops numeric Trans tags", () => { + const fixture = completeFixture(); + fixture.en.common.first = "Open <1>{{url}}"; + fixture[ZH_CN].common.first = "打开 {{url}}"; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("Trans tag structure mismatch: first"); + }); + + it("rejects a translation that changes a numeric Trans tag index", () => { + const fixture = completeFixture(); + fixture.en.common.first = "Open <1>{{url}}"; + fixture[ZH_CN].common.first = "打开 <2>{{url}}"; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("Trans tag structure mismatch: first"); + }); +}); + +describe("translated value validation", () => { + it("rejects an empty translated value", () => { + const fixture = completeFixture(); + fixture[ZH_CN].common.first = ""; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("empty translated value: first"); + }); + + it("rejects a whitespace-only translated value", () => { + const fixture = completeFixture(); + fixture[ZH_CN].common.first = " \t\n "; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("empty translated value: first"); + }); + + it("rejects a non-string translated value", () => { + const fixture = completeFixture(); + fixture[ZH_CN].common.first = 42; + const result = runFixture(fixture); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(ZH_CN); + expect(result.stderr).toContain("common"); + expect(result.stderr).toContain("non-string translated value: first"); + expect(result.stderr).not.toContain("matchAll is not a function"); + }); +}); diff --git a/apps/web/scripts/lib/i18n-catalogs.mjs b/apps/web/scripts/lib/i18n-catalogs.mjs new file mode 100644 index 0000000000..5b59b6de97 --- /dev/null +++ b/apps/web/scripts/lib/i18n-catalogs.mjs @@ -0,0 +1,151 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function readLocaleNamespaces(localesDir, locale) { + const dir = path.join(localesDir, locale); + const namespaces = new Map(); + if (!fs.existsSync(dir)) return namespaces; + for (const file of fs + .readdirSync(dir) + .filter((entry) => entry.endsWith(".json")) + .sort()) { + const namespace = file.replace(/\.json$/, ""); + const messages = JSON.parse(fs.readFileSync(path.join(dir, file), "utf8")); + namespaces.set(namespace, new Map(Object.entries(messages))); + } + return namespaces; +} + +export function discoverRealLocales(localesDir) { + if (!fs.existsSync(localesDir)) return []; + return fs + .readdirSync(localesDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !["en", "pseudo"].includes(entry.name)) + .map((entry) => entry.name) + .sort(); +} + +function interpolationPlaceholders(message) { + return [ + ...new Set([...message.matchAll(/\{\{\s*-?\s*([^},\s]+)[^}]*\}\}/g)].map((match) => match[1])), + ].sort(); +} + +function numericTagStructure(message) { + const stack = []; + const nodes = []; + for (const match of message.matchAll(/<(\/)?(\d+)>/g)) { + const closing = Boolean(match[1]); + const tag = match[2]; + if (closing) { + if (stack.at(-1) !== tag) return null; + stack.pop(); + continue; + } + nodes.push([...stack, tag].join("/")); + stack.push(tag); + } + return stack.length === 0 ? nodes.sort() : null; +} + +function sameItems(left, right) { + return left !== null && right !== null && JSON.stringify(left) === JSON.stringify(right); +} + +function translatedValueIssue(value, context) { + if (typeof value !== "string") { + return { ...context, type: "non-string translated value" }; + } + if (value.trim().length === 0) { + return { ...context, type: "empty translated value" }; + } + return null; +} + +function messageParityIssues(sourceMessage, translatedMessage, context) { + const issues = []; + if ( + !sameItems( + interpolationPlaceholders(sourceMessage), + interpolationPlaceholders(translatedMessage), + ) + ) { + issues.push({ ...context, type: "interpolation placeholder mismatch" }); + } + if (!sameItems(numericTagStructure(sourceMessage), numericTagStructure(translatedMessage))) { + issues.push({ ...context, type: "Trans tag structure mismatch" }); + } + return issues; +} + +function translatedValueIssues(translated, locale) { + const issues = []; + for (const [namespace, translatedMessages] of translated) { + for (const [key, value] of translatedMessages) { + const valueIssue = translatedValueIssue(value, { locale, namespace, key }); + if (valueIssue) issues.push(valueIssue); + } + } + return issues; +} + +function namespaceParityIssues(source, translated, locale) { + const issues = []; + for (const namespace of source.keys()) { + if (!translated.has(namespace)) { + issues.push({ locale, namespace, type: "missing namespace" }); + continue; + } + const sourceMessages = source.get(namespace); + const translatedMessages = translated.get(namespace); + issues.push( + ...messageParityIssuesForNamespace(sourceMessages, translatedMessages, locale, namespace), + ); + } + return issues; +} + +function messageParityIssuesForNamespace(sourceMessages, translatedMessages, locale, namespace) { + const issues = []; + for (const key of sourceMessages.keys()) { + if (!translatedMessages.has(key)) { + issues.push({ locale, namespace, type: "missing key", key }); + continue; + } + const sourceMessage = sourceMessages.get(key); + const translatedMessage = translatedMessages.get(key); + if (typeof sourceMessage !== "string" || typeof translatedMessage !== "string") continue; + issues.push( + ...messageParityIssues(sourceMessage, translatedMessage, { locale, namespace, key }), + ); + } + for (const key of translatedMessages.keys()) { + if (!sourceMessages.has(key)) { + issues.push({ locale, namespace, type: "extra key", key }); + } + } + return issues; +} + +function extraNamespaceIssues(source, translated, locale) { + const issues = []; + for (const namespace of translated.keys()) { + if (!source.has(namespace)) { + issues.push({ locale, namespace, type: "extra namespace" }); + } + } + return issues; +} + +export function realLocaleParityIssues(source, translated, locale) { + return [ + ...translatedValueIssues(translated, locale), + ...namespaceParityIssues(source, translated, locale), + ...extraNamespaceIssues(source, translated, locale), + ]; +} + +export function formatParityIssue(issue) { + const suffix = issue.key ? `: ${issue.key}` : ""; + return `${issue.locale} / ${issue.namespace}: ${issue.type}${suffix}`; +} diff --git a/apps/web/src/locales/en/chat.json b/apps/web/src/locales/en/chat.json index 2cd33adc6e..fddb68a3a4 100644 --- a/apps/web/src/locales/en/chat.json +++ b/apps/web/src/locales/en/chat.json @@ -35,6 +35,6 @@ "providerQuotaReset": "Capacity resets {{resetAt}}.", "providerQuotaResetUnknown": "Try again when the provider makes capacity available.", "providerQuotaProviderFallback": "Provider", - "turnGroupToolCalls_one": "tool call", - "turnGroupToolCalls_other": "tool calls" + "turnGroupToolCalls_one": "{{count}} tool call", + "turnGroupToolCalls_other": "{{count}} tool calls" } diff --git a/apps/web/src/locales/en/executors.json b/apps/web/src/locales/en/executors.json index 450c18c448..a839db8e71 100644 --- a/apps/web/src/locales/en/executors.json +++ b/apps/web/src/locales/en/executors.json @@ -56,8 +56,8 @@ "dockerfile": "Dockerfile", "dockerfileContent": "Dockerfile Content", "domain": "Domain", - "eGMyApiToken": "e.g. my-api-token", - "eGProductionDevelopment": "e.g. Production, Development", + "eGMyApiToken": "e.g. {{example}}", + "eGProductionDevelopment": "e.g. {{example}}", "enabled": "Enabled", "envVarsConfiguredCount_one": "{{count}} configured", "envVarsConfiguredCount_other": "{{count}} configured", @@ -155,7 +155,7 @@ "success": "Success", "task": "Task", "testConnection": "Test Connection", - "theRemoteDockerHostUrlTcp": "The remote Docker host URL (tcp://, ssh://).", + "theRemoteDockerHostUrlTcp": "The remote Docker host URL ({{schemes}}).", "thisActionCannotBeUndone": "This action cannot be undone.", "tlsCertificatePath": "TLS certificate path", "tlsVerify": "TLS verify", diff --git a/apps/web/src/locales/en/github.json b/apps/web/src/locales/en/github.json index 21fac589d9..b9126c5131 100644 --- a/apps/web/src/locales/en/github.json +++ b/apps/web/src/locales/en/github.json @@ -86,7 +86,7 @@ "deletedTasks_other": "Deleted {{count}} tasks", "disconnect": "Disconnect", "disconnectFailed": "Disconnect failed", - "eGBugEnhancementPriorityHigh": "e.g. bug, enhancement, priority:high", + "eGBugEnhancementPriorityHigh": "e.g. {{examples}}", "editDetails": "Edit details", "editIssueWatch": "Edit Issue Watch", "editPrompt": "Edit prompt", @@ -252,7 +252,7 @@ "placeholderPrNumber": "PR number", "placeholderPrTitle": "PR title", "placeholderPrUrl": "PR URL", - "placeholderRepository": "Repository (owner/name)", + "placeholderRepository": "Repository", "placeholderSourceBranch": "Source branch", "placeholderTargetBranch": "Target branch", "pollIntervalSeconds": "Poll Interval (seconds)", diff --git a/apps/web/src/locales/en/settings.json b/apps/web/src/locales/en/settings.json index e74b09d7a2..da2c8aca06 100644 --- a/apps/web/src/locales/en/settings.json +++ b/apps/web/src/locales/en/settings.json @@ -50,7 +50,7 @@ "configureArchiveSafeguardsAndDefaultsFor": "Configure archive safeguards and defaults for tasks created by agents", "configureBackendAndExecutionResourceSampling": "Configure backend and execution resource sampling", "configureChatInputBehavior": "Configure chat input behavior", - "configureSpritesApiToken": "Configure a <1>SPRITES_API_TOKEN environment variable in the executor profile, referencing a secret with your Sprites.dev API token.", + "configureSpritesApiToken": "Configure a <1>{{envVar}} environment variable in the executor profile, referencing a secret with your Sprites.dev API token.", "configureTerminalAppearanceAndBehavior": "Configure terminal appearance and behavior", "configureTheIncludedCodeEditorAnd": "Configure the included code editor and external editors", "configureTheInitialDesktopTaskWorkbench": "Configure the initial desktop task workbench.", @@ -526,7 +526,7 @@ "voiceEnableLabel": "Show the mic button on the chat composer", "voiceEnableDescription": "When off, the voice button is hidden entirely and no voice-related code runs. Settings below are preserved and re-applied when you turn it back on.", "voiceUnavailableTitle": "Voice input is unavailable in this browser.", - "voiceUnavailableInsecure": "Microphone APIs require HTTPS or localhost. You appear to be on an insecure HTTP origin — load this page over HTTPS (or http://localhost) to enable voice input.", + "voiceUnavailableInsecure": "Microphone APIs require {{secureScheme}} or {{localhostHost}}. You appear to be on an insecure {{insecureScheme}} origin — load this page over {{secureScheme}} (or {{localhostUrl}}) to enable voice input.", "voiceUnavailableUnsupported": "Your browser doesn't expose either the Web Speech API or MediaRecorder. Try Chrome, Edge, or Safari 14.5+.", "voiceEngineTitle": "Transcription Engine", "voiceEngineUnsupported": "Not supported in this browser.", diff --git a/apps/web/src/locales/en/workflows.json b/apps/web/src/locales/en/workflows.json index 6ddfc2346f..d5413eea2c 100644 --- a/apps/web/src/locales/en/workflows.json +++ b/apps/web/src/locales/en/workflows.json @@ -188,7 +188,7 @@ "stepPromptUsageHint": "Type {{open}} for placeholders ({{taskPrompt}} inserts the task description) or @ to reference a saved prompt by name — its content is attached as hidden context, and editing the saved prompt updates every step that references it. Note: {{taskPrompt}} only expands in the step prompt itself, not inside a referenced saved prompt.", "syncCompletedWithWarnings": "Sync completed with warnings", "syncConfigSaved": "Workflow sync configuration saved", - "syncDirectoryHelp": "The directory should contain workflow export files (.yml/.yaml/.json) in the kandev_workflow format — the same format produced by workflow export.", + "syncDirectoryHelp": "The directory should contain workflow export files ({{extensions}}) in the {{format}} format — the same format produced by workflow export.", "synced": "Synced", "syncedBadgeTooltip": "Read-only - managed by workflow sync from {{source}}. Edit or remove it in the synced repository.", "syncedReadOnlyReason": "Managed by workflow sync — edit or remove it in the synced repository", diff --git a/apps/web/src/locales/pseudo/chat.json b/apps/web/src/locales/pseudo/chat.json index 8c8a582b74..eca752f599 100644 --- a/apps/web/src/locales/pseudo/chat.json +++ b/apps/web/src/locales/pseudo/chat.json @@ -35,6 +35,6 @@ "providerQuotaReset": "Ćàƥàćĩţŷ ŕēśēţś {{resetAt}}.", "providerQuotaResetUnknown": "Ţŕŷ àĝàĩń ŵĥēń ţĥē ƥŕōvĩďēŕ ḿàķēś ćàƥàćĩţŷ àvàĩĺàƀĺē.", "providerQuotaProviderFallback": "Ƥŕōvĩďēŕ", - "turnGroupToolCalls_one": "ţōōĺ ćàĺĺ", - "turnGroupToolCalls_other": "ţōōĺ ćàĺĺś" + "turnGroupToolCalls_one": "{{count}} ţōōĺ ćàĺĺ", + "turnGroupToolCalls_other": "{{count}} ţōōĺ ćàĺĺś" } diff --git a/apps/web/src/locales/pseudo/executors.json b/apps/web/src/locales/pseudo/executors.json index ac362edfda..127f7df2e9 100644 --- a/apps/web/src/locales/pseudo/executors.json +++ b/apps/web/src/locales/pseudo/executors.json @@ -56,8 +56,8 @@ "dockerfile": "Ďōćķēŕƒĩĺē", "dockerfileContent": "Ďōćķēŕƒĩĺē Ćōńţēńţ", "domain": "Ďōḿàĩń", - "eGMyApiToken": "ē.ĝ. ḿŷ-àƥĩ-ţōķēń", - "eGProductionDevelopment": "ē.ĝ. Ƥŕōďũćţĩōń, Ďēvēĺōƥḿēńţ", + "eGMyApiToken": "ē.ĝ. {{example}}", + "eGProductionDevelopment": "ē.ĝ. {{example}}", "enabled": "Ēńàƀĺēď", "envVarsConfiguredCount_one": "{{count}} ćōńƒĩĝũŕēď", "envVarsConfiguredCount_other": "{{count}} ćōńƒĩĝũŕēď", @@ -155,7 +155,7 @@ "success": "Śũććēśś", "task": "Ţàśķ", "testConnection": "Ţēśţ Ćōńńēćţĩōń", - "theRemoteDockerHostUrlTcp": "Ţĥē ŕēḿōţē Ďōćķēŕ ĥōśţ ŨŔĹ (ţćƥ://, śśĥ://).", + "theRemoteDockerHostUrlTcp": "Ţĥē ŕēḿōţē Ďōćķēŕ ĥōśţ ŨŔĹ ({{schemes}}).", "thisActionCannotBeUndone": "Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.", "tlsCertificatePath": "ŢĹŚ ćēŕţĩƒĩćàţē ƥàţĥ", "tlsVerify": "ŢĹŚ vēŕĩƒŷ", diff --git a/apps/web/src/locales/pseudo/github.json b/apps/web/src/locales/pseudo/github.json index 89bcb7ddb4..cc6fae8643 100644 --- a/apps/web/src/locales/pseudo/github.json +++ b/apps/web/src/locales/pseudo/github.json @@ -86,7 +86,7 @@ "deletedTasks_other": "Ďēĺēţēď {{count}} ţàśķś", "disconnect": "Ďĩśćōńńēćţ", "disconnectFailed": "Ďĩśćōńńēćţ ƒàĩĺēď", - "eGBugEnhancementPriorityHigh": "ē.ĝ. ƀũĝ, ēńĥàńćēḿēńţ, ƥŕĩōŕĩţŷ:ĥĩĝĥ", + "eGBugEnhancementPriorityHigh": "ē.ĝ. {{examples}}", "editDetails": "Ēďĩţ ďēţàĩĺś", "editIssueWatch": "Ēďĩţ Ĩśśũē Ŵàţćĥ", "editPrompt": "Ēďĩţ ƥŕōḿƥţ", @@ -252,7 +252,7 @@ "placeholderPrNumber": "ƤŔ ńũḿƀēŕ", "placeholderPrTitle": "ƤŔ ţĩţĺē", "placeholderPrUrl": "ƤŔ ŨŔĹ", - "placeholderRepository": "Ŕēƥōśĩţōŕŷ (ōŵńēŕ/ńàḿē)", + "placeholderRepository": "Ŕēƥōśĩţōŕŷ", "placeholderSourceBranch": "Śōũŕćē ƀŕàńćĥ", "placeholderTargetBranch": "Ţàŕĝēţ ƀŕàńćĥ", "pollIntervalSeconds": "Ƥōĺĺ Ĩńţēŕvàĺ (śēćōńďś)", diff --git a/apps/web/src/locales/pseudo/settings.json b/apps/web/src/locales/pseudo/settings.json index 9e180e7481..c5aff3fbc0 100644 --- a/apps/web/src/locales/pseudo/settings.json +++ b/apps/web/src/locales/pseudo/settings.json @@ -50,7 +50,7 @@ "configureArchiveSafeguardsAndDefaultsFor": "Ćōńƒĩĝũŕē àŕćĥĩvē śàƒēĝũàŕďś àńď ďēƒàũĺţś ƒōŕ ţàśķś ćŕēàţēď ƀŷ àĝēńţś", "configureBackendAndExecutionResourceSampling": "Ćōńƒĩĝũŕē ƀàćķēńď àńď ēxēćũţĩōń ŕēśōũŕćē śàḿƥĺĩńĝ", "configureChatInputBehavior": "Ćōńƒĩĝũŕē ćĥàţ ĩńƥũţ ƀēĥàvĩōŕ", - "configureSpritesApiToken": "Ćōńƒĩĝũŕē à <1>ŚƤŔĨŢĒŚ_ÀƤĨ_ŢŌĶĒŃ ēńvĩŕōńḿēńţ vàŕĩàƀĺē ĩń ţĥē ēxēćũţōŕ ƥŕōƒĩĺē, ŕēƒēŕēńćĩńĝ à śēćŕēţ ŵĩţĥ ŷōũŕ Śƥŕĩţēś.ďēv ÀƤĨ ţōķēń.", + "configureSpritesApiToken": "Ćōńƒĩĝũŕē à <1>{{envVar}} ēńvĩŕōńḿēńţ vàŕĩàƀĺē ĩń ţĥē ēxēćũţōŕ ƥŕōƒĩĺē, ŕēƒēŕēńćĩńĝ à śēćŕēţ ŵĩţĥ ŷōũŕ Śƥŕĩţēś.ďēv ÀƤĨ ţōķēń.", "configureTerminalAppearanceAndBehavior": "Ćōńƒĩĝũŕē ţēŕḿĩńàĺ àƥƥēàŕàńćē àńď ƀēĥàvĩōŕ", "configureTheIncludedCodeEditorAnd": "Ćōńƒĩĝũŕē ţĥē ĩńćĺũďēď ćōďē ēďĩţōŕ àńď ēxţēŕńàĺ ēďĩţōŕś", "configureTheInitialDesktopTaskWorkbench": "Ćōńƒĩĝũŕē ţĥē ĩńĩţĩàĺ ďēśķţōƥ ţàśķ ŵōŕķƀēńćĥ.", @@ -526,7 +526,7 @@ "voiceEnableLabel": "Śĥōŵ ţĥē ḿĩć ƀũţţōń ōń ţĥē ćĥàţ ćōḿƥōśēŕ", "voiceEnableDescription": "Ŵĥēń ōƒƒ, ţĥē vōĩćē ƀũţţōń ĩś ĥĩďďēń ēńţĩŕēĺŷ àńď ńō vōĩćē-ŕēĺàţēď ćōďē ŕũńś. Śēţţĩńĝś ƀēĺōŵ àŕē ƥŕēśēŕvēď àńď ŕē-àƥƥĺĩēď ŵĥēń ŷōũ ţũŕń ĩţ ƀàćķ ōń.", "voiceUnavailableTitle": "Vōĩćē ĩńƥũţ ĩś ũńàvàĩĺàƀĺē ĩń ţĥĩś ƀŕōŵśēŕ.", - "voiceUnavailableInsecure": "Ḿĩćŕōƥĥōńē ÀƤĨś ŕēqũĩŕē ĤŢŢƤŚ ōŕ ĺōćàĺĥōśţ. Ŷōũ àƥƥēàŕ ţō ƀē ōń àń ĩńśēćũŕē ĤŢŢƤ ōŕĩĝĩń — ĺōàď ţĥĩś ƥàĝē ōvēŕ ĤŢŢƤŚ (ōŕ ĥţţƥ://ĺōćàĺĥōśţ) ţō ēńàƀĺē vōĩćē ĩńƥũţ.", + "voiceUnavailableInsecure": "Ḿĩćŕōƥĥōńē ÀƤĨś ŕēqũĩŕē {{secureScheme}} ōŕ {{localhostHost}}. Ŷōũ àƥƥēàŕ ţō ƀē ōń àń ĩńśēćũŕē {{insecureScheme}} ōŕĩĝĩń — ĺōàď ţĥĩś ƥàĝē ōvēŕ {{secureScheme}} (ōŕ {{localhostUrl}}) ţō ēńàƀĺē vōĩćē ĩńƥũţ.", "voiceUnavailableUnsupported": "Ŷōũŕ ƀŕōŵśēŕ ďōēśń'ţ ēxƥōśē ēĩţĥēŕ ţĥē Ŵēƀ Śƥēēćĥ ÀƤĨ ōŕ ḾēďĩàŔēćōŕďēŕ. Ţŕŷ Ćĥŕōḿē, Ēďĝē, ōŕ Śàƒàŕĩ 14.5+.", "voiceEngineTitle": "Ţŕàńśćŕĩƥţĩōń Ēńĝĩńē", "voiceEngineUnsupported": "Ńōţ śũƥƥōŕţēď ĩń ţĥĩś ƀŕōŵśēŕ.", diff --git a/apps/web/src/locales/pseudo/workflows.json b/apps/web/src/locales/pseudo/workflows.json index 86ebd152de..a1a0a12834 100644 --- a/apps/web/src/locales/pseudo/workflows.json +++ b/apps/web/src/locales/pseudo/workflows.json @@ -188,7 +188,7 @@ "stepPromptUsageHint": "Ţŷƥē {{open}} ƒōŕ ƥĺàćēĥōĺďēŕś ({{taskPrompt}} ĩńśēŕţś ţĥē ţàśķ ďēśćŕĩƥţĩōń) ōŕ @ ţō ŕēƒēŕēńćē à śàvēď ƥŕōḿƥţ ƀŷ ńàḿē — ĩţś ćōńţēńţ ĩś àţţàćĥēď àś ĥĩďďēń ćōńţēxţ, àńď ēďĩţĩńĝ ţĥē śàvēď ƥŕōḿƥţ ũƥďàţēś ēvēŕŷ śţēƥ ţĥàţ ŕēƒēŕēńćēś ĩţ. Ńōţē: {{taskPrompt}} ōńĺŷ ēxƥàńďś ĩń ţĥē śţēƥ ƥŕōḿƥţ ĩţśēĺƒ, ńōţ ĩńśĩďē à ŕēƒēŕēńćēď śàvēď ƥŕōḿƥţ.", "syncCompletedWithWarnings": "Śŷńć ćōḿƥĺēţēď ŵĩţĥ ŵàŕńĩńĝś", "syncConfigSaved": "Ŵōŕķƒĺōŵ śŷńć ćōńƒĩĝũŕàţĩōń śàvēď", - "syncDirectoryHelp": "Ţĥē ďĩŕēćţōŕŷ śĥōũĺď ćōńţàĩń ŵōŕķƒĺōŵ ēxƥōŕţ ƒĩĺēś (.ŷḿĺ/.ŷàḿĺ/.ĵśōń) ĩń ţĥē ķàńďēv_ŵōŕķƒĺōŵ ƒōŕḿàţ — ţĥē śàḿē ƒōŕḿàţ ƥŕōďũćēď ƀŷ ŵōŕķƒĺōŵ ēxƥōŕţ.", + "syncDirectoryHelp": "Ţĥē ďĩŕēćţōŕŷ śĥōũĺď ćōńţàĩń ŵōŕķƒĺōŵ ēxƥōŕţ ƒĩĺēś ({{extensions}}) ĩń ţĥē {{format}} ƒōŕḿàţ — ţĥē śàḿē ƒōŕḿàţ ƥŕōďũćēď ƀŷ ŵōŕķƒĺōŵ ēxƥōŕţ.", "synced": "Śŷńćēď", "syncedBadgeTooltip": "Ŕēàď-ōńĺŷ - ḿàńàĝēď ƀŷ ŵōŕķƒĺōŵ śŷńć ƒŕōḿ {{source}}. Ēďĩţ ōŕ ŕēḿōvē ĩţ ĩń ţĥē śŷńćēď ŕēƥōśĩţōŕŷ.", "syncedReadOnlyReason": "Ḿàńàĝēď ƀŷ ŵōŕķƒĺōŵ śŷńć — ēďĩţ ōŕ ŕēḿōvē ĩţ ĩń ţĥē śŷńćēď ŕēƥōśĩţōŕŷ", diff --git a/apps/web/src/locales/zh-cn/agents.json b/apps/web/src/locales/zh-cn/agents.json new file mode 100644 index 0000000000..12f0cc5a0f --- /dev/null +++ b/apps/web/src/locales/zh-cn/agents.json @@ -0,0 +1,198 @@ +{ + "actionCannotBeUndone": "此操作无法撤销。", + "add": "添加", + "addProfile": "添加配置", + "addTuiAgent": "添加 TUI 代理", + "addTuiAgentDescription": "将 CLI 工具注册为 TUI 代理。它会以默认透传配置出现在代理列表中。", + "agentCliFlags": "代理 CLI 标志", + "agentCliFlagsDescription": "启动时代理 CLI 接收的标志。仅应用已启用的项。", + "agentInstallAlreadyInProgress": "代理安装已在进行中。", + "agentInstallationInProgress": "正在安装代理", + "agentNotFound": "未找到代理。", + "agentProfiles": "代理配置", + "agentProfilesDescription": "按代理管理现有配置。", + "agentProfileSettings": "{{name}} 配置设置", + "agentProfilesTitle": "{{name}} 配置", + "approveUpdate": "批准更新", + "authenticationRequired": "需要身份验证", + "authRequiredOpenLoginTerminal": "需要身份验证 - 点击打开登录终端", + "authRequiredOpenShell": "需要身份验证 - 点击打开 shell 并登录", + "automaticallyAvailable": "自动可用", + "availableCommandsCount_one": "可用命令({{count}})", + "availableCommandsCount_other": "可用命令({{count}})", + "availableSlashCommands": "可用斜杠命令", + "availableSlashCommandsHelp": "在会话聊天中输入即可调用 - 例如 <0>{{example}}。", + "availableToInstall": "可安装", + "availableToInstallDescription": "点击「安装」在 kandev 主机上运行该代理的安装脚本。进度会实时推送;可并行启动多个安装。", + "backToAgents": "返回代理", + "backToModelSettings": "从 {{name}} 返回模型设置", + "builtIn": "内置", + "cannotDeleteAgentProfile": "无法删除代理配置", + "capabilityAuthRequired": "需要身份验证。请在终端运行代理 CLI 完成认证,然后刷新。", + "capabilityNotInstalled": "未安装代理 CLI。", + "capabilityProbeFailed": "探测失败。请查看代理日志了解详情。", + "capabilityProbing": "正在检查代理能力…", + "changeTierMappingsFirst": "删除此配置前,请先更改这些工作区层级映射。", + "checkingAuthentication": "正在检查身份验证", + "checkingCapabilitiesAndAuth": "正在检查代理能力与身份验证…", + "checkingLatestRuntimeVersion": "正在检查最新运行时版本…", + "cliFlagsEnabledCount_one": "已启用 {{enabled}} / {{count}}", + "cliFlagsEnabledCount_other": "已启用 {{enabled}} / {{count}}", + "command": "命令", + "commandPrefix": "命令前缀", + "commandPrefixHelp": "附加在代理启动命令前的 token,使其在沙箱启动器下运行(例如 <0>{{example}})。值会按 shell 分词。留空则直接运行代理。仅适用于 ACP 会话 — 当配置使用 TUI 透传时无效。", + "commandPreview": "命令预览", + "commandPreviewHelp": "根据当前设置将执行的 CLI 命令。", + "commandRequired": "命令为必填项", + "commandThatWillRun": "将要运行的命令", + "configured": "已配置", + "configureProfilesIntro": "为此代理配置配置文件与默认值。", + "conflictQuickChatsTitle": "快速聊天:", + "conflictTasksTitle": "任务:", + "conflictTierMappingRow": "{{workspace}} 中 {{provider}} 的 <0>{{tier}} 层级", + "conflictTierMappingsTitle": "工作区层级映射:", + "conflictWatchersTitle": "监控(将被禁用):", + "copied": "已复制", + "copyInstallCommand": "复制安装命令", + "copyToClipboard": "复制到剪贴板", + "create": "创建", + "createAgentProfileTitle": "创建 {{name}} 配置", + "createNewProfile": "创建新配置", + "createProfileIntro": "为此代理创建新配置。", + "creating": "正在创建…", + "defaultProfile": "默认配置", + "defaultSuffix": "(默认)", + "delete": "删除", + "deleteAgent": "删除代理", + "deleteAgentDescription": "这将永久移除该代理及其所有配置。此操作无法撤销。", + "deleteAgentProfileDescription": "这将永久删除此配置。此操作无法撤销。", + "deleteAgentProfileTitle": "删除代理配置?", + "deleteAgentTitle": "删除 {{name}}?", + "deleteAnyway": "仍然删除", + "deleteAnywayConsequences": "这些会话将无法再使用此配置,所列监控将被禁用。此操作无法撤销。", + "deleteProfile": "删除配置", + "details": "详情", + "detectedAt": "检测路径:{{path}}", + "disableSetting": "禁用 {{name}}", + "displayName": "显示名称", + "displayNameRequired": "显示名称为必填项", + "done": "完成", + "enableMcp": "启用 MCP", + "enableMcpDescription": "允许此配置在会话中使用 MCP 服务器。", + "enableSetting": "启用 {{name}}", + "everyProfileNeedsNameAndModel": "每个配置都需要名称和模型。", + "exampleValue": "例如 {{example}}", + "failedToCreateAgent": "创建代理失败", + "failedToDeleteProfile": "删除配置失败", + "failedToFetchCapabilities": "获取能力失败", + "failedToLoadCommandPreview": "加载命令预览失败", + "failedToLoadMcpConfig": "加载 MCP 配置失败", + "failedToRefresh": "刷新失败:{{error}}", + "failedToSaveAgent": "保存代理失败", + "failedToSaveMcpConfig": "保存 MCP 配置失败", + "failedToSaveProfile": "保存配置失败", + "filterModels": "筛选模型…", + "fixInvalidMcpConfig": "请先修复无效的 MCP 配置再保存。", + "fixInvalidMcpJson": "请先修复无效的 MCP JSON 再保存。", + "flag": "标志", + "goToAgents": "前往代理", + "hostTerminal": "主机终端", + "hostTerminalDescription": "在 kandev 主机上运行 shell。可用于手动安装或配置代理。", + "install": "安装", + "installationNotDetected": "未检测到安装", + "installed": "已安装", + "installedAgents": "已安装的代理", + "installedAgentsDescription": "在本机检测到的代理已可配置。", + "installing": "正在安装…", + "installQueued": "已排队…", + "invalidJson": "无效的 JSON", + "invalidMcpConfig": "无效的 MCP 配置", + "kandevMcpChip": "✓ {{name}}", + "kandevMcpTools": "工具:{{tools}}", + "manageAgentsFromMainPage": "请从代理主页管理代理。", + "mcpConfigMustBeJsonObject": "MCP 服务器配置必须是 JSON 对象", + "mcpConfiguration": "MCP 配置", + "mcpDraftHint": "配置保存后将应用 MCP 设置。", + "mcpKeyMustBeJsonObject": "{{key}} 必须是 JSON 对象", + "mcpPassthroughInjectionHint": "在 CLI 透传模式下,kandev 通过 {{mechanism}} 注入这些 MCP 服务器。", + "mcpSaveProfileHint": "保存此配置以配置 MCP 服务器。", + "mcpServersHelp": "MCP 定义存储在数据库中,并在运行时按执行器解析。这不会覆盖你的本地代理配置。", + "mcpServersJson": "MCP 服务器(JSON)", + "model": "模型", + "modelHeading": "模型", + "modelRequiredForAllProfiles": "所有配置都需要模型。", + "noAuthHintLogin": "点击「打开终端」登录,然后刷新。", + "noAuthHintNotInstalled": "安装代理 CLI,然后点击「刷新」。", + "noAuthHintShell": "点击「打开终端」启动 shell,运行代理的登录命令,然后刷新。", + "noAuthLoginRequired": "未认证 - 需要登录", + "noCliFlagsConfigured": "未配置 CLI 标志。请在下方添加。", + "noCommandPreview": "无可用命令预览。", + "noInstalledAgentsDetected": "未检测到已安装的代理。请在下方安装,然后点击「重新扫描」。", + "noModeFound": "未找到模式。", + "noModelsFound": "未找到模型。", + "notInstalled": "未安装", + "openTerminal": "打开终端", + "pageDescription": "发现已安装的代理、安装新代理并管理其配置。", + "popularServers": "常用服务器", + "probingCapabilities": "正在探测代理能力… 通常需要几秒钟。", + "profileInUseIntro": "此配置当前正在使用。删除后将影响以下内容:", + "profileName": "配置名称", + "profileNameRequired": "配置名称为必填项。", + "profileNameRequiredDescription": "保存前请输入配置名称。", + "profileNameRequiredTitle": "配置名称为必填项", + "profileNotFound": "未找到配置。", + "profileSettings": "配置设置", + "profileStartModelSettings": "配置启动模型设置", + "promptTokenHint": "<0>{{token}} 将替换为你的任务描述或后续消息。", + "ptyConnectionError": "连接错误", + "refresh": "刷新", + "refreshCapabilitiesTooltip": "刷新代理能力(模型 + 模式)", + "remove": "移除", + "removeCliFlag": "移除 {{name}}", + "removeThisProfile": "移除此配置", + "requestFailed": "请求失败", + "rescan": "重新扫描", + "retry": "重试", + "retryUpdate": "重试更新", + "retryVersionCheck": "重试版本检查", + "reviewUpdateBeforeApplying": "在 Kandev 更改此主机运行时之前,请先审阅更新。", + "runtimeAlreadyUpToDate": "运行时已是最新。", + "runtimeUpdatedRefreshFailed": "运行时已更新,但无法刷新能力:{{error}}", + "runtimeUpdatedSuccess": "运行时更新成功。", + "runtimeUpdateExplainer": "这将更新此 Kandev 主机上的托管运行时,然后刷新其模型与能力。", + "runtimeUpdateFailed": "运行时更新失败。", + "runtimeUpdateSessionsNote": "活动会话会继续运行;仅后续探测与启动会使用刷新后的运行时。", + "runtimeVersion": "运行时版本", + "scanningForInstalledAgents": "正在扫描已安装的代理…", + "searchModes": "搜索模式…", + "selectAModel": "选择模型…", + "sessionEnded": "会话已结束。", + "sessionEndedWithCode": "会话已结束(退出码 {{code}})。", + "setupProfile": "设置配置", + "signInToAgent": "登录:{{name}}", + "startingSession": "正在启动会话…", + "startMode": "启动模式", + "startModel": "启动模型", + "terminal": "终端", + "tuiCommandHelp": "在 PATH 上查找的二进制名称。使用 <0>{{token}} 插入模型值。", + "tuiModelHelp": "在代理选择器下拉菜单中显示的配置标签", + "unableToStartUpdate": "无法开始更新:{{error}}", + "untitledQuickChat": "未命名快速聊天", + "untitledTask": "未命名任务", + "updateAgent": "更新 {{name}}", + "updatePhaseQueued": "更新已排队…", + "updatePhaseRefreshing": "正在刷新代理能力…", + "updatePhaseResolving": "正在检查最新版本…", + "updatePhaseUpdating": "正在更新运行时…", + "upToDate": "已是最新", + "valueOptional": "值(可选)", + "valueOptionalLabel": "值(可选)", + "valuePlaceholder": "值", + "watcherKindGithubIssue": "GitHub Issues", + "watcherKindGithubReview": "GitHub PR Reviews", + "watcherKindJira": "Jira", + "watcherKindLinear": "Linear", + "searchModels": "搜索模型…", + "noModelFound": "未找到模型。", + "selectAModelShort": "选择模型" +} diff --git a/apps/web/src/locales/zh-cn/automations.json b/apps/web/src/locales/zh-cn/automations.json new file mode 100644 index 0000000000..d66ec3b410 --- /dev/null +++ b/apps/web/src/locales/zh-cn/automations.json @@ -0,0 +1,155 @@ +{ + "addCondition": "添加条件", + "addRepository": "添加仓库", + "agentProfileLabel": "代理配置", + "agentProfilePlaceholder": "选择代理", + "alreadyAdded": "已添加", + "authorsLabel": "作者(逗号分隔)", + "automationCreated": "自动化已创建", + "availablePlaceholders": "可用占位符", + "availablePlaceholdersColon": "可用占位符:", + "baseBranchesLabel": "基础分支(逗号分隔)", + "branchPatternsLabel": "分支模式(逗号分隔,支持 glob)", + "checkNamesLabel": "检查名称(逗号分隔,可选)", + "ciConclusionCancelled": "已取消", + "ciConclusionFailure": "失败", + "ciConclusionSuccess": "成功", + "ciConclusionsLabel": "结论", + "columnEnabled": "已启用", + "columnLastTriggered": "上次触发", + "columnMode": "模式", + "columnName": "名称", + "columnTriggers": "触发器", + "comingSoonSuffix": "(即将推出)", + "completeRequiredFields": "请先填写必填的自动化字段,再保存。", + "configurationLabel": "配置", + "couldNotRefreshRuns": "无法刷新运行记录 — 已从本地缓存恢复", + "createWorkspace": "创建工作区", + "cronEveryDay": "每天", + "cronEveryHour": "每小时", + "cronEveryWeek": "每周", + "cronExpressionLabel": "Cron 表达式", + "cronHelp": "标准五字段 cron 或快捷方式({{shortcuts}})", + "customIntervalLabel": "自定义间隔", + "delete": "删除", + "deleteAll": "全部删除", + "deleteAllRuns": "删除所有运行记录", + "deleteAllRunsDescription": "这将永久删除此自动化的所有运行记录 — 包括当前未加载的记录 — 以及关联的任务。此操作无法撤销。", + "deleteAllRunsTitle": "删除所有运行记录?", + "deleteRun": "删除运行记录", + "done": "完成", + "enabledLabel": "已启用", + "eventsLabel": "事件", + "excludeDraftPrs": "排除草稿 PR", + "executionModeLabel": "执行模式", + "executionModePlaceholder": "选择模式", + "executionModeRun": "运行 — 即发即忘,不显示在看板中", + "executionModeRunShort": "运行", + "executionModeTask": "任务 — 创建可跟踪的看板任务", + "executionModeTaskShort": "任务", + "executorProfileLabel": "执行器配置", + "executorProfilePlaceholder": "选择执行器", + "failedToDeleteAutomation": "删除自动化失败", + "failedToDeleteRun": "删除运行记录失败", + "failedToDeleteRuns": "删除运行记录失败", + "failedToLoadWebhookSecret": "加载 webhook 密钥失败:{{error}}", + "failedToSaveAutomation": "保存自动化失败:{{error}}", + "instructionsLabel": "说明", + "invalidExpression": "表达式无效。请使用带时长的 {{every}}、快捷写法或五字段 cron。", + "listDescription": "创建自动触发代理任务的规则。", + "loading": "正在加载…", + "loadingAutomations": "正在加载自动化…", + "maxConcurrentRuns": "最大并发运行数", + "nameHelp": "输入自动化名称后才能启用保存。", + "nameLabel": "名称", + "namePlaceholder": "自动化名称", + "neverTriggered": "从未触发", + "newAutomation": "新建自动化", + "noAutomationsYet": "还没有自动化", + "noRepositoriesHelp": "未选择仓库时,此自动化会针对工作区的第一个仓库运行。", + "noRunsYet": "还没有运行记录", + "noTriggers": "无", + "noWorkspacesDescription": "请先创建工作区,然后在其中安排自动化。", + "noWorkspacesYet": "还没有工作区", + "orUseWebhookInstead": "或者改用 webhook", + "pickWorkspace": "选择一个工作区来管理其中的自动化。", + "prEventOpened": "已打开", + "prEventOpenedTooltip": "检测到新的开放 PR 时触发一次。每个 PR 编号只会去重一次 — 同一个 PR 不会再次触发。", + "prTriggerRepositoryHelp": "PR 触发器始终使用 PR 自己的仓库。", + "presetDaily": "每天", + "presetHours_one": "{{count}} 小时", + "presetHours_other": "{{count}} 小时", + "presetMinutes_one": "{{count}} 分钟", + "presetMinutes_other": "{{count}} 分钟", + "presetWeekly": "每周", + "pushTriggerHelp": "代码推送到匹配的分支时触发", + "recentRuns_one": "最近运行记录({{count}})", + "recentRuns_other": "最近运行记录({{count}})", + "refresh": "刷新", + "removeRepository": "移除仓库", + "repositoryLabel": "仓库", + "repositoryNone": "无 — 不使用仓库", + "repositoryPlaceholderAuto": "自动", + "required": "必填", + "runStatusArchived": "已归档", + "runStatusCancelled": "已取消", + "runStatusFailed": "失败", + "runStatusRunning": "运行中", + "runStatusSkipped": "已跳过", + "runStatusSucceeded": "成功", + "runStatusTriggered": "已触发", + "runsColumnError": "错误", + "runsColumnTask": "任务", + "runsColumnTime": "时间", + "runsColumnTrigger": "触发器", + "samplePayloadHelp": "粘贴示例 JSON 请求体以发现可用占位符。", + "samplePayloadLabel": "示例请求体(可选)", + "scheduleLabel": "计划", + "scheduleSyntaxHelp": "使用 <0>{{every}} 加时长(例如 <1>{{example1}}、<2>{{example2}}),或使用 <3>{{hourly}}、<4>{{daily}}、<5>{{weekly}} 等快捷方式。", + "scheduleTooltip": "检查匹配事件的频率。后台进程每 30 秒检查一次。计划会在后端重启后保留。", + "searchConditions": "搜索条件…", + "selectRepositoryPlaceholder": "选择仓库", + "summaryCiCompleted": "CI 已完成", + "summaryCron": "Cron:{{expression}}", + "summaryCustomSchedule": "自定义计划", + "summaryPrEvents": "PR:{{events}}", + "summaryPullRequestEvent": "拉取请求事件", + "summaryPushToBranch": "推送到分支", + "summaryWebhook": "Webhook", + "switchToScheduled": "切换为计划运行", + "taskTitleHelp": "留空以使用默认值。支持占位符。", + "taskTitleLabel": "任务标题", + "taskTitlePlaceholder": "[自动] 自动化名称", + "thenDescription": "每次此自动化触发时都会创建一个新任务", + "thenTitle": "然后", + "triggerCategoryGithub": "GitHub", + "triggerCategoryWebhook": "Webhook", + "triggerInfoGithubPr": "按你的计划轮询 GitHub API,查找符合筛选条件的 PR。", + "triggerInfoNotImplemented": "尚未实现。", + "triggerInfoScheduled": "每 30 秒检查一次。Cron 计划匹配时触发。", + "triggerInfoWebhook": "Webhook URL 收到 POST 请求时立即触发。", + "triggerLabelGithubCi": "GitHub CI", + "triggerLabelGithubPr": "GitHub PR", + "triggerLabelGithubPush": "GitHub 推送", + "triggerLabelScheduled": "计划运行", + "triggerLabelWebhook": "Webhook", + "triggerManually": "手动触发", + "unknownTriggerType": "未知触发器类型", + "watchForLabel": "监控", + "webhookCreatedDescription": "配置外部系统,将请求 POST 到此 URL,并在 <0>{{header}} 请求头中提供密钥。你可以随时回到此自动化,再次复制这些值。", + "webhookSecretLabel": "Webhook 密钥", + "webhookSecretLoading": "正在加载…", + "webhookUrlAfterSave": "保存自动化后即可使用 Webhook URL。", + "webhookUrlLabel": "Webhook URL", + "webhookUsageHelp": "发送带 JSON 请求体的 POST 请求,并在 <0>{{header}} 请求头中提供密钥。使用 <1>{{placeholder}} 引用请求体字段,例如 <2>{{example}}。", + "whenDescription": "导致此自动化运行的原因", + "whenTitle": "当", + "workflowHelp": "选择工作流后才能启用保存。", + "workflowLabel": "工作流", + "workflowPlaceholder": "选择工作流", + "workflowStepHelp": "选择工作流步骤后才能启用保存。", + "workflowStepHelpNoWorkflow": "请先选择工作流,再选择步骤。", + "workflowStepLabel": "工作流步骤", + "workflowStepPlaceholder": "选择步骤", + "workflowStepPlaceholderNoWorkflow": "请先选择工作流" +} diff --git a/apps/web/src/locales/zh-cn/azuredevops.json b/apps/web/src/locales/zh-cn/azuredevops.json new file mode 100644 index 0000000000..fdf710b28a --- /dev/null +++ b/apps/web/src/locales/zh-cn/azuredevops.json @@ -0,0 +1,156 @@ +{ + "actionHintAria": "{{kind}} 操作提示 {{index}}", + "actionLabelAria": "{{kind}} 操作标签 {{index}}", + "addPrAction": "添加拉取请求操作", + "addPrQuery": "添加拉取请求查询", + "addPrWatch": "添加拉取请求监控", + "addWorkItemAction": "添加工作项操作", + "addWorkItemQuery": "添加工作项查询", + "addWorkItemWatch": "添加工作项监控", + "agentProfileId": "代理配置 ID", + "azureProjectId": "Azure 项目 ID", + "azureRepositoryIdOptional": "Azure 仓库 ID(可选)", + "baseBranch": "基础分支", + "checkedAt": "检查于 {{at}}", + "cleanupAlways": "始终", + "cleanupAuto": "自动", + "cleanupNever": "从不", + "cleanupPolicy": "清理策略", + "cleanupPolicyAlways": "始终", + "cleanupPolicyAuto": "自动", + "cleanupPolicyNever": "从不", + "closeWatchEditor": "关闭监控编辑器", + "configurationRemoved": "已移除 Azure DevOps 配置", + "configurationSaved": "已保存 Azure DevOps 配置", + "connected": "已连接", + "connectedAs": "已连接为 {{name}}", + "connectionFailed": "连接失败", + "couldNotLoadQuickActions": "无法加载现有快捷操作:{{error}}", + "createPersonalAccessToken": "创建个人访问令牌", + "createWatch": "创建监控", + "creator": "创建者", + "creatorId": "创建者 ID", + "defaultProject": "默认项目", + "defaultQueries": "默认查询", + "defaultQueriesDescription": "在 {{path}} 上显示的拉取请求和工作项查询快捷方式。", + "defaultQueriesSaved": "已保存 Azure DevOps 默认查询", + "defaultQueryInvalid": "每个默认查询都必须包含标签和有效的筛选条件。", + "delete": "删除", + "deleteWatchConfirm": "删除此监控?", + "disable": "禁用", + "disabled": "已禁用", + "edit": "编辑", + "editPrompt": "编辑提示词", + "editWatchTitle": "编辑{{kind}}监控", + "enable": "启用", + "enabled": "已启用", + "enterValidOrganizationUrl": "请输入有效的组织 URL,以打开其令牌设置。", + "executorProfileId": "执行器配置 ID", + "failedToLoadConfig": "加载 Azure DevOps 配置失败:{{error}}", + "failedToLoadQuickActions": "加载快捷操作失败。", + "failedToSaveDefaultQueries": "保存默认查询失败", + "failedToSaveQuickActions": "保存快捷操作失败", + "group": "分组", + "groupCreated": "已创建", + "groupInbox": "收件箱", + "hidePrompt": "隐藏提示词", + "hint": "提示", + "hintOptional": "提示(可选)", + "howToCreatePat": "如何创建个人访问令牌", + "icon": "图标", + "identityIdPlaceholder": "{{token}} 或身份 ID", + "identityPlaceholder": "{{token}} 或身份", + "integrationDescription": "此工作区使用的 Azure DevOps Services 组织、项目和 PAT。", + "integrationTitle": "Azure DevOps 集成", + "kandevRepositoryId": "Kandev 仓库 ID", + "label": "标签", + "loadingProjects": "正在加载项目...", + "loadingWatchers": "正在加载监控…", + "maxInflightTasks": "最大进行中任务数(0 表示不限制)", + "newMatches_one": "{{count}} 个新匹配项。", + "newMatches_other": "{{count}} 个新匹配项。", + "newWatchTitle": "新建{{kind}}监控", + "noPullRequestWatches": "尚无拉取请求监控。", + "notCheckedYet": "尚未检查", + "noWorkItemWatches": "尚无工作项监控。", + "optional": "可选", + "organizationUrl": "组织 URL", + "pastePat": "粘贴 PAT", + "patHelpTitle": "创建用于编辑看板的个人访问令牌", + "patStepCheckScopes": "在 <1>{{workItems}} 下勾选“读取和写入”。在 <4>{{code}} 下勾选“读取”。其他范围均不要勾选。", + "patStepChooseScopes": "选择此组织、较短的有效期和“自定义范围”。", + "patStepCreateAndPaste": "创建令牌,复制后粘贴到此字段。", + "patStepOpenTokenSettings": "打开令牌设置并选择“新建令牌”。", + "personalAccessToken": "个人访问令牌", + "placeholderTitleDescription": "Azure DevOps 工作项或拉取请求的标题", + "placeholderUrlDescription": "Azure DevOps 工作项或拉取请求的 URL", + "pollIntervalSeconds": "轮询间隔(秒)", + "prompt": "提示词", + "promptPlaceholdersHint": "输入 {{open}} 可查看可用占位符。运行操作时会替换 <3>{{url}} 和 <6>{{title}}。", + "pullRequestAny": "任意", + "pullRequestFilter": "拉取请求筛选条件", + "pullRequestKindLower": "拉取请求", + "pullRequestKindNoun": "拉取请求", + "pullRequestKindTitle": "拉取请求", + "pullRequestsTab": "拉取请求", + "pullRequestWatches": "拉取请求监控", + "pullRequestWatchesDescription": "根据匹配的 Azure DevOps 拉取请求自动创建任务。", + "pullRequestWatchFieldsRequired": "项目、仓库、工作流、步骤、代理和执行器均为必填项。", + "queryCreatorAria": "{{kind}} 查询创建者 {{index}}", + "queryGroupAria": "{{kind}} 查询分组 {{index}}", + "queryLabelAria": "{{kind}} 查询标签 {{index}}", + "queryReviewerAria": "{{kind}} 查询审阅者 {{index}}", + "queryStatusAria": "{{kind}} 查询状态 {{index}}", + "queryTopAria": "{{kind}} 查询上限 {{index}}", + "queryWiqlAria": "{{kind}} 查询 WIQL {{index}}", + "quickActionInvalid": "每个快捷操作都必须包含标签和提示词。", + "quickActions": "快捷操作", + "quickActionsDescription": "从拉取请求或工作项启动任务时,在 {{path}} 上显示的提示词。", + "quickActionsSaved": "已保存 Azure DevOps 快捷操作", + "remove": "移除", + "removeActionAria": "移除 {{kind}} 操作 {{index}}", + "removeConfigurationConfirm": "移除 Azure DevOps 配置?", + "removeFailed": "移除失败:{{error}}", + "removeQueryAria": "移除 {{kind}} 查询 {{index}}", + "resetWatchConfirm_one": "重置此{{policy}}监控并移除 {{count}} 个任务?", + "resetWatchConfirm_other": "重置此{{policy}}监控并移除 {{count}} 个任务?", + "reviewer": "审阅者", + "reviewerId": "审阅者 ID", + "runNow": "立即运行", + "save": "保存", + "savedCredential": "已保存的凭据", + "saveFailed": "保存失败:{{error}}", + "saving": "正在保存...", + "savingEllipsis": "正在保存…", + "status": "状态", + "statusAbandoned": "已放弃", + "statusActive": "活动", + "statusAll": "全部", + "statusAny": "任意状态", + "statusCompleted": "已完成", + "statusLine": "状态:{{status}}", + "testConnection": "测试连接", + "testing": "正在测试...", + "top": "上限", + "update": "更新", + "updateWatch": "更新监控", + "watchDeleted": "已删除监控。", + "watchDisabled": "已禁用监控。", + "watchEnabled": "已启用监控。", + "watchReset": "已重置监控。", + "watchStatusAbandoned": "已放弃", + "watchStatusActive": "活动", + "watchStatusCompleted": "已完成", + "watchSummary_one": "{{projectId}} · 每 {{count}} 秒 · 清理策略:{{policy}}", + "watchSummary_other": "{{projectId}} · 每 {{count}} 秒 · 清理策略:{{policy}}", + "workflowId": "工作流 ID", + "workflowStepId": "工作流步骤 ID", + "workItemKindLower": "工作项", + "workItemKindNoun": "工作项", + "workItemKindTitle": "工作项", + "workItemQuery": "工作项查询", + "workItemsTab": "工作项", + "workItemWatches": "工作项监控", + "workItemWatchesDescription": "根据匹配的 Azure DevOps 工作项查询自动创建任务。", + "workItemWatchFieldsRequired": "项目、WIQL、仓库、工作流、步骤、代理和执行器均为必填项。" +} diff --git a/apps/web/src/locales/zh-cn/chat.json b/apps/web/src/locales/zh-cn/chat.json new file mode 100644 index 0000000000..423aa779f8 --- /dev/null +++ b/apps/web/src/locales/zh-cn/chat.json @@ -0,0 +1,40 @@ +{ + "collapseMessage": "折叠消息", + "collapseQueuedMessages": "折叠排队中的消息", + "clearAll": "全部清除", + "clearAllQueuedMessages": "清除所有排队中的消息", + "editQueuedMessage": "编辑排队中的消息", + "expandMessage": "展开消息", + "failedToClearQueuedMessages": "清除排队中的消息失败。", + "failedToMergeQueuedMessages": "合并排队中的消息失败。", + "failedToRemoveQueuedMessage": "移除排队中的消息失败。", + "failedToRunQueuedMessage": "运行排队中的消息失败。", + "mergeReferenceOverflow": "这些排队中的消息包含过多实体引用,无法合并。", + "mergeWithAbove": "与上方消息合并", + "queueCapacity_one": "{{count}} / {{max}}", + "queueCapacity_other": "{{count}} / {{max}}", + "queueCapacityFull_one": "{{count}} / {{max}} · 已满", + "queueCapacityFull_other": "{{count}} / {{max}} · 已满", + "queueChipExpand_one": "{{count}} 条排队消息,点击展开", + "queueChipExpand_other": "{{count}} 条排队消息,点击展开", + "queueCount_one": "{{count}}", + "queueCount_other": "{{count}}", + "queueEditAlreadySent": "消息已发送 — 代理在你的编辑生效前已取走它。", + "queueEditSaveFailed": "保存编辑失败。请重试。", + "queueFullSuffix": "· 已满", + "queued": "已排队", + "queuedCount_one": "{{count}} 条排队消息", + "queuedCount_other": "{{count}} 条排队消息", + "queuedMessages": "排队中的消息", + "removeQueuedMessage": "移除排队中的消息", + "runNext": "运行下一个", + "runNextQueuedMessage": "运行下一个排队消息", + "technicalDetails": "技术详情", + "providerQuotaTitle": "{{provider}} 使用额度已达上限", + "providerQuotaModel": "模型:{{model}}", + "providerQuotaReset": "容量将于 {{resetAt}} 恢复。", + "providerQuotaResetUnknown": "请在提供商恢复可用容量后重试。", + "providerQuotaProviderFallback": "提供商", + "turnGroupToolCalls_one": "{{count}} 次工具调用", + "turnGroupToolCalls_other": "{{count}} 次工具调用" +} diff --git a/apps/web/src/locales/zh-cn/common.json b/apps/web/src/locales/zh-cn/common.json new file mode 100644 index 0000000000..ca80684ed7 --- /dev/null +++ b/apps/web/src/locales/zh-cn/common.json @@ -0,0 +1,91 @@ +{ + "aboutContextCompactionCount": "关于推断的压缩次数", + "agents": "代理", + "automations": "自动化", + "backToTaskOverview": "返回任务概览", + "cancel": "取消", + "changelog": "更新日志", + "commandAgentsSettings": "代理设置", + "commandAgentsSettingsKeywords": "代理, 代理设置, 代理配置, 已安装代理, claude", + "breadcrumb": "面包屑", + "commandConfigChatKeywords": "配置聊天, 配置模式, 配置 kandev, 工作流配置, mcp 配置", + "commandExecutorsSettings": "执行器设置", + "commandExecutorsSettingsKeywords": "执行器, 执行器配置, 执行环境, 环境变量, 运行时, 计算", + "commandGoToAllTasks": "前往全部任务", + "commandGoToAllTasksKeywords": "任务, 列表, 全部", + "commandGoToGitHubDashboard": "前往 GitHub 仪表盘", + "commandGoToGitHubDashboardKeywords": "github, 仪表盘, pr, 拉取请求, 代码审阅, issues, 审阅", + "commandGoToHome": "前往首页", + "commandGoToHomeKeywords": "首页, 看板, 面板", + "commandGoToSettings": "前往设置", + "commandGoToSettingsKeywords": "设置, 偏好, 配置, 通用设置", + "commandGoToStats": "前往统计", + "commandGoToStatsKeywords": "统计, 数据分析, 分析, 指标", + "commandGroupActions": "操作", + "commandGroupNavigation": "导航", + "commandGroupPreferences": "偏好", + "commandGroupSettings": "设置", + "commandPromptsSettings": "提示词设置", + "commandPromptsSettingsKeywords": "提示词, 提示词设置, 自定义提示词, 提示词片段, 提示词模板", + "commandQuickChat": "快速聊天", + "commandQuickChatKeywords": "快速聊天, 新建快速聊天, 快速提问, 询问代理", + "commandSwitchToDarkMode": "切换到深色模式", + "commandSwitchToLightMode": "切换到浅色模式", + "commandThemeKeywords": "主题, 颜色主题, 外观", + "commandWorkspaceSettings": "工作区设置", + "commandWorkspaceSettingsKeywords": "工作区, 工作区", + "configurationChat": "配置聊天", + "contextCompactionCountHelp": "Kandev 根据观察到的上下文用量下降推断此计数。样本缺失或提供商重置可能导致结果仅为近似值。", + "contextCompactions": "压缩次数", + "createAWorkspaceToConfigureThisIntegration": "创建工作区后即可配置此集成。", + "dAgo": "{{diffDay}}天前", + "edit": "编辑", + "executors": "执行器", + "externalMcp": "外部 MCP", + "hAgo": "{{diffHr}}小时前", + "integrations": "集成", + "justNow": "刚刚", + "mAgo": "{{diffMin}}分钟前", + "multiRepoUnavailableLocal": "在 Local 支持其初始启动路径投影同级仓库之前,多仓库任务不可用。", + "multiRepoUnavailableRemoteDocker": "在 Remote Docker 支持创建任务实例之前,多仓库任务不可用。", + "multiRepoUnsupportedExecutor": "此执行器不支持多仓库任务。", + "plugins": "插件", + "prDetails": "PR 详情", + "presetIconBug": "缺陷", + "presetIconCheck": "勾选", + "presetIconCode": "代码", + "presetIconEye": "眼睛", + "presetIconMessage": "消息", + "presetIconSearch": "搜索", + "presetIconSparkle": "闪光", + "presetIconTool": "工具", + "prompts": "提示词", + "requestFailed": "请求失败", + "reset": "重置", + "resetWatchChecking": "正在检查将删除多少任务…", + "resetWatchDeleteAllTasks": "这会删除此前由此监控创建的所有任务,包括已归档任务。监控的轮询游标也会被清除,因此下次检查会重新导入当前所有匹配项。此操作无法撤销。", + "resetWatchDeleteTasks_one": "这会删除此前由此监控创建的 {{count}} 个任务,包括已归档任务。监控的轮询游标也会被清除,因此下次检查会重新导入当前所有匹配项。此操作无法撤销。", + "resetWatchDeleteTasks_other": "这会删除此前由此监控创建的 {{count}} 个任务,包括已归档任务。监控的轮询游标也会被清除,因此下次检查会重新导入当前所有匹配项。此操作无法撤销。", + "resetWatchNoTasks": "此监控尚未创建任何任务,将只清除轮询状态。监控的轮询游标也会被清除,因此下次检查会重新导入当前所有匹配项。此操作无法撤销。", + "resetWatchPreviewFailedRequired": "无法加载受影响的任务数量。请重新预览后再重置此监控。", + "resetWatchTitle": "重置 {{label}}?", + "resetting": "正在重置…", + "retryPreview": "重新预览", + "save": "保存", + "settings": "设置", + "shell": "命令解释器", + "status": "状态", + "subtaskTitle": "子任务标题", + "system": "系统", + "taskLoading": "正在加载任务…", + "taskUnavailable": "任务不可用", + "taskUnavailableDescription": "无法加载此任务。它可能已被删除,或者你没有访问权限。", + "title": "标题", + "toastRegionLabel": "通知", + "unsavedChanges": "未保存的更改", + "workspace": "工作区", + "workspaces": "工作区", + "activeSubagents_one": "{{count}} 个子代理运行中", + "activeSubagents_other": "{{count}} 个子代理运行中", + "openFullPage": "打开完整页面" +} diff --git a/apps/web/src/locales/zh-cn/configChat.json b/apps/web/src/locales/zh-cn/configChat.json new file mode 100644 index 0000000000..406c26ec75 --- /dev/null +++ b/apps/web/src/locales/zh-cn/configChat.json @@ -0,0 +1,21 @@ +{ + "agentProfileHeading": "配置代理配置文件", + "agentProfileHelp": "选择可访问配置工具的代理。这将成为工作区默认值。", + "closePanel": "关闭配置聊天", + "dialogDescription": "让代理管理你的工作流、代理配置与 MCP 配置。", + "noAgentProfiles": "暂无可用的代理配置。请先在代理设置中创建一个。", + "openInQuickChat": "在快速聊天中打开", + "profileUnavailable": "所选代理配置尚不可用。请稍后再试。", + "promptHeading": "你想配置什么?", + "promptPlaceholder": "询问任何与配置相关的问题…", + "startChat": "开始聊天", + "startChatAria": "开始配置聊天", + "startingChat": "正在开始聊天…", + "suggestionAddReviewStep": "为我的工作流添加一个「代码审阅」步骤", + "suggestionCreateProfile": "创建一个启用自动批准的新代理配置", + "suggestionShowWorkflow": "显示当前工作流配置", + "suggestionUpdateMcp": "更新默认代理配置的 MCP 服务器", + "tagline": "用自然语言配置 Kandev", + "tryAsking": "试着问", + "unknownError": "未知错误" +} diff --git a/apps/web/src/locales/zh-cn/executors.json b/apps/web/src/locales/zh-cn/executors.json new file mode 100644 index 0000000000..1c8c12796a --- /dev/null +++ b/apps/web/src/locales/zh-cn/executors.json @@ -0,0 +1,172 @@ +{ + "action": "操作", + "add": "添加", + "addRule": "添加规则", + "advanced": "高级", + "allow": "允许", + "allowlistGithubPlaywright": "白名单 GitHub + Playwright", + "apiKey": "API 密钥", + "areYouSureThisActionCannot": "确定吗?此操作无法撤销。", + "areYouSureYouWantTo": "确定要删除此配置吗?此操作无法撤销。", + "authFilesNotFoundOnThisMachine": "{{files}} — 本机未找到这些文件", + "backToExecutor": "返回执行器", + "backToExecutors": "返回执行器", + "buildFailedWithMessage": "构建失败:{{message}}", + "buildFailedWithStatus": "构建失败({{status}}):{{text}}", + "buildImage": "构建镜像", + "building": "正在构建…", + "checking": "正在检查…", + "chooseAnExecutorTypeToRun": "选择一种执行器类型,以在你的基础设施上运行环境。", + "cleanupScript": "清理脚本", + "configureAuthenticationForToolsAndAgents": "为远程环境中的工具和代理配置身份验证。", + "configured": "已配置", + "confirmDelete": "确认删除", + "connected": "已连接", + "connectedActiveSprites_one": "已连接。{{count}} 个活动 sprite。", + "connectedActiveSprites_other": "已连接。{{count}} 个活动 sprite。", + "connectionTestFailed": "连接测试失败", + "connectionTestPassed": "连接测试通过", + "connectsToARemoteDockerHost": "连接到远程 Docker 主机。仓库将在容器内克隆。", + "copyAuthFiles": "复制认证文件", + "createANewProfileForThis": "为此执行器创建新配置。可在配置页设置脚本和环境变量。", + "createExecutor": "创建执行器", + "createNewSecret": "创建新密钥…", + "createProfile": "创建配置", + "creating": "正在创建…", + "defaultNoTls": "默认(无 TLS)", + "defineNetworkAccessRulesAppliedWhen": "定义为此配置创建 sprite 时应用的网络访问规则。", + "defineTheDockerImageBuildAnd": "定义 Docker 镜像。可在此构建并测试。", + "delete": "删除", + "deleteExecutor": "删除执行器", + "deleteProfile": "删除配置", + "deleting": "正在删除…", + "deny": "拒绝", + "descriptionCustom": "自定义执行器。", + "descriptionLocalDocker": "在本机运行 Docker 容器。", + "descriptionLocalPc": "直接在仓库文件夹中运行代理。", + "descriptionRemoteDocker": "连接到远程 Docker 主机。", + "descriptionSprites": "在 Sprites.dev 云沙箱中运行代理。", + "descriptionWorktree": "创建 git worktree,用于隔离的代理会话。", + "differentConfigurationsForThisExecutorEach": "此执行器的不同配置。每个配置可拥有自己的准备脚本、环境变量和设置。", + "disabled": "已禁用", + "disconnected": "已断开", + "dockerContainers": "Docker 容器", + "dockerContainersCreatedByThisProfile": "由此配置创建的 Docker 容器。", + "dockerHost": "Docker 主机", + "dockerfile": "Dockerfile", + "dockerfileContent": "Dockerfile 内容", + "domain": "域名", + "eGMyApiToken": "例如 {{example}}", + "eGProductionDevelopment": "例如 {{example}}", + "enabled": "已启用", + "envVarsConfiguredCount_one": "已配置 {{count}} 个", + "envVarsConfiguredCount_other": "已配置 {{count}} 个", + "environmentVariables": "环境变量", + "executorName": "执行器名称", + "executorNotFound": "未找到执行器", + "executorType": "执行器类型", + "failed": "失败", + "failedToConnectToBackend": "连接后端失败", + "failedToCreateProfile": "创建配置失败", + "failedToCreateSecret": "创建密钥失败", + "failedToSaveProfile": "保存配置失败", + "filesSelected": "已选文件", + "gitIdentity": "Git 身份", + "gitIdentityAutoDetect": "自动检测", + "gitIdentityCustom": "自定义", + "gitTokenOptional": "Git token(可选)", + "gitUserEmail": "Git 用户邮箱", + "gitUserName": "Git 用户名", + "goToExecutors": "前往执行器", + "image": "镜像", + "imageTag": "镜像标签", + "include": "包含", + "injectedIntoTheExecutionEnvironmentUse": "注入到执行环境。令牌和 API 密钥请使用密钥模式;字面值存储在配置 JSON 中。", + "injectedIntoTheExecutionEnvironmentVariables": "注入到执行环境。变量可引用密钥以存放敏感值。", + "invalidJson": "无效的 JSON", + "janeDeveloper": "Jane Developer", + "key": "键", + "loading": "加载中…", + "loadingEditor": "正在加载编辑器…", + "localDocker": "本地 Docker", + "localDockerExecutor": "本地 Docker 执行器", + "localGitIdentityNotDetected": "本机未检测到本地 git user.name/user.email", + "mcpPolicy": "MCP 策略", + "mcpPolicyJson": "MCP 策略 JSON", + "mcpPolicyMustBeAJsonObject": "MCP 策略必须是 JSON 对象", + "mcpPolicyOverridesForExecutor": "此执行器上 MCP 服务器的 JSON 策略覆盖。", + "mcpPolicyOverridesForProfile": "此配置上 MCP 服务器的 JSON 策略覆盖。", + "mode": "模式", + "name": "名称", + "networkPolicies": "网络策略", + "newProfile": "新配置", + "noDockerContainers": "没有 Docker 容器。", + "noEnvironmentVariablesConfigured": "未配置环境变量。", + "noEnvironmentVariablesConfiguredAddOne": "未配置环境变量。请在下方添加。", + "noNetworkPolicyRulesConfigured": "未配置网络策略规则。", + "noProfilesConfigured": "未配置配置文件。", + "noResponseBody": "无响应正文", + "noTransferableCredentialsFound": "未找到可传输的凭据。", + "none": "无", + "notConfigured": "未配置", + "onlyHttpSse": "仅 HTTP/SSE", + "onlyStdio": "仅 stdio", + "optionalAuthorIdentityAppliedInRemote": "应用于远程执行器环境的可选作者身份。", + "optionalPattern": "可选模式", + "overrideIdentity": "覆盖身份", + "pasteYourSecretValue": "粘贴你的密钥值…", + "pathToTlsCertificatesForThe": "Docker 主机的 TLS 证书路径。", + "personalAccessTokenForCloningRepositories": "在容器内克隆仓库用的个人访问令牌。未设置时从主机环境自动检测。", + "prepareScript": "准备脚本", + "prepareScriptBadge": "准备脚本", + "prepareScriptDescriptionLocal": "在代理启动前于主机上运行。", + "prepareScriptDescriptionRemote": "在代理启动前于执行环境内运行。输入 {{trigger}} 查看可用占位符。", + "profileDetails": "配置详情", + "profileForExecutor": "{{name}} 的配置", + "profileNameIsRequired": "配置名称为必填项。", + "profileNotFound": "未找到配置", + "profileSaved": "配置已保存", + "profiles": "配置", + "provideSecret": "提供密钥", + "quickPresets": "快速预设", + "relatedDockerContainersRemoved_one": "还将移除 {{count}} 个相关 Docker 容器。", + "relatedDockerContainersRemoved_other": "还将移除 {{count}} 个相关 Docker 容器。", + "remoteAuthMethod": "远程认证方式", + "remoteCredentials": "远程凭据", + "remoteDocker": "远程 Docker", + "remoteDockerExecutor": "远程 Docker 执行器", + "remove": "移除", + "removeEnvVar": "移除环境变量", + "removeEnvVarNamed": "移除 {{key}}", + "removeRelatedDockerContainers": "移除相关 Docker 容器", + "removeThisExecutor": "移除此执行器", + "repositoriesWillBeMountedAsVolumes": "仓库将在运行时以卷形式挂载。", + "rewriteLocalhostForDocker": "为 Docker 重写 localhost", + "runsAfterTheAgentSessionEnds": "在代理会话结束后运行,用于清理任务。", + "save": "保存", + "secret": "密钥", + "selectASecret": "选择密钥…", + "selectOrCreateASecret": "选择或创建密钥…", + "selectSecret": "选择密钥…", + "selectTheSecretContainingYourSprites": "选择包含 Sprites.dev API token 的密钥。", + "setACustomNameAndEmail": "为远程 git 提交设置自定义名称和邮箱。", + "setEnvVarViaStoredSecret": "通过已存密钥设置 <1>{{envVar}}", + "stop": "停止", + "success": "成功", + "task": "任务", + "testConnection": "测试连接", + "theRemoteDockerHostUrlTcp": "远程 Docker 主机 URL({{schemes}})。", + "thisActionCannotBeUndone": "此操作无法撤销。", + "tlsCertificatePath": "TLS 证书路径", + "tlsVerify": "TLS 验证", + "tokenConfiguredButUnableToConnect": "已配置 token,但无法连接。", + "typeTokenToConfirmDeletion": "输入“{{token}}”以确认删除。此操作无法撤销。", + "unknownError": "未知错误", + "untrackedTask": "未跟踪", + "useDefaults": "使用默认值", + "useLocalGitConfig": "使用本地 git 配置", + "usedByRemoteExecutorsForCommit": "由远程执行器用于提交作者配置。", + "usesTheLocalDockerDaemonOn": "使用本机上的本地 Docker 守护进程。", + "value": "值", + "verifyingConnection": "正在验证连接…" +} diff --git a/apps/web/src/locales/zh-cn/github.json b/apps/web/src/locales/zh-cn/github.json new file mode 100644 index 0000000000..ef002b4491 --- /dev/null +++ b/apps/web/src/locales/zh-cn/github.json @@ -0,0 +1,371 @@ +{ + "aRegistrationCanBeReusedAcross": "一个注册可跨工作区复用,也可将工作和个人应用分开。每个工作区自行选择并安装凭据。", + "accountsUnavailable": "账户不可用", + "actionPromptPlaceholderHelp": "输入 {{token}} 可查看可用占位符。执行操作时会替换 <4>{{url}} 和 <7>{{title}}。", + "actions": "操作", + "active": "活跃", + "addAnExistingGithubApp": "添加现有 GitHub 应用", + "addExistingApp": "添加现有应用", + "addIssueAction": "添加议题操作", + "addIssueQuery": "添加议题查询", + "addPrAction": "添加 PR 操作", + "addPrQuery": "添加 PR 查询", + "addRepository": "添加仓库", + "addWatch": "添加监控", + "agentProfile": "代理配置", + "allRepositories": "所有仓库", + "allRepositoriesAllowedByThisWorkspace": "此工作区允许的所有仓库", + "allowsInstallationElsewhereItDoesNot": "允许在其他位置安装。不会发布到 Marketplace,也不会授予仓库权限。", + "anExecutorProfileGhTokenOr": "执行器配置中的 {{ghToken}} 或 {{githubToken}} 会覆盖该任务的工作区托管凭据。", + "anImportedAppMustHaveAt": "导入的应用至少必须具备以下仓库权限和订阅事件。", + "apiRateLimit": "API 速率限制", + "appId": "应用 ID", + "appInstallationFailed": "应用安装失败", + "approvalRate": "通过率", + "authenticationUnavailable": "认证不可用", + "auto": "自动", + "automaticallyCreateTasksForGithubIssues": "为符合条件的 GitHub 议题自动创建任务。", + "automaticallyCreateTasksForPrsThat": "自动为需要你审核的 PR 创建任务。", + "automaticallyCreateTasksWhenNewGithub": "当新的 GitHub 议题符合条件时自动创建任务。", + "automaticallyCreateTasksWhenNewPull": "当新的拉取请求需要你审核时自动创建任务。", + "automation": "自动化", + "available": "可用", + "availablePlaceholders": "可用占位符:", + "avgTimeToMerge": "平均合并时间", + "backToRegisteredApps": "返回已注册应用", + "bestForPersonalOrCompanyInternal": "最适合个人或公司内部自动化。", + "changeConnection": "更改连接", + "changeGithubConnection": "更改 GitHub 连接", + "checkNow": "立即检查", + "checkingForNewIssues": "正在检查新议题...", + "checkingForNewPrs": "正在检查新 PR...", + "checkingGithubConnection": "正在检查 GitHub 连接...", + "chooseAGithubApp": "选择 GitHub 应用", + "chooseHowNewlyLaunchedTasksAuthenticate": "选择新启动任务向 GitHub 认证的方式。这不会改变上方的工作区自动化连接。", + "chooseTheAutomationAndPersonalIdentities": "选择此工作区使用的自动化身份和个人身份。", + "chooseTheExactAccountKandevDoes": "选择确切账户。Kandev 不会在 CLI 活跃账户改变时静默跟随。", + "ciPassRate": "CI 通过率", + "cleanUpClosed": "清理已关闭项", + "cleanUpMerged": "清理已合并项", + "cleaning": "正在清理…", + "cleanupBehavior": "清理行为", + "cleanupFailed": "清理失败", + "cleanupPolicyAlways": "始终删除", + "cleanupPolicyAlwaysDescription": "“始终删除”优先于保留规则:即使任务有用户参与或启用了 PR 生命周期提示,也会在合并或关闭时删除。", + "cleanupPolicyAuto": "自动(推荐)", + "cleanupPolicyAutoDescription": "当已合并或关闭的 PR 任务有用户参与或启用了 PR 生命周期提示时保留,否则删除。", + "cleanupPolicyNever": "从不自动删除", + "cleanupPolicyNeverDescription": "保留所有任务。请从任务列表手动删除。", + "cliAccountActiveSuffix": " - 当前", + "clientId": "客户端 ID", + "clientSecret": "客户端密钥", + "comments": "评论", + "configureTheExistingAppOnGithub": "在 GitHub 上配置现有应用", + "connectGithub": "连接 GitHub", + "connectIdentity": "连接身份", + "connectionMethod": "连接方式", + "continueOnGithub": "在 GitHub 上继续", + "controlsHowNewlyLaunchedTasksAuthenticate": "控制新启动任务如何使用 Git HTTPS 和 gh 命令向 GitHub 认证。托管工作区凭据使用 Kandev 的工作区连接;执行器凭据使用所选执行器的 Git 或 SSH 设置。", + "copy": "复制 {{label}}", + "couldNotLoadGithubCliAccounts": "无法加载 GitHub CLI 账户。{{error}}", + "create": "创建", + "createAGithubApp": "创建 GitHub 应用", + "createIssueWatch": "创建议题监控", + "createNewApp": "新建应用", + "createReviewWatch": "创建审核监控", + "created": "已创建", + "createdByKandev": "由 Kandev 创建", + "credentialUsedForRepositorySyncWatches": "用于仓库同步、监控,以及托管代理的 git 和 gh 命令的凭据。执行器配置令牌优先。", + "currentGithubApiRateAndQuery": "此工作区连接当前的 GitHub API 速率和查询限制。", + "customQuery": "自定义查询", + "defaultQueries": "默认查询", + "defaultQueriesAreStillLoading": "默认查询仍在加载。", + "defaultQueriesSaved": "默认查询已保存", + "delete": "删除", + "deletedTasks_one": "已删除 {{count}} 个任务", + "deletedTasks_other": "已删除 {{count}} 个任务", + "disconnect": "断开连接", + "disconnectFailed": "断开连接失败", + "eGBugEnhancementPriorityHigh": "例如 {{examples}}", + "editDetails": "编辑详情", + "editIssueWatch": "编辑议题监控", + "editPrompt": "编辑提示词", + "editReviewWatch": "编辑审核监控", + "enable": "启用", + "enabled": "已启用", + "enterANameUpTo100Characters": "请输入不超过 100 个字符的名称。", + "enterAPublicHostNotLocalhost": "请输入公共主机,不能是 localhost。", + "enterAPublicHttpsOrigin": "请输入公共 HTTPS 源。", + "enterAValidGithubAccountLogin": "请输入有效的 GitHub 账户登录名。", + "enterAnOriginWithoutAPath": "请输入不含路径、查询参数或片段的源。", + "enterTheAppClientId": "请输入应用客户端 ID。", + "enterTheAppClientSecret": "请输入应用客户端密钥。", + "enterTheGithubAppSlug": "请输入 GitHub 应用标识。", + "enterTheNumericGithubAppId": "请输入数字形式的 GitHub 应用 ID。", + "enterTheWebhookSecret": "请输入网络钩子密钥。", + "executorProfile": "执行器配置", + "explainHowManagedTaskCredentialsWork": "说明托管任务凭据的工作方式", + "explainRepositoryScope": "说明仓库范围", + "explainTaskGitAccess": "说明任务 Git 访问方式", + "explainWorkspaceGithubIdentity": "说明工作区 GitHub 身份", + "failedToCheckForIssues": "检查议题失败", + "failedToCheckForPrs": "检查 PR 失败", + "failedToDeleteIssueWatch": "删除议题监控失败", + "failedToDeleteReviewWatch": "删除审核监控失败", + "failedToLoadGithubWorkspaceSettings": "加载 GitHub 工作区设置失败", + "failedToResetIssueWatch": "重置议题监控失败", + "failedToResetReviewWatch": "重置审核监控失败", + "failedToSaveDefaultQueries": "保存默认查询失败", + "failedToSaveGithubWorkspaceSettings": "保存 GitHub 工作区设置失败", + "failedToSaveQuickActions": "保存快捷操作失败", + "filter": "筛选", + "forWebhooksChooseContentType": "对于网络钩子,请选择 <1>{{contentType}} 作为内容类型,并保持 SSL 验证启用。", + "foundNewIssues_one": "发现 {{count}} 个新议题", + "foundNewIssues_other": "发现 {{count}} 个新议题", + "foundNewPrs_one": "发现 {{count}} 个新 PR", + "foundNewPrs_other": "发现 {{count}} 个新 PR", + "generateSetupInstructions": "生成设置说明", + "githubAccessSettingsSaved": "GitHub 访问设置已保存", + "githubApiAccessIsExhaustedFor": "{{resources}} 的 GitHub API 配额已耗尽。后台 PR 和议题检查将暂停,直至最后一个限制于 {{reset}} 重置。", + "githubApiLimits": "GitHub API 限制", + "githubApp": "GitHub 应用", + "githubAppAdded": "已添加 GitHub 应用", + "githubAppConfigurationInstructions": "GitHub 应用配置说明", + "githubAppConnected": "GitHub 应用已连接", + "githubAppCouldNotBeImported": "无法导入 GitHub 应用", + "githubAppImportedItIsReady": "已导入 GitHub 应用,可以安装。", + "githubAppPermissions": "GitHub 应用权限", + "githubAppSetupCancelled": "已取消 GitHub 应用设置", + "githubAppSetupCouldNotStart": "无法启动 GitHub 应用设置", + "githubAppSlug": "GitHub 应用标识", + "githubCli": "GitHub CLI", + "githubCliAccount": "GitHub CLI 账户", + "githubDidNotReturnARedirectUrl": "GitHub 未返回重定向 URL", + "githubDidNotReturnAnInstallationUrl": "GitHub 未返回安装 URL", + "githubIdentityConnected": "GitHub 身份已连接", + "githubIntegration": "GitHub 集成", + "githubIsReadyToCreateThe": "GitHub 已准备好创建应用", + "githubMustReachThisHttpsOrigin": "GitHub 必须能够访问此 HTTPS 源,以发送签名网络钩子和回调。", + "githubOwnerLogin": "GitHub 所有者登录名", + "githubSearchQuerySupportsFullGithub": "GitHub 搜索查询。支持完整的 GitHub 搜索语法,提供最大的灵活性。", + "githubSearchQueryWhenSetOverrides": "GitHub 搜索查询。设置后将覆盖上方的标签筛选条件。", + "githubSetupWasNotCompleted": "GitHub 设置未完成", + "githubWillShowTheGeneratedPermissions": "GitHub 会显示生成的权限并要求你确认。从 GitHub 返回后会添加注册;仍需明确执行安装,才能将其用于此工作区。", + "githubWorkspaceSettingsSaved": "GitHub 工作区设置已保存", + "graphqlQueryLimit": "GraphQL 查询限制", + "group": "分组", + "hidePrompt": "隐藏提示词", + "hideToken": "隐藏令牌", + "hint": "提示", + "hintOptional": "提示(可选)", + "howManagedTaskCredentialsWork": "托管任务凭据的工作方式", + "howOftenToCheckForNew": "检查新议题的频率。最短 60 秒,最长 3600 秒。", + "howOftenToCheckForNew2": "检查新 PR 的频率。最短 60 秒,最长 3600 秒。", + "icon": "图标", + "identityConnectionFailed": "身份连接失败", + "importSetupCouldNotStart": "无法启动导入设置", + "imported": "已导入", + "inUse": "使用中", + "inbox": "收件箱", + "inheritExecutorGitCredentials": "继承执行器 Git 凭据", + "initialStepForNewTasks": "新任务的初始步骤。", + "initialStepForNewTasksAuto": "新任务的初始步骤。自动启动由该步骤设置。", + "installAGithubAppAboveTo": "请先安装上方的 GitHub 应用以更改工作区连接。安装完成后即可保存任务访问设置。", + "installForThisWorkspace": "为此工作区安装", + "interval": "间隔", + "issueCleanupPolicyAlwaysDescription": "关闭时删除,即使你参与过该任务。", + "issueCleanupPolicyAutoDescription": "删除已关闭议题的任务,但保留你曾在其中输入消息的任务。", + "issueWatch": "议题监控", + "issueWatchCreated": "议题监控已创建", + "issueWatchDeleted": "议题监控已删除", + "issueWatchNotFound": "未找到议题监控", + "issueWatchPromptHelp": "为每个新议题发送给代理的提示词。输入 {{token}} 可插入占位符。", + "issueWatchUpdated": "议题监控已更新", + "issueWatches": "议题监控", + "issues": "议题", + "kandevBrokersTheWorkspacePatNamed": "Kandev 将工作区 PAT、指定的 GitHub CLI 账户或应用身份提供给此任务,用于 GitHub HTTPS 和 gh。", + "kandevFirstReservesExactCallbackAnd": "Kandev 会先预留准确的回调和网络钩子 URL,再引导你完成 GitHub 应用设置。在安装导入的应用前,现有工作区访问保持不变。", + "kandevGeneratesTheExactAppPolicy": "Kandev 会生成准确的应用策略。你在 GitHub 上确认创建后,Kandev 将保存此注册,供工作区选择。", + "kandevManagedOperationsActAs": "Kandev 托管的操作以 {{actor}} 身份执行。", + "kandevManagedOperationsUseTheGithub": "Kandev 托管的操作使用为 {{actor}} 安装的 GitHub 应用。", + "kandevStoresThisTokenForThis": "Kandev 会为此工作区保存该令牌。GitHub 会将操作记录为令牌所有者执行。", + "kandevUsesThisWorkspaceConnectionFor": "Kandev 使用此工作区连接执行仓库同步、监控、后台任务和托管代理的 GitHub 命令。使用 PAT 或 GitHub CLI 账户时,“我的 GitHub”和你触发的操作会使用同一个真人身份。", + "label": "标签", + "labels": "标签", + "labelsCommaSeparated": "标签(逗号分隔)", + "lastPolled": "上次轮询", + "legacySharedConnection": "旧版共享连接", + "limitsGithubPullRequestsAndIssues": "限制此工作区中显示或导入的 GitHub 拉取请求和议题。", + "loadingAccounts": "正在加载账户...", + "loadingOrganizations": "正在加载组织...", + "loadingRegisteredApps": "正在加载已注册应用...", + "loadingTaskAccess": "正在加载任务访问设置…", + "localAndWorktreeTasksUseHost": "本地和工作树任务使用主机可见的 Git 或 SSH 凭据。Docker、SSH 和云端任务使用对应执行器中配置的凭据。", + "managedWorkspaceCredentials": "托管工作区凭据", + "me": "我", + "meMyTeams": "我和我的团队", + "missing": "缺失", + "mode": "模式", + "myGithubIdentity": "我的 GitHub 身份", + "myGithubNowUsesYourVerified": "“我的 GitHub”现在使用你已验证的个人身份。", + "nameInKandev": "Kandev 中的名称", + "needsAttention": "需要处理", + "never": "从不", + "noAutomationConnection": "无自动化连接", + "noGhAccountsFound": "未找到 gh 账户", + "noGithubAppsAreRegisteredYet": "尚未注册 GitHub 应用。请添加现有应用或在下方新建。", + "noIssueWatchesConfiguredCreateOne": "尚未配置议题监控。创建一个以开始监控 GitHub 议题。", + "noNewIssuesFound": "未发现新议题", + "noNewPrsFound": "未发现新 PR", + "noReposFoundFor": "未找到“{{org}}”的仓库", + "noReviewWatchesConfiguredCreateOne": "尚未配置审核监控。创建一个以开始监控 PR。", + "noStatsAvailableYet": "暂无统计数据。", + "noTasksToCleanUp": "没有需要清理的任务", + "onlyMatchIssuesWithTheseLabels": "仅匹配带有这些标签的议题。留空则匹配所有议题。", + "onlyPrsWhereYouAreExplicitly": "仅匹配明确请求你审核的 PR(不包括通过团队成员身份请求)", + "onlyTheAppOwner": "仅应用所有者", + "openGithubAppSettings": "打开 GitHub 应用设置", + "optionalFallsBackToStepDefault": "可选,留空则使用步骤默认值。", + "optionalFallsBackToStepDefault2": "可选,留空则使用步骤默认值。代理将在该执行器环境中运行。", + "optionallyConnectYourGithubUserFor": "你可以选择连接 GitHub 用户,用于“我的 GitHub”和归属于真人的操作。此身份绝不会提供给托管代理;工作区自动化仍以应用身份运行。", + "organization": "组织", + "organizations": "组织", + "otherGithubAccounts": "其他 GitHub 账户", + "pasteTheAppPrivateKey": "请粘贴应用私钥。", + "pause": "暂停", + "pauseOrResumePolling": "暂停或恢复轮询。", + "paused": "已暂停", + "permissionsAvailableToKandevThroughThis": "Kandev 通过此工作区安装的 GitHub 应用获得的权限。", + "personalAccessToken": "个人访问令牌", + "personalAccount": "个人账户", + "personalGithubIdentityDisconnected": "个人 GitHub 身份已断开连接", + "personalOauth": "个人 OAuth", + "placeholderActionTitle": "PR 或议题标题", + "placeholderActionUrl": "PR 或议题 URL", + "placeholderCommaSeparatedLabels": "逗号分隔的标签", + "placeholderIssueAuthor": "议题作者用户名", + "placeholderIssueBody": "议题正文", + "placeholderIssueNumber": "议题编号", + "placeholderIssueTitle": "议题标题", + "placeholderIssueUrl": "议题 URL", + "placeholderPrAuthor": "PR 作者用户名", + "placeholderPrNumber": "PR 编号", + "placeholderPrTitle": "PR 标题", + "placeholderPrUrl": "PR 地址", + "placeholderRepository": "仓库", + "placeholderSourceBranch": "源分支", + "placeholderTargetBranch": "目标分支", + "pollIntervalSeconds": "轮询间隔(秒)", + "prAnalytics": "PR 分析", + "prepareAppOnGithub": "在 GitHub 上准备应用", + "presetIconBug": "缺陷", + "presetIconCheck": "检查", + "presetIconCode": "代码", + "presetIconEye": "眼睛", + "presetIconMessage": "消息", + "presetIconSearch": "搜索", + "presetIconSparkle": "闪光", + "presetIconTool": "工具", + "private": "私有", + "privateKeyPem": "私钥({{extension}})", + "promptsShownOnGithubWhenStarting": "从 PR 或议题启动任务时在 {{route}} 上显示的提示词。", + "prsByDay": "按天统计 PR", + "prsCreated": "已创建 PR", + "prsOnDay_one": "{{date}}:{{count}} 个 PR", + "prsOnDay_other": "{{date}}:{{count}} 个 PR", + "prsReviewed": "已审核 PR", + "prsWhereYouOrAnyOf": "请求你或你的任一团队审核的 PR", + "publicKandevUrl": "公共 Kandev URL", + "pullRequestActivityForThisWorkspace": "此工作区的拉取请求活动。", + "pullRequests": "拉取请求", + "query": "查询", + "queryExample": "例如 {{query}}", + "quickActions": "快捷操作", + "quickActionsAreStillLoading": "快捷操作仍在加载。", + "quickActionsSaved": "快捷操作已保存", + "rateLimitRemaining": "剩余 {{remaining}} / {{limit}} {{unit}}", + "rateLimitUnitPoints": "点数", + "rateLimitUnitRequests": "次请求", + "rateLimitUnknown": "未知", + "reconnectIdentity": "重新连接身份", + "refreshGithubConnection": "刷新 GitHub 连接", + "remove": "移除", + "repositories": "仓库", + "repository": "仓库", + "repositoryPermissions": "仓库权限", + "repositoryScope": "仓库范围", + "repositoryScopeHelp": "限制 Kandev 为此工作区发现的 GitHub 拉取请求和议题,包括“我的 GitHub”结果、审核监控和议题监控。它不会更改 GitHub 权限或仓库访问权限。", + "requiredGithubAppPolicy": "必需的 GitHub 应用策略", + "resetCompleteDeletedTasks_one": "重置完成,已删除 {{count}} 个任务;下次轮询将重新导入。", + "resetCompleteDeletedTasks_other": "重置完成,已删除 {{count}} 个任务;下次轮询将重新导入。", + "resetCompleteNextPollWillRe": "重置完成,下次轮询将重新导入匹配项。", + "resetWatch": "重置监控", + "resets": "· 于 {{reset}} 重置", + "resolvedFromOneNamedHostAccount": "需要时从一个指定的主机账户解析。托管任务会获得受限的代理身份;除非在下方选择,否则不会继承主机 Git。", + "reviewPermissions": "审核权限", + "reviewWatch": "审核监控", + "reviewWatchCreated": "审核监控已创建", + "reviewWatchDeleted": "审核监控已删除", + "reviewWatchNotFound": "未找到审核监控", + "reviewWatchPromptHelp": "为每个新 PR 发送给代理的提示词。输入 {{token}} 可插入占位符,或使用 {{mention}} 按名称引用已保存的提示词。它的展开方式与工作流步骤提示词相同,因为此提示词会成为通过同一组装流程传递的任务描述。", + "reviewWatchUpdated": "审核监控已更新", + "reviewWatches": "审核监控", + "saveChanges": "保存更改", + "saving": "正在保存...", + "savingChanges": "正在保存更改…", + "searchLimit": "搜索限制", + "searchQuery": "搜索查询", + "searching": "正在搜索...", + "selectWorkflow": "选择工作流", + "selectWorkspace": "选择工作区", + "selectedRepositories": "已选仓库", + "setTheseExactUrlsCreateAClientSecret": "设置这些准确的 URL,创建客户端密钥和网络钩子密钥,并下载私钥。此一次性设置于 {{expiresAt}} 过期。", + "showGithubApiLimits": "显示 GitHub API 限制", + "showToken": "显示令牌", + "sidebarQueriesShownOnGithubFor": "在 {{route}} 上为拉取请求和议题显示的侧边栏查询。", + "signInWithGhAuthLogin": "使用 <1>{{command}} 登录,然后重新打开此对话框。", + "someSettingsWereSavedButAnother": "部分设置已保存,但另一项更改失败:{{detail}}", + "startOver": "重新开始", + "storedAppCredentialsMintShortLived": "存储的应用凭据用于签发短期安装令牌。托管任务会针对允许的仓库获得受限的应用代理身份。", + "storedEncryptedByKandevWorkspaceAutomation": "由 Kandev 加密存储。工作区自动化和托管任务的 GitHub HTTPS/gh 操作将以你的账户身份执行。", + "subscribedEvents": "订阅的事件", + "taskAccess": "任务访问:", + "taskAccessUnavailable": "任务访问不可用", + "taskGitAccess": "任务 Git 访问", + "taskPrompt": "任务提示词", + "tasksCreatedByThisWatcherLand": "此监控创建的任务会进入所选工作区。", + "theAppIsReadyToSelect": "该应用已可供此工作区选择和安装。", + "theExistingWorkspaceConnectionWasNot": "现有工作区连接未更改。", + "theGithubResponseCouldNotBe": "无法验证 GitHub 响应。请检查连接后重试。", + "theRepositoryWillBeAutomaticallyCloned": "如果工作区中尚无此仓库,将自动克隆到 {{path}}。", + "theSettingsCouldNotBeSaved": "无法保存设置", + "theStoredAppCredentialsAreUnavailable": "存储的应用凭据不可用。", + "theWorkflowToCreateTasksIn": "用于创建任务的工作流。", + "theWorkspaceChangedBeforeTaskAccess": "保存任务访问设置前工作区已发生变化", + "thisAccountAlsoPowersMyGithub": "此账户也用于“我的 GitHub”和用户触发的操作。", + "thisAppRegistrationIsShared_one": "此应用注册由 {{count}} 个工作区共享。注册更改会影响所有使用它的工作区;安装访问权限仍按工作区隔离。", + "thisAppRegistrationIsShared_other": "此应用注册由 {{count}} 个工作区共享。注册更改会影响所有使用它的工作区;安装访问权限仍按工作区隔离。", + "thisWorkspaceUsesOneCredentialFor": "此工作区使用一个凭据执行仓库同步、监控、后台任务和托管代理的 GitHub 访问。执行器配置凭据仍可优先使用。", + "typeOwnerRepoToSearch": "输入 {{format}} 进行搜索", + "unableToLoadTheCurrentTask": "无法加载当前任务访问设置。", + "unexpectedResponseFromTheServer": "服务器返回了意外响应", + "update": "更新", + "useAnAppWhenAutomationNeeds": "当自动化需要独立身份、短期令牌和仓库级安装控制时,请使用应用。设置过程更复杂,并且需要可公开访问的 HTTPS URL。", + "useCommaSeparatedOwnerRepoValues": "使用逗号分隔的 owner/repo 值。", + "usedByWorkspaces_one": "由 {{count}} 个工作区使用。编辑或删除会影响所有这些工作区。", + "usedByWorkspaces_other": "由 {{count}} 个工作区使用。编辑或删除会影响所有这些工作区。", + "verifyAndImportApp": "验证并导入应用", + "viewPermissions": "查看权限", + "webhook": "网络钩子", + "webhookSecret": "网络钩子密钥", + "whichAllowedGithubRepositoriesToMonitor": "此监控要监测哪些允许访问的 GitHub 仓库。", + "whoCanInstallThisApp": "谁可以安装此应用?", + "withManagedWorkspaceCredentialsKandevConfigures": "使用托管工作区凭据时,Kandev 会在新启动的任务进程内将 agentctl 配置为 Git 凭据帮助程序。当 Git 请求已附加 GitHub HTTPS 仓库的凭据时,该帮助程序会兑换一个限定于任务和仓库的代理租约。凭据按需返回给 Git,不会写入仓库或 Git 配置。gh 命令使用 Kandev 的代理感知 shim。继承执行器会跳过这两者,改用所选执行器可见的凭据。更改应用于新启动的任务和下次恢复,不影响已在运行的进程。", + "workflow": "工作流", + "workflowStep": "工作流步骤", + "workflowStepPromptWrapHelp": "工作流步骤提示词会包裹此提示词。例如,如果步骤提示词为 <2>{{stepPrompt}},最终提示词将变为 <5>{{finalPrompt}}", + "workspaceAutomationNowUsesTheVerified": "工作区自动化现在使用已验证的应用安装。", + "workspaceGithubAccess": "工作区 GitHub 访问", + "workspaceGithubConnectionRemoved": "工作区 GitHub 连接已移除", + "workspaceGithubIdentity": "工作区 GitHub 身份" +} diff --git a/apps/web/src/locales/zh-cn/gitlab.json b/apps/web/src/locales/zh-cn/gitlab.json new file mode 100644 index 0000000000..43e91e3bde --- /dev/null +++ b/apps/web/src/locales/zh-cn/gitlab.json @@ -0,0 +1,151 @@ +{ + "actionHint": "操作提示 {{index}}", + "actionLabel": "操作标签 {{index}}", + "actionPrompt": "操作提示词 {{index}}", + "actions": "操作", + "active": "活跃", + "addIssueAction": "添加议题操作", + "addMrAction": "添加合并请求操作", + "agentProfile": "代理配置", + "allProjects": "所有项目", + "alwaysDelete": "始终删除", + "authenticationMethod": "身份验证方式", + "auto": "自动", + "autoKeepEngagedTasks": "自动;保留进行中的任务", + "between60And3600Seconds": "必须介于 60 到 3600 秒之间。", + "checkNow": "立即检查", + "chooseAWorkspacePatTheLocal": "请选择工作区 PAT、本地 glab CLI 登录凭据或环境提供的令牌。CLI 和环境凭据必须已可供 Kandev 后端使用。", + "chooseWhetherToIncludeOnlyDirect": "选择仅包含直接审核请求,还是包含范围更广的兼容请求。", + "cleanupPolicy": "清理策略", + "clearToken": "清除令牌", + "connectAGitlabAccountSoKandev": "连接 GitLab 账户,以便 Kandev 代表你打开合并请求、读取审核讨论,以及回复或解决讨论。", + "connected": "已连接", + "controlsTaskDeletionWhenTheGitlab": "控制 GitLab 条目关闭时是否删除任务。", + "couldNotReachHost": "无法连接 <1>{{host}}:{{error}}", + "createIssueWatch": "创建 GitLab 议题监控", + "createReviewWatch": "创建 GitLab 审核监控", + "createWatch": "创建监控", + "deleteWatch": "删除监控", + "deleteWatchTitle": "删除 {{label}}?", + "deleting": "正在删除…", + "directAndGroupCompatibleRequests": "直接请求及与群组兼容的请求", + "directRequests": "直接请求", + "editIssueWatch": "编辑 GitLab 议题监控", + "editReviewWatch": "编辑 GitLab 审核监控", + "editWatch": "编辑监控", + "enableThisWatchBeforeCheckingNow": "请先启用此监控,再立即检查", + "enableWatch": "启用监控", + "enterAPersonalAccessTokenToSwitch": "请输入个人访问令牌以切换到 PAT。", + "enterAValidHttpOrHttpsGitlabHost": "请输入有效的 HTTP 或 HTTPS GitLab 主机 URL。", + "environmentToken": "环境令牌", + "everyQuickActionNeedsALabel": "每个快捷操作都需要标签和提示词。", + "executorProfile": "执行器配置", + "failedToClearToken": "清除令牌失败", + "failedToResetQuickActions": "重置快捷操作失败", + "failedToSaveQuickActions": "保存快捷操作失败", + "failedToUpdateAuthenticationMethod": "更新身份验证方式失败", + "foundMatchingIssues_one": "找到 {{count}} 个匹配的议题", + "foundMatchingIssues_other": "找到 {{count}} 个匹配的议题", + "foundMatchingMergeRequests_one": "找到 {{count}} 个匹配的合并请求", + "foundMatchingMergeRequests_other": "找到 {{count}} 个匹配的合并请求", + "gitlabAuthenticationMethodUpdated": "GitLab 身份验证方式已更新", + "gitlabHostUrlOverrideForSelf": "GitLab 主机 URL。自托管实例可覆盖此值;使用 {{host}} 时请保留默认值。", + "gitlabIssueWatch": "GitLab 议题监控", + "gitlabQueryParameters": "GitLab 查询参数", + "gitlabQuickActionsReset": "GitLab 快捷操作已重置", + "gitlabQuickActionsSaved": "GitLab 快捷操作已保存", + "gitlabReviewWatch": "GitLab 审核监控", + "gitlabTokenCleared": "GitLab 令牌已清除", + "gitlabWatchCouldNotBeSaved": "无法保存 GitLab 监控", + "glabCli": "glab CLI", + "hideToken": "隐藏令牌", + "initialStepForEachNewTask": "每个新任务的初始步骤。", + "interval": "间隔", + "intervalMinutes_one": "{{count}} 分钟", + "intervalMinutes_other": "{{count}} 分钟", + "intervalMinutesLabel_one": "间隔 {{count}} 分钟", + "intervalMinutesLabel_other": "间隔 {{count}} 分钟", + "issues": "议题", + "issueWatchCheckFailed": "议题监控检查失败", + "issueWatchCreated": "议题监控已创建", + "issueWatchCreationFailed": "创建议题监控失败", + "issueWatchDeleted": "议题监控已删除", + "issueWatchDeletionFailed": "删除议题监控失败", + "issueWatchDialogDescription": "为新匹配的 GitLab 议题自动创建工作区任务。", + "issueWatches": "议题监控", + "issueWatchReset_one": "议题监控已重置;已删除 {{count}} 个任务", + "issueWatchReset_other": "议题监控已重置;已删除 {{count}} 个任务", + "issueWatchResetFailed": "重置议题监控失败", + "issueWatchUpdated": "议题监控已更新", + "issueWatchUpdateFailed": "更新议题监控失败", + "label": "标签", + "labels": "标签", + "lastChecked": "上次检查", + "lastCheckedAt": "上次检查时间:{{value}}", + "leaveEmptyToMatchMergeRequests": "留空以匹配请求你审核的合并请求。添加参数会明确替换该审核人约束。", + "loggedInAs": "登录身份:<1>{{username}}", + "match": "匹配", + "maximumInFlightTasks": "最大进行中任务数", + "mergeRequestReviewWatches": "合并请求审核监控", + "mergeRequests": "合并请求", + "mockTest": "模拟(测试)", + "never": "从不", + "neverDelete": "永不删除", + "newWatch": "新建监控", + "noLimit": "无限制", + "noNewIssuesMatched": "没有匹配到新议题", + "noNewMergeRequestsMatched": "没有匹配到新的合并请求", + "notConfigured": "未配置", + "notConnected": "未连接", + "noWatchesConfigured": "尚未配置监控。", + "optionalCapPollingResumesWhenActive": "可选上限;当活跃任务数低于此值时恢复轮询。", + "optionalCommaSeparatedGitlabLabelsThat": "可选的以逗号分隔的 GitLab 标签;匹配的议题必须包含这些标签。", + "optionalCommaSeparatedNamespaceProjectPaths": "可选的以逗号分隔的命名空间/项目路径。留空将监控所有可访问的项目。", + "optionalGitlabApiQueryParametersSuch": "可选的 GitLab API 查询参数,例如 {{example}}。", + "optionalOtherwiseUsesTheWorkflowStep": "可选;否则使用工作流步骤的默认值。", + "paused": "已暂停", + "pauseWatch": "暂停监控", + "personalAccessToken": "个人访问令牌", + "pollGitlabForMergeRequestsAwaiting": "轮询 GitLab 中等待审核的合并请求,并为每个新的匹配项创建一个任务。", + "pollGitlabIssuesAndCreateOne": "轮询 GitLab 议题,并为每个新的匹配项创建一个任务。", + "pollingAndCleanup": "轮询与清理", + "pollIntervalSeconds": "轮询间隔(秒)", + "projectPaths": "项目路径", + "projects": "项目", + "promptSentToTheSelectedAgent": "为每个新匹配项发送给所选代理配置的提示词。", + "promptUsingUrlAndTitle": "使用 {{url}} 和 {{title}} 的提示词", + "quickActions": "快捷操作", + "reconnectRequired": "需要重新连接", + "refresh": "刷新", + "remove": "移除 {{label}}", + "resetQuickActionsToDefaults": "将快捷操作重置为默认值", + "resetWatch": "重置监控", + "reviewScope": "审核范围", + "reviewWatchCheckFailed": "审核监控检查失败", + "reviewWatchCreated": "审核监控已创建", + "reviewWatchCreationFailed": "创建审核监控失败", + "reviewWatchDeleted": "审核监控已删除", + "reviewWatchDeletionFailed": "删除审核监控失败", + "reviewWatchDialogDescription": "为新匹配的 GitLab 合并请求自动创建工作区任务。", + "reviewWatchReset_one": "审核监控已重置;已删除 {{count}} 个任务", + "reviewWatchReset_other": "审核监控已重置;已删除 {{count}} 个任务", + "reviewWatchResetFailed": "重置审核监控失败", + "reviewWatchUpdated": "审核监控已更新", + "reviewWatchUpdateFailed": "更新审核监控失败", + "saveChangesBeforeCheckingNow": "立即检查前请先保存更改", + "saving": "正在保存...", + "selectWorkflow": "选择工作流", + "shortHint": "简短提示", + "showToken": "显示令牌", + "taskAutomation": "任务自动化", + "taskPrompt": "任务提示词", + "taskPromptsShownOnTheGitlab": "显示在 GitLab 浏览页面上、用于合并请求和议题的任务提示词。", + "thisWillDeleteEveryTaskCreated": "这会删除由此监控创建的所有任务,包括已归档任务,并移除其轮询历史。此操作无法撤销。", + "unreachable": "无法访问", + "updateWatch": "更新监控", + "watchDeletionFailed": "删除监控失败", + "workflow": "工作流", + "workflowStep": "工作流步骤", + "workflowThatReceivesNewTasks": "接收新任务的工作流。", + "yourTokenMayStillBeValid": "你的令牌可能仍然有效——这看起来是网络或上游服务问题。" +} diff --git a/apps/web/src/locales/zh-cn/jira.json b/apps/web/src/locales/zh-cn/jira.json new file mode 100644 index 0000000000..8dc85a731a --- /dev/null +++ b/apps/web/src/locales/zh-cn/jira.json @@ -0,0 +1,149 @@ +{ + "aCredentialIsRequired": "必须提供凭据。", + "aJiraSiteUrlIsRequired": "必须提供 Jira 站点 URL。", + "actions": "操作", + "active": "活动", + "addPreset": "添加预设", + "agentProfile": "代理配置", + "anEmailAddressIsRequired": "必须提供电子邮箱地址。", + "apiToken": "API 令牌", + "apiTokenRecommended": "API 令牌(推荐)", + "atlassianCloud": "Atlassian Cloud", + "atlassianJqlHelp": "Atlassian JQL。监控会轮询此查询,并为每个新匹配的工单创建一个任务。", + "authenticationMethod": "身份验证方式", + "availablePlaceholders": "可用占位符:", + "browserSessionCookie": "浏览器会话 Cookie", + "capOnOpenTasksCreatedBy": "限制此监控创建的进行中任务数量。留空表示不限制。达到上限后,新匹配项会推迟到下次轮询。", + "checkFailed": "检查失败:{{error}}", + "checkNow": "立即检查", + "connectThisWorkspaceToAtlassianCloud": "将此工作区连接到 Atlassian Cloud 或自托管的 Jira Server / Data Center 实例。凭据会在服务器端加密存储,并仅用于所选工作区。", + "connectedAs": "已连接为 {{name}}", + "cookieExpired": "Cookie 已过期——请粘贴新的 Cookie", + "cookieExpiresInDays_one": "Cookie 将在 {{count}} 天后过期", + "cookieExpiresInDays_other": "Cookie 将在 {{count}} 天后过期", + "cookieExpiresInHours_one": "Cookie 将在 {{count}} 小时后过期", + "cookieExpiresInHours_other": "Cookie 将在 {{count}} 小时后过期", + "cookieInstructions": "在 Atlassian 标签页中打开开发者工具({{shortcut}})→\n应用程序标签页 → 存储 → Cookie → {{host}} →\n找到名为“{{cookieName}}”的行(SSO 租户中可能名为\n“{{ssoCookieName}}”)→ 复制“值”单元格 → 粘贴到下方。\n请勿包含 Cookie 名称或任何“=”符号,只粘贴令牌值。", + "create": "创建", + "createATokenAt": "在 <2>{{url}} 创建令牌", + "createFailed": "创建失败:{{error}}", + "createJiraWatcher": "创建 Jira 监控", + "createPatNoProfileLink": "在 Jira 个人资料 → 个人访问令牌中创建个人访问令牌。所需范围:读取和写入。", + "createPatWithProfileLink": "在 Jira 个人资料(<1>{{url}})→ 个人访问令牌中创建个人访问令牌。所需范围:读取和写入。", + "defaultProjectKeyOptional": "默认项目键(可选)", + "delete": "删除", + "deleteFailed": "删除失败:{{error}}", + "deleteThisJiraWatcher": "删除此 Jira 监控?", + "editJiraWatcher": "编辑 Jira 监控", + "editPrompt": "编辑提示词", + "email": "电子邮箱", + "enable": "启用", + "enabled": "已启用", + "enterAPositiveIntegerOrLeave": "请输入正整数或留空。", + "executorProfile": "执行器配置", + "expiryUnknown": "过期时间未知", + "failedToLoadJiraConfig": "加载 Jira 配置失败:{{error}}", + "failedToSaveTaskPresets": "保存任务预设失败", + "foundNewTickets_one": "发现 {{count}} 个新工单,任务即将显示。", + "foundNewTickets_other": "发现 {{count}} 个新工单,任务即将显示。", + "hidePrompt": "隐藏提示词", + "hideSessionTokenHelp": "隐藏会话令牌复制说明", + "hint": "提示", + "hintOptional": "提示(可选)", + "howOftenToReRunThe": "重新运行 JQL 的频率。最短 60 秒,最长 3600 秒。", + "icon": "图标", + "initialStepForNewTasks": "新任务的初始步骤。", + "instanceType": "实例类型", + "interval": "间隔", + "intervalMinutes_one": "{{count}} 分钟", + "intervalMinutes_other": "{{count}} 分钟", + "jiraConfigurationRemoved": "已移除 Jira 配置", + "jiraConfigurationSaved": "已保存 Jira 配置", + "jiraIntegration": "Jira 集成", + "jiraWatcher": "Jira 监控", + "jiraWatchers": "Jira 监控", + "jiraWatchersDescription": "轮询 JQL 查询,并为每个新匹配的工单自动创建一个 Kandev 任务。", + "jqlError": "JQL 错误:{{error}}", + "label": "标签", + "lastPolled": "上次轮询", + "matchedTickets_one": "本页匹配 {{count}} 个工单。", + "matchedTickets_other": "本页匹配 {{count}} 个工单。", + "maxInFlightTasks": "最大进行中任务数", + "never": "从不", + "newWatcher": "新建监控", + "noCap": "(不限制)", + "noJiraWatchersConfiguredCreateOne": "尚未配置 Jira 监控。创建一个监控,即可根据 JQL 查询自动创建任务。", + "noNewTicketsMatched": "没有匹配的新工单。", + "optionalFallsBackToStepDefault": "可选——留空时使用步骤默认值。", + "pasteATokenToTestTheConnection": "粘贴令牌以测试连接", + "pasteApiTokenHere": "在此粘贴 API 令牌", + "pastePersonalAccessTokenHere": "在此粘贴个人访问令牌", + "pasteSessionTokenValue": "粘贴 {{cookieName}} 值", + "pause": "暂停", + "pauseOrResumePolling": "暂停或恢复轮询。", + "paused": "已暂停", + "personalAccessToken": "个人访问令牌", + "placeholderAssigneeDisplayName": "经办人显示名称", + "placeholderIssueType": "问题类型", + "placeholderPriority": "优先级", + "placeholderProjectKey": "项目键", + "placeholderReporterDisplayName": "报告者显示名称", + "placeholderStatusName": "状态名称", + "placeholderTicketDescription": "工单描述", + "placeholderTicketKey": "工单键", + "placeholderTicketSummary": "工单摘要", + "placeholderTicketUrl": "工单 URL", + "pollIntervalSeconds": "轮询间隔(秒)", + "presetIconBug": "缺陷", + "presetIconCheck": "勾选", + "presetIconCode": "代码", + "presetIconEye": "眼睛", + "presetIconMessage": "消息", + "presetIconSearch": "搜索", + "presetIconSparkle": "闪光", + "presetIconTool": "工具", + "presetPlaceholderTicketDescription": "工单描述", + "presetPlaceholderTicketKey": "Jira 工单键", + "presetPlaceholderTicketSummary": "工单摘要", + "presetPlaceholderTicketUrl": "Jira 工单的 URL", + "presetPromptPlaceholderHelp": "输入 {{token}} 可查看可用占位符。运行操作时会替换 <4>{{key}}、<7>{{url}}、<10>{{title}} 和 <13>{{description}}。", + "promptFieldHelp": "为每个新工单发送给代理的提示词。输入 {{token}} 可插入占位符。", + "remove": "移除", + "removeConfiguration": "移除配置", + "removeJiraConfiguration": "移除 Jira 配置?", + "resetCompleteDeletedTasks_one": "重置完成——已删除 {{count}} 个任务;下次轮询将重新导入匹配项。", + "resetCompleteDeletedTasks_other": "重置完成——已删除 {{count}} 个任务;下次轮询将重新导入匹配项。", + "resetCompleteNoTasksDeleted": "重置完成——下次轮询将重新导入匹配项。", + "resetFailed": "重置失败:{{error}}", + "resetWatch": "重置监控", + "saveFailed": "保存失败:{{error}}", + "savedLeaveBlank": "(已保存——留空可保持当前值)", + "saving": "正在保存…", + "selectWorkflow": "选择工作流", + "selectWorkspace": "选择工作区", + "selfHostedJiraServerOrDataCenter": "自托管 Jira(Server 或 Data Center)。", + "serverDataCenter": "Server / Data Center", + "sessionTokenValue": "会话令牌值", + "showSessionTokenHelp": "显示会话令牌复制说明", + "siteUrl": "站点 URL", + "sitesHostedAt": "托管在 {{domain}} 的站点。", + "taskPresets": "任务预设", + "taskPresetsDescription": "从工单启动任务时,在 {{route}} 上显示的提示词。", + "taskPresetsSaved": "已保存任务预设", + "taskPrompt": "任务提示词", + "tasksAreCreatedInThisWorkflow": "任务将在此工作流中创建。", + "tasksCreatedByThisWatcherLand": "此监控创建的任务会进入所选工作区。", + "testConnection": "测试连接", + "testFailed": "失败:{{error}}", + "testJql": "测试 JQL", + "testingConnection": "正在测试...", + "testingJql": "正在测试…", + "update": "更新", + "updateFailed": "更新失败:{{error}}", + "watchDialogDescription": "轮询 JQL 查询,并为每个新匹配的工单自动创建一个 Kandev 任务。可以绑定仓库,让每个任务在该代码库中运行;也可以留空,让任务在无仓库的情况下运行。", + "watcherCreated": "已创建监控", + "watcherDeleted": "已删除监控", + "watcherUpdated": "已更新监控", + "workflow": "工作流", + "workflowStep": "工作流步骤" +} diff --git a/apps/web/src/locales/zh-cn/linear.json b/apps/web/src/locales/zh-cn/linear.json new file mode 100644 index 0000000000..ce22d1e592 --- /dev/null +++ b/apps/web/src/locales/zh-cn/linear.json @@ -0,0 +1,143 @@ +{ + "actions": "操作", + "active": "活动", + "agentProfile": "代理配置", + "anApiKeyIsRequired": "必须提供 API 密钥。", + "any": "(任意)", + "anyTeam": "(任意团队)", + "apiKey": "API 密钥", + "assignee": "经办人", + "assigneeHelp": "按问题分配给谁进行筛选。", + "availablePlaceholders": "可用占位符:", + "checkFailed": "检查失败:{{error}}", + "checkNow": "立即检查", + "chooseATeam": "选择团队", + "configurationRemoved": "已移除 Linear 配置", + "configurationSaved": "已保存 Linear 配置", + "connectedAs": "已连接为 {{name}}", + "connectedAsWithOrg": "已连接为 {{name}}({{org}})", + "create": "创建", + "createFailed": "创建失败:{{error}}", + "createLinearWatcher": "创建 Linear 监控", + "createPersonalApiKeyAt": "创建个人 API 密钥:", + "creator": "创建者", + "creatorHelp": "匹配由某位用户创建的问题。", + "defaultTeamOptional": "默认团队(可选)", + "delete": "删除", + "deleteFailed": "删除失败:{{error}}", + "deleteThisLinearWatcher": "删除此 Linear 监控?", + "dispatchOrder": "分派顺序", + "dispatchOrderHelp": "达到进行中任务上限后,将按此顺序分派问题,以便最重要的问题优先运行。", + "editLinearWatcher": "编辑 Linear 监控", + "enable": "启用", + "enabled": "已启用", + "enabledHelp": "暂停或恢复轮询。", + "enterAPositiveIntegerOrBlank": "请输入正整数或留空。", + "estimateMax": "最大估算值", + "estimateMaxHelp": "点数上限(可选)。", + "estimateMaxPlaceholder": "例如 5", + "estimateMin": "最小估算值", + "estimateMinHelp": "点数下限(可选)。", + "estimateMinPlaceholder": "例如 1", + "executorProfile": "执行器配置", + "failedToLoadConfig": "加载 Linear 配置失败:{{error}}", + "fallsBackToStepDefault": "可选——留空时使用步骤默认值。", + "filter": "筛选条件", + "foundNewIssues_one": "发现 {{count}} 个新问题,任务即将显示。", + "foundNewIssues_other": "发现 {{count}} 个新问题,任务即将显示。", + "interval": "间隔", + "intervalMinutes_one": "{{count}} 分钟", + "intervalMinutes_other": "{{count}} 分钟", + "labels": "标签", + "labelsHelp": "单击可切换。匹配包含任意一个所选标签的问题。", + "labelsHelpNoTeam": "请先选择团队,再选择具体标签。", + "lastPolled": "上次轮询", + "linearIntegration": "Linear 集成", + "linearIntegrationDescription": "使用个人 API 密钥将此工作区连接到 Linear。凭据会在服务器端加密存储,并仅用于所选工作区。", + "linearWatcher": "Linear 监控", + "linearWatchers": "Linear 监控", + "linearWatchersDescription": "轮询 Linear 筛选条件,并为每个新匹配的问题自动创建一个 Kandev 任务。", + "loadingEllipsis": "正在加载…", + "loadingLabels": "正在加载标签…", + "loadingStates": "正在加载状态…", + "loadingTeams": "正在加载团队…", + "maxInflightTasks": "最大进行中任务数", + "maxInflightTasksHelp": "限制此监控创建的进行中任务数量。留空表示不限制。达到上限后,新匹配项会推迟到下次轮询。", + "me": "我", + "never": "从不", + "newWatcher": "新建监控", + "noCap": "(不限制)", + "noDefault": "无默认值", + "noLabelsAvailableForThisTeam": "此团队没有可用标签。", + "noLinearWatchersConfiguredCreateOne": "尚未配置 Linear 监控。创建一个监控,即可根据筛选的问题自动创建任务。", + "noNewIssuesMatched": "没有匹配的新问题。", + "noWorkflowStatesAvailable": "没有可用的工作流状态。", + "pasteAnApiKeyToTest": "粘贴 API 密钥以测试连接", + "pause": "暂停", + "paused": "已暂停", + "pickATeamFirst": "请先选择团队", + "placeholderAssigneeDisplayName": "经办人显示名称", + "placeholderCreatorDisplayName": "问题创建者显示名称", + "placeholderIssueDescription": "问题描述正文", + "placeholderIssueIdentifier": "问题标识符", + "placeholderIssueTitle": "问题标题", + "placeholderIssueUrl": "Linear 问题 URL", + "placeholderPriorityLabel": "优先级标签", + "placeholderTeamKey": "团队键", + "placeholderWorkflowStateName": "工作流状态名称", + "pollIntervalHelp": "重新运行搜索的频率。最短 60 秒,最长 3600 秒。", + "pollIntervalSeconds": "轮询间隔(秒)", + "priority": "优先级", + "priorityHelp": "单击可切换。匹配任意一个所选优先级的问题。", + "priorityHigh": "高", + "priorityLow": "低", + "priorityMedium": "中", + "priorityNone": "无优先级", + "priorityUrgent": "紧急", + "promptFieldHelp": "为每个新问题发送给代理的提示词。输入 {{token}} 可插入占位符。", + "query": "查询", + "queryHelp": "在标题和描述中进行自由文本匹配(可选)。", + "queryPlaceholder": "身份验证缺陷", + "removeConfiguration": "移除配置", + "removeLinearConfigurationConfirm": "移除 Linear 配置?", + "resetCompleteDeletedTasks_one": "重置完成——已删除 {{count}} 个任务;下次轮询将重新导入匹配项。", + "resetCompleteDeletedTasks_other": "重置完成——已删除 {{count}} 个任务;下次轮询将重新导入匹配项。", + "resetCompleteNoTasksDeleted": "重置完成——下次轮询将重新导入匹配项。", + "resetFailed": "重置失败:{{error}}", + "resetWatch": "重置监控", + "saveFailed": "保存失败:{{error}}", + "savedLeaveBlank": "(已保存——留空可保持当前值)", + "saving": "正在保存…", + "selectWorkflow": "选择工作流", + "selectWorkspace": "选择工作区", + "sortCreatedAsc": "创建时间(最早优先)", + "sortCreatedDesc": "创建时间(最新优先)", + "sortDefault": "默认(Linear 顺序)", + "sortPriorityAsc": "优先级(从低到高)", + "sortPriorityDesc": "优先级(从高到低)", + "sortUpdatedAsc": "更新时间(最早更新优先)", + "sortUpdatedDesc": "更新时间(最近更新优先)", + "states": "状态", + "statesHelp": "单击状态可切换。未选择时匹配团队中的所有状态。", + "statesHelpNoTeam": "请先选择团队,再选择具体工作流状态。", + "taskPrompt": "任务提示词", + "team": "团队", + "teamHelp": "将匹配范围限制为一个团队。", + "testConnection": "测试连接", + "testFailed": "失败:{{error}}", + "testing": "正在测试…", + "unassigned": "未分配", + "unknownAccount": "未知账户", + "unknownError": "未知错误", + "update": "更新", + "updateFailed": "更新失败:{{error}}", + "watchDialogDescription": "使用结构化筛选条件轮询 Linear,并为每个新匹配的问题自动创建一个 Kandev 任务。可以绑定仓库,让每个任务在该代码库中运行;也可以留空,让任务在无仓库的情况下运行。", + "watcherCreated": "已创建监控", + "watcherDeleted": "已删除监控", + "watcherUpdated": "已更新监控", + "workflow": "工作流", + "workflowHelp": "任务将在此工作流中创建。", + "workflowStep": "工作流步骤", + "workflowStepHelp": "新任务的初始步骤。", + "workspaceHelp": "此监控创建的任务会进入所选工作区。" +} diff --git a/apps/web/src/locales/zh-cn/sentry.json b/apps/web/src/locales/zh-cn/sentry.json new file mode 100644 index 0000000000..df62a4218d --- /dev/null +++ b/apps/web/src/locales/zh-cn/sentry.json @@ -0,0 +1,154 @@ +{ + "actions": "操作", + "active": "活动", + "addInstance": "添加实例", + "agentProfile": "代理配置", + "any": "(任意)", + "authToken": "身份验证令牌", + "availablePlaceholders": "可用占位符:", + "cantDeleteInstanceInUse_one": "无法删除“{{name}}”:仍有 {{count}} 个监控绑定到该实例。请先重新分配或移除这些监控。", + "cantDeleteInstanceInUse_other": "无法删除“{{name}}”:仍有 {{count}} 个监控绑定到该实例。请先重新分配或移除这些监控。", + "checkFailed": "检查失败:{{error}}", + "checkNow": "立即检查", + "connectedAs": "已连接为 {{name}}", + "create": "创建", + "createFailed": "创建失败:{{error}}", + "createSentryWatcher": "创建 Sentry 监控", + "delete": "删除", + "deleteFailed": "删除失败:{{error}}", + "deleteInstanceAria": "删除 {{name}} Sentry 实例", + "deleteThisSentryWatcher": "删除此 Sentry 监控?", + "edit": "编辑", + "editInstanceAria": "编辑 {{name}} Sentry 实例", + "editSentryWatcher": "编辑 Sentry 监控", + "enable": "启用", + "enabled": "已启用", + "enabledHelp": "暂停或恢复轮询。", + "enterAPositiveIntegerOrBlank": "请输入正整数或留空。", + "enterNameAndTokenBeforeSaving": "保存前请输入实例名称和身份验证令牌。", + "environment": "环境", + "environmentHelp": "可选——限制为一个环境。", + "executorProfile": "执行器配置", + "failedToLoadInstances": "加载 Sentry 实例失败:{{error}}", + "fallsBackToStepDefault": "可选——留空时使用步骤默认值。", + "filter": "筛选条件", + "foundNewIssues_one": "发现 {{count}} 个新问题,任务即将显示。", + "foundNewIssues_other": "发现 {{count}} 个新问题,任务即将显示。", + "grantReadAccess": "为以下范围授予读取权限:", + "instance": "实例", + "instanceNameTaken": "此工作区中已存在名为“{{name}}”的实例。", + "instanceRemoved": "已移除 Sentry 实例", + "instanceSaved": "已保存 Sentry 实例", + "instanceUnavailable": "(不可用)", + "instanceUrl": "实例 URL", + "instanceUrlHelp": "Sentry 实例的基础 URL。使用 Sentry SaaS 时保留 {{defaultUrl}};使用自托管安装时,请改为对应地址(例如 {{exampleUrl}})。", + "instances": "实例", + "interval": "间隔", + "intervalMinutes_one": "{{count}} 分钟", + "intervalMinutes_other": "{{count}} 分钟", + "lastPolled": "上次轮询", + "levelDebug": "调试", + "levelError": "错误", + "levelFatal": "致命", + "levelInfo": "信息", + "levelWarning": "警告", + "levels": "级别", + "levelsHelp": "单击可切换。匹配任意一个所选级别的问题。", + "loadingEllipsis": "正在加载…", + "maxInflightTasks": "最大进行中任务数", + "maxInflightTasksHelp": "限制此监控创建的进行中任务数量。留空表示不限制。达到上限后,新匹配项会推迟到下次轮询。", + "name": "名称", + "nameHelp": "此实例的标签,在工作区内必须唯一。", + "namePlaceholder": "生产环境、自托管等", + "never": "从不", + "newInstance": "新建实例", + "newWatcher": "新建监控", + "noCap": "(不限制)", + "noInstancesInWorkspace": "此工作区中没有 Sentry 实例", + "noInstancesYet": "尚无 Sentry 实例。添加一个实例以连接此工作区。", + "noNewIssuesMatched": "没有匹配的新问题。", + "noOrganizationsAvailable": "没有可用组织", + "noProjectsAvailable": "没有可用项目", + "noProjectsAvailableSentence": "没有可用项目。", + "noWatchersConfigured": "尚未配置 Sentry 监控。创建一个监控,即可根据筛选的问题自动创建任务。", + "organizationSlug": "组织短名称", + "organizationSlugHelp": "要轮询的 Sentry 组织。", + "pasteAnAuthTokenToTest": "粘贴身份验证令牌以测试连接", + "pause": "暂停", + "paused": "已暂停", + "placeholderAssigneeDisplayName": "经办人显示名称", + "placeholderCulprit": "问题源(函数或文件)", + "placeholderEventCount": "事件总数", + "placeholderFirstSeen": "首次发现问题的时间", + "placeholderIssueLevel": "问题级别", + "placeholderIssueStatus": "问题状态", + "placeholderIssueTitle": "问题标题", + "placeholderIssueUrl": "Sentry 问题永久链接", + "placeholderLastSeen": "最近发现问题的时间", + "placeholderProjectSlug": "Sentry 项目短名称", + "placeholderShortId": "Sentry 短 ID", + "placeholderUserCount": "受影响的独立用户数", + "pollIntervalHelp": "重新运行搜索的频率。最短 60 秒,最长 3600 秒。", + "pollIntervalSeconds": "轮询间隔(秒)", + "projectSlug": "项目短名称", + "promptFieldHelp": "为每个新问题发送给代理的提示词。输入 {{token}} 可插入占位符。", + "projectSlugHelp": "要轮询的 Sentry 项目。可选择一个或多个。", + "projectsSelected_one": "已选择 {{count}} 个项目", + "projectsSelected_other": "已选择 {{count}} 个项目", + "query": "查询", + "queryHelp": "自由文本 Sentry 搜索查询(可选)。", + "removeInstanceConfirm": "移除 Sentry 实例“{{name}}”?", + "requiredTokenScopes": "所需令牌范围", + "resetCompleteDeletedTasks_one": "重置完成——已删除 {{count}} 个任务;下次轮询将重新导入匹配项。", + "resetCompleteDeletedTasks_other": "重置完成——已删除 {{count}} 个任务;下次轮询将重新导入匹配项。", + "resetCompleteNoTasksDeleted": "重置完成——下次轮询将重新导入匹配项。", + "resetFailed": "重置失败:{{error}}", + "resetWatch": "重置监控", + "saveFailed": "保存失败:{{error}}", + "savedLeaveBlank": "(已保存——留空可保持当前值)", + "saving": "正在保存…", + "scopeEventRead": "问题和事件——浏览问题并运行监控", + "scopeOrgRead": "组织——解析组织并列出问题", + "scopeProjectRead": "项目——列出项目并限定搜索范围", + "searchProjects": "搜索项目...", + "selectAnInstance": "选择实例", + "selectOrganization": "选择组织", + "selectProjects": "选择项目", + "selectWorkflow": "选择工作流", + "selectWorkspace": "选择工作区", + "sentryInstance": "Sentry 实例", + "sentryInstanceHelp": "此监控轮询的 Sentry 实例。创建后无法更改。", + "sentryIntegration": "Sentry 集成", + "sentryIntegrationDescription": "将此工作区连接到 Sentry。为每个 Sentry 组织或自托管主机添加一个命名实例;凭据会在服务器端加密存储。", + "sentryWatcher": "Sentry 监控", + "sentryWatchers": "Sentry 监控", + "sentryWatchersDescription": "轮询 Sentry 筛选条件,并为每个新匹配的问题自动创建一个 Kandev 任务。", + "statsPeriod": "统计周期", + "statsPeriodHelp": "向前追溯多长时间查找匹配的问题。", + "statsPeriodLast14Days": "过去 14 天", + "statsPeriodLast24Hours": "过去 24 小时", + "statsPeriodLast30Days": "过去 30 天", + "statsPeriodLast7Days": "过去 7 天", + "statsPeriodLastHour": "过去 1 小时", + "statusIgnored": "已忽略", + "statusResolved": "已解决", + "statusUnresolved": "未解决", + "statuses": "状态", + "statusesHelp": "单击可切换。匹配任意一个所选状态的问题。", + "taskPrompt": "任务提示词", + "testConnection": "测试连接", + "testFailed": "失败:{{error}}", + "testing": "正在测试...", + "update": "更新", + "updateFailed": "更新失败:{{error}}", + "useStepDefault": "(使用步骤默认值)", + "watchDialogDescription": "使用结构化筛选条件轮询 Sentry,并为每个新匹配的问题自动创建一个 Kandev 任务。可以绑定仓库,让每个任务在该代码库中运行;也可以留空,让任务在无仓库的情况下运行。", + "watcherCreated": "已创建监控", + "watcherDeleted": "已删除监控", + "watcherUpdated": "已更新监控", + "workflow": "工作流", + "workflowHelp": "任务将在此工作流中创建。", + "workflowStep": "工作流步骤", + "workflowStepHelp": "新任务的初始步骤。", + "workspaceHelp": "此监控创建的任务会进入所选工作区。" +} diff --git a/apps/web/src/locales/zh-cn/settings.json b/apps/web/src/locales/zh-cn/settings.json new file mode 100644 index 0000000000..1284f81756 --- /dev/null +++ b/apps/web/src/locales/zh-cn/settings.json @@ -0,0 +1,619 @@ +{ + "aboutAffectedKandevMcpTools": "关于受影响的 Kandev MCP 工具", + "actionsFor": "{{target}} 的操作", + "activeKandevSpritesSpritesAreDestroyed": "活跃的 Kandev 云沙盒。代理停止时会销毁这些云沙盒。", + "activeSprites_one": "{{count}} 个活跃云沙盒。", + "activeSprites_other": "{{count}} 个活跃云沙盒。", + "addAMissingToolTabBeside": "在所选拆分区域旁添加缺少的工具标签页。", + "addAppriseProvider": "添加 Apprise 提供商", + "addCustomEditor": "添加自定义编辑器", + "addEditor": "添加编辑器", + "addPanel": "添加面板", + "addProvider": "添加提供商", + "addSecret": "添加密钥", + "addTheTabToTheSelected": "将 {{panel}} 标签页添加到所选拆分区域。", + "affectedKandevMcpTool": "受影响的 Kandev MCP 工具", + "agent": "代理", + "apiTokenIsConfigured": "API 令牌已配置。", + "appearance": "外观", + "appriseNotInstalled": "Apprise 尚未安装。你可以稍后添加它,以启用远程通知。<2>查看安装说明。", + "appriseProvider": "Apprise 提供商", + "appriseServiceUrls": "服务 URL", + "appriseUrlRequired": "至少需要一个 Apprise 服务 URL。", + "archiveConfirmation": "归档确认", + "arrangeSplit": "调整拆分布局", + "aSecretNameAndValueAreRequired": "需要填写密钥名称和值。", + "aSecretNameIsRequired": "需要填写密钥名称。", + "autoInstallIfNotFound": "未找到时自动安装", + "autoStart": "自动启动", + "autoStartLanguageServersWhenOpening": "打开文件时自动启动语言服务器,以获取诊断、悬停信息和转到定义功能。你也可以按文件分别开关每个服务器。", + "builtIn": "内置", + "builtInBrowserPanel": "内置浏览器面板", + "cancel": "取消", + "changesPanel": "变更面板", + "changesPanelLayout": "变更面板布局", + "changesTheLanguageOfTheKandev": "更改 Kandev 界面的语言。更改会立即生效,并记住在此设备上。任务标题、聊天以及你创建的其他内容不会翻译。", + "chatInput": "聊天输入", + "chatInputAndCommandShortcuts": "聊天输入和命令快捷键", + "chooseAMonospaceFontForThe": "为终端选择等宽字体。Nerd Fonts 包含 CLI 工具所需的图标。", + "chooseTheLanguageUsedAcrossThe": "选择 Kandev 界面使用的语言", + "chooseWhatOpensWhenKandevStarts": "选择 Kandev 启动时或访问其不带路径的主页时打开的内容", + "chooseWhichTranscriptNavigationControls": "选择要显示的对话记录导航控件,包括仅限桌面的固定提示栏", + "clearShortcut": "清除快捷键", + "clickAShortcutToRecordA": "点击快捷键以录制新的按键组合。", + "clickAUrlInTheTerminal": "在终端中点击 URL 即可打开。", + "cmdCtrlEnterToSend": "按 Cmd/Ctrl+Enter 发送", + "collectExecutionEnvironmentMetrics": "收集执行环境指标", + "colorTheme": "颜色主题", + "command": "命令", + "completeTheRequiredEditorFieldsBefore": "保存前请填写必需的编辑器字段。", + "configureArchiveSafeguardsAndDefaultsFor": "配置代理创建任务时的归档保护和默认设置", + "configureBackendAndExecutionResourceSampling": "配置后端和执行资源采样", + "configureChatInputBehavior": "配置聊天输入行为", + "configureSpritesApiToken": "在执行器配置中设置 <1>{{envVar}} 环境变量,并引用保存 Sprites.dev API 令牌的密钥。", + "configureTerminalAppearanceAndBehavior": "配置终端外观和行为", + "configureTheIncludedCodeEditorAnd": "配置内置代码编辑器和外部编辑器", + "configureTheInitialDesktopTaskWorkbench": "配置桌面任务工作区的初始布局。", + "confirmBeforeArchivingTasks": "归档任务前确认", + "connected": "已连接", + "connection": "连接", + "connectionLostDescription": "连接已丢失至少 10 秒,实时更新可能已过期。", + "connectionTestFailed": "连接测试失败", + "connectionTestPassed": "连接测试通过", + "connectionUnstableDescription": "连接不稳定,正在重新连接 Kandev。", + "continueEditing": "继续编辑", + "couldnTSave": "<0> 无法保存", + "couldnTSave2": "无法保存", + "cpu": "CPU %", + "cpuTemp": "CPU 温度", + "createANewEditableLayoutBased": "基于“默认”新建可编辑布局。", + "createANewSplitOfThe": "在所选拆分区域的{{direction}}新建拆分区域。", + "createASeparateCustomCopyOf": "为所选布局创建独立的自定义副本。", + "custom": "自定义", + "customEditors": "自定义编辑器", + "customEditorType": "自定义", + "customEllipsis": "自定义...", + "customFontExample": "例如 \"我的自定义字体\"", + "customized": "已自定义", + "customizeHowChangedFilesAreDisplayed": "自定义变更文件的显示方式", + "customizeHowTheApplicationLooks": "自定义应用外观", + "customizeKeyboardShortcutsForTheCommand": "自定义命令面板的键盘快捷键", + "dark": "深色", + "decreaseHeight": "减小高度", + "decreaseWidth": "减小宽度", + "default": "默认", + "defaultMenloMonaco": "默认(Menlo / Monaco)", + "defaults": "默认值:", + "delete": "删除", + "deleteLayoutProfile": "删除布局配置", + "deleteLayoutProfileNamed": "删除 {{name}}?", + "deleteSecret": "删除密钥", + "deleteThisCustomLayoutAfterConfirmation": "确认后删除此自定义布局。", + "desktopNotifications": "桌面通知", + "desktopNotificationsDescription": "当选定的代理回合、问题或 Office 事件发生时通知此设备。", + "desktopOnlyWhileYouScrollPast": "仅限桌面端。滚动越过最后一条提示时,将缩短后的副本固定到记录顶部,并提供展开开关。", + "destroyAll": "全部销毁", + "discardAndLeave": "放弃更改并离开", + "discarding": "正在放弃...", + "disconnected": "已断开连接", + "disk": "磁盘 %", + "diskPath": "磁盘路径", + "displayChangedFilesAsAFlat": "将变更文件显示为包含完整路径的平铺列表,或显示为文件夹树。", + "displayLanguage": "显示语言", + "done": "完成", + "duplicate": "复制", + "duplicateLayout": "复制布局", + "edit": "编辑", + "editEditor": "编辑 {{name}}", + "editorIntegrationsAndDefaults": "编辑器集成与默认设置", + "editorName": "编辑器名称", + "editors": "编辑器", + "editorType": "编辑器类型", + "editSecret": "编辑密钥", + "enableDesktopNotifications": "启用桌面通知", + "enableNotificationSound": "启用通知声音", + "enterAShellPathOrCommand": "输入代理环境中可用的命令解释器路径或命令。", + "enterToSend": "按 Enter 发送", + "externalProviders": "外部提供商", + "externalProvidersDescription": "配置用于远程通知的外部提供商。", + "failedToConnectToBackend": "无法连接到后端", + "failedToSaveLayoutProfiles": "无法保存布局配置", + "fileEditor": "文件编辑器", + "fileListView": "文件列表视图", + "fixInvalidLspServerConfigurationBefore": "保存前请修复无效的 LSP 服务器配置。", + "flatList": "平铺列表", + "fontFamily": "字体", + "fontSize": "字号", + "frequencySeconds": "频率(秒)", + "general": "常规", + "health": "运行状况", + "home": "主页", + "hostedUrl": "托管 URL", + "icons": "图标", + "increaseHeight": "增大高度", + "increaseWidth": "增大宽度", + "installed": "已安装", + "invalidJson": "无效 JSON", + "keyboardShortcuts": "键盘快捷键", + "language": "语言", + "languageServers": "语言服务器", + "lastVisitedTask": "上次访问的任务", + "layout": "布局", + "layoutDirectionAbove": "上方", + "layoutDirectionAboveInline": "上方", + "layoutDirectionBelow": "下方", + "layoutDirectionBelowInline": "下方", + "layoutDirectionLeft": "左侧", + "layoutDirectionLeftInline": "左侧", + "layoutDirectionOfTarget": "{{direction}}(相对于 {{target}})", + "layoutDirectionRight": "右侧", + "layoutDirectionRightInline": "右侧", + "layoutEditorForProfile": "{{name}} 编辑器", + "layoutProfile": "布局配置", + "layoutProfileCopyName": "{{name}} 副本", + "layoutProfileName": "布局配置名称", + "layoutProfileNamesMustNotBe": "布局配置名称不能为空", + "layoutProfiles": "布局配置", + "layoutProfilesWereNotSaved": "布局配置未保存", + "layouts": "布局", + "ligatures": "连字", + "light": "浅色", + "loading": "加载中...", + "loadingSecrets": "正在加载密钥...", + "lspConfigCustomBadge": "自定义", + "lspInstallHintGo": "运行“{{command}}”。需要安装 Go。", + "lspInstallHintPython": "安装 {{package}}(通过 {{manager}})到 {{dir}}", + "lspInstallHintRust": "从 GitHub Releases 将 {{binary}} 二进制文件下载到 {{dir}}", + "lspInstallHintTypescript": "安装 {{server}} 和 {{runtime}}(通过 {{manager}})到 {{dir}}", + "makesAgentctlValuesAvailableToPlugins": "使 agentctl 的值可供插件和其他使用方使用。内置状态栏仍仅由宿主使用。", + "makeTheOriginalDefaultLayoutThe": "将原始“默认”布局设为新任务的起始布局。", + "makeTheSelectedSplit40Pixels": "使所选拆分区域缩窄 40 像素。", + "makeTheSelectedSplit40Pixels2": "使所选拆分区域加宽 40 像素。", + "makeTheSelectedSplit40Pixels3": "使所选拆分区域缩短 40 像素。", + "makeTheSelectedSplit40Pixels4": "使所选拆分区域增高 40 像素。", + "manageApiKeysAndCredentialsSecrets": "管理 API 密钥和凭据。密钥会在静态存储时加密,并通过执行器配置文件的环境变量注入代理环境。", + "manageSpritesDevRemoteSandboxIntegration": "管理 Sprites.dev 远程沙盒集成,以便在隔离的云环境中运行代理。", + "markMessagesThatArrivedWhileA": "标记任务不可见期间收到的消息。立即生效。", + "mcpAffectedToolsHelp": "<0>create_task_kandev 会创建单独任务。<3>spawn_session_kandev 会向当前任务添加会话,因此不使用此偏好设置。", + "mcpCreateTaskScope": "<0>create_task_kandev 会创建新任务和子任务。该设置仅在调用时未提供 <4>agent_profile_id 时生效。", + "mcpSpawnSessionScope": "<0>spawn_session_kandev 和你自行创建的任务不受影响。明确选择的配置始终优先。", + "mcpTaskDefaultsAndArchiveSafeguards": "MCP 任务默认设置、归档保护和对话记录偏好", + "mcpTaskProfileCurrentTask": "当前任务配置", + "mcpTaskProfileCurrentTaskDescription": "新任务使用创建它的任务所用的相同配置。若后续工作需要相同模型和代理设置,请选择此项。这可能会复用费用更高的配置。", + "mcpTaskProfileWorkspaceDefault": "工作区默认配置", + "mcpTaskProfileWorkspaceDefaultDescription": "设置了工作流配置时,新任务会使用该配置;否则使用接收任务的工作区默认配置。选择此项可让代理创建的任务使用标准工作区模型,避免意外复用费用较高的配置。", + "memory": "内存 %", + "mergeThisSplitIntoTheSplit": "将此拆分区域合并到包含 {{target}} 的拆分区域。", + "mergeWithTarget": "与 {{target}} 合并", + "messageSubmitKey": "消息提交快捷键", + "metrics": "指标", + "moveTabLeft": "左移标签页", + "moveTabRight": "右移标签页", + "moveTabToSplit": "将标签页移至拆分区域", + "moveThisSplitOf": "按{{direction}}移动此拆分区域,目标为 {{target}}。", + "moveThisTabIntoANewSplit": "将此标签页移到当前拆分区域旁的新拆分区域中。", + "moveThisTabIntoAnotherExisting": "将此标签页移到另一个现有拆分区域中。", + "moveThisTabIntoTheSplit": "将此标签页移到包含 {{target}} 的拆分区域中。", + "moveThisTabOnePositionLeft": "在所在拆分区域中将此标签页左移一个位置。", + "moveThisTabOnePositionRight": "在所在拆分区域中将此标签页右移一个位置。", + "mustBeAJsonObject": "必须是 JSON 对象", + "name": "名称", + "nameEGOpenaiProductionKey": "名称(例如 OpenAI 生产密钥)", + "nerdFonts": "Nerd Fonts", + "new": "新建", + "newBrowserTab": "新建浏览器标签页", + "newCustomEditor": "新建自定义编辑器", + "newLayout": "新建布局", + "newSecret": "新建密钥", + "newTaskSessionsWillUseThis": "新的任务会话将使用此命令解释器。现有会话继续使用当前命令解释器。", + "noActiveSprites": "当前没有活跃的云沙盒。", + "noAppriseProviders": "尚未配置 Apprise 提供商。", + "noCustomEditorsYet": "还没有自定义编辑器。", + "noCustomProfiles": "暂无自定义配置", + "noEditorFound": "未找到编辑器。", + "noProvidersConfiguredYet": "还没有配置提供商。", + "noSecretsYetAddYourFirst": "还没有密钥。添加第一个密钥即可开始。", + "notConfigured": "未配置", + "notificationEventClarificationRequested": "代理需要回答", + "notificationEventClarificationRequestedDescription": "代理明确提出问题时通知。", + "notificationEventOfficeInboxItem": "Office 收件箱项目", + "notificationEventOfficeInboxItemDescription": "有新的收件箱项目需要你处理时通知。", + "notificationEvents": "通知事件", + "notificationEventsDescription": "选择每种通知类型应接收通知的提供商。", + "notificationEventToggle": "{{event}}({{name}})", + "notificationEventTurnFinished": "代理回合已完成", + "notificationEventTurnFinishedDescription": "每个代理回合完成后通知。", + "notificationEventUpdateAvailable": "Kandev 有可用更新", + "notificationEventUpdateAvailableDescription": "有更新版本的 Kandev 发布时通知。", + "notificationPermissionCheckFailed": "Kandev 无法检查通知权限。请重试,然后检查浏览器或操作系统的通知设置。", + "notificationPermissionEnable": "启用", + "notificationPermissionEnabled": "已启用", + "notificationPermissionRetry": "重试", + "notifications": "通知", + "notificationsBlockedInBrowser": "浏览器已阻止通知。请在网站设置中启用通知,然后点击刷新。", + "notificationsBlockedInOs": "操作系统的应用通知设置已阻止通知。请在那里启用通知,然后点击刷新。", + "notificationsDescription": "配置提供商,并选择哪些事件应触发提醒。", + "notificationsNotShowingHint": "如果看不到通知,请检查操作系统设置并允许此浏览器发送通知。", + "notificationSound": "通知声音", + "notificationSoundDescription": "当选定的代理回合、问题或 Office 事件发生时在此设备上播放声音。", + "notificationSoundSelect": "通知声音", + "notificationsUnsupported": "此浏览器不支持桌面通知。", + "notificationType": "通知类型", + "notifyWhenThisEventOccurs": "此事件发生时通知。", + "notInstalled": "未安装", + "openKandevTo": "默认视图", + "openLinksIn": "打开链接的方式", + "opensAVsCodeRemoteSsh": "使用该协议打开 VS Code Remote SSH URL(示例:{{example}})。", + "openSettingsMenu": "打开设置菜单", + "openSettingsMenuWithConnectionIssue": "{{description}} 打开设置菜单", + "optionalScheme": "可选协议({{schemes}})", + "optionalUsername": "可选用户名", + "overrideSettingsSentToEachLanguage": "通过 <2>{{method}} 覆盖发送给每个语言服务器的设置。JSON 格式。", + "pickTheDefaultShellForTask": "选择任务会话的默认命令解释器", + "pluginShortcuts": "插件快捷键", + "preferredShell": "首选命令解释器", + "pressAKeyCombo": "按下按键组合...", + "pressCmdCtrlEnterToSend": "按 Cmd/Ctrl+Enter 发送消息。按 Enter 换行。", + "pressEnterToSendMessagesPress": "按 Enter 发送消息。按 Shift+Enter 换行。", + "previewSound": "试听声音", + "profileForTasksCreatedByAgents": "代理创建任务时使用的配置", + "programming": "编程", + "providerName": "提供商名称", + "providersAndNotificationEvents": "提供商和通知事件", + "pxRange": "px(8-24)", + "refreshNotificationPermission": "刷新通知权限", + "refreshPermissionStatus": "刷新权限状态", + "remove": "移除", + "removePanel": "移除面板", + "removesTheHostMarkerAndProgress": "移除主机标记和进度条,同时保留指标图标和值。", + "removeThisTabFromTheSaved": "从已保存布局中移除此标签页。", + "reset": "重置", + "resetBuiltInLayout": "重置内置布局", + "resetClearShortcut": "重置(清除快捷键)", + "resetToDefault": "恢复默认设置", + "resizeRepositionOrMergeTheSelected": "调整所选拆分区域的大小、位置或合并它。", + "resourceMetrics": "资源指标", + "restoreTheOriginalBuiltInLayout": "恢复原始内置布局并舍弃其覆盖设置。", + "resumeTheMostRecentlyOpenedTask": "在此设备上恢复此工作区中最近打开的任务。", + "retrySave": "重试保存", + "runningSprites": "运行中的云沙盒", + "sameShortcutAs": "与以下项使用相同快捷键:{{shortcuts}}", + "saveAndLeave": "保存并离开", + "saveChanges": "保存更改", + "saveChangesBeforeLeaving": "离开前保存更改?", + "saved": "已保存", + "saving": "正在保存...", + "savingChanges": "正在保存更改", + "searchEditors": "搜索编辑器...", + "secrets": "密钥", + "secretValue": "密钥值", + "selectADefaultEditor": "选择默认编辑器", + "selectAShell": "选择命令解释器", + "sendTestNotification": "发送测试通知", + "sendTestNotificationFor": "发送测试通知给 {{name}}", + "serverConfiguration": "服务器配置", + "setTheFontSizeForThe": "设置终端字号。默认值为 13px。", + "settings": "设置", + "shellOptionsUnavailable": "命令解释器选项不可用", + "shellTerminalFontsAndLinkBehavior": "命令解释器、终端字体和链接行为", + "showAnchoredPromptBar": "显示固定提示栏", + "showCleanupDetailsAndSubtaskOptions": "归档开始前显示清理详情和子任务选项。", + "showHostMetricsInStatusBar": "在状态栏显示主机指标", + "showNewDividerInTranscripts": "在记录中显示“新消息”分隔线", + "showScrollToLastPrompt": "显示滚动到最后一条提示按钮", + "showScrollToStart": "显示滚动到开头按钮", + "showsKandevHostValuesOnlyCollection": "仅显示 Kandev 主机值。客户端显示这些值时才开始收集。", + "showTheControlThatJumpsTo": "显示跳转到记录开头的控件。", + "showTheJumpControlAfterYour": "当最新提示滚动到记录上方后显示跳转控件。", + "showThePerSessionButtonThat": "显示每个会话的按钮,用于临时开关记录自动滚动。隐藏该按钮不会改变默认行为:新记录仍会自动滚动。", + "showTranscriptAutoScrollControl": "显示记录自动滚动控件", + "simplifiedMetrics": "简化指标", + "soundPresetChime": "钟声", + "soundPresetDing": "叮声", + "soundPresetPlim": "轻响", + "soundPresetPop": "啵声", + "splitTab": "拆分标签页", + "spriteHealthCold": "冷却", + "spriteHealthFailed": "失败", + "spriteHealthHealthy": "健康", + "spriteHealthRunning": "运行中", + "spriteHealthStarting": "正在启动", + "spriteHealthStopped": "已停止", + "spriteHealthStopping": "正在停止", + "spriteHealthUnhealthy": "异常", + "spriteHealthUnknown": "未知", + "sprites": "Sprites.dev 云沙盒", + "spritesDevProvidesEphemeralCloudSandboxes": "Sprites.dev 提供临时云沙盒,用于远程运行代理。", + "startOnYourSavedKanbanPipelineOrList": "从保存的看板、流水线或列表视图开始。", + "startupPage": "启动页面", + "statusWithConnectionIssue": "状态。{{description}}", + "submitShortcut": "提交快捷键", + "supportedEditors": "支持的编辑器", + "supportsPlaceholders": "支持占位符:{{placeholders}}", + "systemDefault": "系统默认", + "systemLoad1Min": "系统负载(1 分钟)", + "taskActions": "任务操作", + "taskOverview": "任务概览", + "taskWorkbenchLayoutProfilesAndDefaults": "任务工作台布局配置和默认设置", + "terminal": "终端", + "terminalFont": "终端字体", + "terminalFontSize": "终端字号", + "terminalFontSizeMustBeA": "终端字号必须是 8 到 24 之间的数字。", + "terminalLinks": "终端链接", + "testConnection": "测试连接", + "testNotificationBody": "如果你能读到这条消息,说明浏览器通知正常工作。", + "testNotificationTitle": "测试通知", + "theBuiltInDefaultLayoutWill": "内置“默认”布局将成为默认布局。", + "themeMetricsAndChangesPanelPreferences": "主题、启动页面、指标和变更面板偏好", + "thisAppliesWhenKandevStarts": "这适用于 Kandev 启动或打开不带路径的主页时。主页、返回和工作流导航始终打开任务概览。", + "thisLayoutIsUsedAsThe": "此布局将用作新任务的起始布局。", + "thisPageHasUnsavedChangesSave": "此页面有未保存的更改。请保存、放弃或继续编辑。", + "thisProfileWillBeRemovedWhen": "保存时将移除此配置。", + "thisSecret": "此密钥", + "thisWillPermanentlyRemoveSecret": "此操作将永久移除 <2>{{name}}。此操作无法撤销。", + "transcriptNavigation": "对话记录导航", + "tree": "文件树", + "unableToConnect": "无法连接。", + "unavailable": "不可用", + "unbound": "未绑定", + "unreadMessages": "未读消息", + "untitledLayout": "未命名布局", + "uptime": "运行时间", + "useAsDefault": "设为默认", + "useBuiltInDefault": "使用内置“默认”布局", + "usefulWhenKandevIsSelfHosted": "如果 Kandev 部署在远程服务器上,这项设置很有用:你可以从全局状态栏或手机端的状态抽屉查看机器资源的轻量信息。", + "useThisLayoutAsTheStarting": "将此布局用作新任务的起始布局。", + "useThisSettingWhenAnAgent": "当代理调用会创建任务的 Kandev MCP 工具时使用此设置。如果调用未选择配置,Kandev 必须为新任务分配代理配置。该配置控制任务的代理、模型和设置。", + "utilityAgents": "实用代理", + "viewAvailableSettings": "查看可用设置", + "visualEditorUnavailable": "可视化编辑器不可用", + "voiceMode": "语音模式", + "vsCodeRemoteSsh": "VS Code Remote SSH", + "whenEnabledInstallYourProjectS": "启用后,会通过仓库设置脚本安装项目依赖(例如运行 <2>{{command}}),以避免缺少类型错误。", + "agentGeneratedTaskTitles": "由代理生成任务标题", + "agentGeneratedTaskTitlesDescription": "启用后,新建任务和子任务表单会隐藏手动标题字段,并要求填写提示词。Kandev 会立即使用提示词的前六个词生成临时标题,然后让代理将其替换为约三个词的简洁标题。如果代理无法重命名任务,你仍可编辑临时标题。现有任务和经过编辑的任务会继续显示常规标题字段。", + "useAgentForNewTaskTitles": "使用代理生成新任务标题", + "agentGeneratedTaskTitlesDisabledHint": "默认关闭;请先保存此设置,再打开创建对话框。", + "logsPageTitle": "日志", + "logsPageDescription": "下载一个大小受限的诊断 ZIP,其中包含前端和后端日志。", + "diagnosticReviewBeforeSharing": "分享前请检查", + "diagnosticReviewDescription": "此 ZIP 包含最多三天的后端日志,以及所连接浏览器在本地保留的前端控制台事件。其中可能包含 URL、控制台参数、堆栈、路径、运行时元数据和用户可见错误。", + "diagnosticBundleTitle": "前端和后端诊断日志", + "diagnosticBundleDescription": "Kandev 会向已连接的浏览器标签页请求其最多三天且数量受限的控制台历史记录,将其与保留的后端日志文件合并,并下载为一个 ZIP。", + "diagnosticBackendEvents": "后端日志包含 Kandev 记录的启动、生命周期、API、执行器和错误事件。", + "diagnosticFrontendEvents": "前端日志包含此浏览器配置中记录的控制台错误、警告和诊断通知报告。", + "diagnosticNoMessages": "标准诊断包不包含聊天记录、会话消息或代理消息。", + "diagnosticIncidentalText": "日志条目中已写入的附带文本不会自动脱敏。分享前请检查 ZIP。", + "diagnosticCustomize": "自定义诊断包", + "diagnosticCustomizerTitle": "自定义诊断包", + "diagnosticCustomizerDescription": "选择要包含的证据。默认选择后端和前端日志,且不会将任何选择保存为偏好设置。", + "diagnosticSourcesTitle": "证据来源", + "diagnosticSourceBackend": "后端日志", + "diagnosticSourceBackendDescription": "最多三天的 Kandev 后端保留日志文件。", + "diagnosticSourceFrontend": "前端日志", + "diagnosticSourceFrontendDescription": "来自已连接浏览器标签页、数量受限的控制台事件。", + "diagnosticSourceRuntime": "运行时索引", + "diagnosticSourceRuntimeDescription": "白名单内的会话状态和执行器元数据;不包含消息或提示词。", + "diagnosticSourceACP": "ACP 调试消息", + "diagnosticSourceACPDescription": "所选会话的原始及标准化代理协议帧。", + "diagnosticSelectSessions": "选择 ACP 会话", + "diagnosticSessionsDescription_one": "最多选择 {{count}} 个符合条件的会话。无法收集不可用的会话。", + "diagnosticSessionsDescription_other": "最多选择 {{count}} 个符合条件的会话。无法收集不可用的会话。", + "diagnosticSessionsLoading": "正在加载符合条件的会话…", + "diagnosticSessionUnavailable": "不可用", + "diagnosticSessionRetained": "主机已保留", + "diagnosticSessionReachable": "可访问", + "diagnosticSelectedSessions_one": "已选择 {{count}} 个会话", + "diagnosticSelectedSessions_other": "已选择 {{count}} 个会话", + "diagnosticSelectAllACPSessions": "选择所有 ACP 会话", + "diagnosticClearACPSelection": "清除 ACP 选择", + "diagnosticOpenTask": "在新标签页中打开任务 {{title}}", + "diagnosticSourceRequired": "请至少选择一个证据来源。", + "diagnosticACPSelectionRequired": "请至少选择一个符合条件的 ACP 会话。", + "diagnosticACPSelectionLimit": "ACP 会话最多可选择十个。", + "diagnosticACPWarning": "ACP 调试消息可能包含提示词、响应、工具调用、文件内容、MCP 数据、从环境中获得的值以及密钥。分享前请仔细检查此 ZIP。", + "diagnosticACPNoSessions": "调试模式下可以下载 ACP 数据,但此账户没有可包含的合格会话。", + "diagnosticBundleCancel": "取消", + "diagnosticBundleCreate": "创建诊断包", + "diagnosticBundlePrepareError": "无法准备诊断包。", + "diagnosticBundleFailed": "诊断包创建失败。", + "diagnosticBusy": "诊断功能正忙。请在 {{seconds}} 秒后重试。", + "diagnosticCollectingFrontendLogs": "正在收集前端日志…", + "diagnosticPreparingZip": "正在准备 ZIP…", + "diagnosticZipDownloading": "诊断 ZIP 正在下载。", + "diagnosticPartialZipDownloading": "正在下载部分诊断 ZIP:{{warnings}}", + "diagnosticPartialZipUnavailable": "正在下载部分诊断 ZIP;部分前端日志不可用。", + "updateChannel": "更新通道", + "updateChannelDescription": "选择此托管服务检查并应用的发布版本。", + "updateChannelRequiresManagedService": "Nightly 更新需要 Kandev 管理的 npm 或 npx 用户服务。", + "updateChannelRequiresNpmInstallation": "Nightly 更新仅适用于 npm 或 npx 安装。", + "updateChannelRequiresServiceMetadata": "Nightly 更新需要有效的 Kandev 服务元数据。", + "updateChannelRequiresUserService": "Nightly 更新仅适用于用户服务。", + "updateChannelSettingsUnavailable": "更新通道设置不可用。", + "updateChannelStable": "稳定版", + "updateChannelStableDescription": "已签名的 GitHub 发布。推荐大多数用户使用。", + "updateChannelNightly": "Nightly", + "updateChannelNightlyDescription": "通过 npm 从 main 获取的安装级预发布构建。应用使用显示的确切版本。", + "updateChannelSaveFailed": "无法保存更新通道。请重试。", + "updateChannelSavingNotice": "在检查或应用更新前正在保存通道更改。", + "updateChannelUnsavedNotice": "检查或应用更新前请先保存通道更改。", + "updateChannelUnavailable": "此安装无法使用 Nightly 更新通道。", + "updateChannelUnsupportedServiceManager": "此服务管理器不支持 nightly 更新。", + "updateChannelLatestNightly": "最新 nightly", + "updateChannelLatestRelease": "最新正式版", + "externalMcpIntro": "若要从运行在 Kandev <2>外部 的编码代理(例如主机上的 Claude Code、Cursor 或 Codex)管理 Kandev,或从 Kandev 内的 <4>透传代理 管理,请使用此功能。", + "externalMcpIntroPassthrough": "在 Kandev 内以正常模式启动的代理已自动接入 Kandev MCP,无需额外设置。", + "externalMcpEndpoints": "端点", + "externalMcpEndpointsDescription": "与 Kandev 使用相同主机与端口。本机代理使用 localhost;LAN、VPN 与反向代理客户端使用可达的 Kandev URL。", + "externalMcpStreamableHttp": "Streamable HTTP", + "externalMcpStreamableHttpHint": "推荐用于 Claude Code、Cursor 和 Codex。", + "externalMcpSse": "Server-Sent Events(SSE)", + "externalMcpSseHint": "兼容旧版 MCP 客户端的传输方式。", + "externalMcpAvailableTools": "可用工具", + "externalMcpToolCount_one": "{{count}} 个工具", + "externalMcpToolCount_other": "{{count}} 个工具", + "externalMcpCategoryCount_one": "{{count}} 个类别", + "externalMcpCategoryCount_other": "{{count}} 个类别", + "externalMcpToolsSummary": "{{tools}},共 {{categories}} — 展开可预览外部代理能做什么。", + "externalMcpSnippets": "配置片段", + "externalMcpSnippetsDescription": "粘贴到代理的全局 MCP 配置中。", + "externalMcpConfigPathOrCli": "{{path}} — 或运行下方 CLI 命令", + "externalMcpOpenCodePaths": "{{projectPath}}(项目)或 {{globalPath}}(全局)", + "externalMcpOneLiner": "一行命令(写入 {{path}})", + "externalMcpCopied": "已复制", + "externalMcpCopyUrl": "复制 URL", + "externalMcpCopyToClipboard": "复制到剪贴板", + "externalMcpGroupWorkspaces": "工作区与工作流", + "externalMcpGroupWorkspacesDescription": "发现工作区并对工作流进行 CRUD。", + "externalMcpGroupWorkflowSteps": "工作流步骤(列)", + "externalMcpGroupWorkflowStepsDescription": "管理构成工作流的列。", + "externalMcpGroupAgents": "代理", + "externalMcpGroupAgentsDescription": "检查并配置代理目录。", + "externalMcpGroupAgentProfiles": "代理配置与 MCP 配置", + "externalMcpGroupAgentProfilesDescription": "管理配置级 MCP 服务器设置。", + "externalMcpGroupExecutors": "执行器", + "externalMcpGroupExecutorsDescription": "对执行器配置进行 CRUD(Docker、独立、Sprites…)。", + "externalMcpGroupTasks": "任务", + "externalMcpGroupTasksDescription": "列出、移动、归档并更新任务状态。", + "externalMcpGroupTaskCreation": "任务创建", + "externalMcpGroupTaskCreationDescription": "从外部代理创建新的顶层任务或子任务。", + "externalMcpToolListWorkspaces": "列出所有工作区。", + "externalMcpToolListWorkflows": "列出工作区中的工作流。", + "externalMcpToolCreateWorkflow": "创建工作流。", + "externalMcpToolUpdateWorkflow": "重命名或更新工作流。", + "externalMcpToolDeleteWorkflow": "删除工作流及其所有步骤(破坏性)。", + "externalMcpToolListWorkflowSteps": "列出工作流中的所有步骤。", + "externalMcpToolCreateWorkflowStep": "添加带提示词、颜色与事件钩子的新列。", + "externalMcpToolUpdateWorkflowStep": "编辑步骤设置或事件。", + "externalMcpToolDeleteWorkflowStep": "删除步骤(破坏性)。", + "externalMcpToolReorderWorkflowSteps": "设置步骤 ID 的完整有序列表。", + "externalMcpToolListAgents": "列出已配置的代理及其配置。", + "externalMcpToolUpdateAgent": "切换 MCP 支持或设置配置路径。", + "externalMcpToolCreateAgentProfile": "创建新代理配置(模型、自动批准)。", + "externalMcpToolDeleteAgentProfile": "删除代理配置。", + "externalMcpToolListAgentProfiles": "列出代理的配置。", + "externalMcpToolUpdateAgentProfile": "更新名称、模型或自动批准。", + "externalMcpToolGetMcpConfig": "读取配置的 MCP 服务器映射。", + "externalMcpToolUpdateMcpConfig": "替换配置的 MCP 服务器映射。", + "externalMcpToolListExecutors": "列出所有执行器。", + "externalMcpToolListExecutorProfiles": "列出执行器的配置。", + "externalMcpToolCreateExecutorProfile": "创建带配置、准备/清理脚本的配置。", + "externalMcpToolUpdateExecutorProfile": "更新执行器配置。", + "externalMcpToolDeleteExecutorProfile": "删除执行器配置。", + "externalMcpToolListTasks": "列出工作流中的任务。", + "externalMcpToolMoveTask": "将任务移到其他步骤或位置。", + "externalMcpToolDeleteTask": "永久删除任务。", + "externalMcpToolArchiveTask": "归档任务。", + "externalMcpToolUpdateTaskState": "设置任务状态:{{states}}。", + "externalMcpToolCreateTask": "创建顶层任务,或在显式 {{parentIdParam}} 下创建子任务。", + "promptsPageDescription": "为聊天输入创建可复用的提示词片段。", + "promptsInvalidReason": "提示词名称与内容为必填项。", + "promptMentionHelp": "在聊天输入中使用 <1>{{token}} 可插入提示词内容。按名称匹配提示词并就地展开。", + "promptsCustomHeading": "自定义提示词", + "promptAdd": "添加提示词", + "promptNamePlaceholder": "提示词名称", + "promptContentPlaceholder": "提示词内容", + "promptsLoading": "正在加载提示词…", + "promptsEmpty": "尚无提示词。添加第一个提示词以开始使用。", + "promptDelete": "删除提示词", + "promptDeleteThisPrompt": "此提示词", + "promptDeleteDescription": "这将永久移除 <2>{{name}}。此操作无法撤销。", + "promptCreateFailed": "无法创建提示词", + "promptSaveFailed": "无法保存提示词", + "promptDeleteFailed": "无法删除提示词", + "voiceModePageDescription": "配置聊天输入框中语音输入的工作方式。", + "voiceEnableTitle": "启用语音输入", + "voiceEnableLabel": "在聊天输入框显示麦克风按钮", + "voiceEnableDescription": "关闭后,语音按钮完全隐藏,且不运行任何语音相关代码。下方设置会保留,重新开启时再次生效。", + "voiceUnavailableTitle": "此浏览器不支持语音输入。", + "voiceUnavailableInsecure": "麦克风 API 需要 {{secureScheme}} 或 {{localhostHost}}。你似乎在不安全的 {{insecureScheme}} 源上 — 请通过 {{secureScheme}}(或 {{localhostUrl}})加载此页面以启用语音输入。", + "voiceUnavailableUnsupported": "你的浏览器未暴露 Web Speech API 或 MediaRecorder。请尝试 Chrome、Edge 或 Safari 14.5+。", + "voiceEngineTitle": "转写引擎", + "voiceEngineUnsupported": "此浏览器不支持。", + "voiceEngineAuto": "自动", + "voiceEngineAutoDescription": "使用此浏览器中可用的最佳引擎。", + "voiceEngineWebSpeech": "Web Speech(浏览器内)", + "voiceEngineWebSpeechDescription": "免费、即时,使用浏览器内置语音识别。", + "voiceEngineWhisperWeb": "Whisper Web(私有,浏览器内)", + "voiceEngineWhisperWebDescription": "完全在此设备上运行 OpenAI Whisper。首次使用会下载模型(40–240 MB)。", + "voiceEngineWhisperServer": "Whisper Server(OpenAI)", + "voiceEngineWhisperServerDescription": "将音频发送到后端,再转发到 OpenAI 的 Whisper API。需要在服务器上配置 API 密钥。", + "voiceEngineBadgeLocal": "本地", + "voiceEngineBadgeServer": "服务器", + "voiceBehavior": "行为", + "voiceLanguagesGroup": "语言", + "voiceLanguageHint": "语言与实际说话不匹配时,识别质量会急剧下降。", + "voiceLanguageAuto": "自动检测(浏览器语言)", + "voiceLanguageEnUs": "英语(美国)", + "voiceLanguageEnGb": "英语(英国)", + "voiceLanguageEsEs": "西班牙语(西班牙)", + "voiceLanguageEsMx": "西班牙语(墨西哥)", + "voiceLanguagePtPt": "葡萄牙语(葡萄牙)", + "voiceLanguagePtBr": "葡萄牙语(巴西)", + "voiceLanguageFrFr": "法语", + "voiceLanguageDeDe": "德语", + "voiceLanguageItIt": "意大利语", + "voiceLanguageJaJp": "日语", + "voiceLanguageZhCn": "中文(简体)", + "voiceActivation": "激活方式", + "voiceActivationToggle": "点击开始 / 停止", + "voiceActivationHold": "按住说话", + "voiceAutoSend": "转写后自动发送", + "voiceAutoSendDescription": "转写插入后立即提交消息。", + "voiceWhisperModelTitle": "Whisper Web 模型", + "voiceWhisperModelHint": "模型在首次使用时下载并缓存在浏览器中。切换模型会在下次录音时触发另一次下载。", + "voiceWhisperTiny": "Tiny", + "voiceWhisperTinyHint": "最快,准确度较低", + "voiceWhisperBase": "Base", + "voiceWhisperBaseHint": "均衡默认", + "voiceWhisperSmall": "Small", + "voiceWhisperSmallHint": "准确度最高,加载较慢", + "voiceShortcutTitle": "{{shortcut}} 快捷键", + "voiceShortcutHint": "点击快捷键可录制新的按键组合。所有键盘快捷键也可在通用设置中编辑。", + "utilityAgentsPageDescription": "用于提交、PR 与提示词的一次性 AI 助手。", + "model": "模型", + "utilityDefaultModelTitle": "默认实用代理模型", + "utilityDefaultModelDescription": "选择所有内置实用操作使用的默认模型。", + "utilityDefaultModelAriaLabel": "默认实用模型设置", + "utilitySelectAgent": "选择代理…", + "utilitySelectModel": "选择模型…", + "utilityActionsTitle": "操作", + "utilityDefaultWithModel": "默认({{model}})", + "utilityModelNotConfigured": "未配置", + "utilityCustomAgentsTitle": "自定义实用代理", + "utilityCustomAgentsDescription": "使用自定义提示词创建你自己的实用代理。", + "utilityCustomAgentsEmpty": "无自定义实用代理。", + "utilityAddCustomAgent": "添加", + "utilityAgentDialogCreateTitle": "创建实用代理", + "utilityAgentDialogEditTitle": "编辑实用代理", + "utilityAgentDialogCreate": "创建代理", + "utilityAgentDialogSave": "保存更改", + "utilityAgentNamePlaceholder": "例如 commit-message", + "utilityAgentDescriptionLabel": "描述", + "utilityAgentDescriptionPlaceholder": "简要描述此代理的作用", + "utilityAgentPromptTemplate": "提示词模板", + "utilityAgentPromptHint": "输入 <1>{{sigil}} 可查看可用变量并自动完成", + "configChatAgentTitle": "配置聊天代理", + "configChatAgentDescription": "选择用于配置聊天的代理配置。此代理可管理你的工作流、代理配置与 MCP 配置。", + "configChatAgentPlaceholder": "选择代理配置…", + "configChatAgentNoDefault": "无默认", + "inferenceAgentProbing": "正在设置 {{name}}…", + "inferenceAgentAuthRequired": "登录 {{name}} 以加载模型。", + "inferenceAgentNotInstalled": "本机未安装 {{name}} CLI。", + "inferenceAgentNotConfigured": "{{name}} 未配置为推理用途。", + "inferenceAgentProbeFailed": "{{name}} 探测失败。", + "inferenceAgentNoModels": "{{name}} 未公布任何模型。", + "inferenceAgentUnavailable": "{{name}} 已不可用。", + "inferenceAgentThisAgent": "此代理", + "inferenceAgentRefresh": "刷新", + "inferenceAgentRefreshing": "正在刷新…", + "changelogPageDescription": "查看所有发布并管理通知偏好", + "changelogNotificationsDescription": "控制顶栏中的发布说明通知", + "changelogReleaseHistory": "发布历史", + "changelogReleaseHistoryDescription": "所有版本及其发布说明", + "changelogTopbarTitle": "顶栏发布通知", + "changelogTopbarLabel": "有新发布时显示通知", + "changelogTopbarDescription": "启用后,有新版本发布时顶栏会显示闪光图标", + "changelogResetSeenLabel": "重置已查看发布", + "changelogResetSeenDescription": "清除上次查看的版本,使顶栏通知再次出现" +} diff --git a/apps/web/src/locales/zh-cn/sidebar.json b/apps/web/src/locales/zh-cn/sidebar.json new file mode 100644 index 0000000000..8e2b8bda68 --- /dev/null +++ b/apps/web/src/locales/zh-cn/sidebar.json @@ -0,0 +1,76 @@ +{ + "account": "账户", + "activeWorkspaceBadge": "活动", + "activity": "活动", + "addAgent": "添加代理", + "addProject": "添加项目", + "addWorkspace": "添加工作区", + "agentTopology": "代理拓扑", + "apiTokens": "API 令牌", + "applicationStatus": "应用状态", + "applicationStatusItems": "应用状态项", + "closeSettings": "关闭设置", + "collapseGroup": "折叠 {{label}}", + "collapseSidebar": "折叠侧边栏", + "connected": "已连接", + "connectedToKandev": "已连接到 Kandev", + "connecting": "正在连接", + "connectingToKandev": "正在连接到 Kandev", + "connection": "连接", + "connectionError": "连接错误", + "connectionErrorDetail": "连接错误:{{error}}", + "connectionLost": "连接已断开", + "connectionLostDescription": "连接已断开至少 10 秒。实时更新可能已过期。", + "connectionUnavailable": "连接不可用", + "connectionUnstable": "连接不稳定", + "connectionUnstableDescription": "连接不稳定。正在重新连接到 Kandev。", + "costs": "费用", + "enabledBadge": "已启用", + "expandGroup": "展开 {{label}}", + "expandSidebar": "展开侧边栏", + "filtersAndSort": "筛选与排序", + "home": "首页", + "improveKandev": "完善 Kandev", + "inbox": "收件箱", + "kanban": "看板", + "kandevHome": "Kandev 首页", + "logOut": "退出登录", + "maxViews_one": "最多 {{count}} 个", + "maxViews_other": "最多 {{count}} 个", + "newKanbanWorkspace": "新建看板工作区", + "newOfficeWorkspace": "新建办公室工作区", + "newTask": "新建任务", + "newView": "新建视图", + "newViewUnavailable": "无法新建视图。{{reason}}", + "noAgentsYet": "尚无代理", + "noProjectsYet": "尚无项目", + "noWorkspaces": "无工作区", + "office": "办公室", + "offline": "离线", + "openStatus": "打开状态", + "pausedBadge": "已暂停", + "preferences": "偏好", + "profileAndPassword": "个人资料与密码", + "projects": "项目", + "quickChat": "快速聊天", + "reconnecting": "正在重连", + "reconnectingToKandev": "正在重新连接到 Kandev", + "repositories": "仓库", + "resizeSidebar": "调整侧边栏大小", + "routines": "例行程序", + "routing": "路由", + "runErrors_one": "{{count}} 个错误", + "runErrors_other": "{{count}} 个错误", + "saveDiscardFirst": "请先保存/丢弃", + "skills": "技能", + "stats": "统计", + "switchWorkspace": "切换工作区", + "tasks": "任务", + "toggleTheme": "切换主题", + "unsavedFilterChanges": "未保存的筛选更改", + "viewAll": "全部", + "viewLabel": "视图:{{name}}", + "whatsNew": "新功能", + "work": "工作", + "workspaceFallback": "工作区" +} diff --git a/apps/web/src/locales/zh-cn/slack.json b/apps/web/src/locales/zh-cn/slack.json new file mode 100644 index 0000000000..2a60bd201e --- /dev/null +++ b/apps/web/src/locales/zh-cn/slack.json @@ -0,0 +1,39 @@ +{ + "agentDisabled": "{{agent}} — 已禁用", + "agentNotConfigured": "{{agent}} — 未配置", + "agentUsesDefaultModel": "{{name}}(使用默认模型)", + "agentWithModel": "{{name}}({{model}})", + "chooseUtilityAgent": "选择实用代理", + "commandPrefix": "命令前缀", + "commandPrefixHelp": "你在 Slack 中发送的消息如果以此前缀开头,就会成为 Kandev 任务。默认值:", + "configurationRemoved": "已移除 Slack 配置", + "configurationSaved": "已保存 Slack 配置", + "connectedAs": "已连接为 {{name}}", + "connectedAsWithTeam": "已连接为 {{name}}({{team}})", + "cookieValue": "{{cookie}} Cookie 值", + "createUtilityAgentFirst": "请先在“设置 → 实用代理”中创建一个", + "credentialsHelp": "在浏览器中打开 Slack,复制 `{{cookie}}` Cookie 的值和本地存储中的 `{{prefix}}` 令牌。两者都必填。", + "deleteFailed": "删除失败:{{error}}", + "failedToLoadConfig": "加载 Slack 配置失败:{{error}}", + "integrationDescription": "将 Slack 话题串收集为所选工作区的任务。在你可见的任意话题串中输入 {{example}},配置的实用代理便会创建任务。", + "integrationTitle": "Slack 集成", + "loadingEllipsis": "正在加载…", + "pasteCredentialsToTest": "粘贴令牌和 Cookie 以测试连接", + "pollingInterval": "轮询间隔(秒)", + "pollingIntervalHelp": "检查 Slack 中新 <1>{{prefix}} 消息的频率。值越小响应越快,值越大调用 Slack API 的次数越少。范围:{{min}}–{{max}} 秒。默认值:{{fallback}} 秒。", + "promptTokensHelp": "自定义提示词可以引用 <1>{{tokens}}。使用至少一个令牌时,模板将定义完整提示词;否则会自动在前面加上默认的 Slack 分流系统提示词。", + "removeConfiguration": "移除配置", + "removeConfigurationConfirm": "移除 Slack 配置?", + "removing": "正在移除...", + "saveFailed": "保存失败:{{error}}", + "savedLeaveBlank": "(已保存——留空可保持不变)", + "secretsRequired": "会话令牌和 Cookie 均为必填项。", + "sessionToken": "会话令牌({{prefix}}…)", + "testConnection": "测试连接", + "testFailed": "失败:{{error}}", + "testing": "正在测试...", + "triageAgent": "分流代理", + "triageAgentHelp": "用于解读每条 Slack 消息并创建 Kandev 任务的实用代理。运行时会接入 Kandev MCP 工具({{tools}}),以便根据上下文选择目标 Kandev 工作区、工作流和仓库。内置代理使用<4>设置 → 实用代理中的默认模型。", + "triageAgentRequired": "必须选择分流代理。", + "unsupportedAuthWarning": "<0>浏览器会话身份验证(不受支持):Slack 经常轮换会话 Cookie,因此身份验证过期后可能需要重新连接。机器人安装和用户 OAuth 已列入路线图。" +} diff --git a/apps/web/src/locales/zh-cn/system.json b/apps/web/src/locales/zh-cn/system.json new file mode 100644 index 0000000000..7b1506a51f --- /dev/null +++ b/apps/web/src/locales/zh-cn/system.json @@ -0,0 +1,453 @@ +{ + "aboutArch": "架构", + "aboutBuildTime": "构建时间", + "aboutCommit": "提交", + "aboutDocumentation": "文档", + "aboutGoVersion": "Go 版本", + "aboutLoading": "加载中…", + "aboutOs": "操作系统", + "aboutPageDescription": "版本、构建元数据与链接。", + "aboutReportIssue": "报告问题", + "aboutTitle": "关于 kandev", + "aboutVersion": "版本", + "actionDone": "完成", + "actionFailed": "失败", + "actionRunning": "运行中…", + "backupsColumnActions": "操作", + "backupsColumnCreated": "创建时间", + "backupsColumnKind": "类型", + "backupsColumnName": "名称", + "backupsColumnSize": "大小", + "backupsCreate": "创建快照", + "backupsCreateFailed": "创建快照失败", + "backupsCreateTimeout": "创建快照未在 {{seconds}} 秒内完成", + "backupsCreating": "正在创建…", + "backupsDeleteFailed": "删除失败", + "backupsDeleteLabel": "删除 {{name}}", + "backupsDownloadLabel": "下载 {{name}}", + "backupsEmpty": "尚无备份。版本升级时自动创建,或通过上方按钮手动创建的快照会显示在这里。", + "backupsHelp": "备份是 Kandev 数据库(任务、会话、设置)的完整副本。每次版本升级前 Kandev 会自动创建一份,以便出错时回滚。你也可以在出厂重置等风险操作前手动创建。", + "backupsKindAuto": "自动", + "backupsKindManual": "手动", + "backupsLoading": "正在加载备份…", + "backupsPageDescription": "存储在 {{path}} 下的 {{command}} 快照。", + "backupsRestoreLabel": "恢复 {{name}}", + "backupsRetention_one": "磁盘上仅保留最新一份备份。", + "backupsRetention_other": "磁盘上仅保留最新 {{count}} 份备份。", + "backupsTitle": "备份", + "changelogEmpty": "暂无更新日志条目。", + "changelogViewOnGitHub": "在 GitHub 上查看", + "close": "关闭", + "createUserDescription": "使用你设置的密码直接创建账户。若希望用户自选密码,请改用邀请链接。", + "createUserDisplayName": "显示名称", + "createUserEmail": "邮箱", + "createUserFailed": "无法创建用户。", + "createUserPassword": "密码", + "createUserRole": "角色", + "createUserSubmit": "创建用户", + "createUserSubmitting": "正在创建…", + "createUserTitle": "添加用户", + "databaseDriver": "驱动", + "databaseFactoryReset": "出厂重置", + "databaseFactoryResetHelp": "清除全部 Kandev 数据:任务、会话、设置、仓库与 worktree。无法撤销。重置前会先创建备份,但恢复需在重置后使用备份页面。仅在你希望从头开始时使用。", + "databaseLastBackup": "上次备份", + "databaseLoading": "正在加载数据库统计…", + "databaseMaintenanceUnavailable": "SQLite 维护操作对 {{driver}} 不可用。", + "databaseNever": "从未", + "databaseOptimize": "优化", + "databaseOptimizeHelp": "让数据库刷新内部索引,使常见查询保持快速。大量删除数据后或应用感觉变慢时运行。快速且安全 — 不更改任何数据。", + "databasePageDescription": "数据库驱动、大小与可用维护控件。", + "databasePath": "路径", + "databaseRunOptimize": "运行优化", + "databaseRunVacuum": "运行 vacuum", + "databaseSchemaVersion": "架构版本", + "databaseSize": "大小", + "databaseSizeLong": "数据库大小", + "databaseTitle": "数据库", + "databaseVacuum": "Vacuum", + "databaseVacuumHelp": "回收数据库文件中的未用空间。删除任务或会话后文件可能随时间增大;vacuum 会将其压缩。随时可安全运行 — 不更改任何数据。若数据库看起来很大,可偶尔运行一次。", + "databaseWal": "WAL", + "databaseWalHelp": "预写日志。SQLite 先将新更改写入此附属文件,再随时间合并到主数据库。非零 WAL 大小是正常的 — 负载下会暂时增大,检查点时会缩小。运行 vacuum 会将其重置。", + "databaseWhatIs": "什么是 {{label}}?", + "desktopUpdateFailed": "桌面更新失败", + "diskCalculating": "正在计算…", + "diskColumnPath": "路径", + "diskColumnSize": "大小", + "diskComputedAt": "计算于 {{at}}", + "diskOpen": "打开", + "diskRefresh": "刷新", + "diskRefreshed": "已刷新", + "diskRefreshHelp": "遍历数据文件夹中的每个目录并累计文件大小。结果会缓存几小时;点击可立即重新计算。大型工作区可能需要几秒。", + "diskRefreshing": "正在刷新…", + "diskRowBackups": "备份", + "diskRowDataDir": "数据目录", + "diskRowQuickChat": "快速聊天", + "diskRowRepos": "仓库", + "diskRowSessions": "会话", + "diskRowTasks": "任务", + "diskRowWorktrees": "Worktree", + "diskTotal": "总计", + "diskUnmeasuredDirs": "部分目录无法测量:", + "diskUsageTitle": "磁盘用量", + "dismiss": "关闭", + "factoryResetCompleteBody": "Kandev 数据已清除。请退出并重新启动应用以全新开始 — 正在运行的后端仍持有现已清空数据库的连接,在重启前不会重建缓存。若需回滚,可在备份页面使用重置前快照。", + "factoryResetCompleteTitle": "出厂重置完成", + "factoryResetFailed": "重置失败", + "factoryResetPlaceholder": "输入 {{token}} 以确认", + "factoryResetRequestFailed": "重置请求失败", + "factoryResetSnapshot": "擦除前会自动在 <1>{{path}} 下创建快照,若改变主意可从备份页面恢复。", + "factoryResetTitle": "出厂重置", + "factoryResetTypeToConfirm": "输入 <1>{{token}} 以启用确认按钮。擦除完成后需要你退出并重新启动 Kandev — 后端不会自动重启。", + "factoryResetWipes": "这将清除整个 kandev 安装:数据库、worktree、仓库克隆、会话、任务与快速聊天会话。", + "factoryResetWiping": "正在清除数据…", + "featureToggleDebug": "调试", + "featureToggleEnv": "环境变量:{{envVar}}", + "featureToggleEnvControlled": "由启动环境控制", + "featureToggleExperimental": "实验性", + "featureTogglePendingRestart": "待重启", + "featureToggleRequiresRestart": "需要重启", + "featureTogglesLoadError": "无法加载功能开关。", + "featureTogglesLoadFailed": "加载功能开关失败", + "featureTogglesLoading": "正在加载功能开关…", + "featureToggleSource": "来源:{{source}}", + "featureToggleSourceDefault": "默认", + "featureToggleSourceEnv": "环境变量", + "featureToggleSourceOverride": "已保存的覆盖", + "featureTogglesPageDescription": "管理 Kandev 功能与诊断开关。", + "featureTogglesRetry": "重试", + "featureTogglesSaved": "功能开关已保存", + "featureToggleSwitchLabel": "切换 {{label}}", + "featureToggleUseDefault": "使用默认值", + "healthAllPass": "所有系统检查通过。", + "healthHealthy": "健康", + "healthIssue": "问题", + "healthIssueCount_one": "{{count}} 个问题", + "healthIssueCount_other": "{{count}} 个问题", + "healthLoading": "正在加载健康检查…", + "healthPassing": "通过", + "healthSeverityError": "错误", + "healthSeverityInfo": "信息", + "healthSeverityWarning": "警告", + "healthSystemChecks": "系统检查", + "healthTitle": "健康状态", + "healthWhatsMonitored": "监控内容", + "inviteCopied": "邀请链接已复制", + "inviteCopy": "复制邀请链接", + "inviteCreate": "创建邀请链接", + "inviteCreating": "正在创建…", + "inviteDescription": "生成一次性邀请链接。邮箱留空则任何人可用所选角色注册;填写邮箱则将邀请绑定到该地址。", + "inviteDone": "完成", + "inviteEmailOptional": "邮箱(可选)", + "inviteFailed": "无法创建邀请。", + "inviteReadyDescription": "将此链接分享给受邀者。仅显示一次 — 请立即复制。", + "inviteReadyTitle": "邀请链接已就绪", + "inviteTitle": "邀请用户", + "inviteUrlLabel": "邀请链接", + "jobStateDone": "完成", + "jobStateFailed": "失败", + "jobStateQueued": "已排队", + "jobStateRunning": "运行中", + "licensesCount": "{{visible}} / {{total}}", + "licensesEmpty": "没有匹配筛选的条目。", + "licensesFilterPlaceholder": "按名称、许可证或生态筛选…", + "licensesPageDescription": "随 kandev 发布的每个 npm 与 Go 依赖的开源许可证。", + "licensesSource": "来源", + "licensesStaleWarning": "因生成失败,Go 许可证条目复用了已提交的清单。在 go-licenses 正常时请重新生成以刷新 Go 依赖数据。", + "licensesTitle": "第三方许可证", + "licensesToggleLabel": "显示 {{name}} 的许可证文本", + "messageQueueAdminOnly": "只有管理员可以更改此设置。", + "messageQueueConfigured": "已配置:", + "messageQueueDescription": "控制每个会话在新工作被拒绝前可以容纳多少条待处理消息。", + "messageQueueEffective": "当前生效:", + "messageQueueEffectiveHelp": "当前生效值会立即用于新的接纳操作。降低此值不会清理已有的排队条目。", + "messageQueueEnvironmentLockTitle": "由环境管理", + "messageQueueEnvironmentLocked": "{{variable}} 会覆盖已保存的设置。请更改环境值并重启 Kandev,之后才能在此处再次编辑。", + "messageQueueLimitDescription": "此限制会立即应用于新接纳的消息。降低限制不会移除已在等待的消息,已接受的投递重试仍然安全。", + "messageQueueLimitTitle": "每会话队列上限", + "messageQueueLoadFailed": "无法加载消息队列设置。", + "messageQueueLoading": "正在加载消息队列设置…", + "messageQueueMaximumLabel": "每个会话的最大消息数", + "messageQueueRetry": "重试", + "messageQueueSaveFailed": "保存消息队列设置失败。", + "messageQueueSourceDefault": "默认值", + "messageQueueSourceEnvironment": "环境", + "messageQueueSourceSetting": "已保存的设置", + "messageQueueTitle": "消息队列", + "messageQueueUnlimited": "无限制", + "messageQueueUnlimitedHelp": "设置为 0 表示无限制。正整数表示一个会话中待处理消息达到该数量后拒绝新消息。", + "messageQueueValidation": "请输入大于或等于 0 的整数。", + "navAbout": "关于", + "navBackups": "备份", + "navDatabase": "数据库", + "navFeatureToggles": "功能开关", + "navLicenses": "许可证", + "navLogs": "日志", + "navStorage": "存储", + "navUpdates": "更新", + "navUsers": "用户", + "reloadPage": "重新加载页面", + "restartAction": "重启", + "restartManualHint": "方便时从终端或服务管理器重启。", + "restartPendingChanges": "已保存的开关更改将在下次启动 Kandev 时生效。", + "restartPhaseDoneBody": "后端已重新运行。请重新加载页面以使用最新功能开关状态重新连接。", + "restartPhaseDoneTitle": "Kandev 已重启", + "restartPhaseErrorBody": "无法完成重启。", + "restartPhaseErrorTitle": "重启失败", + "restartPhaseRestartingBody": "Kandev 正在停止并重新启动。此页面会自动检测新进程。", + "restartPhaseRestartingTitle": "正在重启 Kandev", + "restartPhaseStartingBody": "正在准备本地监督程序重启请求。", + "restartPhaseStartingTitle": "请求重启", + "restartRequired": "需要重启", + "restartStartFailed": "重启 Kandev 失败", + "restartSupportDetails": "重启支持详情", + "restartSupportedHelp": "当 Kandev 在受支持的本地监督程序下运行时,可从此页面重启。", + "restartTimedOut": "重启耗时超过预期。请刷新以查看当前状态。", + "restartUnsupportedHelp": "在部署预览、非托管终端运行或不含重启监督程序的启动模式下,自动重启不可用。", + "restoreAction": "恢复", + "restoreBody": "将 <1>{{name}} 覆盖到当前数据库。暂存副本就绪后,需要你退出并重新启动 Kandev 以加载新数据 — 后端不会自动重启。", + "restoreCompleteBody": "<0>{{name}} 已写入当前数据库。请退出并重新启动 Kandev 以加载恢复的数据 — 正在运行的后端仍持有旧版本连接,重启前会提供过期行。", + "restoreCompleteTitle": "恢复完成", + "restoreFailed": "恢复失败", + "restorePlaceholder": "输入 {{token}} 以确认", + "restoreRequestFailed": "恢复请求失败", + "restoreTitle": "恢复快照", + "restoreTypeToConfirm": "输入 <1>{{token}} 以启用确认按钮。", + "restoreWriting": "正在将快照写入实时数据库…", + "selfUpdateDone": "已更新到 {{version}}。", + "selfUpdateDoneUnknown": "已更新到最新版本。", + "selfUpdateFailed": "更新失败。", + "selfUpdateHelperFailed": "更新助手未能启动。请检查服务日志。", + "selfUpdateInstalling": "正在下载并安装 {{version}}…", + "selfUpdateInstallingUnknown": "正在下载并安装新版本…", + "selfUpdateKeepOpen": "请保持此页面打开。更新完成后会自动刷新。", + "selfUpdateRestarting": "正在重启 Kandev — 最多可能需要一分钟。", + "selfUpdateStartFailed": "启动更新失败", + "selfUpdateStarting": "正在开始更新到 {{version}}…", + "selfUpdateStartingUnknown": "正在开始更新到新版本…", + "selfUpdateTimedOut": "更新耗时超过预期。请刷新以查看当前版本。", + "settingsRouteNotPorted": "此设置路由由 SPA shell 处理,但其专用客户端页面仍在移植中:<1>{{pathname}}", + "statusPageDescription": "健康检查、磁盘用量与版本摘要。", + "storageAcknowledgeDaemon": "确认守护进程", + "storageAcknowledgeDedicatedFirst": "请先确认专用 Docker 守护进程。", + "storageActionFailed": "存储操作失败", + "storageActionPending": "请等待当前存储操作完成。", + "storageActionsDescription": "分析可得到只读快照;若要立即回收空间,可运行已启用的清理规则。", + "storageActionsTitle": "安全回收磁盘空间", + "storageAdoptCache": "接管缓存", + "storageAdoptDialogDescription": "将允许 Kandev 轮换并隔离位于 {{path}} 的现有缓存。路径必须为绝对路径,且与 Kandev 回收站位于同一文件系统。", + "storageAdoptDialogFallbackPath": "所选路径", + "storageAdoptDialogTitle": "接管外部 Go 构建缓存", + "storageAdoptionPending": "请等待 Go 缓存接管完成。", + "storageAnalysisComplete": "分析完成", + "storageAnalysisDescription": "当前用量与可回收空间的只读分解。偶尔运行「分析」以刷新估算;它从不删除或移动任何内容。", + "storageAnalysisFailed": "分析失败", + "storageAnalysisRunning": "存储分析仍在运行。", + "storageAnalysisTitle": "存储分析", + "storageAnalyze": "分析", + "storageAnalyzing": "正在分析…", + "storageBuildCacheKeepHelp": "清理符合条件的记录时,Docker 大约保留这么多构建缓存。值越大可保留更多可复用层,但回收空间更少。", + "storageBuildCacheKeepLabel": "保留的构建缓存(GB)", + "storageBuildCacheUnusedHelp": "仅未使用时长超过此阈值的构建缓存记录符合条件。增大该值可更久地保护近期构建层。", + "storageBuildCacheUnusedLabel": "构建缓存须未使用(小时)", + "storageBusyForceHint": "仍运行清理可能会中断这些活动工作。", + "storageBusyIntro": "清理可能会中断以下工作:", + "storageBusyTitle": "存储清理发现活动中的 Kandev 工作", + "storageCheckIntervalHelp": "Kandev 检查是否应执行计划维护的频率。发现 Kandev 忙碌的检查会被跳过,并在下一间隔重试。", + "storageCheckIntervalLabel": "检查间隔(小时)", + "storageCleanContainersAria": "清理 Kandev 容器", + "storageCleanDockerBuildCacheAria": "清理 Docker 构建缓存", + "storageCleanGoCache": "清理 Go 缓存", + "storageCleaning": "正在清理…", + "storageCleanOrphanWorkspacesAria": "清理孤立任务工作区", + "storageCleanUnusedImagesAria": "清理未使用的 Docker 镜像", + "storageCleanupComplete": "清理完成", + "storageCleanupFailed": "清理失败", + "storageCleanupRunning": "存储清理仍在运行。", + "storageClearEligible": "清除符合条件的项", + "storageClearEligibleDialogTitle": "清除符合条件的隔离项", + "storageContainersDescription": "移除由 Kandev 创建并标记、且已停止未使用的容器。", + "storageContainersHelp": "仅考虑标记为 Kandev 管理的已停止容器,且清单须确认不再需要。运行中的容器与无关 Docker 容器绝不会被此选项移除。", + "storageDedicatedDaemon": "专用 Docker 守护进程", + "storageDedicatedDaemonDescription": "确认无关工作负载不共享此 Docker 守护进程。", + "storageDedicatedDaemonHelp": "构建缓存与镜像所有权无法可靠归属到 Kandev。此确认会解锁守护进程范围的清理,仅当配置的 Docker 守护进程专用于 Kandev 时才应启用。Kandev 从不执行卷范围的 prune。", + "storageDedicatedDialogDescription": "构建缓存与未使用镜像清理会影响整个配置的守护进程,包括 Kandev 以外创建的资源。仅在守护进程专用于此安装时确认。", + "storageDedicatedDialogTitle": "使用此专用 Docker 守护进程", + "storageDefaultDockerHost": "默认 Docker 主机", + "storageDelete": "删除", + "storageDeleteDialogDescription": "此操作无法撤销。Kandev 将永久移除 {{path}}。", + "storageDeleteDialogFallbackPath": "所选隔离条目", + "storageDeleteDialogTitle": "永久删除隔离数据", + "storageDeletePermanently": "永久删除", + "storageDeletionComplete": "删除完成", + "storageDescription": "查看磁盘用量,并在安装需要时从 Kandev 拥有的工作区、缓存与 Docker 资源回收空间。", + "storageDockerBuildCache": "Docker 构建缓存", + "storageDockerBuildCacheDescription": "移除旧构建缓存,同时保留配置的量。", + "storageDockerBuildCacheHelp": "使用 Docker 的年龄与存储过滤器移除旧构建缓存。不会运行 docker system prune,不会全局移除卷,且在确认专用守护进程前保持禁用。", + "storageDockerDescription": "可选的守护进程范围清理。仅当此 Docker 守护进程专用于 Kandev 时启用。", + "storageDockerImageLayers": "Docker 镜像层", + "storageDockerTitle": "Docker 清理", + "storageDockerUnavailable": "配置主机上 Docker 不可用。", + "storageDockerUnavailableBadge": "Docker 不可用", + "storageDockerUnavailableNotice": "Docker 不可用;此主机无法运行 Docker 清理选项。", + "storageDockerUnmeasured": "无法测量 Docker 用量", + "storageEligibleNow": "当前符合条件", + "storageEnableGoCacheFirst": "请先启用托管的 Go 缓存。", + "storageEnableManagedGoCacheAria": "启用托管 Go 缓存", + "storageEnterCachePathFirst": "请先输入绝对缓存路径。", + "storageExternalGoCache": "外部 Go 缓存", + "storageExternalGoCacheDescription": "可选允许 Kandev 维护托管路径外的现有主机缓存。", + "storageExternalGoCacheHelp": "接管授予 Kandev 轮换此现有缓存的明确权限。除非你接管该精确绝对路径并确认破坏性访问,Kandev 从不清扫默认用户缓存或其他路径。", + "storageForceClearAll": "强制全部清除", + "storageForceClearDescription": "尝试立即永久移除每个隔离项,成功的条目无法撤销。失败的条目可能仍可见并可重试。", + "storageForceClearDialogTitle": "强制清除全部隔离", + "storageGoBuildCache": "Go 构建缓存", + "storageGoCacheBelowLimit": "Go 构建缓存低于配置的大小限制。", + "storageGoCacheMaxHelp": "这是清理触发阈值,不是硬配额。任务活动时缓存可能超过此大小。维护会在超过限制后将所属缓存轮换到隔离区并重建空缓存。", + "storageGoCacheMaxLabel": "最大缓存大小(GB)", + "storageGoCacheNotOwned": "仅可清理 Kandev 拥有的 Go 构建缓存。", + "storageGoCacheSectionDescription": "为新的主机本地执行使用并修剪 Kandev 拥有的缓存。", + "storageHistoryEmpty": "尚无存储维护运行记录。", + "storageHistoryLoading": "正在加载维护记录…", + "storageIdleHelp": "计划清理仅在没有任何任务、shell 命令、测试、设置、清理或镜像构建使用托管资源这么久之后才启动。「立即运行」不等待此计时器,但资源活动时仍会拒绝运行。", + "storageIdleLabel": "要求空闲(分钟)", + "storageKandevContainers": "Kandev 容器", + "storageLastAnalyzed": "上次分析 {{time}}", + "storageLoadingData": "正在加载存储数据…", + "storageMaintenanceHistory": "维护历史", + "storageManagedContainerCount_one": "{{count}} 个托管容器", + "storageManagedContainerCount_other": "{{count}} 个托管容器", + "storageManagedGoCache": "托管 Go 缓存", + "storageManagedGoCacheDescription": "新的主机本地执行使用 {{path}}。", + "storageManagedGoCacheHelp": "启用后,Kandev 为新的本地任务进程提供专用 Go 构建缓存,并在超过最大值时于维护期间轮换。关闭后新执行不再使用托管缓存,但不会删除它。", + "storageMoreInformationAbout": "关于 {{label}} 的更多信息", + "storageNoEligibleEntries": "尚无符合条件的隔离条目。", + "storageOrphanGraceHelp": "工作区须至少闲置这么久才可归类为孤立。增大该值会让旧工作区在进入隔离前保留更久。", + "storageOrphanGraceLabel": "孤立前等待(小时)", + "storageOrphanWorkspaces": "孤立任务工作区", + "storageOrphanWorkspacesDescription": "将已确认的孤立工作区移至隔离区。", + "storageOrphanWorkspacesHelp": "仅当清单确认没有活动任务、环境、会话或受保护 worktree 使用时,Kandev 才选择任务工作区。工作区先移至隔离区,可在永久删除前恢复。", + "storagePolicyDescription": "选择 Kandev 可自动回收的内容及其须遵守的安全限制。", + "storagePolicyLoadingBlock": "请等待存储设置加载完成。", + "storagePolicyTitle": "维护策略", + "storageProtectedUntil": "保护至 <1>{{deadline}}", + "storagePurgeEligibleCount_one": "这将永久移除 {{count}} 个符合条件的项。", + "storagePurgeEligibleCount_other": "这将永久移除 {{count}} 个符合条件的项。", + "storagePurgeProtectedCount_one": "{{count}} 个受保护项在保留期限前仍会保留。", + "storagePurgeProtectedCount_other": "{{count}} 个受保护项在保留期限前仍会保留。", + "storageQuarantineCardDescription": "清理会先将可恢复数据移到此处,而不是立即删除。若仍需要可恢复,确定不需要时可永久删除。", + "storageQuarantineCleanupRunning": "隔离清理仍在运行。", + "storageQuarantinedResources": "已隔离资源", + "storageQuarantineEligibleCount_one": "当前符合条件 {{count}} 项", + "storageQuarantineEligibleCount_other": "当前符合条件 {{count}} 项", + "storageQuarantineEmpty": "没有可恢复的隔离资源。", + "storageQuarantineHeading": "隔离区", + "storageQuarantineHelp": "隔离区是 Kandev 的可恢复暂存区。孤立任务工作区与轮换的 Go 缓存在删除前会移到这里。保留期内可恢复;期限过后,后续维护可能会永久删除。", + "storageQuarantineLoading": "正在加载隔离区…", + "storageQuarantineMovedAside_one": "{{count}} 项已暂存以便在永久删除前恢复", + "storageQuarantineMovedAside_other": "{{count}} 项已暂存以便在永久删除前恢复", + "storageQuarantineProtectedCount_one": "{{count}} 项受保护", + "storageQuarantineProtectedCount_other": "{{count}} 项受保护", + "storageQuarantineRetentionHelp": "清理会先将孤立工作区与轮换的 Go 缓存移入 Kandev 回收站,而不是立即删除。保留期内可恢复;期限过后,后续维护可能会永久删除。", + "storageQuarantineRetentionLabel": "隔离项保留(小时)", + "storageQuarantineSafetyDescription": "在永久删除前,将可恢复资源保留一段宽限期。", + "storageQuarantineSafetyTitle": "隔离安全", + "storageQuarantineScheduleOff": "自动隔离清理已关闭。准备好时请手动运行维护或使用隔离操作。", + "storageQuarantineScheduleOn_one": "符合条件的条目会在期限后的首次成功维护中移除(空闲门控允许时每 {{count}} 小时)。", + "storageQuarantineScheduleOn_other": "符合条件的条目会在期限后的首次成功维护中移除(空闲门控允许时每 {{count}} 小时)。", + "storageQuarantineTotal": "隔离区总量:{{size}}", + "storageQuarantineUnmeasured": "无法测量隔离区用量", + "storageRefreshFailed": "刷新存储数据:{{message}}", + "storageResourceTypeGoCache": "go 缓存", + "storageResourceTypeTaskWorkspace": "任务工作区", + "storageRestore": "恢复", + "storageRetentionEnds": "保留期结束于 {{deadline}}。", + "storageRunAnyway": "仍然运行", + "storageRunNow": "立即运行", + "storageScheduleDescription": "控制何时允许启动自动维护。计划关闭时手动操作仍可用。", + "storageScheduledMaintenance": "计划维护", + "storageScheduledMaintenanceDescription": "使用已启用的资源规则定期回收磁盘空间。", + "storageScheduledMaintenanceHelp": "启用后,Kandev 按配置间隔检查此策略,并仅在空闲达到要求后启动。关闭不会禁用「分析」或「立即运行」。", + "storageScheduleTitle": "计划", + "storageSectionUnavailable": "无法加载此存储区域", + "storageTaskWorkspaces": "任务工作区", + "storageTitle": "存储", + "storageToastAnalysisStarted": "已开始存储分析", + "storageToastCacheAdopted": "已接管 Go 构建缓存", + "storageToastDeletionStarted": "已开始永久删除", + "storageToastEligiblePurgeStarted": "已开始符合条件的隔离清理", + "storageToastForcedPurgeStarted": "已开始强制隔离清理", + "storageToastMaintenanceStarted": "已开始存储维护", + "storageToastPolicySaved": "存储策略已保存", + "storageToastQuarantineRestored": "已恢复隔离资源", + "storageTotalCounted": "已统计总量:{{size}}", + "storageTotalPartial": "部分测量结果", + "storageTrashPath": "回收站:{{path}}", + "storageTypeToConfirm": "输入 <1>{{phrase}} 以继续。", + "storageTypeToConfirmAria": "输入 {{phrase}} 以确认", + "storageUnavailableValue": "不可用", + "storageUnusedDockerImages": "未使用的 Docker 镜像", + "storageUnusedImagesDescription": "移除没有任何容器使用的旧镜像。", + "storageUnusedImagesDetail": "无任何容器使用且超过配置年龄", + "storageUnusedImagesHelp": "仅当没有运行中或已停止的容器引用该镜像,且其年龄超过配置值时才移除。这是守护进程范围的操作,因此需要专用守护进程确认。", + "storageUnusedImagesHoursHelp": "镜像须对所有容器未使用且超过此年龄后,Kandev 才可移除。增大该值会更久保留旧镜像。", + "storageUnusedImagesHoursLabel": "镜像须未使用(小时)", + "storageUserGoBuildCache": "用户 Go 构建缓存", + "storageWorkspacesDescription": "回收 Kandev 能明确识别为不再使用的资源。", + "storageWorkspacesDetail": "宽限期后可回收 {{reclaimable}} · 活动 {{active}}", + "storageWorkspacesTitle": "工作区与容器", + "uiStateHelp": "清除保存在浏览器中的布局与 UI 偏好(面板大小、固定任务、展开分组等)。若应用显示异常、面板卡住或布局不对,可使用此选项。任务、会话与服务器数据不受影响 — 仅按浏览器的 UI 状态。重置后页面会重新加载。", + "uiStateReset": "重置", + "uiStateResetLabel": "重置浏览器布局", + "uiStateTitle": "UI 状态", + "updateAvailable": "有可用更新", + "updatesApply": "应用更新", + "updatesApplyConfirmDesktop": "Kandev 将安装 {{version}} 并重启桌面应用。", + "updatesApplyConfirmService": "Kandev 将更新到 {{version}},重新安装用户服务并重启。", + "updatesApplyConfirmTitle": "应用更新?", + "updatesCheckNow": "立即检查", + "updatesCurrentVersion": "当前版本", + "updatesDownloading": "正在下载更新({{downloaded}} 字节)…", + "updatesDownloadingOfTotal": "正在下载更新({{downloaded}} / {{total}} 字节)…", + "updatesInstalling": "正在安装更新…", + "updatesLastChecked": "上次检查 {{at}}", + "updatesLatestRelease": "最新发布", + "updatesNever": "从未", + "updatesNotificationsHint": "通知偏好在 <2>通知 中管理。", + "updatesPageDescription": "当前版本与最新发布,以及完整 kandev 更新日志。", + "updatesReleaseNotes": "发布说明", + "updatesRetryAfter_one": "已检查过。请在 {{count}} 秒后重试。", + "updatesRetryAfter_other": "已检查过。请在 {{count}} 秒后重试。", + "updatesTitle": "更新", + "updatesUpToDate": "Kandev 已是最新。", + "usersAddUser": "添加用户", + "usersColumnActions": "操作", + "usersColumnEmail": "邮箱", + "usersColumnName": "名称", + "usersColumnRole": "角色", + "usersColumnStatus": "状态", + "usersConfirm": "确认", + "usersConfirmDisable": "禁用 {{email}}?他们将在所有位置被登出。", + "usersConfirmEnable": "重新启用 {{email}}?", + "usersConfirmRoleChange": "将 {{email}} 改为 {{role}}?", + "usersDisable": "禁用", + "usersEmpty": "尚无用户。", + "usersEnable": "启用", + "usersInviteLink": "邀请链接", + "usersLastAdminGuard": "此部署必须至少保留一名活动管理员。", + "usersLoadFailed": "加载用户失败。", + "usersLoading": "正在加载用户…", + "usersMakeAdmin": "设为管理员", + "usersMakeMember": "设为成员", + "usersPageDescription": "管理此实例的账户、角色与邀请链接。", + "usersRoleAdmin": "管理员", + "usersRoleMember": "成员", + "usersStatusActive": "活动", + "usersStatusDisabled": "已禁用", + "usersTakesEffectImmediately": "{{email}} — 立即生效。", + "usersTitle": "用户", + "usersUpdateFailed": "无法更新用户", + "versionCurrent": "当前", + "versionLatest": "最新", + "versionTitle": "版本", + "versionViewUpdates": "查看更新" +} diff --git a/apps/web/src/locales/zh-cn/workflows.json b/apps/web/src/locales/zh-cn/workflows.json new file mode 100644 index 0000000000..62213c636e --- /dev/null +++ b/apps/web/src/locales/zh-cn/workflows.json @@ -0,0 +1,242 @@ +{ + "aConfiguredRepository": "已配置的仓库", + "addCondition": "添加条件", + "adding": "正在添加…", + "addWorkflow": "添加工作流", + "agentProfile": "代理配置", + "agentProfileHelp": "此工作流中任务的默认代理配置。设置后,任务创建对话框中的代理选择器将被锁定。", + "allowManualMove": "允许手动移动", + "allowManualMoveHelp": "允许在看板上将任务拖入此步骤。", + "applyAnyway": "仍然应用", + "autoArchive": "自动归档", + "autoArchiveAfter": "在", + "autoArchiveHelp": "任务在此步骤停留设定小时数后自动归档。适用于工作流最后一步(例如 Done),以保持看板整洁。", + "autoArchiveHours": "小时后", + "automaticWorkflowCycle": "自动工作流循环", + "autoStartAgent": "自动启动代理", + "autoStartAgentHelp": "任务进入此步骤时自动启动代理。", + "autoSync": "自动同步", + "autoSyncOff": "自动同步已关闭", + "autoTransition": "自动流转", + "childrenCompletedHelp": "用于父任务步骤。当每个活动的直接子任务均为 COMPLETED、FAILED 或 CANCELLED 时,Kandev 运行此流转一次。已归档与临时子任务会被忽略。孙任务不计入;若父任务没有子任务则不运行。", + "childrenCompletedHelpAria": "子任务完成流转的工作方式", + "chooseAgentFamily": "选择代理系列", + "chooseModelAndSessionSettings": "选择模型和会话设置", + "close": "关闭", + "color": "颜色", + "colorBlue": "蓝色", + "colorCyan": "青色", + "colorGray": "灰色", + "colorGreen": "绿色", + "colorIndigo": "靛色", + "colorOrange": "橙色", + "colorPurple": "紫色", + "colorRed": "红色", + "colorYellow": "黄色", + "configureOriginalSessionOptions": "配置原会话设置选项", + "configureOriginalSessionOptionsDescription": "仅当现有对话标签页的初始代理系列与某条规则匹配时,才更改该标签页。", + "confirmWorkflowCycle": "确认工作流循环", + "confirmWorkflowCycleDescription": "继续前请审阅重复的代理运行。", + "copied": "已复制", + "copy": "复制", + "createAnyway": "仍然创建", + "customAgentProfile": "自定义代理配置", + "customTemplate": "自定义", + "customTemplateDescription": "从零创建你自己的代理工作流。", + "cycle": "循环", + "delete": "删除", + "deleteAndArchiveTasks": "删除并归档任务", + "deleteStep": "删除步骤", + "deleteStepAffectsTasks_one": "删除 {{stepName}} 还将影响其 {{count}} 个任务。", + "deleteStepAffectsTasks_other": "删除 {{stepName}} 还将影响其 {{count}} 个任务。", + "deleteStepAndTasks": "删除步骤及任务", + "deleteStepDialogTitle": "删除步骤", + "deleteStepNoTasks": "这将永久删除 {{stepName}} 工作流步骤。", + "deleteStepWithMigration_one": "{{stepName}} 有 {{count}} 个任务。选择迁移目标,或删除该步骤及其任务。", + "deleteStepWithMigration_other": "{{stepName}} 有 {{count}} 个任务。选择迁移目标,或删除该步骤及其任务。", + "deleteWorkflow": "删除工作流", + "deleteWorkflowDialogTitle": "删除工作流", + "deleteWorkflowNoTasks": "这将永久删除工作流及其所有步骤。", + "deleteWorkflowWithTasks_one": "此工作流有 {{count}} 个任务。选择迁移目标,或删除工作流并归档任务。", + "deleteWorkflowWithTasks_other": "此工作流有 {{count}} 个任务。选择迁移目标,或删除工作流并归档任务。", + "directoryLine": "目录 {{path}}", + "disablePlanMode": "禁用计划模式", + "disablePlanModeOnComplete": "完成时禁用计划模式", + "disablePlanModeOnCompleteHelp": "代理完成一轮后关闭计划模式,即使任务仍留在此步骤。", + "disablePlanModeOnExitHelp": "在此步骤的每一轮保持计划模式开启,仅当任务移到其他步骤时关闭。", + "doNothing": "不执行任何操作", + "doNothingWaitForUser": "不执行任何操作(等待用户)", + "everySeconds_one": "每 {{count}} 秒", + "everySeconds_other": "每 {{count}} 秒", + "export": "导出", + "exportAll": "全部导出", + "exportWorkflowsTitle": "导出工作流", + "exportWorkflowTitle": "导出工作流", + "failedToAddWorkflowStep": "添加工作流步骤失败", + "failedToCheckWorkflowStepTasks": "检查工作流步骤任务失败", + "failedToCheckWorkflowTasks": "检查工作流任务失败", + "failedToDeleteStep": "删除步骤失败", + "failedToDeleteWorkflow": "删除工作流失败", + "failedToExportWorkflow": "导出工作流失败", + "failedToExportWorkflows": "导出工作流失败", + "failedToImportWorkflows": "导入工作流失败", + "failedToLoadSyncConfig": "加载工作流同步配置失败:{{error}}", + "failedToLoadWorkflowSteps": "加载工作流步骤失败", + "failedToMigrateTasks": "迁移任务失败", + "failedToSaveWorkflowChanges": "保存工作流更改失败", + "failedToUpdateWorkflowStep": "更新工作流步骤失败", + "githubSync": "GitHub 同步", + "githubSyncDescription": "自动将 GitHub 仓库中的工作流定义同步到此工作区。", + "hopNumber": "跃点 {{index}}", + "import": "导入", + "importCompleteTitle": "导入完成", + "importCreated": "已创建:{{names}}", + "importing": "正在导入…", + "importSkipped": "已跳过(已存在):{{names}}", + "importWorkflowsTitle": "导入工作流", + "invalidYaml": "无效的 YAML", + "keepCurrent": "保留当前设置", + "lastAttempt": "上次尝试 {{when}}", + "lastSynced": "上次同步 {{when}}", + "loadingWorkflowSteps": "正在加载工作流步骤…", + "manageWorkflowsForThisWorkspace": "管理此工作区的工作流。", + "migrateAndDelete": "迁移并删除", + "migrateAndDeleteStep": "迁移并删除步骤", + "migrating": "正在迁移…", + "moreInformation": "更多信息", + "moveToNextStep": "移至下一步", + "moveToPreviousStep": "移至上一步", + "moveToSpecificStep": "移至指定步骤", + "name": "名称", + "noFeederStep": "无馈送步骤", + "noneUseTaskDefault": "无(使用任务默认)", + "noProfileOverride": "无配置覆盖", + "notARecognizedGitHubLink": "无法识别的 GitHub 仓库链接。", + "notSyncedYet": "尚未同步 — 请使用「立即同步」", + "onExit": "退出时", + "onExitActions": "退出时操作", + "onExitHelp": "离开此步骤时运行(进入下一步之前)。", + "onTurnCompleteHelp": "代理完成一轮后运行。可用于自动推进流水线中的任务。", + "onTurnCompleteLabel": "轮次完成时", + "onTurnCompleteTrigger": "轮次完成时", + "onTurnStartHelp": "用户发送消息时运行。可用于审阅循环(例如收到反馈后移回进行中)。", + "onTurnStartLabel": "轮次开始时", + "onTurnStartTrigger": "轮次开始时", + "onUserMessage": "用户消息时", + "orPasteYaml": "或粘贴 YAML", + "overrideAgentProfileHelp": "覆盖此步骤的代理配置。进入此步骤时,如果使用其他配置,将创建具有全新上下文的新会话。", + "overrideOriginalSessionOptions": "覆盖原会话选项", + "overrideOriginalSessionOptionsHelp": "保留原对话标签页,同时更改其在此步骤中的会话设置。例如,先使用 5.6 Sol 模型,再切换到 5.6 Luna 进行实现工作。", + "pasteGitHubLinkHint": "粘贴 GitHub 链接 — /tree/… 链接也会带上分支和目录。", + "planMode": "计划模式", + "planModeHelp": "代理提出计划,而不是直接做更改。", + "pollDisabledHint": "仅在你按下「立即同步」时运行同步。", + "pollEnabledHint": "按此间隔检查仓库(最短 60 秒)。", + "pollIntervalSeconds": "轮询间隔(秒)", + "potentialRepeatedAgentRun": "可能重复的代理运行", + "promptSourceStepPrompt": "重新进入「{{stepName}}」会发送其步骤提示词,而不是任务描述。", + "promptSourceStepPromptWithTaskDescription": "重新进入「{{stepName}}」会发送已渲染的步骤提示词(含任务描述)。", + "promptSourceTaskDescription": "「{{stepName}}」没有步骤提示词,因此重新进入时会发送任务描述。", + "pullFrom": "拉取自", + "pullFromHelp": "当此步骤有容量时,可选的馈送步骤用于拉取排队中的工作。", + "readOnlyManagedBySync": "只读 — 由工作流同步管理", + "remove": "移除", + "removeAgentCondition": "移除代理条件 {{index}}", + "removeConditionalSessionConfigBeforeProfile": "请先移除条件会话配置,再为此步骤选择固定代理配置。", + "removeSyncConfirm": "移除工作流同步配置?这不会删除已同步的工作流。", + "repositoryLink": "仓库链接", + "repositoryRoot": "(仓库根目录)", + "resetAgentContext": "重置代理上下文", + "resetAgentContextHelp": "进入此步骤时用全新对话上下文重启代理。适用于需要无偏见视角的审阅步骤。", + "resetAgentContextHelpWithProfile": "无需此操作 — 切换代理配置已会创建带全新上下文的新会话。", + "restoreOriginal": "恢复原始设置", + "retryPartialSaveBeforeLeaving": "离开前请重试未完成的工作流保存", + "returnToWorkflow": "返回工作流", + "runCompletionActionsWhenTurnCancelled": "取消轮次时运行完成操作", + "runCompletionActionsWhenTurnCancelledHelp": "仅在用户明确取消轮次时适用。目标步骤的进入操作可能会立即启动另一轮代理。", + "save": "保存", + "saveBeforeExporting": "导出前请先保存工作流。", + "saveFailed": "保存失败:{{error}}", + "saveWorkflowsBeforeOrdering": "排序前请先保存工作流", + "saving": "正在保存…", + "seconds": "秒", + "selectAction": "选择操作", + "selectedStepFallback": "已选", + "selectStep": "选择步骤", + "selectWorkflow": "选择工作流", + "sessionConfigCarryForwardWarning": "由于复用原对话标签页,之前的工作流设置可能会沿用到此步骤。", + "sessionConfigCarryForwardWarningMessage": "{{stepName}} 中更改的设置可能会沿用到此步骤的 {{agentName}}。请选择保留、恢复原始或设置新值。", + "sessionConfigEmptyRules": "添加条件,以选择哪些初始代理接收这些设置。", + "sessionConfigFixedProfileWarning": "请先移除固定配置覆盖,再配置原会话。这两种行为不能同时使用。", + "sessionConfigKeep": "保留", + "sessionConfigModelOptionsUnavailable": "此代理的模型选项不可用。请刷新代理能力后再保存此条件。", + "sessionConfigRestore": "恢复", + "sessionConfigRulesHelp": "规则应用于任务的第一个代理会话,不会创建或切换对话标签页。", + "sessionConfigSetNew": "重新设置", + "setSettings": "设置选项", + "settingsForAgent": "{{agent}} 的设置", + "showInCommandPanel": "在命令面板中显示", + "showInCommandPanelHelp": "打开命令面板(Cmd+K)时显示此步骤中的任务。可用于从快速访问中隐藏待办或完成步骤。", + "startStep": "起始步骤", + "startStepHelp": "新任务从此步骤开始。每个工作流只能有一个起始步骤。", + "stepIsPartOfReplayCycle": "{{stepName}} 属于回放循环", + "stepNamePlaceholder": "步骤名称", + "stepPrompt": "步骤提示词", + "stepPromptHelp": "为此步骤中的代理提供自定义指令。使用 {{taskPrompt}} 可插入任务描述。", + "stepPromptPlaceholderExample": "使用 OAuth2 实现用户身份验证", + "stepPromptPlaceholderTaskPrompt": "用户提供的原始任务描述", + "stepPromptUsageHint": "输入 {{open}} 可使用占位符({{taskPrompt}} 会插入任务描述),或输入 @ 引用已保存的提示词。其内容会作为隐藏上下文附加;编辑已保存的提示词时,所有引用该提示词的步骤都会更新。注意:{{taskPrompt}} 只会在步骤提示词本身展开,不会在所引用的已保存提示词中展开。", + "syncCompletedWithWarnings": "同步完成,但有警告", + "syncConfigSaved": "工作流同步配置已保存", + "syncDirectoryHelp": "目录应包含 {{format}} 格式的工作流导出文件({{extensions}})— 与工作流导出产生的格式相同。", + "synced": "已同步", + "syncedBadgeTooltip": "只读 - 由 {{source}} 的工作流同步管理。请在同步仓库中编辑或移除。", + "syncedReadOnlyReason": "由工作流同步管理 — 请在同步仓库中编辑或移除", + "syncFailed": "同步失败", + "syncFailedWithError": "同步失败:{{error}}", + "syncingFromRepository": "正在从 <1>{{repository}} 同步", + "syncNow": "立即同步", + "syncRemoved": "已移除工作流同步 — 已同步的工作流可再次编辑", + "syncRemoveFailed": "删除失败:{{error}}", + "syncResolvedTarget": "正在同步 {{owner}}/{{repo}} @ {{branch}} — 目录 {{directory}}。", + "targetStep": "目标步骤", + "targetWorkflow": "目标工作流", + "template": "模板", + "templateCodeReview": "代码审阅", + "templatePlan": "计划", + "templates": "模板:", + "templateSecurityAudit": "安全审计", + "transitionAuto": "自动", + "transitionBack": "返回", + "transitionGoto": "跳转", + "transitionManual": "手动", + "transitions": "流转", + "unsavedStepChangesDiscarded": "未保存的步骤更改将被丢弃。", + "unsavedWorkflowChangesDiscarded": "未保存的工作流更改将被丢弃。", + "update": "更新", + "uploadYamlFile": "上传 YAML 文件", + "userActionRequired": "需要用户操作", + "waitForAgentCompletionSignal": "等待代理完成信号", + "waitForAgentCompletionSignalHelp": "仅在代理调用 {{tool}} 后才自动推进。否则将轮次结束视为完成。", + "waitingForFirstSync": "等待首次同步…", + "waitingForRetry": "正在等待重试失败的更改。", + "whenAgentIs": "当代理为", + "whenChildTasksComplete": "当子任务完成时", + "wipLimit": "在制品限制", + "wipLimitHelp": "此步骤中允许的最大活动任务数。溢出的任务在队列中可见。使用 0 表示无限制。", + "wipLimitsNote": "在制品限制约束的是活动工作,而非可见性。溢出任务会留在看板上直到有容量;若所选馈送步骤也已满,则拒绝创建新任务。", + "workflowCycleBlocked": "工作流循环已阻止", + "workflowCycleBlockedDescription": "继续前请更改工作流步骤以消除自动循环。", + "workflowCycleBlockingBody": "此路径会重新进入「{{stepName}}」,并可能在没有用户再次操作的情况下重新启动代理。", + "workflowCycleReplayPathFor": "{{stepName}} 的回放路径", + "workflowCycleWarningBody": "此路径可能重新进入「{{stepName}}」,并在用户操作后再次启动代理。", + "workflowDetails": "工作流详情", + "workflowName": "工作流名称", + "workflowNameIsRequired": "工作流名称为必填项", + "workflowNamePlaceholder": "我的项目工作流", + "workflows": "工作流", + "workflowsSectionDescription": "创建带自动流转的自主流水线,或由你手动移动任务的手动工作流", + "workflowSteps": "工作流步骤", + "workflowSyncCompleted": "工作流同步完成", + "workspaceSettings": "工作区设置" +} diff --git a/apps/web/src/locales/zh-cn/workspaces.json b/apps/web/src/locales/zh-cn/workspaces.json new file mode 100644 index 0000000000..a7782b795d --- /dev/null +++ b/apps/web/src/locales/zh-cn/workspaces.json @@ -0,0 +1,106 @@ +{ + "addLocalRepository": "添加本地仓库", + "addScript": "添加脚本", + "addWorkspace": "添加工作区", + "alwaysPullBeforeWorktree": "创建新 worktree 前始终拉取", + "backToWorkspaces": "返回工作区", + "branchTemplateIssueKey": "ticket 的别名。按模板可读性任选其一。", + "branchTemplateLiteralPrefix": "直接写字面前缀,例如 <0>{{example}}。", + "branchTemplatePlaceholders": "分支模板占位符", + "branchTemplateSuffix": "短随机后缀。可选,但建议使用以避免分支名冲突。示例:{{example}}。", + "branchTemplateTaskId": "Kandev 任务 UUID,已清理为适合分支名。示例:{{example}}。", + "branchTemplateTicket": "优先使用任务标识;否则使用 Jira、Linear、GitHub issue 或 GitHub PR 元数据。示例:{{example}}。", + "branchTemplateTitle": "任务标题清理为小写 ASCII、连字符分隔,最长 20 字符。示例:{{example}}。", + "branchTemplateTitleFull": "与 title 相同的清理规则,但最长 80 字符。示例:{{example}}。", + "buildScriptChip": "构建脚本", + "checking": "正在检查…", + "cleanupScript": "清理脚本", + "cleanupScriptChip": "清理脚本", + "cleanupScriptHelp": "任务完成时运行,用于清理工作区。", + "close": "关闭", + "closeRepositoryEditor": "关闭仓库编辑器", + "confirmDelete": "确认删除", + "copyFiles": "复制文件", + "copyFilesHelp": "复制到新 worktree 的 gitignored 路径。在条目后追加 <0>{{symlink}} 可链接回主仓库。使用 <1>{{escapedSymlink}} 表示以 <2>{{symlink}} 结尾的字面文件名。", + "copyFilesPathsResolved": "路径相对于仓库根解析,并植入每个新 worktree,保持相对位置。worktree 中已有文件不会被覆盖。", + "copyFilesPatternBraces": "<0>{{braces}} 大括号交替,例如 <1>{{example}}", + "copyFilesPatternGlobstar": "<0>{{globstar}} 匹配任意层目录,例如 <1>{{example}}", + "copyFilesPatternLiteral": "<0>{{pattern}} 字面文件或目录(目录会递归复制)", + "copyFilesPatternWildcards": "<0>{{star}}、<1>{{question}}、<2>{{charClass}} 单段通配符", + "copyFilesRemoteFallback": "远程执行器复制文件内容,而不是创建符号链接。", + "copyFilesSizeLimit": "复制到远程执行器时,超过 {{limit}} 的文件会被跳过。本地 worktree 复制无大小上限。", + "customScriptsLabel": "自定义脚本", + "customScripts_one": "{{count}} 个自定义脚本", + "customScripts_other": "{{count}} 个自定义脚本", + "defaultAgentProfile": "默认代理配置", + "defaultExecutor": "默认执行器", + "delete": "删除", + "deleteRepository": "删除仓库", + "deleteRepositoryConfirm": "这将移除仓库及其脚本。此操作无法撤销。", + "deleteRepositoryTitle": "删除仓库", + "deleteRepositoryActiveSessions_one": "此仓库正被 {{count}} 个活动代理会话使用。请先停止或完成后再删除仓库。", + "deleteRepositoryActiveSessions_other": "此仓库正被 {{count}} 个活动代理会话使用。请先停止或完成后再删除仓库。", + "deleteThisWorkspace": "删除此工作区", + "deleteWorkspace": "删除工作区", + "devScript": "开发脚本", + "devScriptChip": "开发脚本", + "devScriptHelp": "用于启动此仓库的预览开发服务器。使用 <0>{{port}} 自动分配端口。", + "discoverRepositoryDescription": "选择已发现的仓库,或提供绝对路径进行验证。", + "discoveredRepositories": "已发现的仓库", + "edit": "编辑", + "failedToCheckRepositorySessions": "检查仓库会话失败", + "failedToCreateWorkspace": "创建工作区失败", + "failedToDeleteRepository": "删除仓库失败", + "failedToDeleteWorkspace": "删除工作区失败", + "failedToDiscoverRepositories": "发现仓库失败", + "failedToSaveRepository": "保存仓库失败", + "failedToSaveWorkspace": "保存工作区失败", + "filterRepositories": "筛选仓库…", + "invalidRepositoryPath": "无效的仓库路径", + "localPath": "本地路径", + "localPathNotSet": "未设置本地路径", + "manageRepositoriesConnected": "管理连接到此工作区的仓库。", + "manageWorkspaceDetails": "管理工作区详情,并跳转到工作流或仓库。", + "manageYourWorkspacesAndWorkflows": "管理你的工作区与工作流", + "manualPath": "手动路径", + "name": "名称", + "noAgentProfilesAvailable": "无可用代理配置", + "noCustomScripts": "无自定义脚本", + "noDefault": "无默认", + "noExecutorsAvailable": "无可用执行器", + "noRepositoriesFound": "未找到仓库。", + "noScriptsYet": "尚无脚本。", + "noWorkspacesConfigured": "未配置工作区。添加第一个工作区以开始使用。", + "patternSyntax": "模式语法", + "remoteRepository": "远程仓库", + "repositories": "仓库", + "repositoriesInThisWorkspace": "此工作区中的仓库", + "repository": "仓库", + "repositoryName": "仓库名称", + "scanningRepositories": "正在扫描仓库…", + "scriptName": "脚本名称", + "selectDefaultAgentProfile": "选择默认代理配置", + "selectDefaultExecutor": "选择默认执行器", + "setupScript": "设置脚本", + "setupScriptHelp": "在克隆仓库或创建 git worktree 时运行。", + "sourceLocal": "本地", + "sourceRemote": "远程", + "supportedPatterns": "支持的模式:", + "thisActionCannotBeUndone": "此操作无法撤销。", + "typeWorkspaceNameToConfirm": "输入工作区名称 <0>{{name}} 以确认删除。此操作无法撤销。", + "untitledRepository": "未命名仓库", + "useRepository": "使用仓库", + "validGitRepository": "有效的 git 仓库", + "validate": "验证", + "workflows": "工作流", + "workflowsManagedInThisWorkspace": "此工作区中管理的工作流", + "workspaceLinks": "工作区链接", + "workspaceName": "工作区名称", + "workspaceNameIsRequired": "工作区名称为必填项。", + "workspaceNamePlaceholder": "我的工作区", + "workspaceNotFound": "未找到工作区", + "workspaceSettings": "工作区设置", + "workspaceSettingsLink": "工作区设置", + "worktreeBranchTemplate": "Worktree 分支模板", + "worktreeSync": "Worktree 同步" +} diff --git a/docs/i18n.md b/docs/i18n.md index 352bef27d5..f79c549fff 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -1,8 +1,9 @@ # Internationalization (i18n) Kandev's web UI is internationalized with [i18next](https://www.i18next.com) + -[react-i18next](https://react.i18next.com). English is the source locale; a -generated **pseudo-locale** is used for QA. Requirements live in +[react-i18next](https://react.i18next.com). English is the source locale, +Simplified Chinese (`zh-cn`) is the second shipped human locale, and a generated +**pseudo-locale** is used for QA. Requirements live in [`docs/specs/platform/i18n.md`](specs/platform/i18n.md). The migration is **in progress, one directory per PR**. The runtime, the gates, @@ -37,11 +38,14 @@ Two enforcement layers, and it matters which is which: ``` 3. Regenerate the pseudo catalog: `pnpm run i18n:pseudo`. -**Namespaces** map to product areas. Two exist so far — `common` -(shared/anything cross-cutting) and `settings` — and each migrated directory adds -the one it needs (`sidebar`, `statusBar`, `chat`, `kanban`, `task`, …). A string -used in two or more areas belongs in `common` so it is translated once. A new -namespace needs no registration: `lib/i18n/index.ts` discovers catalogs by glob. +**Namespaces** map to product areas. Current namespaces are `agents`, +`automations`, `azuredevops`, `chat`, `common` (shared/anything cross-cutting), `configChat`, +`executors`, `github`, `gitlab`, `jira`, `linear`, `sentry`, `settings`, +`sidebar`, `slack`, `system`, `workflows`, and `workspaces`. Each migrated area +can add the namespace it needs (`statusBar`, `kanban`, `task`, …). A string used +in two or more areas belongs in `common` so it is translated once. A new +namespace needs no registration: `lib/i18n/index.ts` discovers catalogs by glob, +but every real locale must add the same namespace in the same change. ### Interpolation @@ -819,11 +823,33 @@ Do it in its own PR, not inside a copy-only migration. ## Adding a real language -1. Add the locale to `SUPPORTED_LOCALES` + `LOCALE_LABELS` in - `apps/web/lib/i18n/index.ts`, and to `supportedLocales` in - `apps/backend/internal/i18n/i18n.go`. -2. Copy `src/locales/en/*.json` to `src/locales//` and translate the - values (keys stay identical). -3. Add `apps/backend/internal/i18n/locales/.json` with the same keys as - `en.json` (the backend catalog is tiny — see Backend above). -4. The switcher, formatting, and `` pick it up automatically. +Use a canonical lowercase locale id such as `zh-cn`. Locale negotiation accepts +case variants such as `zh-CN`, but cookies and `` use the canonical id. + +1. Add the locale to `SUPPORTED_LOCALES` and give it a fixed endonym in + `LOCALE_LABELS` in `apps/web/lib/i18n/index.ts`. Add the same id to + `supportedLocales` in `apps/backend/internal/i18n/i18n.go`. Keep the frontend + and backend lists synchronized. +2. Copy every `apps/web/src/locales/en/*.json` namespace into + `apps/web/src/locales//`. Translate values only: never rename keys, + plural suffixes, interpolation placeholders, `` tags, command tokens, + shortcuts, filenames, URLs, or brand names. +3. Add `apps/backend/internal/i18n/locales/.json` with exactly the same + keys as the backend `en.json` and translate its values. +4. Add focused frontend tests for registration, selection, activation, cookie + and boot restoration, ``, and `Intl` locale forwarding. Add backend + tests for canonicalization, cookie and `Accept-Language` negotiation, message + lookup, exact catalog parity, and the first shell response. +5. Run `pnpm run i18n:check` from `apps/web`. It discovers every real locale + except `en` and generated `pseudo`, then fails on any missing or extra + namespace or key. Regenerate `pseudo` only from English with + `pnpm run i18n:pseudo`; never copy a human translation into it. +6. In a development or E2E build, select the new language under **Settings > + General > Appearance**, reload, and switch back to English. Confirm translated + copy, the locale cookie, ``, interpolation, layout, and then use + `pseudo` to find English literals on already migrated surfaces. + +Kandev's migration remains incremental. A complete locale catalog guarantees +that every currently externalized message has a translation; it does not mean +that every screen has been externalized. Unmigrated surfaces continue to render +English until their directory joins `i18nGuardFiles`. diff --git a/docs/plans/i18n-zh-cn/plan.md b/docs/plans/i18n-zh-cn/plan.md new file mode 100644 index 0000000000..4d1542cb83 --- /dev/null +++ b/docs/plans/i18n-zh-cn/plan.md @@ -0,0 +1,155 @@ +--- +spec: docs/specs/platform/i18n.md +created: 2026-08-02 +status: done +--- + +# Implementation Plan: Simplified Chinese Locale + +## Overview + +Extend the existing eager i18next and embedded Go catalog design with canonical +locale `zh-cn`. Implement the human catalogs and locale registration first, +then make locale negotiation and catalog parity strict, and finally prove the +persisted browser flow, public documentation, and user-requested repository-wide +checks without changing unrelated i18n migration scope. + +--- + +## Backend + +### Locale registration and negotiation + +- `apps/backend/internal/i18n/i18n.go`: register `zh-cn`; normalize supported + locale input case-insensitively to the canonical lowercase id so cookies and + `Accept-Language: zh-CN` converge on the same locale. +- `apps/backend/internal/i18n/locales/zh-cn.json`: translate the three + server-rendered browser error messages with an exact key match to `en.json`. +- `apps/backend/internal/i18n/i18n_test.go`: cover canonical normalization, + case-insensitive BCP-47 input, Chinese message lookup, exact catalog parity, + cookie precedence, q-value ordering, region matching, and English fallback. +- `apps/backend/internal/webapp/shell_test.go`: prove a `zh-cn` runtime locale + produces `` in the first server response. + +## Frontend + +### Catalogs and locale runtime + +- Add `apps/web/src/locales/zh-cn/*.json` for all 18 current namespaces, + mirroring all 3,300 current English keys while preserving interpolation + variables, plural key suffixes, `` tags, code tokens, shortcuts, and + brand names. +- `apps/web/lib/i18n/index.ts`: register `zh-cn`, label it with the fixed endonym + `简体中文`, and retain the rule that only `pseudo` is hidden in production. +- `apps/web/lib/i18n/index.test.ts` and `boot.test.ts`: cover support detection, + selection lists, activation, translated catalog resolution, cookie persistence, + ``, boot-payload/cookie restoration, and invalid fallback. +- `apps/web/lib/i18n/formats.test.ts`: prove `zh-cn` reaches `Intl.NumberFormat` + and `Intl.DateTimeFormat` through stable options while `pseudo` maps to `en`. + No production formatter change is expected unless the RED test exposes one. + +### Catalog integrity gate + +- Refactor the smallest reusable catalog-parity operation out of + `apps/web/scripts/check-i18n-keys.mjs` and cover it with a temporary-fixture + test under `apps/web/scripts/`. +- Discover every committed real locale directory except source `en` and generated + `pseudo`. Require exact namespace parity and exact per-namespace key parity, + and report locale, namespace, missing keys, and extra keys. +- Preserve the existing source-key/orphan checks and the generated pseudo sync + instruction; never generate or rewrite the English catalog. + +## Documentation + +- `docs/i18n.md`: document the complete real-language workflow, synchronized + frontend/backend locale lists, fixed endonym labels, strict catalog check, + pseudo generation boundary, and partial-migration limitation. +- `docs/public/feature-status.md`: describe English/Simplified Chinese selection + and state that only migrated surfaces are translated. +- Keep this spec aligned with the shipped real-locale contract. + +## Tests + +- **Frontend locale contract:** `apps/web/lib/i18n/index.test.ts` and + `boot.test.ts`; activate and restore a real Chinese locale and reject invalid + input. +- **Formatting contract:** `apps/web/lib/i18n/formats.test.ts`; spy on or compare + stable `Intl` behavior without asserting an OS-specific full date string. +- **Catalog parity:** a focused script test with temporary locale directories; + prove missing namespace, extra namespace, missing key, and extra key failures. +- **Backend negotiation:** table-driven tests in + `apps/backend/internal/i18n/i18n_test.go` plus the shell lang test in + `apps/backend/internal/webapp/shell_test.go`. + +## E2E Tests + +- **Scenario:** GIVEN Appearance settings, WHEN 简体中文 is selected, THEN + migrated copy and `` appear, reload preserves both through + the locale cookie, and the test restores English. +- **Files:** `apps/web/e2e/tests/i18n/language-switch.spec.ts` and + `apps/web/e2e/tests/i18n/mobile-language-switch.spec.ts`. +- **What to verify:** stable translated text, canonical document locale, cookie + persistence, reload behavior, and isolation from the existing pseudo test on + both desktop and the `mobile-chrome` Pixel 5 project. +- Capture a clean Chinese Appearance screenshot for the PR and manually inspect + translated surfaces for raw keys, broken interpolation/tags, and overflow. + +## Verification Results + +Implementation and focused verification are complete. The locale catalogs have +exact parity across 18 namespaces and 3,300 frontend keys; the backend catalogs +have exact parity across 34 messages. The focused frontend contract suite passed +32/32, both i18n gates passed, web typecheck passed, the affected backend system +suite and changed-code lint passed, and desktop and mobile language-switch E2E +passed 3/3 and 1/1 respectively. The Chinese Appearance capture was manually +reviewed and remains an ignored asset. + +The full repository Make wrappers were not rerun for this focused rebase fixup; +their statuses and approved focused alternatives are recorded in Task 06. The +branch remains limited to the locale catalogs, validator/tests, mobile E2E +coverage, the backend test synchronization fix, and synchronized documentation. + +--- + +## Implementation Waves And Parallel Candidates + +The default is sequential execution in this primary conversation. No subagent +work is authorized by these waves. + +Wave 1: + +- [x] [Task 01: Frontend zh-cn integration](task-01-frontend-locale-catalogs.md) + +Wave 2: + +- [x] [Task 02: Backend locale negotiation](task-02-backend-locale-negotiation.md) +- [x] [Task 03: Catalog parity gate](task-03-catalog-parity-gate.md) + +Wave 3: + +- [x] [Task 04: Chinese locale E2E](task-04-chinese-locale-e2e.md) + +Wave 4: + +- [x] [Task 05: Localization documentation](task-05-localization-documentation.md) + +Wave 5: + +- [x] [Task 06: Repository verification](task-06-repository-verification.md) + +## Risks + +- Human review is required for 3,300 Chinese messages; key parity alone cannot + prove natural wording, preserved semantic tokens, or layout fit. +- Node 24 and Corepack select pnpm 9.15.9 from `apps/package.json`. Go 1.26.0 is + installed under the current user's program directory and registered in the + user PATH; Task 02 and repository verification successfully invoked it + directly. +- Full E2E and manual `make dev` validation are environment-sensitive and may + expose baseline failures; focused locale evidence must remain separately + recorded. + +## Open Questions + +None. The user supplied the locale id, translation scope, persistence contract, +verification commands, branch name, commit title, and PR requirements. diff --git a/docs/plans/i18n-zh-cn/task-01-frontend-locale-catalogs.md b/docs/plans/i18n-zh-cn/task-01-frontend-locale-catalogs.md new file mode 100644 index 0000000000..a8407d1d62 --- /dev/null +++ b/docs/plans/i18n-zh-cn/task-01-frontend-locale-catalogs.md @@ -0,0 +1,71 @@ +--- +id: "01-frontend-locale-catalogs" +title: "Frontend zh-cn integration" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +spec: "../../specs/platform/i18n.md" +--- + +# Task 01: Frontend zh-cn integration + +## Acceptance + +- `zh-cn` is supported and selectable in production with fixed label `简体中文`; + only `pseudo` remains production-hidden. +- All 18 Chinese namespace catalogs exactly mirror the English catalogs and + preserve variables, plural suffixes, `` tags, tokens, and brand names. +- Activation and boot restoration set i18next, ``, and the locale + cookie to `zh-cn`; Intl wrappers use Chinese locale data while pseudo uses en. + +## Verification + +```bash +cd apps && pnpm install --frozen-lockfile +cd apps/web && pnpm exec vitest run lib/i18n/index.test.ts lib/i18n/boot.test.ts lib/i18n/formats.test.ts +cd apps/web && pnpm run typecheck +``` + +Run each changed behavioral test once in RED before production edits, then rerun +the exact focused suite after the final production or catalog edit. + +## Files likely touched + +- `apps/web/src/locales/zh-cn/common.json` +- `apps/web/src/locales/zh-cn/settings.json` +- `apps/web/lib/i18n/index.ts` +- `apps/web/lib/i18n/index.test.ts` +- `apps/web/lib/i18n/boot.test.ts` +- `apps/web/lib/i18n/formats.test.ts` + +## Dependencies + +None. + +## Parallelism + +Sequential. Catalogs and locale registration form one runtime contract. + +## Inputs + +- Spec: **What**, **Scenarios**, **Failure modes**, and **Resolved decisions**. +- Plan: **Frontend → Catalogs and locale runtime**. +- Existing patterns: `en` and generated `pseudo` catalogs, locale activation, + boot resolution, and current Intl wrapper tests. + +## Output contract + +Report RED/GREEN evidence, key counts by namespace, files changed, exact commands +and results, translation preservation checks, blockers/risks, and synchronized +task/plan status. + +## Results + +- RED: `pnpm exec vitest run lib/i18n/index.test.ts lib/i18n/boot.test.ts lib/i18n/formats.test.ts` failed 8 of 20 tests because `zh-cn` normalized to `en`, was absent from both selectable lists, and formatted with English Intl data. +- GREEN: the same focused command passed 20 of 20 tests after registering the locale, loading the catalogs, and configuring i18next to retain the lowercase canonical resource id. +- Catalogs: 18 namespaces and 3,300 keys; every namespace, key, and key order + matches English, with identical interpolation-placeholder and `` tag + sets. Two messages reorder named placeholders to follow natural Chinese + grammar without changing their values or markup. +- `pnpm run typecheck` passed. diff --git a/docs/plans/i18n-zh-cn/task-02-backend-locale-negotiation.md b/docs/plans/i18n-zh-cn/task-02-backend-locale-negotiation.md new file mode 100644 index 0000000000..03e1366fae --- /dev/null +++ b/docs/plans/i18n-zh-cn/task-02-backend-locale-negotiation.md @@ -0,0 +1,62 @@ +--- +id: "02-backend-locale-negotiation" +title: "Backend locale negotiation" +status: done +wave: 2 +depends_on: ["01-frontend-locale-catalogs"] +plan: "plan.md" +spec: "../../specs/platform/i18n.md" +--- + +# Task 02: Backend locale negotiation + +## Acceptance + +- Backend support is synchronized with the frontend and canonicalizes `zh-cn` + and `zh-CN` to `zh-cn` without weakening unknown-locale English fallback. +- Cookie choice wins over weighted `Accept-Language`; `zh-CN` negotiates to the + Chinese catalog and server-rendered browser messages differ from English. +- Chinese and English backend catalogs have exactly the same keys, and the first + shell response renders ``. + +## Verification + +```bash +cd apps/backend && go test ./internal/i18n/... ./internal/webapp/... ./internal/backendapp/... +``` + +Add the smallest table-driven tests first and observe their expected RED result +before changing locale registration or normalization. + +## Files likely touched + +- `apps/backend/internal/i18n/locales/zh-cn.json` +- `apps/backend/internal/i18n/i18n.go` +- `apps/backend/internal/i18n/i18n_test.go` +- `apps/backend/internal/webapp/shell_test.go` + +## Dependencies + +Task 01 defines the canonical frontend locale list that the backend mirrors. + +## Parallelism + +Sequential because frontend/backend locale-list parity is a shared contract. + +## Inputs + +- Spec scenarios for cookie, Accept-Language, server errors, and shell lang. +- Plan: **Backend → Locale registration and negotiation**. +- Existing patterns: `parseAcceptLanguage`, `T`, `Keys`, and shell lang tests. + +## Output contract + +Report RED/GREEN evidence, exact commands and results, catalog key comparison, +files changed, fallback behavior, blockers/risks, and synchronized task/plan +status. + +## Results + +- RED: `go test ./internal/i18n/... ./internal/webapp/...` failed on Chinese normalization, message lookup, Cookie precedence, weighted `Accept-Language: zh-CN`, and server-rendered `lang`. +- GREEN: `go test ./internal/i18n/... ./internal/webapp/... ./internal/backendapp/...` passed after registering `zh-cn` and normalizing supported locale input to the lowercase canonical id. +- The 34-key backend Chinese catalog exactly matches English; tests reject missing and extra keys. diff --git a/docs/plans/i18n-zh-cn/task-03-catalog-parity-gate.md b/docs/plans/i18n-zh-cn/task-03-catalog-parity-gate.md new file mode 100644 index 0000000000..74192882a2 --- /dev/null +++ b/docs/plans/i18n-zh-cn/task-03-catalog-parity-gate.md @@ -0,0 +1,68 @@ +--- +id: "03-catalog-parity-gate" +title: "Catalog parity gate" +status: done +wave: 2 +depends_on: ["01-frontend-locale-catalogs"] +plan: "plan.md" +spec: "../../specs/platform/i18n.md" +--- + +# Task 03: Catalog parity gate + +## Acceptance + +- `i18n:check` discovers every committed real locale and fails for missing or + extra namespaces and for missing or extra keys within a namespace. +- Errors name the locale, namespace, and specific drift; pseudo retains its + generated strict-sync behavior and English is never regenerated. +- Temporary-fixture tests cover all four real-locale drift classes and the + passing complete-catalog case. + +## Verification + +```bash +cd apps/web && pnpm exec vitest run scripts/check-i18n-keys.test.ts +cd apps/web && pnpm run i18n:check +``` + +Write the fixture tests first and confirm the current en/pseudo-only comparison +cannot satisfy them before changing the script. + +## Files likely touched + +- `apps/web/scripts/check-i18n-keys.mjs` +- `apps/web/scripts/check-i18n-keys.test.ts` +- `apps/web/scripts/lib/i18n-catalogs.mjs` if a pure helper keeps the CLI small + +## Dependencies + +Task 01 supplies the first real-locale directory and expected complete catalogs. + +## Parallelism + +Sequential because this task validates Task 01's catalog structure. + +## Inputs + +- Spec: real-locale parity scenario and resolved decision. +- Plan: **Frontend → Catalog integrity gate**. +- Existing pattern: script utilities with focused Vitest tests under + `apps/web/scripts/`. + +## Output contract + +Report RED/GREEN evidence, fixture cases, actual complete-catalog output, exact +commands and results, files changed, blockers/risks, and synchronized task/plan +status. + +## Results + +- RED: `pnpm exec vitest run scripts/check-i18n-keys.test.ts` passed only the complete fixture and failed four assertions because missing/extra real-locale namespaces and keys were ignored. +- GREEN: `pnpm exec vitest run scripts/check-i18n-keys.test.ts` passed all 12 + fixture tests, including placeholder/tag drift and empty, whitespace-only, and + non-string translated values. +- `pnpm run i18n:check` passed against current catalogs: 2,790 referenced keys, + 3,300 English entries, 4 orphans (pre-existing), `pseudo` and `zh-cn` in sync, + 53 `` elements valid, and no inline plural or module-scope translation + defects. diff --git a/docs/plans/i18n-zh-cn/task-04-chinese-locale-e2e.md b/docs/plans/i18n-zh-cn/task-04-chinese-locale-e2e.md new file mode 100644 index 0000000000..77466208fa --- /dev/null +++ b/docs/plans/i18n-zh-cn/task-04-chinese-locale-e2e.md @@ -0,0 +1,81 @@ +--- +id: "04-chinese-locale-e2e" +title: "Chinese locale E2E" +status: done +wave: 3 +depends_on: + - "01-frontend-locale-catalogs" + - "02-backend-locale-negotiation" + - "03-catalog-parity-gate" +plan: "plan.md" +spec: "../../specs/platform/i18n.md" +--- + +# Task 04: Chinese locale E2E + +## Acceptance + +- Playwright proves selecting 简体中文 changes stable migrated copy and + ``, survives reload through the locale cookie, and restores English. +- The `mobile-chrome` project proves the same selection, translated copy, + cookie persistence, reload, and English restoration on the supplied Pixel 5 + viewport without a per-test viewport override. +- The existing pseudo-locale scenario remains intact and passing. +- A fresh desktop Chinese Appearance screenshot is captured, inspected for + secrets and layout problems, stored under ignored `apps/web/.pr-assets/`, and + excluded from the feature branch. + +## Verification + +```bash +(cd apps/web && pnpm e2e:run --host --project chromium -- tests/i18n/language-switch.spec.ts) +(cd apps/web && pnpm e2e:run --host --project mobile-chrome -- tests/i18n/mobile-language-switch.spec.ts) +``` + +Add the Chinese scenario first and observe its expected RED result before the +feature is complete, then rerun the focused managed headless spec after the final +relevant edit. + +## Files likely touched + +- `apps/web/e2e/tests/i18n/language-switch.spec.ts` +- `apps/web/e2e/tests/i18n/mobile-language-switch.spec.ts` +- Ignored screenshot assets under `apps/web/.pr-assets/` only; never commit them + to the feature branch. + +## Dependencies + +Tasks 01-03 must be done so the E2E runs against the integrated runtime, +backend, and catalogs. + +## Parallelism + +Sequential. This task is the browser-level integration proof. + +## Inputs + +- Spec scenarios for the Chinese switch, reload, shell lang, and pseudo QA. +- Plan: **E2E Tests**. +- Existing language switch spec and repository `e2e` workflow. + +## Output contract + +Report RED/GREEN evidence, discovered test count, exact command and result, +cookie/lang/text assertions, screenshot path and redaction/layout review, +files changed, blockers/risks, and synchronized task/plan status. + +## Results + +- Desktop `language-switch.spec.ts` passed 3/3, including the existing pseudo + scenario and the Simplified Chinese selection, canonical `lang="zh-cn"`, + stable `显示语言` copy, `kandev_locale=zh-cn`, reload persistence, and + restoration to English. +- Mobile `mobile-language-switch.spec.ts` passed 1/1 under the + `mobile-chrome` Pixel 5 project. It scopes options through the active listbox, + checks the Chinese label and cookie after selection and reload, then restores + English to prevent cookie leakage. +- Captured + `apps/web/.pr-assets/language-switch--simplified-chinese-locale-desktop.png`. + Manual review found no secrets, raw keys, broken interpolation, or overflow. + English copy on unmigrated sidebar surfaces is the documented migration + boundary. The screenshot remains ignored and is excluded from the branch. diff --git a/docs/plans/i18n-zh-cn/task-05-localization-documentation.md b/docs/plans/i18n-zh-cn/task-05-localization-documentation.md new file mode 100644 index 0000000000..c96f8704ab --- /dev/null +++ b/docs/plans/i18n-zh-cn/task-05-localization-documentation.md @@ -0,0 +1,73 @@ +--- +id: "05-localization-documentation" +title: "Localization documentation" +status: done +wave: 4 +depends_on: + - "01-frontend-locale-catalogs" + - "02-backend-locale-negotiation" + - "03-catalog-parity-gate" + - "04-chinese-locale-e2e" +plan: "plan.md" +spec: "../../specs/platform/i18n.md" +--- + +# Task 05: Localization documentation + +## Acceptance + +- `docs/i18n.md` documents all steps for adding a real language, fixed endonym + labels, synchronized frontend/backend locale lists, strict catalog validation, + the generated pseudo boundary, and the partial-migration limitation. +- `docs/public/feature-status.md` tells users that English and 简体中文 are + selectable while unmigrated surfaces remain English. +- Public-doc validation passes and the product spec matches implemented behavior. + +## Verification + +```bash +node --test scripts/validate-public-docs.test.mjs +node scripts/validate-public-docs.mjs +rg -n "zh-cn|简体中文|real language|真实语言" docs/i18n.md docs/public/feature-status.md docs/specs/platform/i18n.md +``` + +## Files likely touched + +- `docs/i18n.md` +- `docs/public/feature-status.md` +- `docs/specs/platform/i18n.md` + +## Dependencies + +Tasks 01-04 provide the exact implemented behavior and browser evidence the docs +must describe. + +## Parallelism + +Sequential. Documentation follows actual implementation and verified behavior. + +## Inputs + +- Spec, completed task results, and plan **Documentation** section. +- Repository `docs-maintainer` workflow and existing public feature-status table. + +## Output contract + +Report docs impact, exact commands and results, files changed, terminology and +link checks, blockers/risks, and synchronized task/plan status. + +## Results + +- Updated `docs/i18n.md` with the shipped `zh-cn` locale, current namespaces, + canonical locale ids, fixed endonym labels, synchronized frontend/backend + registration, value-only translation rules, strict real-locale parity, + focused tests, pseudo generation boundary, and manual reload/layout checks. +- Updated `docs/public/feature-status.md` with a Limited Web UI localization row: + English and 简体中文 are selectable, while unmigrated surfaces remain English. +- The existing product spec already records the implemented Chinese selection, + cookie, `Accept-Language`, shell, `Intl`, production visibility, and parity + contracts. +- `node --test scripts/validate-public-docs.test.mjs`: 58/58 passed. +- `node scripts/validate-public-docs.mjs`: validated 41 published pages. +- The requested terminology search found the documented locale id and endonym + across the guide, public status page, and product spec. diff --git a/docs/plans/i18n-zh-cn/task-06-repository-verification.md b/docs/plans/i18n-zh-cn/task-06-repository-verification.md new file mode 100644 index 0000000000..3f28fec269 --- /dev/null +++ b/docs/plans/i18n-zh-cn/task-06-repository-verification.md @@ -0,0 +1,113 @@ +--- +id: "06-repository-verification" +title: "Repository verification" +status: done +wave: 5 +depends_on: + - "01-frontend-locale-catalogs" + - "02-backend-locale-negotiation" + - "03-catalog-parity-gate" + - "04-chinese-locale-e2e" + - "05-localization-documentation" +plan: "plan.md" +spec: "../../specs/platform/i18n.md" +--- + +# Task 06: Repository verification + +## Acceptance + +- Every user-requested check is either run and recorded or, when a native-Windows + wrapper is unavailable, its approved cross-platform equivalent is run and the + skipped wrapper is recorded; unrelated baseline or environment failures are + identified without concealment. +- Manual browser inspection covers Chinese text, canonical html lang, reload, + switching back, raw keys, interpolation/Trans integrity, and obvious overflow + on the desktop capture; the mobile scenario covers the same locale lifecycle + at the `mobile-chrome` viewport. +- Final diff contains no unrelated formatting, generated files, dependency + upgrades, accidental pseudo edits, or uncommitted screenshot binaries. + +## Verification + +```bash +(cd apps/web && pnpm exec vitest run scripts/check-i18n-keys.test.ts lib/i18n/index.test.ts lib/i18n/boot.test.ts lib/i18n/formats.test.ts) +(cd apps/web && pnpm run i18n:check) +(cd apps/web && pnpm run i18n:ratchet) +(cd apps/web && pnpm run typecheck) +(cd apps/backend && go test -race ./internal/system/...) +(cd apps/backend && golangci-lint run ./... --new-from-rev="origin/main" --timeout=5m) +(cd apps/web && pnpm e2e:run --host --project chromium -- tests/i18n/language-switch.spec.ts) +(cd apps/web && pnpm e2e:run --host --project mobile-chrome -- tests/i18n/mobile-language-switch.spec.ts) +git diff --check +``` + +The ledger below records the root Make targets separately from the focused +cross-platform commands actually used for this rebase fixup. The isolated +Playwright language-switch runs are the approved alternative to the `make dev` +manual browser flow. + +After the checks and manual inspection pass, reconcile all task/plan results, +review the complete diff, then use the repository `commit` and `push` workflows. +PR metadata is outside this task's scope. + +The managed Playwright run captures the desktop Appearance state under the +ignored `.pr-assets/` directory. Inspect that image after the run and keep it +out of the feature branch. + +## Files likely touched + +- `docs/plans/i18n-zh-cn/plan.md` +- `docs/plans/i18n-zh-cn/task-*.md` +- No new production files are expected; any required fix returns to its owning + task and reruns that task's exact check. + +## Dependencies + +Tasks 01-05 must be complete. + +## Parallelism + +Sequential. This task reconciles all evidence before Git delivery. + +## Inputs + +- All completed task results, user-requested validation list, manual acceptance + criteria, and repository delivery workflows. + +## Output contract + +Report the full command ledger with outcomes/counts, manual browser results, +final diff review, toolchain/environment limits, blockers/risks, synchronized +task/plan status, and readiness for commit, push, and PR creation. + +## Results + +- Frontend contract suite: 4 files, 32/32 tests passed. +- `pnpm run i18n:check`: 2,790 referenced keys, 3,300 English entries, 4 + pre-existing orphans, 18 namespaces, and exact pseudo/zh-cn parity; the + `` index and inline-plural/module-scope checks also passed. +- `pnpm run i18n:ratchet` passed with `0 added + 8 modified file(s) clean` and + the 248-entry guard allowlist intact. Web typecheck passed. +- The affected backend update packages passed their 500-run race regression, + the complete `internal/system/...` race suite passed, and the changed-code + `golangci-lint` run passed. +- Desktop language-switch E2E passed 3/3 and mobile-chrome language-switch E2E + passed 1/1. The desktop Chinese Appearance capture at + `apps/web/.pr-assets/language-switch--simplified-chinese-locale-desktop.png` + was manually checked for raw keys, broken interpolation, overflow, and + secrets; the asset remains ignored and untracked. + +| Requested repository check | Status | Evidence or approved alternative | +| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------- | +| `make fmt` | Not run | Focused `gofmt` and changed-file formatting checks passed. | +| `make typecheck` | Not run | `apps/web` `pnpm run typecheck` passed; no full Make wrapper was needed for the focused rebase fixup. | +| `make test` | Not run | Focused Vitest and `go test -race ./internal/system/...` passed. | +| `make lint` | Not run | Changed-code `golangci-lint` passed; the i18n ratchet passed. | +| `make test-e2e` | Not run | Focused desktop and mobile managed Playwright runs passed. | +| `git diff --check` | Passed | No whitespace errors. | +| `make dev` | Not run | Approved managed Playwright run plus manual screenshot inspection covered the browser flow. | + +The repository-wide Make wrappers were intentionally not rerun as part of this +focused rebase fixup; the ledger records that limitation rather than implying +full-suite coverage. No generated build output or screenshot binary is tracked. diff --git a/docs/public/feature-status.md b/docs/public/feature-status.md index 6d7a974ebf..8a568f8479 100644 --- a/docs/public/feature-status.md +++ b/docs/public/feature-status.md @@ -74,6 +74,7 @@ Released builds can lag this page. Check **Settings > System > About** or `kande | Language servers | Limited | Current built-in mapping covers TypeScript/JavaScript, Go, Rust, and Python. Auto-start/installation default off, and subprocesses run on the backend host rather than inside Docker, SSH, or Sprites runtimes. | | Integrated shell terminals | Dependency-bound | Create, reopen, rename, and terminate are shipped. Shell settings affect new terminals, and executor/task-environment availability controls where the PTY runs. | | Prompts, keyboard shortcuts, notifications, appearance, and task actions | Supported | These are normal settings surfaces. Browser notification permission, custom command validity, and local shell/editor availability still apply to their effects. | +| Web UI localization | Limited | **Settings > General > Appearance** offers English and 简体中文. The selection persists across reloads, but localization migration is incremental: currently externalized surfaces use the selected language while unmigrated surfaces remain in English. | diff --git a/docs/specs/platform/i18n.md b/docs/specs/platform/i18n.md index 45b8f6bbd2..bbb738351a 100644 --- a/docs/specs/platform/i18n.md +++ b/docs/specs/platform/i18n.md @@ -1,18 +1,18 @@ --- status: building created: 2026-07-27 +updated: 2026-08-02 owner: jcfs --- # Internationalization (i18n) ## Why -Every user-facing string in the Kandev web UI is a hardcoded English literal, so -the product cannot be presented in any other language and non-English speakers -are locked out. There is no infrastructure to externalize strings, switch -locale, or format dates and numbers per the user's language. This feature makes -the entire web frontend locale-ready with English as the source language, so -translations can be added later without touching component code. +Kandev's i18n foundation lets migrated web surfaces externalize copy, switch +locale, and format dates and numbers per language, but English is still the only +human language shipped. Adding Simplified Chinese makes the existing localized +surface usable by Chinese-speaking users while preserving English as the source +language and the pseudo-locale as the migration QA oracle. ## What - The web UI SHALL render all user-facing text through a translation layer @@ -20,13 +20,14 @@ translations can be added later without touching component code. covers visible copy, `placeholder`, `aria-label`/`aria-description`, `title`, `alt`, toast/`sonner` messages, form-validation messages, empty-state and error copy, and confirm/dialog text. -- English SHALL be the source locale and the only shipped human language in this - iteration. Copy lives in per-namespace JSON catalogs keyed by stable ids; the - `pseudo` catalog is generated from `en`, never hand-edited. -- The app SHALL expose a **language switcher** in Settings. With only English - available it lists English plus a **pseudo-locale** (see below); selecting a - locale re-renders the UI in that locale without a full reload and persists the - choice across restarts. +- English SHALL remain the source locale. The shipped human locales SHALL be + `en` and `zh-cn`; copy lives in per-namespace JSON catalogs keyed by stable + ids. The `pseudo` catalog is generated from `en`, never hand-edited. +- The app SHALL expose a **language switcher** in Settings. It lists English and + Simplified Chinese by their fixed endonyms. Selecting a locale re-renders the + UI without a full reload and persists the + choice across restarts. Development and E2E builds additionally list the + **pseudo-locale** (see below). - The app SHALL ship a **pseudo-locale** (`pseudo`) that transforms every extracted message (accents + padding, e.g. `Ĉřéàţé ţàśķ`) so QA can visually confirm a string was externalized and detect truncation. The pseudo-locale is @@ -49,9 +50,10 @@ translations can be added later without touching component code. migration PR extends the allowlist by the path it converts, so coverage only ever grows and un-migrated code is never a blocker. - CI SHALL verify keys and catalogs do not drift: every `ns:key` referenced in - source must exist in the `en` catalog, and `pseudo` must carry the same key - set. (Catalogs are NOT regenerated from source — with key-based ids the - English copy exists only in the catalog.) + source must exist in the `en` catalog; every committed real locale must have + exactly the same namespaces and key sets as `en`; and `pseudo` must remain in + strict generated sync with `en`. (Catalogs are NOT regenerated from source — + with key-based ids the English copy exists only in the catalog.) - The full sweep SHALL externalize **every** user-facing string across `apps/web/components/` (1,121 files), `apps/web/app/` (285 files), and the `@kandev/ui` primitives, leaving no user-facing English literal outside the @@ -64,36 +66,38 @@ single change. A single PR covering it touches ~1,130 files and conflicts with `main` continuously; the first attempt could not be kept alive long enough to merge. Sequence: -1. **Foundation** (this iteration): runtime, catalogs, backend locale plumbing, +1. **Foundation**: runtime, catalogs, backend locale plumbing, language switcher, CI gates, codemods, the scoped lint guard, and one page (Settings → General → Appearance) migrated end-to-end as the worked example. -2. **Per-directory migrations**: each PR externalizes one directory, appends it +2. **First real language**: ship `zh-cn` catalogs for every existing namespace, + register the locale in the frontend and backend, and strengthen catalog + parity checks for every committed real locale. +3. **Per-directory migrations**: each PR externalizes one directory, appends it to `i18nGuardFiles`, and adds its screen to the pseudo-coverage spec. -3. **Close-out**: `@kandev/ui` primitives, the shared-task artifacts, and +4. **Close-out**: `@kandev/ui` primitives, the shared-task artifacts, and removing the env guard on the pseudo-coverage e2e spec once coverage is total. ## Data model i18n adds no server-side persistent entities. It introduces: - **Message catalogs** — checked-in JSON under - `apps/web/src/locales//.json` for `en` and `pseudo`. `en` is - hand-authored (keys are stable ids; the English copy lives only here); - `pseudo` is generated from `en` by `scripts/generate-pseudo-locale.mjs` and is - never hand-edited. Loaded at runtime via Vite glob import — no build plugin. + `apps/web/src/locales//.json` for `en`, `zh-cn`, and + `pseudo`. `en` is hand-authored (keys are stable ids; the English copy lives + only here); `zh-cn` is a human-maintained translation whose namespace and key + sets exactly mirror `en`; `pseudo` is generated from `en` by + `scripts/generate-pseudo-locale.mjs` and is never hand-edited. Catalogs load at + runtime via Vite glob import — no build plugin. - **Locale preference** — the user's chosen locale, one value from the supported - set. Stored client-side in the existing user-settings mechanism (persisted the - same way other UI preferences are, so it survives restart) and mirrored to a - cookie read by the Go shell so the initial `` and first paint match - the persisted locale. Field: `locale: string` (BCP-47-ish tag, default `en`). + set. A dedicated `kandev_locale` cookie is the authoritative persisted value + read by the Go shell so the initial `` and first paint match the + selected locale. It is hydrated into the in-memory user-settings/store mirror + for the switcher UI. Field: `locale: string` (BCP-47-ish tag, default `en`). -There is no new SQLite table; the preference rides existing user-settings -persistence. If user-settings has no suitable slot, a dedicated cookie -(`kandev_locale`) is the source of truth for the shell and is hydrated into the -store at boot. +There is no SQLite or user-settings persistence for locale. The cookie is the +single persisted source of truth and is hydrated into the store at boot. ## API surface -No new HTTP/WS contract is required for English-only shipping. Two integration -points: +No new HTTP/WS contract is required to add a locale. Two integration points: - **Go shell** (`apps/backend/internal/webapp/shell.go`): reads the `kandev_locale` cookie (default `en`) and emits `` plus, @@ -115,6 +119,17 @@ Frontend module contract (new): - **GIVEN** a fresh install with no locale cookie, **WHEN** the UI loads, **THEN** `` is `en`, all text renders from the `en` catalog, and the language switcher shows English selected. +- **GIVEN** the Settings language switcher, **WHEN** the user selects + **简体中文**, **THEN** every currently migrated message resolves from the + `zh-cn` catalog, `` becomes `zh-cn`, and the selection survives a + reload through the `kandev_locale=zh-cn` cookie. +- **GIVEN** no locale cookie and `Accept-Language: zh-CN`, **WHEN** the Go shell + or a server-rendered browser error is returned, **THEN** locale negotiation + selects `zh-cn`, the shell emits ``, and the backend message + comes from the Simplified Chinese catalog. +- **GIVEN** `zh-cn` is active, **WHEN** number and date/time helpers format a + value, **THEN** they pass `zh-cn` to `Intl`; **AND GIVEN** `pseudo` is active, + **THEN** those helpers continue to use English formatting data. - **GIVEN** the Settings language switcher, **WHEN** the user selects the pseudo-locale in a dev build, **THEN** visible strings render with accented padded glyphs (e.g. `Ĉřéàţé`), the choice persists across reload, and @@ -139,7 +154,10 @@ Frontend module contract (new): - **GIVEN** a plural key (`x_one` / `x_other`) rendered with a `count`, **WHEN** count is 1 vs many, **THEN** i18next selects the matching form. - **GIVEN** a production build, **WHEN** the language switcher renders, **THEN** - the pseudo-locale is not offered as a selectable option. + English and 简体中文 are offered and the pseudo-locale is not. +- **GIVEN** an English namespace or key changes, **WHEN** `i18n:check` runs, + **THEN** every real locale missing or adding a namespace or key fails with an + error naming the locale, namespace, and missing or extra entries. ## Failure modes - **Missing translation for the active locale** → i18next falls back to `en`, @@ -153,15 +171,15 @@ Frontend module contract (new): English `defaultProps`; no runtime dependency on the app i18n instance. ## Persistence guarantees -- The locale preference survives a kandev restart (cookie + user-settings). +- The locale preference survives a kandev restart through the + `kandev_locale` cookie. - Message catalogs are source-controlled build inputs; nothing i18n-related is written to the server DB or worktrees at runtime. - No in-flight session, queued run, or WS state is affected by a locale change; switching locale is a pure client re-render. ## Out of scope -- Shipping any real second language (no `es`/`pt`/`fr`/etc. translations in this - iteration — infrastructure + English source + pseudo-locale only). +- Shipping human languages other than English and Simplified Chinese. - Translating the CLI launcher, logs, or ACP/agent output, and the ~1,100 diagnostic API error strings (`http.Error` / `gin.H{"error": …}`) — the SPA maps API failures onto its own translated copy, so those stay English by design. @@ -191,6 +209,14 @@ Frontend module contract (new): - **Catalog format**: commit JSON catalogs per namespace; loaded directly at runtime. Because keys (not source text) are the ids, CI verifies key/catalog drift rather than re-extracting — regeneration would destroy the English copy. +- **Simplified Chinese locale id**: use lowercase `zh-cn` as the canonical + persisted and document locale. Locale negotiation is case-insensitive, so + BCP-47 input such as `zh-CN` resolves to `zh-cn`. The switcher label is the + fixed endonym `简体中文`, not a translated message. +- **Real-locale parity**: every committed real locale must contain every English + namespace and exactly the same keys, including plural suffixes and `` + structures. Runtime English fallback remains a safety net, not a substitute + for a complete checked-in catalog. - **Library**: react-i18next (superseding the initial Lingui implementation) — chosen so the English copy lives in one place and components stay free of inline source text.