From e6efdb9e32bba72fcd095e15bd90826ee974d0b4 Mon Sep 17 00:00:00 2001 From: Roxi Date: Thu, 30 Jul 2026 16:00:45 +0800 Subject: [PATCH] feat(i18n): migrate renderer strings to i18next + catalog fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert all hardcoded UI strings in components and pages to useTranslation() / t() across 6 namespaces: common, components, home, onboarding, settings, workspace - Add missing ReviewerCard reflagged key (components.reviewer.reflagged) - Add 8 missing ProviderList keys in settings.model.list/.endpoint (browserLoginHint, tokenPrefix, modelPrefix, keyPrefix, modelsCount, expiresLabel, endpoint.speaksThe) with en/zh-Hans/zh-Hant translations - Fix curly-apostrophe in en/settings.json browserLoginHint (U+2019→U+0027) Simplifications (ponytail-review follow-up): - home-dialogs.render.test: replace 26-line inline mock with createI18nTestStub() from shared test stub - provider-key-security: remove unnecessary ApiKeySecurityCopyKeys named union; TS infers from as-const branches - resources.test: derive leafPaths from leafValues (-4 lines) All 7392 tests pass (kernel-executor failures are pre-existing/environmental) Co-Authored-By: Claude Opus 5 --- CONTRIBUTING.md | 8 +- docs/i18n-glossary.md | 143 +++ .../settings-general-darwin.png | Bin 110244 -> 116257 bytes package-lock.json | 83 +- package.json | 2 + scripts/fix-electron-path.cjs | 3 +- src/main/compute/job-notifier.test.ts | 7 +- src/main/settings/validate.test.ts | 4 +- src/main/settings/validate.ts | 4 +- src/main/skills/net-fetch.ts | 3 +- .../src/components/CloseConfirmModal.tsx | 29 +- .../src/components/CompletedJobCard.tsx | 23 +- .../src/components/DataRootMissingDialog.tsx | 28 +- .../src/components/DataRootWarning.tsx | 28 +- .../src/components/DownloadProgressLine.tsx | 4 +- src/renderer/src/components/FieldHelp.tsx | 53 +- .../src/components/GitHubStarBadge.tsx | 6 +- .../src/components/JobDetailModal.tsx | 28 +- .../src/components/JobStatusBadge.tsx | 29 +- .../src/components/JobTerminalOutput.tsx | 12 +- .../LanguageControls.render.test.tsx | 117 ++ .../src/components/LanguageControls.tsx | 112 ++ .../src/components/LegacyDataMoveDialog.tsx | 38 +- .../src/components/LifecycleToast.tsx | 9 +- .../src/components/RemoteJobBadge.tsx | 17 +- src/renderer/src/components/RemoteJobRow.tsx | 6 +- src/renderer/src/components/ReviewerCard.tsx | 128 +- src/renderer/src/components/ThemeControls.tsx | 51 +- src/renderer/src/components/UpdateCapsule.tsx | 6 +- src/renderer/src/components/UpdateDialog.tsx | 47 +- .../components-batch-i18n.render.test.tsx | 141 +++ .../components-i18n.render.test.tsx | 128 ++ .../GlobalSearchDialog.i18n.render.test.tsx | 275 +++++ .../global-search/GlobalSearchDialog.tsx | 130 +- .../global-search/global-search-catalog.ts | 6 +- .../components/streamdown/AgentMarkdown.tsx | 65 +- .../components/streamdown/LinkSafetyModal.tsx | 17 +- .../src/components/ui/message-scroller.tsx | 39 +- src/renderer/src/i18n/i18next.d.ts | 15 + src/renderer/src/i18n/index.ts | 58 + src/renderer/src/i18n/resources.test.ts | 181 +++ src/renderer/src/i18n/resources.ts | 60 + .../src/lib/format-relative-time.test.ts | 35 +- src/renderer/src/lib/format-relative-time.ts | 33 +- .../src/lib/locale-preference.test.ts | 109 ++ src/renderer/src/lib/locale-preference.ts | 71 ++ src/renderer/src/locales/en/common.json | 50 + src/renderer/src/locales/en/components.json | 207 ++++ src/renderer/src/locales/en/home.json | 60 + src/renderer/src/locales/en/onboarding.json | 86 ++ src/renderer/src/locales/en/settings.json | 1083 +++++++++++++++++ src/renderer/src/locales/en/workspace.json | 432 +++++++ src/renderer/src/locales/zh-Hans/common.json | 50 + .../src/locales/zh-Hans/components.json | 193 +++ src/renderer/src/locales/zh-Hans/home.json | 58 + .../src/locales/zh-Hans/onboarding.json | 86 ++ .../src/locales/zh-Hans/settings.json | 1071 ++++++++++++++++ .../src/locales/zh-Hans/workspace.json | 426 +++++++ src/renderer/src/locales/zh-Hant/common.json | 50 + .../src/locales/zh-Hant/components.json | 193 +++ src/renderer/src/locales/zh-Hant/home.json | 58 + .../src/locales/zh-Hant/onboarding.json | 86 ++ .../src/locales/zh-Hant/settings.json | 1071 ++++++++++++++++ .../src/locales/zh-Hant/workspace.json | 426 +++++++ src/renderer/src/main.tsx | 8 + .../src/pages/home/DeleteProjectDialog.tsx | 27 +- .../pages/home/HomePage.persistence.test.tsx | 26 +- src/renderer/src/pages/home/HomePage.tsx | 74 +- .../src/pages/home/ProjectFormDialog.tsx | 17 +- .../pages/home/home-dialogs.render.test.tsx | 6 + .../src/pages/onboarding/AgentStep.tsx | 15 +- .../pages/onboarding/EnvironmentSetupCard.tsx | 48 +- .../src/pages/onboarding/EnvironmentStep.tsx | 20 +- .../src/pages/onboarding/LocationStep.tsx | 71 +- .../src/pages/onboarding/NotebookStep.tsx | 15 +- .../src/pages/onboarding/OnboardingWizard.tsx | 23 +- .../src/pages/onboarding/ProviderStep.tsx | 69 +- .../src/pages/settings/ActiveModelSelect.tsx | 7 +- .../src/pages/settings/AgentFrameworkCard.tsx | 18 +- .../pages/settings/AgentHomeImportView.tsx | 94 +- .../pages/settings/AgentInstallSourceMenu.tsx | 24 +- .../src/pages/settings/AgentPanel.tsx | 58 +- .../src/pages/settings/AppIconSection.tsx | 16 +- .../src/pages/settings/AppVersionSection.tsx | 25 +- .../settings/ClaudeIsolatedSignInModal.tsx | 61 +- .../src/pages/settings/ComputeAddForm.tsx | 78 +- .../pages/settings/ComputeApprovalDialog.tsx | 45 +- .../src/pages/settings/ComputeHostDetail.tsx | 183 +-- .../src/pages/settings/ComputePanel.tsx | 82 +- .../src/pages/settings/ConnectorAddForm.tsx | 112 +- .../settings/ConnectorApprovalDialog.tsx | 29 +- .../pages/settings/ConnectorDetailView.tsx | 62 +- .../src/pages/settings/ConnectorsPanel.tsx | 97 +- .../settings/FileBrowserModal.render.test.tsx | 12 + .../src/pages/settings/FileBrowserModal.tsx | 244 ++-- .../src/pages/settings/GeneralPanel.tsx | 179 +-- .../ModelFrameworkCompatibilityAlert.tsx | 18 +- .../src/pages/settings/NetworkPanel.tsx | 43 +- .../settings/ProviderForm.render.test.tsx | 25 +- .../src/pages/settings/ProviderForm.tsx | 240 ++-- .../src/pages/settings/ProviderList.tsx | 140 ++- .../src/pages/settings/ProvidersPanel.tsx | 58 +- .../pages/settings/ReasoningEffortSelect.tsx | 9 +- .../pages/settings/RepairFrameworkDialog.tsx | 88 +- .../RuntimeUninstallControl.render.test.tsx | 8 +- .../settings/RuntimeUninstallControl.tsx | 6 +- .../src/pages/settings/RuntimesPanel.tsx | 153 +-- .../src/pages/settings/SettingsPage.tsx | 174 +-- .../src/pages/settings/SkillDetailView.tsx | 50 +- .../src/pages/settings/SkillEditor.tsx | 123 +- .../settings/SkillImportApprovalDialog.tsx | 48 +- .../settings/SkillImportCandidatePreview.tsx | 18 +- .../src/pages/settings/SkillImportView.tsx | 47 +- .../src/pages/settings/SkillUploadView.tsx | 201 ++- .../src/pages/settings/SkillsPanel.tsx | 113 +- .../pages/settings/StorageMigrationModal.tsx | 72 +- .../src/pages/settings/StoragePanel.tsx | 178 +-- .../pages/settings/SwitchFrameworkDialog.tsx | 14 +- .../pages/settings/ToolPermissionControl.tsx | 15 +- .../pages/settings/UninstallRuntimeDialog.tsx | 15 +- .../agent-dialogs-i18n.render.test.tsx | 107 ++ .../agent-home-import-i18n.render.test.tsx | 134 ++ .../settings/agent-panel-i18n.render.test.tsx | 113 ++ .../pages/settings/claude-install-progress.ts | 24 +- .../claude-sign-in-i18n.render.test.tsx | 95 ++ .../settings/compute-i18n.render.test.tsx | 276 +++++ .../pages/settings/compute-probed-label.ts | 22 + .../settings/connectors-i18n.render.test.tsx | 189 +++ .../file-browser-i18n.render.test.tsx | 292 +++++ .../general-sections-i18n.render.test.tsx | 145 +++ .../src/pages/settings/mirror-view.test.ts | 13 +- .../src/pages/settings/mirror-view.ts | 13 +- .../settings/model-panel-i18n.render.test.tsx | 114 ++ .../network-panel-i18n.render.test.tsx | 118 ++ .../settings/provider-form-value.test.ts | 10 +- .../src/pages/settings/provider-form-value.ts | 79 +- .../pages/settings/provider-key-security.ts | 40 +- .../settings/runtime-uninstall-hint.test.ts | 43 +- .../pages/settings/runtime-uninstall-hint.ts | 22 +- .../settings/runtimes-panel-i18n.test.ts | 53 + .../src/pages/settings/runtimes-panel-view.ts | 37 + .../skill-editor-i18n.render.test.tsx | 128 ++ .../skill-upload-i18n.render.test.tsx | 192 +++ .../settings/storage-i18n.render.test.tsx | 199 +++ .../pages/settings/validation-message.test.ts | 54 +- .../src/pages/settings/validation-message.ts | 46 +- .../workspace/ComposerAgentControlsMenu.tsx | 109 +- .../pages/workspace/ComposerContextUsage.tsx | 15 +- .../ComposerModelPicker.render.test.tsx | 8 +- .../pages/workspace/ComposerModelPicker.tsx | 20 +- .../src/pages/workspace/ConversationPanel.tsx | 65 +- .../pages/workspace/DeleteSessionDialog.tsx | 14 +- .../workspace/EditMessageConfirmDialog.tsx | 72 +- .../src/pages/workspace/EnvStatusBanner.tsx | 11 +- .../src/pages/workspace/NotebookPreview.tsx | 28 +- .../pages/workspace/NotebookRunOutputs.tsx | 52 +- .../workspace/PermissionApprovalControls.tsx | 177 +-- .../pages/workspace/PreviewFileSurface.tsx | 225 ++-- .../src/pages/workspace/PreviewPanel.tsx | 100 +- .../src/pages/workspace/ProjectFilesView.tsx | 104 +- .../pages/workspace/RenameSessionDialog.tsx | 17 +- .../src/pages/workspace/ReportErrorDialog.tsx | 56 +- .../workspace/SessionInterruptedBanner.tsx | 47 +- ...SessionNotebookDialog.interaction.test.tsx | 10 +- .../pages/workspace/SessionNotebookDialog.tsx | 62 +- .../pages/workspace/SessionReviewerPanel.tsx | 23 +- .../src/pages/workspace/SpecialistSubmenu.tsx | 33 +- .../workspace/WorkspaceAgentLoadingRow.tsx | 6 +- .../pages/workspace/WorkspaceMessageItem.tsx | 239 ++-- .../workspace/WorkspaceMessageScroller.tsx | 4 +- .../src/pages/workspace/WorkspacePage.tsx | 4 +- .../WorkspaceSidebar.render.test.tsx | 4 + .../src/pages/workspace/WorkspaceSidebar.tsx | 50 +- .../workspace/WorkspaceToolDetailsRow.tsx | 67 +- .../WorkspaceWebSearchActivityRow.tsx | 51 +- .../src/pages/workspace/artifact-preview.tsx | 16 +- .../workspace/composer-model-picker-utils.ts | 24 +- .../composer/ArtifactMentionPopup.tsx | 26 +- .../workspace/composer/SkillMentionPopup.tsx | 25 +- .../workspace/previews/PreviewFallback.tsx | 74 +- .../workspace/previews/PreviewToolContent.tsx | 5 +- .../workspace/previews/preview-errors.ts | 23 +- .../previews/renderers/CsvPreview.tsx | 11 +- .../previews/renderers/FastaPreview.tsx | 5 +- .../previews/renderers/HtmlPreview.tsx | 22 +- .../previews/renderers/ImagePreview.tsx | 6 +- .../previews/renderers/JsonPreview.tsx | 18 +- .../previews/renderers/MarkdownPreview.tsx | 5 +- .../previews/renderers/MoleculePreview.tsx | 21 +- .../previews/renderers/OfficePreview.tsx | 47 +- .../previews/renderers/PdbPreview.tsx | 47 +- .../previews/renderers/PdfPreview.tsx | 11 +- .../previews/renderers/PdfThumbnail.tsx | 4 +- .../previews/renderers/SourcePreview.tsx | 12 +- .../previews/renderers/TextPreview.tsx | 5 +- .../previews/renderers/ZoomablePreview.tsx | 8 +- .../workspace/session-dialogs.render.test.tsx | 4 + .../workspace/workspace-components.test.tsx | 6 +- src/renderer/src/stores/locale-store.ts | 36 + .../src/stores/session-job-store.test.ts | 21 +- src/renderer/web/bootstrap.ts | 8 + src/shared/download-progress.test.ts | 14 +- src/shared/download-progress.ts | 7 +- src/shared/locale.test.ts | 104 ++ src/shared/locale.ts | 76 ++ src/shared/remote-fs.ts | 4 +- src/shared/settings.ts | 45 +- test/i18n-test-stub.tsx | 62 + test/setup-i18n.ts | 11 + vitest.config.ts | 6 +- 210 files changed, 13962 insertions(+), 2866 deletions(-) create mode 100644 docs/i18n-glossary.md create mode 100644 src/renderer/src/components/LanguageControls.render.test.tsx create mode 100644 src/renderer/src/components/LanguageControls.tsx create mode 100644 src/renderer/src/components/components-batch-i18n.render.test.tsx create mode 100644 src/renderer/src/components/components-i18n.render.test.tsx create mode 100644 src/renderer/src/components/global-search/GlobalSearchDialog.i18n.render.test.tsx create mode 100644 src/renderer/src/i18n/i18next.d.ts create mode 100644 src/renderer/src/i18n/index.ts create mode 100644 src/renderer/src/i18n/resources.test.ts create mode 100644 src/renderer/src/i18n/resources.ts create mode 100644 src/renderer/src/lib/locale-preference.test.ts create mode 100644 src/renderer/src/lib/locale-preference.ts create mode 100644 src/renderer/src/locales/en/common.json create mode 100644 src/renderer/src/locales/en/components.json create mode 100644 src/renderer/src/locales/en/home.json create mode 100644 src/renderer/src/locales/en/onboarding.json create mode 100644 src/renderer/src/locales/en/settings.json create mode 100644 src/renderer/src/locales/en/workspace.json create mode 100644 src/renderer/src/locales/zh-Hans/common.json create mode 100644 src/renderer/src/locales/zh-Hans/components.json create mode 100644 src/renderer/src/locales/zh-Hans/home.json create mode 100644 src/renderer/src/locales/zh-Hans/onboarding.json create mode 100644 src/renderer/src/locales/zh-Hans/settings.json create mode 100644 src/renderer/src/locales/zh-Hans/workspace.json create mode 100644 src/renderer/src/locales/zh-Hant/common.json create mode 100644 src/renderer/src/locales/zh-Hant/components.json create mode 100644 src/renderer/src/locales/zh-Hant/home.json create mode 100644 src/renderer/src/locales/zh-Hant/onboarding.json create mode 100644 src/renderer/src/locales/zh-Hant/settings.json create mode 100644 src/renderer/src/locales/zh-Hant/workspace.json create mode 100644 src/renderer/src/pages/settings/agent-dialogs-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/agent-home-import-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/agent-panel-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/claude-sign-in-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/compute-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/compute-probed-label.ts create mode 100644 src/renderer/src/pages/settings/connectors-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/file-browser-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/general-sections-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/model-panel-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/network-panel-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/runtimes-panel-i18n.test.ts create mode 100644 src/renderer/src/pages/settings/runtimes-panel-view.ts create mode 100644 src/renderer/src/pages/settings/skill-editor-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/skill-upload-i18n.render.test.tsx create mode 100644 src/renderer/src/pages/settings/storage-i18n.render.test.tsx create mode 100644 src/renderer/src/stores/locale-store.ts create mode 100644 src/shared/locale.test.ts create mode 100644 src/shared/locale.ts create mode 100644 test/i18n-test-stub.tsx create mode 100644 test/setup-i18n.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a1cd1bf7..7531747e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,7 +117,13 @@ Use one of these standard type prefixes: - Formatting is handled by Prettier. `npm run format` is optional; review its changes before committing because it rewrites files across the repository. - Linting is enforced by ESLint; run `npm run lint`. -- Keep user-facing strings, comments, and documentation in English. +- Keep comments and documentation in English. +- User-facing strings go through i18next, not JSX literals. Author the copy in + `src/renderer/src/locales/en/`, then translate it in the `zh-Hans` and + `zh-Hant` folders — `docs/i18n-glossary.md` is the binding terminology + reference, and the catalog-parity tests fail on a missing or inconsistent + translation. Keys are semantic paths (`general.appearance.title`), never the + English source text. ## Required Checks diff --git a/docs/i18n-glossary.md b/docs/i18n-glossary.md new file mode 100644 index 000000000..2ce9bd462 --- /dev/null +++ b/docs/i18n-glossary.md @@ -0,0 +1,143 @@ +# Localization Glossary + +The binding reference for the `zh-Hans` and `zh-Hant` message catalogs under +`src/renderer/src/locales/`. English (`en`) is the source language: every key is authored there +first, and the other catalogs translate it. + +Traditional Chinese is a **separate translation**, not a character conversion of Simplified. The +software vocabulary genuinely differs (`file` is 文件 in Simplified but 檔案 in Traditional, where +文件 means _document_), so running a converter over `zh-Hans` produces wrong copy. Translate from the +English source and consult the tables below. + +## Kept in English + +Never translated, in any catalog: + +`Open Science` (product name), `Claude`, `Codex`, `opencode`, `Agent`, `Notebook`, `MCP`, `ACP`, +`API`, `CLI`, `SSH`, `GitHub`, `Star`, `Discord`, `Python`, `Jupyter`, `token`, and all model names. + +- `Agent` and `Notebook` are first-class feature names that appear in dense UI; the English terms + are shorter and less ambiguous than 智能体 / 笔记本. Explanatory prose may describe them in + Chinese, but the labels stay English. +- `token` stays English because `12k tokens` reads more clearly than 词元 or 令牌 in the context + indicator, and matches how the surrounding community writes it. +- The `Open Science` name is fixed by `docs/design.md`, but the home tagline beneath it **is** + translated. + +## Core domain nouns + +| en | zh-Hans | zh-Hant | +| ------------------ | ---------- | ---------- | +| project | 项目 | 專案 | +| session | 会话 | 會話 | +| conversation | 对话 | 對話 | +| workspace | 工作区 | 工作區 | +| message | 消息 | 訊息 | +| task | 任务 | 任務 | +| run | 运行 | 執行 | +| turn | 轮次 | 輪次 | +| agent framework | Agent 框架 | Agent 框架 | +| model | 模型 | 模型 | +| provider | 模型服务商 | 模型服務商 | +| subscription | 订阅 | 訂閱 | +| skill | 技能 | 技能 | +| connector | 连接器 | 連接器 | +| kernel | 内核 | 核心 | +| artifact | 产物 | 產物 | +| activity group | 活动分组 | 活動分組 | +| tool | 工具 | 工具 | +| compute host | 计算主机 | 運算主機 | +| runtime | 运行时 | 執行環境 | +| environment | 环境 | 環境 | +| preview | 预览 | 預覽 | +| reasoning effort | 推理强度 | 推理強度 | +| context | 上下文 | 上下文 | +| context compaction | 上下文压缩 | 上下文壓縮 | + +## Simplified / Traditional divergences + +The highest-risk table. A character converter gets most of these wrong. + +| en | zh-Hans | zh-Hant | +| ------------------ | ------- | -------- | +| file | 文件 | 檔案 | +| document | 文档 | 文件 | +| folder | 文件夹 | 資料夾 | +| data | 数据 | 資料 | +| information | 信息 | 資訊 | +| software | 软件 | 軟體 | +| program | 程序 | 程式 | +| default | 默认 | 預設 | +| settings | 设置 | 設定 | +| network | 网络 | 網路 | +| cache | 缓存 | 快取 | +| process | 进程 | 行程 | +| thread | 线程 | 執行緒 | +| queue | 队列 | 佇列 | +| storage | 存储 | 儲存 | +| credential | 凭据 | 憑證 | +| log | 日志 | 記錄檔 | +| mirror | 镜像源 | 鏡像來源 | +| tray | 托盘 | 系統匣 | +| bookmark | 书签 | 書籤 | +| archive (verb) | 归档 | 封存 | +| approve / approval | 批准 | 核准 | + +Note the `file` / `document` inversion: Traditional 文件 means what Simplified calls 文档. Getting +this pair backwards is the single most common failure in Simplified-to-Traditional conversion. + +## Actions and states + +| en | zh-Hans | zh-Hant | +| -------------------- | -------------- | -------------- | +| create / new | 新建 | 新增 | +| edit | 编辑 | 編輯 | +| rename | 重命名 | 重新命名 | +| delete | 删除 | 刪除 | +| retry | 重试 | 重試 | +| resume | 继续 | 繼續 | +| stop | 停止 | 停止 | +| cancel | 取消 | 取消 | +| install / uninstall | 安装 / 卸载 | 安裝 / 移除 | +| validate | 验证 | 驗證 | +| import / export | 导入 / 导出 | 匯入 / 匯出 | +| upload / download | 上传 / 下载 | 上傳 / 下載 | +| reveal in folder | 在文件夹中显示 | 在資料夾中顯示 | +| minimize to tray | 最小化到托盘 | 最小化至系統匣 | +| idle | 空闲 | 閒置 | +| running | 运行中 | 執行中 | +| waiting for approval | 等待批准 | 等待核准 | +| failed | 失败 | 失敗 | +| completed | 已完成 | 已完成 | +| pending | 待处理 | 待處理 | + +## Interface chrome + +| en | zh-Hans | zh-Hant | +| --------------------- | ------------------ | ------------------ | +| Home | 首页 | 首頁 | +| Onboarding | 初始设置 | 初始設定 | +| General | 通用 | 一般 | +| Appearance | 外观 | 外觀 | +| Theme | 主题 | 主題 | +| System / Light / Dark | 系统 / 浅色 / 深色 | 系統 / 淺色 / 深色 | +| Language | 语言 | 語言 | +| Notifications | 通知 | 通知 | +| Diagnostics | 诊断 | 診斷 | +| Permissions | 权限 | 權限 | +| Data root | 数据目录 | 資料目錄 | +| Command line tool | 命令行工具 | 命令列工具 | + +## Style rules + +- Full-width punctuation (`,。:;?`) in Chinese prose. Code, paths, and commands keep their + original half-width characters. +- One half-width space between Chinese and Latin script (`使用 Claude 模型`). No space between a + number and a Chinese unit that reads as one word (`5 分钟` takes the space; `12k` is not split). +- Second person is 你, never 您 — it matches the supportive, non-authoritative tone `docs/design.md` + asks for. +- Short labels (buttons, table headers, menu items) take no trailing period. Full sentences do. +- No exclamation points, per `docs/design.md`. +- Don't pad imperatives with 请. `Check the network` is 检查网络连接, not 请检查网络连接. +- Language names in the language picker are written in their own language and never translated: + `English`, `简体中文`, `繁體中文`. Only the `System` option follows the interface language. diff --git a/e2e/visual-regression.spec.ts-snapshots/settings-general-darwin.png b/e2e/visual-regression.spec.ts-snapshots/settings-general-darwin.png index f88087e3c6a625ad62b0ae880607a39d1b1045ef..d214cb2e4ff29d2fe047b24ce2441c592566e9b6 100644 GIT binary patch literal 116257 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%0Av)qk2IPY$kVDZSsrEQnHk!Jb=WFJ| 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

H1lBC5&7Ln*hM0Lgw8O3A|VM)$sgi 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�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

V+- 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%0A}PBr6&wk)Z9PN*j^<`) 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 diff --git a/package-lock.json b/package-lock.json index c6ec90ec0..cde2d8882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "i18next": "^25.10.10", "jsdom": "^29.1.1", "lucide-react": "^1.23.0", "motion": "^12.42.2", @@ -82,6 +83,7 @@ "radix-ui": "^1.6.1", "react": "^19.2.1", "react-dom": "^19.2.1", + "react-i18next": "^16.6.6", "react-resizable-panels": "^4.12.0", "react-zoom-pan-pinch": "^4.0.3", "saxes": "6.0.0", @@ -13118,6 +13120,16 @@ "dev": true, "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.1.0.tgz", + "integrity": "sha512-E0oAXcELOtsXe+BmpJ2EZyedbldPpriV5vICzEuo6xjC/D1lDukOI7KrpfQGF2Qc4wWEy0nk3bFORS2K5ZAhFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "license": "MIT", @@ -13199,6 +13211,38 @@ "node": ">=18.18.0" } }, + "node_modules/i18next": { + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "license": "MIT", @@ -18324,6 +18368,34 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-i18next": { + "version": "16.6.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.6.tgz", + "integrity": "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.10.9", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "license": "MIT" @@ -21102,7 +21174,6 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -21860,6 +21931,16 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "dev": true, diff --git a/package.json b/package.json index ab82138b0..03381d81b 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "i18next": "^25.10.10", "jsdom": "^29.1.1", "lucide-react": "^1.23.0", "motion": "^12.42.2", @@ -115,6 +116,7 @@ "radix-ui": "^1.6.1", "react": "^19.2.1", "react-dom": "^19.2.1", + "react-i18next": "^16.6.6", "react-resizable-panels": "^4.12.0", "react-zoom-pan-pinch": "^4.0.3", "saxes": "6.0.0", diff --git a/scripts/fix-electron-path.cjs b/scripts/fix-electron-path.cjs index 6e33dc7b8..513879c62 100644 --- a/scripts/fix-electron-path.cjs +++ b/scripts/fix-electron-path.cjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-require-imports */ // Normalize node_modules/electron/path.txt so the electron binary resolves without a trailing // newline. Different npmmirror CDNs / extraction paths have shipped this 32-byte file with an // appended LF; electron's own index.js reads it via fs.readFileSync(pathFile, 'utf-8') without @@ -30,5 +30,4 @@ if (normalized === raw) { fs.writeFileSync(pathFile, normalized, 'utf-8') -// eslint-disable-next-line no-console console.log('fix-electron-path: stripped trailing newline from node_modules/electron/path.txt') diff --git a/src/main/compute/job-notifier.test.ts b/src/main/compute/job-notifier.test.ts index ee9f4f041..234d364e6 100644 --- a/src/main/compute/job-notifier.test.ts +++ b/src/main/compute/job-notifier.test.ts @@ -215,7 +215,12 @@ describe('emitJobNotification', () => { const hostRepo: Pick = { get: vi.fn() } const broadcast = vi.fn() - await emitJobNotification(job, { jobRepository: jobRepo, hostRepository: hostRepo, storageRoot, broadcast }) + await emitJobNotification(job, { + jobRepository: jobRepo, + hostRepository: hostRepo, + storageRoot, + broadcast + }) // Neither update nor broadcast should be called expect(mockUpdate).not.toHaveBeenCalled() diff --git a/src/main/settings/validate.test.ts b/src/main/settings/validate.test.ts index bf8f58c3e..6d09d8588 100644 --- a/src/main/settings/validate.test.ts +++ b/src/main/settings/validate.test.ts @@ -431,9 +431,7 @@ describe('validate: provider dispatch', () => { it('surfaces a 5xx error message from a bridge validation', async () => { const fetchImpl = vi .fn() - .mockResolvedValue( - new Response('{"error":{"message":"Gateway timeout"}}', { status: 502 }) - ) + .mockResolvedValue(new Response('{"error":{"message":"Gateway timeout"}}', { status: 502 })) const result = await validateProvider( { type: 'custom', apiEndpoints: ['openai'], baseUrl: 'https://g/v1', key: 'k', model: 'm' }, diff --git a/src/main/settings/validate.ts b/src/main/settings/validate.ts index 24dece909..b049de396 100644 --- a/src/main/settings/validate.ts +++ b/src/main/settings/validate.ts @@ -499,7 +499,9 @@ const validateProviderThroughLocalResponsesAdapter = async ( if (category !== 'ok') { return toResult(category, { status: response.status, - ...(category === 'unknown' || category === 'server-error' ? { message: providerMessage } : {}) + ...(category === 'unknown' || category === 'server-error' + ? { message: providerMessage } + : {}) }) } if (!adapter.hasRequiredToolCall(bodyText)) { diff --git a/src/main/skills/net-fetch.ts b/src/main/skills/net-fetch.ts index 4188a8ae5..5c6052df2 100644 --- a/src/main/skills/net-fetch.ts +++ b/src/main/skills/net-fetch.ts @@ -13,5 +13,4 @@ export const netFetch: FetchLike = (url, init) => // probe, which reads the error body and aborts on timeout. Same proxy-honoring Chromium stack. A lazy // arrow wrapper (like netFetch) so `net.fetch` is only read at call time — reading it eagerly at module // load crashes any test whose electron mock omits `net` — while the method call preserves the receiver. -export const netFetchStandard = ((input, init) => - net.fetch(input as string, init)) as typeof fetch +export const netFetchStandard = ((input, init) => net.fetch(input as string, init)) as typeof fetch diff --git a/src/renderer/src/components/CloseConfirmModal.tsx b/src/renderer/src/components/CloseConfirmModal.tsx index 55c62fab7..7bc631495 100644 --- a/src/renderer/src/components/CloseConfirmModal.tsx +++ b/src/renderer/src/components/CloseConfirmModal.tsx @@ -1,5 +1,6 @@ import { AlertDialog } from 'radix-ui' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { dialogOverlayClassName, dialogPanelClassName } from '@/components/ui/dialog-chrome' @@ -7,6 +8,7 @@ import { useRetainedDialogValue } from '@/components/ui/use-retained-dialog-valu import { resolveActiveSessionDisplay, truncateLabel } from '@/lib/active-session-display' import { cn } from '@/lib/utils' import { useNavigationStore } from '@/stores/navigation-store' +import { APP } from '../../../shared/app-config' import type { ActiveSessionInfo } from '../../../shared/storage' import type { CloseConfirmChoice, @@ -30,6 +32,7 @@ export const CloseConfirmModal = ({ }: { onOpenChange?: (open: boolean) => void }): React.JSX.Element | null => { + const { t } = useTranslation(['components', 'common']) const [request, setRequest] = useState(undefined) const [remember, setRemember] = useState(true) @@ -61,10 +64,12 @@ export const CloseConfirmModal = ({ const isQuitVariant = dialogRequest.variant === 'quit' const hasSessions = dialogRequest.sessions.length > 0 - const title = isQuitVariant ? 'Quit Open Science?' : 'Minimize or quit?' + const title = isQuitVariant + ? t('components:closeConfirm.quitTitle', { appName: APP.name }) + : t('components:closeConfirm.chooseTitle') const description = isQuitVariant - ? 'Work is still running and will be interrupted if you quit.' - : 'This app can keep running in the tray, or you can quit.' + ? t('components:closeConfirm.quitDescription') + : t('components:closeConfirm.chooseDescription') return ( ) @@ -123,23 +134,23 @@ export const CloseConfirmModal = ({ onChange={(event) => setRemember(event.target.checked)} className="size-4 shrink-0 accent-primary" /> - Don't ask again + {t('components:closeConfirm.dontAskAgain')} ) : null}

diff --git a/src/renderer/src/components/CompletedJobCard.tsx b/src/renderer/src/components/CompletedJobCard.tsx index 373cfe54f..d547787ee 100644 --- a/src/renderer/src/components/CompletedJobCard.tsx +++ b/src/renderer/src/components/CompletedJobCard.tsx @@ -1,4 +1,5 @@ import { Zap, ChevronRight } from 'lucide-react' +import { useTranslation } from 'react-i18next' import type { JobSummary } from '../../../shared/compute' @@ -13,24 +14,28 @@ type CompletedJobCardProps = { onOpen: (job: JobSummary) => void } -// Returns the status label and color class based on job status. -function getStatusDisplay(job: JobSummary): { label: string; colorClass: string } { +type StatusKey = 'jobRow.finished' | 'jobRow.failed' | 'jobRow.timedOut' | 'jobRow.error' + +// Returns the catalog key and color class for a job's terminal status. +function getStatusLabelKey(job: JobSummary): { key: StatusKey | null; colorClass: string } { switch (job.status) { case 'success': - return { label: 'finished', colorClass: 'text-green-600 dark:text-green-400' } + return { key: 'jobRow.finished', colorClass: 'text-green-600 dark:text-green-400' } case 'failed': - return { label: 'failed', colorClass: 'text-red-600 dark:text-red-400' } + return { key: 'jobRow.failed', colorClass: 'text-red-600 dark:text-red-400' } case 'timeout': - return { label: 'timed out', colorClass: 'text-red-600 dark:text-red-400' } + return { key: 'jobRow.timedOut', colorClass: 'text-red-600 dark:text-red-400' } case 'error': - return { label: 'error', colorClass: 'text-red-600 dark:text-red-400' } + return { key: 'jobRow.error', colorClass: 'text-red-600 dark:text-red-400' } default: - return { label: job.status, colorClass: 'text-muted-foreground' } + return { key: null, colorClass: 'text-muted-foreground' } } } export function CompletedJobCard({ job, onOpen }: CompletedJobCardProps): React.JSX.Element { - const { label, colorClass } = getStatusDisplay(job) + const { t } = useTranslation('components') + const { key, colorClass } = getStatusLabelKey(job) + const label = key ? t(key) : job.status const intentDisplay = job.intent.length > 70 ? `${job.intent.slice(0, 67)}…` : job.intent @@ -40,7 +45,7 @@ export function CompletedJobCard({ job, onOpen }: CompletedJobCardProps): React. data-testid="completed-job-card" className="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border border-border bg-muted/30 px-3.5 py-2.5 text-left text-[12px] hover:bg-muted/50 transition-colors" onClick={() => onOpen(job)} - aria-label={`Completed remote job: ${job.intent}`} + aria-label={t('jobRow.cardAriaLabel', { intent: job.intent })} >
- {formatProgressLine(progress)} + {formatProgressLine(t, progress)}
( - - - - + + - - -) + {content} + + + + ) +} export { FieldHelp } diff --git a/src/renderer/src/components/GitHubStarBadge.tsx b/src/renderer/src/components/GitHubStarBadge.tsx index de124890a..442370c11 100644 --- a/src/renderer/src/components/GitHubStarBadge.tsx +++ b/src/renderer/src/components/GitHubStarBadge.tsx @@ -1,5 +1,6 @@ import { Star } from 'lucide-react' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { formatStarCount } from '@/lib/format-star-count' import { cn } from '@/lib/utils' @@ -22,6 +23,7 @@ type GitHubStarBadgeProps = { // unavailable it degrades to an icon-only link. Clicking opens the repo in the system browser via // the window-open handler in src/main/windows.ts. const GitHubStarBadge = ({ className }: GitHubStarBadgeProps): React.JSX.Element => { + const { t } = useTranslation('components') const [stars, setStars] = useState(null) useEffect(() => { @@ -43,7 +45,9 @@ const GitHubStarBadge = ({ className }: GitHubStarBadgeProps): React.JSX.Element }, []) const label = - stars === null ? `Open ${APP.name} on GitHub` : `Star ${APP.name} on GitHub, ${stars} stars` + stars === null + ? t('github.openAriaLabel', { app: APP.name }) + : t('github.starAriaLabel', { app: APP.name, count: stars }) return ( s.jobsById) const jobs = Array.from(jobsById.values()) .filter((j) => j.session_id === sessionId) @@ -45,7 +47,7 @@ function SessionJobsList({
{jobs.length === 0 ? (
- No remote jobs in this session. + {t('jobs.empty')}
) : ( jobs.map((job) => { @@ -84,7 +86,7 @@ function SessionJobsList({ {/* Footer */}
@@ -102,6 +104,7 @@ type JobDetailViewProps = { } function JobDetailView({ job, onBack, onOpenFileBrowser }: JobDetailViewProps): React.JSX.Element { + const { t } = useTranslation('components') const [activeTab, setActiveTab] = useState('stdout') // Pull latest data from the store on every render (store subscribes to compute:job-updated). @@ -163,7 +166,7 @@ function JobDetailView({ job, onBack, onOpenFileBrowser }: JobDetailViewProps): onClick={onBack} >
- Job ID + + {t('jobs.jobId')} + {latestJob.job_id} @@ -293,6 +298,7 @@ export function JobDetailModal({ initialJob, onClose }: JobDetailModalProps): React.JSX.Element { + const { t } = useTranslation('components') const [view, setView] = useState(() => initialJob ? { kind: 'detail', job: initialJob } : { kind: 'list' } ) @@ -346,14 +352,14 @@ export function JobDetailModal({ className={dialogPanelClassName( 'z-[70] flex w-[640px] max-w-[calc(100vw-2rem)] max-h-[82vh] flex-col overflow-hidden p-0' )} - aria-label="Remote job details" + aria-label={t('jobs.modalLabel')} data-testid="job-detail-modal" > {/* Header */}
- Running jobs in this session + {t('jobs.listTitle')} - diff --git a/src/renderer/src/components/JobStatusBadge.tsx b/src/renderer/src/components/JobStatusBadge.tsx index e1f81380d..0f1e0d12a 100644 --- a/src/renderer/src/components/JobStatusBadge.tsx +++ b/src/renderer/src/components/JobStatusBadge.tsx @@ -1,44 +1,50 @@ +import { useTranslation } from 'react-i18next' + import type { ComputeJobStatus } from '../../../shared/compute' // Maps job status to badge styling. Follows design.md §6 badge pattern (same conventions as ReviewCard). // Terminal statuses consolidate under semantic groups: success=Done, error/failed/timeout=Failed. -const STATUS_STYLE: Record = { +// The label is a catalog key rather than literal text — the map is module-level, so it cannot call a +// hook; the component resolves it at render time and re-resolves when the language changes. +// `as const satisfies` rather than a plain annotation: the literal key strings must survive for t()'s +// compile-time key checking, while `satisfies` still forces every ComputeJobStatus to be covered. +const STATUS_STYLE = { queued: { - label: 'Queued', + labelKey: 'jobStatus.queued', className: 'bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-950/40 dark:text-slate-400 dark:border-slate-800/50' }, submitted: { - label: 'Queued', + labelKey: 'jobStatus.queued', className: 'bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-950/40 dark:text-slate-400 dark:border-slate-800/50' }, running: { - label: 'Running', + labelKey: 'jobStatus.running', className: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/20 dark:text-amber-300 dark:border-amber-800/50' }, success: { - label: 'Done', + labelKey: 'jobStatus.done', className: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-950/20 dark:text-green-300 dark:border-green-800/50' }, failed: { - label: 'Failed', + labelKey: 'jobStatus.failed', className: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/20 dark:text-red-300 dark:border-red-800/50' }, timeout: { - label: 'Timeout', + labelKey: 'jobStatus.timeout', className: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/20 dark:text-red-300 dark:border-red-800/50' }, error: { - label: 'Error', + labelKey: 'jobStatus.error', className: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/20 dark:text-red-300 dark:border-red-800/50' } -} +} as const satisfies Record type JobStatusBadgeProps = { status: ComputeJobStatus @@ -46,13 +52,14 @@ type JobStatusBadgeProps = { // Status badge for job detail modal header (top-right). Follows design.md §6 color table. export function JobStatusBadge({ status }: JobStatusBadgeProps): React.JSX.Element { - const { label, className } = STATUS_STYLE[status] + const { t } = useTranslation('components') + const { labelKey, className } = STATUS_STYLE[status] return ( - {label} + {t(labelKey)} ) } diff --git a/src/renderer/src/components/JobTerminalOutput.tsx b/src/renderer/src/components/JobTerminalOutput.tsx index 9be93bca5..55b1cdd0b 100644 --- a/src/renderer/src/components/JobTerminalOutput.tsx +++ b/src/renderer/src/components/JobTerminalOutput.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from 'react-i18next' + // Dark terminal output box for job stdout/stderr (design.md §6 JobTerminalOutput). // Background #0d1117 and color #c9d1d9 are component-local — not in global CSS tokens. type JobTerminalOutputProps = { @@ -6,11 +8,15 @@ type JobTerminalOutputProps = { } // Renders the tail of stdout or stderr in a dark terminal-style box. -// Shows a dim empty-state prompt when no content is available. +// Shows a dim empty-state prompt when no content is available. The empty message defaults inside the +// body rather than in the parameter list — a default value can't call a hook. export function JobTerminalOutput({ content, - emptyMessage = 'No output yet.' + emptyMessage }: JobTerminalOutputProps): React.JSX.Element { + const { t } = useTranslation('components') + const emptyText = emptyMessage ?? t('jobs.noOutput') + if (!content || content.trim().length === 0) { return (
- {emptyMessage} + {emptyText}
) } diff --git a/src/renderer/src/components/LanguageControls.render.test.tsx b/src/renderer/src/components/LanguageControls.render.test.tsx new file mode 100644 index 000000000..36f61388d --- /dev/null +++ b/src/renderer/src/components/LanguageControls.render.test.tsx @@ -0,0 +1,117 @@ +// @vitest-environment jsdom +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { i18next } from '@/i18n' +import { useLocaleStore } from '@/stores/locale-store' +import { clickRadixMenuItem, openRadixMenu } from '@/pages/settings/test-utils' +import { LanguagePreferenceMenu, LanguageSelect } from './LanguageControls' + +if (!Element.prototype.hasPointerCapture) { + Element.prototype.hasPointerCapture = (): boolean => false + Element.prototype.setPointerCapture = (): void => undefined + Element.prototype.releasePointerCapture = (): void => undefined +} +if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = (): void => undefined +} + +let container: HTMLDivElement +let root: Root + +const render = (element: React.JSX.Element): void => { + act(() => { + root.render(element) + }) +} + +beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + localStorage.clear() + useLocaleStore.setState({ preference: 'system', locale: 'en' }) +}) + +afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() + localStorage.clear() + useLocaleStore.setState({ preference: 'system', locale: 'en' }) + act(() => { + void i18next.changeLanguage('en') + }) +}) + +describe('LanguageSelect', () => { + it('lists System plus every locale, each written in its own language', () => { + render() + openRadixMenu(container.querySelector('button')) + + const options = Array.from(document.querySelectorAll('[role="option"]')).map( + (option) => option.textContent + ) + + // Language names are never translated — a reader stranded in the wrong language must still be + // able to recognize their own. + expect(options).toEqual(['System', 'English', '简体中文', '繁體中文']) + }) + + it('switches the interface language and persists the choice', () => { + render() + openRadixMenu(container.querySelector('button')) + + const traditional = Array.from(document.querySelectorAll('[role="option"]')).find((option) => + option.textContent?.includes('繁體中文') + ) + clickRadixMenuItem(traditional as HTMLElement) + + expect(useLocaleStore.getState().preference).toBe('zh-Hant') + expect(useLocaleStore.getState().locale).toBe('zh-Hant') + expect(i18next.language).toBe('zh-Hant') + expect(document.documentElement.lang).toBe('zh-Hant') + expect(localStorage.getItem('open-science-language')).toBe('zh-Hant') + }) + + it('renders its own label in the newly selected language', () => { + render() + expect(container.querySelector('button')?.getAttribute('aria-label')).toBe('Interface language') + + act(() => { + useLocaleStore.getState().setPreference('zh-Hans') + }) + + expect(container.querySelector('button')?.getAttribute('aria-label')).toBe('界面语言') + }) +}) + +describe('LanguagePreferenceMenu', () => { + it('names the active choice in its trigger', () => { + render() + expect(container.querySelector('button')?.getAttribute('aria-label')).toBe('Language: System') + + act(() => { + useLocaleStore.getState().setPreference('zh-Hant') + }) + + expect(container.querySelector('button')?.getAttribute('aria-label')).toBe('語言: 繁體中文') + }) + + it('marks only the active choice', () => { + act(() => { + useLocaleStore.getState().setPreference('zh-Hans') + }) + render() + openRadixMenu(container.querySelector('button')) + + const active = Array.from(document.querySelectorAll('[role="menuitem"]')).filter((item) => + item.querySelector('svg:last-child') + ) + + expect(active).toHaveLength(1) + expect(active[0]?.textContent).toContain('简体中文') + }) +}) diff --git a/src/renderer/src/components/LanguageControls.tsx b/src/renderer/src/components/LanguageControls.tsx new file mode 100644 index 000000000..96f190b03 --- /dev/null +++ b/src/renderer/src/components/LanguageControls.tsx @@ -0,0 +1,112 @@ +import { Check, Languages } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select' +import { cn } from '@/lib/utils' +import { useLocaleStore } from '@/stores/locale-store' +import { LANGUAGE_PREFERENCES, type LanguagePreference } from '../../../shared/locale' + +// Language names are written in their own language and never translated, so a reader who has landed in +// the wrong language can still find their way out. Only the 'System' option follows the interface +// language. Order puts 'System' first, then the locales in LOCALES order. +const useOptions = (): { value: LanguagePreference; label: string; description?: string }[] => { + const { t } = useTranslation() + + return LANGUAGE_PREFERENCES.map((value) => + value === 'system' + ? { + value, + label: t('language.system'), + description: t('language.systemDescription') + } + : { value, label: t(`language.names.${value}` as const) } + ) +} + +// Language picker for Settings > Appearance. A Select rather than a segmented control: four options +// with CJK labels overflow the row width the theme control fits into. +export const LanguageSelect = (): React.JSX.Element => { + const { t } = useTranslation() + const preference = useLocaleStore((state) => state.preference) + const setPreference = useLocaleStore((state) => state.setPreference) + const options = useOptions() + const active = options.find((option) => option.value === preference) ?? options[0] + + return ( + + ) +} + +type LanguagePreferenceMenuProps = { + className?: string +} + +// Compact icon button for the home header, sized to match the neighboring theme / GitHub / settings +// actions. The trigger is a single glyph (unlike the theme menu, whose icon encodes the current +// choice) because a language has no natural icon; the active choice is shown by the check in the list. +export const LanguagePreferenceMenu = ({ + className +}: LanguagePreferenceMenuProps): React.JSX.Element => { + const { t } = useTranslation() + const preference = useLocaleStore((state) => state.preference) + const setPreference = useLocaleStore((state) => state.setPreference) + const options = useOptions() + const active = options.find((option) => option.value === preference) ?? options[0] + + return ( + + + + + + {t('language.label')} + {options.map(({ value, label, description }) => ( + setPreference(value)} className="gap-2"> + + {label} + {description ? ( + + {description} + + ) : null} + + {preference === value ? ( + + ))} + + + ) +} diff --git a/src/renderer/src/components/LegacyDataMoveDialog.tsx b/src/renderer/src/components/LegacyDataMoveDialog.tsx index b010d3a21..4c67a7803 100644 --- a/src/renderer/src/components/LegacyDataMoveDialog.tsx +++ b/src/renderer/src/components/LegacyDataMoveDialog.tsx @@ -1,6 +1,7 @@ import { AlertDialog } from 'radix-ui' import { FolderInput, FolderOpen } from 'lucide-react' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { @@ -10,6 +11,7 @@ import { dialogTitleClassName } from '@/components/ui/dialog-chrome' import { StorageMigrationModal } from '@/pages/settings/StorageMigrationModal' +import { APP } from '../../../shared/app-config' type LegacyDataMoveDialogProps = { // The hidden config root where a legacy install's data currently lives (e.g. ~/.open-science). @@ -32,6 +34,7 @@ const LegacyDataMoveDialog = ({ defaultParent, onDismiss }: LegacyDataMoveDialogProps): React.JSX.Element => { + const { t } = useTranslation('components') // When set, hand off to the shared migration modal targeting this parent; null returns to the prompt. const [migrationTarget, setMigrationTarget] = useState(null) // The exact /OpenScience path "Move to OpenScience" would create. Resolved server-side via @@ -68,8 +71,8 @@ const LegacyDataMoveDialog = ({ // would mean abandoning the legacy data, which isn't what "move it out" should do here. setPickError( inspection.kind === 'adopt' - ? 'That folder already contains Open Science data. Pick an empty folder, or use the default location.' - : (inspection.error ?? 'That folder can’t be used. Pick another one.') + ? t('dataRoot.legacyMove.occupied', { appName: APP.name }) + : (inspection.error ?? t('dataRoot.legacyMove.unusable')) ) } finally { setIsPicking(false) @@ -96,30 +99,33 @@ const LegacyDataMoveDialog = ({ - Move your data to a visible folder? + {t('dataRoot.legacyMove.title')} - Your research data is in a hidden folder. Moving it into a visible OpenScience folder - makes it easy to find and back up — your settings and history stay where they are. + {t('dataRoot.legacyMove.description')}
- Current (hidden) + + {t('dataRoot.legacyMove.currentLabel')} +
                 {currentDataRoot}
               
- New location + + {t('dataRoot.legacyMove.newLabel')} +
-                {destination ?? 'Resolving…'}
+                {destination ?? t('dataRoot.legacyMove.resolving')}
               
@@ -133,7 +139,7 @@ const LegacyDataMoveDialog = ({
-

- You can always change this later in Settings → Data location. We won't ask again. -

+

{t('dataRoot.legacyMove.note')}

diff --git a/src/renderer/src/components/LifecycleToast.tsx b/src/renderer/src/components/LifecycleToast.tsx index 69c5f1de5..216ee3126 100644 --- a/src/renderer/src/components/LifecycleToast.tsx +++ b/src/renderer/src/components/LifecycleToast.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { X } from 'lucide-react' +import { useTranslation } from 'react-i18next' import type { ExternalSessionNotice } from '@/hooks/useLifecycleSync' @@ -14,6 +15,8 @@ const LifecycleToast = ({ onDismiss: () => void onView: () => void }): React.JSX.Element | null => { + const { t } = useTranslation('components') + useEffect(() => { if (!notice) return const timeout = window.setTimeout(onDismiss, AUTO_DISMISS_MS) @@ -29,7 +32,7 @@ const LifecycleToast = ({ className="fixed right-3 top-3 z-50 flex max-w-sm items-center gap-3 rounded-lg border border-border-100 bg-bg-200 px-3 py-2 text-sm text-text-100 shadow-lg" > - Session created externally + {t('lifecycleToast.sessionCreated')} {notice.title} @@ -39,11 +42,11 @@ const LifecycleToast = ({ onClick={onView} className="shrink-0 rounded px-2 py-1 text-xs font-medium text-accent-main-100 hover:bg-bg-300" > - View + {t('lifecycleToast.view')}

- REMOTE · {activeJobs.length} + {t('jobs.tooltipHeading', { count: activeJobs.length })}

{activeJobs.map((job) => (
@@ -113,10 +118,10 @@ export const RemoteJobBadge = ({ sessionId, onOpenJobList }: RemoteJobBadgeProps gap: '4px', cursor: onOpenJobList ? 'pointer' : 'default' }} - aria-label={`${allJobs.length} remote job${allJobs.length !== 1 ? 's' : ''}`} + aria-label={t('jobs.badgeIdleLabel', { count: allJobs.length })} > - {allJobs.length} jobs + {t('jobs.badgeIdleText', { count: allJobs.length })} ) } diff --git a/src/renderer/src/components/RemoteJobRow.tsx b/src/renderer/src/components/RemoteJobRow.tsx index 4820d608f..c6562f123 100644 --- a/src/renderer/src/components/RemoteJobRow.tsx +++ b/src/renderer/src/components/RemoteJobRow.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Zap, ChevronRight } from 'lucide-react' import type { JobSummary } from '../../../shared/compute' @@ -15,6 +16,7 @@ type RemoteJobRowProps = { } export function RemoteJobRow({ job, onOpen }: RemoteJobRowProps): React.JSX.Element { + const { t } = useTranslation('components') const [now, setNow] = useState(() => Date.now()) // Tick every second while the job is still running so elapsed time stays fresh. @@ -36,7 +38,7 @@ export function RemoteJobRow({ job, onOpen }: RemoteJobRowProps): React.JSX.Elem data-testid="remote-job-row" className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left hover:bg-muted/50 transition-colors" onClick={() => onOpen(job)} - aria-label={`Remote job: ${job.intent}`} + aria-label={t('jobRow.rowAriaLabel', { intent: job.intent })} > - {job.status === 'submitted' ? 'queued' : 'running'} · {elapsedStr} + {job.status === 'submitted' ? t('jobRow.queued') : t('jobRow.running')} · {elapsedStr}
)} diff --git a/src/renderer/src/components/ThemeControls.tsx b/src/renderer/src/components/ThemeControls.tsx index f1583ab83..405aa5bb3 100644 --- a/src/renderer/src/components/ThemeControls.tsx +++ b/src/renderer/src/components/ThemeControls.tsx @@ -1,4 +1,5 @@ import { Check, Monitor, Moon, Sun } from 'lucide-react' +import { useTranslation } from 'react-i18next' import { DropdownMenu, @@ -11,34 +12,39 @@ import { cn } from '@/lib/utils' import type { ThemePreference } from '@/lib/theme' import { useThemeStore } from '@/stores/theme-store' -type ThemeOption = { +// Single source of truth for the three choices, shared by the settings segmented control and the +// home-header menu so their labels, icons, and order never drift. Labels are catalog keys resolved at +// render time — the list is module-level and cannot call a hook. +const THEME_OPTIONS = [ + { + value: 'system', + labelKey: 'theme.system', + descriptionKey: 'theme.systemDescription', + Icon: Monitor + }, + { value: 'light', labelKey: 'theme.light', descriptionKey: 'theme.lightDescription', Icon: Sun }, + { value: 'dark', labelKey: 'theme.dark', descriptionKey: 'theme.darkDescription', Icon: Moon } +] as const satisfies readonly { value: ThemePreference - label: string - description: string + labelKey: string + descriptionKey: string Icon: typeof Monitor -} - -// Single source of truth for the three choices, shared by the settings segmented control and the -// home-header menu so their labels, icons, and order never drift. -const THEME_OPTIONS: ThemeOption[] = [ - { value: 'system', label: 'System', description: 'Match your device', Icon: Monitor }, - { value: 'light', label: 'Light', description: 'Always light', Icon: Sun }, - { value: 'dark', label: 'Dark', description: 'Always dark', Icon: Moon } -] +}[] // Three-way segmented control for the Settings > Appearance section. The selected segment carries a // raised surface; the whole group is a radiogroup so it reads correctly to assistive tech. export const ThemeSegmentedControl = (): React.JSX.Element => { + const { t } = useTranslation('common') const preference = useThemeStore((state) => state.preference) const setPreference = useThemeStore((state) => state.setPreference) return (
- {THEME_OPTIONS.map(({ value, label, Icon }) => { + {THEME_OPTIONS.map(({ value, labelKey, Icon }) => { const selected = preference === value return ( ) })} @@ -72,6 +78,7 @@ type ThemePreferenceMenuProps = { // The trigger shows the icon for the *current preference* (Monitor / Sun / Moon); the popover lists // all three choices with a check beside the active one. export const ThemePreferenceMenu = ({ className }: ThemePreferenceMenuProps): React.JSX.Element => { + const { t } = useTranslation('common') const preference = useThemeStore((state) => state.preference) const setPreference = useThemeStore((state) => state.setPreference) const active = THEME_OPTIONS.find((option) => option.value === preference) ?? THEME_OPTIONS[0] @@ -82,8 +89,8 @@ export const ThemePreferenceMenu = ({ className }: ThemePreferenceMenuProps): Re ) } diff --git a/src/renderer/src/components/UpdateDialog.tsx b/src/renderer/src/components/UpdateDialog.tsx index 736fde912..02fe311bf 100644 --- a/src/renderer/src/components/UpdateDialog.tsx +++ b/src/renderer/src/components/UpdateDialog.tsx @@ -1,5 +1,6 @@ import { Download, ExternalLink, RefreshCw, X } from 'lucide-react' import { Dialog } from 'radix-ui' +import { useTranslation } from 'react-i18next' import { DownloadProgressLine } from '@/components/DownloadProgressLine' import { ExternalTextLink } from '@/components/ExternalTextLink' @@ -20,6 +21,7 @@ import { formatBytes } from '../../../shared/update' // before a large download. Opened from the external capsule and the settings About section. When the // manifest carries no notes, it links to the matching GitHub release so the user can still read them. const UpdateDialog = (): React.JSX.Element | null => { + const { t } = useTranslation('components') const status = useUpdateStore((state) => state.status) const isOpen = useUpdateStore((state) => state.isDialogOpen) const closeDialog = useUpdateStore((state) => state.closeDialog) @@ -49,9 +51,12 @@ const UpdateDialog = (): React.JSX.Element | null => { >
- Update available + {t('update.title')} - v{dialogStatus.current} → v{dialogStatus.latest} + {t('update.versionRange', { + current: dialogStatus.current, + latest: dialogStatus.latest + })}
@@ -59,7 +64,7 @@ const UpdateDialog = (): React.JSX.Element | null => { type="button" variant="ghost" size="icon-sm" - aria-label="Close" + aria-label={t('dialog.close')} disabled={isApplying} className={dialogCloseButtonClassName} > @@ -70,19 +75,21 @@ const UpdateDialog = (): React.JSX.Element | null => { {dialogStatus.notes ? (
-

What's new

+

+ {t('update.whatsNew')} +

- View full release notes on GitHub + {t('update.viewNotes')}
) : (
- Release notes aren't available in-app for this version.{' '} + {t('update.noNotes')}{' '} - View release notes on GitHub + {t('update.viewNotesShort')}
)} @@ -106,19 +113,19 @@ const UpdateDialog = (): React.JSX.Element | null => { {isApplying ? (
- Open Science is stopping background tasks and will close to finish installing. The - update may take a moment; please don't reopen the app during this step. The - updated app will reopen automatically. + {t('update.applyingNotice')}
) : null} {dialogStatus.state === 'error' ? (
-

{dialogStatus.error ?? 'Update failed'}

+

+ {dialogStatus.error ?? t('update.failed')} +

{/* Fallback when the in-app update fails (e.g. a blocked/failed in-place install): let the user grab the installer by hand, mirroring the macOS manual-reinstall path. */} - Download manually + {t('update.downloadManually')}
) : null} @@ -130,7 +137,7 @@ const UpdateDialog = (): React.JSX.Element | null => { disabled={isApplying} className="rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-bg-300" > - {isReady ? 'Close' : 'Cancel'} + {isReady ? t('dialog.close') : t('dialog.cancel')} {isApplying ? ( ) : isReady ? ( @@ -168,10 +175,12 @@ const UpdateDialog = (): React.JSX.Element | null => { >
diff --git a/src/renderer/src/components/components-batch-i18n.render.test.tsx b/src/renderer/src/components/components-batch-i18n.render.test.tsx new file mode 100644 index 000000000..3eea6960f --- /dev/null +++ b/src/renderer/src/components/components-batch-i18n.render.test.tsx @@ -0,0 +1,141 @@ +// @vitest-environment jsdom +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import type { JobSummary } from '../../../shared/compute' +import { CompletedJobCard } from './CompletedJobCard' +import { RemoteJobRow } from './RemoteJobRow' +import { UpdateCapsule } from './UpdateCapsule' +import { GitHubStarBadge } from './GitHubStarBadge' +import { LinkSafetyModal } from './streamdown/LinkSafetyModal' +import { useUpdateStore } from '@/stores/update-store' + +;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + +// window.api stubs so component effects that call IPC don't throw. +Object.defineProperty(globalThis.window, 'api', { + configurable: true, + writable: true, + value: { github: { getStars: () => Promise.resolve(null) } } +}) + +let container: HTMLDivElement +let root: Root + +beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) +}) + +afterEach(() => { + act(() => root.unmount()) + container.remove() +}) + +// Shared defaults for the required JobSummary fields the cards don't read, so each fixture below +// only spells out what its assertions depend on. +const jobDefaults = { + provider_id: 'p1', + shape: 'cpu-small', + session_id: 's1', + started_at: undefined, + finished_at: undefined, + exit_code: undefined, + error_code: undefined, + remote_workdir: undefined, + stdout_tail: undefined, + stderr_tail: undefined, + notified_at: undefined, + notification_consumed_at: undefined +} satisfies Partial + +const successJob: JobSummary = { + ...jobDefaults, + job_id: 'j1', + intent: 'Run tests', + status: 'success', + display_name: 'host-1', + created_at: Date.now() - 5000, + started_at: Date.now() - 4000, + finished_at: Date.now() +} + +const submittedJob: JobSummary = { + ...jobDefaults, + job_id: 'j2', + intent: 'Build project', + status: 'submitted', + display_name: 'host-2', + created_at: Date.now() - 1000 +} + +describe('components batch i18n — en strings', () => { + it('CompletedJobCard renders translated "finished" status', () => { + act(() => { + root.render() + }) + expect(container.textContent).toContain('finished') + }) + + it('CompletedJobCard aria-label uses catalog key', () => { + act(() => { + root.render() + }) + const btn = container.querySelector('[data-testid="completed-job-card"]') + expect(btn?.getAttribute('aria-label')).toContain('Run tests') + }) + + it('RemoteJobRow renders translated "queued" label', () => { + act(() => { + root.render() + }) + expect(container.textContent).toContain('queued') + }) + + it('RemoteJobRow aria-label uses catalog key', () => { + act(() => { + root.render() + }) + const btn = container.querySelector('[data-testid="remote-job-row"]') + expect(btn?.getAttribute('aria-label')).toContain('Build project') + }) + + it('UpdateCapsule renders translated "Update" label when update is available', () => { + useUpdateStore.setState({ + status: { state: 'available', current: '0.7.2', latest: '0.8.0', notes: '' } + }) + act(() => { + root.render() + }) + expect(container.textContent).toContain('Update') + const btn = container.querySelector('button') + expect(btn?.getAttribute('aria-label')).toContain('0.8.0') + }) + + it('GitHubStarBadge aria-label uses catalog key', () => { + act(() => { + root.render() + }) + const link = container.querySelector('a') + expect(link?.getAttribute('aria-label')).toContain('GitHub') + }) + + it('LinkSafetyModal renders translated description and action labels', () => { + act(() => { + root.render( + + ) + }) + // Modal is rendered into document.body via portal — query there. + expect(document.body.textContent).toContain('external website') + expect(document.body.textContent).toContain('Copy link') + expect(document.body.textContent).toContain('Open link') + }) +}) diff --git a/src/renderer/src/components/components-i18n.render.test.tsx b/src/renderer/src/components/components-i18n.render.test.tsx new file mode 100644 index 000000000..36db68864 --- /dev/null +++ b/src/renderer/src/components/components-i18n.render.test.tsx @@ -0,0 +1,128 @@ +// @vitest-environment jsdom +// Proves the shared components read from the catalog rather than shipping literals: each renders in +// English, then re-renders in Chinese after a language change. Catalog parity tests can't catch a +// component that never calls t() at all. +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { i18next } from '@/i18n' +import { JobStatusBadge } from './JobStatusBadge' +import { JobTerminalOutput } from './JobTerminalOutput' +import { DataRootWarning } from './DataRootWarning' +import { ThemeSegmentedControl } from './ThemeControls' + +let container: HTMLDivElement +let root: Root + +const render = (element: React.JSX.Element): void => { + act(() => { + root.render(element) + }) +} + +const switchTo = (language: string): void => { + act(() => { + void i18next.changeLanguage(language) + }) +} + +beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) +}) + +afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() + switchTo('en') +}) + +describe('JobStatusBadge', () => { + it('translates the status label and re-renders on language change', () => { + render() + expect(container.textContent).toBe('Running') + + switchTo('zh-Hans') + expect(container.textContent).toBe('运行中') + + switchTo('zh-Hant') + expect(container.textContent).toBe('執行中') + }) + + it('maps submitted and queued onto the same label', () => { + switchTo('zh-Hans') + render() + expect(container.textContent).toBe('排队中') + + render() + expect(container.textContent).toBe('排队中') + }) +}) + +describe('JobTerminalOutput', () => { + it('translates its own empty state but honours an explicit override', () => { + switchTo('zh-Hans') + render() + expect(container.textContent).toBe('暂无输出。') + + render() + expect(container.textContent).toBe('caller wins') + }) +}) + +describe('DataRootWarning', () => { + it('interpolates the app name into the translated advisory', () => { + switchTo('zh-Hans') + render() + + // The app name is a product noun and stays Latin in every locale. + expect(container.textContent).toContain('Open Science') + expect(container.textContent).toContain('不要移动、重命名或删除其中的文件') + }) +}) + +describe('reviewer count strings', () => { + it('picks the right plural form per locale', () => { + switchTo('en') + expect(i18next.t('components:reviewer.findings', { count: 1 })).toBe('1 finding') + expect(i18next.t('components:reviewer.findings', { count: 3 })).toBe('3 findings') + + // Chinese has one plural category; both counts must resolve, not fall back to the raw key. + switchTo('zh-Hans') + expect(i18next.t('components:reviewer.findings', { count: 1 })).toBe('1 项发现') + expect(i18next.t('components:reviewer.findings', { count: 3 })).toBe('3 项发现') + expect(i18next.t('components:reviewer.checks', { count: 1 })).toBe('1 项检查') + }) +}) + +describe('remote job badge strings', () => { + it('pluralizes the job counts through the catalog', () => { + switchTo('en') + expect(i18next.t('components:jobs.badgeIdleText', { count: 1 })).toBe('1 job') + expect(i18next.t('components:jobs.badgeIdleText', { count: 4 })).toBe('4 jobs') + expect(i18next.t('components:jobs.badgeActiveLabel', { count: 1 })).toBe('1 running remote job') + + switchTo('zh-Hant') + expect(i18next.t('components:jobs.badgeIdleText', { count: 1 })).toBe('1 個任務') + expect(i18next.t('components:jobs.badgeIdleText', { count: 4 })).toBe('4 個任務') + }) +}) + +describe('ThemeSegmentedControl', () => { + it('translates all three options and the group label', () => { + switchTo('zh-Hant') + render() + + const group = container.querySelector('[role="radiogroup"]') + expect(group?.getAttribute('aria-label')).toBe('主題') + + const labels = Array.from(container.querySelectorAll('[role="radio"]')).map( + (radio) => radio.textContent + ) + expect(labels).toEqual(['系統', '淺色', '深色']) + }) +}) diff --git a/src/renderer/src/components/global-search/GlobalSearchDialog.i18n.render.test.tsx b/src/renderer/src/components/global-search/GlobalSearchDialog.i18n.render.test.tsx new file mode 100644 index 000000000..0d45f8e39 --- /dev/null +++ b/src/renderer/src/components/global-search/GlobalSearchDialog.i18n.render.test.tsx @@ -0,0 +1,275 @@ +// @vitest-environment jsdom +// Proves the command palette reads its chrome, row metadata, and shortcut footer from the catalog: +// it renders in English, then re-renders in both Chinese locales after a language change. Catalog +// parity tests can't catch a component that never calls t() at all. +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { i18next } from '@/i18n' +import type { ChatSession } from '@/stores/session-store' +import { + createInitialPreviewWorkbenchState, + usePreviewWorkbenchStore +} from '@/stores/preview-workbench-store' +import { createInitialProjectState, useProjectStore } from '@/stores/project-store' +import { createInitialSessionState, useSessionStore } from '@/stores/session-store' +import { useNavigationStore } from '@/stores/navigation-store' + +import { GlobalSearchDialog } from './GlobalSearchDialog' + +let container: HTMLDivElement +let root: Root + +const day = 24 * 60 * 60 * 1_000 + +const artifact = { + id: 'artifact-1', + source: 'artifact' as const, + sourceFileId: 'artifact-1', + sourceVersionId: 'version-1', + projectId: 'project-a', + sessionId: 'session-a', + name: 'sin.png', + path: 'artifact-version:project-a/session-a/artifact-1/version-1', + size: 12, + sortAtMs: Date.now() - 3 * day, + originSession: { state: 'active' as const } +} + +const switchTo = (language: string): void => { + act(() => { + void i18next.changeLanguage(language) + }) +} + +const bodyText = (): string => document.body.textContent ?? '' + +const ariaLabels = (): (string | null)[] => + [...document.body.querySelectorAll('[role="group"]')].map((group) => + group.getAttribute('aria-label') + ) + +const shortcutFooterText = (): string => + document.body.querySelector('[data-testid="global-search-footer"]')?.textContent ?? '' + +// Artifact rows carry a thumbnail and repeat their origin Session's title, so match on the absence +// of the thumbnail rather than on the title text. +const sessionRowText = (): string => + [...document.body.querySelectorAll('[role="option"]')].find( + (option) => option.querySelector('[data-testid="global-search-artifact-thumbnail"]') === null + )?.textContent ?? '' + +beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + window.localStorage.clear() + useProjectStore.setState({ + ...createInitialProjectState(), + isLoaded: true, + projects: [ + { + id: 'project-a', + name: 'Alpha', + description: '', + isExample: false, + createdAt: 1, + updatedAt: 2 + } + ] + }) + useSessionStore.setState({ + ...createInitialSessionState(), + selectedSessionId: 'session-a', + sessions: [ + { + id: 'session-a', + projectId: 'project-a', + title: 'Python 绘制 sin 函数图', + cwd: '/workspace', + status: 'idle', + createdAt: Date.now(), + updatedAt: Date.now() - 3 * day, + messages: [], + artifacts: [] + } + ] as ChatSession[] + }) + useNavigationStore.setState({ + view: 'workspace', + activeProjectId: 'project-a', + userNavigationRevision: 0, + explicitNavigationRevision: 0, + pendingCustomizePrefill: undefined, + pendingArtifactMention: undefined, + artifactMentionAvailability: { projectId: 'project-a', canMention: true } + }) + usePreviewWorkbenchStore.setState(createInitialPreviewWorkbenchState()) + Object.defineProperty(window, 'api', { + configurable: true, + value: { + projectFiles: { + searchArtifacts: vi.fn().mockResolvedValue({ + primary: { items: [artifact], totalCount: 1 }, + other: [], + isIndexComplete: true + }) + }, + previewResources: { + acquire: vi.fn().mockResolvedValue({ + id: 'preview-resource-1', + url: 'open-science-preview://preview-resource-1', + mimeType: 'image/png' + }), + release: vi.fn().mockResolvedValue(undefined) + } + } + }) +}) + +afterEach(() => { + act(() => root.unmount()) + container.remove() + vi.restoreAllMocks() + switchTo('en') +}) + +const openPalette = async (): Promise => { + await act(async () => { + root.render() + await new Promise((resolve) => window.setTimeout(resolve, 20)) + }) +} + +// React tracks the input's value on the node, so a plain assignment is invisible to it. +const search = async (value: string): Promise => { + const input = document.body.querySelector('input[role="combobox"]') + const valueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set + await act(async () => { + valueSetter?.call(input, value) + input?.dispatchEvent(new Event('input', { bubbles: true })) + await new Promise((resolve) => window.setTimeout(resolve, 180)) + }) +} + +describe('GlobalSearchDialog i18n', () => { + it('translates the section headings and their group labels', async () => { + await openPalette() + expect(ariaLabels()).toEqual(['Recent artifacts', 'Recent sessions', 'Commands']) + expect(bodyText()).toContain('New session') + + switchTo('zh-Hans') + expect(ariaLabels()).toEqual(['最近的产物', '最近的会话', '命令']) + expect(bodyText()).toContain('新建会话') + + switchTo('zh-Hant') + expect(ariaLabels()).toEqual(['最近的產物', '最近的會話', '命令']) + expect(bodyText()).toContain('新增會話') + }) + + it('translates the search placeholder and the accessible dialog title', async () => { + await openPalette() + const placeholder = (): string | null => + document.body + .querySelector('input[role="combobox"]') + ?.getAttribute('placeholder') ?? null + + expect(placeholder()).toBe('Search this project…') + expect(bodyText()).toContain('Command palette') + + switchTo('zh-Hans') + expect(placeholder()).toBe('在此项目中搜索…') + expect(bodyText()).toContain('命令面板') + + switchTo('zh-Hant') + expect(placeholder()).toBe('在這個專案中搜尋…') + }) + + it('translates the shortcut footer', async () => { + await openPalette() + expect(shortcutFooterText()).toBe('↑↓navigate↵open⇧↵mentionescclose') + + switchTo('zh-Hans') + expect(shortcutFooterText()).toBe('↑↓切换↵打开⇧↵提及esc关闭') + + switchTo('zh-Hant') + expect(shortcutFooterText()).toBe('↑↓切換↵開啟⇧↵提及esc關閉') + }) + + it('renders the row metadata — artifact count and elapsed time — through the catalog', async () => { + await openPalette() + expect(sessionRowText()).toContain('0 artifacts · 3 days ago') + + switchTo('zh-Hans') + expect(sessionRowText()).toContain('0 个产物 · 3 天前') + + switchTo('zh-Hant') + expect(sessionRowText()).toContain('0 個產物 · 3 天前') + }) + + it('pluralizes the English artifact count and elapsed unit, and keeps Chinese uninflected', async () => { + useSessionStore.setState({ + ...useSessionStore.getState(), + sessions: [ + { + ...useSessionStore.getState().sessions[0], + artifacts: [{ id: 'a-1' }], + updatedAt: Date.now() - day + } + ] as ChatSession[] + }) + await openPalette() + expect(sessionRowText()).toContain('1 artifact · 1 day ago') + + switchTo('zh-Hans') + expect(sessionRowText()).toContain('1 个产物 · 1 天前') + }) + + it('translates the no-match empty state and interpolates the query', async () => { + Object.defineProperty(window, 'api', { + configurable: true, + value: { + projectFiles: { + searchArtifacts: vi.fn().mockResolvedValue({ + primary: { items: [], totalCount: 0 }, + other: [], + isIndexComplete: true + }) + } + } + }) + await openPalette() + await search('cosine') + + expect(bodyText()).toContain('No sessions or artifacts match “cosine”.') + + switchTo('zh-Hans') + expect(bodyText()).toContain('没有会话或产物匹配“cosine”。') + + switchTo('zh-Hant') + expect(bodyText()).toContain('沒有會話或產物符合「cosine」。') + }) + + it('translates the artifact load failure into a retry row', async () => { + Object.defineProperty(window, 'api', { + configurable: true, + value: { + projectFiles: { searchArtifacts: vi.fn().mockRejectedValue('not an Error') } + } + }) + await openPalette() + await search('sin') + + expect(bodyText()).toContain('Could not load artifacts — retry') + + switchTo('zh-Hans') + expect(bodyText()).toContain('无法加载产物 —— 重试') + + switchTo('zh-Hant') + expect(bodyText()).toContain('無法載入產物 —— 重試') + }) +}) diff --git a/src/renderer/src/components/global-search/GlobalSearchDialog.tsx b/src/renderer/src/components/global-search/GlobalSearchDialog.tsx index 6bde13537..ac3cb13d9 100644 --- a/src/renderer/src/components/global-search/GlobalSearchDialog.tsx +++ b/src/renderer/src/components/global-search/GlobalSearchDialog.tsx @@ -5,6 +5,7 @@ * contrast: inherited from the app's verified semantic tokens · slop: pass */ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { ArrowUpRight, AtSign, Hash, MessageCircle, Search } from 'lucide-react' import { Dialog } from 'radix-ui' @@ -14,6 +15,7 @@ import { dialogOverlayClassName, dialogPanelClassName } from '@/components/ui/di import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { relativeTimeParts } from '@/lib/format-relative-time' import { resolveCustomizeProjectId } from '@/lib/last-opened-project' import { cn } from '@/lib/utils' import { ArtifactPreview } from '@/pages/workspace/artifact-preview' @@ -61,22 +63,10 @@ const emptyArtifactState: ArtifactState = { isIndexComplete: true } -const getErrorMessage = (error: unknown): string => - error instanceof Error ? error.message : 'Could not load artifacts.' - -const pluralizeTime = (value: number, unit: string): string => - `${value} ${unit}${value === 1 ? '' : 's'} ago` - -const formatRelativeTime = (timestamp: number): string => { - const elapsed = Math.max(0, Date.now() - timestamp) - const minutes = Math.floor(elapsed / 60_000) - if (minutes < 1) return 'just now' - if (minutes < 60) return pluralizeTime(minutes, 'minute') - const hours = Math.floor(minutes / 60) - if (hours < 24) return pluralizeTime(hours, 'hour') - const days = Math.floor(hours / 24) - return days < 7 ? pluralizeTime(days, 'day') : pluralizeTime(Math.floor(days / 7), 'week') -} +// An IPC rejection carries an English message from the main process, so it is only shown when it +// exists; the fallback is the one the catalog owns. +const getErrorMessage = (error: unknown, fallback: string): string => + error instanceof Error ? error.message : fallback const artifactToPreviewItem = ( artifact: ProjectFileItem @@ -121,6 +111,7 @@ export const GlobalSearchDialog = ({ onOpenChange, isSessionPersistenceReady }: GlobalSearchDialogProps): React.JSX.Element => { + const { t } = useTranslation('components') const inputRef = useRef(null) const requestVersionRef = useRef(0) const listboxId = useId() @@ -146,6 +137,15 @@ export const GlobalSearchDialog = ({ ) const openFileDialog = usePreviewWorkbenchStore((state) => state.openFileDialog) + // Verbose wording ("3 days ago"), unlike the compact `home.relativeTime` labels: rows here have + // room, and the timestamp is the only recency cue a search hit carries. + const relativeTime = (timestamp: number): string => { + const { unit, count } = relativeTimeParts(timestamp) + return unit === 'now' + ? t('globalSearch.elapsed.now') + : t(`globalSearch.elapsed.${unit}`, { count }) + } + const primaryProjectId = useMemo( () => activeProjectId ?? resolveCustomizeProjectId(projects), [activeProjectId, projects] @@ -267,11 +267,11 @@ export const GlobalSearchDialog = ({ } catch (error) { if (version !== requestVersionRef.current) return setArtifactStatus('error') - setArtifactError(getErrorMessage(error)) + setArtifactError(getErrorMessage(error, t('globalSearch.loadArtifactsFailed'))) setFailedArtifactCursor(cursor) } }, - [isSearchMode, otherProjectIds, primaryProject, trimmedQuery] + [isSearchMode, otherProjectIds, primaryProject, t, trimmedQuery] ) useEffect(() => { @@ -399,7 +399,7 @@ export const GlobalSearchDialog = ({ !session.isPending ) if (!isStillAvailable) { - setActionError('This session is no longer available.') + setActionError(t('globalSearch.sessionUnavailable')) return } openSession(row.session.projectId, row.session.id, 'user') @@ -442,7 +442,8 @@ export const GlobalSearchDialog = ({ previewArtifact, primaryProject, reloadArtifacts, - sessions + sessions, + t ] ) @@ -505,16 +506,21 @@ export const GlobalSearchDialog = ({ {session.title} - {session.projectId !== primaryProject?.id ? `${session.projectName} · ` : ''} - {session.artifactCount} artifact{session.artifactCount === 1 ? '' : 's'} ·{' '} - {formatRelativeTime(session.updatedAt)} + {session.projectId !== primaryProject?.id + ? `${session.projectName ?? t('globalSearch.unknownProject')} · ` + : ''} + {t('globalSearch.artifactCount', { count: session.artifactCount })} ·{' '} + {relativeTime(session.updatedAt)} {active ? ( - + ) : ( - Session + {t('globalSearch.sessionBadge')} )}
@@ -561,13 +567,15 @@ export const GlobalSearchDialog = ({ {artifact.projectId !== primaryProject?.id - ? `${projectNames.get(artifact.projectId) ?? 'Unknown project'} · ` + ? `${projectNames.get(artifact.projectId) ?? t('globalSearch.unknownProject')} · ` : ''} {artifact.originSession?.title ?? sessionTitles.get(`${artifact.projectId}:${artifact.sessionId}`) ?? - 'Unknown session'}{' '} + t('globalSearch.unknownSession')}{' '} ·{' '} - {createdAt === undefined ? 'Creation time unavailable' : formatRelativeTime(createdAt)} + {createdAt === undefined + ? t('globalSearch.createdAtUnavailable') + : relativeTime(createdAt)} {active ? ( @@ -583,7 +591,7 @@ export const GlobalSearchDialog = ({ size="icon-xs" tabIndex={-1} className="cursor-pointer" - aria-label={`Mention ${artifact.name}`} + aria-label={t('globalSearch.mention', { name: artifact.name })} disabled={!canMention} onClick={(event) => { event.stopPropagation() @@ -596,8 +604,8 @@ export const GlobalSearchDialog = ({ {canMention - ? `Mention ${artifact.name}` - : 'Mention is unavailable while the composer cannot accept another artifact.'} + ? t('globalSearch.mention', { name: artifact.name }) + : t('globalSearch.mentionUnavailable')} ) : null} @@ -609,7 +617,7 @@ export const GlobalSearchDialog = ({ size="icon-xs" tabIndex={-1} className="cursor-pointer" - aria-label={`Open ${artifact.name}`} + aria-label={t('globalSearch.open', { name: artifact.name })} onClick={(event) => { event.stopPropagation() previewArtifact(artifact) @@ -618,7 +626,7 @@ export const GlobalSearchDialog = ({
-

- You are about to visit an external website. -

+

{t('linkSafety.description')}

100 ? 'sd-link-safety-url max-scroll' : 'sd-link-safety-url'} @@ -161,12 +162,12 @@ const LinkSafetyModal = ({ {copied && isOpen ? ( <> - Copied + {t('linkSafety.copied')} ) : ( <> - Copy link + {t('linkSafety.copyLink')} )} @@ -179,7 +180,7 @@ const LinkSafetyModal = ({ }} > - Open link + {t('linkSafety.openLink')}
diff --git a/src/renderer/src/components/ui/message-scroller.tsx b/src/renderer/src/components/ui/message-scroller.tsx index 70cf11399..6380e496a 100644 --- a/src/renderer/src/components/ui/message-scroller.tsx +++ b/src/renderer/src/components/ui/message-scroller.tsx @@ -1,14 +1,15 @@ -import * as React from "react" +import * as React from 'react' +import { useTranslation } from 'react-i18next' import { MessageScroller as MessageScrollerPrimitive, useMessageScroller, useMessageScrollerScrollable, - useMessageScrollerVisibility, -} from "@shadcn/react/message-scroller" + useMessageScrollerVisibility +} from '@shadcn/react/message-scroller' -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { ArrowDownIcon } from "lucide-react" +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { ArrowDownIcon } from 'lucide-react' function MessageScrollerProvider( props: React.ComponentProps @@ -24,7 +25,7 @@ function MessageScroller({ ) @@ -71,7 +72,7 @@ function MessageScrollerItem({ data-slot="message-scroller-item" scrollAnchor={scrollAnchor} className={cn( - "min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]", + 'min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]', className )} {...props} @@ -80,15 +81,16 @@ function MessageScrollerItem({ } function MessageScrollerButton({ - direction = "end", + direction = 'end', className, children, render, - variant = "secondary", - size = "icon-sm", + variant = 'secondary', + size = 'icon-sm', ...props }: React.ComponentProps & - Pick, "variant" | "size">) { + Pick, 'variant' | 'size'>) { + const { t } = useTranslation('common') return ( } @@ -105,10 +107,9 @@ function MessageScrollerButton({ > {children ?? ( <> - + - {direction === "end" ? "Scroll to end" : "Scroll to start"} + {direction === 'end' ? t('scroll.toEnd') : t('scroll.toStart')} )} @@ -125,5 +126,5 @@ export { MessageScrollerButton, useMessageScroller, useMessageScrollerScrollable, - useMessageScrollerVisibility, + useMessageScrollerVisibility } diff --git a/src/renderer/src/i18n/i18next.d.ts b/src/renderer/src/i18n/i18next.d.ts new file mode 100644 index 000000000..c93c55bf2 --- /dev/null +++ b/src/renderer/src/i18n/i18next.d.ts @@ -0,0 +1,15 @@ +// Declaration merging that makes t() key-checked at compile time against the English catalog. +// A typo or a stale key after a rename fails `npm run typecheck:web` instead of silently rendering +// the raw key string at runtime. + +import type { DEFAULT_NAMESPACE, Resources } from './resources' + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: typeof DEFAULT_NAMESPACE + resources: Resources + // Keys are authored as semantic paths, never as English source text, so the separators stay + // at their i18next defaults ('.' for nesting, ':' for the namespace prefix). + returnNull: false + } +} diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts new file mode 100644 index 000000000..2309901df --- /dev/null +++ b/src/renderer/src/i18n/index.ts @@ -0,0 +1,58 @@ +// i18next setup for the renderer, shared by the Electron window and the localhost web build. +// +// initI18n() is called synchronously before React mounts (main.tsx / web/bootstrap.ts) so the first +// paint is already in the resolved language. i18next's init is synchronous when resources are passed +// inline and no async backend is configured, which is why the catalogs are statically imported. + +import i18next, { type i18n } from 'i18next' +import { initReactI18next } from 'react-i18next' + +import { DEFAULT_LOCALE, LOCALES, type Locale } from '../../../shared/locale' +import { DEFAULT_NAMESPACE, resources } from './resources' + +// zh-Hant falls back to English, not to zh-Hans. Falling back across scripts would mix Simplified and +// Traditional in one screen whenever a key is missing, which reads worse than a clean English string +// and hides the gap from reviewers. +const fallbackLng: Record = { + 'zh-Hant': [DEFAULT_LOCALE], + 'zh-Hans': [DEFAULT_LOCALE], + default: [DEFAULT_LOCALE] +} + +let initialized = false + +export const initI18n = (locale: Locale): i18n => { + if (initialized) { + void i18next.changeLanguage(locale) + return i18next + } + + void i18next.use(initReactI18next).init({ + lng: locale, + fallbackLng, + supportedLngs: [...LOCALES], + resources, + defaultNS: DEFAULT_NAMESPACE, + ns: Object.keys(resources[DEFAULT_LOCALE]), + // Our copy goes through JSX, which escapes on its own; i18next escaping on top would double-encode + // apostrophes and quotes that appear throughout the English source. + interpolation: { escapeValue: false }, + returnNull: false + }) + + initialized = true + return i18next +} + +// Switches the active language on an already-initialized instance. Safe to call before init (the +// store's setter may run in a test that never bootstrapped): init then applies the same locale. +export const setI18nLocale = (locale: Locale): void => { + if (!initialized) { + initI18n(locale) + return + } + + void i18next.changeLanguage(locale) +} + +export { i18next } diff --git a/src/renderer/src/i18n/resources.test.ts b/src/renderer/src/i18n/resources.test.ts new file mode 100644 index 000000000..a80c5acc5 --- /dev/null +++ b/src/renderer/src/i18n/resources.test.ts @@ -0,0 +1,181 @@ +// Catalog parity and glossary guards. These run on every `npm run test`, so a PR that adds an English +// key without translating it, or that translates a term inconsistently, fails before review. + +import { describe, expect, it } from 'vitest' + +import { DEFAULT_LOCALE, LOCALES, type Locale } from '../../../shared/locale' +import { resources } from './resources' + +type Catalog = Record + +const leafValues = (value: Catalog, prefix = ''): [string, unknown][] => + Object.entries(value).flatMap(([key, child]): [string, unknown][] => { + const path = prefix ? `${prefix}.${key}` : key + return child !== null && typeof child === 'object' + ? leafValues(child as Catalog, path) + : [[path, child]] + }) + +// Flattens a namespace to dotted leaf paths so two catalogs can be compared as sets. +const leafPaths = (value: Catalog, prefix = ''): string[] => + leafValues(value, prefix).map(([key]) => key) + +// {{name}} interpolation placeholders and markers consumed by the Trans component. Both must +// survive translation: a dropped placeholder renders a blank where a value belongs, and a dropped tag +// makes Trans throw away the wrapped element. +const markers = (text: string): string[] => + [...text.matchAll(/\{\{(\w+)\}\}|<(\/?\w+)>/g)].map((match) => match[0]).sort() + +// i18next plural suffixes. A key's plural *forms* legitimately differ per locale — English needs +// `_one` and `_other`, Chinese has a single category and needs only `_other` — so key-set parity is +// compared on the base name, and the categories themselves are asserted separately below. +const PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/ + +const baseName = (path: string): string => path.replace(PLURAL_SUFFIX, '') + +// The plural categories the locale's grammar actually has, straight from ICU data. +const pluralCategories = (locale: Locale): Set => + new Set(new Intl.PluralRules(locale).resolvedOptions().pluralCategories) + +const namespaces = Object.keys(resources[DEFAULT_LOCALE]) as (keyof typeof resources.en)[] +const translated = LOCALES.filter((locale) => locale !== DEFAULT_LOCALE) + +const catalog = (locale: Locale, namespace: keyof typeof resources.en): Catalog => + resources[locale][namespace] as unknown as Catalog + +describe.each(translated)('%s catalog', (locale) => { + it.each(namespaces)('has exactly the English keys in %s', (namespace) => { + // Compared on base names so a plural key counts as translated regardless of how many categories + // the target grammar needs. + const expected = [ + ...new Set(leafPaths(catalog(DEFAULT_LOCALE, namespace)).map(baseName)) + ].sort() + const actual = [...new Set(leafPaths(catalog(locale, namespace)).map(baseName))].sort() + + expect(actual).toEqual(expected) + }) + + it.each(namespaces)('has no empty strings in %s', (namespace) => { + const empty = leafValues(catalog(locale, namespace)) + .filter(([, value]) => typeof value !== 'string' || value.trim().length === 0) + .map(([path]) => path) + + expect(empty).toEqual([]) + }) + + it.each(namespaces)('preserves every placeholder and tag in %s', (namespace) => { + const source = new Map(leafValues(catalog(DEFAULT_LOCALE, namespace))) + const mismatched = leafValues(catalog(locale, namespace)) + .filter(([path, value]) => { + const original = source.get(path) + if (typeof original !== 'string' || typeof value !== 'string') return false + return markers(original).join('|') !== markers(value).join('|') + }) + .map(([path]) => path) + + expect(mismatched).toEqual([]) + }) +}) + +// Every locale must carry exactly the plural categories its grammar defines — no more, no less. +// Too few and i18next falls back to the key at some counts; too many (e.g. a hand-copied `_one` in a +// Chinese catalog) is dead copy that silently never renders. +describe.each(LOCALES)('%s plural forms', (locale) => { + it.each(namespaces)('match the locale grammar in %s', (namespace) => { + const expected = pluralCategories(locale) + const byBase = new Map>() + + for (const path of leafPaths(catalog(locale, namespace))) { + const suffix = PLURAL_SUFFIX.exec(path)?.[1] + if (!suffix) continue + const base = baseName(path) + byBase.set(base, (byBase.get(base) ?? new Set()).add(suffix)) + } + + const wrong = [...byBase.entries()] + .filter(([, suffixes]) => { + if (suffixes.size !== expected.size) return true + return [...suffixes].some((suffix) => !expected.has(suffix)) + }) + .map(([base, suffixes]) => `${base}: ${[...suffixes].sort().join(',')}`) + + expect(wrong).toEqual([]) + }) +}) + +// Script guards. Traditional copy must not contain simplified-only characters (and vice versa) — the +// signature of a catalog that was produced by running a converter over the other one, or of a +// copy-paste between the two. Character-level rather than term-level on purpose: it catches terms the +// glossary hasn't enumerated yet. Both sets are high-frequency-in-UI subsets, not exhaustive. +const SIMPLIFIED_ONLY = '设网软数据夹缓认项连组织际检验证权应务边浅业开关闭结' +const TRADITIONAL_ONLY = '設網軟數據夾緩認項連組織際檢驗證權應務邊淺業開關閉結' + +const offendingChars = (text: string, forbidden: string): string[] => [ + ...new Set([...text].filter((char) => forbidden.includes(char))) +] + +describe('script purity', () => { + it.each(namespaces)('zh-Hant %s contains no simplified-only characters', (namespace) => { + const offenders = leafValues(catalog('zh-Hant', namespace)).flatMap(([path, value]) => + typeof value === 'string' + ? offendingChars(value, SIMPLIFIED_ONLY).map((char) => `${path}: ${char}`) + : [] + ) + + expect(offenders).toEqual([]) + }) + + it.each(namespaces)('zh-Hans %s contains no traditional-only characters', (namespace) => { + const offenders = leafValues(catalog('zh-Hans', namespace)).flatMap(([path, value]) => + typeof value === 'string' + ? offendingChars(value, TRADITIONAL_ONLY).map((char) => `${path}: ${char}`) + : [] + ) + + expect(offenders).toEqual([]) + }) + + it('keeps language names untranslated across every catalog', () => { + // The picker always lists each language in its own script, so these three are identical everywhere. + for (const locale of LOCALES) { + const names = (catalog(locale, 'common').language as Catalog).names as Record + expect(names).toEqual({ en: 'English', 'zh-Hans': '简体中文', 'zh-Hant': '繁體中文' }) + } + }) +}) + +// Trans parses its string with an HTML parser, so a tag named after a void element is self-closed and +// its label escapes the wrapper — e.g. Guide renders an empty
followed by bare text, +// producing a link nobody can click. Marker parity can't catch this: both catalogs carry the same tag. +const VOID_ELEMENTS = new Set([ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr' +]) + +describe('Trans tag names', () => { + it.each(LOCALES)('%s uses no void HTML element as a wrapping tag', (locale) => { + const offenders = namespaces.flatMap((namespace) => + leafValues(catalog(locale, namespace)).flatMap(([path, value]) => + typeof value === 'string' + ? [...value.matchAll(/<(\w+)>[^<]/g)] + .filter((match) => VOID_ELEMENTS.has(match[1].toLowerCase())) + .map((match) => `${namespace}.${path}: <${match[1]}>`) + : [] + ) + ) + + expect(offenders).toEqual([]) + }) +}) diff --git a/src/renderer/src/i18n/resources.ts b/src/renderer/src/i18n/resources.ts new file mode 100644 index 000000000..3d68449ce --- /dev/null +++ b/src/renderer/src/i18n/resources.ts @@ -0,0 +1,60 @@ +// The message catalogs, statically imported so they are in the bundle before the first paint. All +// three locales ship together: this is a desktop app (and a localhost web build), so there is no +// network cost to amortize, and lazy loading would reintroduce the language flash that reading the +// preference synchronously exists to avoid. +// +// Adding a namespace: create the JSON in all three locale folders and register it in every entry +// below. The catalog-parity test fails if a key exists in one locale but not the others. + +import enCommon from '../locales/en/common.json' +import enComponents from '../locales/en/components.json' +import enHome from '../locales/en/home.json' +import enOnboarding from '../locales/en/onboarding.json' +import enSettings from '../locales/en/settings.json' +import enWorkspace from '../locales/en/workspace.json' +import zhHansCommon from '../locales/zh-Hans/common.json' +import zhHansComponents from '../locales/zh-Hans/components.json' +import zhHansHome from '../locales/zh-Hans/home.json' +import zhHansOnboarding from '../locales/zh-Hans/onboarding.json' +import zhHansSettings from '../locales/zh-Hans/settings.json' +import zhHansWorkspace from '../locales/zh-Hans/workspace.json' +import zhHantCommon from '../locales/zh-Hant/common.json' +import zhHantComponents from '../locales/zh-Hant/components.json' +import zhHantHome from '../locales/zh-Hant/home.json' +import zhHantOnboarding from '../locales/zh-Hant/onboarding.json' +import zhHantSettings from '../locales/zh-Hant/settings.json' +import zhHantWorkspace from '../locales/zh-Hant/workspace.json' + +// 'common' holds anything shared across pages (shared controls, generic verbs, generic states). +// Feature namespaces mirror the pages/ folder layout so a key's home is unambiguous. +export const DEFAULT_NAMESPACE = 'common' + +export const resources = { + en: { + common: enCommon, + components: enComponents, + home: enHome, + onboarding: enOnboarding, + settings: enSettings, + workspace: enWorkspace + }, + 'zh-Hans': { + common: zhHansCommon, + components: zhHansComponents, + home: zhHansHome, + onboarding: zhHansOnboarding, + settings: zhHansSettings, + workspace: zhHansWorkspace + }, + 'zh-Hant': { + common: zhHantCommon, + components: zhHantComponents, + home: zhHantHome, + onboarding: zhHantOnboarding, + settings: zhHantSettings, + workspace: zhHantWorkspace + } +} as const + +// The English catalog is the source of truth for key names and for the TypeScript key union. +export type Resources = (typeof resources)['en'] diff --git a/src/renderer/src/lib/format-relative-time.test.ts b/src/renderer/src/lib/format-relative-time.test.ts index ccf199490..2f8684eef 100644 --- a/src/renderer/src/lib/format-relative-time.test.ts +++ b/src/renderer/src/lib/format-relative-time.test.ts @@ -1,24 +1,33 @@ import { describe, expect, it } from 'vitest' -import { formatRelativeTime } from './format-relative-time' +import { relativeTimeParts } from './format-relative-time' const NOW = 1_800_000_000_000 const day = 24 * 60 * 60 * 1000 -describe('formatRelativeTime', () => { - it('formats sub-minute, minute, hour, and day ranges', () => { - expect(formatRelativeTime(NOW - 10_000, NOW)).toBe('now') - expect(formatRelativeTime(NOW - 5 * 60 * 1000, NOW)).toBe('5m') - expect(formatRelativeTime(NOW - 3 * 60 * 60 * 1000, NOW)).toBe('3h') - expect(formatRelativeTime(NOW - 3 * day, NOW)).toBe('3d') - expect(formatRelativeTime(NOW - 21 * day, NOW)).toBe('3w') +describe('relativeTimeParts', () => { + it('picks the right bucket for sub-minute, minute, hour, day, and week ranges', () => { + expect(relativeTimeParts(NOW - 10_000, NOW)).toEqual({ unit: 'now', count: 0 }) + expect(relativeTimeParts(NOW - 5 * 60 * 1000, NOW)).toEqual({ unit: 'minute', count: 5 }) + expect(relativeTimeParts(NOW - 3 * 60 * 60 * 1000, NOW)).toEqual({ unit: 'hour', count: 3 }) + expect(relativeTimeParts(NOW - 3 * day, NOW)).toEqual({ unit: 'day', count: 3 }) + expect(relativeTimeParts(NOW - 21 * day, NOW)).toEqual({ unit: 'week', count: 3 }) }) - it('does not render "0y" for the 360-364 day gap between the month and year buckets', () => { + it('does not report a zero year for the 360-364 day gap between the month and year buckets', () => { // Regression: with `months < 12` the /30 approximation made 360-364 days fall through to days/365 = 0. - expect(formatRelativeTime(NOW - 360 * day, NOW)).toBe('12mo') - expect(formatRelativeTime(NOW - 364 * day, NOW)).toBe('12mo') - expect(formatRelativeTime(NOW - 365 * day, NOW)).toBe('1y') - expect(formatRelativeTime(NOW - 400 * day, NOW)).toBe('1y') + expect(relativeTimeParts(NOW - 360 * day, NOW)).toEqual({ unit: 'month', count: 12 }) + expect(relativeTimeParts(NOW - 364 * day, NOW)).toEqual({ unit: 'month', count: 12 }) + expect(relativeTimeParts(NOW - 365 * day, NOW)).toEqual({ unit: 'year', count: 1 }) + expect(relativeTimeParts(NOW - 400 * day, NOW)).toEqual({ unit: 'year', count: 1 }) + }) + + it('never reports a zero count for a just-crossed minute boundary', () => { + // 45-59s is past the 'now' cutoff but floors to 0 minutes; the bucket clamps it to 1. + expect(relativeTimeParts(NOW - 50_000, NOW)).toEqual({ unit: 'minute', count: 1 }) + }) + + it('treats a future timestamp as now rather than counting backwards', () => { + expect(relativeTimeParts(NOW + 10 * day, NOW)).toEqual({ unit: 'now', count: 0 }) }) }) diff --git a/src/renderer/src/lib/format-relative-time.ts b/src/renderer/src/lib/format-relative-time.ts index b7f84f090..5e6dea7b1 100644 --- a/src/renderer/src/lib/format-relative-time.ts +++ b/src/renderer/src/lib/format-relative-time.ts @@ -1,31 +1,46 @@ -// Formats a past timestamp as a compact relative label (e.g. "5m", "17h", "3d") for dense lists. -export const formatRelativeTime = (timestamp: number, now: number = Date.now()): string => { +// Picks the coarsest sensible bucket for a past timestamp, for compact labels in dense lists. +// +// This stays pure and locale-free: it returns the bucket and the count, and the caller renders them +// through the catalog. English wants "5m"/"3d" with no space; Chinese wants "5 分钟前"/"3 天前" with a +// different word order — a suffix table alone can't express that, so the unit becomes a catalog key. +export type RelativeTimeUnit = 'now' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' + +export type RelativeTimeParts = { + unit: RelativeTimeUnit + // Always ≥ 1 except for 'now', where it carries no meaning and is 0. + count: number +} + +export const relativeTimeParts = ( + timestamp: number, + now: number = Date.now() +): RelativeTimeParts => { const elapsedMs = Math.max(0, now - timestamp) const seconds = Math.floor(elapsedMs / 1000) - if (seconds < 45) return 'now' + if (seconds < 45) return { unit: 'now', count: 0 } const minutes = Math.floor(seconds / 60) - if (minutes < 60) return `${Math.max(1, minutes)}m` + if (minutes < 60) return { unit: 'minute', count: Math.max(1, minutes) } const hours = Math.floor(minutes / 60) - if (hours < 24) return `${hours}h` + if (hours < 24) return { unit: 'hour', count: hours } const days = Math.floor(hours / 24) - if (days < 7) return `${days}d` + if (days < 7) return { unit: 'day', count: days } const weeks = Math.floor(days / 7) - if (weeks < 5) return `${weeks}w` + if (weeks < 5) return { unit: 'week', count: weeks } const months = Math.floor(days / 30) // Switch to years only at a real year. Using `months < 12` here would leave 360–364 days (months === 12 // by the /30 approximation, but still < 365) falling through to `days / 365` === 0, rendering "0y". - if (days < 365) return `${months}mo` + if (days < 365) return { unit: 'month', count: months } - return `${Math.floor(days / 365)}y` + return { unit: 'year', count: Math.floor(days / 365) } } diff --git a/src/renderer/src/lib/locale-preference.test.ts b/src/renderer/src/lib/locale-preference.test.ts new file mode 100644 index 000000000..33fadf1ab --- /dev/null +++ b/src/renderer/src/lib/locale-preference.test.ts @@ -0,0 +1,109 @@ +// @vitest-environment jsdom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { + applyHtmlLang, + getStoredPreference, + LANGUAGE_STORAGE_KEY, + persistPreference, + resolveInitialLocale, + resolvePreference, + systemLanguageTags +} from './locale-preference' + +// navigator.languages is a read-only accessor in jsdom, so stub it per test. +const stubLanguages = (languages: string[] | undefined, language = 'en-US'): void => { + vi.spyOn(navigator, 'languages', 'get').mockReturnValue(languages as readonly string[]) + vi.spyOn(navigator, 'language', 'get').mockReturnValue(language) +} + +beforeEach(() => { + localStorage.clear() +}) + +afterEach(() => { + vi.restoreAllMocks() + localStorage.clear() +}) + +describe('systemLanguageTags', () => { + it('returns the full prioritized list when present', () => { + stubLanguages(['zh-TW', 'zh', 'en']) + expect(systemLanguageTags()).toEqual(['zh-TW', 'zh', 'en']) + }) + + it('falls back to the single navigator.language when the list is unavailable', () => { + stubLanguages(undefined, 'zh-CN') + expect(systemLanguageTags()).toEqual(['zh-CN']) + }) + + it('falls back to the single language when the list is empty', () => { + stubLanguages([], 'zh-HK') + expect(systemLanguageTags()).toEqual(['zh-HK']) + }) +}) + +describe('stored preference', () => { + it('is undefined before the user picks one', () => { + expect(getStoredPreference()).toBeUndefined() + expect(resolvePreference()).toBe('system') + }) + + it('round-trips a persisted choice', () => { + persistPreference('zh-Hant') + expect(getStoredPreference()).toBe('zh-Hant') + expect(resolvePreference()).toBe('zh-Hant') + }) + + it('ignores a stored value that is not a known preference', () => { + // A downgrade, a hand-edited value, or a locale we later dropped support for. + localStorage.setItem(LANGUAGE_STORAGE_KEY, 'zh-CN') + expect(getStoredPreference()).toBeUndefined() + expect(resolvePreference()).toBe('system') + }) + + it('survives storage that throws', () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('storage disabled') + }) + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('storage disabled') + }) + + expect(getStoredPreference()).toBeUndefined() + expect(() => persistPreference('zh-Hans')).not.toThrow() + }) +}) + +describe('resolveInitialLocale', () => { + it('detects from the device when no choice is stored', () => { + stubLanguages(['ja', 'zh-TW', 'en']) + expect(resolveInitialLocale()).toBe('zh-Hant') + }) + + it('falls back to English when the device language is unsupported', () => { + stubLanguages(['ja-JP', 'ko-KR']) + expect(resolveInitialLocale()).toBe('en') + }) + + it('honors an explicit stored choice over the device language', () => { + stubLanguages(['zh-TW']) + persistPreference('en') + expect(resolveInitialLocale()).toBe('en') + }) + + it('re-detects when the stored choice is system', () => { + stubLanguages(['zh-CN']) + persistPreference('system') + expect(resolveInitialLocale()).toBe('zh-Hans') + }) +}) + +describe('applyHtmlLang', () => { + it('writes a valid BCP-47 tag onto the document element', () => { + applyHtmlLang('zh-Hant') + expect(document.documentElement.lang).toBe('zh-Hant') + applyHtmlLang('en') + expect(document.documentElement.lang).toBe('en') + }) +}) diff --git a/src/renderer/src/lib/locale-preference.ts b/src/renderer/src/lib/locale-preference.ts new file mode 100644 index 000000000..335cd518d --- /dev/null +++ b/src/renderer/src/lib/locale-preference.ts @@ -0,0 +1,71 @@ +// Interface language preference, stored per device. Mirrors lib/theme.ts: a pure display choice read +// synchronously from localStorage and applied before React renders (see main.tsx and web/bootstrap.ts) +// so the first paint is already in the right language. Both the Electron renderer and the localhost +// web build bootstrap through this module. +// +// Detection reads navigator.languages rather than the host OS locale on purpose. In the web build the +// backend may run on a machine whose OS language differs from the person using the browser; the +// browser's list is the one that describes the reader. The main process has its own detection (via +// app.getLocale()) for surfaces that paint before the renderer exists. + +import { + DEFAULT_LANGUAGE_PREFERENCE, + htmlLang, + isLanguagePreference, + resolveLocale, + type LanguagePreference, + type Locale +} from '../../../shared/locale' + +const STORAGE_KEY = 'open-science-language' + +// The host's prioritized language list. navigator.languages is the full list; navigator.language is +// the single top entry and exists everywhere navigator.languages might not (older WebViews, jsdom +// configurations). Empty in non-DOM contexts, which resolves to the English default. +export const systemLanguageTags = (): string[] => { + if (typeof navigator === 'undefined') return [] + if (Array.isArray(navigator.languages) && navigator.languages.length > 0) { + return [...navigator.languages] + } + + return navigator.language ? [navigator.language] : [] +} + +// The stored preference, or undefined when the user has never picked one. +export const getStoredPreference = (): LanguagePreference | undefined => { + try { + const value = localStorage.getItem(STORAGE_KEY) + return isLanguagePreference(value) ? value : undefined + } catch { + // Private-mode / disabled storage: treat as "no stored choice". + return undefined + } +} + +// The effective preference: the explicit stored choice, else 'system' (auto-detect) on first run. +export const resolvePreference = (): LanguagePreference => + getStoredPreference() ?? DEFAULT_LANGUAGE_PREFERENCE + +// Resolves a preference to the locale to paint, detecting from the host when it is 'system'. +export const resolveLocalePreference = (preference: LanguagePreference): Locale => + resolveLocale(preference, systemLanguageTags()) + +// The locale to paint on first load, resolving the stored (or default 'system') preference. +export const resolveInitialLocale = (): Locale => resolveLocalePreference(resolvePreference()) + +// Reflects the locale onto , which assistive tech and the browser's line-breaking both +// read. Guarded for non-DOM contexts (tests importing the store). +export const applyHtmlLang = (locale: Locale): void => { + if (typeof document === 'undefined') return + document.documentElement.lang = htmlLang(locale) +} + +export const persistPreference = (preference: LanguagePreference): void => { + try { + localStorage.setItem(STORAGE_KEY, preference) + } catch { + // Non-fatal: the language still applies for this session, it just won't be remembered. + } +} + +export { STORAGE_KEY as LANGUAGE_STORAGE_KEY } diff --git a/src/renderer/src/locales/en/common.json b/src/renderer/src/locales/en/common.json new file mode 100644 index 000000000..186c1017d --- /dev/null +++ b/src/renderer/src/locales/en/common.json @@ -0,0 +1,50 @@ +{ + "language": { + "label": "Language", + "ariaLabel": "Interface language", + "system": "System", + "systemDescription": "Match your device", + "names": { + "en": "English", + "zh-Hans": "简体中文", + "zh-Hant": "繁體中文" + } + }, + "theme": { + "label": "Theme", + "ariaLabel": "Theme", + "system": "System", + "systemDescription": "Match your device", + "light": "Light", + "lightDescription": "Always light", + "dark": "Dark", + "darkDescription": "Always dark", + "triggerLabel": "Theme: {{theme}}" + }, + "actions": { + "open": "Open", + "opening": "Opening…", + "reveal": "Reveal", + "working": "Working…", + "back": "Back", + "cancel": "Cancel", + "continue": "Continue", + "delete": "Delete", + "quit": "Quit", + "minimizeToTray": "Minimize to tray", + "close": "Close", + "deleting": "Deleting…" + }, + "states": { + "notAvailableYet": "Not available yet." + }, + "compatibility": { + "routeSeparator": " or ", + "codexNeedsResponses": "{{provider}} speaks /v1/chat/completions. Codex requires /v1/responses; choose an OpenAI Responses provider or switch the agent framework.", + "routeMismatch": "{{framework}} needs {{frameworkRoutes}}, but {{provider}} speaks {{providerRoutes}}." + }, + "scroll": { + "toEnd": "Scroll to end", + "toStart": "Scroll to start" + } +} diff --git a/src/renderer/src/locales/en/components.json b/src/renderer/src/locales/en/components.json new file mode 100644 index 000000000..e50870ba2 --- /dev/null +++ b/src/renderer/src/locales/en/components.json @@ -0,0 +1,207 @@ +{ + "dialog": { + "close": "Close", + "cancel": "Cancel", + "back": "Back" + }, + "update": { + "title": "Update available", + "versionRange": "v{{current}} → v{{latest}}", + "whatsNew": "What's new", + "viewNotes": "View full release notes on GitHub", + "noNotes": "Release notes aren't available in-app for this version.", + "viewNotesShort": "View release notes on GitHub", + "failed": "Update failed", + "downloadManually": "Download manually", + "restart": "Restart to update", + "applyingNotice": "Open Science is stopping background tasks and will close to finish installing. The update may take a moment; please don't reopen the app during this step. The updated app will reopen automatically.", + "preparing": "Preparing update…", + "openInstaller": "Open installer", + "downloading": "Downloading {{percent}}%", + "downloadSized": "Download update ({{size}})", + "download": "Download update" + }, + "closeConfirm": { + "quitTitle": "Quit {{appName}}?", + "chooseTitle": "Minimize or quit?", + "quitDescription": "Work is still running and will be interrupted if you quit.", + "chooseDescription": "This app can keep running in the tray, or you can quit.", + "rowTitle": "{{project}} — {{title}}", + "dontAskAgain": "Don't ask again" + }, + "fieldHelp": { + "trigger": "More information" + }, + "lifecycleToast": { + "sessionCreated": "Session created externally", + "view": "View", + "dismiss": "Dismiss" + }, + "jobStatus": { + "queued": "Queued", + "running": "Running", + "done": "Done", + "failed": "Failed", + "timeout": "Timeout", + "error": "Error" + }, + "jobs": { + "badgeActiveLabel_one": "{{count}} running remote job", + "badgeActiveLabel_other": "{{count}} running remote jobs", + "badgeActiveText": "{{count}} running · {{elapsed}}", + "badgeIdleLabel_one": "{{count}} remote job", + "badgeIdleLabel_other": "{{count}} remote jobs", + "badgeIdleText_one": "{{count}} job", + "badgeIdleText_other": "{{count}} jobs", + "tooltipHeading": "REMOTE · {{count}}", + "modalLabel": "Remote job details", + "listTitle": "Running jobs in this session", + "empty": "No remote jobs in this session.", + "provider": "Provider", + "status": "Status", + "runtime": "Runtime", + "remoteWorkdir": "Remote workdir", + "jobId": "Job ID", + "noOutput": "No output yet." + }, + "reviewer": { + "title": "Reviewer", + "reviewing": "Reviewing…", + "error": "Review error", + "noIssues": "No issues found", + "noIssuesStale": "No issues found (outdated)", + "findings_one": "{{count}} finding", + "findings_other": "{{count}} findings", + "findingsStale_one": "{{count}} finding (outdated)", + "findingsStale_other": "{{count}} findings (outdated)", + "pending": "Review pending", + "checks_one": "{{count}} check", + "checks_other": "{{count}} checks", + "fixLimitReached": "fix limit reached", + "goToTranscript": "Go to transcript", + "staleNotice": "Turn changed after this review ran.", + "rerun": "Re-run review", + "rerunning": "Re-running…", + "selfCorrectNote": "The agent reads these findings and self-corrects in its next message.", + "reflagged": "re-flagged ×{{count}}" + }, + "dataRoot": { + "warning": "{{appName}} manages this folder. Don't move, rename, or delete files inside it — doing so can break your projects and history.", + "missing": { + "title": "Data folder not found", + "description": "Your data folder {{path}} can't be found. It may have been deleted, or it's on a drive that isn't connected.", + "stillMissing": "Still not found. Reconnect the drive and try again, or choose another location.", + "notUsable": "The selected folder is not usable.", + "switchFailed": "Could not switch to this folder.", + "retry": "Reconnect & retry", + "retrying": "Checking…", + "chooseAnother": "Choose another location", + "switching": "Switching…", + "continueEmpty": "Continue with an empty folder", + "continueNote": "{{appName}} will recreate the folder as you use it. Files from the old location won't be available until it's reconnected." + }, + "legacyMove": { + "title": "Move your data to a visible folder?", + "description": "Your research data is in a hidden folder. Moving it into a visible OpenScience folder makes it easy to find and back up — your settings and history stay where they are.", + "currentLabel": "Current (hidden)", + "currentAriaLabel": "Current data location", + "newLabel": "New location", + "newAriaLabel": "New data location", + "resolving": "Resolving…", + "occupied": "That folder already contains {{appName}} data. Pick an empty folder, or use the default location.", + "unusable": "That folder can't be used. Pick another one.", + "moveToDefault": "Move to OpenScience", + "chooseFolder": "Choose another folder…", + "checking": "Checking…", + "keepHere": "Keep it in the current folder", + "note": "You can always change this later in Settings → Data location. We won't ask again." + } + }, + "jobRow": { + "rowAriaLabel": "Remote job: {{intent}}", + "queued": "queued", + "running": "running", + "cardAriaLabel": "Completed remote job: {{intent}}", + "finished": "finished", + "failed": "failed", + "timedOut": "timed out", + "error": "error" + }, + "updateCapsule": { + "label": "Update", + "ariaLabel": "Update available: {{version}}" + }, + "github": { + "openAriaLabel": "Open {{app}} on GitHub", + "starAriaLabel": "Star {{app}} on GitHub, {{count}} stars" + }, + "linkSafety": { + "dialogAriaLabel": "Open external link?", + "description": "You are about to visit an external website.", + "copyLink": "Copy link", + "copied": "Copied", + "openLink": "Open link" + }, + "mermaid": { + "errorTitle": "Mermaid syntax could not be rendered", + "commonCauses": "Common causes: an xychart is missing the title keyword, axis labels are not quoted, or y-axis or bar/line data rows are missing.", + "viewSource": "View source", + "retry": "Retry" + }, + "download": { + "reconnecting": "Connection lost, resuming… (attempt {{attempt}})", + "downloaded": "downloaded" + }, + "globalSearch": { + "title": "Command palette", + "placeholder": "Search this project…", + "resultCount_one": "{{count}} result", + "resultCount_other": "{{count}} results", + "noProject": "Create a project to search sessions and artifacts.", + "recentArtifacts": "Recent artifacts", + "recentSessions": "Recent sessions", + "commands": "Commands", + "newSession": "New session", + "artifacts": "Artifacts", + "sessions": "Sessions", + "otherProjects": "Other projects", + "searchingArtifacts": "Searching artifacts…", + "loadArtifactsFailed": "Could not load artifacts.", + "retryArtifacts": "Could not load artifacts — retry", + "retryMoreArtifacts": "Could not load more — retry", + "showMore": "+{{count}} more matches — show more", + "noMatches": "No sessions or artifacts match “{{query}}”.", + "indexIncomplete": "Some artifact results may be missing.", + "sessionUnavailable": "This session is no longer available.", + "sessionBadge": "Session", + "artifactCount_one": "{{count}} artifact", + "artifactCount_other": "{{count}} artifacts", + "unknownProject": "Unknown project", + "unknownSession": "Unknown session", + "createdAtUnavailable": "Creation time unavailable", + "mention": "Mention {{name}}", + "mentionUnavailable": "Mention is unavailable while the composer cannot accept another artifact.", + "open": "Open {{name}}", + "elapsed": { + "now": "just now", + "minute_one": "{{count}} minute ago", + "minute_other": "{{count}} minutes ago", + "hour_one": "{{count}} hour ago", + "hour_other": "{{count}} hours ago", + "day_one": "{{count}} day ago", + "day_other": "{{count}} days ago", + "week_one": "{{count}} week ago", + "week_other": "{{count}} weeks ago", + "month_one": "{{count}} month ago", + "month_other": "{{count}} months ago", + "year_one": "{{count}} year ago", + "year_other": "{{count}} years ago" + }, + "shortcut": { + "navigate": "navigate", + "open": "open", + "mention": "mention", + "close": "close" + } + } +} diff --git a/src/renderer/src/locales/en/home.json b/src/renderer/src/locales/en/home.json new file mode 100644 index 000000000..93b67708f --- /dev/null +++ b/src/renderer/src/locales/en/home.json @@ -0,0 +1,60 @@ +{ + "header": { + "beta": "Beta", + "environmentRepair": "Open environment repair", + "environmentOneFailure": "{{label}} needs attention", + "environmentManyFailures": "{{count}} environment items need attention", + "modelSettings": "Model settings", + "newProject": "New project", + "environmentShort": "Environment" + }, + "projects": { + "heading": "Projects", + "sectionLabel": "Projects", + "loadError": "Could not load projects: {{error}}", + "empty": "No projects yet. Create one to get started.", + "exampleBadge": "Example", + "sessionCount_one": "{{count}} session", + "sessionCount_other": "{{count}} sessions", + "sessionCountUnavailable": "Session count unavailable", + "rowActions": "Open actions for {{name}}", + "actionsMenu": "Project actions", + "rename": "Rename…", + "delete": "Delete" + }, + "recent": { + "heading": "Recent sessions", + "sectionLabel": "Recent sessions", + "empty": "Sessions you start will appear here." + }, + "form": { + "createTitle": "New project", + "createDescription": "Group related sessions under a project. You can rename it later.", + "createSubmit": "Create project", + "editTitle": "Edit project", + "editDescription": "Rename this project or update its description.", + "editSubmit": "Save changes", + "nameLabel": "Name", + "namePlaceholder": "e.g. Reproduction of published research", + "descriptionLabel": "Description", + "descriptionOptional": "(optional)", + "descriptionPlaceholder": "What is this project about?", + "saveFailed": "Could not save project." + }, + "deleteDialog": { + "title": "Delete project?", + "description": "This will permanently delete \"{{name}}\". Generated artifacts and uploaded files stored by Open Science will also be deleted. Files in the project's working folder are not deleted. This action cannot be undone.", + "descriptionWithSessions_one": "This will permanently delete \"{{name}}\" and its {{count}} session. Generated artifacts and uploaded files stored by Open Science will also be deleted. Files in the project's working folder are not deleted. This action cannot be undone.", + "descriptionWithSessions_other": "This will permanently delete \"{{name}}\" and its {{count}} sessions. Generated artifacts and uploaded files stored by Open Science will also be deleted. Files in the project's working folder are not deleted. This action cannot be undone.", + "descriptionWithIncompleteCatalog": "This will permanently delete \"{{name}}\" and all of its saved conversations, including any that could not be loaded during recovery. Generated artifacts and uploaded files stored by Open Science will also be deleted. Files in the project's working folder are not deleted. This action cannot be undone." + }, + "relativeTime": { + "now": "now", + "minute": "{{count}}m", + "hour": "{{count}}h", + "day": "{{count}}d", + "week": "{{count}}w", + "month": "{{count}}mo", + "year": "{{count}}y" + } +} diff --git a/src/renderer/src/locales/en/onboarding.json b/src/renderer/src/locales/en/onboarding.json new file mode 100644 index 000000000..1cd9c6492 --- /dev/null +++ b/src/renderer/src/locales/en/onboarding.json @@ -0,0 +1,86 @@ +{ + "shell": { + "eyebrow": "FIRST-TIME SETUP", + "title": "Set up your research workspace.", + "intro": "A quick host check confirms this computer is ready, you connect the model you want to use, then you choose where your data lives.", + "relaunching": "Setting up your workspace…", + "progressLabel": "Setup progress" + }, + "steps": { + "environment": "Environment", + "agent": "Agent runtime", + "provider": "Model provider", + "notebook": "Notebook runtime", + "location": "Data location" + }, + "footer": { + "allChecksPassed": "All required environment checks passed.", + "completeRequired": "Complete every required item above to continue." + }, + "environment": { + "title": "Prepare environment", + "description": "Open Science confirms its core requirements before your first research session.", + "checkAgain": "Check again", + "checking": "Checking…", + "sectionLabel": "Prepare environment", + "requirementsLabel": "Environment requirements", + "waiting": "Waiting to check…", + "resolveHint": "Resolve the items marked Action needed, then choose Check again.", + "setupFailed": "Setup could not be completed", + "checkLabels": { + "system": "System compatibility", + "storage": "App storage permission", + "secure-storage": "Secure credential storage", + "install-network": "Installation network" + }, + "status": { + "passed": "Ready", + "warning": "Review", + "failed": "Action needed" + } + }, + "location": { + "title": "Where should Open Science store your data?", + "description": "Large files (artifacts, notebooks, environments) go here. Your settings and history always stay in the default location. You can change this later in Settings.", + "sectionLabel": "Choose data location", + "relaunchError": "Could not switch to the new location: {{error}} You can retry or keep the default location.", + "relaunchFallbackError": "Could not restart to apply the new location.", + "locationLabel": "Location", + "pathLabel": "Data location path", + "browse": "Browse…", + "chosenNotice": "Your data will be stored in {{path}}. Open Science will restart to set this up. Use default location instead", + "adoptNotice": "This folder already contains Open Science data — it will be used as-is (nothing is moved).", + "finish": "Finish", + "confirmTitle": "Restart to set up your data?", + "confirmDescription": "Open Science will restart to set up your data at {{path}}.", + "keepDefault": "Keep default", + "restart": "Restart" + }, + "agent": { + "title": "Set up the agent runtime", + "description": "Pick the agent Open Science drives, then install it. Only this agent needs to be installed to continue." + }, + "provider": { + "title": "Connect a model", + "description": "Choose the provider Open Science should use for new research sessions.", + "sectionLabel": "Configure model", + "encryptionUnavailable": "Secure key storage is unavailable. API keys cannot be saved until the system keychain is unlocked or authorized.", + "claudeSignInHint": "Sign in with your browser to connect your Claude subscription. We'll open Claude in your browser to authorize, then bring you right back.", + "cancelSignIn": "Cancel sign-in", + "waitingForSignIn": "Waiting for sign-in…", + "testingConnection": "Testing connection…", + "signInAndContinue": "Sign in & continue", + "testAndContinue": "Test & continue", + "claudeProviderChanged": "The Claude provider changed during sign-in. Review the selection and try again.", + "codexProviderChanged": "The Codex provider changed during sign-in. Review the selection and try again.", + "providerChangedDuringTest": "The provider changed during testing. Try again.", + "claudeTokenSaveFailed": "Could not save the Claude token.", + "providerSaveFailed": "Could not save provider." + }, + "notebook": { + "title": "Notebook runtime (optional)", + "description": "Notebooks run in an app-managed Python environment by default. You can change any of this later in Settings → Runtimes.", + "provisioning": "Setting up the notebook runtime — wait for it to finish, or cancel it, to continue.", + "optional": "Optional — nothing here is required to finish setup." + } +} diff --git a/src/renderer/src/locales/en/settings.json b/src/renderer/src/locales/en/settings.json new file mode 100644 index 000000000..d4e82ee82 --- /dev/null +++ b/src/renderer/src/locales/en/settings.json @@ -0,0 +1,1083 @@ +{ + "shell": { + "dialogTitle": "Settings", + "dialogDescription": "Manage your agent runtime and model providers.", + "navLabel": "Settings", + "groups": { + "capabilities": "Capabilities", + "workspace": "Workspace", + "remoteAccess": "Remote access" + }, + "panels": { + "skills": "Skills", + "connectors": "Connectors", + "specialists": "Specialists", + "compute": "Compute", + "network": "Network", + "model": "Model", + "agent": "Agent", + "runtimes": "Runtimes", + "storage": "Storage", + "general": "General", + "permissions": "Permissions", + "remote-control": "Remote control" + }, + "back": "Back", + "forward": "Forward", + "backToRoot": "Back to {{panel}}", + "maximize": "Maximize", + "restore": "Restore", + "close": "Close settings", + "panelsLower": { + "skills": "skills", + "model": "model", + "network": "network", + "connectors": "connectors", + "compute": "compute" + }, + "save": "Save", + "saving": "Saving…", + "closeNav": "Close settings navigation", + "openNav": "Open settings navigation", + "navigation": "Navigation", + "dismissError": "Dismiss settings error", + "dismiss": "Dismiss" + }, + "model": { + "activeModel": "Active model", + "activeModelDescription": "The model that drives new agent sessions.", + "selectModel": "Select a model", + "reasoningEffort": "Reasoning effort", + "reasoningEffortDescription": "Higher levels think longer, while lower levels respond faster. Choices follow the selected model and preserve relative strength when models change; some agent frameworks may approximate unsupported levels. Applies to subsequent requests.", + "providers": "Providers", + "encryptionUnavailable": "Secure key storage is unavailable. API keys cannot be saved until the system keychain is unlocked or authorized.", + "errors": { + "testFailed": "Could not test the provider connection.", + "codexSignInFailed": "Could not sign in to Codex.", + "codexSignOutFailed": "Could not sign out of Codex.", + "claudeTokenSaveFailed": "Could not save the Claude token.", + "claudeSignInFailed": "Could not sign in to Claude.", + "claudeSignOutFailed": "Could not sign out of Claude.", + "claudeDisconnectFailed": "Could not disconnect Claude from Open Science.", + "providerSaveFailed": "Could not save provider.", + "codexSignOutIncomplete": "Codex sign-out did not complete. Try again.", + "claudeTokenSaveRetry": "Could not save the Claude token. Try again.", + "claudeSignInRetry": "Could not sign in to Claude. Try again.", + "claudeSignOutIncomplete": "Claude sign-out did not complete. Try again.", + "codexReimportFailed": "Could not re-import the Codex login." + }, + "list": { + "incompatible": "Not compatible with the active agent framework.", + "empty": "No providers yet. Add one to choose your model source.", + "testFailedWithMessage": "Test failed: {{message}}", + "testFailed": "Connection test failed.", + "claudeSubscription": "Claude subscription", + "testing": "Testing…", + "connectionVerified": "Connection verified", + "authImported": "Authentication imported into Open Science", + "codexStoredSeparately": "Codex login stored separately by Open Science", + "notSignedIn": "Not signed in", + "keyNeedsReentry": "Key needs re-entry", + "checkCodexLogin": "Check Codex login", + "testConnection": "Test connection", + "browserLoginHint": "Opening your browser to sign in… Didn't open? Cancel and use a setup token.", + "tokenPrefix": "Token: {{masked}}", + "modelPrefix": "Model: {{name}}", + "keyPrefix": "Key: {{masked}}", + "modelsCount_one": "{{count}} model", + "modelsCount_other": "{{count}} models", + "expiresLabel": "Expires" + }, + "testFailure": { + "auth": "Test failed: authentication rejected — check the API key.", + "network": "Test failed: could not reach the endpoint — check the base URL/connection.", + "badUrl": "Test failed: the base URL looks invalid.", + "modelNotFound": "Test failed: the configured model was not found.", + "timeout": "Test failed: the connection timed out.", + "serverError": "Test failed: the gateway or upstream service is temporarily unavailable.", + "withMessage": "Test failed: {{message}}", + "withMessageAndStatus": "Test failed: {{message}} (HTTP {{status}})" + }, + "type": { + "custom": "Custom", + "official": "Official" + }, + "compatibility": { + "activeModelIncompatible": "Active model isn't compatible with {{framework}}", + "codexBridgeUnsupported": "Model not supported over the Codex bridge", + "codexBridgeDescription": "This model is not supported over the Codex Chat Completions bridge. Pick another model for a Codex session.", + "switchGuidance": "Pick a compatible model below, or switch the agent framework above — otherwise conversations on this framework won't start." + }, + "effort": { + "default": "Default" + }, + "actions": { + "reimportCodexLogin": "Re-import Codex login", + "signIn": "Sign in", + "signOut": "Sign out", + "signInWithBrowser": "Sign in with browser", + "useSetupToken": "Use setup token", + "cancelSignIn": "Cancel sign-in", + "disconnect": "Disconnect from Open Science", + "edit": "Edit", + "delete": "Delete" + }, + "endpoint": { + "speaksThe": "Speaks the {{protocol}}" + }, + "refresh": { + "loaded_one": "Loaded {{count}} model from the vendor.", + "loaded_other": "Loaded {{count}} models from the vendor.", + "failed": "Couldn't fetch models: {{reason}}. Using the bundled list.", + "requestFailed": "request failed" + } + }, + "providerForm": { + "apiKey": "API key", + "getApiKey": "Get an API key", + "keyPlaceholderStored": "{{masked}} — leave blank to keep", + "storedKeyFallback": "stored key", + "keyNeedsReentry": "The stored key could not be decrypted. Enter it again to continue.", + "providerType": "Provider type", + "name": "Name", + "providerName": "Provider name", + "namePlaceholder": "e.g. My gateway", + "baseUrl": "Base URL", + "baseUrlHelp": "The gateway root; a trailing /v1 is added automatically. Choose the API format below to match the endpoint.", + "apiFormat": "API format", + "apiFormatHelp": "Which chat API this gateway speaks. Claude Code uses /v1/messages, OpenCode accepts Messages or Chat Completions, and Codex uses /v1/responses. A provider is only selectable under an agent framework that supports its format.", + "supportedModelsHelp": "Bundled with the app. Refresh from the vendor to pull the latest. Choose one from the Active model selector after adding.", + "model": "Model", + "modelOptionalOverride": "(optional override)", + "modelPlaceholderClaudeDefault": "Leave blank to use Claude's default", + "contextWindow": "Context window", + "endpoint": "Endpoint", + "supportedModels": "Supported models", + "refreshing": "Refreshing…", + "refreshFromVendor": "Refresh from vendor", + "imageInput": "Image input", + "imageInputDescription": "Enable only when this gateway and model accept image content.", + "supportsImageInput": "Supports image input", + "reasoningEffort": "Reasoning effort", + "reasoningEffortDescription": "Choose the exact effort levels accepted by this model. Open Science maps five relative strengths onto them, then sends the selected level using the request format below. Disable when the model does not accept an effort parameter.", + "supportsReasoningEffort": "Supports reasoning effort", + "reasoningEffortLevels": "Reasoning effort levels", + "requestFormat": "Request format", + "reasoningEffortRequestFormat": "Reasoning effort request format", + "codexAuth": "Codex authentication", + "codexShared": "Import existing Codex sign-in", + "codexIsolated": "Sign in with Open Science", + "codexSharedHint": "Copies Codex authentication and, when compatible, the active provider's non-secret loopback route into Open Science app data. Other global config, Skills and sessions are not imported.", + "codexIsolatedHint": "Stores a separate Codex login in Open Science app data without changing your Codex CLI profile.", + "claudeAuth": "Claude authentication", + "claudeShared": "Use existing Claude profile (Recommended)", + "claudeIsolated": "Sign in separately (isolated)", + "claudeSharedHint": "Recommended. Uses your existing Claude login from ~/.claude. Sign in once via browser OAuth and use across all Claude tools.", + "claudeIsolatedHint": "Advanced. Signs in through the browser and stores a separate Claude login in Open Science, completely isolated from your personal Claude profile.", + "claudeSharedDetail": "Sign in via browser OAuth. The Settings card will open your browser to sign in with your Claude account. Your credentials are stored in ~/.claude.", + "claudeIsolatedDetail": "Run claude setup-token in a terminal and paste the token below. It is stored encrypted under your app-owned Claude config dir; nothing is read from or written to ~/.claude.", + "claudeIsolatedPasteHint": "Paste the token in the Settings card after saving — the wizard's Test & continue flow signs you in.", + "security": { + "availableTitle": "Your key stays private.", + "availableDescription": "It is stored only on this device and never uploaded to Open Science. Your OS secure storage protects it, and it is sent only to the selected provider when you make a request.", + "unavailableTitle": "Secure storage is unavailable.", + "unavailableDescription": "Open Science will not save API keys until the operating-system credential vault is available. Unlock or authorize the system keychain, then retry." + }, + "groups": { + "codex": "Codex subscription", + "claude": "Claude subscription", + "api": "Official API", + "other": "Other" + }, + "kinds": { + "customLabel": "Custom Gateway", + "codexDescription": "Use an existing Codex profile or sign in with a separate Open Science profile.", + "claudeDescription": "Use an existing Claude profile or sign in with a separate Open Science profile.", + "officialDescription": "API key — models provided", + "customDescription": "Base URL, key, and model for a Messages or Chat Completions endpoint" + }, + "validation": { + "baseUrlRequired": "Base URL is required.", + "modelRequired": "Model is required.", + "keyRequired": "API key is required.", + "contextWindowInvalid": "Context window must be a positive whole number of tokens." + } + }, + "claudeSignIn": { + "title": "Sign in with Anthropic", + "description": "Use a long-lived OAuth token from claude setup-token. The token is encrypted in Open Science app storage and never read from or written to ~/.claude. See Anthropic's setup-token guide for the full flow.", + "browserPending": "Opening your browser to sign in… finish there and this closes automatically. Didn't open, or prefer a token? Paste one below.", + "step1": "Step 1 · Run", + "copyCommand": "Copy command", + "copied": "Copied", + "copy": "Copy", + "pasteLabel": "Paste the token printed by setup-token", + "pasteLabelStep2": "Step 2 · Paste the token printed by setup-token", + "tokenInputLabel": "Claude setup token", + "signingIn": "Signing in…", + "signIn": "Sign in", + "tokenSaveFailed": "Could not save the Claude token." + }, + "validation": { + "ok": "Connection succeeded.", + "network": "Could not reach the endpoint. Check your network and base URL.", + "auth": "Authentication failed. Check the API key.", + "modelNotFound": "The model was rejected. Check the model name for this gateway.", + "badUrl": "The base URL is invalid. Enter a full URL like https://gateway.example/v1.", + "timeout": "The request timed out and was stopped.", + "incompatible": "This provider isn't compatible with the active agent framework.", + "serverError": "The gateway or upstream service is temporarily unavailable. Try again later.", + "unknown": "Validation failed for an unknown reason.", + "withDetail": "{{base}} ({{detail}})", + "withStatus": "{{base}} (HTTP {{status}})" + }, + "breadcrumb": { + "newSkill": "New skill", + "uploadSkills": "Upload skills", + "importFromGitHub": "Import from GitHub", + "importInstalledSkills": "Import installed skills", + "addProvider": "Add provider", + "packageMirror": "Package mirror", + "addConnector": "Add connector", + "addSshHost": "Add SSH host", + "newSpecialist": "New specialist", + "editSpecialist": "Edit specialist", + "editNamed": "Edit {{name}}", + "connectorFallbackName": "connector" + }, + "about": { + "title": "About", + "ariaLabel": "App version", + "checking": "Checking…", + "checkNow": "Check now", + "checkingForUpdates": "Checking for updates…", + "available": "New version {{version}} is available", + "downloadingStatus": "Downloading… {{percent}}%", + "downloaded": "Update downloaded — open the installer to finish", + "upToDate": "You are on the latest version", + "checkFailed": "Update check failed", + "downloadingButton": "Downloading {{percent}}%", + "updateReady": "Update ready", + "updateTo": "Update to {{version}}" + }, + "appIcon": { + "title": "App icon", + "description": "Choose the built-in icon shown in the app window, and in the Dock on macOS.", + "note": "The new icon appears right away in the app window, and in the Dock on macOS. The icon in Finder, Explorer, the taskbar, or the Start menu is part of the installed app and stays the same." + }, + "general": { + "appearance": { + "title": "Appearance", + "description": "Choose how the app looks and reads. System follows your device; the other choices stay fixed. Your selection is remembered on this device.", + "theme": { + "label": "Theme", + "description": "Follow the system setting, or force light or dark." + }, + "language": { + "label": "Language", + "description": "Follow the system setting, or pick a language. System is detected once at startup, so a change to your device language takes effect the next time the app opens." + } + }, + "windowBehavior": { + "title": "Window behavior", + "description": "Choose what the titlebar close button does.", + "closeAction": { + "label": "When closing the window", + "description": "Ask each time, keep {{appName}} running in the tray, or quit the app.", + "ask": "Ask every time" + } + }, + "notifications": { + "title": "Notifications", + "description": "Get a desktop notification when a task finishes, fails, or waits for your approval while you're away from the app.", + "tasks": { + "label": "Task notifications", + "description": "Selecting a notification brings {{appName}} back to the front and opens the task.", + "toggleLabel": "Toggle task notifications" + }, + "note": "Notifications only appear while you're using another app. Tasks you cancel and failures the app retries automatically stay silent. Your operating system may ask for notification permission the first time one appears." + }, + "diagnostics": { + "title": "Diagnostics", + "description": "View this device's runtime log — it records what the app is doing so problems can be diagnosed.", + "logFile": { + "label": "Log file", + "pathLabel": "Log file path", + "openFailed": "Could not open the log file.", + "revealFailed": "Could not reveal the log file." + }, + "issueHint": "Something not working? Open an issue on GitHub and attach the log above. It stays on this device and is never sent automatically; it may contain local file paths, so review it before sharing." + }, + "cli": { + "title": "Command line tool", + "description": "Install the open-science command so you can start, stop, and check the backend from a terminal, then use it entirely from your browser.", + "rowLabel": "open-science", + "install": "Install command", + "uninstall": "Uninstall command", + "pathLabel": "Command line tool path", + "updateFailed": "Could not update the command-line tool.", + "usage": "Once installed, run open-science start to launch the backend and open the authenticated URL, then open-science stop to shut it down. status and url are also available." + }, + "community": { + "title": "Enjoying {{appName}}?", + "description": "It's free and open source. Star it on GitHub to help others find it, and come build in public with us on Discord and X. Thanks for being here.", + "ariaLabel": "Community", + "discordLabel": "Join the {{appName}} community on Discord", + "xLabel": "Follow {{appName}} on X", + "websiteLabel": "Open the {{appName}} website", + "website": "Website" + } + }, + "runtimes": { + "title": "Notebook runtimes", + "description": "Enable the environments each notebook language may run in. The app-managed environment is on by default; enable your own interpreters to make them available to the agent.", + "provenance": { + "appManaged": "App-managed", + "agentCreated": "Agent-created", + "conda": "Conda: {{name}}", + "system": "System" + }, + "ready": "Ready", + "notRunnable": "Not runnable", + "downloadingManaged": "Downloading managed runtime…", + "installedAndReady": "Installed and ready", + "managedNotSetUp": "Managed runtime is not set up yet", + "detecting": "Detecting runtimes…", + "allowPackageInstall": "Allow package install", + "allowPackageInstallDescription": "Lets Open Science install packages into this environment. Installs go to your own environment, not the app-managed storage.", + "pickRscript": "Pick your Rscript executable — e.g. Rscript.exe (Windows) or bin/Rscript (macOS/Linux). Choose the file, not a folder.", + "pickPython": "Pick your Python interpreter executable — e.g. python.exe (Windows) or bin/python (macOS/Linux). Choose the file, not a folder.", + "resetRuntime": "Reset runtime", + "retrySetup": "Retry setup", + "downloadAndSetUp": "Download and set up", + "recheck": "Recheck", + "cancel": "Cancel", + "forceStop": "Stop running work", + "drainDisable": "Disable after current work", + "disableTitle": "Disable {{label}}?", + "disableImpact": "It is in use by {{total}} active session(s) — {{running}} running, {{idle}} idle. Disabling lets any running cell finish, then closes its kernel; those sessions must switch to another runtime to keep working.", + "packages": { + "button": "Packages", + "dialogTitle": "Packages in {{label}}", + "dialogDescription": "Installed packages in this environment.", + "filterPlaceholder": "Filter packages…", + "filterAriaLabel": "Filter packages", + "count": "{{visible}} of {{total}}", + "condaSplit": "{{conda}} conda, {{pypi}} pypi", + "loading": "Listing packages…", + "retry": "Retry", + "columnName": "Name", + "columnVersion": "Version", + "columnBuild": "Build", + "columnChannel": "Channel", + "noMatch": "No packages match “{{filter}}”.", + "noneInstalled": "No packages installed.", + "close": "Close", + "listFailed": "Could not list packages." + }, + "errors": { + "recheck": "Could not re-check runtimes.", + "change": "Could not change that runtime.", + "auth": "Could not change package-install authorization.", + "add": "Could not add that interpreter.", + "refresh": "Could not refresh runtime readiness.", + "reset": "Could not reset the runtime.", + "cancel": "Could not cancel the setup." + } + }, + "agent": { + "frameworkTitle": "Agent framework", + "frameworkDescription": "Choose which coding-agent backend drives your sessions. Select a card to switch; switching starts a fresh agent session, and open conversations have their transcript replayed to the new backend. The active runtime can't be uninstalled — switch to the other one first.", + "detecting": "Detecting…", + "redetect": "Re-detect", + "detectionFailed": "One or more agent runtimes could not be detected.", + "installerFailed": "The installer could not be started.", + "repairIssuesLabel": "Agent runtime repair issues", + "installBlockersLabel": "Agent installation blockers", + "selectedAgentFallback": "The selected agent", + "cannotBeAccessed": "{{agent}} cannot be accessed.", + "repairBeforeUse": "Repair the selected agent before using it.", + "groups": { + "installed": "Installed", + "available": "Available" + }, + "blockers": { + "systemRequirements": "System requirements not met", + "installNetwork": "Installation network unavailable" + }, + "frameworks": { + "claude": { + "description": "Anthropic's agentic coding tool for the terminal.", + "notReadyHint": "Install Claude Agent below, or install it manually and re-detect." + }, + "opencode": { + "description": "Open-source coding agent for the terminal.", + "notReadyHint": "OpenCode is required for this framework. Install it below, or install it manually (see opencode.ai/docs) and re-detect." + }, + "codex": { + "description": "OpenAI's coding agent, connected through the Codex ACP adapter.", + "notReadyHintRepair": "The adapter or its paired native Codex runtime did not pass detection. Reinstall the managed pair below, or repair your manual installation and re-detect.", + "notReadyHintInstall": "Codex ACP is required for this framework. Install it below, or install it manually and re-detect." + } + }, + "installSources": { + "managed": { + "label": "App-managed download (recommended)", + "claudeDescription": "Downloads a self-contained Claude — no Node.js or npm required.", + "codexDescription": "Downloads a self-contained Codex ACP runtime — no Node.js or npm required.", + "opencodeDescription": "Downloads a self-contained OpenCode — no Node.js or npm required." + }, + "npm": { + "label": "npm (global install)" + }, + "officialScript": { + "ps1": "Official install.ps1", + "sh": "Official install.sh", + "generic": "Official install script" + }, + "menuLabel": "Install source", + "npmNotFound": "npm not found" + }, + "card": { + "install": "Install", + "repair": "Repair", + "installing": "Installing…", + "starting": "Starting…", + "active": "Active", + "installed": "Installed", + "hideLog": "Hide log", + "showLog": "Show log", + "installProgress": "Install progress", + "installLog": "Install log" + }, + "progress": { + "resolving": "Resolving…", + "downloading": "Downloading…", + "downloadingSized": "Downloading — {{received}} / {{total}} MB", + "extracting": "Extracting…", + "installing": "Installing…" + }, + "uninstall": { + "notManaged": "{{label}} was found on your system but isn't managed by the app, so it can't be uninstalled from here. Remove it with the tool you used to install it — for example `{{command}}`, your package manager, or by deleting it from your PATH — then re-detect.", + "active": "{{label}} is the active agent framework and can't be uninstalled. Switch to another framework first, then uninstall.", + "promptInFlight": "A task is running — wait for it to finish before uninstalling.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling…", + "title": "Uninstall {{name}}?", + "description": "This removes the {{name}} runtime this app downloaded and manages. A separate {{name}} you installed yourself is not affected. You can reinstall it here at any time.", + "cancel": "Cancel" + }, + "repairDialog": { + "title": "{{name}} needs repair", + "description": "Repair this agent before selecting it." + }, + "switchDialog": { + "title": "Switch to {{name}}?", + "description": "A conversation can't be resumed on a different backend, so switching starts a fresh agent session. Open conversations keep their existing messages, and their transcript is replayed to {{name}} so it can pick up where you left off (tool state is not carried over). New conversations are unaffected.", + "switch": "Switch" + } + }, + "skills": { + "detail": { + "updatedToday": "Updated today", + "updatedDaysAgo_one": "Updated {{count}} day ago", + "updatedDaysAgo_other": "Updated {{count}} days ago", + "sourceImported": "Imported", + "sourcePersonal": "Personal", + "sourceFeatured": "Featured", + "toggleAriaLabel": "Toggle {{name}}", + "files": "Files", + "details": "Details", + "author": "Author", + "license": "License", + "thirdParty": "Third-party software, content, terms, and information" + }, + "editor": { + "slugRequired": "Skill ID is required.", + "slugCharset": "Only lowercase letters, numbers, and hyphens.", + "slugReserved": "Can't start with {{prefixes}}.", + "slugTaken": "A skill with this ID already exists.", + "identity": "Identity", + "identityDescription": "How this skill appears in the catalog and to the agent.", + "name": "Name", + "nameAriaLabel": "Skill name", + "namePlaceholder": "e.g. Changelog style", + "id": "Skill ID", + "idAriaLabel": "Skill ID", + "idHintNew": "Used as the folder name — lowercase a–z, 0–9, hyphens. Locked after creation.", + "idHintLocked": "The skill ID is fixed after creation.", + "description": "Description", + "descriptionAriaLabel": "Skill description", + "descriptionPlaceholder": "One sentence — what does this skill teach the agent, and when does it apply?", + "descriptionHint": "This is how the agent decides when to use the skill — be specific.", + "content": "Content", + "contentDescription": "Markdown shown to the agent when the skill is invoked.", + "contentModeAriaLabel": "Content mode", + "modeWrite": "Write", + "modeUpload": "Upload", + "bodyAriaLabel": "Skill body", + "bodyPlaceholder": "# Instructions\n\nStep-by-step guidance for the agent…", + "bodyHint": "Paste a full SKILL.md — if it has a --- metadata block at the top, the fields above auto-fill.", + "metadataAriaLabel": "Skill metadata", + "savedMetadata": "Saved metadata", + "clearMetadataAriaLabel": "Clear skill metadata", + "clear": "Clear", + "dropToUpload": "Drop to upload", + "uploadContent": "Upload a SKILL.md or text file", + "uploadContentHint": "Its contents fill the editor; switch back to Write to tweak.", + "references": "References", + "referencesDescription": "Supporting files (scripts, templates, data) the skill can read at runtime.", + "dropReferences": "Drop reference files", + "addReferencesAriaLabel": "Add reference files", + "referencesBrowse": "Drop reference files or click to browse", + "referencesHint": "Saved under references/ in the skill.", + "removeReferenceAriaLabel": "Remove {{path}}", + "saving": "Saving…", + "save": "Save", + "publish": "Publish", + "loading": "Loading…" + }, + "panel": { + "filterAll": "All", + "filterFeatured": "Featured", + "filterImported": "Imported", + "filterPersonal": "Personal", + "groupFeatured": "Featured", + "groupFeaturedSubtitle": "Research skills bundled with the app.", + "groupImported": "Imported", + "groupImportedSubtitle": "Skills you added from GitHub.", + "groupPersonal": "Personal", + "groupPersonalSubtitle": "Your custom skills.", + "conversationImports": "Conversation imports", + "conversationImportsDescription": "Choose what conversations can import into Open Science.", + "skillPackages": "Skill packages", + "skillPackagesDescription": "Let the agent detect attached .zip and .skill packages and ask before importing them.", + "filterAriaLabel": "Filter skills by source", + "conversationImportToggleAriaLabel": "Toggle conversation Skill imports", + "searchAriaLabel": "Search skills", + "searchPlaceholder": "Search skills…", + "addSkill": "Add skill", + "writeFromScratch": "Write from scratch", + "writeFromScratchHint": "Open the skill creator", + "uploadSkills": "Upload skills", + "uploadSkillsHint": "Pick SKILL.md files", + "importFromGitHub": "Import from GitHub", + "importFromGitHubHint": "Add a skill from a repo", + "importInstalledSkills": "Import installed skills", + "importInstalledSkillsHint": "Scan global skill folders", + "editAriaLabel": "Edit {{name}}", + "deleteAriaLabel": "Delete {{name}}", + "toggleAriaLabel": "Toggle {{name}}", + "emptyPersonal": "Create a skill to teach Claude a workflow you use.", + "emptyImported": "No imported skills yet.", + "emptySearch": "No skills match your search.", + "desktopOnlyFallback": "Installed-skill import is available in the desktop app." + }, + "githubImport": { + "title": "Import from GitHub", + "description": "Preview a repo or skill folder (owner/repo, owner/repo@ref, or a github.com URL), then pick the skills you want to import.", + "inputAriaLabel": "GitHub skill URL or repo", + "inputPlaceholder": "owner/repo, owner/repo@ref, or a github.com URL", + "working": "Working…", + "preview": "Preview", + "found_one": "Found {{count}} skill", + "found_other": "Found {{count}} skills", + "selectAllAriaLabel": "Select all", + "selectAll": "Select all", + "invert": "Invert", + "importSelected": "Import selected ({{count}})", + "selectSkillAriaLabel": "Select {{name}}", + "previewSkillAriaLabel": "Preview {{name}}", + "imported": "Imported", + "importedHeading": "Imported skills", + "importedEmpty": "No imported skills yet. Repos you import from will appear here.", + "noSkillsFound": "No skills found in that repo.", + "scanFailed": "Scan failed.", + "importedCount_one": "Imported {{count}} skill.", + "importedCount_other": "Imported {{count}} skills.", + "importedThenFailed": "Imported {{count}}, then failed." + }, + "importApproval": { + "title": "Import Skill package?", + "description": "The agent requested an import from {{attachment}}. Review and choose exactly what Open Science may install.", + "found_one": "Found {{count}} skill", + "found_other": "Found {{count}} skills", + "selectAllAriaLabel": "Select all", + "selectAll": "Select all", + "invert": "Invert", + "selectCandidateAriaLabel": "Select {{name}}", + "alreadyImported": "Already imported", + "updatesExisting": "Updates existing", + "preview": "Preview", + "notImportable": "Not importable", + "importSelected": "Import selected", + "importCount_one": "Import {{count}} Skill", + "importCount_other": "Import {{count}} Skills" + }, + "upload": { + "title": "Upload skills", + "description": "Add skills from SKILL.md files or .zip / .skill bundles on your computer. You can select several files at once, and a single archive may contain multiple skills.", + "dropToUpload": "Drop to upload", + "inputAriaLabel": "Upload skill files", + "reading": "Reading…", + "dropHint": "Drag and drop or click to upload", + "formatHint": ".md files need a name and description in YAML frontmatter. .zip or .skill bundles must contain a SKILL.md. You'll confirm before anything is added.", + "writeInstead": "Write from scratch instead", + "confirmTitle": "Confirm import", + "confirmDescription": "Pick the skills you want to add. Nothing is written until you import.", + "found_one": "Found {{count}} skill", + "found_other": "Found {{count}} skills", + "selectAllAriaLabel": "Select all", + "selectAll": "Select all", + "invert": "Invert", + "selectCandidateAriaLabel": "Select {{name}}", + "previewCandidateAriaLabel": "Preview {{name}}", + "alreadyImported": "Already imported", + "nameExists": "Name exists", + "importing": "Importing…", + "importSelected": "Import selected ({{selected}})", + "chooseDifferent": "Choose different files", + "skipped_one": "Skipped {{count}} skill", + "skipped_other": "Skipped {{count}} skills", + "summary": "Imported {{imported}} · skipped {{skipped}} · failed {{failed}}", + "errorTooLarge": "{{fileName}}: file is too large (limit {{limit}}).", + "errorNoSkills": "{{fileName}}: no skills found in the bundle.", + "errorFile": "{{fileName}}: {{message}}", + "errorNoName": "{{fileName}}: needs a name in its YAML frontmatter.", + "errorUnsupported": "{{fileName}}: unsupported file — upload a .md file or a .zip / .skill bundle.", + "errorPreviewTooLarge": "{{fileName}}: preview exceeds the {{limit}} limit. You can still import it.", + "errorSelectionTooLarge": "Selection is too large ({{total}}); upload at most {{limit}} at a time." + }, + "candidatePreview": { + "fallbackTitle": "Skill preview", + "loadingSource": "Loading candidate content…", + "closeAriaLabel": "Close preview", + "loading": "Loading preview…", + "metadata": "Metadata", + "files": "Files" + }, + "agentHomeImport": { + "title": "Import installed skills", + "scanOne": "Scan {{path}} on this computer. Check skills to copy into Open Science; the originals stay in place.", + "scanTwo": "Scan {{path}} and {{frameworkPath}} on this computer. Check skills to copy into Open Science; the originals stay in place.", + "scanning": "Scanning…", + "rescan": "Rescan", + "skillsFound_one": "{{count}} skill found", + "skillsFound_other": "{{count}} skills found", + "selectAllAriaLabel": "Select all installed skills", + "selectAll": "Select all", + "invert": "Invert", + "importing": "Importing…", + "importSelected": "Import selected ({{count}})", + "selectSkillAriaLabel": "Select {{name}}", + "previewSkillAriaLabel": "Preview {{name}}", + "sourceShared": "Shared", + "imported": "Imported", + "empty": "No installed skills found in the scanned global folders.", + "scanFailed": "Scan failed.", + "importedCount_one": "Imported {{count}} skill.", + "importedCount_other": "Imported {{count}} skills.", + "importedWithFailures": "Imported {{count}}; {{failureCount}} failed. {{error}}", + "importFailed": "Could not import the selected skills." + } + }, + "toolPermission": { + "allow": "Always allow", + "ask": "Require approval", + "block": "Block" + }, + "network": { + "mirror": { + "title": "Package mirror", + "description": "Where the notebook environment fetches conda and Python packages from when installing or updating.", + "statusUnconfigured": "Not configured — packages come from the public hosts (conda.anaconda.org, pypi.org)", + "statusConfigured": "Fetching packages from {{hosts}}", + "configure": "Configure", + "edit": "Edit", + "condaChannel": "Conda channel mirror", + "pypiIndex": "Python package index (pip)", + "caBundle": "CA bundle path", + "optional": "(optional)", + "caBundleHint": "PEM bundle for a corporate TLS proxy; trusted by conda, pip, and R downloads.", + "viewMirrors": "View available mirrors", + "cancel": "Cancel", + "save": "Save", + "saving": "Saving…", + "saveFailed": "Could not save the package mirror." + } + }, + "storage": { + "appStorage": { + "title": "Application storage", + "description": "Open Science needs write access to its private configuration directory.", + "revealFinder": "Reveal in Finder", + "revealFolder": "Reveal in folder", + "revealFailed": "Could not reveal application storage.", + "checking": "Checking…", + "checkAgain": "Check again", + "continueToAgent": "Continue to repair Agent" + }, + "location": { + "title": "Data location", + "description": "Where Open Science stores your projects, artifacts, and other app data on this device.", + "changeLocation": "Change location", + "loading": "Loading…", + "label": "Location", + "pathAriaLabel": "Data root path", + "onDisk": "{{size}} on disk", + "onDiskDefault": "{{size}} on disk · default location", + "newLocation": "New location", + "pathPlaceholder": "/path/to/new/location", + "browse": "Browse…", + "moveBackToDefault": "Or move it back to the default location ({{path}})", + "defaultUnusable": "The default location is not usable.", + "willStoreIn": "Data will be stored in {{path}}", + "adoptNotice": "This folder already contains Open Science data. It will be used as-is (not merged)your current data folder is kept, so you can switch back. The app will restart.", + "moveNotice": "Your existing data (~{{size}}) will be moved to the new location — your files come with it, and nothing is left behind in the current folder.", + "runtimeRebuildNotice": "Python/R environments are rebuilt at the new location on first use (not moved).", + "switching": "Switching…", + "useThisFolder": "Use this folder", + "switchFailed": "Could not switch to this folder." + }, + "usage": { + "title": "Disk usage", + "empty": "No data yet.", + "total": "Total", + "available": "Available on disk", + "legendTitle": "{{label}}: {{size}}", + "categories": { + "artifacts": "Artifacts", + "uploads": "Uploads", + "runtime": "Runtime", + "notebooks": "Notebooks", + "workspaces": "Session workspaces" + } + }, + "changeWarning": { + "title": "Change data location?", + "description": "You can move Open Science's data to another folder on this device." + }, + "migration": { + "phases": { + "scan": "Scanning files…", + "copy": "Copying files…", + "verify": "Verifying…", + "delete": "Cleaning up…" + }, + "detectingTitle": "Checking for running sessions…", + "detectingDescription": "One moment.", + "confirmTitle": "Move app data?", + "confirmDescription": "Starting this move will interrupt the running sessions below and restart the app.", + "interruptAndMove": "Interrupt and move", + "movingTitle": "Moving app data…", + "preparing": "Preparing…", + "elapsedAriaLabel": "Elapsed time", + "elapsed": "Elapsed {{time}}", + "doNotQuit": "Don't quit Open Science or turn off your computer until this finishes.", + "doneTitle": "Data copied", + "doneDescription": "Restart to switch to the new location. Nothing is changed until you do — choose Keep current location to stay where you are and discard the copy.", + "discarding": "Discarding…", + "keepCurrentLocation": "Keep current location", + "restartNow": "Restart now", + "committingTitle": "Switching over…", + "committingDescription": "Finishing up and restarting. This can take a moment — please don't quit.", + "switchoverTitle": "Data moved — please restart", + "failedTitle": "Move failed", + "genericError": "Something went wrong. Please close and try again." + }, + "adoptDialog": { + "title": "Use this folder?", + "description": "Open Science will restart and use this folder as-is — its contents are not merged with your current data, and anything it's missing will show as unavailable. Your current data folder is left untouched, so you can switch back." + } + }, + "connectors": { + "approval": { + "title": "Allow external request?", + "description": "Claude wants to call a connector tool that sends data to an external service. Approve only if you trust this connector with the current request.", + "connector": "Connector", + "tool": "Tool", + "args": "Args", + "deny": "Deny", + "allowOnce": "Allow once", + "thisConversation": "This session", + "thisProject": "This project", + "global": "Global" + }, + "detail": { + "featured": "Featured", + "toggleAriaLabel": "Toggle {{name}}", + "skipApprovals": "Skip approvals", + "skipApprovalsDescription": "Allow Claude to use every tool from this connector without showing an approval card each time.", + "skipApprovalsAriaLabel": "Skip approvals for {{name}}", + "tools": "Tools", + "toolsDescription": "What Claude can do with this connector", + "noTools": "This connector has no tools.", + "toolPermissionAriaLabel": "Permission for {{name}}", + "noToolDescription": "No description provided for this tool.", + "details": "Details", + "thirdParty": "Third-party software, content, terms, and information", + "terms": "Terms", + "askFallback": "Ask when no Session, Project, or Global permission applies.", + "rememberedApprovals_one": "Remembered approvals: {{count}}", + "rememberedApprovals_other": "Remembered approvals: {{count}}", + "rememberedBlocked": "currently blocked", + "rememberedUnnecessary": "currently unnecessary", + "scopeGlobal": "Global", + "scopeProject": "Project", + "scopeSession": "Session", + "managePermissions": "Manage permissions" + }, + "panel": { + "contactEmail": "Contact email", + "contactEmailDescription": "When allowed, shared with research data services that ask for a contact email (such as those run by NCBI, EBI, and OurResearch) on requests made on your behalf.", + "contactEmailAriaLabel": "Contact email", + "apiKeyAriaLabel": "NCBI API key", + "apiKeyPlaceholder": "Optional API key", + "apiKeyHint": "Higher NCBI rate limits (optional).", + "save": "Save", + "clearKey": "Clear key", + "notSet": "Not set", + "edit": "Edit", + "filterAriaLabel": "Filter connectors by group", + "filterAll": "All", + "filterFeatured": "Featured", + "filterDirectory": "Directory", + "filterCustom": "Custom", + "searchAriaLabel": "Search connectors", + "searchPlaceholder": "Search connectors…", + "addConnector": "Add connector", + "localCommand": "Local command", + "localCommandDescription": "Run an MCP server via a command", + "remoteServer": "Remote server", + "remoteServerDescription": "Connect to an MCP server URL", + "featuredSubtitle": "Research connectors from Anthropic", + "directorySubtitle": "Syncs with the Claude Connectors Directory", + "customSubtitle": "Connectors you added", + "noMatches": "No connectors match your search.", + "noCustom": "Add a custom connector to connect your own server.", + "editNamed": "Edit {{name}}", + "removeNamed": "Remove {{name}}" + }, + "form": { + "typeAriaLabel": "Connector type", + "localCommand": "Local command", + "remoteServer": "Remote server", + "optional": "(optional)", + "name": "Name", + "nameAriaLabel": "Name", + "namePlaceholder": "e.g. Memory server", + "nameImmutable": "The name is the connector's identity and can't be changed.", + "description": "Description", + "descriptionAriaLabel": "Description", + "descriptionPlaceholder": "What this connector provides", + "command": "Command", + "commandAriaLabel": "Command", + "customCommandAriaLabel": "Custom command", + "commandOptions": { + "npx": "npx — Node package", + "uvx": "uvx — Python (uv)", + "node": "node — script file", + "python3": "python3 — script file", + "docker": "docker — container", + "other": "Other…" + }, + "args": "Arguments", + "argsAriaLabel": "Arguments", + "argsHint": "Separated by spaces or newlines.", + "env": "Environment variables", + "envAriaLabel": "Environment variables", + "envHint": "One KEY=VALUE per line.", + "envHintEdit": "One KEY=VALUE per line. Leave blank to keep the current values.", + "url": "Server URL", + "urlAriaLabel": "Server URL", + "transport": "Transport", + "transportAriaLabel": "Transport", + "headers": "Headers", + "headersAriaLabel": "Headers", + "headersHint": "One Name: Value per line (not JSON).", + "headersHintEdit": "One Name: Value per line (not JSON). Leave blank to keep the current values.", + "trustAriaLabel": "I trust this connector", + "trust": "I trust this connector. Only add connectors from developers you trust.", + "saveFailed": "Failed to save connector.", + "saving": "Saving…", + "adding": "Adding…", + "saveChanges": "Save changes", + "addConnector": "Add connector" + } + }, + "compute": { + "banner": "Connect where heavy compute runs — your own servers over SSH.", + "list": { + "title": "SSH hosts", + "description": "Servers, clusters or job submission nodes from your SSH host lists", + "add": "Add SSH host", + "removed": "Removed {{name}}.", + "loadError": "Couldn't load hosts: {{message}}", + "loading": "Loading hosts…", + "empty": "No SSH hosts yet. Add one to let Open Science run compute on your servers." + }, + "status": { + "connected": "Connected", + "failed": "Probe failed", + "notProbed": "Not probed" + }, + "probed": { + "never": "Not probed yet", + "now": "probed just now", + "minute": "probed {{count}} min ago", + "hour": "probed {{count}} h ago", + "day": "probed {{count}} d ago", + "week": "probed {{count}} w ago", + "month": "probed {{count}} mo ago", + "year": "probed {{count}} y ago" + }, + "card": { + "browse": "Browse files on {{name}}", + "browseDisabled": "Host must be probed and reachable to browse files", + "browseTooltip": "Browse files", + "browseDisabledTooltip": "Probe the host first to enable browsing", + "remove": "Remove {{name}}", + "removeTooltip": "Remove host" + }, + "add": { + "intro": "Pick a host alias from your ~/.ssh/config, or type one. Open Science will use it as a compute provider via your existing SSH key — no credentials are copied.", + "aliasFromConfig": "From ~/.ssh/config", + "aliasSelectAriaLabel": "Pick a host from ~/.ssh/config", + "aliasSelectEmpty": "No hosts in ~/.ssh/config", + "aliasSelectPlaceholder": "Pick a host…", + "aliasTypedLabel": "Or type a host alias", + "aliasTypedPlaceholder": "e.g. biowulf, lab-gpu, coder.myworkspace", + "detailsLabel": "Anything Open Science should know? (optional)", + "detailsCount": "{{used}} / {{limit}} chars", + "detailsPlaceholder": "How do jobs run here — sbatch, qsub, or just bash? Is it OK to pip/conda install, and where should new envs go? Any partition, account, or module to use?", + "advanced": "Advanced (override ~/.ssh/config)", + "advancedHint": "By default Open Science resolves connection details by running ssh -G against the alias in your ~/.ssh/config. Set these only if you need to override that.", + "user": "User", + "userHint": "Leave empty to use User from ~/.ssh/config.", + "port": "Port", + "portHint": "Leave empty for 22 or Port from ~/.ssh/config.", + "identity": "Identity file", + "identityHint": "Leave empty for ssh-agent / IdentityFile from ~/.ssh/config.", + "submit": "Add", + "submitting": "Adding host…", + "failed": "Could not add host." + }, + "detail": { + "missing": "This host no longer exists.", + "loading": "Loading host…", + "probe": "Probe", + "probing": "Probing…", + "retryProbe": "Retry probe", + "probeFailedUnexpectedly": "Probe failed unexpectedly.", + "remove": "Remove", + "resources": "Resources", + "cpus": "CPUs", + "ram": "GB RAM", + "scheduler": "scheduler", + "details": "Details", + "detailsDescription": "Free-form notes about this provider. Open Science reads and adds to them as it learns.", + "detailsAriaLabel": "Details document", + "detailsCount": "{{used}} / {{limit}} chars", + "detailsTooLong": "Details must be {{limit}} characters or fewer.", + "detailsSaveFailed": "Failed to save details.", + "detailsSkeleton": "(auto-generated from probe)", + "detailsEmpty": "No notes yet. Click Edit to add details about this provider.", + "showMore": "Show more", + "showLess": "Show less", + "scratch": "Scratch root", + "scratchDescription": "Working directory for remote jobs. Pinned paths are never overwritten by re-probe.", + "scratchAriaLabel": "Scratch root path", + "scratchPlaceholder": "/scratch/username", + "scratchSaveFailed": "Failed to set scratch root.", + "scratchPinned": "PINNED", + "scratchEmpty": "Not set. Will be updated from $SCRATCH on next probe.", + "concurrency": "Concurrent job limit", + "concurrencyDescription": "Maximum jobs running at the same time on this host (1–500). Not yet enforced.", + "concurrencyAriaLabel": "Concurrent job limit", + "concurrencyInvalid": "Must be an integer between 1 and 500.", + "concurrencySaveFailed": "Failed to set concurrency limit.", + "concurrencyDefault": "{{value}} (default)", + "edit": "Edit", + "save": "Save", + "saving": "Saving…" + }, + "approval": { + "title": "Allow remote command?", + "description": "Remote commands run as your account on the host and are not sandboxed. Approve only if you trust this command.", + "host": "Host", + "intent": "Intent", + "command": "Command", + "inputs": "Inputs", + "showFull": "Show full command", + "showLess": "Show less", + "deny": "Deny", + "once": "Once", + "conversation": "This session", + "project": "This project", + "always": "Always", + "broadScopeSubject": "remote commands on {{host}}" + } + }, + "fileBrowser": { + "ariaLabel": "Remote file browser", + "host": "Host", + "close": "Close file browser", + "back": "Go back", + "up": "Go up one level", + "goTo": "Go to", + "goToLocations": "Go-to locations", + "scratch": "Scratch", + "home": "Home", + "pinCurrent": "Pin current folder", + "bookmarks": "Bookmarks", + "removeBookmark": "Remove bookmark {{path}}", + "addressAriaLabel": "Current directory path", + "refresh": "Refresh directory listing", + "loading": "Loading", + "contents": "Directory contents", + "columnName": "Name", + "columnSize": "Size", + "columnModified": "Modified", + "empty": "Empty directory", + "truncated": "Showing first 5,000 entries. Navigate into a subdirectory to see more.", + "errorTitle": "Couldn't open this path.", + "retry": "Retry", + "goHome": "Go to home", + "invalidPath": "Path must be absolute and contain no control characters.", + "unknownError": "Unknown error", + "details": "Details", + "closeDetail": "Close detail panel", + "metaSize": "SIZE", + "metaModified": "MODIFIED", + "metaType": "TYPE", + "noPreview": "No preview · {{size}}", + "download": "Download", + "downloading": "Downloading…", + "downloadAriaLabel": "Download file to OS Downloads folder", + "downloadFailed": "Download failed", + "savedToDownloads": "Saved to Downloads: {{name}}", + "showInFinder": "Show in Finder", + "addToProject": "Add to project", + "addToProjectTitle": "Coming soon — remote import to project artifacts is not yet available", + "addToProjectAriaLabel": "Add file to current project as artifact (coming soon)", + "copyPath": "Copy path", + "copied": "Copied!", + "copyAriaLabel": "Copy remote absolute path to clipboard", + "age": { + "second": "{{count}}s", + "minute": "{{count}}m", + "hour": "{{count}}h", + "day": "{{count}}d" + }, + "type": { + "file": "file", + "text": "Text", + "image": "Image", + "archive": "Archive", + "binary": "Binary", + "library": "Library", + "log": "Log", + "rData": "R Data" + } + } +} diff --git a/src/renderer/src/locales/en/workspace.json b/src/renderer/src/locales/en/workspace.json new file mode 100644 index 000000000..1c60fb02d --- /dev/null +++ b/src/renderer/src/locales/en/workspace.json @@ -0,0 +1,432 @@ +{ + "composer": { + "placeholder": "Ask anything — / for skills, @ for files, {{shortcut}} to search", + "ariaLabel": "Ask anything", + "addAttachmentOrReview": "Add attachment or request review", + "attachFiles": "Attach files", + "attachmentLimits": "Any file type · {{size}} per file. Large files are linked, not embedded.", + "requestReview": "Request review", + "cancelRun": "Cancel run", + "sendMessage": "Send message", + "dropFiles": "Drop files to attach", + "removeAttachment": "Remove attachment {{name}}", + "cancelAttachment": "Cancel attachment {{name}}", + "removeFailedAttachment": "Remove failed attachment {{name}}", + "uploadingFile": "Uploading {{name}}", + "notebook": "Notebook", + "openNotebook": "Open notebook", + "compacting": "Compacting conversation to fit the context limit…" + }, + "previews": { + "fileMissing": "This file is no longer available — it may have been moved or deleted.", + "fileOutsideStorage": "This file isn't in your current storage location — it may belong to a data folder you moved away from.", + "missingTag": "Missing", + "unavailableTitle": "Preview unavailable", + "fileUnavailableTitle": "File unavailable", + "unsupportedMessage": "This file type isn't supported for preview", + "renderingPreview": "Rendering preview", + "retry": "Retry", + "loadingCode": "Preparing code file", + "loadingCsv": "Preparing data", + "loadingFasta": "Preparing sequence", + "loadingHtml": "Preparing document", + "loadingImage": "Preparing image", + "loadingJson": "Preparing data", + "loadingMarkdown": "Preparing document", + "loadingMolecule": "Preparing structure", + "loadingPdb": "Preparing structure", + "loadingPdf": "Preparing document", + "loadingPresentation": "Preparing presentation", + "loadingSpreadsheet": "Preparing spreadsheet", + "loadingText": "Preparing text file", + "loadingTiff": "Preparing image", + "loadingUnknown": "Preparing preview", + "loadingWord": "Preparing document", + "errorCsv": "CSV couldn't be read for preview", + "errorFasta": "FASTA couldn't be read for preview", + "errorHtml": "HTML couldn't be read for preview", + "errorImage": "Image couldn't be loaded for preview", + "errorJson": "JSON couldn't be read for preview", + "errorMarkdown": "Markdown couldn't be read for preview", + "errorMolecule": "Structure file couldn't be read for preview", + "errorPdb": "PDB couldn't be read for preview", + "errorPdf": "This PDF couldn't be rendered for preview", + "errorText": "File couldn't be read for preview", + "errorOffice": "This Office file couldn't be rendered for preview", + "previewOf": "Preview of {{name}}", + "columnFallback": "Column {{index}}", + "invalidJson": "Invalid JSON", + "invalidJsonDetail": "Invalid JSON: {{error}}", + "zoomIn": "Zoom in", + "zoomOut": "Zoom out", + "resetZoom": "Reset zoom", + "htmlRender": "Render", + "htmlSource": "Source", + "htmlShowRendered": "Show rendered HTML", + "htmlShowSource": "Show HTML source", + "pageNumber": "Page {{page}}", + "previousPage": "Previous page", + "previousPreviewPage": "Previous preview page", + "nextPage": "Next page", + "nextPreviewPage": "Next preview page", + "moleculeViewerBanner": "Using OpenChemLib viewer", + "pdbViewerBanner": "Using 3Dmol.js viewer", + "moleculeRenderFailed": "Structure could not be rendered: {{error}}", + "moleculeRenderError": "Could not render structure", + "moleculeRendererFailed": "Molecule renderer failed to load", + "moleculePreviewOf": "Structure preview of {{name}}", + "pdbPreviewOf": "3D preview of {{name}}", + "pdbAtomCount": "{{count}} atoms", + "pdbStyleLabel": "Style:", + "pdbCartoon": "Cartoon", + "pdbStick": "Stick", + "pdbSphere": "Sphere", + "pdbSurface": "Surface", + "pdbLine": "Line", + "pdbCartoonUnavailable": "Cartoon requires a protein or nucleic-acid backbone", + "pdbViewerFailed": "Viewer failed", + "pdbViewerInitFailed": "PDB viewer could not be initialized: {{error}}", + "pdbControlsHint": "Drag to rotate · Scroll to zoom · Shift + drag to pan", + "officeChecking": "Checking the Office file", + "officeStarting": "Starting Office preview", + "officeTooLargeTitle": "File too large to preview", + "officeTooLargeMessage": "This file is larger than 40 MB. Download it to view.", + "officeInvalidPackage": "This Office file is damaged or unsupported. Download it to view.", + "officeResourceLimit": "This Office file exceeds the safe preview limits. Download it to view.", + "figureOutput": "Figure output", + "csvStats": "{{rows}} rows · {{columns}} columns", + "csvStatsTruncated": "{{rows}}+ rows · {{columns}} columns", + "csvHiddenColumns": "+{{count}} more" + }, + "mention": { + "skillSuggestions": "Skill suggestions", + "artifactSuggestions": "Artifact suggestions", + "loadingFiles": "Loading project files…", + "loadError": "Could not load project files", + "noArtifacts": "No artifacts yet", + "sectionUploads": "User uploads", + "sectionArtifacts": "Other artifacts", + "sourceFeatured": "Featured", + "sourceImported": "Imported", + "sourcePersonal": "Personal", + "hintNavigate": "navigate", + "hintSelect": "select", + "hintClose": "close" + }, + "modelPicker": { + "noModelAvailable": "No model available", + "noModelAvailableAriaLabel": "No model available — open settings", + "selectModel": "Select model", + "noCompatibleModel": "No compatible model", + "openSettings": "Open Settings" + }, + "uploadStatus": { + "queued": "Queued", + "cancelling": "Cancelling…", + "failed": "Upload failed" + }, + "preview": { + "expand": "Expand preview panel", + "collapse": "Collapse preview panel", + "resizeRightPanel": "Resize right panel", + "openPreviews": "Open previews", + "closePreview": "Close preview of {{title}}", + "closePreviewTooltip": "Close preview", + "openFullScreen": "Open full screen preview", + "openFullScreenOf": "Open full screen preview of {{title}}", + "localBadge": "This computer" + }, + "reportError": { + "button": "Report error", + "ariaLabel": "Report this error" + }, + "permission": { + "scopeOnce": "Once", + "scopeOnceSubtitle": "This call only", + "scopeSession": "This session", + "scopeSessionSubtitle": "Across restarts for this session", + "allow": "Allow", + "deny": "Deny", + "allowOnce": "Allow once", + "allowAlways": "Allow always", + "rejectOnce": "Reject once", + "rejectAlways": "Reject always", + "authorizationScope": "Authorization scope", + "impactInfo": "Permission impact information", + "permissionInfo": "Permission information", + "chooseScope": "Choose authorization scope", + "scopeDescriptionOnce": "Approval applies to this call only.", + "scopeDescriptionSession": "Approval remains attached to this session across restarts.", + "scopeDescriptionSessionNotebook": "Approval covers later {{runtime}} calls in this session.", + "scopeDescriptionNone": "No approval scope is available for this request.", + "actionTitleNotebook": "Run notebook cell", + "actionTitleArtifact": "Artifact file input", + "actionTitleMcp": "External service input", + "actionTitleCommand": "Run command", + "runtimePython": "Python", + "runtimeR": "R", + "runtimeJs": "JavaScript REPL", + "runtimeBash": "notebook shell", + "scopeProject": "This project", + "scopeProjectSubtitle": "Across sessions in this project", + "scopeGlobal": "Global", + "scopeGlobalSubtitle": "Across all projects", + "suffixOnce": "once", + "suffixSession": "for this session", + "suffixProject": "for this project", + "suffixGlobal": "globally", + "scopeDescriptionProject": "Approval applies to matching calls in this project.", + "scopeDescriptionGlobal": "Approval applies to matching calls in every project." + }, + "reviewer": { + "title": "Session Reviewer", + "checks": "Checks", + "noChecks": "No checks recorded.", + "noReview": "No review available for this session.", + "statusPass": "pass", + "statusWarn": "warn", + "statusFail": "fail", + "refLabel": "Ref:", + "log": "Reviewer log", + "collapseLog": "Collapse Reviewer log", + "expandLog": "Expand Reviewer log", + "input": "Input", + "output": "Output", + "exitCode": "exit {{code}}", + "staleNotice": "This turn changed after the review ran (e.g. an artifact was edited). The result below may be out of date — re-run the review to refresh it." + }, + "errorReport": { + "title": "Report this error", + "description": "This report is posted publicly on GitHub. Edit the error text below to remove anything sensitive before sharing. Your runtime log stays on this device and is never attached automatically.", + "errorDetails": "Error details", + "alsoIncluded": "Also included", + "reportEnvironment": "Report environment", + "githubPrefill": "GitHub issue prefill", + "consent": "I've reviewed the details above and agree to share them in a public GitHub issue, subject to GitHub's", + "privacyStatement": "Privacy Statement", + "revealLog": "Reveal log file", + "copyDetails": "Copy details", + "copied": "Copied", + "openIssue": "Open GitHub issue" + }, + "notebook": { + "title": "Session notebook", + "agents_one": "{{count}} agent", + "agents_other": "{{count}} agents", + "cells_one": "{{count}} cell", + "cells_other": "{{count}} cells", + "repl": "{{count}} repl", + "shell": "{{count}} shell", + "loading": "Loading notebook…", + "loadFailed": "Failed to load notebook.", + "noRecords": "No execution records for this session.", + "exporting": "Exporting…", + "exportIpynb": ".ipynb", + "exportAll": "All ({{count}})", + "downloadAs": "Download {{kernel}} cells as .ipynb", + "downloadAsAriaLabel": "Download {{kernel}} as .ipynb", + "downloadAsGeneric": "Download as .ipynb", + "errorAtLine": "error (line {{line}})", + "error": "error", + "downloadAllAriaLabel": "Download separate notebooks by kernel ({{count}})", + "downloadAllTooltip": "Save one .ipynb per data kernel ({{count}} files) to a chosen directory.", + "noDataKernel": "Run a Python or R cell first to enable .ipynb export.", + "controlFallback": " (control tab falls back to most recent data kernel)", + "savedNotebooks": "Saved {{count}} notebooks to {{directory}}" + }, + "files": { + "filterAriaLabel": "Filter project files", + "artifacts": "Artifacts", + "localSection": "This computer", + "addLocalFolder": "Add local folder…", + "soon": "Soon", + "remote": "Remote", + "hostUnreachable": "Host unreachable", + "addHost": "Add SSH host…", + "retryIndexing": "Retry indexing project files", + "uploads": "Your uploads", + "allArtifacts": "All artifacts", + "sessionFallback": "Session {{id}}", + "noMore": "No more", + "loading": "Loading...", + "loadMore": "Load more", + "loadMoreSessions": "Load more sessions", + "fileCount_one": "{{count}} file", + "fileCount_other": "{{count}} files", + "indexIncomplete": "Some files could not be indexed yet.", + "retrying": "Retrying...", + "retry": "Retry", + "noFiles": "No files yet", + "noSearchMatches": "No files match “{{query}}”", + "generatedFiles": "Generated files", + "loadMoreFrom": "Load more files from {{title}}", + "loadMoreUploads": "Load more uploaded files" + }, + "sidebar": { + "allProjects": "All projects", + "sessions": "Sessions", + "new": "New", + "files": "Files", + "settings": "Settings", + "sessionActions": "Session actions", + "sessionActionsFor": "Open actions for {{title}}", + "statusIdle": "Idle", + "statusRunning": "Running", + "statusWaiting": "Waiting for permission", + "statusError": "Error", + "sessionStatus": "Session status: {{status}}", + "pin": "Pin", + "unpin": "Unpin", + "rename": "Rename…", + "viewNotebook": "View notebook", + "delete": "Delete", + "pinned": "Pinned", + "active": "Active", + "closeNavigation": "Close navigation" + }, + "message": { + "generated": "GENERATED · {{count}}", + "expandFiles": "Expand generated files", + "collapseFiles": "Collapse generated files", + "moreFiles": "+{{count}} more", + "showLess": "Show less", + "editPlaceholder": "Edit your message", + "editAriaLabel": "Edit message", + "cancel": "Cancel", + "send": "Send", + "copy": "Copy message", + "copied": "Copied", + "edit": "Edit message", + "previousRevision": "Previous message revision", + "nextRevision": "Next message revision", + "revisionCounter": "Message revision", + "previewAttachment": "Preview uploaded attachment {{name}}", + "previewGeneratedFile": "Preview generated file {{name}}", + "openSkill": "Open skill {{name}}", + "previewArtifact": "Preview {{name}}", + "conversation": "Conversation", + "sent": "Sent", + "completed": "Completed", + "failed": "Failed", + "elapsed": "Elapsed {{duration}}", + "elapsedLabel": "Elapsed run time", + "usage": "Usage", + "usageAvailable": "Token usage for this response", + "usageUnavailable": "Token usage unavailable for this response", + "usageInput": "Input", + "usageOutput": "Output", + "usageCache": "Cache", + "usageCacheRead": "Cache read", + "usageCacheWrite": "Cache write", + "usageBreakdown": "{{entries}}; Total {{total}} tokens", + "usageBreakdownUnavailable": "Token usage breakdown unavailable", + "usageTotal": "Total", + "usageTurns_one": "{{count}} turn", + "usageTurns_other": "{{count}} turns", + "usageFramework": "Agent framework: {{name}}", + "usageProvider": "Model provider: {{name}}", + "usageProviderWithModel": "Model provider: {{name}}; model: {{model}}", + "usageAgentDetail": "Agent: {{name}}", + "usageModelDetail": "Model: {{model}}" + }, + "deleteSession": { + "title": "Delete Session?", + "description": "This will permanently delete \"{{title}}\". Artifacts created in this session will remain in the project. This action cannot be undone.", + "delete": "Delete" + }, + "renameSession": { + "title": "Rename session", + "description": "Update the name shown in the session list.", + "nameAriaLabel": "Session name", + "rename": "Rename" + }, + "conversation": { + "newTitle": "New conversation" + }, + "interrupted": { + "resume": "Resume", + "resuming": "Resuming…", + "resumeAriaLabel": "Resume session", + "defaultMessage": "This session was interrupted." + }, + "agentControls": { + "ariaLabel": "Agent controls: {{profile}}, auto-review {{autoReview}}", + "permissionMode": "Permission mode", + "permissionDescription": "How much the agent can do without asking first.", + "profileAsk": "Ask for approval", + "profileAskShort": "Ask", + "profileAskDescription": "Ask before file edits, commands, network, and MCP tools.", + "profileAuto": "Auto-approve edits", + "profileAutoShort": "Auto", + "profileAutoDescription": "Auto-approve edits to files in the workspace. Still ask before commands, network, and MCP.", + "profileFull": "Full access", + "profileFullShort": "Full access", + "profileFullDescription": "Run everything without prompts, including commands and network.", + "profileFullUnavailable": "The current agent does not support native bypass mode.", + "conservativeNote": "This agent has no native auto mode. Open Science auto-approves only edits to files inside the workspace — commands, network, and MCP tools still ask.", + "autoReview": "Auto-review", + "autoReviewDescription": "A reviewer agent checks every change before it lands.", + "grantsTitle": "Allowed this session", + "clearAll": "Clear all", + "clearAllAriaLabel": "Clear all session grants", + "revokeGrantAriaLabel": "Revoke session grant for {{label}}", + "grantsBadgeAriaLabel": "{{count}} allowed this session", + "specialist": "Specialist", + "specialistDescription": "Bind a personal specialist to this conversation.", + "specialistNone": "None", + "specialistUnavailable": "Unavailable", + "specialistCreateNew": "Create new…", + "ssh": "SSH", + "compute": "Compute", + "computeDescription": "Run jobs on a remote SSH host, or manage hosts.", + "noHosts": "No SSH hosts registered", + "loading": "Loading…", + "manageCompute": "Manage compute...", + "fullAccessDialogTitle": "Enable Full access?", + "fullAccessDialogDescription": "The agent can run commands, change files, execute notebook code, and make network requests without asking first. Authentication failures and execution errors can still stop the run.", + "enable": "Enable" + }, + "editConfirm": { + "title": "Resend on a new branch?", + "description_one": "Sending this edited prompt starts a new branch from here. The {{count}} turn that currently follows remains available from the message revision controls.", + "description_other": "Sending this edited prompt starts a new branch from here. The {{count}} turns that currently follow remain available from the message revision controls.", + "branch": "Branch and resend" + }, + "envStatus": { + "failed": "Environment update failed", + "updating": "Updating the notebook environment… {{percent}}%", + "retry": "Retry" + }, + "context": { + "estimatedAriaLabel": "Estimated context: {{label}}", + "usedAriaLabel": "Context used: {{label}}", + "window": "Context window", + "compact": "Compact", + "compacting": "Compacting…", + "compactAriaLabel": "Compact context", + "compactingAriaLabel": "Compacting context" + }, + "notebookPreview": { + "sourceUser": "you", + "placeholder": "run code in this kernel...", + "rRestartBanner": "Installed R packages need a kernel restart to load.", + "restartR": "Restart R kernel", + "restarting": "Restarting…", + "pythonKernelLabel": "Python kernel · shared with the agent", + "statusRunning": "running", + "statusIdle": "idle", + "rPreparing": "R (preparing…)" + }, + "tool": { + "outputTruncated": "Output truncated", + "loadingPreview": "Loading preview…", + "searchQuery": "query", + "webSearch": "Web Search", + "searchResult_one": "{{count}} result", + "searchResult_other": "{{count}} results" + }, + "agentLoading": { + "responding": "Agent is responding", + "slow": "· taking longer than usual" + } +} diff --git a/src/renderer/src/locales/zh-Hans/common.json b/src/renderer/src/locales/zh-Hans/common.json new file mode 100644 index 000000000..84b0e45ee --- /dev/null +++ b/src/renderer/src/locales/zh-Hans/common.json @@ -0,0 +1,50 @@ +{ + "language": { + "label": "语言", + "ariaLabel": "界面语言", + "system": "系统", + "systemDescription": "跟随设备设置", + "names": { + "en": "English", + "zh-Hans": "简体中文", + "zh-Hant": "繁體中文" + } + }, + "theme": { + "label": "主题", + "ariaLabel": "主题", + "system": "系统", + "systemDescription": "跟随设备设置", + "light": "浅色", + "lightDescription": "始终浅色", + "dark": "深色", + "darkDescription": "始终深色", + "triggerLabel": "主题:{{theme}}" + }, + "actions": { + "open": "打开", + "opening": "正在打开…", + "reveal": "在文件夹中显示", + "working": "处理中…", + "back": "上一步", + "cancel": "取消", + "continue": "继续", + "delete": "删除", + "quit": "退出", + "minimizeToTray": "最小化到托盘", + "close": "关闭", + "deleting": "删除中…" + }, + "states": { + "notAvailableYet": "暂不可用。" + }, + "compatibility": { + "routeSeparator": " 或 ", + "codexNeedsResponses": "{{provider}} 使用 /v1/chat/completions。Codex 需要 /v1/responses;请选择一个 OpenAI Responses 服务商,或切换 Agent 框架。", + "routeMismatch": "{{framework}} 需要 {{frameworkRoutes}},但 {{provider}} 使用 {{providerRoutes}}。" + }, + "scroll": { + "toEnd": "滚动到底部", + "toStart": "滚动到顶部" + } +} diff --git a/src/renderer/src/locales/zh-Hans/components.json b/src/renderer/src/locales/zh-Hans/components.json new file mode 100644 index 000000000..d6e1fd752 --- /dev/null +++ b/src/renderer/src/locales/zh-Hans/components.json @@ -0,0 +1,193 @@ +{ + "dialog": { + "close": "关闭", + "cancel": "取消", + "back": "返回" + }, + "update": { + "title": "有可用更新", + "versionRange": "v{{current}} → v{{latest}}", + "whatsNew": "更新内容", + "viewNotes": "在 GitHub 上查看完整发布说明", + "noNotes": "这个版本的发布说明无法在应用内显示。", + "viewNotesShort": "在 GitHub 上查看发布说明", + "failed": "更新失败", + "downloadManually": "手动下载", + "restart": "重启以更新", + "applyingNotice": "Open Science 正在停止后台任务,并将关闭以完成安装。更新可能需要一些时间,请勿在此期间重新打开应用。更新完成后应用会自动重新打开。", + "preparing": "正在准备更新……", + "openInstaller": "打开安装程序", + "downloading": "正在下载 {{percent}}%", + "downloadSized": "下载更新({{size}})", + "download": "下载更新" + }, + "closeConfirm": { + "quitTitle": "退出 {{appName}}?", + "chooseTitle": "最小化还是退出?", + "quitDescription": "还有任务正在运行,退出会中断它们。", + "chooseDescription": "应用可以保留在托盘中继续运行,你也可以直接退出。", + "rowTitle": "{{project}} — {{title}}", + "dontAskAgain": "不再询问" + }, + "fieldHelp": { + "trigger": "更多信息" + }, + "lifecycleToast": { + "sessionCreated": "会话由外部创建", + "view": "查看", + "dismiss": "忽略" + }, + "jobStatus": { + "queued": "排队中", + "running": "运行中", + "done": "已完成", + "failed": "失败", + "timeout": "超时", + "error": "错误" + }, + "jobs": { + "badgeActiveLabel_other": "{{count}} 个远程任务正在运行", + "badgeActiveText": "{{count}} 个运行中 · {{elapsed}}", + "badgeIdleLabel_other": "{{count}} 个远程任务", + "badgeIdleText_other": "{{count}} 个任务", + "tooltipHeading": "远程 · {{count}}", + "modalLabel": "远程任务详情", + "listTitle": "本会话中运行的任务", + "empty": "本会话没有远程任务。", + "provider": "服务商", + "status": "状态", + "runtime": "运行时长", + "remoteWorkdir": "远程工作目录", + "jobId": "任务 ID", + "noOutput": "暂无输出。" + }, + "reviewer": { + "title": "审查", + "reviewing": "正在审查…", + "error": "审查出错", + "noIssues": "未发现问题", + "noIssuesStale": "未发现问题(已过期)", + "findings_other": "{{count}} 项发现", + "findingsStale_other": "{{count}} 项发现(已过期)", + "pending": "等待审查", + "checks_other": "{{count}} 项检查", + "fixLimitReached": "已达修复上限", + "goToTranscript": "跳转到对话记录", + "staleNotice": "这次审查之后轮次发生了变化。", + "rerun": "重新审查", + "rerunning": "正在重新审查…", + "selfCorrectNote": "Agent 会读取这些发现,并在下一条消息中自行修正。", + "reflagged": "已重新标记 ×{{count}}" + }, + "dataRoot": { + "warning": "这个文件夹由 {{appName}} 管理。不要移动、重命名或删除其中的文件,否则可能破坏你的项目和历史记录。", + "missing": { + "title": "找不到数据文件夹", + "description": "找不到你的数据文件夹 {{path}}。它可能已被删除,或者位于一个尚未连接的磁盘上。", + "stillMissing": "仍然找不到。重新连接磁盘后再试,或者换一个位置。", + "notUsable": "所选文件夹不可用。", + "switchFailed": "无法切换到这个文件夹。", + "retry": "重新连接并重试", + "retrying": "正在检查…", + "chooseAnother": "换一个位置", + "switching": "正在切换…", + "continueEmpty": "使用空文件夹继续", + "continueNote": "{{appName}} 会在你使用过程中重新创建这个文件夹。原位置中的文件在重新连接前无法使用。" + }, + "legacyMove": { + "title": "把数据移到可见的文件夹?", + "description": "你的研究数据放在一个隐藏文件夹里。把它移到可见的 OpenScience 文件夹便于查找和备份 —— 你的设置和历史记录会留在原处。", + "currentLabel": "当前位置(隐藏)", + "currentAriaLabel": "当前数据位置", + "newLabel": "新位置", + "newAriaLabel": "新数据位置", + "resolving": "正在解析…", + "occupied": "这个文件夹里已经有 {{appName}} 的数据。请选一个空文件夹,或者使用默认位置。", + "unusable": "这个文件夹不可用。换一个吧。", + "moveToDefault": "移到 OpenScience", + "chooseFolder": "选择其他文件夹…", + "checking": "正在检查…", + "keepHere": "保留在当前文件夹", + "note": "之后可以随时在「设置 → 数据位置」中修改。我们不会再询问。" + } + }, + "jobRow": { + "rowAriaLabel": "远程作业:{{intent}}", + "queued": "排队中", + "running": "运行中", + "cardAriaLabel": "已完成的远程作业:{{intent}}", + "finished": "已完成", + "failed": "失败", + "timedOut": "已超时", + "error": "错误" + }, + "updateCapsule": { + "label": "更新", + "ariaLabel": "有可用更新:{{version}}" + }, + "github": { + "openAriaLabel": "在 GitHub 上打开 {{app}}", + "starAriaLabel": "在 GitHub 上为 {{app}} 点 Star,{{count}} 个 Star" + }, + "linkSafety": { + "dialogAriaLabel": "打开外部链接?", + "description": "你即将访问一个外部网站。", + "copyLink": "复制链接", + "copied": "已复制", + "openLink": "打开链接" + }, + "mermaid": { + "errorTitle": "无法渲染 Mermaid 语法", + "commonCauses": "常见原因:xychart 缺少 title 关键字,轴标签没有加引号,或者 y-axisbar/line 的数据行缺失。", + "viewSource": "查看源码", + "retry": "重试" + }, + "download": { + "reconnecting": "连接已断开,正在恢复…(第 {{attempt}} 次尝试)", + "downloaded": "已下载" + }, + "globalSearch": { + "title": "命令面板", + "placeholder": "在此项目中搜索…", + "resultCount_other": "{{count}} 条结果", + "noProject": "新建一个项目,即可搜索会话和产物。", + "recentArtifacts": "最近的产物", + "recentSessions": "最近的会话", + "commands": "命令", + "newSession": "新建会话", + "artifacts": "产物", + "sessions": "会话", + "otherProjects": "其他项目", + "searchingArtifacts": "正在搜索产物…", + "loadArtifactsFailed": "无法加载产物。", + "retryArtifacts": "无法加载产物 —— 重试", + "retryMoreArtifacts": "无法加载更多 —— 重试", + "showMore": "另有 {{count}} 条匹配 —— 显示更多", + "noMatches": "没有会话或产物匹配“{{query}}”。", + "indexIncomplete": "部分产物结果可能未显示。", + "sessionUnavailable": "此会话已不存在。", + "sessionBadge": "会话", + "artifactCount_other": "{{count}} 个产物", + "unknownProject": "未知项目", + "unknownSession": "未知会话", + "createdAtUnavailable": "创建时间不可用", + "mention": "提及 {{name}}", + "mentionUnavailable": "输入框无法再接收产物,暂时无法提及。", + "open": "打开 {{name}}", + "elapsed": { + "now": "刚刚", + "minute_other": "{{count}} 分钟前", + "hour_other": "{{count}} 小时前", + "day_other": "{{count}} 天前", + "week_other": "{{count}} 周前", + "month_other": "{{count}} 个月前", + "year_other": "{{count}} 年前" + }, + "shortcut": { + "navigate": "切换", + "open": "打开", + "mention": "提及", + "close": "关闭" + } + } +} diff --git a/src/renderer/src/locales/zh-Hans/home.json b/src/renderer/src/locales/zh-Hans/home.json new file mode 100644 index 000000000..e6d6f5b5a --- /dev/null +++ b/src/renderer/src/locales/zh-Hans/home.json @@ -0,0 +1,58 @@ +{ + "header": { + "beta": "测试版", + "environmentRepair": "打开环境修复", + "environmentOneFailure": "{{label}} 需要处理", + "environmentManyFailures": "{{count}} 项环境配置需要处理", + "modelSettings": "模型设置", + "newProject": "新建项目", + "environmentShort": "环境" + }, + "projects": { + "heading": "项目", + "sectionLabel": "项目", + "loadError": "无法加载项目:{{error}}", + "empty": "还没有项目。创建一个开始使用。", + "exampleBadge": "示例", + "sessionCount_other": "{{count}} 个会话", + "sessionCountUnavailable": "会话数不可用", + "rowActions": "打开 {{name}} 的操作菜单", + "actionsMenu": "项目操作", + "rename": "重命名…", + "delete": "删除" + }, + "recent": { + "heading": "最近会话", + "sectionLabel": "最近会话", + "empty": "你启动的会话会出现在这里。" + }, + "form": { + "createTitle": "新建项目", + "createDescription": "把相关会话归入一个项目,之后可以重命名。", + "createSubmit": "创建项目", + "editTitle": "编辑项目", + "editDescription": "重命名此项目或修改其描述。", + "editSubmit": "保存更改", + "nameLabel": "名称", + "namePlaceholder": "例如:复现已发表的研究", + "descriptionLabel": "描述", + "descriptionOptional": "(选填)", + "descriptionPlaceholder": "这个项目是关于什么的?", + "saveFailed": "无法保存项目。" + }, + "deleteDialog": { + "title": "删除项目?", + "description": "这将永久删除“{{name}}”。Open Science 存储的生成产物和上传文件也会被删除。项目工作文件夹中的文件不会被删除。此操作无法撤销。", + "descriptionWithSessions_other": "这将永久删除“{{name}}”及其 {{count}} 个会话。Open Science 存储的生成产物和上传文件也会被删除。项目工作文件夹中的文件不会被删除。此操作无法撤销。", + "descriptionWithIncompleteCatalog": "这将永久删除“{{name}}”及其所有已保存的会话,包括恢复期间无法加载的会话。Open Science 存储的生成产物和上传文件也会被删除。项目工作文件夹中的文件不会被删除。此操作无法撤销。" + }, + "relativeTime": { + "now": "刚刚", + "minute": "{{count}} 分钟前", + "hour": "{{count}} 小时前", + "day": "{{count}} 天前", + "week": "{{count}} 周前", + "month": "{{count}} 个月前", + "year": "{{count}} 年前" + } +} diff --git a/src/renderer/src/locales/zh-Hans/onboarding.json b/src/renderer/src/locales/zh-Hans/onboarding.json new file mode 100644 index 000000000..251dcbc04 --- /dev/null +++ b/src/renderer/src/locales/zh-Hans/onboarding.json @@ -0,0 +1,86 @@ +{ + "shell": { + "eyebrow": "首次设置", + "title": "配置你的研究工作区。", + "intro": "先做一次快速的主机检查,确认这台电脑就绪;然后连接你要使用的模型;最后选择数据的存放位置。", + "relaunching": "正在准备你的工作区…", + "progressLabel": "设置进度" + }, + "steps": { + "environment": "环境", + "agent": "智能体运行时", + "provider": "模型提供方", + "notebook": "Notebook 运行时", + "location": "数据位置" + }, + "footer": { + "allChecksPassed": "所有必需的环境检查均已通过。", + "completeRequired": "请完成上面每一项必需内容才能继续。" + }, + "environment": { + "title": "准备环境", + "description": "Open Science 会在你的第一次研究会话前确认其核心依赖。", + "checkAgain": "重新检查", + "checking": "检查中…", + "sectionLabel": "准备环境", + "requirementsLabel": "环境依赖", + "waiting": "等待检查…", + "resolveHint": "请先处理标记为「需要处理」的项目,然后点击「重新检查」。", + "setupFailed": "设置未能完成", + "checkLabels": { + "system": "系统兼容性", + "storage": "应用存储权限", + "secure-storage": "凭据安全存储", + "install-network": "安装所需网络" + }, + "status": { + "passed": "就绪", + "warning": "待确认", + "failed": "需要处理" + } + }, + "provider": { + "title": "连接模型", + "description": "选择 Open Science 在新研究会话中使用的提供方。", + "sectionLabel": "配置模型", + "encryptionUnavailable": "安全密钥存储不可用。在系统钥匙串解锁或授权之前,无法保存 API 密钥。", + "claudeSignInHint": "通过浏览器登录以连接你的 Claude 订阅。我们会在浏览器中打开 Claude 进行授权,然后带你回到这里。", + "cancelSignIn": "取消登录", + "waitingForSignIn": "等待登录…", + "testingConnection": "正在测试连接…", + "signInAndContinue": "登录并继续", + "testAndContinue": "测试并继续", + "claudeProviderChanged": "登录过程中 Claude 提供方发生了变化。请检查所选项后重试。", + "codexProviderChanged": "登录过程中 Codex 提供方发生了变化。请检查所选项后重试。", + "providerChangedDuringTest": "测试过程中提供方发生了变化。请重试。", + "claudeTokenSaveFailed": "无法保存 Claude 令牌。", + "providerSaveFailed": "无法保存提供方。" + }, + "location": { + "title": "Open Science 应该把你的数据存放在哪里?", + "description": "大文件(产物、notebook、环境)存放在这里。你的设置和历史记录始终保留在默认位置。之后可以在「设置」中修改。", + "sectionLabel": "选择数据位置", + "relaunchError": "无法切换到新位置:{{error}} 你可以重试,或继续使用默认位置。", + "relaunchFallbackError": "无法重启以应用新位置。", + "locationLabel": "位置", + "pathLabel": "数据位置路径", + "browse": "浏览…", + "chosenNotice": "你的数据将存放在 {{path}}。Open Science 会重启以完成设置。改用默认位置", + "adoptNotice": "该文件夹已包含 Open Science 数据 —— 将按原样使用(不会移动任何内容)。", + "finish": "完成", + "confirmTitle": "重启以设置数据位置?", + "confirmDescription": "Open Science 将重启,并在 {{path}} 完成数据设置。", + "keepDefault": "保留默认", + "restart": "重启" + }, + "agent": { + "title": "设置智能体运行时", + "description": "选择 Open Science 驱动的智能体,然后安装它。只需安装这一个智能体即可继续。" + }, + "notebook": { + "title": "Notebook 运行时(可选)", + "description": "Notebook 默认在应用托管的 Python 环境中运行。之后可以在「设置 → 运行时」中修改这些配置。", + "provisioning": "正在设置 Notebook 运行时 —— 请等待它完成,或取消它,然后继续。", + "optional": "可选 —— 这里的内容都不是完成设置的必需项。" + } +} diff --git a/src/renderer/src/locales/zh-Hans/settings.json b/src/renderer/src/locales/zh-Hans/settings.json new file mode 100644 index 000000000..83b16dcfd --- /dev/null +++ b/src/renderer/src/locales/zh-Hans/settings.json @@ -0,0 +1,1071 @@ +{ + "shell": { + "dialogTitle": "设置", + "dialogDescription": "管理你的 Agent 运行时和模型服务商。", + "navLabel": "设置", + "groups": { + "capabilities": "能力", + "workspace": "工作区", + "remoteAccess": "远程访问" + }, + "panels": { + "skills": "技能", + "connectors": "连接器", + "specialists": "专家", + "compute": "计算", + "network": "网络", + "model": "模型", + "agent": "Agent", + "runtimes": "运行时", + "storage": "存储", + "general": "通用", + "permissions": "权限", + "remote-control": "远程控制" + }, + "back": "上一步", + "forward": "下一步", + "backToRoot": "返回{{panel}}", + "maximize": "最大化", + "restore": "还原", + "close": "关闭设置", + "panelsLower": { + "skills": "技能", + "model": "模型", + "network": "网络", + "connectors": "连接器", + "compute": "计算" + }, + "save": "保存", + "saving": "保存中…", + "closeNav": "关闭设置导航", + "openNav": "打开设置导航", + "navigation": "导航", + "dismissError": "忽略设置错误", + "dismiss": "忽略" + }, + "model": { + "activeModel": "当前模型", + "activeModelDescription": "驱动新建 Agent 会话的模型。", + "selectModel": "选择模型", + "reasoningEffort": "推理强度", + "reasoningEffortDescription": "强度越高思考越久,越低响应越快。该选择随所选模型变化,并在换模型时保持相对强度;部分 Agent 框架可能会用近似档位替代不支持的强度。对后续请求生效。", + "providers": "模型服务商", + "encryptionUnavailable": "安全密钥存储不可用。在系统钥匙串解锁或授权之前,无法保存 API 密钥。", + "errors": { + "testFailed": "无法测试模型服务商连接。", + "codexSignInFailed": "无法登录 Codex。", + "codexSignOutFailed": "无法退出 Codex。", + "claudeTokenSaveFailed": "无法保存 Claude token。", + "claudeSignInFailed": "无法登录 Claude。", + "claudeSignOutFailed": "无法退出 Claude。", + "claudeDisconnectFailed": "无法断开 Claude 与 Open Science 的连接。", + "providerSaveFailed": "无法保存模型服务商。", + "codexSignOutIncomplete": "Codex 退出未完成。重试一次。", + "claudeTokenSaveRetry": "无法保存 Claude token。重试一次。", + "claudeSignInRetry": "无法登录 Claude。重试一次。", + "claudeSignOutIncomplete": "Claude 退出未完成。重试一次。", + "codexReimportFailed": "无法重新导入 Codex 登录信息。" + }, + "list": { + "incompatible": "与当前 Agent 框架不兼容。", + "empty": "还没有模型服务商。添加一个来选择模型来源。", + "testFailedWithMessage": "测试失败:{{message}}", + "testFailed": "连接测试失败。", + "claudeSubscription": "Claude 订阅", + "testing": "测试中…", + "connectionVerified": "连接已验证", + "authImported": "认证信息已导入 Open Science", + "codexStoredSeparately": "Codex 登录信息由 Open Science 单独存储", + "notSignedIn": "未登录", + "keyNeedsReentry": "密钥需重新输入", + "checkCodexLogin": "检查 Codex 登录", + "testConnection": "测试连接", + "browserLoginHint": "正在打开浏览器以登录…未打开?取消并使用 Setup Token。", + "tokenPrefix": "Token:{{masked}}", + "modelPrefix": "模型:{{name}}", + "keyPrefix": "密钥:{{masked}}", + "modelsCount": "{{count}} 个模型", + "expiresLabel": "到期:" + }, + "testFailure": { + "auth": "测试失败:认证被拒绝 —— 检查 API 密钥。", + "network": "测试失败:无法访问该端点 —— 检查基础 URL 和网络连接。", + "badUrl": "测试失败:基础 URL 看起来无效。", + "modelNotFound": "测试失败:未找到所配置的模型。", + "timeout": "测试失败:连接超时。", + "serverError": "测试失败:网关或上游服务暂时不可用。", + "withMessage": "测试失败:{{message}}", + "withMessageAndStatus": "测试失败:{{message}}(HTTP {{status}})" + }, + "type": { + "custom": "自定义", + "official": "官方" + }, + "compatibility": { + "activeModelIncompatible": "当前模型与 {{framework}} 不兼容", + "codexBridgeUnsupported": "该模型不支持通过 Codex 桥接使用", + "codexBridgeDescription": "该模型不支持通过 Codex 的 Chat Completions 桥接使用。为 Codex 会话另选一个模型。", + "switchGuidance": "在下方选一个兼容的模型,或在上方切换 Agent 框架 —— 否则该框架上的对话无法开始。" + }, + "effort": { + "default": "默认" + }, + "actions": { + "reimportCodexLogin": "重新导入 Codex 登录", + "signIn": "登录", + "signOut": "退出登录", + "signInWithBrowser": "通过浏览器登录", + "useSetupToken": "使用设置令牌", + "cancelSignIn": "取消登录", + "disconnect": "从 Open Science 断开连接", + "edit": "编辑", + "delete": "删除" + }, + "endpoint": { + "speaksThe": "使用 {{protocol}} 协议" + }, + "refresh": { + "loaded_other": "已从供应商加载 {{count}} 个模型。", + "failed": "无法获取模型:{{reason}}。将使用内置列表。", + "requestFailed": "请求失败" + } + }, + "providerForm": { + "apiKey": "API 密钥", + "getApiKey": "获取 API 密钥", + "keyPlaceholderStored": "{{masked}} — 留空则保持不变", + "storedKeyFallback": "已保存的密钥", + "keyNeedsReentry": "已保存的密钥无法解密。重新输入后才能继续。", + "providerType": "服务商类型", + "name": "名称", + "providerName": "服务商名称", + "namePlaceholder": "例如:我的网关", + "baseUrl": "基础 URL", + "baseUrlHelp": "网关根地址;末尾的 /v1 会自动补上。在下方选择与该端点匹配的 API 格式。", + "apiFormat": "API 格式", + "apiFormatHelp": "该网关使用的对话 API。Claude Code 用 /v1/messages,OpenCode 接受 Messages 或 Chat Completions,Codex 用 /v1/responses。模型服务商只能在支持其格式的 Agent 框架下选用。", + "supportedModelsHelp": "随应用内置。从厂商刷新可拉取最新列表。添加后在「当前模型」选择器中选用。", + "model": "模型", + "modelOptionalOverride": "(可选覆盖)", + "modelPlaceholderClaudeDefault": "留空则使用 Claude 的默认模型", + "contextWindow": "上下文窗口", + "endpoint": "端点", + "supportedModels": "支持的模型", + "refreshing": "刷新中…", + "refreshFromVendor": "从厂商刷新", + "imageInput": "图像输入", + "imageInputDescription": "仅在该网关和模型接受图像内容时启用。", + "supportsImageInput": "支持图像输入", + "reasoningEffort": "推理强度", + "reasoningEffortDescription": "选择该模型实际接受的强度档位。Open Science 会把五个相对强度映射到这些档位,再按下方的请求格式发送所选档位。模型不接受强度参数时请关闭。", + "supportsReasoningEffort": "支持推理强度", + "reasoningEffortLevels": "推理强度档位", + "requestFormat": "请求格式", + "reasoningEffortRequestFormat": "推理强度请求格式", + "codexAuth": "Codex 认证方式", + "codexShared": "导入现有的 Codex 登录", + "codexIsolated": "用 Open Science 登录", + "codexSharedHint": "将 Codex 认证信息,以及兼容时当前服务商的非机密回环路由,复制到 Open Science 应用数据中。其他全局配置、技能和会话不会导入。", + "codexIsolatedHint": "在 Open Science 应用数据中单独存储一份 Codex 登录,不改动你的 Codex CLI 配置。", + "claudeAuth": "Claude 认证方式", + "claudeShared": "使用现有的 Claude 配置(推荐)", + "claudeIsolated": "单独登录(隔离)", + "claudeSharedHint": "推荐。使用 ~/.claude 中你现有的 Claude 登录。通过浏览器 OAuth 登录一次,即可在所有 Claude 工具中使用。", + "claudeIsolatedHint": "进阶。通过浏览器登录,并在 Open Science 中单独存储一份 Claude 登录,与你个人的 Claude 配置完全隔离。", + "claudeSharedDetail": "通过浏览器 OAuth 登录。设置卡片会打开浏览器,让你用 Claude 账号登录。凭据存储在 ~/.claude 中。", + "claudeIsolatedDetail": "在终端运行 claude setup-token,把 token 粘贴到下方。它会加密存储在应用自有的 Claude 配置目录中;不会读取或写入 ~/.claude。", + "claudeIsolatedPasteHint": "保存后在设置卡片中粘贴 token —— 向导的「测试并继续」流程会为你完成登录。", + "security": { + "availableTitle": "你的密钥不会外泄。", + "availableDescription": "它只保存在这台设备上,绝不会上传到 Open Science。由操作系统的安全存储保护,仅在你发起请求时发送给所选的模型服务商。", + "unavailableTitle": "安全存储不可用。", + "unavailableDescription": "在操作系统凭据保管库可用之前,Open Science 不会保存 API 密钥。解锁或授权系统钥匙串后重试。" + }, + "groups": { + "codex": "Codex 订阅", + "claude": "Claude 订阅", + "api": "官方 API", + "other": "其他" + }, + "kinds": { + "customLabel": "自定义网关", + "codexDescription": "使用现有的 Codex 配置,或用单独的 Open Science 配置登录。", + "claudeDescription": "使用现有的 Claude 配置,或用单独的 Open Science 配置登录。", + "officialDescription": "API 密钥 —— 已内置模型列表", + "customDescription": "为 Messages 或 Chat Completions 端点填写基础 URL、密钥和模型" + }, + "validation": { + "baseUrlRequired": "必须填写基础 URL。", + "modelRequired": "必须填写模型。", + "keyRequired": "必须填写 API 密钥。", + "contextWindowInvalid": "上下文窗口必须是正整数 token 数。" + } + }, + "claudeSignIn": { + "title": "使用 Anthropic 账号登录", + "description": "使用 claude setup-token 生成的长期 OAuth token。该 token 会加密存储在 Open Science 应用存储中,不会读取或写入 ~/.claude。完整流程见 Anthropic 的 setup-token 指南。", + "browserPending": "正在打开浏览器登录…在浏览器中完成后,此窗口会自动关闭。没有打开,或更想用 token?在下方粘贴一个。", + "step1": "第 1 步 · 运行", + "copyCommand": "复制命令", + "copied": "已复制", + "copy": "复制", + "pasteLabel": "粘贴 setup-token 输出的 token", + "pasteLabelStep2": "第 2 步 · 粘贴 setup-token 输出的 token", + "tokenInputLabel": "Claude 设置令牌", + "signingIn": "登录中…", + "signIn": "登录", + "tokenSaveFailed": "无法保存 Claude token。" + }, + "validation": { + "ok": "连接成功。", + "network": "无法访问该端点。检查网络连接和基础 URL。", + "auth": "认证失败。检查 API 密钥。", + "modelNotFound": "该模型被拒绝。检查此网关下的模型名称。", + "badUrl": "基础 URL 无效。填写完整 URL,例如 https://gateway.example/v1。", + "timeout": "请求超时并已中止。", + "incompatible": "该模型服务商与当前的 Agent 框架不兼容。", + "serverError": "网关或上游服务暂时不可用。稍后重试。", + "unknown": "验证失败,原因未知。", + "withDetail": "{{base}}({{detail}})", + "withStatus": "{{base}}(HTTP {{status}})" + }, + "breadcrumb": { + "newSkill": "新建技能", + "uploadSkills": "上传技能", + "importFromGitHub": "从 GitHub 导入", + "importInstalledSkills": "导入已安装的技能", + "addProvider": "添加模型服务商", + "packageMirror": "软件包镜像源", + "addConnector": "添加连接器", + "addSshHost": "添加 SSH 主机", + "newSpecialist": "新建专家", + "editSpecialist": "编辑专家", + "editNamed": "编辑 {{name}}", + "connectorFallbackName": "连接器" + }, + "about": { + "title": "关于", + "ariaLabel": "应用版本", + "checking": "检查中…", + "checkNow": "立即检查", + "checkingForUpdates": "正在检查更新…", + "available": "新版本 {{version}} 可用", + "downloadingStatus": "正在下载… {{percent}}%", + "downloaded": "更新已下载完成,打开安装程序即可完成升级", + "upToDate": "已是最新版本", + "checkFailed": "更新检查失败", + "downloadingButton": "正在下载 {{percent}}%", + "updateReady": "更新已就绪", + "updateTo": "更新到 {{version}}" + }, + "appIcon": { + "title": "应用图标", + "description": "选择应用窗口中显示的内置图标,在 macOS 上也会用于 Dock。", + "note": "新图标会立即出现在应用窗口和 macOS 的 Dock 中。Finder、资源管理器、任务栏或开始菜单中的图标属于已安装的应用,不会改变。" + }, + "general": { + "appearance": { + "title": "外观", + "description": "设置应用的外观与语言。选择「系统」会跟随设备,其余选项保持固定。你的选择会记录在这台设备上。", + "theme": { + "label": "主题", + "description": "跟随系统设置,或固定为浅色、深色。" + }, + "language": { + "label": "语言", + "description": "跟随系统设置,或指定一种语言。「系统」只在启动时检测一次,修改设备语言后需重新打开应用才会生效。" + } + }, + "windowBehavior": { + "title": "窗口行为", + "description": "设置标题栏关闭按钮的行为。", + "closeAction": { + "label": "关闭窗口时", + "description": "每次询问、让 {{appName}} 保留在托盘中运行,或直接退出应用。", + "ask": "每次询问" + } + }, + "notifications": { + "title": "通知", + "description": "当你不在应用中时,任务完成、失败或等待批准时会收到桌面通知。", + "tasks": { + "label": "任务通知", + "description": "点击通知会把 {{appName}} 带到前台并打开对应任务。", + "toggleLabel": "开关任务通知" + }, + "note": "只有在你使用其他应用时才会出现通知。你主动取消的任务,以及应用会自动重试的失败,都不会发出通知。首次出现通知时,操作系统可能会询问通知权限。" + }, + "diagnostics": { + "title": "诊断", + "description": "查看这台设备的运行日志 —— 它记录了应用正在做什么,便于排查问题。", + "logFile": { + "label": "日志文件", + "pathLabel": "日志文件路径", + "openFailed": "无法打开日志文件。", + "revealFailed": "无法在文件夹中显示日志文件。" + }, + "issueHint": "遇到问题?在 GitHub 上提交 issue 并附上上面的日志。日志保存在这台设备上,不会自动发送;其中可能包含本地文件路径,分享前请先查看内容。" + }, + "cli": { + "title": "命令行工具", + "description": "安装 open-science 命令,即可在终端里启动、停止和检查后端服务,然后完全在浏览器中使用。", + "rowLabel": "open-science", + "install": "安装命令", + "uninstall": "卸载命令", + "pathLabel": "命令行工具路径", + "updateFailed": "无法更新命令行工具。", + "usage": "安装完成后,运行 open-science start 启动后端并打开带鉴权的地址,再用 open-science stop 关闭。此外还提供 statusurl 两个命令。" + }, + "community": { + "title": "{{appName}} 用得还顺手吗?", + "description": "它免费且开源。在 GitHub 上点亮 Star 能让更多人发现它,也欢迎来 Discord 和 X 和我们一起开放地构建。谢谢你在这里。", + "ariaLabel": "社区", + "discordLabel": "加入 {{appName}} 的 Discord 社区", + "xLabel": "在 X 上关注 {{appName}}", + "websiteLabel": "打开 {{appName}} 官网", + "website": "官网" + } + }, + "runtimes": { + "title": "Notebook 运行时", + "description": "启用每种 Notebook 语言可运行的环境。应用自带的环境默认开启;启用你自己的解释器可让 Agent 使用它们。", + "provenance": { + "appManaged": "应用托管", + "agentCreated": "Agent 创建", + "conda": "Conda:{{name}}", + "system": "系统" + }, + "ready": "就绪", + "notRunnable": "无法运行", + "downloadingManaged": "正在下载托管运行时…", + "installedAndReady": "已安装并就绪", + "managedNotSetUp": "托管运行时尚未设置", + "detecting": "正在检测运行时…", + "allowPackageInstall": "允许安装软件包", + "allowPackageInstallDescription": "允许 Open Science 向该环境安装软件包。安装会写入你自己的环境,而不是应用托管的存储空间。", + "pickRscript": "选择你的 Rscript 可执行文件 —— 例如 Rscript.exe(Windows)或 bin/Rscript(macOS/Linux)。请选择文件,而不是文件夹。", + "pickPython": "选择你的 Python 解释器可执行文件 —— 例如 python.exe(Windows)或 bin/python(macOS/Linux)。请选择文件,而不是文件夹。", + "resetRuntime": "重置运行时", + "retrySetup": "重试设置", + "downloadAndSetUp": "下载并设置", + "recheck": "重新检测", + "cancel": "取消", + "forceStop": "停止正在运行的工作", + "drainDisable": "当前工作完成后禁用", + "disableTitle": "要禁用 {{label}} 吗?", + "disableImpact": "它正被 {{total}} 个活跃会话使用 —— {{running}} 个运行中,{{idle}} 个空闲。禁用会让正在运行的单元格先执行完,然后关闭它的内核;这些会话需要切换到其他运行时才能继续工作。", + "packages": { + "button": "软件包", + "dialogTitle": "{{label}} 中的软件包", + "dialogDescription": "该环境中已安装的软件包。", + "filterPlaceholder": "筛选软件包…", + "filterAriaLabel": "筛选软件包", + "count": "已显示 {{visible}} / {{total}}", + "condaSplit": "{{conda}} 个 conda,{{pypi}} 个 pypi", + "loading": "正在列出软件包…", + "retry": "重试", + "columnName": "名称", + "columnVersion": "版本", + "columnBuild": "构建号", + "columnChannel": "通道", + "noMatch": "没有匹配“{{filter}}”的软件包。", + "noneInstalled": "未安装任何软件包。", + "close": "关闭", + "listFailed": "无法列出软件包。" + }, + "errors": { + "recheck": "无法重新检测运行时。", + "change": "无法更改该运行时。", + "auth": "无法更改软件包安装授权。", + "add": "无法添加该解释器。", + "refresh": "无法刷新运行时就绪状态。", + "reset": "无法重置该运行时。", + "cancel": "无法取消该设置流程。" + } + }, + "agent": { + "frameworkTitle": "Agent 框架", + "frameworkDescription": "选择驱动会话的编码 Agent 后端。选中一张卡片即可切换;切换会开启一个全新的 Agent 会话,已打开的对话会把其记录重放到新后端。当前使用中的运行时无法卸载 —— 请先切换到另一个。", + "detecting": "检测中…", + "redetect": "重新检测", + "detectionFailed": "有一个或多个 Agent 运行时无法检测到。", + "installerFailed": "安装程序无法启动。", + "repairIssuesLabel": "Agent 运行时修复问题", + "installBlockersLabel": "Agent 安装阻塞项", + "selectedAgentFallback": "所选 Agent", + "cannotBeAccessed": "无法访问 {{agent}}。", + "repairBeforeUse": "使用前请先修复所选的 Agent。", + "groups": { + "installed": "已安装", + "available": "可安装" + }, + "blockers": { + "systemRequirements": "不满足系统要求", + "installNetwork": "安装所需网络不可用" + }, + "frameworks": { + "claude": { + "description": "Anthropic 面向终端的 Agent 编码工具。", + "notReadyHint": "在下方安装 Claude Agent,或手动安装后重新检测。" + }, + "opencode": { + "description": "面向终端的开源编码 Agent。", + "notReadyHint": "该框架需要 OpenCode。在下方安装,或手动安装(参见 opencode.ai/docs)后重新检测。" + }, + "codex": { + "description": "OpenAI 的编码 Agent,通过 Codex ACP 适配器接入。", + "notReadyHintRepair": "适配器或与其配对的原生 Codex 运行时未通过检测。在下方重新安装托管的配对组合,或修复你的手动安装后重新检测。", + "notReadyHintInstall": "该框架需要 Codex ACP。在下方安装,或手动安装后重新检测。" + } + }, + "installSources": { + "managed": { + "label": "应用托管下载(推荐)", + "claudeDescription": "下载自带运行环境的 Claude —— 无需 Node.js 或 npm。", + "codexDescription": "下载自带运行环境的 Codex ACP 运行时 —— 无需 Node.js 或 npm。", + "opencodeDescription": "下载自带运行环境的 OpenCode —— 无需 Node.js 或 npm。" + }, + "npm": { + "label": "npm(全局安装)" + }, + "officialScript": { + "ps1": "官方 install.ps1", + "sh": "官方 install.sh", + "generic": "官方安装脚本" + }, + "menuLabel": "安装来源", + "npmNotFound": "未找到 npm" + }, + "card": { + "install": "安装", + "repair": "修复", + "installing": "安装中…", + "starting": "启动中…", + "active": "使用中", + "installed": "已安装", + "hideLog": "隐藏日志", + "showLog": "显示日志", + "installProgress": "安装进度", + "installLog": "安装日志" + }, + "progress": { + "resolving": "解析中…", + "downloading": "下载中…", + "downloadingSized": "下载中 —— {{received}} / {{total}} MB", + "extracting": "解压中…", + "installing": "安装中…" + }, + "uninstall": { + "notManaged": "在你的系统中找到了 {{label}},但它不由本应用管理,因此无法从这里卸载。请用你当初安装它的工具移除 —— 例如 `{{command}}`、你的包管理器,或从 PATH 中删除它 —— 然后重新检测。", + "active": "{{label}} 是当前使用中的 Agent 框架,无法卸载。请先切换到其他框架,再执行卸载。", + "promptInFlight": "有任务正在运行 —— 请等它结束后再卸载。", + "uninstall": "卸载", + "uninstalling": "卸载中…", + "title": "卸载 {{name}}?", + "description": "这会移除本应用下载并管理的 {{name}} 运行时。你自己另行安装的 {{name}} 不受影响。你随时可以在这里重新安装。", + "cancel": "取消" + }, + "repairDialog": { + "title": "{{name}} 需要修复", + "description": "请先修复该 Agent,然后再选择它。" + }, + "switchDialog": { + "title": "切换到 {{name}}?", + "description": "对话无法在另一个后端上恢复,因此切换会开启一个新的 Agent 会话。已打开的对话会保留现有消息,并把记录重放给 {{name}},让它接着之前的进度继续(工具状态不会带过去)。新对话不受影响。", + "switch": "切换" + } + }, + "skills": { + "detail": { + "updatedToday": "今天更新", + "updatedDaysAgo_other": "{{count}} 天前更新", + "sourceImported": "已导入", + "sourcePersonal": "个人", + "sourceFeatured": "精选", + "toggleAriaLabel": "开关 {{name}}", + "files": "文件", + "details": "详情", + "author": "作者", + "license": "许可证", + "thirdParty": "第三方软件、内容、条款与信息" + }, + "editor": { + "slugRequired": "技能 ID 不能为空。", + "slugCharset": "只能使用小写字母、数字和连字符。", + "slugReserved": "不能以 {{prefixes}} 开头。", + "slugTaken": "已存在使用该 ID 的技能。", + "identity": "标识", + "identityDescription": "这个技能在目录中以及对 Agent 呈现的样子。", + "name": "名称", + "nameAriaLabel": "技能名称", + "namePlaceholder": "例如:变更日志风格", + "id": "技能 ID", + "idAriaLabel": "技能 ID", + "idHintNew": "用作文件夹名 —— 小写 a–z、0–9、连字符。创建后不可更改。", + "idHintLocked": "技能 ID 在创建后即固定。", + "description": "描述", + "descriptionAriaLabel": "技能描述", + "descriptionPlaceholder": "一句话 —— 这个技能教给 Agent 什么,什么时候适用?", + "descriptionHint": "Agent 依据这句话判断何时使用该技能 —— 写具体些。", + "content": "内容", + "contentDescription": "技能被调用时展示给 Agent 的 Markdown。", + "contentModeAriaLabel": "内容模式", + "modeWrite": "撰写", + "modeUpload": "上传", + "bodyAriaLabel": "技能正文", + "bodyPlaceholder": "# 说明\n\n给 Agent 的分步指引…", + "bodyHint": "粘贴完整的 SKILL.md —— 如果开头有 --- 元数据块,上面的字段会自动填充。", + "metadataAriaLabel": "技能元数据", + "savedMetadata": "已保存的元数据", + "clearMetadataAriaLabel": "清除技能元数据", + "clear": "清除", + "dropToUpload": "拖到此处上传", + "uploadContent": "上传 SKILL.md 或文本文件", + "uploadContentHint": "其内容会填入编辑器;切回「撰写」即可调整。", + "references": "参考文件", + "referencesDescription": "技能在运行时可读取的辅助文件(脚本、模板、数据)。", + "dropReferences": "拖入参考文件", + "addReferencesAriaLabel": "添加参考文件", + "referencesBrowse": "拖入参考文件,或点击浏览", + "referencesHint": "保存在技能的 references/ 下。", + "removeReferenceAriaLabel": "移除 {{path}}", + "saving": "保存中…", + "save": "保存", + "publish": "发布", + "loading": "加载中…" + }, + "panel": { + "filterAll": "全部", + "filterFeatured": "精选", + "filterImported": "已导入", + "filterPersonal": "个人", + "groupFeatured": "精选", + "groupFeaturedSubtitle": "随应用附带的科研技能。", + "groupImported": "已导入", + "groupImportedSubtitle": "你从 GitHub 添加的技能。", + "groupPersonal": "个人", + "groupPersonalSubtitle": "你自定义的技能。", + "conversationImports": "对话导入", + "conversationImportsDescription": "选择对话可以向 Open Science 导入什么。", + "skillPackages": "技能包", + "skillPackagesDescription": "让 Agent 识别附加的 .zip 与 .skill 包,并在导入前询问你。", + "filterAriaLabel": "按来源筛选技能", + "conversationImportToggleAriaLabel": "开关对话中的技能导入", + "searchAriaLabel": "搜索技能", + "searchPlaceholder": "搜索技能…", + "addSkill": "添加技能", + "writeFromScratch": "从零开始写", + "writeFromScratchHint": "打开技能创建器", + "uploadSkills": "上传技能", + "uploadSkillsHint": "挑选 SKILL.md 文件", + "importFromGitHub": "从 GitHub 导入", + "importFromGitHubHint": "从仓库添加技能", + "importInstalledSkills": "导入已安装的技能", + "importInstalledSkillsHint": "扫描全局技能文件夹", + "editAriaLabel": "编辑 {{name}}", + "deleteAriaLabel": "删除 {{name}}", + "toggleAriaLabel": "开关 {{name}}", + "emptyPersonal": "创建一个技能,把你常用的流程教给 Claude。", + "emptyImported": "还没有导入任何技能。", + "emptySearch": "没有技能符合你的搜索。", + "desktopOnlyFallback": "已安装技能的导入功能仅在桌面应用中可用。" + }, + "githubImport": { + "title": "从 GitHub 导入", + "description": "预览一个仓库或技能文件夹(owner/repo、owner/repo@ref,或 github.com 链接),然后挑选要导入的技能。", + "inputAriaLabel": "GitHub 技能链接或仓库", + "inputPlaceholder": "owner/repo、owner/repo@ref,或 github.com 链接", + "working": "处理中…", + "preview": "预览", + "found_other": "找到 {{count}} 个技能", + "selectAllAriaLabel": "全选", + "selectAll": "全选", + "invert": "反选", + "importSelected": "导入所选({{count}})", + "selectSkillAriaLabel": "选择 {{name}}", + "previewSkillAriaLabel": "预览 {{name}}", + "imported": "已导入", + "importedHeading": "已导入的技能", + "importedEmpty": "还没有导入任何技能。你导入过的仓库会显示在这里。", + "noSkillsFound": "那个仓库里没有找到技能。", + "scanFailed": "扫描失败。", + "importedCount_other": "已导入 {{count}} 个技能。", + "importedThenFailed": "已导入 {{count}} 个,随后失败。" + }, + "importApproval": { + "title": "导入技能包?", + "description": "Agent 请求从 {{attachment}} 导入。请检查并明确选择允许 Open Science 安装的内容。", + "found_other": "找到 {{count}} 个技能", + "selectAllAriaLabel": "全选", + "selectAll": "全选", + "invert": "反选", + "selectCandidateAriaLabel": "选择 {{name}}", + "alreadyImported": "已导入", + "updatesExisting": "将更新现有技能", + "preview": "预览", + "notImportable": "无法导入", + "importSelected": "导入所选", + "importCount_other": "导入 {{count}} 个技能" + }, + "upload": { + "title": "上传技能", + "description": "从本机的 SKILL.md 文件或 .zip / .skill 压缩包添加技能。可一次选择多个文件,单个压缩包也可以包含多个技能。", + "dropToUpload": "拖放到此处上传", + "inputAriaLabel": "上传技能文件", + "reading": "正在读取…", + "dropHint": "拖放文件或点击上传", + "formatHint": ".md 文件需要在 YAML frontmatter 中写明 name 和 description。.zip 或 .skill 压缩包必须包含 SKILL.md。添加前你会先确认一次。", + "writeInstead": "改为从零编写", + "confirmTitle": "确认导入", + "confirmDescription": "选择要添加的技能。在你导入之前不会写入任何内容。", + "found_other": "找到 {{count}} 个技能", + "selectAllAriaLabel": "全选", + "selectAll": "全选", + "invert": "反选", + "selectCandidateAriaLabel": "选择 {{name}}", + "previewCandidateAriaLabel": "预览 {{name}}", + "alreadyImported": "已导入", + "nameExists": "名称已存在", + "importing": "正在导入…", + "importSelected": "导入所选({{selected}})", + "chooseDifferent": "重新选择文件", + "skipped_other": "已跳过 {{count}} 个技能", + "summary": "已导入 {{imported}} 个 · 跳过 {{skipped}} 个 · 失败 {{failed}} 个", + "errorTooLarge": "{{fileName}}:文件过大(上限 {{limit}})。", + "errorNoSkills": "{{fileName}}:压缩包中未找到技能。", + "errorFile": "{{fileName}}:{{message}}", + "errorNoName": "{{fileName}}:需要在 YAML frontmatter 中写明 name。", + "errorUnsupported": "{{fileName}}:不支持的文件,请上传 .md 文件或 .zip / .skill 压缩包。", + "errorPreviewTooLarge": "{{fileName}}:预览内容超过 {{limit}} 上限,但仍可导入。", + "errorSelectionTooLarge": "所选文件过大({{total}}),每次最多上传 {{limit}}。" + }, + "candidatePreview": { + "fallbackTitle": "技能预览", + "loadingSource": "正在加载候选内容…", + "closeAriaLabel": "关闭预览", + "loading": "正在加载预览…", + "metadata": "元数据", + "files": "文件" + }, + "agentHomeImport": { + "title": "导入已安装的技能", + "scanOne": "扫描这台计算机上的 {{path}}。勾选要复制到 Open Science 的技能;原文件保持不动。", + "scanTwo": "扫描这台计算机上的 {{path}}{{frameworkPath}}。勾选要复制到 Open Science 的技能;原文件保持不动。", + "scanning": "扫描中…", + "rescan": "重新扫描", + "skillsFound_other": "找到 {{count}} 个技能", + "selectAllAriaLabel": "全选已安装的技能", + "selectAll": "全选", + "invert": "反选", + "importing": "导入中…", + "importSelected": "导入所选({{count}})", + "selectSkillAriaLabel": "选择 {{name}}", + "previewSkillAriaLabel": "预览 {{name}}", + "sourceShared": "共享", + "imported": "已导入", + "empty": "在扫描的全局文件夹中没有找到已安装的技能。", + "scanFailed": "扫描失败。", + "importedCount_other": "已导入 {{count}} 个技能。", + "importedWithFailures": "已导入 {{count}} 个;{{failureCount}} 个失败。{{error}}", + "importFailed": "无法导入所选技能。" + } + }, + "toolPermission": { + "allow": "始终允许", + "ask": "需要批准", + "block": "阻止" + }, + "network": { + "mirror": { + "title": "软件包镜像", + "description": "Notebook 环境在安装或更新时,从哪里获取 conda 和 Python 软件包。", + "statusUnconfigured": "未配置 —— 软件包来自公共源(conda.anaconda.org、pypi.org)", + "statusConfigured": "正在从 {{hosts}} 获取软件包", + "configure": "配置", + "edit": "编辑", + "condaChannel": "Conda 频道镜像", + "pypiIndex": "Python 软件包索引(pip)", + "caBundle": "CA 证书包路径", + "optional": "(可选)", + "caBundleHint": "用于企业 TLS 代理的 PEM 证书包;conda、pip 和 R 下载都会信任它。", + "viewMirrors": "查看可用镜像", + "cancel": "取消", + "save": "保存", + "saving": "保存中…", + "saveFailed": "无法保存软件包镜像。" + } + }, + "storage": { + "appStorage": { + "title": "应用存储", + "description": "Open Science 需要对其私有配置目录的写入权限。", + "revealFinder": "在 Finder 中显示", + "revealFolder": "在文件夹中显示", + "revealFailed": "无法显示应用存储。", + "checking": "检查中…", + "checkAgain": "重新检查", + "continueToAgent": "继续修复 Agent" + }, + "location": { + "title": "数据位置", + "description": "Open Science 在这台设备上存放项目、产物和其他应用数据的位置。", + "changeLocation": "更改位置", + "loading": "加载中…", + "label": "位置", + "pathAriaLabel": "数据根目录路径", + "onDisk": "占用磁盘 {{size}}", + "onDiskDefault": "占用磁盘 {{size}} · 默认位置", + "newLocation": "新位置", + "pathPlaceholder": "/path/to/new/location", + "browse": "浏览…", + "moveBackToDefault": "或移回默认位置 ({{path}})", + "defaultUnusable": "默认位置不可用。", + "willStoreIn": "数据将存放在 {{path}}", + "adoptNotice": "该文件夹已包含 Open Science 数据,将按原样使用(不合并)——你当前的数据文件夹会保留,可以随时切换回去。应用将重启。", + "moveNotice": "你现有的数据(约 {{size}})将被移动到新位置——文件会一起迁移,当前文件夹中不会留下任何内容。", + "runtimeRebuildNotice": "Python/R 环境会在首次使用时于新位置重建(不迁移)。", + "switching": "切换中…", + "useThisFolder": "使用该文件夹", + "switchFailed": "无法切换到该文件夹。" + }, + "usage": { + "title": "磁盘占用", + "empty": "暂无数据。", + "total": "总计", + "available": "磁盘可用空间", + "legendTitle": "{{label}}:{{size}}", + "categories": { + "artifacts": "产物", + "uploads": "上传", + "runtime": "运行时", + "notebooks": "Notebook", + "workspaces": "会话工作区" + } + }, + "changeWarning": { + "title": "更改数据位置?", + "description": "你可以把 Open Science 的数据移动到这台设备上的其他文件夹。" + }, + "migration": { + "phases": { + "scan": "正在扫描文件…", + "copy": "正在复制文件…", + "verify": "正在校验…", + "delete": "正在清理…" + }, + "detectingTitle": "正在检查运行中的会话…", + "detectingDescription": "请稍候。", + "confirmTitle": "移动应用数据?", + "confirmDescription": "开始移动会中断下列运行中的会话,并重启应用。", + "interruptAndMove": "中断并移动", + "movingTitle": "正在移动应用数据…", + "preparing": "准备中…", + "elapsedAriaLabel": "已用时间", + "elapsed": "已用 {{time}}", + "doNotQuit": "完成之前请不要退出 Open Science 或关闭计算机。", + "doneTitle": "数据已复制", + "doneDescription": "重启后即切换到新位置。在此之前不会有任何变化——选择“保留当前位置”可留在原处并丢弃副本。", + "discarding": "正在丢弃…", + "keepCurrentLocation": "保留当前位置", + "restartNow": "立即重启", + "committingTitle": "正在切换…", + "committingDescription": "正在收尾并重启。可能需要一点时间,请不要退出。", + "switchoverTitle": "数据已移动——请重启", + "failedTitle": "移动失败", + "genericError": "出了点问题。请关闭后重试。" + }, + "adoptDialog": { + "title": "使用该文件夹?", + "description": "Open Science 将重启并按原样使用该文件夹——其内容不会与你当前的数据合并,缺少的内容会显示为不可用。你当前的数据文件夹保持不变,可以随时切换回去。" + } + }, + "connectors": { + "approval": { + "title": "允许外部请求?", + "description": "Claude 想要调用一个会把数据发送到外部服务的连接器工具。仅在你信任该连接器处理当前请求时才批准。", + "connector": "连接器", + "tool": "工具", + "args": "参数", + "deny": "拒绝", + "allowOnce": "允许一次", + "thisConversation": "此会话", + "thisProject": "此项目", + "global": "全局" + }, + "detail": { + "featured": "精选", + "toggleAriaLabel": "启用或停用 {{name}}", + "skipApprovals": "跳过批准", + "skipApprovalsDescription": "允许 Claude 使用该连接器的所有工具,且每次都不再显示批准卡片。", + "skipApprovalsAriaLabel": "为 {{name}} 跳过批准", + "tools": "工具", + "toolsDescription": "Claude 可以用该连接器做什么", + "noTools": "该连接器没有工具。", + "toolPermissionAriaLabel": "{{name}} 的权限", + "noToolDescription": "该工具未提供描述。", + "details": "详情", + "thirdParty": "第三方软件、内容、条款与信息", + "terms": "条款", + "askFallback": "当没有会话、项目或全局权限适用时询问。", + "rememberedApprovals_other": "已记住的批准:{{count}}", + "rememberedBlocked": "当前已阻止", + "rememberedUnnecessary": "当前无需使用", + "scopeGlobal": "全局", + "scopeProject": "项目", + "scopeSession": "会话", + "managePermissions": "管理权限" + }, + "panel": { + "contactEmail": "联系邮箱", + "contactEmailDescription": "在获得允许时,代表你发出的请求会把该邮箱提供给需要联系邮箱的科研数据服务(例如 NCBI、EBI 和 OurResearch 运营的服务)。", + "contactEmailAriaLabel": "联系邮箱", + "apiKeyAriaLabel": "NCBI API 密钥", + "apiKeyPlaceholder": "可选的 API 密钥", + "apiKeyHint": "提高 NCBI 速率上限(可选)。", + "save": "保存", + "clearKey": "清除密钥", + "notSet": "未设置", + "edit": "编辑", + "filterAriaLabel": "按分组筛选连接器", + "filterAll": "全部", + "filterFeatured": "精选", + "filterDirectory": "目录", + "filterCustom": "自定义", + "searchAriaLabel": "搜索连接器", + "searchPlaceholder": "搜索连接器…", + "addConnector": "添加连接器", + "localCommand": "本地命令", + "localCommandDescription": "通过命令运行 MCP 服务器", + "remoteServer": "远程服务器", + "remoteServerDescription": "连接到 MCP 服务器 URL", + "featuredSubtitle": "来自 Anthropic 的科研连接器", + "directorySubtitle": "与 Claude Connectors Directory 同步", + "customSubtitle": "你添加的连接器", + "noMatches": "没有连接器匹配你的搜索。", + "noCustom": "添加自定义连接器即可接入你自己的服务器。", + "editNamed": "编辑 {{name}}", + "removeNamed": "移除 {{name}}" + }, + "form": { + "typeAriaLabel": "连接器类型", + "localCommand": "本地命令", + "remoteServer": "远程服务器", + "optional": "(可选)", + "name": "名称", + "nameAriaLabel": "名称", + "namePlaceholder": "例如 Memory server", + "nameImmutable": "名称是连接器的身份标识,创建后不能修改。", + "description": "描述", + "descriptionAriaLabel": "描述", + "descriptionPlaceholder": "这个连接器提供什么", + "command": "命令", + "commandAriaLabel": "命令", + "customCommandAriaLabel": "自定义命令", + "commandOptions": { + "npx": "npx — Node 包", + "uvx": "uvx — Python (uv)", + "node": "node — 脚本文件", + "python3": "python3 — 脚本文件", + "docker": "docker — 容器", + "other": "其他…" + }, + "args": "参数", + "argsAriaLabel": "参数", + "argsHint": "用空格或换行分隔。", + "env": "环境变量", + "envAriaLabel": "环境变量", + "envHint": "每行一条 KEY=VALUE。", + "envHintEdit": "每行一条 KEY=VALUE。留空则保留当前值。", + "url": "服务器 URL", + "urlAriaLabel": "服务器 URL", + "transport": "传输方式", + "transportAriaLabel": "传输方式", + "headers": "请求头", + "headersAriaLabel": "请求头", + "headersHint": "每行一条 Name: Value(不是 JSON)。", + "headersHintEdit": "每行一条 Name: Value(不是 JSON)。留空则保留当前值。", + "trustAriaLabel": "我信任这个连接器", + "trust": "我信任这个连接器。只添加来自你信任的开发者的连接器。", + "saveFailed": "保存连接器失败。", + "saving": "保存中…", + "adding": "添加中…", + "saveChanges": "保存修改", + "addConnector": "添加连接器" + } + }, + "compute": { + "banner": "连接运行重型计算的地方 —— 通过 SSH 使用你自己的服务器。", + "list": { + "title": "SSH 主机", + "description": "来自你 SSH 主机列表的服务器、集群或作业提交节点", + "add": "添加 SSH 主机", + "removed": "已移除 {{name}}。", + "loadError": "无法加载主机:{{message}}", + "loading": "正在加载主机…", + "empty": "还没有 SSH 主机。添加一台,让 Open Science 在你的服务器上运行计算。" + }, + "status": { + "connected": "已连接", + "failed": "探测失败", + "notProbed": "未探测" + }, + "probed": { + "never": "尚未探测", + "now": "刚刚探测", + "minute": "{{count}} 分钟前探测", + "hour": "{{count}} 小时前探测", + "day": "{{count}} 天前探测", + "week": "{{count}} 周前探测", + "month": "{{count}} 个月前探测", + "year": "{{count}} 年前探测" + }, + "card": { + "browse": "浏览 {{name}} 上的文件", + "browseDisabled": "需要先探测到主机可连接,才能浏览文件", + "browseTooltip": "浏览文件", + "browseDisabledTooltip": "先探测主机才能浏览", + "remove": "移除 {{name}}", + "removeTooltip": "移除主机" + }, + "add": { + "intro": "从 ~/.ssh/config 中选一个主机别名,或者直接输入。Open Science 会用你现有的 SSH 密钥把它作为计算提供方 —— 不会复制任何凭据。", + "aliasFromConfig": "来自 ~/.ssh/config", + "aliasSelectAriaLabel": "从 ~/.ssh/config 中选择主机", + "aliasSelectEmpty": "~/.ssh/config 中没有主机", + "aliasSelectPlaceholder": "选择一台主机…", + "aliasTypedLabel": "或输入主机别名", + "aliasTypedPlaceholder": "例如 biowulf、lab-gpu、coder.myworkspace", + "detailsLabel": "有什么想让 Open Science 知道的吗?(可选)", + "detailsCount": "{{used}} / {{limit}} 字符", + "detailsPlaceholder": "这里的作业怎么跑 —— sbatch、qsub,还是直接 bash?可以用 pip/conda 安装吗,新环境该放在哪里?需要指定哪个分区、账户或模块?", + "advanced": "高级(覆盖 ~/.ssh/config)", + "advancedHint": "默认情况下,Open Science 会对 ~/.ssh/config 里的别名运行 ssh -G 来解析连接信息。只有需要覆盖时才填这些。", + "user": "用户", + "userHint": "留空则使用 ~/.ssh/config 中的 User。", + "port": "端口", + "portHint": "留空则使用 22 或 ~/.ssh/config 中的 Port。", + "identity": "身份密钥文件", + "identityHint": "留空则使用 ssh-agent 或 ~/.ssh/config 中的 IdentityFile。", + "submit": "添加", + "submitting": "正在添加主机…", + "failed": "无法添加主机。" + }, + "detail": { + "missing": "这台主机已不存在。", + "loading": "正在加载主机…", + "probe": "探测", + "probing": "正在探测…", + "retryProbe": "重新探测", + "probeFailedUnexpectedly": "探测意外失败。", + "remove": "移除", + "resources": "资源", + "cpus": "CPU", + "ram": "GB 内存", + "scheduler": "调度器", + "details": "详细信息", + "detailsDescription": "关于这个提供方的自由格式笔记。Open Science 会读取它,并在了解更多后补充内容。", + "detailsAriaLabel": "详细信息文档", + "detailsCount": "{{used}} / {{limit}} 字符", + "detailsTooLong": "详细信息不能超过 {{limit}} 个字符。", + "detailsSaveFailed": "保存详细信息失败。", + "detailsSkeleton": "(由探测自动生成)", + "detailsEmpty": "还没有笔记。点击「编辑」为这个提供方添加详细信息。", + "showMore": "展开", + "showLess": "收起", + "scratch": "临时目录", + "scratchDescription": "远程作业的工作目录。已固定的路径不会被重新探测覆盖。", + "scratchAriaLabel": "临时目录路径", + "scratchPlaceholder": "/scratch/username", + "scratchSaveFailed": "设置临时目录失败。", + "scratchPinned": "已固定", + "scratchEmpty": "未设置。下次探测时会从 $SCRATCH 更新。", + "concurrency": "并发作业上限", + "concurrencyDescription": "这台主机上同时运行的作业数上限(1–500)。暂未强制生效。", + "concurrencyAriaLabel": "并发作业上限", + "concurrencyInvalid": "必须是 1 到 500 之间的整数。", + "concurrencySaveFailed": "设置并发上限失败。", + "concurrencyDefault": "{{value}}(默认)", + "edit": "编辑", + "save": "保存", + "saving": "正在保存…" + }, + "approval": { + "title": "允许执行远程命令?", + "description": "远程命令以你在该主机上的账户身份运行,不在沙箱中。只有信任这条命令时才批准。", + "host": "主机", + "intent": "意图", + "command": "命令", + "inputs": "输入", + "showFull": "显示完整命令", + "showLess": "收起", + "deny": "拒绝", + "once": "仅此一次", + "conversation": "此会话", + "project": "本项目", + "always": "始终允许", + "broadScopeSubject": "{{host}} 上的远程命令" + } + }, + "fileBrowser": { + "ariaLabel": "远程文件浏览器", + "host": "主机", + "close": "关闭文件浏览器", + "back": "返回", + "up": "向上一级", + "goTo": "前往", + "goToLocations": "快速前往位置", + "scratch": "临时目录", + "home": "主目录", + "pinCurrent": "固定当前文件夹", + "bookmarks": "书签", + "removeBookmark": "移除书签 {{path}}", + "addressAriaLabel": "当前目录路径", + "refresh": "刷新目录列表", + "loading": "正在加载", + "contents": "目录内容", + "columnName": "名称", + "columnSize": "大小", + "columnModified": "修改时间", + "empty": "空目录", + "truncated": "仅显示前 5,000 个条目。进入子目录可查看更多。", + "errorTitle": "无法打开此路径。", + "retry": "重试", + "goHome": "前往主目录", + "invalidPath": "路径必须是绝对路径,且不能包含控制字符。", + "unknownError": "未知错误", + "details": "详细信息", + "closeDetail": "关闭详情面板", + "metaSize": "大小", + "metaModified": "修改时间", + "metaType": "类型", + "noPreview": "无预览 · {{size}}", + "download": "下载", + "downloading": "正在下载…", + "downloadAriaLabel": "将文件下载到系统下载文件夹", + "downloadFailed": "下载失败", + "savedToDownloads": "已保存到下载文件夹:{{name}}", + "showInFinder": "在访达中显示", + "addToProject": "添加到项目", + "addToProjectTitle": "即将推出 —— 远程文件导入到项目产物暂不可用", + "addToProjectAriaLabel": "将文件作为产物添加到当前项目(即将推出)", + "copyPath": "复制路径", + "copied": "已复制!", + "copyAriaLabel": "复制远程绝对路径到剪贴板", + "age": { + "second": "{{count}} 秒", + "minute": "{{count}} 分", + "hour": "{{count}} 小时", + "day": "{{count}} 天" + }, + "type": { + "file": "文件", + "text": "文本", + "image": "图片", + "archive": "压缩包", + "binary": "二进制", + "library": "库", + "log": "日志", + "rData": "R 数据" + } + } +} diff --git a/src/renderer/src/locales/zh-Hans/workspace.json b/src/renderer/src/locales/zh-Hans/workspace.json new file mode 100644 index 000000000..5cb73db41 --- /dev/null +++ b/src/renderer/src/locales/zh-Hans/workspace.json @@ -0,0 +1,426 @@ +{ + "composer": { + "placeholder": "输入任何内容——/ 调用技能,@ 引用文件,{{shortcut}} 搜索", + "ariaLabel": "输入任何内容", + "addAttachmentOrReview": "添加附件或请求审阅", + "attachFiles": "附加文件", + "attachmentLimits": "支持任意文件类型 · 每个文件最大 {{size}}。大型文件以链接形式附加,不嵌入内容。", + "requestReview": "请求审阅", + "cancelRun": "取消运行", + "sendMessage": "发送消息", + "dropFiles": "拖放文件以附加", + "removeAttachment": "删除附件 {{name}}", + "cancelAttachment": "取消附件 {{name}}", + "removeFailedAttachment": "删除失败的附件 {{name}}", + "uploadingFile": "正在上传 {{name}}", + "notebook": "Notebook", + "openNotebook": "打开 Notebook", + "compacting": "正在压缩对话以适应上下文限制……" + }, + "previews": { + "fileMissing": "此文件已不可用 — 可能已被移动或删除。", + "fileOutsideStorage": "此文件不在当前存储位置 — 它可能属于你已迁离的数据文件夹。", + "missingTag": "缺失", + "unavailableTitle": "无法预览", + "fileUnavailableTitle": "文件不可用", + "unsupportedMessage": "此文件类型不支持预览", + "renderingPreview": "正在渲染预览", + "retry": "重试", + "loadingCode": "正在准备代码文件", + "loadingCsv": "正在准备数据", + "loadingFasta": "正在准备序列", + "loadingHtml": "正在准备文档", + "loadingImage": "正在准备图片", + "loadingJson": "正在准备数据", + "loadingMarkdown": "正在准备文档", + "loadingMolecule": "正在准备结构", + "loadingPdb": "正在准备结构", + "loadingPdf": "正在准备文档", + "loadingPresentation": "正在准备演示文稿", + "loadingSpreadsheet": "正在准备电子表格", + "loadingText": "正在准备文本文件", + "loadingTiff": "正在准备图片", + "loadingUnknown": "正在准备预览", + "loadingWord": "正在准备文档", + "errorCsv": "无法读取 CSV 以进行预览", + "errorFasta": "无法读取 FASTA 以进行预览", + "errorHtml": "无法读取 HTML 以进行预览", + "errorImage": "无法加载图片以进行预览", + "errorJson": "无法读取 JSON 以进行预览", + "errorMarkdown": "无法读取 Markdown 以进行预览", + "errorMolecule": "无法读取结构文件以进行预览", + "errorPdb": "无法读取 PDB 以进行预览", + "errorPdf": "无法渲染此 PDF 以进行预览", + "errorText": "无法读取文件以进行预览", + "errorOffice": "无法渲染此 Office 文件以进行预览", + "previewOf": "{{name}} 的预览", + "columnFallback": "第 {{index}} 列", + "invalidJson": "无效的 JSON", + "invalidJsonDetail": "无效的 JSON:{{error}}", + "zoomIn": "放大", + "zoomOut": "缩小", + "resetZoom": "重置缩放", + "htmlRender": "渲染", + "htmlSource": "源码", + "htmlShowRendered": "显示渲染后的 HTML", + "htmlShowSource": "显示 HTML 源码", + "pageNumber": "第 {{page}} 页", + "previousPage": "上一页", + "previousPreviewPage": "上一预览页", + "nextPage": "下一页", + "nextPreviewPage": "下一预览页", + "moleculeViewerBanner": "使用 OpenChemLib 查看器", + "pdbViewerBanner": "使用 3Dmol.js 查看器", + "moleculeRenderFailed": "无法渲染结构:{{error}}", + "moleculeRenderError": "无法渲染结构", + "moleculeRendererFailed": "分子渲染器加载失败", + "moleculePreviewOf": "{{name}} 的结构预览", + "pdbPreviewOf": "{{name}} 的 3D 预览", + "pdbAtomCount": "{{count}} 个原子", + "pdbStyleLabel": "样式:", + "pdbCartoon": "卡通", + "pdbStick": "棍状", + "pdbSphere": "球状", + "pdbSurface": "表面", + "pdbLine": "线状", + "pdbCartoonUnavailable": "卡通样式需要蛋白质或核酸主链", + "pdbViewerFailed": "查看器失败", + "pdbViewerInitFailed": "无法初始化 PDB 查看器:{{error}}", + "pdbControlsHint": "拖动旋转 · 滚动缩放 · Shift + 拖动平移", + "officeChecking": "正在检查 Office 文件", + "officeStarting": "正在启动 Office 预览", + "officeTooLargeTitle": "文件过大,无法预览", + "officeTooLargeMessage": "此文件大于 40 MB。请下载后查看。", + "officeInvalidPackage": "此 Office 文件已损坏或不受支持。请下载后查看。", + "officeResourceLimit": "此 Office 文件超出安全预览限制。请下载后查看。", + "figureOutput": "图表输出", + "csvStats": "{{rows}} 行 · {{columns}} 列", + "csvStatsTruncated": "{{rows}}+ 行 · {{columns}} 列", + "csvHiddenColumns": "+{{count}} 更多" + }, + "mention": { + "skillSuggestions": "技能建议", + "artifactSuggestions": "Artifact 建议", + "noArtifacts": "暂无 Artifacts", + "loadingFiles": "正在加载项目文件……", + "loadError": "无法加载项目文件", + "sectionUploads": "用户上传", + "sectionArtifacts": "其他 Artifacts", + "sourceFeatured": "精选", + "sourceImported": "已导入", + "sourcePersonal": "个人", + "hintNavigate": "导航", + "hintSelect": "选择", + "hintClose": "关闭" + }, + "modelPicker": { + "noModelAvailable": "无可用模型", + "noModelAvailableAriaLabel": "无可用模型 — 打开设置", + "selectModel": "选择模型", + "noCompatibleModel": "无兼容模型", + "openSettings": "打开设置" + }, + "uploadStatus": { + "queued": "排队中", + "cancelling": "正在取消……", + "failed": "上传失败" + }, + "preview": { + "expand": "展开预览面板", + "collapse": "收起预览面板", + "resizeRightPanel": "调整右侧面板大小", + "openPreviews": "已打开的预览", + "closePreview": "关闭 {{title}} 的预览", + "closePreviewTooltip": "关闭预览", + "openFullScreen": "全屏预览", + "openFullScreenOf": "全屏预览 {{title}}", + "localBadge": "这台电脑" + }, + "reportError": { + "button": "报告错误", + "ariaLabel": "报告此错误" + }, + "permission": { + "scopeOnce": "仅一次", + "scopeOnceSubtitle": "仅此次调用", + "scopeSession": "此会话", + "scopeSessionSubtitle": "重启后对此会话仍然有效", + "allow": "允许", + "deny": "拒绝", + "allowOnce": "仅允许一次", + "allowAlways": "始终允许", + "rejectOnce": "仅拒绝一次", + "rejectAlways": "始终拒绝", + "authorizationScope": "授权范围", + "impactInfo": "权限影响说明", + "permissionInfo": "权限说明", + "chooseScope": "选择授权范围", + "scopeDescriptionOnce": "授权仅适用于本次调用。", + "scopeDescriptionSession": "重启后批准仍然附加于此会话。", + "scopeDescriptionSessionNotebook": "批准涵盖此会话中后续的 {{runtime}} 调用。", + "scopeDescriptionNone": "此请求没有可用的授权范围。", + "actionTitleNotebook": "运行 Notebook 单元格", + "actionTitleArtifact": "写入 Artifact 文件", + "actionTitleMcp": "外部服务输入", + "actionTitleCommand": "运行命令", + "runtimePython": "Python", + "runtimeR": "R", + "runtimeJs": "JavaScript REPL", + "runtimeBash": "Notebook 终端", + "scopeProject": "此项目", + "scopeProjectSubtitle": "适用于此项目中的所有会话", + "scopeGlobal": "全局", + "scopeGlobalSubtitle": "适用于所有项目", + "suffixOnce": "一次", + "suffixSession": "用于此会话", + "suffixProject": "用于此项目", + "suffixGlobal": "全局应用", + "scopeDescriptionProject": "批准适用于此项目中的匹配调用。", + "scopeDescriptionGlobal": "批准适用于所有项目中的匹配调用。" + }, + "reviewer": { + "title": "会话审阅器", + "checks": "检查项", + "noChecks": "未记录任何检查项。", + "noReview": "此会话暂无可用的审查结果。", + "statusPass": "通过", + "statusWarn": "警告", + "statusFail": "失败", + "refLabel": "引用:", + "log": "审阅器日志", + "collapseLog": "收起审阅器日志", + "expandLog": "展开审阅器日志", + "input": "输入", + "output": "输出", + "exitCode": "退出码 {{code}}", + "staleNotice": "此轮次在审阅运行后发生了更改(例如某个 Artifact 被编辑)。以下结果可能已过期——请重新运行审阅以刷新。" + }, + "errorReport": { + "title": "报告此错误", + "description": "此报告将公开发布在 GitHub 上。请在分享前编辑以下错误文本,删除任何敏感信息。运行日志保存在本设备上,不会自动附加。", + "errorDetails": "错误详情", + "alsoIncluded": "同时包含", + "reportEnvironment": "报告环境信息", + "githubPrefill": "GitHub Issue 预填内容", + "consent": "我已审阅以上详情,并同意将其发布至 GitHub 公开 Issue,遵守 GitHub 的", + "privacyStatement": "隐私声明", + "revealLog": "显示日志文件", + "copyDetails": "复制详情", + "copied": "已复制", + "openIssue": "提交 GitHub Issue" + }, + "notebook": { + "title": "会话 Notebook", + "agents_other": "{{count}} 个 Agent", + "cells_other": "{{count}} 个单元格", + "repl": "{{count}} repl", + "shell": "{{count}} shell", + "loading": "正在加载 Notebook……", + "loadFailed": "加载 Notebook 失败。", + "noRecords": "此会话没有执行记录。", + "exporting": "正在导出……", + "exportIpynb": ".ipynb", + "exportAll": "全部 ({{count}})", + "downloadAs": "将 {{kernel}} 单元格下载为 .ipynb", + "downloadAsAriaLabel": "下载 {{kernel}} 为 .ipynb", + "downloadAsGeneric": "下载为 .ipynb", + "errorAtLine": "错误(第 {{line}} 行)", + "error": "错误", + "downloadAllAriaLabel": "按内核分别下载 Notebook ({{count}})", + "downloadAllTooltip": "将每个数据内核的 .ipynb 分别保存到所选目录(共 {{count}} 个文件)。", + "noDataKernel": "请先运行 Python 或 R 单元格以启用 .ipynb 导出。", + "controlFallback": "(控制面板标签将回退到最近的数据内核)", + "savedNotebooks": "已将 {{count}} 个 Notebook 保存到 {{directory}}" + }, + "files": { + "filterAriaLabel": "筛选项目文件", + "artifacts": "Artifacts", + "localSection": "这台电脑", + "addLocalFolder": "添加本地文件夹……", + "soon": "即将推出", + "remote": "远程", + "hostUnreachable": "主机不可达", + "addHost": "添加 SSH 主机……", + "retryIndexing": "重试索引项目文件", + "uploads": "我的上传", + "allArtifacts": "全部 Artifacts", + "sessionFallback": "会话 {{id}}", + "noMore": "没有更多", + "loading": "正在加载……", + "loadMore": "加载更多", + "loadMoreSessions": "加载更多会话", + "fileCount_other": "{{count}} 个文件", + "indexIncomplete": "部分文件尚未完成索引。", + "retrying": "正在重试……", + "retry": "重试", + "noFiles": "暂无文件", + "noSearchMatches": "没有文件匹配“{{query}}”", + "generatedFiles": "生成的文件", + "loadMoreFrom": "加载 {{title}} 的更多文件", + "loadMoreUploads": "加载更多已上传文件" + }, + "sidebar": { + "allProjects": "所有项目", + "sessions": "会话", + "new": "新建", + "files": "文件", + "settings": "设置", + "sessionActions": "会话操作", + "sessionActionsFor": "打开 {{title}} 的操作", + "statusIdle": "空闲", + "statusRunning": "运行中", + "statusWaiting": "等待权限", + "statusError": "错误", + "sessionStatus": "会话状态:{{status}}", + "pin": "固定", + "unpin": "取消固定", + "rename": "重命名……", + "viewNotebook": "查看 Notebook", + "delete": "删除", + "pinned": "已固定", + "active": "活跃", + "closeNavigation": "关闭导航" + }, + "message": { + "generated": "已生成 · {{count}}", + "expandFiles": "展开生成的文件", + "collapseFiles": "收起生成的文件", + "moreFiles": "+{{count}} 个", + "showLess": "收起", + "editPlaceholder": "编辑消息", + "editAriaLabel": "编辑消息", + "cancel": "取消", + "send": "发送", + "copy": "复制消息", + "copied": "已复制", + "edit": "编辑消息", + "previousRevision": "上一个消息版本", + "nextRevision": "下一个消息版本", + "revisionCounter": "消息版本", + "previewAttachment": "预览上传的附件 {{name}}", + "previewGeneratedFile": "预览生成的文件 {{name}}", + "openSkill": "打开技能 {{name}}", + "previewArtifact": "预览 {{name}}", + "conversation": "对话", + "sent": "已发送", + "completed": "已完成", + "failed": "已失败", + "elapsed": "耗时 {{duration}}", + "elapsedLabel": "运行耗时", + "usage": "用量", + "usageAvailable": "此回复的 token 用量", + "usageUnavailable": "此回复没有 token 用量数据", + "usageInput": "输入", + "usageOutput": "输出", + "usageCache": "缓存", + "usageCacheRead": "缓存读取", + "usageCacheWrite": "缓存写入", + "usageBreakdown": "{{entries}};共 {{total}} 个 token", + "usageBreakdownUnavailable": "没有 token 用量明细", + "usageTotal": "总计", + "usageTurns_other": "{{count}} 轮", + "usageFramework": "Agent 框架:{{name}}", + "usageProvider": "模型供应商:{{name}}", + "usageProviderWithModel": "模型供应商:{{name}};模型:{{model}}", + "usageAgentDetail": "Agent:{{name}}", + "usageModelDetail": "模型:{{model}}" + }, + "deleteSession": { + "title": "删除会话?", + "description": "将永久删除“{{title}}”。此会话中创建的 Artifacts 将保留在项目中。此操作无法撤销。", + "delete": "删除" + }, + "renameSession": { + "title": "重命名会话", + "description": "更新会话列表中显示的名称。", + "nameAriaLabel": "会话名称", + "rename": "重命名" + }, + "conversation": { + "newTitle": "新对话" + }, + "interrupted": { + "resume": "恢复", + "resuming": "正在恢复……", + "resumeAriaLabel": "恢复会话", + "defaultMessage": "此会话已中断。" + }, + "agentControls": { + "ariaLabel": "Agent 控制:{{profile}},自动审阅{{autoReview}}", + "permissionMode": "权限模式", + "permissionDescription": "Agent 在未经请求的情况下可执行的操作范围。", + "profileAsk": "请求批准", + "profileAskShort": "询问", + "profileAskDescription": "在编辑文件、执行命令、网络请求和 MCP 工具前询问。", + "profileAuto": "自动批准编辑", + "profileAutoShort": "自动", + "profileAutoDescription": "自动批准工作区内的文件编辑。命令、网络请求和 MCP 仍需询问。", + "profileFull": "完全访问", + "profileFullShort": "完全访问", + "profileFullDescription": "无需提示直接执行所有操作,包括命令和网络请求。", + "profileFullUnavailable": "当前 Agent 不支持原生绕过模式。", + "conservativeNote": "此 Agent 没有原生自动模式。Open Science 仅自动批准工作区内的文件编辑——命令、网络请求和 MCP 工具仍需询问。", + "autoReview": "自动审阅", + "autoReviewDescription": "审阅 Agent 会在每次更改落地前进行检查。", + "grantsTitle": "本次会话已允许", + "clearAll": "全部清除", + "clearAllAriaLabel": "清除本次会话的所有授权", + "revokeGrantAriaLabel": "撤销 {{label}} 的会话授权", + "grantsBadgeAriaLabel": "本次会话已允许 {{count}} 项", + "specialist": "专家", + "specialistDescription": "为此对话绑定一位个人专家。", + "specialistNone": "无", + "specialistUnavailable": "不可用", + "specialistCreateNew": "新建……", + "ssh": "SSH", + "compute": "计算", + "computeDescription": "在远端 SSH 主机上运行任务,或管理主机。", + "noHosts": "未注册 SSH 主机", + "loading": "加载中……", + "manageCompute": "管理计算资源……", + "fullAccessDialogTitle": "启用完全访问?", + "fullAccessDialogDescription": "Agent 可以无需询问直接运行命令、修改文件、执行 Notebook 代码和发起网络请求。身份验证失败和执行错误仍会中止运行。", + "enable": "启用" + }, + "editConfirm": { + "title": "在新分支上重新发送?", + "description_other": "发送此编辑后的提示会从这里开始一个新分支。当前紧随其后的 {{count}} 个轮次仍可通过消息版本控件访问。", + "branch": "创建分支并重新发送" + }, + "envStatus": { + "failed": "环境更新失败", + "updating": "正在更新 Notebook 环境…… {{percent}}%", + "retry": "重试" + }, + "context": { + "estimatedAriaLabel": "预估上下文:{{label}}", + "usedAriaLabel": "已用上下文:{{label}}", + "window": "上下文窗口", + "compact": "压缩", + "compacting": "正在压缩…", + "compactAriaLabel": "压缩上下文", + "compactingAriaLabel": "正在压缩上下文" + }, + "notebookPreview": { + "sourceUser": "您", + "placeholder": "在此内核中运行代码…", + "rRestartBanner": "已安装的 R 包需要重启内核才能加载。", + "restartR": "重启 R 内核", + "restarting": "正在重启…", + "pythonKernelLabel": "Python 内核 · 与 Agent 共享", + "statusRunning": "运行中", + "statusIdle": "空闲", + "rPreparing": "R(准备中…)" + }, + "tool": { + "outputTruncated": "输出已截断", + "loadingPreview": "正在加载预览…", + "searchQuery": "查询", + "webSearch": "Web 搜索", + "searchResult": "{{count}} 个结果" + }, + "agentLoading": { + "responding": "Agent 正在响应", + "slow": "· 响应比平时慢" + } +} diff --git a/src/renderer/src/locales/zh-Hant/common.json b/src/renderer/src/locales/zh-Hant/common.json new file mode 100644 index 000000000..3849926d9 --- /dev/null +++ b/src/renderer/src/locales/zh-Hant/common.json @@ -0,0 +1,50 @@ +{ + "language": { + "label": "語言", + "ariaLabel": "介面語言", + "system": "系統", + "systemDescription": "跟隨裝置設定", + "names": { + "en": "English", + "zh-Hans": "简体中文", + "zh-Hant": "繁體中文" + } + }, + "theme": { + "label": "主題", + "ariaLabel": "主題", + "system": "系統", + "systemDescription": "跟隨裝置設定", + "light": "淺色", + "lightDescription": "一律淺色", + "dark": "深色", + "darkDescription": "一律深色", + "triggerLabel": "主題:{{theme}}" + }, + "actions": { + "open": "開啟", + "opening": "正在開啟…", + "reveal": "在資料夾中顯示", + "working": "處理中…", + "back": "上一步", + "cancel": "取消", + "continue": "繼續", + "delete": "刪除", + "quit": "結束", + "minimizeToTray": "最小化到系統匣", + "close": "關閉", + "deleting": "刪除中…" + }, + "states": { + "notAvailableYet": "尚不可用。" + }, + "compatibility": { + "routeSeparator": " 或 ", + "codexNeedsResponses": "{{provider}} 使用 /v1/chat/completions。Codex 需要 /v1/responses;請選擇一個 OpenAI Responses 服務商,或切換 Agent 框架。", + "routeMismatch": "{{framework}} 需要 {{frameworkRoutes}},但 {{provider}} 使用 {{providerRoutes}}。" + }, + "scroll": { + "toEnd": "捲動至底部", + "toStart": "捲動至頂部" + } +} diff --git a/src/renderer/src/locales/zh-Hant/components.json b/src/renderer/src/locales/zh-Hant/components.json new file mode 100644 index 000000000..333880ad4 --- /dev/null +++ b/src/renderer/src/locales/zh-Hant/components.json @@ -0,0 +1,193 @@ +{ + "dialog": { + "close": "關閉", + "cancel": "取消", + "back": "返回" + }, + "update": { + "title": "有可用更新", + "versionRange": "v{{current}} → v{{latest}}", + "whatsNew": "更新內容", + "viewNotes": "在 GitHub 上檢視完整發布說明", + "noNotes": "這個版本的發布說明無法在應用程式內顯示。", + "viewNotesShort": "在 GitHub 上檢視發布說明", + "failed": "更新失敗", + "downloadManually": "手動下載", + "restart": "重新啟動以更新", + "applyingNotice": "Open Science 正在停止背景工作,並將關閉以完成安裝。更新可能需要一些時間,請勿在此期間重新開啟應用程式。更新完成後應用程式會自動重新開啟。", + "preparing": "正在準備更新……", + "openInstaller": "開啟安裝程式", + "downloading": "正在下載 {{percent}}%", + "downloadSized": "下載更新({{size}})", + "download": "下載更新" + }, + "closeConfirm": { + "quitTitle": "結束 {{appName}}?", + "chooseTitle": "最小化還是結束?", + "quitDescription": "還有任務正在執行,結束會中斷它們。", + "chooseDescription": "應用程式可以保留在系統匣中繼續執行,你也可以直接結束。", + "rowTitle": "{{project}} — {{title}}", + "dontAskAgain": "不再詢問" + }, + "fieldHelp": { + "trigger": "更多資訊" + }, + "lifecycleToast": { + "sessionCreated": "會話由外部建立", + "view": "檢視", + "dismiss": "忽略" + }, + "jobStatus": { + "queued": "排隊中", + "running": "執行中", + "done": "已完成", + "failed": "失敗", + "timeout": "逾時", + "error": "錯誤" + }, + "jobs": { + "badgeActiveLabel_other": "{{count}} 個遠端任務正在執行", + "badgeActiveText": "{{count}} 個執行中 · {{elapsed}}", + "badgeIdleLabel_other": "{{count}} 個遠端任務", + "badgeIdleText_other": "{{count}} 個任務", + "tooltipHeading": "遠端 · {{count}}", + "modalLabel": "遠端任務詳情", + "listTitle": "本會話中執行的任務", + "empty": "本會話沒有遠端任務。", + "provider": "服務商", + "status": "狀態", + "runtime": "執行時間", + "remoteWorkdir": "遠端工作目錄", + "jobId": "任務 ID", + "noOutput": "尚無輸出。" + }, + "reviewer": { + "title": "審查", + "reviewing": "正在審查…", + "error": "審查出錯", + "noIssues": "未發現問題", + "noIssuesStale": "未發現問題(已過期)", + "findings_other": "{{count}} 項發現", + "findingsStale_other": "{{count}} 項發現(已過期)", + "pending": "等待審查", + "checks_other": "{{count}} 項檢查", + "fixLimitReached": "已達修復上限", + "goToTranscript": "跳轉到對話記錄", + "staleNotice": "這次審查之後輪次發生了變化。", + "rerun": "重新審查", + "rerunning": "正在重新審查…", + "selfCorrectNote": "Agent 會讀取這些發現,並在下一則訊息中自行修正。", + "reflagged": "已重新標記 ×{{count}}" + }, + "dataRoot": { + "warning": "這個資料夾由 {{appName}} 管理。不要移動、重新命名或刪除其中的檔案,否則可能破壞你的專案和歷史記錄。", + "missing": { + "title": "找不到資料夾", + "description": "找不到你的資料夾 {{path}}。它可能已被刪除,或者位於一個尚未連接的磁碟上。", + "stillMissing": "仍然找不到。重新連接磁碟後再試,或者換一個位置。", + "notUsable": "所選資料夾不可用。", + "switchFailed": "無法切換到這個資料夾。", + "retry": "重新連接並重試", + "retrying": "正在檢查…", + "chooseAnother": "換一個位置", + "switching": "正在切換…", + "continueEmpty": "使用空資料夾繼續", + "continueNote": "{{appName}} 會在你使用過程中重新建立這個資料夾。原位置中的檔案在重新連接前無法使用。" + }, + "legacyMove": { + "title": "把資料移到可見的資料夾?", + "description": "你的研究資料放在一個隱藏資料夾裡。把它移到可見的 OpenScience 資料夾便於尋找和備份 —— 你的設定和歷史記錄會留在原處。", + "currentLabel": "目前位置(隱藏)", + "currentAriaLabel": "目前資料位置", + "newLabel": "新位置", + "newAriaLabel": "新資料位置", + "resolving": "正在解析…", + "occupied": "這個資料夾裡已經有 {{appName}} 的資料。請選一個空資料夾,或者使用預設位置。", + "unusable": "這個資料夾不可用。換一個吧。", + "moveToDefault": "移到 OpenScience", + "chooseFolder": "選擇其他資料夾…", + "checking": "正在檢查…", + "keepHere": "保留在目前資料夾", + "note": "之後可以隨時在「設定 → 資料位置」中修改。我們不會再詢問。" + } + }, + "jobRow": { + "rowAriaLabel": "遠端工作:{{intent}}", + "queued": "佇列中", + "running": "執行中", + "cardAriaLabel": "已完成的遠端工作:{{intent}}", + "finished": "已完成", + "failed": "失敗", + "timedOut": "已逾時", + "error": "錯誤" + }, + "updateCapsule": { + "label": "更新", + "ariaLabel": "有可用更新:{{version}}" + }, + "github": { + "openAriaLabel": "在 GitHub 上開啟 {{app}}", + "starAriaLabel": "在 GitHub 上為 {{app}} 點 Star,{{count}} 個 Star" + }, + "linkSafety": { + "dialogAriaLabel": "開啟外部連結?", + "description": "你即將前往一個外部網站。", + "copyLink": "複製連結", + "copied": "已複製", + "openLink": "開啟連結" + }, + "mermaid": { + "errorTitle": "無法渲染 Mermaid 語法", + "commonCauses": "常見原因:xychart 缺少 title 關鍵字,軸標籤沒有加引號,或是 y-axisbar/line 的資料列缺失。", + "viewSource": "查看原始碼", + "retry": "重試" + }, + "download": { + "reconnecting": "連線已中斷,正在恢復…(第 {{attempt}} 次嘗試)", + "downloaded": "已下載" + }, + "globalSearch": { + "title": "命令面板", + "placeholder": "在這個專案中搜尋…", + "resultCount_other": "{{count}} 筆結果", + "noProject": "新增一個專案,即可搜尋會話和產物。", + "recentArtifacts": "最近的產物", + "recentSessions": "最近的會話", + "commands": "命令", + "newSession": "新增會話", + "artifacts": "產物", + "sessions": "會話", + "otherProjects": "其他專案", + "searchingArtifacts": "正在搜尋產物…", + "loadArtifactsFailed": "無法載入產物。", + "retryArtifacts": "無法載入產物 —— 重試", + "retryMoreArtifacts": "無法載入更多 —— 重試", + "showMore": "另有 {{count}} 筆符合 —— 顯示更多", + "noMatches": "沒有會話或產物符合「{{query}}」。", + "indexIncomplete": "部分產物結果可能未顯示。", + "sessionUnavailable": "這個會話已不存在。", + "sessionBadge": "會話", + "artifactCount_other": "{{count}} 個產物", + "unknownProject": "未知專案", + "unknownSession": "未知會話", + "createdAtUnavailable": "建立時間無法取得", + "mention": "提及 {{name}}", + "mentionUnavailable": "輸入框無法再接收產物,暫時無法提及。", + "open": "開啟 {{name}}", + "elapsed": { + "now": "剛剛", + "minute_other": "{{count}} 分鐘前", + "hour_other": "{{count}} 小時前", + "day_other": "{{count}} 天前", + "week_other": "{{count}} 週前", + "month_other": "{{count}} 個月前", + "year_other": "{{count}} 年前" + }, + "shortcut": { + "navigate": "切換", + "open": "開啟", + "mention": "提及", + "close": "關閉" + } + } +} diff --git a/src/renderer/src/locales/zh-Hant/home.json b/src/renderer/src/locales/zh-Hant/home.json new file mode 100644 index 000000000..f57e4449c --- /dev/null +++ b/src/renderer/src/locales/zh-Hant/home.json @@ -0,0 +1,58 @@ +{ + "header": { + "beta": "測試版", + "environmentRepair": "開啟環境修復", + "environmentOneFailure": "{{label}} 需要處理", + "environmentManyFailures": "{{count}} 項環境設定需要處理", + "modelSettings": "模型設定", + "newProject": "新增專案", + "environmentShort": "環境" + }, + "projects": { + "heading": "專案", + "sectionLabel": "專案", + "loadError": "無法載入專案:{{error}}", + "empty": "還沒有專案。建立一個開始使用。", + "exampleBadge": "範例", + "sessionCount_other": "{{count}} 個會話", + "sessionCountUnavailable": "會話數不可用", + "rowActions": "開啟 {{name}} 的操作選單", + "actionsMenu": "專案操作", + "rename": "重新命名…", + "delete": "刪除" + }, + "recent": { + "heading": "最近會話", + "sectionLabel": "最近會話", + "empty": "你啟動的會話會出現在這裡。" + }, + "form": { + "createTitle": "新增專案", + "createDescription": "把相關會話歸入一個專案,之後可以重新命名。", + "createSubmit": "建立專案", + "editTitle": "編輯專案", + "editDescription": "重新命名此專案或修改其描述。", + "editSubmit": "儲存變更", + "nameLabel": "名稱", + "namePlaceholder": "例如:重現已發表的研究", + "descriptionLabel": "描述", + "descriptionOptional": "(選填)", + "descriptionPlaceholder": "這個專案是關於什麼的?", + "saveFailed": "無法儲存專案。" + }, + "deleteDialog": { + "title": "刪除專案?", + "description": "這將永久刪除「{{name}}」。Open Science 儲存的生成產物和上傳檔案也會被刪除。專案工作資料夾中的檔案不會被刪除。此操作無法復原。", + "descriptionWithSessions_other": "這將永久刪除「{{name}}」及其 {{count}} 個工作階段。Open Science 儲存的生成產物和上傳檔案也會被刪除。專案工作資料夾中的檔案不會被刪除。此操作無法復原。", + "descriptionWithIncompleteCatalog": "這將永久刪除「{{name}}」及其所有已儲存的工作階段,包括復原期間無法載入的工作階段。Open Science 儲存的生成產物和上傳檔案也會被刪除。專案工作資料夾中的檔案不會被刪除。此操作無法復原。" + }, + "relativeTime": { + "now": "剛剛", + "minute": "{{count}} 分鐘前", + "hour": "{{count}} 小時前", + "day": "{{count}} 天前", + "week": "{{count}} 週前", + "month": "{{count}} 個月前", + "year": "{{count}} 年前" + } +} diff --git a/src/renderer/src/locales/zh-Hant/onboarding.json b/src/renderer/src/locales/zh-Hant/onboarding.json new file mode 100644 index 000000000..5f58bb09c --- /dev/null +++ b/src/renderer/src/locales/zh-Hant/onboarding.json @@ -0,0 +1,86 @@ +{ + "shell": { + "eyebrow": "首次設定", + "title": "設定你的研究工作區。", + "intro": "先做一次快速的主機檢查,確認這台電腦就緒;然後連接你要使用的模型;最後選擇資料的存放位置。", + "relaunching": "正在準備你的工作區…", + "progressLabel": "設定進度" + }, + "steps": { + "environment": "環境", + "agent": "代理執行環境", + "provider": "模型供應方", + "notebook": "Notebook 執行環境", + "location": "資料位置" + }, + "footer": { + "allChecksPassed": "所有必需的環境檢查均已通過。", + "completeRequired": "請完成上面每一項必需內容才能繼續。" + }, + "environment": { + "title": "準備環境", + "description": "Open Science 會在你的第一次研究會話前確認其核心依賴。", + "checkAgain": "重新檢查", + "checking": "檢查中…", + "sectionLabel": "準備環境", + "requirementsLabel": "環境依賴", + "waiting": "等待檢查…", + "resolveHint": "請先處理標記為「需要處理」的項目,然後點擊「重新檢查」。", + "setupFailed": "設定未能完成", + "checkLabels": { + "system": "系統相容性", + "storage": "應用儲存權限", + "secure-storage": "憑證安全儲存", + "install-network": "安裝所需網路" + }, + "status": { + "passed": "就緒", + "warning": "待確認", + "failed": "需要處理" + } + }, + "provider": { + "title": "連接模型", + "description": "選擇 Open Science 在新研究會話中使用的供應方。", + "sectionLabel": "設定模型", + "encryptionUnavailable": "安全金鑰儲存不可用。在系統鑰匙串解鎖或授權之前,無法儲存 API 金鑰。", + "claudeSignInHint": "透過瀏覽器登入以連接你的 Claude 訂閱。我們會在瀏覽器中開啟 Claude 進行授權,然後帶你回到這裡。", + "cancelSignIn": "取消登入", + "waitingForSignIn": "等待登入…", + "testingConnection": "正在測試連線…", + "signInAndContinue": "登入並繼續", + "testAndContinue": "測試並繼續", + "claudeProviderChanged": "登入過程中 Claude 供應方發生了變化。請檢查所選項後重試。", + "codexProviderChanged": "登入過程中 Codex 供應方發生了變化。請檢查所選項後重試。", + "providerChangedDuringTest": "測試過程中供應方發生了變化。請重試。", + "claudeTokenSaveFailed": "無法儲存 Claude 權杖。", + "providerSaveFailed": "無法儲存供應方。" + }, + "location": { + "title": "Open Science 應該把你的資料存放在哪裡?", + "description": "大型檔案(產物、notebook、環境)存放在這裡。你的設定和歷史記錄始終保留在預設位置。之後可以在「設定」中修改。", + "sectionLabel": "選擇資料位置", + "relaunchError": "無法切換到新位置:{{error}} 你可以重試,或繼續使用預設位置。", + "relaunchFallbackError": "無法重新啟動以套用新位置。", + "locationLabel": "位置", + "pathLabel": "資料位置路徑", + "browse": "瀏覽…", + "chosenNotice": "你的資料將存放在 {{path}}。Open Science 會重新啟動以完成設定。改用預設位置", + "adoptNotice": "該資料夾已包含 Open Science 資料 —— 將按原樣使用(不會移動任何內容)。", + "finish": "完成", + "confirmTitle": "重新啟動以設定資料位置?", + "confirmDescription": "Open Science 將重新啟動,並在 {{path}} 完成資料設定。", + "keepDefault": "保留預設", + "restart": "重新啟動" + }, + "agent": { + "title": "設定代理執行環境", + "description": "選擇 Open Science 驅動的代理,然後安裝它。只需安裝這一個代理即可繼續。" + }, + "notebook": { + "title": "Notebook 執行環境(選用)", + "description": "Notebook 預設在應用託管的 Python 環境中執行。之後可以在「設定 → 執行環境」中修改這些設定。", + "provisioning": "正在設定 Notebook 執行環境 —— 請等待它完成,或取消它,然後繼續。", + "optional": "選用 —— 這裡的內容都不是完成設定的必需項。" + } +} diff --git a/src/renderer/src/locales/zh-Hant/settings.json b/src/renderer/src/locales/zh-Hant/settings.json new file mode 100644 index 000000000..2c5276461 --- /dev/null +++ b/src/renderer/src/locales/zh-Hant/settings.json @@ -0,0 +1,1071 @@ +{ + "shell": { + "dialogTitle": "設定", + "dialogDescription": "管理你的 Agent 執行環境和模型服務商。", + "navLabel": "設定", + "groups": { + "capabilities": "能力", + "workspace": "工作區", + "remoteAccess": "遠端存取" + }, + "panels": { + "skills": "技能", + "connectors": "連接器", + "specialists": "專家", + "compute": "運算", + "network": "網路", + "model": "模型", + "agent": "Agent", + "runtimes": "執行環境", + "storage": "儲存", + "general": "一般", + "permissions": "權限", + "remote-control": "遠端控制" + }, + "back": "上一步", + "forward": "下一步", + "backToRoot": "返回{{panel}}", + "maximize": "最大化", + "restore": "還原", + "close": "關閉設定", + "panelsLower": { + "skills": "技能", + "model": "模型", + "network": "網路", + "connectors": "連接器", + "compute": "運算" + }, + "save": "儲存", + "saving": "儲存中…", + "closeNav": "關閉設定導覽", + "openNav": "開啟設定導覽", + "navigation": "導覽", + "dismissError": "忽略設定錯誤", + "dismiss": "忽略" + }, + "model": { + "activeModel": "目前模型", + "activeModelDescription": "驅動新增 Agent 會話的模型。", + "selectModel": "選擇模型", + "reasoningEffort": "推理強度", + "reasoningEffortDescription": "強度越高思考越久,越低回應越快。該選擇隨所選模型變化,並在換模型時保持相對強度;部分 Agent 框架可能會用近似層級替代不支援的強度。對後續請求生效。", + "providers": "模型服務商", + "encryptionUnavailable": "安全金鑰儲存空間不可用。在系統鑰匙圈解鎖或授權之前,無法儲存 API 金鑰。", + "errors": { + "testFailed": "無法測試模型服務商連線。", + "codexSignInFailed": "無法登入 Codex。", + "codexSignOutFailed": "無法登出 Codex。", + "claudeTokenSaveFailed": "無法儲存 Claude token。", + "claudeSignInFailed": "無法登入 Claude。", + "claudeSignOutFailed": "無法登出 Claude。", + "claudeDisconnectFailed": "無法中斷 Claude 與 Open Science 的連線。", + "providerSaveFailed": "無法儲存模型服務商。", + "codexSignOutIncomplete": "Codex 登出未完成。重試一次。", + "claudeTokenSaveRetry": "無法儲存 Claude token。重試一次。", + "claudeSignInRetry": "無法登入 Claude。重試一次。", + "claudeSignOutIncomplete": "Claude 登出未完成。重試一次。", + "codexReimportFailed": "無法重新匯入 Codex 登入資訊。" + }, + "list": { + "incompatible": "與目前的 Agent 框架不相容。", + "empty": "還沒有模型服務商。新增一個來選擇模型來源。", + "testFailedWithMessage": "測試失敗:{{message}}", + "testFailed": "連線測試失敗。", + "claudeSubscription": "Claude 訂閱", + "testing": "測試中…", + "connectionVerified": "連線已驗證", + "authImported": "認證資訊已匯入 Open Science", + "codexStoredSeparately": "Codex 登入資訊由 Open Science 單獨儲存", + "notSignedIn": "未登入", + "keyNeedsReentry": "金鑰需重新輸入", + "checkCodexLogin": "檢查 Codex 登入", + "testConnection": "測試連線", + "browserLoginHint": "正在開啟瀏覽器以登入…未開啟?取消並使用 Setup Token。", + "tokenPrefix": "Token:{{masked}}", + "modelPrefix": "模型:{{name}}", + "keyPrefix": "金鑰:{{masked}}", + "modelsCount": "{{count}} 個模型", + "expiresLabel": "到期:" + }, + "testFailure": { + "auth": "測試失敗:認證被拒絕 —— 檢查 API 金鑰。", + "network": "測試失敗:無法連上該端點 —— 檢查基礎 URL 和網路連線。", + "badUrl": "測試失敗:基礎 URL 看起來無效。", + "modelNotFound": "測試失敗:找不到所設定的模型。", + "timeout": "測試失敗:連線逾時。", + "serverError": "測試失敗:閘道或上游服務暫時不可用。", + "withMessage": "測試失敗:{{message}}", + "withMessageAndStatus": "測試失敗:{{message}}(HTTP {{status}})" + }, + "type": { + "custom": "自訂", + "official": "官方" + }, + "compatibility": { + "activeModelIncompatible": "目前模型與 {{framework}} 不相容", + "codexBridgeUnsupported": "該模型不支援透過 Codex 橋接使用", + "codexBridgeDescription": "該模型不支援透過 Codex 的 Chat Completions 橋接使用。為 Codex 會話另選一個模型。", + "switchGuidance": "在下方選一個相容的模型,或在上方切換 Agent 框架 —— 否則該框架上的對話無法開始。" + }, + "effort": { + "default": "預設" + }, + "actions": { + "reimportCodexLogin": "重新匯入 Codex 登入", + "signIn": "登入", + "signOut": "登出", + "signInWithBrowser": "以瀏覽器登入", + "useSetupToken": "使用設定權杖", + "cancelSignIn": "取消登入", + "disconnect": "從 Open Science 中斷連接", + "edit": "編輯", + "delete": "刪除" + }, + "endpoint": { + "speaksThe": "使用 {{protocol}} 協定" + }, + "refresh": { + "loaded_other": "已從供應商載入 {{count}} 個模型。", + "failed": "無法取得模型:{{reason}}。將使用內建清單。", + "requestFailed": "請求失敗" + } + }, + "providerForm": { + "apiKey": "API 金鑰", + "getApiKey": "取得 API 金鑰", + "keyPlaceholderStored": "{{masked}} — 留空則保持不變", + "storedKeyFallback": "已儲存的金鑰", + "keyNeedsReentry": "已儲存的金鑰無法解密。重新輸入後才能繼續。", + "providerType": "服務商類型", + "name": "名稱", + "providerName": "服務商名稱", + "namePlaceholder": "例如:我的閘道", + "baseUrl": "基礎 URL", + "baseUrlHelp": "閘道根位址;結尾的 /v1 會自動補上。在下方選擇與該端點相符的 API 格式。", + "apiFormat": "API 格式", + "apiFormatHelp": "該閘道使用的對話 API。Claude Code 用 /v1/messages,OpenCode 接受 Messages 或 Chat Completions,Codex 用 /v1/responses。模型服務商只能在支援其格式的 Agent 框架下選用。", + "supportedModelsHelp": "隨應用內建。從廠商重新整理可取得最新清單。新增後在「目前模型」選擇器中選用。", + "model": "模型", + "modelOptionalOverride": "(可選覆寫)", + "modelPlaceholderClaudeDefault": "留空則使用 Claude 的預設模型", + "contextWindow": "上下文視窗", + "endpoint": "端點", + "supportedModels": "支援的模型", + "refreshing": "重新整理中…", + "refreshFromVendor": "從廠商重新整理", + "imageInput": "圖像輸入", + "imageInputDescription": "僅在該閘道和模型接受圖像內容時啟用。", + "supportsImageInput": "支援圖像輸入", + "reasoningEffort": "推理強度", + "reasoningEffortDescription": "選擇該模型實際接受的強度層級。Open Science 會把五個相對強度對應到這些層級,再依下方的請求格式送出所選層級。模型不接受強度參數時請關閉。", + "supportsReasoningEffort": "支援推理強度", + "reasoningEffortLevels": "推理強度層級", + "requestFormat": "請求格式", + "reasoningEffortRequestFormat": "推理強度請求格式", + "codexAuth": "Codex 認證方式", + "codexShared": "匯入現有的 Codex 登入", + "codexIsolated": "用 Open Science 登入", + "codexSharedHint": "將 Codex 認證資訊,以及相容時目前服務商的非機密回環路由,複製到 Open Science 應用資料中。其他全域設定、技能和會話不會匯入。", + "codexIsolatedHint": "在 Open Science 應用資料中單獨儲存一份 Codex 登入,不改動你的 Codex CLI 設定。", + "claudeAuth": "Claude 認證方式", + "claudeShared": "使用現有的 Claude 設定(建議)", + "claudeIsolated": "單獨登入(隔離)", + "claudeSharedHint": "建議。使用 ~/.claude 中你現有的 Claude 登入。透過瀏覽器 OAuth 登入一次,即可在所有 Claude 工具中使用。", + "claudeIsolatedHint": "進階。透過瀏覽器登入,並在 Open Science 中單獨儲存一份 Claude 登入,與你個人的 Claude 設定完全隔離。", + "claudeSharedDetail": "透過瀏覽器 OAuth 登入。設定卡片會開啟瀏覽器,讓你用 Claude 帳號登入。憑證儲存在 ~/.claude 中。", + "claudeIsolatedDetail": "在終端機執行 claude setup-token,把 token 貼到下方。它會加密儲存在應用自有的 Claude 設定目錄中;不會讀取或寫入 ~/.claude。", + "claudeIsolatedPasteHint": "儲存後在設定卡片中貼上 token —— 精靈的「測試並繼續」流程會為你完成登入。", + "security": { + "availableTitle": "你的金鑰不會外流。", + "availableDescription": "它只儲存在這台裝置上,絕不會上傳到 Open Science。由作業系統的安全儲存保護,僅在你發出請求時傳送給所選的模型服務商。", + "unavailableTitle": "安全儲存不可用。", + "unavailableDescription": "在作業系統憑證保管庫可用之前,Open Science 不會儲存 API 金鑰。解鎖或授權系統鑰匙串後重試。" + }, + "groups": { + "codex": "Codex 訂閱", + "claude": "Claude 訂閱", + "api": "官方 API", + "other": "其他" + }, + "kinds": { + "customLabel": "自訂閘道", + "codexDescription": "使用現有的 Codex 設定,或用單獨的 Open Science 設定登入。", + "claudeDescription": "使用現有的 Claude 設定,或用單獨的 Open Science 設定登入。", + "officialDescription": "API 金鑰 —— 已內建模型清單", + "customDescription": "為 Messages 或 Chat Completions 端點填寫基礎 URL、金鑰和模型" + }, + "validation": { + "baseUrlRequired": "必須填寫基礎 URL。", + "modelRequired": "必須填寫模型。", + "keyRequired": "必須填寫 API 金鑰。", + "contextWindowInvalid": "上下文視窗必須是正整數 token 數。" + } + }, + "claudeSignIn": { + "title": "使用 Anthropic 帳號登入", + "description": "使用 claude setup-token 產生的長期 OAuth token。該 token 會加密儲存在 Open Science 應用儲存中,不會讀取或寫入 ~/.claude。完整流程見 Anthropic 的 setup-token 指南。", + "browserPending": "正在開啟瀏覽器登入…在瀏覽器中完成後,此視窗會自動關閉。沒有開啟,或更想用 token?在下方貼上一個。", + "step1": "第 1 步 · 執行", + "copyCommand": "複製命令", + "copied": "已複製", + "copy": "複製", + "pasteLabel": "貼上 setup-token 輸出的 token", + "pasteLabelStep2": "第 2 步 · 貼上 setup-token 輸出的 token", + "tokenInputLabel": "Claude 設定權杖", + "signingIn": "登入中…", + "signIn": "登入", + "tokenSaveFailed": "無法儲存 Claude token。" + }, + "validation": { + "ok": "連線成功。", + "network": "無法連上該端點。檢查網路連線和基礎 URL。", + "auth": "認證失敗。檢查 API 金鑰。", + "modelNotFound": "該模型被拒絕。檢查此閘道下的模型名稱。", + "badUrl": "基礎 URL 無效。填寫完整 URL,例如 https://gateway.example/v1。", + "timeout": "請求逾時並已中止。", + "incompatible": "該模型服務商與目前的 Agent 框架不相容。", + "serverError": "閘道或上游服務暫時不可用。稍後重試。", + "unknown": "驗證失敗,原因不明。", + "withDetail": "{{base}}({{detail}})", + "withStatus": "{{base}}(HTTP {{status}})" + }, + "breadcrumb": { + "newSkill": "新增技能", + "uploadSkills": "上傳技能", + "importFromGitHub": "從 GitHub 匯入", + "importInstalledSkills": "匯入已安裝的技能", + "addProvider": "新增模型服務商", + "packageMirror": "軟體套件鏡像來源", + "addConnector": "新增連接器", + "addSshHost": "新增 SSH 主機", + "newSpecialist": "新增專家", + "editSpecialist": "編輯專家", + "editNamed": "編輯 {{name}}", + "connectorFallbackName": "連接器" + }, + "about": { + "title": "關於", + "ariaLabel": "應用版本", + "checking": "檢查中…", + "checkNow": "立即檢查", + "checkingForUpdates": "正在檢查更新…", + "available": "新版本 {{version}} 可用", + "downloadingStatus": "正在下載… {{percent}}%", + "downloaded": "更新已下載完成,開啟安裝程式即可完成升級", + "upToDate": "已是最新版本", + "checkFailed": "更新檢查失敗", + "downloadingButton": "正在下載 {{percent}}%", + "updateReady": "更新已就緒", + "updateTo": "更新到 {{version}}" + }, + "appIcon": { + "title": "應用圖示", + "description": "選擇應用視窗中顯示的內建圖示,在 macOS 上也會用於 Dock。", + "note": "新圖示會立即出現在應用視窗和 macOS 的 Dock 中。Finder、檔案總管、工作列或開始功能表中的圖示屬於已安裝的應用,不會改變。" + }, + "general": { + "appearance": { + "title": "外觀", + "description": "設定應用程式的外觀與語言。選擇「系統」會跟隨裝置,其餘選項保持固定。你的選擇會記錄在這台裝置上。", + "theme": { + "label": "主題", + "description": "跟隨系統設定,或固定為淺色、深色。" + }, + "language": { + "label": "語言", + "description": "跟隨系統設定,或指定一種語言。「系統」只在啟動時偵測一次,變更裝置語言後需重新開啟應用程式才會生效。" + } + }, + "windowBehavior": { + "title": "視窗行為", + "description": "設定標題列關閉按鈕的行為。", + "closeAction": { + "label": "關閉視窗時", + "description": "每次詢問、讓 {{appName}} 保留在系統匣中執行,或直接結束應用程式。", + "ask": "每次詢問" + } + }, + "notifications": { + "title": "通知", + "description": "當你不在應用程式中時,任務完成、失敗或等待核准時會收到桌面通知。", + "tasks": { + "label": "任務通知", + "description": "點選通知會把 {{appName}} 帶到前景並開啟對應任務。", + "toggleLabel": "切換任務通知" + }, + "note": "只有在你使用其他應用程式時才會出現通知。你主動取消的任務,以及應用程式會自動重試的失敗,都不會發出通知。首次出現通知時,作業系統可能會詢問通知權限。" + }, + "diagnostics": { + "title": "診斷", + "description": "檢視這台裝置的執行記錄檔 —— 它記錄了應用程式正在做什麼,便於排查問題。", + "logFile": { + "label": "記錄檔", + "pathLabel": "記錄檔路徑", + "openFailed": "無法開啟記錄檔。", + "revealFailed": "無法在資料夾中顯示記錄檔。" + }, + "issueHint": "遇到問題?在 GitHub 上提交 issue 並附上上面的記錄檔。記錄檔保存在這台裝置上,不會自動傳送;其中可能包含本機檔案路徑,分享前請先檢視內容。" + }, + "cli": { + "title": "命令列工具", + "description": "安裝 open-science 命令,即可在終端機裡啟動、停止和檢查後端服務,然後完全在瀏覽器中使用。", + "rowLabel": "open-science", + "install": "安裝命令", + "uninstall": "移除命令", + "pathLabel": "命令列工具路徑", + "updateFailed": "無法更新命令列工具。", + "usage": "安裝完成後,執行 open-science start 啟動後端並開啟帶驗證的網址,再用 open-science stop 關閉。此外還提供 statusurl 兩個命令。" + }, + "community": { + "title": "{{appName}} 用得還順手嗎?", + "description": "它免費且開源。在 GitHub 上點亮 Star 能讓更多人發現它,也歡迎來 Discord 和 X 和我們一起開放地建構。謝謝你在這裡。", + "ariaLabel": "社群", + "discordLabel": "加入 {{appName}} 的 Discord 社群", + "xLabel": "在 X 上追蹤 {{appName}}", + "websiteLabel": "開啟 {{appName}} 官方網站", + "website": "官方網站" + } + }, + "runtimes": { + "title": "Notebook 執行環境", + "description": "啟用每種 Notebook 語言可執行的環境。應用自帶的環境預設開啟;啟用你自己的解譯器可讓 Agent 使用它們。", + "provenance": { + "appManaged": "應用託管", + "agentCreated": "Agent 建立", + "conda": "Conda:{{name}}", + "system": "系統" + }, + "ready": "就緒", + "notRunnable": "無法執行", + "downloadingManaged": "正在下載託管執行環境…", + "installedAndReady": "已安裝並就緒", + "managedNotSetUp": "託管執行環境尚未設定", + "detecting": "正在偵測執行環境…", + "allowPackageInstall": "允許安裝套件", + "allowPackageInstallDescription": "允許 Open Science 向該環境安裝套件。安裝會寫入你自己的環境,而不是應用託管的儲存空間。", + "pickRscript": "選擇你的 Rscript 可執行檔 —— 例如 Rscript.exe(Windows)或 bin/Rscript(macOS/Linux)。請選擇檔案,而不是資料夾。", + "pickPython": "選擇你的 Python 解譯器可執行檔 —— 例如 python.exe(Windows)或 bin/python(macOS/Linux)。請選擇檔案,而不是資料夾。", + "resetRuntime": "重設執行環境", + "retrySetup": "重試設定", + "downloadAndSetUp": "下載並設定", + "recheck": "重新偵測", + "cancel": "取消", + "forceStop": "停止正在執行的工作", + "drainDisable": "目前工作完成後停用", + "disableTitle": "要停用 {{label}} 嗎?", + "disableImpact": "它正被 {{total}} 個活躍工作階段使用 —— {{running}} 個執行中,{{idle}} 個閒置。停用會讓正在執行的儲存格先執行完,然後關閉它的核心;這些工作階段需要切換到其他執行環境才能繼續工作。", + "packages": { + "button": "套件", + "dialogTitle": "{{label}} 中的套件", + "dialogDescription": "該環境中已安裝的套件。", + "filterPlaceholder": "篩選套件…", + "filterAriaLabel": "篩選套件", + "count": "已顯示 {{visible}} / {{total}}", + "condaSplit": "{{conda}} 個 conda,{{pypi}} 個 pypi", + "loading": "正在列出套件…", + "retry": "重試", + "columnName": "名稱", + "columnVersion": "版本", + "columnBuild": "建置編號", + "columnChannel": "頻道", + "noMatch": "沒有符合「{{filter}}」的套件。", + "noneInstalled": "未安裝任何套件。", + "close": "關閉", + "listFailed": "無法列出套件。" + }, + "errors": { + "recheck": "無法重新偵測執行環境。", + "change": "無法變更該執行環境。", + "auth": "無法變更套件安裝授權。", + "add": "無法新增該解譯器。", + "refresh": "無法重新整理執行環境就緒狀態。", + "reset": "無法重設該執行環境。", + "cancel": "無法取消該設定流程。" + } + }, + "agent": { + "frameworkTitle": "Agent 框架", + "frameworkDescription": "選擇驅動會話的編碼 Agent 後端。選取一張卡片即可切換;切換會開啟一個全新的 Agent 會話,已開啟的對話會把其記錄重播到新後端。目前使用中的執行環境無法卸載 —— 請先切換到另一個。", + "detecting": "偵測中…", + "redetect": "重新偵測", + "detectionFailed": "有一個或多個 Agent 執行環境無法偵測到。", + "installerFailed": "安裝程式無法啟動。", + "repairIssuesLabel": "Agent 執行環境修復問題", + "installBlockersLabel": "Agent 安裝阻擋項", + "selectedAgentFallback": "所選 Agent", + "cannotBeAccessed": "無法存取 {{agent}}。", + "repairBeforeUse": "使用前請先修復所選的 Agent。", + "groups": { + "installed": "已安裝", + "available": "可安裝" + }, + "blockers": { + "systemRequirements": "不符合系統需求", + "installNetwork": "安裝所需網路不可用" + }, + "frameworks": { + "claude": { + "description": "Anthropic 面向終端的 Agent 編碼工具。", + "notReadyHint": "在下方安裝 Claude Agent,或手動安裝後重新偵測。" + }, + "opencode": { + "description": "面向終端的開源編碼 Agent。", + "notReadyHint": "此框架需要 OpenCode。在下方安裝,或手動安裝(參見 opencode.ai/docs)後重新偵測。" + }, + "codex": { + "description": "OpenAI 的編碼 Agent,透過 Codex ACP 轉接器接入。", + "notReadyHintRepair": "轉接器或與其配對的原生 Codex 執行環境未通過偵測。在下方重新安裝託管的配對組合,或修復你的手動安裝後重新偵測。", + "notReadyHintInstall": "此框架需要 Codex ACP。在下方安裝,或手動安裝後重新偵測。" + } + }, + "installSources": { + "managed": { + "label": "應用託管下載(建議)", + "claudeDescription": "下載自帶執行環境的 Claude —— 無需 Node.js 或 npm。", + "codexDescription": "下載自帶執行環境的 Codex ACP 執行時 —— 無需 Node.js 或 npm。", + "opencodeDescription": "下載自帶執行環境的 OpenCode —— 無需 Node.js 或 npm。" + }, + "npm": { + "label": "npm(全域安裝)" + }, + "officialScript": { + "ps1": "官方 install.ps1", + "sh": "官方 install.sh", + "generic": "官方安裝指令碼" + }, + "menuLabel": "安裝來源", + "npmNotFound": "找不到 npm" + }, + "card": { + "install": "安裝", + "repair": "修復", + "installing": "安裝中…", + "starting": "啟動中…", + "active": "使用中", + "installed": "已安裝", + "hideLog": "隱藏日誌", + "showLog": "顯示日誌", + "installProgress": "安裝進度", + "installLog": "安裝日誌" + }, + "progress": { + "resolving": "解析中…", + "downloading": "下載中…", + "downloadingSized": "下載中 —— {{received}} / {{total}} MB", + "extracting": "解壓中…", + "installing": "安裝中…" + }, + "uninstall": { + "notManaged": "在你的系統中找到了 {{label}},但它不由本應用管理,因此無法從這裡卸載。請用你當初安裝它的工具移除 —— 例如 `{{command}}`、你的套件管理器,或從 PATH 中刪除它 —— 然後重新偵測。", + "active": "{{label}} 是目前使用中的 Agent 框架,無法卸載。請先切換到其他框架,再執行卸載。", + "promptInFlight": "有任務正在執行 —— 請等它結束後再卸載。", + "uninstall": "卸載", + "uninstalling": "卸載中…", + "title": "卸載 {{name}}?", + "description": "這會移除本應用下載並管理的 {{name}} 執行環境。你自己另行安裝的 {{name}} 不受影響。你隨時可以在這裡重新安裝。", + "cancel": "取消" + }, + "repairDialog": { + "title": "{{name}} 需要修復", + "description": "請先修復該 Agent,然後再選擇它。" + }, + "switchDialog": { + "title": "切換到 {{name}}?", + "description": "對話無法在另一個後端上恢復,因此切換會開啟一個新的 Agent 工作階段。已開啟的對話會保留現有訊息,並把記錄重播給 {{name}},讓它接著之前的進度繼續(工具狀態不會帶過去)。新對話不受影響。", + "switch": "切換" + } + }, + "skills": { + "detail": { + "updatedToday": "今天更新", + "updatedDaysAgo_other": "{{count}} 天前更新", + "sourceImported": "已匯入", + "sourcePersonal": "個人", + "sourceFeatured": "精選", + "toggleAriaLabel": "開關 {{name}}", + "files": "檔案", + "details": "詳情", + "author": "作者", + "license": "授權條款", + "thirdParty": "第三方軟體、內容、條款與資訊" + }, + "editor": { + "slugRequired": "技能 ID 不能為空。", + "slugCharset": "只能使用小寫字母、數字和連字號。", + "slugReserved": "不能以 {{prefixes}} 開頭。", + "slugTaken": "已存在使用該 ID 的技能。", + "identity": "識別資訊", + "identityDescription": "這個技能在目錄中以及對 Agent 呈現的樣子。", + "name": "名稱", + "nameAriaLabel": "技能名稱", + "namePlaceholder": "例如:變更記錄風格", + "id": "技能 ID", + "idAriaLabel": "技能 ID", + "idHintNew": "用作資料夾名稱 —— 小寫 a–z、0–9、連字號。建立後不可更改。", + "idHintLocked": "技能 ID 在建立後即固定。", + "description": "描述", + "descriptionAriaLabel": "技能描述", + "descriptionPlaceholder": "一句話 —— 這個技能教給 Agent 什麼,什麼時候適用?", + "descriptionHint": "Agent 依據這句話判斷何時使用該技能 —— 寫具體些。", + "content": "內容", + "contentDescription": "技能被呼叫時展示給 Agent 的 Markdown。", + "contentModeAriaLabel": "內容模式", + "modeWrite": "撰寫", + "modeUpload": "上傳", + "bodyAriaLabel": "技能內文", + "bodyPlaceholder": "# 說明\n\n給 Agent 的分步指引…", + "bodyHint": "貼上完整的 SKILL.md —— 如果開頭有 --- 中介資料區塊,上面的欄位會自動填入。", + "metadataAriaLabel": "技能中介資料", + "savedMetadata": "已儲存的中介資料", + "clearMetadataAriaLabel": "清除技能中介資料", + "clear": "清除", + "dropToUpload": "拖到此處上傳", + "uploadContent": "上傳 SKILL.md 或文字檔", + "uploadContentHint": "其內容會填入編輯器;切回「撰寫」即可調整。", + "references": "參考檔案", + "referencesDescription": "技能在執行時可讀取的輔助檔案(指令碼、範本、資料)。", + "dropReferences": "拖入參考檔案", + "addReferencesAriaLabel": "新增參考檔案", + "referencesBrowse": "拖入參考檔案,或點擊瀏覽", + "referencesHint": "儲存在技能的 references/ 下。", + "removeReferenceAriaLabel": "移除 {{path}}", + "saving": "儲存中…", + "save": "儲存", + "publish": "發布", + "loading": "載入中…" + }, + "panel": { + "filterAll": "全部", + "filterFeatured": "精選", + "filterImported": "已匯入", + "filterPersonal": "個人", + "groupFeatured": "精選", + "groupFeaturedSubtitle": "隨應用附帶的科研技能。", + "groupImported": "已匯入", + "groupImportedSubtitle": "你從 GitHub 加入的技能。", + "groupPersonal": "個人", + "groupPersonalSubtitle": "你自訂的技能。", + "conversationImports": "對話匯入", + "conversationImportsDescription": "選擇對話可以向 Open Science 匯入什麼。", + "skillPackages": "技能套件", + "skillPackagesDescription": "讓 Agent 辨識附加的 .zip 與 .skill 套件,並在匯入前詢問你。", + "filterAriaLabel": "依來源篩選技能", + "conversationImportToggleAriaLabel": "開關對話中的技能匯入", + "searchAriaLabel": "搜尋技能", + "searchPlaceholder": "搜尋技能…", + "addSkill": "新增技能", + "writeFromScratch": "從零開始寫", + "writeFromScratchHint": "開啟技能建立器", + "uploadSkills": "上傳技能", + "uploadSkillsHint": "挑選 SKILL.md 檔案", + "importFromGitHub": "從 GitHub 匯入", + "importFromGitHubHint": "從儲存庫新增技能", + "importInstalledSkills": "匯入已安裝的技能", + "importInstalledSkillsHint": "掃描全域技能資料夾", + "editAriaLabel": "編輯 {{name}}", + "deleteAriaLabel": "刪除 {{name}}", + "toggleAriaLabel": "開關 {{name}}", + "emptyPersonal": "建立一個技能,把你常用的流程教給 Claude。", + "emptyImported": "還沒有匯入任何技能。", + "emptySearch": "沒有技能符合你的搜尋。", + "desktopOnlyFallback": "已安裝技能的匯入功能僅在桌面應用程式中可用。" + }, + "githubImport": { + "title": "從 GitHub 匯入", + "description": "預覽一個儲存庫或技能資料夾(owner/repo、owner/repo@ref,或 github.com 連結),然後挑選要匯入的技能。", + "inputAriaLabel": "GitHub 技能連結或儲存庫", + "inputPlaceholder": "owner/repo、owner/repo@ref,或 github.com 連結", + "working": "處理中…", + "preview": "預覽", + "found_other": "找到 {{count}} 個技能", + "selectAllAriaLabel": "全選", + "selectAll": "全選", + "invert": "反選", + "importSelected": "匯入所選({{count}})", + "selectSkillAriaLabel": "選擇 {{name}}", + "previewSkillAriaLabel": "預覽 {{name}}", + "imported": "已匯入", + "importedHeading": "已匯入的技能", + "importedEmpty": "還沒有匯入任何技能。你匯入過的儲存庫會顯示在這裡。", + "noSkillsFound": "那個儲存庫裡沒有找到技能。", + "scanFailed": "掃描失敗。", + "importedCount_other": "已匯入 {{count}} 個技能。", + "importedThenFailed": "已匯入 {{count}} 個,隨後失敗。" + }, + "importApproval": { + "title": "匯入技能套件?", + "description": "Agent 請求從 {{attachment}} 匯入。請檢查並明確選擇允許 Open Science 安裝的內容。", + "found_other": "找到 {{count}} 個技能", + "selectAllAriaLabel": "全選", + "selectAll": "全選", + "invert": "反選", + "selectCandidateAriaLabel": "選擇 {{name}}", + "alreadyImported": "已匯入", + "updatesExisting": "將更新現有技能", + "preview": "預覽", + "notImportable": "無法匯入", + "importSelected": "匯入所選", + "importCount_other": "匯入 {{count}} 個技能" + }, + "upload": { + "title": "上傳技能", + "description": "從本機的 SKILL.md 檔案或 .zip / .skill 壓縮檔新增技能。可一次選擇多個檔案,單一壓縮檔也可以包含多個技能。", + "dropToUpload": "拖放到此處上傳", + "inputAriaLabel": "上傳技能檔案", + "reading": "正在讀取…", + "dropHint": "拖放檔案或點按上傳", + "formatHint": ".md 檔案需要在 YAML frontmatter 中寫明 name 與 description。.zip 或 .skill 壓縮檔必須包含 SKILL.md。新增前你會先確認一次。", + "writeInstead": "改為從零撰寫", + "confirmTitle": "確認匯入", + "confirmDescription": "選擇要新增的技能。在你匯入之前不會寫入任何內容。", + "found_other": "找到 {{count}} 個技能", + "selectAllAriaLabel": "全選", + "selectAll": "全選", + "invert": "反選", + "selectCandidateAriaLabel": "選擇 {{name}}", + "previewCandidateAriaLabel": "預覽 {{name}}", + "alreadyImported": "已匯入", + "nameExists": "名稱已存在", + "importing": "正在匯入…", + "importSelected": "匯入所選({{selected}})", + "chooseDifferent": "重新選擇檔案", + "skipped_other": "已略過 {{count}} 個技能", + "summary": "已匯入 {{imported}} 個 · 略過 {{skipped}} 個 · 失敗 {{failed}} 個", + "errorTooLarge": "{{fileName}}:檔案過大(上限 {{limit}})。", + "errorNoSkills": "{{fileName}}:壓縮檔中未找到技能。", + "errorFile": "{{fileName}}:{{message}}", + "errorNoName": "{{fileName}}:需要在 YAML frontmatter 中寫明 name。", + "errorUnsupported": "{{fileName}}:不支援的檔案,請上傳 .md 檔案或 .zip / .skill 壓縮檔。", + "errorPreviewTooLarge": "{{fileName}}:預覽內容超過 {{limit}} 上限,但仍可匯入。", + "errorSelectionTooLarge": "所選檔案過大({{total}}),每次最多上傳 {{limit}}。" + }, + "candidatePreview": { + "fallbackTitle": "技能預覽", + "loadingSource": "正在載入候選內容…", + "closeAriaLabel": "關閉預覽", + "loading": "正在載入預覽…", + "metadata": "中介資料", + "files": "檔案" + }, + "agentHomeImport": { + "title": "匯入已安裝的技能", + "scanOne": "掃描這台電腦上的 {{path}}。勾選要複製到 Open Science 的技能;原檔案保持不動。", + "scanTwo": "掃描這台電腦上的 {{path}}{{frameworkPath}}。勾選要複製到 Open Science 的技能;原檔案保持不動。", + "scanning": "掃描中…", + "rescan": "重新掃描", + "skillsFound_other": "找到 {{count}} 個技能", + "selectAllAriaLabel": "全選已安裝的技能", + "selectAll": "全選", + "invert": "反選", + "importing": "匯入中…", + "importSelected": "匯入所選({{count}})", + "selectSkillAriaLabel": "選擇 {{name}}", + "previewSkillAriaLabel": "預覽 {{name}}", + "sourceShared": "共用", + "imported": "已匯入", + "empty": "在掃描的全域資料夾中沒有找到已安裝的技能。", + "scanFailed": "掃描失敗。", + "importedCount_other": "已匯入 {{count}} 個技能。", + "importedWithFailures": "已匯入 {{count}} 個;{{failureCount}} 個失敗。{{error}}", + "importFailed": "無法匯入所選技能。" + } + }, + "toolPermission": { + "allow": "一律允許", + "ask": "需要核准", + "block": "封鎖" + }, + "network": { + "mirror": { + "title": "套件鏡像", + "description": "Notebook 環境在安裝或更新時,從哪裡取得 conda 和 Python 套件。", + "statusUnconfigured": "未設定 —— 套件來自公共來源(conda.anaconda.org、pypi.org)", + "statusConfigured": "正在從 {{hosts}} 取得套件", + "configure": "設定", + "edit": "編輯", + "condaChannel": "Conda 頻道鏡像", + "pypiIndex": "Python 套件索引(pip)", + "caBundle": "CA 憑證包路徑", + "optional": "(選填)", + "caBundleHint": "用於企業 TLS 代理的 PEM 憑證包;conda、pip 和 R 下載都會信任它。", + "viewMirrors": "查看可用鏡像", + "cancel": "取消", + "save": "儲存", + "saving": "儲存中…", + "saveFailed": "無法儲存套件鏡像。" + } + }, + "storage": { + "appStorage": { + "title": "應用儲存", + "description": "Open Science 需要對其私有設定目錄的寫入權限。", + "revealFinder": "在 Finder 中顯示", + "revealFolder": "在資料夾中顯示", + "revealFailed": "無法顯示應用儲存。", + "checking": "檢查中…", + "checkAgain": "重新檢查", + "continueToAgent": "繼續修復 Agent" + }, + "location": { + "title": "資料位置", + "description": "Open Science 在這台裝置上存放專案、產物和其他應用資料的位置。", + "changeLocation": "變更位置", + "loading": "載入中…", + "label": "位置", + "pathAriaLabel": "資料根目錄路徑", + "onDisk": "佔用磁碟 {{size}}", + "onDiskDefault": "佔用磁碟 {{size}} · 預設位置", + "newLocation": "新位置", + "pathPlaceholder": "/path/to/new/location", + "browse": "瀏覽…", + "moveBackToDefault": "或移回預設位置 ({{path}})", + "defaultUnusable": "預設位置不可用。", + "willStoreIn": "資料將存放在 {{path}}", + "adoptNotice": "該資料夾已包含 Open Science 資料,將按原樣使用(不合併)——你目前的資料夾會保留,可以隨時切換回去。應用將重新啟動。", + "moveNotice": "你現有的資料(約 {{size}})將被移動到新位置——檔案會一起遷移,目前資料夾中不會留下任何內容。", + "runtimeRebuildNotice": "Python/R 環境會在首次使用時於新位置重建(不遷移)。", + "switching": "切換中…", + "useThisFolder": "使用該資料夾", + "switchFailed": "無法切換到該資料夾。" + }, + "usage": { + "title": "磁碟佔用", + "empty": "暫無資料。", + "total": "總計", + "available": "磁碟可用空間", + "legendTitle": "{{label}}:{{size}}", + "categories": { + "artifacts": "產物", + "uploads": "上傳", + "runtime": "執行環境", + "notebooks": "Notebook", + "workspaces": "工作階段工作區" + } + }, + "changeWarning": { + "title": "變更資料位置?", + "description": "你可以把 Open Science 的資料移動到這台裝置上的其他資料夾。" + }, + "migration": { + "phases": { + "scan": "正在掃描檔案…", + "copy": "正在複製檔案…", + "verify": "正在校驗…", + "delete": "正在清理…" + }, + "detectingTitle": "正在檢查執行中的工作階段…", + "detectingDescription": "請稍候。", + "confirmTitle": "移動應用資料?", + "confirmDescription": "開始移動會中斷下列執行中的工作階段,並重新啟動應用。", + "interruptAndMove": "中斷並移動", + "movingTitle": "正在移動應用資料…", + "preparing": "準備中…", + "elapsedAriaLabel": "已用時間", + "elapsed": "已用 {{time}}", + "doNotQuit": "完成之前請不要退出 Open Science 或關閉電腦。", + "doneTitle": "資料已複製", + "doneDescription": "重新啟動後即切換到新位置。在此之前不會有任何變化——選擇「保留目前位置」可留在原處並丟棄副本。", + "discarding": "正在丟棄…", + "keepCurrentLocation": "保留目前位置", + "restartNow": "立即重新啟動", + "committingTitle": "正在切換…", + "committingDescription": "正在收尾並重新啟動。可能需要一點時間,請不要退出。", + "switchoverTitle": "資料已移動——請重新啟動", + "failedTitle": "移動失敗", + "genericError": "出了點問題。請關閉後重試。" + }, + "adoptDialog": { + "title": "使用該資料夾?", + "description": "Open Science 將重新啟動並按原樣使用該資料夾——其內容不會與你目前的資料合併,缺少的內容會顯示為不可用。你目前的資料夾保持不變,可以隨時切換回去。" + } + }, + "connectors": { + "approval": { + "title": "允許外部請求?", + "description": "Claude 想要呼叫一個會把資料傳送到外部服務的連接器工具。僅在你信任該連接器處理目前請求時才批准。", + "connector": "連接器", + "tool": "工具", + "args": "參數", + "deny": "拒絕", + "allowOnce": "允許一次", + "thisConversation": "此工作階段", + "thisProject": "此專案", + "global": "全域" + }, + "detail": { + "featured": "精選", + "toggleAriaLabel": "啟用或停用 {{name}}", + "skipApprovals": "跳過核准", + "skipApprovalsDescription": "允許 Claude 使用該連接器的所有工具,且每次都不再顯示核准卡片。", + "skipApprovalsAriaLabel": "為 {{name}} 跳過核准", + "tools": "工具", + "toolsDescription": "Claude 可以用該連接器做什麼", + "noTools": "該連接器沒有工具。", + "toolPermissionAriaLabel": "{{name}} 的權限", + "noToolDescription": "該工具未提供描述。", + "details": "詳細資料", + "thirdParty": "第三方軟體、內容、條款與資訊", + "terms": "條款", + "askFallback": "當沒有工作階段、專案或全域權限適用時詢問。", + "rememberedApprovals_other": "已記住的核准:{{count}}", + "rememberedBlocked": "目前已封鎖", + "rememberedUnnecessary": "目前無需使用", + "scopeGlobal": "全域", + "scopeProject": "專案", + "scopeSession": "工作階段", + "managePermissions": "管理權限" + }, + "panel": { + "contactEmail": "聯絡電子郵件", + "contactEmailDescription": "在獲得允許時,代表你發出的請求會把該電子郵件提供給需要聯絡電子郵件的研究資料服務(例如 NCBI、EBI 和 OurResearch 營運的服務)。", + "contactEmailAriaLabel": "聯絡電子郵件", + "apiKeyAriaLabel": "NCBI API 金鑰", + "apiKeyPlaceholder": "選填的 API 金鑰", + "apiKeyHint": "提高 NCBI 速率上限(選填)。", + "save": "儲存", + "clearKey": "清除金鑰", + "notSet": "未設定", + "edit": "編輯", + "filterAriaLabel": "依分組篩選連接器", + "filterAll": "全部", + "filterFeatured": "精選", + "filterDirectory": "目錄", + "filterCustom": "自訂", + "searchAriaLabel": "搜尋連接器", + "searchPlaceholder": "搜尋連接器…", + "addConnector": "新增連接器", + "localCommand": "本機指令", + "localCommandDescription": "透過指令執行 MCP 伺服器", + "remoteServer": "遠端伺服器", + "remoteServerDescription": "連線到 MCP 伺服器 URL", + "featuredSubtitle": "來自 Anthropic 的研究連接器", + "directorySubtitle": "與 Claude Connectors Directory 同步", + "customSubtitle": "你新增的連接器", + "noMatches": "沒有連接器符合你的搜尋。", + "noCustom": "新增自訂連接器即可接入你自己的伺服器。", + "editNamed": "編輯 {{name}}", + "removeNamed": "移除 {{name}}" + }, + "form": { + "typeAriaLabel": "連接器類型", + "localCommand": "本機指令", + "remoteServer": "遠端伺服器", + "optional": "(選填)", + "name": "名稱", + "nameAriaLabel": "名稱", + "namePlaceholder": "例如 Memory server", + "nameImmutable": "名稱是連接器的身分識別,建立後無法修改。", + "description": "說明", + "descriptionAriaLabel": "說明", + "descriptionPlaceholder": "這個連接器提供什麼", + "command": "指令", + "commandAriaLabel": "指令", + "customCommandAriaLabel": "自訂指令", + "commandOptions": { + "npx": "npx — Node 套件", + "uvx": "uvx — Python (uv)", + "node": "node — 指令碼檔案", + "python3": "python3 — 指令碼檔案", + "docker": "docker — 容器", + "other": "其他…" + }, + "args": "參數", + "argsAriaLabel": "參數", + "argsHint": "以空格或換行分隔。", + "env": "環境變數", + "envAriaLabel": "環境變數", + "envHint": "每行一條 KEY=VALUE。", + "envHintEdit": "每行一條 KEY=VALUE。留空則保留目前的值。", + "url": "伺服器 URL", + "urlAriaLabel": "伺服器 URL", + "transport": "傳輸方式", + "transportAriaLabel": "傳輸方式", + "headers": "標頭", + "headersAriaLabel": "標頭", + "headersHint": "每行一條 Name: Value(不是 JSON)。", + "headersHintEdit": "每行一條 Name: Value(不是 JSON)。留空則保留目前的值。", + "trustAriaLabel": "我信任這個連接器", + "trust": "我信任這個連接器。只加入來自你信任的開發者的連接器。", + "saveFailed": "儲存連接器失敗。", + "saving": "儲存中…", + "adding": "新增中…", + "saveChanges": "儲存變更", + "addConnector": "新增連接器" + } + }, + "compute": { + "banner": "連接執行重型計算的地方 —— 透過 SSH 使用你自己的伺服器。", + "list": { + "title": "SSH 主機", + "description": "來自你 SSH 主機清單的伺服器、叢集或工作提交節點", + "add": "加入 SSH 主機", + "removed": "已移除 {{name}}。", + "loadError": "無法載入主機:{{message}}", + "loading": "正在載入主機…", + "empty": "還沒有 SSH 主機。加入一台,讓 Open Science 在你的伺服器上執行計算。" + }, + "status": { + "connected": "已連接", + "failed": "探測失敗", + "notProbed": "未探測" + }, + "probed": { + "never": "尚未探測", + "now": "剛剛探測", + "minute": "{{count}} 分鐘前探測", + "hour": "{{count}} 小時前探測", + "day": "{{count}} 天前探測", + "week": "{{count}} 週前探測", + "month": "{{count}} 個月前探測", + "year": "{{count}} 年前探測" + }, + "card": { + "browse": "瀏覽 {{name}} 上的檔案", + "browseDisabled": "需要先探測到主機可連接,才能瀏覽檔案", + "browseTooltip": "瀏覽檔案", + "browseDisabledTooltip": "先探測主機才能瀏覽", + "remove": "移除 {{name}}", + "removeTooltip": "移除主機" + }, + "add": { + "intro": "從 ~/.ssh/config 中挑一個主機別名,或者直接輸入。Open Science 會用你現有的 SSH 金鑰把它作為計算提供方 —— 不會複製任何憑證。", + "aliasFromConfig": "來自 ~/.ssh/config", + "aliasSelectAriaLabel": "從 ~/.ssh/config 中挑選主機", + "aliasSelectEmpty": "~/.ssh/config 中沒有主機", + "aliasSelectPlaceholder": "挑選一台主機…", + "aliasTypedLabel": "或輸入主機別名", + "aliasTypedPlaceholder": "例如 biowulf、lab-gpu、coder.myworkspace", + "detailsLabel": "有什麼想讓 Open Science 知道的嗎?(選填)", + "detailsCount": "{{used}} / {{limit}} 字元", + "detailsPlaceholder": "這裡的工作怎麼跑 —— sbatch、qsub,還是直接 bash?可以用 pip/conda 安裝嗎,新環境該放在哪裡?需要指定哪個分區、帳戶或模組?", + "advanced": "進階(覆寫 ~/.ssh/config)", + "advancedHint": "預設情況下,Open Science 會對 ~/.ssh/config 裡的別名執行 ssh -G 來解析連線資訊。只有需要覆寫時才填這些。", + "user": "使用者", + "userHint": "留空則使用 ~/.ssh/config 中的 User。", + "port": "通訊埠", + "portHint": "留空則使用 22 或 ~/.ssh/config 中的 Port。", + "identity": "身分金鑰檔案", + "identityHint": "留空則使用 ssh-agent 或 ~/.ssh/config 中的 IdentityFile。", + "submit": "加入", + "submitting": "正在加入主機…", + "failed": "無法加入主機。" + }, + "detail": { + "missing": "這台主機已不存在。", + "loading": "正在載入主機…", + "probe": "探測", + "probing": "正在探測…", + "retryProbe": "重新探測", + "probeFailedUnexpectedly": "探測意外失敗。", + "remove": "移除", + "resources": "資源", + "cpus": "CPU", + "ram": "GB 記憶體", + "scheduler": "排程器", + "details": "詳細資訊", + "detailsDescription": "關於這個提供方的自由格式筆記。Open Science 會讀取它,並在了解更多後補充內容。", + "detailsAriaLabel": "詳細資訊文件", + "detailsCount": "{{used}} / {{limit}} 字元", + "detailsTooLong": "詳細資訊不能超過 {{limit}} 個字元。", + "detailsSaveFailed": "儲存詳細資訊失敗。", + "detailsSkeleton": "(由探測自動產生)", + "detailsEmpty": "還沒有筆記。點擊「編輯」為這個提供方加入詳細資訊。", + "showMore": "展開", + "showLess": "收合", + "scratch": "暫存目錄", + "scratchDescription": "遠端工作的工作目錄。已釘選的路徑不會被重新探測覆寫。", + "scratchAriaLabel": "暫存目錄路徑", + "scratchPlaceholder": "/scratch/username", + "scratchSaveFailed": "設定暫存目錄失敗。", + "scratchPinned": "已釘選", + "scratchEmpty": "未設定。下次探測時會從 $SCRATCH 更新。", + "concurrency": "並行工作上限", + "concurrencyDescription": "這台主機上同時執行的工作數上限(1–500)。尚未強制生效。", + "concurrencyAriaLabel": "並行工作上限", + "concurrencyInvalid": "必須是 1 到 500 之間的整數。", + "concurrencySaveFailed": "設定並行上限失敗。", + "concurrencyDefault": "{{value}}(預設)", + "edit": "編輯", + "save": "儲存", + "saving": "正在儲存…" + }, + "approval": { + "title": "允許執行遠端指令?", + "description": "遠端指令以你在該主機上的帳戶身分執行,不在沙箱中。只有信任這條指令時才核准。", + "host": "主機", + "intent": "意圖", + "command": "指令", + "inputs": "輸入", + "showFull": "顯示完整指令", + "showLess": "收合", + "deny": "拒絕", + "once": "僅此一次", + "conversation": "此工作階段", + "project": "本專案", + "always": "一律允許", + "broadScopeSubject": "{{host}} 上的遠端命令" + } + }, + "fileBrowser": { + "ariaLabel": "遠端檔案瀏覽器", + "host": "主機", + "close": "關閉檔案瀏覽器", + "back": "返回", + "up": "向上一層", + "goTo": "前往", + "goToLocations": "快速前往位置", + "scratch": "暫存目錄", + "home": "主目錄", + "pinCurrent": "釘選目前資料夾", + "bookmarks": "書籤", + "removeBookmark": "移除書籤 {{path}}", + "addressAriaLabel": "目前目錄路徑", + "refresh": "重新整理目錄清單", + "loading": "正在載入", + "contents": "目錄內容", + "columnName": "名稱", + "columnSize": "大小", + "columnModified": "修改時間", + "empty": "空目錄", + "truncated": "僅顯示前 5,000 個項目。進入子目錄可查看更多。", + "errorTitle": "無法開啟此路徑。", + "retry": "重試", + "goHome": "前往主目錄", + "invalidPath": "路徑必須是絕對路徑,且不能包含控制字元。", + "unknownError": "未知錯誤", + "details": "詳細資訊", + "closeDetail": "關閉詳細資訊面板", + "metaSize": "大小", + "metaModified": "修改時間", + "metaType": "類型", + "noPreview": "無預覽 · {{size}}", + "download": "下載", + "downloading": "正在下載…", + "downloadAriaLabel": "將檔案下載到系統下載資料夾", + "downloadFailed": "下載失敗", + "savedToDownloads": "已儲存到下載資料夾:{{name}}", + "showInFinder": "在 Finder 中顯示", + "addToProject": "加入專案", + "addToProjectTitle": "即將推出 —— 遠端檔案匯入到專案產物尚未提供", + "addToProjectAriaLabel": "將檔案作為產物加入目前專案(即將推出)", + "copyPath": "複製路徑", + "copied": "已複製!", + "copyAriaLabel": "複製遠端絕對路徑到剪貼簿", + "age": { + "second": "{{count}} 秒", + "minute": "{{count}} 分", + "hour": "{{count}} 小時", + "day": "{{count}} 天" + }, + "type": { + "file": "檔案", + "text": "文字", + "image": "圖片", + "archive": "壓縮檔", + "binary": "二進位", + "library": "程式庫", + "log": "日誌", + "rData": "R 資料" + } + } +} diff --git a/src/renderer/src/locales/zh-Hant/workspace.json b/src/renderer/src/locales/zh-Hant/workspace.json new file mode 100644 index 000000000..f787cf798 --- /dev/null +++ b/src/renderer/src/locales/zh-Hant/workspace.json @@ -0,0 +1,426 @@ +{ + "composer": { + "placeholder": "輸入任何內容——/ 呼叫技能,@ 引用檔案,{{shortcut}} 搜尋", + "ariaLabel": "輸入任何內容", + "addAttachmentOrReview": "新增附件或請求審閱", + "attachFiles": "附加檔案", + "attachmentLimits": "支援任意檔案類型 · 每個檔案最大 {{size}}。大型檔案以連結形式附加,不嵌入內容。", + "requestReview": "請求審閱", + "cancelRun": "取消執行", + "sendMessage": "傳送訊息", + "dropFiles": "拖放檔案以附加", + "removeAttachment": "移除附件 {{name}}", + "cancelAttachment": "取消附件 {{name}}", + "removeFailedAttachment": "移除失敗的附件 {{name}}", + "uploadingFile": "正在上傳 {{name}}", + "notebook": "Notebook", + "openNotebook": "開啟 Notebook", + "compacting": "正在壓縮對話以符合上下文限制……" + }, + "previews": { + "fileMissing": "此檔案已無法使用 — 可能已被移動或刪除。", + "fileOutsideStorage": "此檔案不在目前的儲存位置 — 它可能屬於你已遷離的資料資料夾。", + "missingTag": "缺失", + "unavailableTitle": "無法預覽", + "fileUnavailableTitle": "檔案無法使用", + "unsupportedMessage": "此檔案類型不支援預覽", + "renderingPreview": "正在算繪預覽", + "retry": "重試", + "loadingCode": "正在準備程式碼檔案", + "loadingCsv": "正在準備資料", + "loadingFasta": "正在準備序列", + "loadingHtml": "正在準備文件", + "loadingImage": "正在準備圖片", + "loadingJson": "正在準備資料", + "loadingMarkdown": "正在準備文件", + "loadingMolecule": "正在準備結構", + "loadingPdb": "正在準備結構", + "loadingPdf": "正在準備文件", + "loadingPresentation": "正在準備簡報", + "loadingSpreadsheet": "正在準備試算表", + "loadingText": "正在準備文字檔案", + "loadingTiff": "正在準備圖片", + "loadingUnknown": "正在準備預覽", + "loadingWord": "正在準備文件", + "errorCsv": "無法讀取 CSV 以進行預覽", + "errorFasta": "無法讀取 FASTA 以進行預覽", + "errorHtml": "無法讀取 HTML 以進行預覽", + "errorImage": "無法載入圖片以進行預覽", + "errorJson": "無法讀取 JSON 以進行預覽", + "errorMarkdown": "無法讀取 Markdown 以進行預覽", + "errorMolecule": "無法讀取結構檔案以進行預覽", + "errorPdb": "無法讀取 PDB 以進行預覽", + "errorPdf": "無法算繪此 PDF 以進行預覽", + "errorText": "無法讀取檔案以進行預覽", + "errorOffice": "無法算繪此 Office 檔案以進行預覽", + "previewOf": "{{name}} 的預覽", + "columnFallback": "第 {{index}} 欄", + "invalidJson": "無效的 JSON", + "invalidJsonDetail": "無效的 JSON:{{error}}", + "zoomIn": "放大", + "zoomOut": "縮小", + "resetZoom": "重設縮放", + "htmlRender": "算繪", + "htmlSource": "原始碼", + "htmlShowRendered": "顯示算繪後的 HTML", + "htmlShowSource": "顯示 HTML 原始碼", + "pageNumber": "第 {{page}} 頁", + "previousPage": "上一頁", + "previousPreviewPage": "上一預覽頁", + "nextPage": "下一頁", + "nextPreviewPage": "下一預覽頁", + "moleculeViewerBanner": "使用 OpenChemLib 檢視器", + "pdbViewerBanner": "使用 3Dmol.js 檢視器", + "moleculeRenderFailed": "無法算繪結構:{{error}}", + "moleculeRenderError": "無法算繪結構", + "moleculeRendererFailed": "分子算繪器載入失敗", + "moleculePreviewOf": "{{name}} 的結構預覽", + "pdbPreviewOf": "{{name}} 的 3D 預覽", + "pdbAtomCount": "{{count}} 個原子", + "pdbStyleLabel": "樣式:", + "pdbCartoon": "卡通", + "pdbStick": "棍狀", + "pdbSphere": "球狀", + "pdbSurface": "表面", + "pdbLine": "線狀", + "pdbCartoonUnavailable": "卡通樣式需要蛋白質或核酸主鏈", + "pdbViewerFailed": "檢視器失敗", + "pdbViewerInitFailed": "無法初始化 PDB 檢視器:{{error}}", + "pdbControlsHint": "拖曳旋轉 · 滾動縮放 · Shift + 拖曳平移", + "officeChecking": "正在檢查 Office 檔案", + "officeStarting": "正在啟動 Office 預覽", + "officeTooLargeTitle": "檔案過大,無法預覽", + "officeTooLargeMessage": "此檔案大於 40 MB。請下載後檢視。", + "officeInvalidPackage": "此 Office 檔案已損毀或不受支援。請下載後檢視。", + "officeResourceLimit": "此 Office 檔案超出安全預覽限制。請下載後檢視。", + "figureOutput": "圖表輸出", + "csvStats": "{{rows}} 列 · {{columns}} 欄", + "csvStatsTruncated": "{{rows}}+ 列 · {{columns}} 欄", + "csvHiddenColumns": "+{{count}} 更多" + }, + "mention": { + "skillSuggestions": "技能建議", + "artifactSuggestions": "Artifact 建議", + "noArtifacts": "暫無 Artifacts", + "loadingFiles": "正在載入專案檔案……", + "loadError": "無法載入專案檔案", + "sectionUploads": "使用者上傳", + "sectionArtifacts": "其他 Artifacts", + "sourceFeatured": "精選", + "sourceImported": "已匯入", + "sourcePersonal": "個人", + "hintNavigate": "瀏覽", + "hintSelect": "選擇", + "hintClose": "關閉" + }, + "modelPicker": { + "noModelAvailable": "無可用模型", + "noModelAvailableAriaLabel": "無可用模型 — 開啟設定", + "selectModel": "選擇模型", + "noCompatibleModel": "無相容模型", + "openSettings": "開啟設定" + }, + "uploadStatus": { + "queued": "排隊中", + "cancelling": "正在取消……", + "failed": "上傳失敗" + }, + "preview": { + "expand": "展開預覽面板", + "collapse": "收起預覽面板", + "resizeRightPanel": "調整右側面板大小", + "openPreviews": "已開啟的預覽", + "closePreview": "關閉 {{title}} 的預覽", + "closePreviewTooltip": "關閉預覽", + "openFullScreen": "全螢幕預覽", + "openFullScreenOf": "全螢幕預覽 {{title}}", + "localBadge": "這台電腦" + }, + "reportError": { + "button": "回報錯誤", + "ariaLabel": "回報此錯誤" + }, + "permission": { + "scopeOnce": "僅一次", + "scopeOnceSubtitle": "僅此次呼叫", + "scopeSession": "此工作階段", + "scopeSessionSubtitle": "重新啟動後對此工作階段仍然有效", + "allow": "允許", + "deny": "拒絕", + "allowOnce": "僅允許一次", + "allowAlways": "始終允許", + "rejectOnce": "僅拒絕一次", + "rejectAlways": "始終拒絕", + "authorizationScope": "授權範圍", + "impactInfo": "權限影響說明", + "permissionInfo": "權限說明", + "chooseScope": "選擇授權範圍", + "scopeDescriptionOnce": "授權僅適用於本次呼叫。", + "scopeDescriptionSession": "重新啟動後核准仍然附加於此工作階段。", + "scopeDescriptionSessionNotebook": "核准涵蓋此工作階段中後續的 {{runtime}} 呼叫。", + "scopeDescriptionNone": "此請求沒有可用的授權範圍。", + "actionTitleNotebook": "執行 Notebook 儲存格", + "actionTitleArtifact": "寫入 Artifact 檔案", + "actionTitleMcp": "外部服務輸入", + "actionTitleCommand": "執行指令", + "runtimePython": "Python", + "runtimeR": "R", + "runtimeJs": "JavaScript REPL", + "runtimeBash": "Notebook 終端", + "scopeProject": "此專案", + "scopeProjectSubtitle": "適用於此專案中的所有工作階段", + "scopeGlobal": "全域", + "scopeGlobalSubtitle": "適用於所有專案", + "suffixOnce": "一次", + "suffixSession": "用於此工作階段", + "suffixProject": "用於此專案", + "suffixGlobal": "全域套用", + "scopeDescriptionProject": "核准適用於此專案中的相符呼叫。", + "scopeDescriptionGlobal": "核准適用於所有專案中的相符呼叫。" + }, + "reviewer": { + "title": "工作階段審閱器", + "checks": "檢查項目", + "noChecks": "未記錄任何檢查項目。", + "noReview": "此工作階段目前沒有可用的審查結果。", + "statusPass": "通過", + "statusWarn": "警告", + "statusFail": "失敗", + "refLabel": "參照:", + "log": "審閱器記錄", + "collapseLog": "收起審閱器記錄", + "expandLog": "展開審閱器記錄", + "input": "輸入", + "output": "輸出", + "exitCode": "結束碼 {{code}}", + "staleNotice": "此輪次在審閱執行後發生了變更(例如某個 Artifact 被編輯)。以下結果可能已過期——請重新執行審閱以更新。" + }, + "errorReport": { + "title": "回報此錯誤", + "description": "此回報將公開發佈在 GitHub 上。請在分享前編輯以下錯誤文字,移除任何敏感資訊。執行記錄儲存在本裝置上,不會自動附加。", + "errorDetails": "錯誤詳情", + "alsoIncluded": "同時包含", + "reportEnvironment": "報告環境資訊", + "githubPrefill": "GitHub Issue 預填內容", + "consent": "我已審閱以上詳情,並同意將其發佈至 GitHub 公開 Issue,遵守 GitHub 的", + "privacyStatement": "隱私聲明", + "revealLog": "顯示記錄檔", + "copyDetails": "複製詳情", + "copied": "已複製", + "openIssue": "提交 GitHub Issue" + }, + "notebook": { + "title": "工作階段 Notebook", + "agents_other": "{{count}} 個 Agent", + "cells_other": "{{count}} 個儲存格", + "repl": "{{count}} repl", + "shell": "{{count}} shell", + "loading": "正在載入 Notebook……", + "loadFailed": "載入 Notebook 失敗。", + "noRecords": "此工作階段沒有執行記錄。", + "exporting": "正在匯出……", + "exportIpynb": ".ipynb", + "exportAll": "全部 ({{count}})", + "downloadAs": "將 {{kernel}} 儲存格下載為 .ipynb", + "downloadAsAriaLabel": "下載 {{kernel}} 為 .ipynb", + "downloadAsGeneric": "下載為 .ipynb", + "errorAtLine": "錯誤(第 {{line}} 行)", + "error": "錯誤", + "downloadAllAriaLabel": "依核心分別下載 Notebook ({{count}})", + "downloadAllTooltip": "將每個資料核心的 .ipynb 分別儲存到所選目錄(共 {{count}} 個檔案)。", + "noDataKernel": "請先執行 Python 或 R 儲存格以啟用 .ipynb 匯出。", + "controlFallback": "(控制面板索引標籤將回退至最近的資料核心)", + "savedNotebooks": "已將 {{count}} 個 Notebook 儲存至 {{directory}}" + }, + "files": { + "filterAriaLabel": "篩選專案檔案", + "artifacts": "Artifacts", + "localSection": "這台電腦", + "addLocalFolder": "新增本機資料夾……", + "soon": "即將推出", + "remote": "遠端", + "hostUnreachable": "主機無法連線", + "addHost": "新增 SSH 主機……", + "retryIndexing": "重試索引專案檔案", + "uploads": "我的上傳", + "allArtifacts": "全部 Artifacts", + "sessionFallback": "工作階段 {{id}}", + "noMore": "沒有更多", + "loading": "正在載入……", + "loadMore": "載入更多", + "loadMoreSessions": "載入更多工作階段", + "fileCount_other": "{{count}} 個檔案", + "indexIncomplete": "部分檔案尚未完成索引。", + "retrying": "正在重試……", + "retry": "重試", + "noFiles": "暫無檔案", + "noSearchMatches": "沒有檔案符合「{{query}}」", + "generatedFiles": "產生的檔案", + "loadMoreFrom": "載入 {{title}} 的更多檔案", + "loadMoreUploads": "載入更多已上傳檔案" + }, + "sidebar": { + "allProjects": "所有專案", + "sessions": "工作階段", + "new": "新建", + "files": "檔案", + "settings": "設定", + "sessionActions": "工作階段操作", + "sessionActionsFor": "開啟 {{title}} 的操作", + "statusIdle": "閒置", + "statusRunning": "執行中", + "statusWaiting": "等待權限", + "statusError": "錯誤", + "sessionStatus": "工作階段狀態:{{status}}", + "pin": "釘選", + "unpin": "取消釘選", + "rename": "重新命名……", + "viewNotebook": "查看 Notebook", + "delete": "刪除", + "pinned": "已釘選", + "active": "活躍", + "closeNavigation": "關閉導覽" + }, + "message": { + "generated": "已產生 · {{count}}", + "expandFiles": "展開產生的檔案", + "collapseFiles": "收起產生的檔案", + "moreFiles": "+{{count}} 個", + "showLess": "收起", + "editPlaceholder": "編輯訊息", + "editAriaLabel": "編輯訊息", + "cancel": "取消", + "send": "傳送", + "copy": "複製訊息", + "copied": "已複製", + "edit": "編輯訊息", + "previousRevision": "上一個訊息版本", + "nextRevision": "下一個訊息版本", + "revisionCounter": "訊息版本", + "previewAttachment": "預覽上傳的附件 {{name}}", + "previewGeneratedFile": "預覽產生的檔案 {{name}}", + "openSkill": "開啟技能 {{name}}", + "previewArtifact": "預覽 {{name}}", + "conversation": "對話", + "sent": "已傳送", + "completed": "已完成", + "failed": "已失敗", + "elapsed": "耗時 {{duration}}", + "elapsedLabel": "執行耗時", + "usage": "用量", + "usageAvailable": "此回覆的 token 用量", + "usageUnavailable": "此回覆沒有 token 用量資料", + "usageInput": "輸入", + "usageOutput": "輸出", + "usageCache": "快取", + "usageCacheRead": "快取讀取", + "usageCacheWrite": "快取寫入", + "usageBreakdown": "{{entries}};共 {{total}} 個 token", + "usageBreakdownUnavailable": "沒有 token 用量明細", + "usageTotal": "總計", + "usageTurns_other": "{{count}} 輪", + "usageFramework": "Agent 框架:{{name}}", + "usageProvider": "模型供應商:{{name}}", + "usageProviderWithModel": "模型供應商:{{name}};模型:{{model}}", + "usageAgentDetail": "Agent:{{name}}", + "usageModelDetail": "模型:{{model}}" + }, + "deleteSession": { + "title": "刪除工作階段?", + "description": "將永久刪除「{{title}}」。此工作階段中建立的 Artifacts 將保留在專案中。此操作無法復原。", + "delete": "刪除" + }, + "renameSession": { + "title": "重新命名工作階段", + "description": "更新工作階段清單中顯示的名稱。", + "nameAriaLabel": "工作階段名稱", + "rename": "重新命名" + }, + "conversation": { + "newTitle": "新對話" + }, + "interrupted": { + "resume": "恢復", + "resuming": "正在恢復……", + "resumeAriaLabel": "恢復工作階段", + "defaultMessage": "此工作階段已中斷。" + }, + "agentControls": { + "ariaLabel": "Agent 控制:{{profile}},自動審閱{{autoReview}}", + "permissionMode": "權限模式", + "permissionDescription": "Agent 在未經請求的情況下可執行的操作範圍。", + "profileAsk": "請求批准", + "profileAskShort": "詢問", + "profileAskDescription": "在編輯檔案、執行指令、網路請求和 MCP 工具前詢問。", + "profileAuto": "自動批准編輯", + "profileAutoShort": "自動", + "profileAutoDescription": "自動批准工作區內的檔案編輯。指令、網路請求和 MCP 仍需詢問。", + "profileFull": "完全存取", + "profileFullShort": "完全存取", + "profileFullDescription": "無需提示直接執行所有操作,包括指令和網路請求。", + "profileFullUnavailable": "目前的 Agent 不支援原生繞過模式。", + "conservativeNote": "此 Agent 沒有原生自動模式。Open Science 僅自動批准工作區內的檔案編輯——指令、網路請求和 MCP 工具仍需詢問。", + "autoReview": "自動審閱", + "autoReviewDescription": "審閱 Agent 會在每次變更落地前進行檢查。", + "grantsTitle": "本次工作階段已允許", + "clearAll": "全部清除", + "clearAllAriaLabel": "清除本次工作階段的所有授權", + "revokeGrantAriaLabel": "撤銷 {{label}} 的工作階段授權", + "grantsBadgeAriaLabel": "本次工作階段已允許 {{count}} 項", + "specialist": "專家", + "specialistDescription": "為此對話綁定一位個人專家。", + "specialistNone": "無", + "specialistUnavailable": "不可用", + "specialistCreateNew": "新建……", + "ssh": "SSH", + "compute": "運算", + "computeDescription": "在遠端 SSH 主機上執行任務,或管理主機。", + "noHosts": "未註冊 SSH 主機", + "loading": "載入中……", + "manageCompute": "管理運算資源……", + "fullAccessDialogTitle": "啟用完全存取?", + "fullAccessDialogDescription": "Agent 可以無需詢問直接執行指令、修改檔案、執行 Notebook 程式碼和發起網路請求。身分驗證失敗和執行錯誤仍會中止執行。", + "enable": "啟用" + }, + "editConfirm": { + "title": "在新分支上重新傳送?", + "description_other": "傳送此編輯後的提示會從這裡開始一個新分支。目前緊接其後的 {{count}} 個輪次仍可透過訊息版本控制項存取。", + "branch": "建立分支並重新傳送" + }, + "envStatus": { + "failed": "環境更新失敗", + "updating": "正在更新 Notebook 環境…… {{percent}}%", + "retry": "重試" + }, + "context": { + "estimatedAriaLabel": "預估內文:{{label}}", + "usedAriaLabel": "已用內文:{{label}}", + "window": "內文視窗", + "compact": "壓縮", + "compacting": "正在壓縮…", + "compactAriaLabel": "壓縮內文", + "compactingAriaLabel": "正在壓縮內文" + }, + "notebookPreview": { + "sourceUser": "您", + "placeholder": "在此核心執行程式碼…", + "rRestartBanner": "已安裝的 R 套件需要重新啟動核心才能載入。", + "restartR": "重新啟動 R 核心", + "restarting": "正在重新啟動…", + "pythonKernelLabel": "Python 核心 · 與 Agent 共用", + "statusRunning": "執行中", + "statusIdle": "閒置", + "rPreparing": "R(準備中…)" + }, + "tool": { + "outputTruncated": "輸出已截斷", + "loadingPreview": "正在載入預覽…", + "searchQuery": "查詢", + "webSearch": "Web 搜尋", + "searchResult": "{{count}} 筆結果" + }, + "agentLoading": { + "responding": "Agent 正在回應", + "slow": "· 比平常花費更長時間" + } +} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 58a1d6a93..6ad36b17d 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -4,6 +4,8 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' import { installStreamdown } from '@/components/streamdown/install-streamdown' +import { initI18n } from '@/i18n' +import { applyHtmlLang, resolveInitialLocale } from '@/lib/locale-preference' import { applyTheme, resolveInitialTheme } from '@/lib/theme' import { installRendererFailureDiagnostics } from './renderer-diagnostics' import { useNavigationStore } from '@/stores/navigation-store' @@ -26,6 +28,12 @@ installRendererFailureDiagnostics({ // Apply the saved theme to before the first paint so dark mode doesn't flash light on startup. applyTheme(resolveInitialTheme()) +// Same reason, for language: initialize i18next synchronously before React mounts so the first paint +// is already translated instead of rendering English and then swapping. +const initialLocale = resolveInitialLocale() +initI18n(initialLocale) +applyHtmlLang(initialLocale) + // Install before React renders so Streamdown hooks work on first interaction. installStreamdown() diff --git a/src/renderer/src/pages/home/DeleteProjectDialog.tsx b/src/renderer/src/pages/home/DeleteProjectDialog.tsx index a9eee8893..655e63845 100644 --- a/src/renderer/src/pages/home/DeleteProjectDialog.tsx +++ b/src/renderer/src/pages/home/DeleteProjectDialog.tsx @@ -1,5 +1,6 @@ import { X } from 'lucide-react' import { AlertDialog } from 'radix-ui' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { @@ -39,6 +40,7 @@ const DeleteProjectDialog = ({ onCancel, onConfirmDelete }: DeleteProjectDialogProps): React.JSX.Element => { + const { t } = useTranslation(['home', 'components', 'common']) const dialogProject = useRetainedDialogValue(project) const dialogSessionCount = useRetainedDialogValue(project ? sessionCount : undefined) ?? sessionCount @@ -59,25 +61,28 @@ const DeleteProjectDialog = ({
- Delete project? + {t('home:deleteDialog.title')} - This will permanently delete "{dialogProject?.name}" + {/* Whole sentences per branch rather than a spliced-in clause: the session count + sits mid-sentence in English but not in every language. */} {dialogHasCompleteSessionCatalog ? dialogSessionCount > 0 - ? ` and its ${dialogSessionCount} ${dialogSessionCount === 1 ? 'session' : 'sessions'}` - : '' - : ' and all of its saved conversations, including any that could not be loaded during recovery'} - . Generated artifacts and uploaded files stored by Open Science will also be - deleted. Files in the project's working folder are not deleted. This action - cannot be undone. + ? t('home:deleteDialog.descriptionWithSessions', { + name: dialogProject?.name, + count: dialogSessionCount + }) + : t('home:deleteDialog.description', { name: dialogProject?.name }) + : t('home:deleteDialog.descriptionWithIncompleteCatalog', { + name: dialogProject?.name + })}
{/* Async confirmation owns dialog closure so a failed deletion remains visible. */} @@ -103,7 +108,7 @@ const DeleteProjectDialog = ({ disabled={!canDelete || isDeleting} onClick={onConfirmDelete} > - {isDeleting ? 'Deleting…' : 'Delete'} + {isDeleting ? t('common:actions.deleting') : t('common:actions.delete')}
diff --git a/src/renderer/src/pages/home/HomePage.persistence.test.tsx b/src/renderer/src/pages/home/HomePage.persistence.test.tsx index 03d7d7b90..5a657e2ea 100644 --- a/src/renderer/src/pages/home/HomePage.persistence.test.tsx +++ b/src/renderer/src/pages/home/HomePage.persistence.test.tsx @@ -66,7 +66,31 @@ vi.mock('radix-ui', () => ({ - ) + ), + Group: ({ children }: { children: ReactNode }) => <>{children}, + Label: ({ children }: { children: ReactNode }) => <>{children}, + Sub: ({ children }: { children: ReactNode }) => <>{children}, + SubContent: ({ children }: { children: ReactNode }) => <>{children}, + SubTrigger: ({ children }: { children: ReactNode }) => <>{children} + }, + // The header's language picker is built on the Select primitive. This suite is about persistence + // recovery, so the picker only has to mount — passthroughs keep it out of the way. + Select: { + Root: ({ children }: { children: ReactNode }) => <>{children}, + Group: ({ children }: { children: ReactNode }) => <>{children}, + Value: ({ children }: { children: ReactNode }) => <>{children}, + Trigger: ({ children }: { children: ReactNode }) => , + Icon: ({ children }: { children: ReactNode }) => <>{children}, + Portal: ({ children }: { children: ReactNode }) => <>{children}, + Content: ({ children }: { children: ReactNode }) => <>{children}, + Viewport: ({ children }: { children: ReactNode }) => <>{children}, + Label: ({ children }: { children: ReactNode }) => <>{children}, + Item: ({ children }: { children: ReactNode }) => <>{children}, + ItemText: ({ children }: { children: ReactNode }) => <>{children}, + ItemIndicator: ({ children }: { children: ReactNode }) => <>{children}, + Separator: () => null, + ScrollUpButton: () => null, + ScrollDownButton: () => null } })) diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index 18488b993..e70f5f42f 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -10,8 +10,9 @@ import { } from 'lucide-react' import { DropdownMenu } from 'radix-ui' import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' -import { formatRelativeTime } from '@/lib/format-relative-time' +import { relativeTimeParts } from '@/lib/format-relative-time' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { useNavigationStore } from '@/stores/navigation-store' @@ -20,6 +21,7 @@ import { useSessionStore } from '@/stores/session-store' import { useProjectStore } from '@/stores/project-store' import { useSettingsStore } from '@/stores/settings-store' import { GitHubStarBadge } from '@/components/GitHubStarBadge' +import { LanguagePreferenceMenu } from '@/components/LanguageControls' import { ThemePreferenceMenu } from '@/components/ThemeControls' import { UpdateCapsule } from '@/components/UpdateCapsule' import { APP } from '../../../../shared/app-config' @@ -83,6 +85,7 @@ const HomePage = ({ canDeleteProjects, hasCompleteSessionCatalog }: HomePageProps): React.JSX.Element => { + const { t } = useTranslation('home') const projects = useProjectStore((state) => state.projects) const loadError = useProjectStore((state) => state.loadError) const createProject = useProjectStore((state) => state.createProject) @@ -207,7 +210,7 @@ const HomePage = ({ if (isCreate) openProject(project.id, 'user') }) .catch((error: unknown) => { - setFormError(error instanceof Error ? error.message : 'Could not save project.') + setFormError(error instanceof Error ? error.message : t('form.saveFailed')) }) .finally(() => { setIsSubmitting(false) @@ -244,12 +247,16 @@ const HomePage = ({ }) } - const formTitle = formState?.mode === 'edit' ? 'Edit project' : 'New project' - const formDescription = - formState?.mode === 'edit' - ? 'Rename this project or update its description.' - : 'Group related sessions under a project. You can rename it later.' - const formSubmitLabel = formState?.mode === 'edit' ? 'Save changes' : 'Create project' + const isEditForm = formState?.mode === 'edit' + const formTitle = isEditForm ? t('form.editTitle') : t('form.createTitle') + const formDescription = isEditForm ? t('form.editDescription') : t('form.createDescription') + const formSubmitLabel = isEditForm ? t('form.editSubmit') : t('form.createSubmit') + + // The bucket comes from a pure helper; the wording is per-locale ("3d" vs "3 天前"). + const relativeTime = (timestamp: number): string => { + const { unit, count } = relativeTimeParts(timestamp) + return unit === 'now' ? t('relativeTime.now') : t(`relativeTime.${unit}`, { count }) + } return (
@@ -264,7 +271,7 @@ const HomePage = ({ > Open Science -
Beta
+
{t('header.beta')}
@@ -273,24 +280,29 @@ const HomePage = ({ type="button" onClick={() => openSettingsToPanel(environmentRepairPanel)} className="inline-flex h-8 items-center gap-1.5 rounded-md border border-danger-000/35 bg-danger-900 px-2.5 text-xs font-medium text-danger-000 transition-colors duration-150 ease-out hover:border-danger-000/55 hover:bg-danger-900/80" - aria-label="Open environment repair" + aria-label={t('header.environmentRepair')} >
-
+

{loadError ? (
- Could not load projects: {loadError} + {t('projects.loadError', { error: loadError })}
) : projectSummaries.length === 0 ? (
- No projects yet. Create one to get started. + {t('projects.empty')}
) : (
@@ -342,31 +354,31 @@ const HomePage = ({ {project.name} {project.isExample ? ( - Example + {t('projects.exampleBadge')} ) : null} {hasCompleteSessionCatalog - ? `${sessionCount} ${sessionCount === 1 ? 'session' : 'sessions'}` - : 'Session count unavailable'} + ? t('projects.sessionCount', { count: sessionCount }) + : t('projects.sessionCountUnavailable')} - {formatRelativeTime(lastActivityAt)} + {relativeTime(lastActivityAt)} openEditDialog(project)} > @@ -396,14 +408,14 @@ const HomePage = ({ )}
-
+

{recentSessions.length === 0 ? (
- Sessions you start will appear here. + {t('recent.empty')}
) : (
@@ -433,7 +445,7 @@ const HomePage = ({ ) : null} - {formatRelativeTime(session.updatedAt)} + {relativeTime(session.updatedAt)} ) diff --git a/src/renderer/src/pages/home/ProjectFormDialog.tsx b/src/renderer/src/pages/home/ProjectFormDialog.tsx index 5f2ac47d1..74eb082b0 100644 --- a/src/renderer/src/pages/home/ProjectFormDialog.tsx +++ b/src/renderer/src/pages/home/ProjectFormDialog.tsx @@ -1,5 +1,6 @@ import { X } from 'lucide-react' import { Dialog } from 'radix-ui' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { @@ -47,6 +48,7 @@ const ProjectFormDialog = ({ onCancel, onConfirm }: ProjectFormDialogProps): React.JSX.Element => { + const { t } = useTranslation(['home', 'components', 'common']) const dialogTitle = useRetainedDialogValue(open ? title : undefined) ?? title const dialogDescription = useRetainedDialogValue(open ? description : undefined) ?? description const dialogSubmitLabel = useRetainedDialogValue(open ? submitLabel : undefined) ?? submitLabel @@ -78,7 +80,7 @@ const ProjectFormDialog = ({ type="button" variant="ghost" size="icon-sm" - aria-label="Close" + aria-label={t('components:dialog.close')} className={dialogCloseButtonClassName} onClick={onCancel} > @@ -91,13 +93,13 @@ const ProjectFormDialog = ({ className="text-xs font-medium text-muted-foreground" htmlFor="project-form-name" > - Name + {t('home:form.nameLabel')} onNameChange(event.target.value)} - placeholder="e.g. Reproduction of published research" + placeholder={t('home:form.namePlaceholder')} autoFocus className={dialogInputClassName} /> @@ -107,13 +109,16 @@ const ProjectFormDialog = ({ className="text-xs font-medium text-muted-foreground" htmlFor="project-form-description" > - Description (optional) + {t('home:form.descriptionLabel')}{' '} + + {t('home:form.descriptionOptional')} +